Commit dd338bea1c6322d58fa479e10011ab9d8281ef74

Authored by Isaac Lewis
Committed by GitHub
2 parents db76748d 697a8854

Merge pull request #1 from ike/logon

Logon, Logoff, and GetUserProfile
lib/syspro.rb
... ... @@ -5,6 +5,10 @@ require "logger"
5 5 require "openssl"
6 6  
7 7 require "syspro/api_resource"
  8 +require "syspro/get_logon_profile"
  9 +require "syspro/get_version"
  10 +require "syspro/logoff"
  11 +require "syspro/logon"
8 12 require "syspro/syspro_client"
9 13 require "syspro/singleton_api_resource"
10 14 require "syspro/syspro_object"
... ... @@ -12,10 +16,8 @@ require "syspro/syspro_response"
12 16 require "syspro/util"
13 17 require "syspro/version"
14 18  
15   -require "syspro/api_operations/get_version"
16 19 require "syspro/api_operations/request"
17 20  
18   -
19 21 module Syspro
20 22 @api_base = "http://syspro.wildlandlabs.com:90/SYSPROWCFService/Rest"
21 23  
... ...
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/get_logon_profile.rb 0 → 100644
  1 +module Syspro
  2 + class GetLogonProfile < ApiResource
  3 + def self.get_logon_profile(user_id)
  4 + params = { "UserId" => user_id }
  5 + resp = self.request(:get, resource_url, params)
  6 + parse_response(resp[0])
  7 + end
  8 +
  9 + def resource_url
  10 + "/GetLogonProfile"
  11 + end
  12 +
  13 + def self.parse_response(resp)
  14 + doc = resp.data
  15 +
  16 + UserProfile.new(
  17 + doc.xpath("//CompanyName").text,
  18 + doc.xpath("//OperatorCode").text,
  19 + doc.xpath("//OperatorGroup").text,
  20 + doc.xpath("//OperatorEmailAddress").text,
  21 + doc.xpath("//OperatorLocation").text,
  22 + doc.xpath("//OperatorLanguageCode").text,
  23 + doc.xpath("//SystemLanguage").text,
  24 + doc.xpath("//AccountingDate").text,
  25 + doc.xpath("//CompanyDate").text,
  26 + doc.xpath("//DefaultArBranch").text,
  27 + doc.xpath("//DefaultApBranch").text,
  28 + doc.xpath("//DefaultBank").text,
  29 + doc.xpath("//DefaultWarehouse").text,
  30 + doc.xpath("//DefaultCustomer").text,
  31 + doc.xpath("//SystemSiteId").text,
  32 + doc.xpath("//SystemNationalityCode").text,
  33 + doc.xpath("//LocalCurrencyCode").text,
  34 + doc.xpath("//CurrencyDescription").text,
  35 + doc.xpath("//DefaultRequisitionUser").text,
  36 + doc.xpath("//XMLToHTMLTransform").text,
  37 + doc.xpath("//CssStyle").text,
  38 + doc.xpath("//CssSuffix").text,
  39 + doc.xpath("//DecimalFormat").text,
  40 + doc.xpath("//DateFormat").text,
  41 + doc.xpath("//FunctionalRole").text,
  42 + doc.xpath("//DatabaseType").text,
  43 + doc.xpath("//SysproVersion").text,
  44 + doc.xpath("//EnetVersion").text,
  45 + doc.xpath("//SysproServerBitWidth").text,
  46 + )
  47 + end
  48 + private_class_method :parse_response
  49 +
  50 + UserProfile = Struct.new(:company_name, :operator_code, :operator_group, :operator_email_address,
  51 + :operator_location, :operator_language_code, :system_language, :accounting_date,
  52 + :company_date, :default_ar_branch, :default_ap_branch, :default_bank, :default_warehouse,
  53 + :default_customer, :system_site_id, :system_nationality_code, :local_currency_code,
  54 + :currency_description, :default_requisition_user, :xml_to_html_transform, :css_style,
  55 + :css_suffix, :decimal_format, :date_format, :functional_role, :database_type, :syspro_version,
  56 + :enet_version, :syspro_server_bit_width)
  57 + end
  58 +end
  59 +
... ...
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
  2 + class GetVersion < ApiResource
  3 + def self.get_version
  4 + resp = self.request(:get, resource_url)
  5 + VersionObject.new(resp[0].http_body)
  6 + end
7 7  
8   - def resource_url
9   - "/GetVersion"
10   - end
  8 + def resource_url
  9 + "/GetVersion"
11 10 end
  11 +
  12 + VersionObject = Struct.new(:version)
12 13 end
13 14 end
14 15  
... ...
lib/syspro/logoff.rb 0 → 100644
  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/logon.rb 0 → 100644
  1 +module Syspro
  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 + UserIdObject.new(resp[0].http_body)
  12 + end
  13 +
  14 + def resource_url
  15 + "/Logon"
  16 + end
  17 +
  18 + UserIdObject = Struct.new(:guid)
  19 + end
  20 +end
  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
... ... @@ -2,11 +2,21 @@ module Syspro
2 2 class SysproClient
3 3 attr_accessor :conn, :api_base
4 4  
  5 + @verify_ssl_warned = false
  6 +
