Commit db76748d4eb8f231c0f8abed232c33598dd6ae86

Authored by Isaac Lewis
1 parent 51fb5579

cop a bunch of Stripe Ruby architecture; working GetVersion route test

@@ -7,10 +7,15 @@ PATH @@ -7,10 +7,15 @@ PATH
7 GEM 7 GEM
8 remote: https://rubygems.org/ 8 remote: https://rubygems.org/
9 specs: 9 specs:
  10 + coderay (1.1.2)
10 faraday (0.14.0) 11 faraday (0.14.0)
11 multipart-post (>= 1.2, < 3) 12 multipart-post (>= 1.2, < 3)
  13 + method_source (0.9.0)
12 minitest (5.11.3) 14 minitest (5.11.3)
13 multipart-post (2.0.0) 15 multipart-post (2.0.0)
  16 + pry (0.11.3)
  17 + coderay (~> 1.1.0)
  18 + method_source (~> 0.9.0)
14 rake (10.5.0) 19 rake (10.5.0)
15 20
16 PLATFORMS 21 PLATFORMS
@@ -19,6 +24,7 @@ PLATFORMS @@ -19,6 +24,7 @@ PLATFORMS
19 DEPENDENCIES 24 DEPENDENCIES
20 bundler (~> 1.16) 25 bundler (~> 1.16)
21 minitest (~> 5.0) 26 minitest (~> 5.0)
  27 + pry (~> 0.11)
22 rake (~> 10.0) 28 rake (~> 10.0)
23 syspro! 29 syspro!
24 30
@@ -4,18 +4,93 @@ require &quot;json&quot; @@ -4,18 +4,93 @@ require &quot;json&quot;
4 require "logger" 4 require "logger"
5 require "openssl" 5 require "openssl"
6 6
7 -require "syspro/version" 7 +require "syspro/api_resource"
8 require "syspro/syspro_client" 8 require "syspro/syspro_client"
  9 +require "syspro/singleton_api_resource"
  10 +require "syspro/syspro_object"
  11 +require "syspro/syspro_response"
  12 +require "syspro/util"
  13 +require "syspro/version"
  14 +
  15 +require "syspro/api_operations/get_version"
  16 +require "syspro/api_operations/request"
  17 +
9 18
10 module Syspro 19 module Syspro
11 - # Your code goes here... 20 + @api_base = "http://syspro.wildlandlabs.com:90/SYSPROWCFService/Rest"
  21 +
  22 + @open_timeout = 30
  23 + @read_timeout = 80
  24 +
  25 + @log_level = nil
  26 + @logger = nil
  27 +
  28 + @max_network_retries = 0
  29 + @max_network_retry_delay = 2
  30 + @initial_network_retry_delay = 0.5
  31 +
  32 + class << self
  33 + attr_accessor :api_base, :open_timeout, :read_timeout
  34 + end
  35 +
  36 + # Options that should be persisted between API requests. This includes
  37 + # client, which is an object containing an HTTP client to reuse.
  38 + OPTS_PERSISTABLE = (
  39 + Set[:client]
  40 + ).freeze
  41 +
  42 + # map to the same values as the standard library's logger
  43 + LEVEL_DEBUG = Logger::DEBUG
  44 + LEVEL_ERROR = Logger::ERROR
  45 + LEVEL_INFO = Logger::INFO
  46 +
  47 + # When set prompts the library to log some extra information to $stdout and
  48 + # $stderr about what it's doing. For example, it'll produce information about
  49 + # requests, responses, and errors that are received. Valid log levels are
  50 + # `debug` and `info`, with `debug` being a little more verbose in places.
12 # 51 #
  52 + # Use of this configuration is only useful when `.logger` is _not_ set. When
  53 + # it is, the decision what levels to print is entirely deferred to the logger.
  54 + def self.log_level
  55 + @log_level
  56 + end
13 57
14 - def self.ca_store  
15 - @ca_store ||= begin  
16 - store = OpenSSL::X509::Store.new  
17 - store.add_file(ca_bundle_path)  
18 - store 58 + def self.log_level=(val)
  59 + # Backwards compatibility for values that we briefly allowed
  60 + if val == "debug"
  61 + val = LEVEL_DEBUG
  62 + elsif val == "info"
  63 + val = LEVEL_INFO
