mirakl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirakl
4
+ class MiraklObject
5
+ include Enumerable
6
+
7
+ # def initialize(id = nil, opts = {})
8
+ # id, @retrieve_params = Util.normalize_id(id)
9
+ # @opts = Util.normalize_opts(opts)
10
+ # @original_values = {}
11
+ # @values = {}
12
+ # # This really belongs in APIResource, but not putting it there allows us
13
+ # # to have a unified inspect method
14
+ # @unsaved_values = Set.new
15
+ # @transient_values = Set.new
16
+ # @values[:id] = id if id
17
+ # end
18
+
19
+ def initialize
20
+ @values = {}
21
+ end
22
+
23
+ def data
24
+ @values
25
+ end
26
+
27
+
28
+
29
+ def self.construct_from(values, opts = {})
30
+ values = Mirakl::Util.symbolize_names(values)
31
+
32
+ # work around protected #initialize_from for now
33
+ new().send(:initialize_from, values, opts)
34
+ end
35
+
36
+
37
+ protected def initialize_from(values, opts, partial = false)
38
+ @opts = Util.normalize_opts(opts)
39
+
40
+ values.each do |k, v|
41
+ @values[k] = Util.convert_to_mirakl_object(v, @opts)
42
+ end
43
+
44
+ self
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirakl
4
+ # MiraklResponse encapsulates some vitals of a response that came back from
5
+ # the Mirakl API.
6
+ class MiraklResponse
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 Stripe request ID of the response.
21
+ attr_accessor :request_id
22
+
23
+ # Initializes a MiraklResponse 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
+
29
+ puts http_resp.inspect
30
+
31
+ resp = MiraklResponse.new
32
+ resp.data = JSON.parse(http_resp[:body], symbolize_names: true)
33
+ resp.http_body = http_resp[:body]
34
+ resp.http_headers = http_resp[:headers]
35
+ resp.http_status = http_resp[:status]
36
+ resp
37
+ end
38
+
39
+ # Initializes a MiraklResponse object from a Faraday HTTP response object.
40
+ #
41
+ # This may throw JSON::ParserError if the response body is not valid JSON.
42
+ def self.from_faraday_response(http_resp)
43
+ resp = MiraklResponse.new
44
+ resp.data = JSON.parse(http_resp.body, symbolize_names: true)
45
+ resp.http_body = http_resp.body
46
+ resp.http_headers = http_resp.headers
47
+ resp.http_status = http_resp.status
48
+ resp
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirakl
4
+ module Requests
5
+ class DR11
6
+ include Mirakl::ApiOperations::Request
7
+
8
+ API_PATH = 'document-request/requests'
9
+
10
+ def self.call(params = {}, opts = {})
11
+ resp, opts = request(:get, API_PATH, params, opts)
12
+ obj = MiraklObject.construct_from(resp.data, opts)
13
+
14
+ obj
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirakl
4
+ module Requests
5
+ class DR74
6
+ include Mirakl::ApiOperations::Request
7
+
8
+ API_PATH = 'document-request/documents/upload'
9
+
10
+ def self.call(params = {}, opts = {})
11
+
12
+ raise ArgumentError, "files must be an array" if params[:files].nil? ||
13
+ !params[:files].is_a?(Array)
14
+
15
+ params[:files] = params[:files].map do |fp|
16
+ unless fp.respond_to?(:read)
17
+ raise ArgumentError, "each file must respond to `#read`"
18
+ end
19
+
20
+ Faraday::FilePart.new(fp, 'application/pdf', File.basename(fp))
21
+ end
22
+
23
+ # params[:files] = params[:files][0]
24
+
25
+ raise ArgumentError, "documents_input must be a Hash" if params[:documents_input].nil? ||
26
+ !params[:documents_input].is_a?(Hash)
27
+
28
+ params[:documents_input] = Faraday::ParamPart.new(params[:documents_input].to_json, 'application/json')
29
+
30
+ puts params.inspect
31
+
32
+ opts = {
33
+ content_type: "multipart/form-data",
34
+ }.merge(Util.normalize_opts(opts))
35
+
36
+ resp, opts = request(:post, API_PATH, params, opts)
37
+ obj = MiraklObject.construct_from(resp.data, opts)
38
+
39
+ obj
40
+ end
41
+
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirakl
4
+ module Requests
5
+ class S07
6
+ include Mirakl::ApiOperations::Request
7
+
8
+ API_PATH = 'shops'
9
+
10
+ def self.call(params = {}, opts = {})
11
+ resp, opts = request(:put, API_PATH, params, opts)
12
+ obj = MiraklObject.construct_from(resp.data, opts)
13
+
14
+ obj
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirakl
4
+ module Requests
5
+ class S20
6
+ include Mirakl::ApiOperations::Request
7
+
8
+ API_PATH = 'shops'
9
+
10
+ def self.call(params = {}, opts = {})
11
+ resp, opts = request(:get, API_PATH, params, opts)
12
+ obj = MiraklObject.construct_from(resp.data, opts)
13
+
14
+ obj
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Mirakl
6
+ module Util
7
+
8
+ # Options that a user is allowed to specify.
9
+ OPTS_USER_SPECIFIED = Set[
10
+ :api_key,
11
+ ].freeze
12
+
13
+ # Options that should be copyable from one StripeObject to another
14
+ # including options that may be internal.
15
+ OPTS_COPYABLE = (
16
+ OPTS_USER_SPECIFIED + Set[:api_base]
17
+ ).freeze
18
+
19
+ # Options that should be persisted between API requests. This includes
20
+ # client, which is an object containing an HTTP client to reuse.
21
+ OPTS_PERSISTABLE = (
22
+ OPTS_USER_SPECIFIED + Set[:client]
23
+ ).freeze
24
+
25
+ # Converts a hash of fields or an array of hashes into a +MiraklObject+ or
26
+ # array of +MiraklObject+s.
27
+ #
28
+ # ==== Attributes
29
+ #
30
+ # * +data+ - Hash of fields and values to be converted into a MiraklObject.
31
+ # * +opts+ - Options for +MiraklObject+ like an API key that will be reused
32
+ # on subsequent API calls.
33
+ def self.convert_to_mirakl_object(data, opts = {})
34
+ opts = normalize_opts(opts)
35
+
36
+ case data
37
+ when Array
38
+ data.map { |i| convert_to_mirakl_object(i, opts) }
39
+ when Hash
40
+ data
41
+ # Try converting to a known object class. If none available, fall back
42
+ # to generic StripeObject
43
+ # MiraklObject.construct_from(data, opts)
44
+ else
45
+ data
46
+ end
47
+ end
48
+
49
+
50
+ def self.log_error(message, data = {})
51
+ if !Mirakl.logger.nil? ||
52
+ !Mirakl.log_level.nil? && Mirakl.log_level <= Mirakl::LEVEL_ERROR
53
+ log_internal(message, data, color: :cyan, level: Mirakl::LEVEL_ERROR,
54
+ logger: Mirakl.logger, out: $stderr)
55
+ end
56
+ end
57
+
58
+ def self.log_info(message, data = {})
59
+ if !Mirakl.logger.nil? ||
60
+ !Mirakl.log_level.nil? && Mirakl.log_level <= Mirakl::LEVEL_INFO
61
+ log_internal(message, data, color: :cyan, level: Mirakl::LEVEL_INFO,
62
+ logger: Mirakl.logger, out: $stdout)
63
+ end
64
+ end
65
+
66
+ def self.log_debug(message, data = {})
67
+ if !Mirakl.logger.nil? ||
68
+ !Mirakl.log_level.nil? && Mirakl.log_level <= Mirakl::LEVEL_DEBUG
69
+ log_internal(message, data, color: :blue, level: Mirakl::LEVEL_DEBUG,
70
+ logger: Mirakl.logger, out: $stdout)
71
+ end
72
+ end
73
+
74
+ def self.symbolize_names(object)
75
+ case object
76
+ when Hash
77
+ new_hash = {}
78
+ object.each do |key, value|
79
+ key = (begin
80
+ key.to_sym
81
+ rescue StandardError
82
+ key
83
+ end) || key
84
+ new_hash[key] = symbolize_names(value)
85
+ end
86
+ new_hash
87
+ when Array
88
+ object.map { |value| symbolize_names(value) }
89
+ else
90
+ object
91
+ end
92
+ end
93
+
94
+ # Encodes a hash of parameters in a way that's suitable for use as query
95
+ # parameters in a URI or as form parameters in a request body. This mainly
96
+ # involves escaping special characters from parameter keys and values (e.g.
97
+ # `&`).
98
+ def self.encode_parameters(params)
99
+ Util.flatten_params(params)
100
+ .map { |k, v| "#{url_encode(k)}=#{url_encode(v)}" }.join("&")
101
+ end
102
+
103
+ # Encodes a string in a way that makes it suitable for use in a set of
104
+ # query parameters in a URI or in a set of form parameters in a request
105
+ # body.
106
+ def self.url_encode(key)
107
+ CGI.escape(key.to_s).
108
+ # Don't use strict form encoding by changing the square bracket control
109
+ # characters back to their literals. This is fine by the server, and
110
+ # makes these parameter strings easier to read.
111
+ gsub("%5B", "[").gsub("%5D", "]")
112
+ end
113
+
114
+ def self.flatten_params(params, parent_key = nil)
115
+ result = []
116
+
117
+ # do not sort the final output because arrays (and arrays of hashes
118
+ # especially) can be order sensitive, but do sort incoming parameters
119
+ params.each do |key, value|
120
+ calculated_key = parent_key ? "#{parent_key}[#{key}]" : key.to_s
121
+ if value.is_a?(Hash)
122
+ result += flatten_params(value, calculated_key)
123
+ elsif value.is_a?(Array)
124
+ result += flatten_params_array(value, calculated_key)
125
+ else
126
+ result << [calculated_key, value]
127
+ end
128
+ end
129
+
130
+ result
131
+ end
132
+
133
+ def self.flatten_params_array(value, calculated_key)
134
+ result = []
135
+ value.each_with_index do |elem, i|
136
+ if elem.is_a?(Hash)
137
+ result += flatten_params(elem, "#{calculated_key}[#{i}]")
138
+ elsif elem.is_a?(Array)
139
+ result += flatten_params_array(elem, calculated_key)
140
+ else
141
+ result << ["#{calculated_key}[#{i}]", elem]
142
+ end
143
+ end
144
+ result
145
+ end
146
+
147
+ # The secondary opts argument can either be a string or hash
148
+ # Turn this value into an api_key and a set of headers
149
+ def self.normalize_opts(opts)
150
+ case opts
151
+ when String
152
+ { api_key: opts }
153
+ when Hash
154
+ check_api_key!(opts.fetch(:api_key)) if opts.key?(:api_key)
155
+ opts.clone
156
+ else
157
+ raise TypeError, "normalize_opts expects a string or a hash"
158
+ end
159
+ end
160
+
161
+ def self.check_string_argument!(key)
162
+ raise TypeError, "argument must be a string" unless key.is_a?(String)
163
+ key
164
+ end
165
+
166
+ def self.check_api_key!(key)
167
+ raise TypeError, "api_key must be a string" unless key.is_a?(String)
168
+ key
169
+ end
170
+
171
+ # Normalizes header keys so that they're all lower case and each
172
+ # hyphen-delimited section starts with a single capitalized letter. For
173
+ # example, `request-id` becomes `Request-Id`. This is useful for extracting
174
+ # certain key values when the user could have set them with a variety of
175
+ # diffent naming schemes.
176
+ def self.normalize_headers(headers)
177
+ headers.each_with_object({}) do |(k, v), new_headers|
178
+ k = k.to_s.tr("_", "-") if k.is_a?(Symbol)
179
+ k = k.split("-").reject(&:empty?).map(&:capitalize).join("-")
180
+
181
+ new_headers[k] = v
182
+ end
183
+ end
184
+
185
+ #
186
+ # private
187
+ #
188
+
189
+ COLOR_CODES = {
190
+ black: 0, light_black: 60,
191
+ red: 1, light_red: 61,
192
+ green: 2, light_green: 62,
193
+ yellow: 3, light_yellow: 63,
194
+ blue: 4, light_blue: 64,
195
+ magenta: 5, light_magenta: 65,
196
+ cyan: 6, light_cyan: 66,
197
+ white: 7, light_white: 67,
198
+ default: 9,
199
+ }.freeze
200
+ private_constant :COLOR_CODES
201
+
202
+ # Uses an ANSI escape code to colorize text if it's going to be sent to a
203
+ # TTY.
204
+ def self.colorize(val, color, isatty)
205
+ return val unless isatty
206
+
207
+ mode = 0 # default
208
+ foreground = 30 + COLOR_CODES.fetch(color)
209
+ background = 40 + COLOR_CODES.fetch(:default)
210
+
211
+ "\033[#{mode};#{foreground};#{background}m#{val}\033[0m"
212
+ end
213
+ private_class_method :colorize
214
+
215
+ # Turns an integer log level into a printable name.
216
+ def self.level_name(level)
217
+ case level
218
+ when LEVEL_DEBUG then "debug"
219
+ when LEVEL_ERROR then "error"
220
+ when LEVEL_INFO then "info"
221
+ else level
222
+ end
223
+ end
224
+ private_class_method :level_name
225
+
226
+ # TODO: Make these named required arguments when we drop support for Ruby
227
+ # 2.0.
228
+ def self.log_internal(message, data = {}, color: nil, level: nil,
229
+ logger: nil, out: nil)
230
+ data_str = data.reject { |_k, v| v.nil? }
231
+ .map do |(k, v)|
232
+ format("%<key>s=%<value>s",
233
+ key: colorize(k, color, logger.nil? && !out.nil? && out.isatty),
234
+ value: wrap_logfmt_value(v))
235
+ end.join(" ")
236
+
237
+ if !logger.nil?
238
+ # the library's log levels are mapped to the same values as the
239
+ # standard library's logger
240
+ logger.log(level,
241
+ format("message=%<message>s %<data_str>s",
242
+ message: wrap_logfmt_value(message),
243
+ data_str: data_str))
244
+ elsif out.isatty
245
+ out.puts format("%<level>s %<message>s %<data_str>s",
246
+ level: colorize(level_name(level)[0, 4].upcase,
247
+ color, out.isatty),
248
+ message: message,
249
+ data_str: data_str)
250
+ else
251
+ out.puts format("message=%<message>s level=%<level>s %<data_str>s",
252
+ message: wrap_logfmt_value(message),
253
+ level: level_name(level),
254
+ data_str: data_str)
255
+ end
256
+ end
257
+ private_class_method :log_internal
258
+
259
+ # Wraps a value in double quotes if it looks sufficiently complex so that
260
+ # it can be read by logfmt parsers.
261
+ def self.wrap_logfmt_value(val)
262
+ # If value is any kind of number, just allow it to be formatted directly
263
+ # to a string (this will handle integers or floats).
264
+ return val if val.is_a?(Numeric)
265
+
266
+ # Hopefully val is a string, but protect in case it's not.
267
+ val = val.to_s
268
+
269
+ if %r{[^\w\-/]} =~ val
270
+ # If the string contains any special characters, escape any double
271
+ # quotes it has, remove newlines, and wrap the whole thing in quotes.
272
+ format(%("%<value>s"), value: val.gsub('"', '\"').delete("\n"))
273
+ else
274
+ # Otherwise use the basic value if it looks like a standard set of
275
+ # characters (and allow a few special characters like hyphens, and
276
+ # slashes)
277
+ val
278
+ end
279
+ end
280
+ private_class_method :wrap_logfmt_value
281
+ end
282
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirakl
4
+ VERSION = "0.0.1".freeze
5
+ end
data/lib/mirakl.rb ADDED
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mirakl Ruby bindings
4
+ require "cgi"
5
+ require "faraday"
6
+ require "json"
7
+ require "logger"
8
+ require "rbconfig"
9
+ require "set"
10
+ require "socket"
11
+ require "uri"
12
+
13
+ require "mirakl/version"
14
+
15
+ # API operations
16
+ require "mirakl/api_operations/request"
17
+
18
+ # Resources
19
+ require "mirakl/requests/dr11"
20
+ require "mirakl/requests/dr74"
21
+ require "mirakl/requests/s07"
22
+ require "mirakl/requests/s20"
23
+
24
+ # API resource support classes
25
+ require "mirakl/util"
26
+ require "mirakl/errors"
27
+ require "mirakl/mirakl_response"
28
+ require "mirakl/mirakl_client"
29
+ require "mirakl/mirakl_object"
30
+
31
+
32
+
33
+ module Mirakl
34
+ @api_base = 'https://octobat-dev.mirakl.net/api/'
35
+
36
+ @open_timeout = 30
37
+ @read_timeout = 80
38
+
39
+ @log_level = nil
40
+ @logger = nil
41
+
42
+ class << self
43
+ attr_accessor :api_key, :api_base, :open_timeout, :read_timeout
44
+ end
45
+
46
+ # map to the same values as the standard library's logger
47
+ LEVEL_DEBUG = Logger::DEBUG
48
+ LEVEL_ERROR = Logger::ERROR
49
+ LEVEL_INFO = Logger::INFO
50
+
51
+ # When set prompts the library to log some extra information to $stdout and
52
+ # $stderr about what it's doing. For example, it'll produce information about
53
+ # requests, responses, and errors that are received. Valid log levels are
54
+ # `debug` and `info`, with `debug` being a little more verbose in places.
55
+ #
56
+ # Use of this configuration is only useful when `.logger` is _not_ set. When
57
+ # it is, the decision what levels to print is entirely deferred to the logger.
58
+ def self.log_level
59
+ @log_level
60
+ end
61
+
62
+ def self.log_level=(val)
63
+ # Backwards compatibility for values that we briefly allowed
64
+ if val == "debug"
65
+ val = LEVEL_DEBUG
66
+ elsif val == "info"
67
+ val = LEVEL_INFO
68
+ end
69
+
70
+ if !val.nil? && ![LEVEL_DEBUG, LEVEL_ERROR, LEVEL_INFO].include?(val)
71
+ raise ArgumentError,
72
+ "log_level should only be set to `nil`, `debug` or `info`"
73
+ end
74
+ @log_level = val
75
+ end
76
+
77
+ # Sets a logger to which logging output will be sent. The logger should
78
+ # support the same interface as the `Logger` class that's part of Ruby's
79
+ # standard library (hint, anything in `Rails.logger` will likely be
80
+ # suitable).
81
+ #
82
+ # If `.logger` is set, the value of `.log_level` is ignored. The decision on
83
+ # what levels to print is entirely deferred to the logger.
84
+ def self.logger
85
+ @logger
86
+ end
87
+
88
+ def self.logger=(val)
89
+ @logger = val
90
+ end
91
+
92
+ end
93
+
94
+ Mirakl.log_level = ENV["MIRAKL_LOG"] unless ENV["MIRAKL_LOG"].nil?
data/mirakl.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(::File.join(::File.dirname(__FILE__), "lib"))
4
+
5
+ require "mirakl/version"
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "mirakl"
9
+ s.version = Mirakl::VERSION
10
+ s.required_ruby_version = ">= 2.1.0"
11
+ s.summary = "Ruby bindings for the Mirakl API"
12
+ s.description = ""
13
+ s.author = "Mirakl"
14
+ s.email = "gaultier.laperche@mirakl.com"
15
+ s.homepage = "https://www.mirakl.com/"
16
+ s.license = "MIT"
17
+
18
+ s.metadata = {
19
+ "bug_tracker_uri" => "https://github.com/mirakl/mirakl-ruby/issues",
20
+ "changelog_uri" =>
21
+ "https://github.com/mirakl/mirakl-ruby/blob/master/CHANGELOG.md",
22
+ "github_repo" => "ssh://github.com/mirakl/mirakl-ruby",
23
+ }
24
+
25
+ s.add_dependency("faraday", "~> 1.8")
26
+ s.add_dependency("net-http-persistent", "~> 3.0")
27
+
28
+ s.files = `git ls-files`.split("\n")
29
+ s.test_files = `git ls-files -- test/*`.split("\n")
30
+ s.executables = `git ls-files -- bin/*`.split("\n")
31
+ .map { |f| ::File.basename(f) }
32
+ s.require_paths = ["lib"]
33
+ end