diff --git a/lib/syspro.rb b/lib/syspro.rb index f572547..681e812 100644 --- a/lib/syspro.rb +++ b/lib/syspro.rb @@ -5,6 +5,9 @@ require "logger" require "openssl" require "syspro/api_resource" +require "syspro/get_version" +require "syspro/logoff" +require "syspro/logon" require "syspro/syspro_client" require "syspro/singleton_api_resource" require "syspro/syspro_object" @@ -12,10 +15,7 @@ require "syspro/syspro_response" require "syspro/util" require "syspro/version" -require "syspro/api_operations/get_version" require "syspro/api_operations/request" -require "syspro/api_operations/logon" - module Syspro @api_base = "http://syspro.wildlandlabs.com:90/SYSPROWCFService/Rest" diff --git a/lib/syspro/api_operations/get_version.rb b/lib/syspro/api_operations/get_version.rb deleted file mode 100644 index 64c85c6..0000000 --- a/lib/syspro/api_operations/get_version.rb +++ /dev/null @@ -1,21 +0,0 @@ -require_relative "request" - -module Syspro - module ApiOperations - class GetVersion - include ApiOperations::Request - - def get_version - resp = self.request(:get, resource_url) - version = VersionObject.new(resp[0].http_body) - end - - def resource_url - "/GetVersion" - end - - VersionObject = Struct.new(:version) - end - end -end - diff --git a/lib/syspro/api_operations/logon.rb b/lib/syspro/api_operations/logon.rb deleted file mode 100644 index 31f36db..0000000 --- a/lib/syspro/api_operations/logon.rb +++ /dev/null @@ -1,27 +0,0 @@ -require_relative "request" - -module Syspro - module ApiOperations - class Logon - include ApiOperations::Request - - def logon(username, password, company_id, company_password = nil) - params = { - "Operator" => username, - "OperatorPassword" => password, - "CompanyId" => company_id, - "CompanyPassword" => company_password - } - resp = self.request(:get, resource_url, params) - user_id = UserIdObject.new(resp[0].http_body) - end - - def resource_url - "/Logon" - end - - UserIdObject = Struct.new(:guid) - end - end -end - diff --git a/lib/syspro/api_operations/request.rb b/lib/syspro/api_operations/request.rb index d6b0f8c..6d27e0d 100644 --- a/lib/syspro/api_operations/request.rb +++ b/lib/syspro/api_operations/request.rb @@ -1,26 +1,48 @@ module Syspro module ApiOperations module Request - def request(method, url, params = {}, opts = {}) - client = SysproClient.active_client + module ClassMethods + def request(method, url, params = {}, opts = {}) + warn_on_opts_in_params(params) + + opts = Util.normalize_opts(opts) + opts[:client] ||= SysproClient.active_client headers = opts.clone + user_id = headers.delete(:user_id) + client = headers.delete(:client) + # Assume all remaining opts must be headers resp = client.execute_request( method, url, headers: headers, + user_id: user_id, params: params ) resp - end + end - def warn_on_opts_in_params(params) + private + + def warn_on_opts_in_params(params) Util::OPTS_USER_SPECIFIED.each do |opt| if params.key?(opt) $stderr.puts("WARNING: #{opt} should be in opts instead of params.") end end + end + end # ClassMethods + + def self.included(base) + base.extend(ClassMethods) + end + + protected + + def request(method, url, params = {}, opts = {}) + opts = @opts.merge(Util.normalize_opts(opts)) + self.class.request(method, url, params, opts) end end end diff --git a/lib/syspro/api_resource.rb b/lib/syspro/api_resource.rb index 1033c54..2b32984 100644 --- a/lib/syspro/api_resource.rb +++ b/lib/syspro/api_resource.rb @@ -1,25 +1,26 @@ -require_relative "syspro_client" -require_relative "api_operations/request" +require "syspro/syspro_object" +require "syspro/api_operations/request" module Syspro - class ApiResource < Syspro::SysproClient + class ApiResource < SysproObject + include Syspro::ApiOperations::Request def self.class_name name.split("::")[-1] end def self.resource_url - if self == APIResource + if self == ApiResource raise NotImplementedError, "APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)" end - "/v1/#{CGI.escape(class_name.downcase)}s" + "/#{CGI.escape(class_name.downcase)}" end def resource_url - unless (id = self["id"]) - raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", "id") - end - "#{self.class.resource_url}/#{CGI.escape(id)}" + #unless (id = self["id"]) + #raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", "id") + #end + #"#{self.class.resource_url}/#{CGI.escape(id)}" end def refresh diff --git a/lib/syspro/get_version.rb b/lib/syspro/get_version.rb new file mode 100644 index 0000000..70af958 --- /dev/null +++ b/lib/syspro/get_version.rb @@ -0,0 +1,15 @@ +module Syspro + class GetVersion < ApiResource + def self.get_version + resp = self.request(:get, resource_url) + version = VersionObject.new(resp[0].http_body) + end + + def resource_url + "/GetVersion" + end + + VersionObject = Struct.new(:version) + end +end + diff --git a/lib/syspro/logoff.rb b/lib/syspro/logoff.rb new file mode 100644 index 0000000..6ce10d4 --- /dev/null +++ b/lib/syspro/logoff.rb @@ -0,0 +1,18 @@ +module Syspro + class Logoff < ApiResource + def self.logoff(user_id) + params = { "UserId" => user_id } + resp = self.request(:get, resource_url, params) + + if resp[0].http_body == "0" + true + else + resp[0].http_body + end + end + + def resource_url + "/Logoff" + end + end +end diff --git a/lib/syspro/logon.rb b/lib/syspro/logon.rb new file mode 100644 index 0000000..8b56c5f --- /dev/null +++ b/lib/syspro/logon.rb @@ -0,0 +1,21 @@ +module Syspro + class Logon < ApiResource + def self.logon(username, password, company_id, company_password = nil) + params = { + "Operator" => username, + "OperatorPassword" => password, + "CompanyId" => company_id, + "CompanyPassword" => company_password + } + resp = self.request(:get, resource_url, params) + user_id = UserIdObject.new(resp[0].http_body) + end + + def resource_url + "/Logon" + end + + UserIdObject = Struct.new(:guid) + end +end + diff --git a/lib/syspro/singleton_api_resource.rb b/lib/syspro/singleton_api_resource.rb index ece7c01..8eabfda 100644 --- a/lib/syspro/singleton_api_resource.rb +++ b/lib/syspro/singleton_api_resource.rb @@ -6,7 +6,7 @@ module Syspro if self == SingletonAPIResource raise NotImplementedError, "SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Customer, etc.)" end - "/v1/#{CGI.escape(class_name.downcase)}" + "/#{CGI.escape(class_name.downcase)}" end def resource_url diff --git a/lib/syspro/syspro_client.rb b/lib/syspro/syspro_client.rb index 26ad093..a8edd8c 100644 --- a/lib/syspro/syspro_client.rb +++ b/lib/syspro/syspro_client.rb @@ -58,8 +58,27 @@ module Syspro end end - def execute_request(method, path, api_base: nil, headers: {}, params: {}) + # Executes the API call within the given block. Usage looks like: + # + # client = StripeClient.new + # charge, resp = client.request { Charge.create } + # + def request + @last_response = nil + old_stripe_client = Thread.current[:stripe_client] + Thread.current[:stripe_client] = self + + begin + res = yield + [res, @last_response] + ensure + Thread.current[:stripe_client] = old_stripe_client + end + end + + def execute_request(method, path, user_id: nil, api_base: nil, headers: {}, params: {}) api_base ||= Syspro.api_base + user_id ||= "" params = Util.objects_to_ids(params) url = api_url(path, api_base) @@ -87,6 +106,7 @@ module Syspro context.body = body context.method = method context.path = path + context.user_id = user_id context.query_params = query_params ? Util.encode_parameters(query_params) : nil http_resp = execute_request_with_rescues(api_base, context) do @@ -243,6 +263,7 @@ module Syspro attr_accessor :path attr_accessor :query_params attr_accessor :request_id + attr_accessor :user_id # The idea with this method is that we might want to update some of # context information because a response that we've received from the API diff --git a/lib/syspro/syspro_object.rb b/lib/syspro/syspro_object.rb index 7c761a4..d0eaa49 100644 --- a/lib/syspro/syspro_object.rb +++ b/lib/syspro/syspro_object.rb @@ -1,101 +1,79 @@ module Syspro class SysproObject + include Enumerable - # Re-initializes the object based on a hash of values (usually one that's - # come back from an API call). Adds or removes value accessors as necessary - # and updates the state of internal data. - # - # Protected on purpose! Please do not expose. - # - # ==== Options - # - # * +:values:+ Hash used to update accessors and values. - # * +:opts:+ Options for SysproObject like an API key. - # * +:partial:+ Indicates that the re-initialization should not attempt to - # remove accessors. - def initialize_from(values, opts, partial = false) + def initialize(id = nil, opts = {}) @opts = Util.normalize_opts(opts) + end - # the `#send` is here so that we can keep this method private - @original_values = self.class.send(:deep_copy, values) - - removed = partial ? Set.new : Set.new(@values.keys - values.keys) - added = Set.new(values.keys - @values.keys) - - # Wipe old state before setting new. This is useful for e.g. updating a - # customer, where there is no persistent card parameter. Mark those values - # which don't persist as transient - - remove_accessors(removed) - add_accessors(added, values) + # Determines the equality of two Syspro objects. Syspro objects are + # considered to be equal if they have the same set of values and each one + # of those values is the same. + def ==(other) + other.is_a?(SysproObject) && @values == other.instance_variable_get(:@values) + end - removed.each do |k| - @values.delete(k) - @transient_values.add(k) - @unsaved_values.delete(k) - end + def to_s(*_args) + JSON.pretty_generate(to_hash) + end - update_attributes(values, opts, dirty: false) - values.each_key do |k| - @transient_values.delete(k) - @unsaved_values.delete(k) - end + def inspect + id_string = respond_to?(:id) && !id.nil? ? " id=#{id}" : "" + "#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values) + end - self + def keys + @values.keys end - def remove_accessors(keys) - # not available in the #instance_eval below - protected_fields = self.class.protected_fields + def values + @values.values + end - metaclass.instance_eval do - keys.each do |k| - next if protected_fields.include?(k) - next if @@permanent_attributes.include?(k) + def to_hash + maybe_to_hash = lambda do |value| + value && value.respond_to?(:to_hash) ? value.to_hash : value + end - # Remove methods for the accessor's reader and writer. - [k, :"#{k}=", :"#{k}?"].each do |method_name| - remove_method(method_name) if method_defined?(method_name) - end - end + @values.each_with_object({}) do |(key, value), acc| + acc[key] = case value + when Array + value.map(&maybe_to_hash) + else + maybe_to_hash.call(value) + end end end - def add_accessors(keys, values) - # not available in the #instance_eval below - protected_fields = self.class.protected_fields - - metaclass.instance_eval do - keys.each do |k| - next if protected_fields.include?(k) - next if @@permanent_attributes.include?(k) - - if k == :method - # Object#method is a built-in Ruby method that accepts a symbol - # and returns the corresponding Method object. Because the API may - # also use `method` as a field name, we check the arity of *args - # to decide whether to act as a getter or call the parent method. - define_method(k) { |*args| args.empty? ? @values[k] : super(*args) } - else - define_method(k) { @values[k] } - end - - define_method(:"#{k}=") do |v| - if v == "" - raise ArgumentError, "You cannot set #{k} to an empty string. " \ - "We interpret empty strings as nil in requests. " \ - "You may set (object).#{k} = nil to delete the property." - end - @values[k] = Util.convert_to_stripe_object(v, @opts) - dirty_value!(@values[k]) - @unsaved_values.add(k) - end + def each(&blk) + @values.each(&blk) + end - if [FalseClass, TrueClass].include?(values[k].class) - define_method(:"#{k}?") { @values[k] } - end + private + + # Produces a deep copy of the given object including support for arrays, + # hashes, and SysproObject. + def self.deep_copy(obj) + case obj + when Array + obj.map { |e| deep_copy(e) } + when Hash + obj.each_with_object({}) do |(k, v), copy| + copy[k] = deep_copy(v) + copy end + when SysproObject + obj.class.construct_from( + deep_copy(obj.instance_variable_get(:@values)), + obj.instance_variable_get(:@opts).select do |k, _v| + Util::OPTS_COPYABLE.include?(k) + end + ) + else + obj end end + private_class_method :deep_copy + end end -- libgit2 0.21.4