19 end 64 end
  65 +
  66 + if !val.nil? && ![LEVEL_DEBUG, LEVEL_ERROR, LEVEL_INFO].include?(val)
  67 + raise ArgumentError, "log_level should only be set to `nil`, `debug` or `info`"
  68 + end
  69 + @log_level = val
  70 + end
  71 +
  72 + # Sets a logger to which logging output will be sent. The logger should
  73 + # support the same interface as the `Logger` class that's part of Ruby's
  74 + # standard library (hint, anything in `Rails.logger` will likely be
  75 + # suitable).
  76 + #
  77 + # If `.logger` is set, the value of `.log_level` is ignored. The decision on
  78 + # what levels to print is entirely deferred to the logger.
  79 + def self.logger
  80 + @logger
  81 + end
  82 +
  83 + def self.logger=(val)
  84 + @logger = val
  85 + end
  86 +
  87 + def self.max_network_retries
  88 + @max_network_retries
20 end 89 end
  90 +
  91 + def self.max_network_retries=(val)
  92 + @max_network_retries = val.to_i
  93 + end
  94 +
  95 + Stripe.log_level = ENV["STRIPE_LOG"] unless ENV["STRIPE_LOG"].nil?
21 end 96 end
lib/syspro/api_operations/get_version.rb 0 โ†’ 100644
  1 +require_relative "request"
  2 +
  3 +module Syspro
  4 + module ApiOperations
  5 + class GetVersion
  6 + include ApiOperations::Request
  7 +
  8 + def resource_url
  9 + "/GetVersion"
  10 + end
  11 + end
  12 + end
  13 +end
  14 +
lib/syspro/api_operations/request.rb 0 โ†’ 100644
  1 +module Syspro
  2 + module ApiOperations
  3 + module Request
  4 + def request(method, url, params = {}, opts = {})
  5 + client = SysproClient.active_client
  6 +
  7 + headers = opts.clone
  8 +
  9 + resp = client.execute_request(
  10 + method, url,
  11 + headers: headers,
  12 + params: params
  13 + )
  14 +
  15 + resp
  16 + end
  17 +
  18 + def warn_on_opts_in_params(params)
  19 + Util::OPTS_USER_SPECIFIED.each do |opt|
  20 + if params.key?(opt)
  21 + $stderr.puts("WARNING: #{opt} should be in opts instead of params.")
  22 + end
  23 + end
  24 + end
  25 + end
  26 + end
  27 +end
  28 +
lib/syspro/api_resource.rb 0 โ†’ 100644
  1 +require_relative "syspro_client"
  2 +require_relative "api_operations/request"
  3 +
  4 +module Syspro
  5 + class ApiResource < Syspro::SysproClient
  6 +
  7 + def self.class_name
  8 + name.split("::")[-1]
  9 + end
  10 +
  11 + def self.resource_url
  12 + if self == APIResource
  13 + raise NotImplementedError, "APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)"
  14 + end
  15 + "/v1/#{CGI.escape(class_name.downcase)}s"
  16 + end
  17 +
  18 + 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)}"
  23 + end
  24 +
  25 + def refresh
  26 + resp, opts = request(:get, resource_url, @retrieve_params)
  27 + initialize_from(resp.data, opts)
  28 + end
  29 +
  30 + def self.retrieve(id, opts = {})
  31 + opts = Util.normalize_opts(opts)
  32 + instance = new(id, opts)
  33 + instance.refresh
  34 + instance
  35 + end
  36 + end
  37 +end
lib/syspro/singleton_api_resource.rb 0 โ†’ 100644
  1 +require_relative "api_resource"
  2 +
  3 +module Syspro
  4 + class SingletonAPIResource < ApiResource
  5 + def self.resource_url
  6 + if self == SingletonAPIResource
  7 + raise NotImplementedError, "SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Customer, etc.)"
  8 + end
  9 + "/v1/#{CGI.escape(class_name.downcase)}"
  10 + end
  11 +
  12 + def resource_url
  13 + self.class.resource_url
  14 + end
  15 +
  16 + def self.retrieve(opts = {})
  17 + instance = new(nil, Util.normalize_opts(opts))
  18 + instance.refresh
  19 + instance
  20 + end
  21 + end
  22 +end
