Commit 3d0157a52ebfc566a9df373c28c81f72dbf1a754
1 parent
3c0896d2
add logoff
Showing
9 changed files
with
162 additions
and
134 deletions
Show diff stats
lib/syspro.rb
@@ -5,6 +5,9 @@ require "logger" | @@ -5,6 +5,9 @@ require "logger" | ||
5 | require "openssl" | 5 | require "openssl" |
6 | 6 | ||
7 | require "syspro/api_resource" | 7 | require "syspro/api_resource" |
8 | +require "syspro/get_version" | ||
9 | +require "syspro/logoff" | ||
10 | +require "syspro/logon" | ||
8 | require "syspro/syspro_client" | 11 | require "syspro/syspro_client" |
9 | require "syspro/singleton_api_resource" | 12 | require "syspro/singleton_api_resource" |
10 | require "syspro/syspro_object" | 13 | require "syspro/syspro_object" |
@@ -12,10 +15,7 @@ require "syspro/syspro_response" | @@ -12,10 +15,7 @@ require "syspro/syspro_response" | ||
12 | require "syspro/util" | 15 | require "syspro/util" |
13 | require "syspro/version" | 16 | require "syspro/version" |
14 | 17 | ||
15 | -require "syspro/api_operations/get_version" | ||
16 | require "syspro/api_operations/request" | 18 | require "syspro/api_operations/request" |
17 | -require "syspro/api_operations/logon" | ||
18 | - | ||
19 | 19 | ||
20 | module Syspro | 20 | module Syspro |
21 | @api_base = "http://syspro.wildlandlabs.com:90/SYSPROWCFService/Rest" | 21 | @api_base = "http://syspro.wildlandlabs.com:90/SYSPROWCFService/Rest" |
lib/syspro/api_operations/request.rb
1 | module Syspro | 1 | module Syspro |
2 | module ApiOperations | 2 | module ApiOperations |
3 | module Request | 3 | module Request |
4 | - def request(method, url, params = {}, opts = {}) | ||
5 | - client = SysproClient.active_client | 4 | + module ClassMethods |
5 | + def request(method, url, params = {}, opts = {}) | ||
6 | + warn_on_opts_in_params(params) | ||
7 | + | ||
8 | + opts = Util.normalize_opts(opts) | ||
9 | + opts[:client] ||= SysproClient.active_client | ||
6 | 10 | ||
7 | headers = opts.clone | 11 | headers = opts.clone |
12 | + user_id = headers.delete(:user_id) | ||
13 | + client = headers.delete(:client) | ||
14 | + # Assume all remaining opts must be headers | ||
8 | 15 | ||
9 | resp = client.execute_request( | 16 | resp = client.execute_request( |
10 | method, url, | 17 | method, url, |
11 | headers: headers, | 18 | headers: headers, |
19 | + user_id: user_id, | ||
12 | params: params | 20 | params: params |
13 | ) | 21 | ) |
14 | 22 | ||
15 | resp | 23 | resp |
16 | - end | 24 | + end |
17 | 25 | ||
18 | - def warn_on_opts_in_params(params) | 26 | + private |
27 | + | ||
28 | + def warn_on_opts_in_params(params) | ||
19 | Util::OPTS_USER_SPECIFIED.each do |opt| | 29 | Util::OPTS_USER_SPECIFIED.each do |opt| |
20 | if params.key?(opt) | 30 | if params.key?(opt) |
21 | $stderr.puts("WARNING: #{opt} should be in opts instead of params.") | 31 | $stderr.puts("WARNING: #{opt} should be in opts instead of params.") |
22 | end | 32 | end |
23 | end | 33 | end |
34 | + end | ||
35 | + end # ClassMethods | ||
36 | + | ||
37 | + def self.included(base) | ||
38 | + base.extend(ClassMethods) | ||
39 | + end | ||
40 | + | ||
41 | + protected | ||
42 | + | ||
43 | + def request(method, url, params = {}, opts = {}) | ||
44 | + opts = @opts.merge(Util.normalize_opts(opts)) | ||
45 | + self.class.request(method, url, params, opts) | ||
24 | end | 46 | end |
25 | end | 47 | end |
26 | end | 48 | end |
lib/syspro/api_resource.rb
1 | -require_relative "syspro_client" | ||
2 | -require_relative "api_operations/request" | 1 | +require "syspro/syspro_object" |
2 | +require "syspro/api_operations/request" | ||
3 | 3 | ||
4 | module Syspro | 4 | module Syspro |
5 | - class ApiResource < Syspro::SysproClient | 5 | + class ApiResource < SysproObject |
6 | + include Syspro::ApiOperations::Request | ||
6 | 7 | ||
7 | def self.class_name | 8 | def self.class_name |
8 | name.split("::")[-1] | 9 | name.split("::")[-1] |
9 | end | 10 | end |
10 | 11 | ||
11 | def self.resource_url | 12 | def self.resource_url |
12 | - if self == APIResource | 13 | + if self == ApiResource |
13 | raise NotImplementedError, "APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)" | 14 | raise NotImplementedError, "APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)" |
14 | end | 15 | end |
15 | - "/v1/#{CGI.escape(class_name.downcase)}s" | 16 | + "/#{CGI.escape(class_name.downcase)}" |
16 | end | 17 | end |
17 | 18 | ||
18 | def resource_url | 19 | def resource_url |
19 | - unless (id = self["id"]) | ||
20 | - raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", "id") | ||
21 | - end | ||
22 | - "#{self.class.resource_url}/#{CGI.escape(id)}" | 20 | + #unless (id = self["id"]) |
21 | + #raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", "id") | ||
22 | + #end | ||
23 | + #"#{self.class.resource_url}/#{CGI.escape(id)}" | ||
23 | end | 24 | end |
24 | 25 | ||
25 | def refresh | 26 | def refresh |
lib/syspro/api_operations/get_version.rb renamed to lib/syspro/get_version.rb
1 | -require_relative "request" | ||
2 | - | ||
3 | module Syspro | 1 | module Syspro |
4 | - module ApiOperations | ||
5 | - class GetVersion | ||
6 | - include ApiOperations::Request | ||
7 | - | ||
8 | - def get_version | ||
9 | - resp = self.request(:get, resource_url) | ||
10 | - version = VersionObject.new(resp[0].http_body) | ||
11 | - end | ||
12 | - | ||
13 | - def resource_url | ||
14 | - "/GetVersion" | ||
15 | - end | 2 | + class GetVersion < ApiResource |
3 | + def self.get_version | ||
4 | + resp = self.request(:get, resource_url) | ||
5 | + version = VersionObject.new(resp[0].http_body) | ||
6 | + end | ||
16 | 7 | ||
17 | - VersionObject = Struct.new(:version) | 8 | + def resource_url |
9 | + "/GetVersion" | ||
18 | end | 10 | end |
11 | + | ||
12 | + VersionObject = Struct.new(:version) | ||
19 | end | 13 | end |
20 | end | 14 | end |
21 | 15 |
1 | +module Syspro | ||
2 | + class Logoff < ApiResource | ||
3 | + def self.logoff(user_id) | ||
4 | + params = { "UserId" => user_id } | ||
5 | + resp = self.request(:get, resource_url, params) | ||
6 | + | ||
7 | + if resp[0].http_body == "0" | ||
8 | + true | ||
9 | + else | ||
10 | + resp[0].http_body | ||
11 | + end | ||
12 | + end | ||
13 | + | ||
14 | + def resource_url | ||
15 | + "/Logoff" | ||
16 | + end | ||
17 | + end | ||
18 | +end |
lib/syspro/api_operations/logon.rb renamed to lib/syspro/logon.rb
1 | -require_relative "request" | ||
2 | - | ||
3 | module Syspro | 1 | module Syspro |
4 | - module ApiOperations | ||
5 | - class Logon | ||
6 | - include ApiOperations::Request | ||
7 | - | ||
8 | - def logon(username, password, company_id, company_password = nil) | ||
9 | - params = { | ||
10 | - "Operator" => username, | ||
11 | - "OperatorPassword" => password, | ||
12 | - "CompanyId" => company_id, | ||
13 | - "CompanyPassword" => company_password | ||
14 | - } | ||
15 | - resp = self.request(:get, resource_url, params) | ||
16 | - user_id = UserIdObject.new(resp[0].http_body) | ||
17 | - end | ||
18 | - | ||
19 | - def resource_url | ||
20 | - "/Logon" | ||
21 | - end | 2 | + class Logon < ApiResource |
3 | + def self.logon(username, password, company_id, company_password = nil) | ||
4 | + params = { | ||
5 | + "Operator" => username, | ||
6 | + "OperatorPassword" => password, | ||
7 | + "CompanyId" => company_id, | ||
8 | + "CompanyPassword" => company_password | ||
9 | + } | ||
10 | + resp = self.request(:get, resource_url, params) | ||
11 | + user_id = UserIdObject.new(resp[0].http_body) | ||
12 | + end | ||
22 | 13 | ||
23 | - UserIdObject = Struct.new(:guid) | 14 | + def resource_url |
15 | + "/Logon" | ||
24 | end | 16 | end |
17 | + | ||
18 | + UserIdObject = Struct.new(:guid) | ||
25 | end | 19 | end |
26 | end | 20 | end |
27 | 21 |
lib/syspro/singleton_api_resource.rb
@@ -6,7 +6,7 @@ module Syspro | @@ -6,7 +6,7 @@ module Syspro | ||
6 | if self == SingletonAPIResource | 6 | if self == SingletonAPIResource |
7 | raise NotImplementedError, "SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Customer, etc.)" | 7 | raise NotImplementedError, "SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Customer, etc.)" |
8 | end | 8 | end |
9 | - "/v1/#{CGI.escape(class_name.downcase)}" | 9 | + "/#{CGI.escape(class_name.downcase)}" |
10 | end | 10 | end |
11 | 11 | ||
12 | def resource_url | 12 | def resource_url |
lib/syspro/syspro_client.rb
@@ -58,8 +58,27 @@ module Syspro | @@ -58,8 +58,27 @@ module Syspro | ||
58 | end | 58 | end |
59 | end | 59 | end |
60 | 60 | ||
61 | - def execute_request(method, path, api_base: nil, headers: {}, params: {}) | 61 | + # Executes the API call within the given block. Usage looks like: |
62 | + # | ||
63 | + # client = StripeClient.new | ||
64 | + # charge, resp = client.request { Charge.create } | ||
65 | + # | ||
66 | + def request | ||
67 | + @last_response = nil | ||
68 | + old_stripe_client = Thread.current[:stripe_client] | ||
69 | + Thread.current[:stripe_client] = self | ||
70 | + | ||
71 | + begin | ||
72 | + res = yield | ||
73 | + [res, @last_response] | ||
74 | + ensure | ||
75 | + Thread.current[:stripe_client] = old_stripe_client | ||
76 | + end | ||
77 | + end | ||
78 | + | ||
79 | + def execute_request(method, path, user_id: nil, api_base: nil, headers: {}, params: {}) | ||
62 | api_base ||= Syspro.api_base | 80 | api_base ||= Syspro.api_base |
81 | + user_id ||= "" | ||
63 | 82 | ||
64 | params = Util.objects_to_ids(params) | 83 | params = Util.objects_to_ids(params) |
65 | url = api_url(path, api_base) | 84 | url = api_url(path, api_base) |
@@ -87,6 +106,7 @@ module Syspro | @@ -87,6 +106,7 @@ module Syspro | ||
87 | context.body = body | 106 | context.body = body |
88 | context.method = method | 107 | context.method = method |
89 | context.path = path | 108 | context.path = path |
109 | + context.user_id = user_id | ||
90 | context.query_params = query_params ? Util.encode_parameters(query_params) : nil | 110 | context.query_params = query_params ? Util.encode_parameters(query_params) : nil |
91 | 111 | ||
92 | http_resp = execute_request_with_rescues(api_base, context) do | 112 | http_resp = execute_request_with_rescues(api_base, context) do |
@@ -243,6 +263,7 @@ module Syspro | @@ -243,6 +263,7 @@ module Syspro | ||
243 | attr_accessor :path | 263 | attr_accessor :path |
244 | attr_accessor :query_params | 264 | attr_accessor :query_params |
245 | attr_accessor :request_id | 265 | attr_accessor :request_id |
266 | + attr_accessor :user_id | ||
246 | 267 | ||
247 | # The idea with this method is that we might want to update some of | 268 | # The idea with this method is that we might want to update some of |
248 | # context information because a response that we've received from the API | 269 | # context information because a response that we've received from the API |
lib/syspro/syspro_object.rb
1 | module Syspro | 1 | module Syspro |
2 | class SysproObject | 2 | class SysproObject |
3 | + include Enumerable | ||
3 | 4 | ||
4 | - # Re-initializes the object based on a hash of values (usually one that's | ||
5 | - # come back from an API call). Adds or removes value accessors as necessary | ||
6 | - # and updates the state of internal data. | ||
7 | - # | ||
8 | - # Protected on purpose! Please do not expose. | ||
9 | - # | ||
10 | - # ==== Options | ||
11 | - # | ||
12 | - # * +:values:+ Hash used to update accessors and values. | ||
13 | - # * +:opts:+ Options for SysproObject like an API key. | ||
14 | - # * +:partial:+ Indicates that the re-initialization should not attempt to | ||
15 | - # remove accessors. | ||
16 | - def initialize_from(values, opts, partial = false) | 5 | + def initialize(id = nil, opts = {}) |
17 | @opts = Util.normalize_opts(opts) | 6 | @opts = Util.normalize_opts(opts) |
7 | + end | ||
18 | 8 | ||
19 | - # the `#send` is here so that we can keep this method private | ||
20 | - @original_values = self.class.send(:deep_copy, values) | ||
21 | - | ||
22 | - removed = partial ? Set.new : Set.new(@values.keys - values.keys) | ||
23 | - added = Set.new(values.keys - @values.keys) | ||
24 | - | ||
25 | - # Wipe old state before setting new. This is useful for e.g. updating a | ||
26 | - # customer, where there is no persistent card parameter. Mark those values | ||
27 | - # which don't persist as transient | ||
28 | - | ||
29 | - remove_accessors(removed) | ||
30 | - add_accessors(added, values) | 9 | + # Determines the equality of two Syspro objects. Syspro objects are |
10 | + # considered to be equal if they have the same set of values and each one | ||
11 | + # of those values is the same. | ||
12 | + def ==(other) | ||
13 | + other.is_a?(SysproObject) && @values == other.instance_variable_get(:@values) | ||
14 | + end | ||
31 | 15 | ||
32 | - removed.each do |k| | ||
33 | - @values.delete(k) | ||
34 | - @transient_values.add(k) | ||
35 | - @unsaved_values.delete(k) | ||
36 | - end | 16 | + def to_s(*_args) |
17 | + JSON.pretty_generate(to_hash) | ||
18 | + end | ||
37 | 19 | ||
38 | - update_attributes(values, opts, dirty: false) | ||
39 | - values.each_key do |k| | ||
40 | - @transient_values.delete(k) | ||
41 | - @unsaved_values.delete(k) | ||
42 | - end | 20 | + def inspect |
21 | + id_string = respond_to?(:id) && !id.nil? ? " id=#{id}" : "" | ||
22 | + "#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values) | ||
23 | + end | ||
43 | 24 | ||
44 | - self | 25 | + def keys |
26 | + @values.keys | ||
45 | end | 27 | end |
46 | 28 | ||
47 | - def remove_accessors(keys) | ||
48 | - # not available in the #instance_eval below | ||
49 | - protected_fields = self.class.protected_fields | 29 | + def values |
30 | + @values.values | ||
31 | + end | ||
50 | 32 | ||
51 | - metaclass.instance_eval do | ||
52 | - keys.each do |k| | ||
53 | - next if protected_fields.include?(k) | ||
54 | - next if @@permanent_attributes.include?(k) | 33 | + def to_hash |
34 | + maybe_to_hash = lambda do |value| | ||
35 | + value && value.respond_to?(:to_hash) ? value.to_hash : value | ||
36 | + end | ||
55 | 37 | ||
56 | - # Remove methods for the accessor's reader and writer. | ||
57 | - [k, :"#{k}=", :"#{k}?"].each do |method_name| | ||
58 | - remove_method(method_name) if method_defined?(method_name) | ||
59 | - end | ||
60 | - end | 38 | + @values.each_with_object({}) do |(key, value), acc| |
39 | + acc[key] = case value | ||
40 | + when Array | ||
41 | + value.map(&maybe_to_hash) | ||
42 | + else | ||
43 | + maybe_to_hash.call(value) | ||
44 | + end | ||
61 | end | 45 | end |
62 | end | 46 | end |
63 | 47 | ||
64 | - def add_accessors(keys, values) | ||
65 | - # not available in the #instance_eval below | ||
66 | - protected_fields = self.class.protected_fields | ||
67 | - | ||
68 | - metaclass.instance_eval do | ||
69 | - keys.each do |k| | ||
70 | - next if protected_fields.include?(k) | ||
71 | - next if @@permanent_attributes.include?(k) | ||
72 | - | ||
73 | - if k == :method | ||
74 | - # Object#method is a built-in Ruby method that accepts a symbol | ||
75 | - # and returns the corresponding Method object. Because the API may | ||
76 | - # also use `method` as a field name, we check the arity of *args | ||
77 | - # to decide whether to act as a getter or call the parent method. | ||
78 | - define_method(k) { |*args| args.empty? ? @values[k] : super(*args) } | ||
79 | - else | ||
80 | - define_method(k) { @values[k] } | ||
81 | - end | ||
82 | - | ||
83 | - define_method(:"#{k}=") do |v| | ||
84 | - if v == "" | ||
85 | - raise ArgumentError, "You cannot set #{k} to an empty string. " \ | ||
86 | - "We interpret empty strings as nil in requests. " \ | ||
87 | - "You may set (object).#{k} = nil to delete the property." | ||
88 | - end | ||
89 | - @values[k] = Util.convert_to_stripe_object(v, @opts) | ||
90 | - dirty_value!(@values[k]) | ||
91 | - @unsaved_values.add(k) | ||
92 | - end | 48 | + def each(&blk) |
49 | + @values.each(&blk) | ||
50 | + end | ||
93 | 51 | ||
94 | - if [FalseClass, TrueClass].include?(values[k].class) | ||
95 | - define_method(:"#{k}?") { @values[k] } | ||
96 | - end | 52 | + private |
53 | + | ||
54 | + # Produces a deep copy of the given object including support for arrays, | ||
55 | + # hashes, and SysproObject. | ||
56 | + def self.deep_copy(obj) | ||
57 | + case obj | ||
58 | + when Array | ||
59 | + obj.map { |e| deep_copy(e) } | ||
60 | + when Hash | ||
61 | + obj.each_with_object({}) do |(k, v), copy| | ||
62 | + copy[k] = deep_copy(v) | ||
63 | + copy | ||
97 | end | 64 | end |
65 | + when SysproObject | ||
66 | + obj.class.construct_from( | ||
67 | + deep_copy(obj.instance_variable_get(:@values)), | ||
68 | + obj.instance_variable_get(:@opts).select do |k, _v| | ||
69 | + Util::OPTS_COPYABLE.include?(k) | ||
70 | + end | ||
71 | + ) | ||
72 | + else | ||
73 | + obj | ||
98 | end | 74 | end |
99 | end | 75 | end |
76 | + private_class_method :deep_copy | ||
77 | + | ||
100 | end | 78 | end |
101 | end | 79 | end |