mirakl 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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