lib/syspro/syspro_client.rb
1 module Syspro 1 module Syspro
2 class SysproClient 2 class SysproClient
  3 + attr_accessor :conn, :api_base
  4 +
3 def initialize(conn = nil) 5 def initialize(conn = nil)
4 self.conn = conn || self.class.default_conn 6 self.conn = conn || self.class.default_conn
5 @system_profiler = SystemProfiler.new 7 @system_profiler = SystemProfiler.new
@@ -14,7 +16,308 @@ module Syspro @@ -14,7 +16,308 @@ module Syspro
14 end 16 end
15 17
16 def get_syspro_version 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 + # A default Faraday connection to be used when one isn't configured. This
  24 + # object should never be mutated, and instead instantiating your own
  25 + # connection and wrapping it in a SysproClient object should be preferred.
  26 + def self.default_conn
  27 + # We're going to keep connections around so that we can take advantage
  28 + # of connection re-use, so make sure that we have a separate connection
  29 + # object per thread.
  30 + Thread.current[:syspro_client_default_conn] ||= begin
  31 + conn = Faraday.new do |c|
  32 + c.use Faraday::Request::Multipart
  33 + c.use Faraday::Request::UrlEncoded
  34 + c.use Faraday::Response::RaiseError
  35 + c.adapter Faraday.default_adapter
  36 + end
  37 +
  38 + #if Syspro.verify_ssl_certs
  39 + #conn.ssl.verify = true
  40 + #conn.ssl.cert_store = Syspro.ca_store
  41 + #else
  42 + conn.ssl.verify = false
  43 +
  44 + unless @verify_ssl_warned
  45 + @verify_ssl_warned = true
  46 + $stderr.puts("WARNING: Running without SSL cert verification. " \
  47 + "You should never do this in production. " \
  48 + "Execute 'Syspro.verify_ssl_certs = true' to enable verification.")
  49 + end
  50 + #end
  51 +
  52 + conn
  53 + end
  54 + end
  55 +
  56 + def execute_request(method, path, api_base: nil, headers: {}, params: {})
  57 + api_base ||= Syspro.api_base
  58 +
  59 + params = Util.objects_to_ids(params)
  60 + url = api_url(path, api_base)
  61 +
  62 + body = nil
  63 + query_params = nil
  64 +
  65 + case method.to_s.downcase.to_sym
  66 + when :get, :head, :delete
  67 + query_params = params
  68 + else
  69 + body = if headers[:content_type] && headers[:content_type] == "multipart/form-data"
  70 + params
  71 + else
  72 + Util.encode_parameters(params)
  73 + end
  74 + end
  75 +
  76 + headers = request_headers(method)
  77 + .update(Util.normalize_headers(headers))
  78 +
  79 + # stores information on the request we're about to make so that we don't
  80 + # have to pass as many parameters around for logging.
  81 + context = RequestLogContext.new
  82 + context.body = body
  83 + context.method = method
  84 + context.path = path
  85 + context.query_params = query_params ? Util.encode_parameters(query_params) : nil
  86 +
  87 + http_resp = execute_request_with_rescues(api_base, context) do
  88 + conn.run_request(method, url, body, headers) do |req|
  89 + req.options.open_timeout = Syspro.open_timeout
  90 + req.options.timeout = Syspro.read_timeout
  91 + req.params = query_params unless query_params.nil?
  92 + end
  93 + end
  94 +
  95 + begin
  96 + resp = SysproResponse.from_faraday_response(http_resp)
  97 + rescue JSON::ParserError
  98 + raise general_api_error(http_resp.status, http_resp.body)
  99 + end
  100 +
  101 + # Allows SysproClient#request to return a response object to a caller.
  102 + @last_response = resp
  103 + [resp]
  104 + end
  105 +
  106 + def general_api_error(status, body)
  107 + APIError.new("Invalid response object from API: #{body.inspect} " \
  108 + "(HTTP response code was #{status})",
  109 + http_status: status, http_body: body)
  110 + end
  111 +
  112 + def api_url(url = "", api_base = nil)
  113 + (api_base || Syspro.api_base) + url
  114 + end
  115 +
  116 + def request_headers(method)
  117 + user_agent = "Syspro/7 RubyBindings/#{Syspro::VERSION}"
  118 +
  119 + headers = {
  120 + "User-Agent" => user_agent,
  121 + "Content-Type" => "application/x-www-form-urlencoded",
  122 + }
  123 +
  124 + headers
  125 + end
  126 +
  127 + def execute_request_with_rescues(api_base, context)
  128 + num_retries = 0
  129 + begin
  130 + request_start = Time.now
  131 + log_request(context, num_retries)
  132 + resp = yield
  133 + context = context.dup_from_response(resp)
  134 + log_response(context, request_start, resp.status, resp.body)
  135 +
  136 + # We rescue all exceptions from a request so that we have an easy spot to
  137 + # implement our retry logic across the board. We'll re-raise if it's a type
  138 + # of exception that we didn't expect to handle.
  139 + rescue StandardError => e
  140 + # If we modify context we copy it into a new variable so as not to
  141 + # taint the original on a retry.
  142 + error_context = context
  143 +
  144 + if e.respond_to?(:response) && e.response
  145 + error_context = context.dup_from_response(e.response)
  146 + log_response(error_context, request_start,
  147 + e.response[:status], e.response[:body])
  148 + else
  149 + log_response_error(error_context, request_start, e)
  150 + end
  151 +
  152 + if self.class.should_retry?(e, num_retries)
  153 + num_retries += 1
  154 + sleep self.class.sleep_time(num_retries)
  155 + retry
  156 + end
  157 +
  158 + case e
  159 + when Faraday::ClientError
  160 + if e.response
  161 + handle_error_response(e.response, error_context)
  162 + else
  163 + handle_network_error(e, error_context, num_retries, api_base)
  164 + end
  165 +
  166 + # Only handle errors when we know we can do so, and re-raise otherwise.
  167 + # This should be pretty infrequent.
  168 + else
  169 + raise
  170 + end
  171 + end
  172 +
  173 + resp
  174 + end
  175 +
  176 + def self.should_retry?(e, num_retries)
  177 + # Retry on timeout-related problems (either on open or read).
  178 + return true if e.is_a?(Faraday::TimeoutError)
  179 +
  180 + # Destination refused the connection, the connection was reset, or a
  181 + # variety of other connection failures. This could occur from a single
  182 + # saturated server, so retry in case it's intermittent.
  183 + return true if e.is_a?(Faraday::ConnectionFailed)
  184 +
  185 + if e.is_a?(Faraday::ClientError) && e.response
  186 + # 409 conflict
  187 + return true if e.response[:status] == 409
  188 + end
  189 +
  190 + false
  191 + end
  192 +
  193 + def log_request(context, num_retries)
  194 + Util.log_info("Request to Syspro API",
  195 + account: context.account,
  196 + api_version: context.api_version,
  197 + method: context.method,
  198 + num_retries: num_retries,
  199 + path: context.path)
  200 + Util.log_debug("Request details",
  201 + body: context.body,
  202 + query_params: context.query_params)
  203 + end
  204 + private :log_request
  205 +
  206 + def log_response(context, request_start, status, body)
  207 + Util.log_info("Response from Syspro API",
  208 + account: context.account,
  209 + api_version: context.api_version,
  210 + elapsed: Time.now - request_start,
  211 + method: context.method,
  212 + path: context.path,
  213 + request_id: context.request_id,
  214 + status: status)
  215 + Util.log_debug("Response details",
  216 + body: body,
  217 + request_id: context.request_id)
  218 + end
  219 + private :log_response
  220 +
  221 + def log_response_error(context, request_start, e)
  222 + Util.log_error("Request error",
  223 + elapsed: Time.now - request_start,
  224 + error_message: e.message,
  225 + method: context.method,
  226 + path: context.path)
  227 + end
  228 + private :log_response_error
  229 +
  230 + # RequestLogContext stores information about a request that's begin made so
  231 + # that we can log certain information. It's useful because it means that we
  232 + # don't have to pass around as many parameters.
  233 + class RequestLogContext
  234 + attr_accessor :body
  235 + attr_accessor :account
  236 + attr_accessor :api_version
  237 + attr_accessor :method
  238 + attr_accessor :path
  239 + attr_accessor :query_params
  240 + attr_accessor :request_id
  241 +
  242 + # The idea with this method is that we might want to update some of
  243 + # context information because a response that we've received from the API
  244 + # contains information that's more authoritative than what we started
  245 + # with for a request. For example, we should trust whatever came back in
  246 + # a `Stripe-Version` header beyond what configuration information that we
  247 + # might have had available.
  248 + def dup_from_response(resp)
  249 + return self if resp.nil?
  250 +
  251 + # Faraday's API is a little unusual. Normally it'll produce a response
  252 + # object with a `headers` method, but on error what it puts into
  253 + # `e.response` is an untyped `Hash`.
  254 + headers = if resp.is_a?(Faraday::Response)
  255 + resp.headers
  256 + else
  257 + resp[:headers]
  258 + end
  259 +
  260 + context = dup
  261 + context.account = headers["Stripe-Account"]
  262 + context.api_version = headers["Stripe-Version"]
  263 + context.request_id = headers["Request-Id"]
  264 + context
  265 + end
  266 + end
  267 +
  268 + # SystemProfiler extracts information about the system that we're running
  269 + # in so that we can generate a rich user agent header to help debug
  270 + # integrations.
  271 + class SystemProfiler
  272 + def self.uname
  273 + if File.exist?("/proc/version")
  274 + File.read("/proc/version").strip
  275 + else
  276 + case RbConfig::CONFIG["host_os"]
  277 + when /linux|darwin|bsd|sunos|solaris|cygwin/i
  278 + uname_from_system
  279 + when /mswin|mingw/i
  280 + uname_from_system_ver
  281 + else
  282 + "unknown platform"
  283 + end
  284 + end
  285 + end
  286 +
  287 + def self.uname_from_system
  288 + (`uname -a 2>/dev/null` || "").strip
  289 + rescue Errno::ENOENT
  290 + "uname executable not found"
  291 + rescue Errno::ENOMEM # couldn't create subprocess
  292 + "uname lookup failed"
  293 + end
  294 +
  295 + def self.uname_from_system_ver
  296 + (`ver` || "").strip
  297 + rescue Errno::ENOENT
  298 + "ver executable not found"
  299 + rescue Errno::ENOMEM # couldn't create subprocess
  300 + "uname lookup failed"
  301 + end
  302 +
  303 + def initialize
  304 + @uname = self.class.uname
  305 + end
  306 +
  307 + def user_agent
  308 + lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