5 7 def initialize(conn = nil)
6 8 self.conn = conn || self.class.default_conn
7 9 @system_profiler = SystemProfiler.new
8 10 end
9 11  
  12 + def logon(username, password, company_id, company_password)
  13 + Syspro::Logon.logon(username, password, company_id, company_password)
  14 + end
  15 +
  16 + def get_syspro_version
  17 + Syspro::GetVersion.get_version
  18 + end
  19 +
10 20 def self.active_client
11 21 Thread.current[:syspro_client] || default_client
12 22 end
... ... @@ -15,11 +25,6 @@ module Syspro
15 25 Thread.current[:syspro_client_default_client] ||= SysproClient.new(default_conn)
16 26 end
17 27  
18   - def get_syspro_version
19   - version_getter = Syspro::ApiOperations::GetVersion.new
20   - version_getter.request(:get, version_getter.resource_url)
21   - end
22   -
23 28 # A default Faraday connection to be used when one isn't configured. This
24 29 # object should never be mutated, and instead instantiating your own
25 30 # connection and wrapping it in a SysproClient object should be preferred.
... ... @@ -53,8 +58,27 @@ module Syspro
53 58 end
54 59 end
55 60  
56   - 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: {})
57 80 api_base ||= Syspro.api_base
  81 + user_id ||= ""
58 82  
59 83 params = Util.objects_to_ids(params)
60 84 url = api_url(path, api_base)
... ... @@ -82,6 +106,7 @@ module Syspro
82 106 context.body = body
83 107 context.method = method
84 108 context.path = path
  109 + context.user_id = user_id
85 110 context.query_params = query_params ? Util.encode_parameters(query_params) : nil
86 111  
87 112 http_resp = execute_request_with_rescues(api_base, context) do
... ... @@ -238,6 +263,7 @@ module Syspro
238 263 attr_accessor :path
239 264 attr_accessor :query_params
240 265 attr_accessor :request_id
  266 + attr_accessor :user_id
241 267  
242 268 # The idea with this method is that we might want to update some of
243 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
... ...
lib/syspro/syspro_response.rb
... ... @@ -26,8 +26,8 @@ module Syspro
26 26 # This may throw JSON::ParserError if the response body is not valid JSON.
27 27 def self.from_faraday_hash(http_resp)
28 28 resp = SysproResponse.new
29   - resp.data = Nokogiri::XML(http_resp[:body])
30 29 resp.http_body = http_resp[:body]
  30 + resp.data = Nokogiri::XML(resp.http_body)
31 31 resp.http_headers = http_resp[:headers]
32 32 resp.http_status = http_resp[:status]
33 33 resp.request_id = http_resp[:headers]["Request-Id"]
... ... @@ -39,8 +39,8 @@ module Syspro
39 39 # This may throw JSON::ParserError if the response body is not valid JSON.
40 40 def self.from_faraday_response(http_resp)
41 41 resp = SysproResponse.new
42   - resp.data = Nokogiri::XML(http_resp[:body])
43 42 resp.http_body = http_resp.body
  43 + resp.data = Nokogiri::XML(resp.http_body)
44 44 resp.http_headers = http_resp.headers
45 45 resp.http_status = http_resp.status
46 46 resp.request_id = http_resp.headers["Request-Id"]
... ...
lib/syspro/util.rb
... ... @@ -125,6 +125,13 @@ module Syspro
125 125 end
126 126 end
127 127  
  128 + def self.url_encode(key)
  129 + CGI.escape(key.to_s).
  130 + # Don't use strict form encoding by changing the square bracket control
  131 + # characters back to their literals. This is fine by the server, and
  132 + # makes these parameter strings easier to read.
  133 + gsub("%5B", "[").gsub("%5D", "]")
  134 + end
128 135 end
129 136 end
130 137  
... ...
test/client_test.rb
... ... @@ -3,6 +3,6 @@ require &quot;test_helper&quot;
3 3 class SysproClientTest < Minitest::Test
4 4 def test_get_syspro_version
5 5 client = ::Syspro::SysproClient.new
6   - assert_match /(\d+\.)?(\d+\.)?(\d+\.)?(\d+)/, client.get_syspro_version[0].http_body
  6 + assert_match (/(\d+\.)?(\d+\.)?(\d+\.)?(\d+)/), client.get_syspro_version.version
7 7 end
8 8 end
... ...
test/logoff_test.rb 0 → 100644
  1 +require "test_helper"
  2 +
  3 +class LogoffTest < Minitest::Test
  4 + def test_successful_logoff
  5 + username = "wland"
  6 + password = "piperita2016"
  7 + company = "L"
  8 + company_password = ""
  9 +
  10 + uid = Syspro::Logon.logon(username, password, company, company_password)
  11 + assert_equal true, Syspro::Logoff.logoff(uid.guid)
  12 + end
  13 +
  14 + def test_logoff_error
  15 + assert_kind_of String, Syspro::Logoff.logoff('1BB5B3050954BB459A5D034DB5CC386980')
  16 + end
  17 +end
  18 +
... ...
test/logon_test.rb 0 → 100644
  1 +require "test_helper"
  2 +
  3 +class LogonTest < Minitest::Test
  4 + def test_logon
  5 + username = "wland"
  6 + password = "piperita2016"
  7 + company = "L"
  8 + company_password = ""
  9 + client = ::Syspro::SysproClient.new
  10 +
  11 + assert_match (/([A-Z0-9]{33})\w/), client.logon(username, password, company, company_password).guid
  12 + end
  13 +end
  14 +
... ...