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 | 5 | require "openssl" |
6 | 6 | |
7 | 7 | require "syspro/api_resource" |
8 | +require "syspro/get_version" | |
9 | +require "syspro/logoff" | |
10 | +require "syspro/logon" | |
8 | 11 | require "syspro/syspro_client" |
9 | 12 | require "syspro/singleton_api_resource" |
10 | 13 | require "syspro/syspro_object" |
... | ... | @@ -12,10 +15,7 @@ require "syspro/syspro_response" |
12 | 15 | require "syspro/util" |
13 | 16 | require "syspro/version" |
14 | 17 | |
15 | -require "syspro/api_operations/get_version" | |
16 | 18 | require "syspro/api_operations/request" |
17 | -require "syspro/api_operations/logon" | |
18 | - | |
19 | 19 | |
20 | 20 | module Syspro |
21 | 21 | @api_base = "http://syspro.wildlandlabs.com:90/SYSPROWCFService/Rest" | ... | ... |
lib/syspro/api_operations/request.rb
1 | 1 | module Syspro |
2 | 2 | module ApiOperations |
3 | 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 | 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 | 16 | resp = client.execute_request( |
10 | 17 | method, url, |
11 | 18 | headers: headers, |
19 | + user_id: user_id, | |
12 | 20 | params: params |
13 | 21 | ) |
14 | 22 | |
15 | 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 | 29 | Util::OPTS_USER_SPECIFIED.each do |opt| |
20 | 30 | if params.key?(opt) |
21 | 31 | $stderr.puts("WARNING: #{opt} should be in opts instead of params.") |
22 | 32 | end |
23 | 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 | 46 | end |
25 | 47 | end |
26 | 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 | 4 | module Syspro |
5 | - class ApiResource < Syspro::SysproClient | |
5 | + class ApiResource < SysproObject | |
6 | + include Syspro::ApiOperations::Request | |
6 | 7 | |
7 | 8 | def self.class_name |
8 | 9 | name.split("::")[-1] |
9 | 10 | end |
10 | 11 | |
11 | 12 | def self.resource_url |
12 | - if self == APIResource | |
13 | + if self == ApiResource | |
13 | 14 | raise NotImplementedError, "APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)" |
14 | 15 | end |
15 | - "/v1/#{CGI.escape(class_name.downcase)}s" | |
16 | + "/#{CGI.escape(class_name.downcase)}" | |
16 | 17 | end |
17 | 18 | |
18 | 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 | 24 | end |
24 | 25 | |
25 | 26 | def refresh | ... | ... |
lib/syspro/api_operations/get_version.rb renamed to lib/syspro/get_version.rb
1 | -require_relative "request" | |
2 | - | |
3 | 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 | 10 | end |
11 | + | |
12 | + VersionObject = Struct.new(:version) | |
19 | 13 | end |
20 | 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 | 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 | 16 | end |
17 | + | |
18 | + UserIdObject = Struct.new(:guid) | |
25 | 19 | end |
26 | 20 | end |
27 | 21 | ... | ... |
lib/syspro/singleton_api_resource.rb
... | ... | @@ -6,7 +6,7 @@ module Syspro |
6 | 6 | if self == SingletonAPIResource |
7 | 7 | raise NotImplementedError, "SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Customer, etc.)" |
8 | 8 | end |
9 | - "/v1/#{CGI.escape(class_name.downcase)}" | |
9 | + "/#{CGI.escape(class_name.downcase)}" | |
10 | 10 | end |
11 | 11 | |
12 | 12 | def resource_url | ... | ... |
lib/syspro/syspro_client.rb
... | ... | @@ -58,8 +58,27 @@ module Syspro |
58 | 58 | end |
59 | 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 | 80 | api_base ||= Syspro.api_base |
81 | + user_id ||= "" | |
63 | 82 | |
64 | 83 | params = Util.objects_to_ids(params) |
65 | 84 | url = api_url(path, api_base) |
... | ... | @@ -87,6 +106,7 @@ module Syspro |
87 | 106 | context.body = body |
88 | 107 | context.method = method |
89 | 108 | context.path = path |
109 | + context.user_id = user_id | |
90 | 110 | context.query_params = query_params ? Util.encode_parameters(query_params) : nil |
91 | 111 | |
92 | 112 | http_resp = execute_request_with_rescues(api_base, context) do |
... | ... | @@ -243,6 +263,7 @@ module Syspro |
243 | 263 | attr_accessor :path |
244 | 264 | attr_accessor :query_params |
245 | 265 | attr_accessor :request_id |
266 | + attr_accessor :user_id | |
246 | 267 | |
247 | 268 | # The idea with this method is that we might want to update some of |
248 | 269 | # context information because a response that we've received from the API | ... | ... |
lib/syspro/syspro_object.rb
1 | 1 | module Syspro |
2 | 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 | 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 | 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 | 45 | end |
62 | 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 | 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 | 74 | end |
99 | 75 | end |
76 | + private_class_method :deep_copy | |
77 | + | |
100 | 78 | end |
101 | 79 | end | ... | ... |