17 309
  310 + {
  311 + application: Syspro.app_info,
  312 + bindings_version: Syspro::VERSION,
  313 + lang: "ruby",
  314 + lang_version: lang_version,
  315 + platform: RUBY_PLATFORM,
  316 + engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
  317 + uname: @uname,
  318 + hostname: Socket.gethostname,
  319 + }.delete_if { |_k, v| v.nil? }
  320 + end
18 end 321 end
19 end 322 end
20 end 323 end
lib/syspro/syspro_object.rb 0 โ†’ 100644
  1 +module Syspro
  2 + class SysproObject
  3 +
  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)
  17 + @opts = Util.normalize_opts(opts)
  18 +
  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)
  31 +
  32 + removed.each do |k|
  33 + @values.delete(k)
  34 + @transient_values.add(k)
  35 + @unsaved_values.delete(k)
  36 + end
  37 +
  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
  43 +
  44 + self
  45 + end
  46 +
  47 + def remove_accessors(keys)
  48 + # not available in the #instance_eval below
  49 + protected_fields = self.class.protected_fields
  50 +
  51 + metaclass.instance_eval do
  52 + keys.each do |k|
  53 + next if protected_fields.include?(k)
  54 + next if @@permanent_attributes.include?(k)
  55 +
  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
  61 + end
  62 + end
  63 +
  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
  93 +
  94 + if [FalseClass, TrueClass].include?(values[k].class)
  95 + define_method(:"#{k}?") { @values[k] }
  96 + end
  97 + end
  98 + end
  99 + end
  100 + end
  101 +end
lib/syspro/syspro_response.rb 0 โ†’ 100644
  1 +require "nokogiri"
  2 +
  3 +module Syspro
  4 + # SysproResponse encapsulates some vitals of a response that came back from
  5 + # the Syspro API.
  6 + class SysproResponse
  7 + # The data contained by the HTTP body of the response deserialized from
  8 + # JSON.
  9 + attr_accessor :data
  10 +
  11 + # The raw HTTP body of the response.
  12 + attr_accessor :http_body
  13 +
  14 + # A Hash of the HTTP headers of the response.
  15 + attr_accessor :http_headers
  16 +
  17 + # The integer HTTP status code of the response.
  18 + attr_accessor :http_status
  19 +
  20 + # The Syspro request ID of the response.
  21 + attr_accessor :request_id
  22 +
  23 + # Initializes a SysproResponse object from a Hash like the kind returned as
  24 + # part of a Faraday exception.
  25 + #
  26 + # This may throw JSON::ParserError if the response body is not valid JSON.
  27 + def self.from_faraday_hash(http_resp)
  28 + resp = SysproResponse.new
  29 + resp.data = Nokogiri::XML(http_resp[:body])
  30 + resp.http_body = http_resp[:body]
  31 + resp.http_headers = http_resp[:headers]
  32 + resp.http_status = http_resp[:status]
  33 + resp.request_id = http_resp[:headers]["Request-Id"]
  34 + resp
  35 + end
  36 +
  37 + # Initializes a SysproResponse object from a Faraday HTTP response object.
  38 + #
  39 + # This may throw JSON::ParserError if the response body is not valid JSON.
  40 + def self.from_faraday_response(http_resp)
  41 + resp = SysproResponse.new
  42 + resp.data = Nokogiri::XML(http_resp[:body])
  43 + resp.http_body = http_resp.body
  44 + resp.http_headers = http_resp.headers
  45 + resp.http_status = http_resp.status
  46 + resp.request_id = http_resp.headers["Request-Id"]
  47 + resp
  48 + end
  49 + end
  50 +end
lib/syspro/util.rb 0 โ†’ 100644
  1 +module Syspro
  2 + class Util
  3 + # Options that a user is allowed to specify.
  4 + OPTS_USER_SPECIFIED = Set[
  5 + # :syspro_version
  6 + ].freeze
  7 +
  8 +
  9 + def self.objects_to_ids(h)
  10 + case h
  11 + when ApiResource
  12 + h.id
  13 + when Hash
  14 + res = {}
  15 + h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
  16 + res
  17 + when Array
  18 + h.map { |v| objects_to_ids(v) }
  19 + else
  20 + h
  21 + end
  22 + end
  23 +
  24 + # Converts a hash of fields or an array of hashes into a +SysproObject+ or
  25 + # array of +SysproObject+s. These new objects will be created as a concrete
  26 + # type as dictated by their `object` field (e.g. an `object` value of
  27 + # `charge` would create an instance of +Charge+), but if `object` is not
  28 + # present or of an unknown type, the newly created instance will fall back
  29 + # to being a +SysproObject+.
  30 + #
  31 + # ==== Attributes
  32 + #
  33 + # * +data+ - Hash of fields and values to be converted into a SysproObject.
  34 + # * +opts+ - Options for +SysproObject+ like an API key that will be reused
  35 + # on subsequent API calls.
  36 + def self.convert_to_syspro_object(data, opts = {})
  37 + case data
  38 + when Array
  39 + data.map { |i| convert_to_syspro_object(i, opts) }
  40 + when Hash
  41 + # Try converting to a known object class. If none available, fall back to generic SysproObject
  42 + object_classes.fetch(data[:object], SysproObject).construct_from(data, opts)
  43 + else
  44 + data
  45 + end
  46 + end
  47 +
  48 +
  49 + # The secondary opts argument can either be a string or hash
  50 + # Turn this value into an api_key and a set of headers
  51 + def self.normalize_opts(opts)
  52 + case opts
  53 + when String
  54 + opts
  55 + when Hash
  56 + opts.clone
  57 + else
  58 + raise TypeError, "normalize_opts expects a string or a hash"
  59 + end
  60 + end
  61 +
  62 + # Normalizes header keys so that they're all lower case and each
  63 + # hyphen-delimited section starts with a single capitalized letter. For
  64 + # example, `request-id` becomes `Request-Id`. This is useful for extracting
  65 + # certain key values when the user could have set them with a variety of
  66 + # diffent naming schemes.
  67 + def self.normalize_headers(headers)
  68 + headers.each_with_object({}) do |(k, v), new_headers|
  69 + if k.is_a?(Symbol)
  70 + k = titlecase_parts(k.to_s.tr("_", "-"))
  71 + elsif k.is_a?(String)
  72 + k = titlecase_parts(k)
  73 + end
  74 +
  75 + new_headers[k] = v
  76 + end
  77 + end
  78 +
  79 + def self.encode_parameters(params)
  80 + Util.flatten_params(params)
  81 + .map { |k, v| "#{url_encode(k)}=#{url_encode(v)}" }.join("&")
  82 + end
  83 +
  84 + def self.flatten_params(params, parent_key = nil)
  85 + result = []
  86 +
  87 + # do not sort the final output because arrays (and arrays of hashes
  88 + # especially) can be order sensitive, but do sort incoming parameters
  89 + params.each do |key, value|
  90 + calculated_key = parent_key ? "#{parent_key}[#{key}]" : key.to_s
  91 + if value.is_a?(Hash)
  92 + result += flatten_params(value, calculated_key)
  93 + elsif value.is_a?(Array)
  94 + check_array_of_maps_start_keys!(value)
  95 + result += flatten_params_array(value, calculated_key)
  96 + else
  97 + result << [calculated_key, value]
  98 + end
  99 + end
  100 +
  101 + result
  102 + end
  103 +
  104 + def self.log_error(message, data = {})
  105 + if !Syspro.logger.nil? ||
  106 + !Syspro.log_level.nil? && Syspro.log_level <= Syspro::LEVEL_ERROR
  107 + log_internal(message, data, color: :cyan,
  108 + level: Syspro::LEVEL_ERROR, logger: Syspro.logger, out: $stderr)
  109 + end
  110 + end
  111 +
  112 + def self.log_info(message, data = {})
  113 + if !Syspro.logger.nil? ||
  114 + !Syspro.log_level.nil? && Syspro.log_level <= Syspro::LEVEL_INFO
  115 + log_internal(message, data, color: :cyan,
  116 + level: Syspro::LEVEL_INFO, logger: Syspro.logger, out: $stdout)
  117 + end
  118 + end
  119 +
  120 + def self.log_debug(message, data = {})
  121 + if !Syspro.logger.nil? ||
  122 + !Syspro.log_level.nil? && Syspro.log_level <= Syspro::LEVEL_DEBUG
  123 + log_internal(message, data, color: :blue,
  124 + level: Syspro::LEVEL_DEBUG, logger: Syspro.logger, out: $stdout)
  125 + end
  126 + end
  127 +
  128 + end
  129 +end
  130 +
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec| @@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30 spec.add_dependency("faraday", "~> 0.10") 30 spec.add_dependency("faraday", "~> 0.10")
31 31
32 spec.add_development_dependency "bundler", "~> 1.16" 32 spec.add_development_dependency "bundler", "~> 1.16"
  33 + spec.add_development_dependency "pry", "~> 0.11"
33 spec.add_development_dependency "rake", "~> 10.0" 34 spec.add_development_dependency "rake", "~> 10.0"
34 spec.add_development_dependency "minitest", "~> 5.0" 35 spec.add_development_dependency "minitest", "~> 5.0"
35 end 36 end
test/client_test.rb
@@ -3,6 +3,6 @@ require &quot;test_helper&quot; @@ -3,6 +3,6 @@ require &quot;test_helper&quot;
3 class SysproClientTest < Minitest::Test 3 class SysproClientTest < Minitest::Test
4 def test_get_syspro_version 4 def test_get_syspro_version
5 client = ::Syspro::SysproClient.new 5 client = ::Syspro::SysproClient.new
6 - refute_nil client.get_syspro_version 6 + assert_match /(\d+\.)?(\d+\.)?(\d+\.)?(\d+)/, client.get_syspro_version[0].http_body
7 end 7 end
8 end 8 end
test/test_helper.rb
1 $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 1 $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2 require "syspro" 2 require "syspro"
3 3
  4 +require "pry"
4 require "minitest/autorun" 5 require "minitest/autorun"