helio-ruby 0.1.0

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,185 @@
1
+ # Helio Ruby bindings
2
+ # API spec at https://helio.zurb.com
3
+ require "cgi"
4
+ require "faraday"
5
+ require "json"
6
+ require "logger"
7
+ require "openssl"
8
+ require "rbconfig"
9
+ require "securerandom"
10
+ require "set"
11
+ require "socket"
12
+ require "uri"
13
+
14
+ # Version
15
+ require "helio/version"
16
+
17
+ # API operations
18
+ require "helio/api_operations/create"
19
+ require "helio/api_operations/delete"
20
+ require "helio/api_operations/list"
21
+ require "helio/api_operations/nested_resource"
22
+ require "helio/api_operations/request"
23
+ require "helio/api_operations/save"
24
+
25
+ # API resource support classes
26
+ require "helio/errors"
27
+ require "helio/util"
28
+ require "helio/helio_client"
29
+ require "helio/helio_object"
30
+ require "helio/helio_response"
31
+ require "helio/list_object"
32
+ require "helio/api_resource"
33
+ require "helio/singleton_api_resource"
34
+
35
+ # Named API resources
36
+ require "helio/customer_list"
37
+ require "helio/participant"
38
+
39
+ module Helio
40
+ DEFAULT_CA_BUNDLE_PATH = File.dirname(__FILE__) + "/data/ca-certificates.crt"
41
+
42
+ @app_info = nil
43
+
44
+ @api_base = "http://api.helio.test/public"
45
+
46
+ @log_level = nil
47
+ @logger = nil
48
+
49
+ @max_network_retries = 0
50
+ @max_network_retry_delay = 2
51
+ @initial_network_retry_delay = 0.5
52
+
53
+ @ca_bundle_path = DEFAULT_CA_BUNDLE_PATH
54
+ @ca_store = nil
55
+ @verify_ssl_certs = true
56
+
57
+ @open_timeout = 30
58
+ @read_timeout = 80
59
+
60
+ class << self
61
+ attr_accessor :api_id, :api_token, :api_base, :verify_ssl_certs, :api_version, :client_id,
62
+ :open_timeout, :read_timeout
63
+
64
+ attr_reader :max_network_retry_delay, :initial_network_retry_delay
65
+ end
66
+
67
+ # Gets the application for a plugin that's identified some. See
68
+ # #set_app_info.
69
+ def self.app_info
70
+ @app_info
71
+ end
72
+
73
+ def self.app_info=(info)
74
+ @app_info = info
75
+ end
76
+
77
+ # The location of a file containing a bundle of CA certificates. By default
78
+ # the library will use an included bundle that can successfully validate
79
+ # Helio certificates.
80
+ def self.ca_bundle_path
81
+ @ca_bundle_path
82
+ end
83
+
84
+ def self.ca_bundle_path=(path)
85
+ @ca_bundle_path = path
86
+
87
+ # empty this field so a new store is initialized
88
+ @ca_store = nil
89
+ end
90
+
91
+ # A certificate store initialized from the the bundle in #ca_bundle_path and
92
+ # which is used to validate TLS on every request.
93
+ #
94
+ # This was added to the give the gem "pseudo thread safety" in that it seems
95
+ # when initiating many parallel requests marshaling the certificate store is
96
+ # the most likely point of failure (see issue #382). Any program attempting
97
+ # to leverage this pseudo safety should make a call to this method (i.e.
98
+ # `Helio.ca_store`) in their initialization code because it marshals lazily
99
+ # and is itself not thread safe.
100
+ def self.ca_store
101
+ @ca_store ||= begin
102
+ store = OpenSSL::X509::Store.new
103
+ store.add_file(ca_bundle_path)
104
+ store
105
+ end
106
+ end
107
+
108
+ # map to the same values as the standard library's logger
109
+ LEVEL_DEBUG = Logger::DEBUG
110
+ LEVEL_ERROR = Logger::ERROR
111
+ LEVEL_INFO = Logger::INFO
112
+
113
+ # When set prompts the library to log some extra information to $stdout and
114
+ # $stderr about what it's doing. For example, it'll produce information about
115
+ # requests, responses, and errors that are received. Valid log levels are
116
+ # `debug` and `info`, with `debug` being a little more verbose in places.
117
+ #
118
+ # Use of this configuration is only useful when `.logger` is _not_ set. When
119
+ # it is, the decision what levels to print is entirely deferred to the logger.
120
+ def self.log_level
121
+ @log_level
122
+ end
123
+
124
+ def self.log_level=(val)
125
+ # Backwards compatibility for values that we briefly allowed
126
+ if val == "debug"
127
+ val = LEVEL_DEBUG
128
+ elsif val == "info"
129
+ val = LEVEL_INFO
130
+ end
131
+
132
+ if !val.nil? && ![LEVEL_DEBUG, LEVEL_ERROR, LEVEL_INFO].include?(val)
133
+ raise ArgumentError, "log_level should only be set to `nil`, `debug` or `info`"
134
+ end
135
+ @log_level = val
136
+ end
137
+
138
+ # Sets a logger to which logging output will be sent. The logger should
139
+ # support the same interface as the `Logger` class that's part of Ruby's
140
+ # standard library (hint, anything in `Rails.logger` will likely be
141
+ # suitable).
142
+ #
143
+ # If `.logger` is set, the value of `.log_level` is ignored. The decision on
144
+ # what levels to print is entirely deferred to the logger.
145
+ def self.logger
146
+ @logger
147
+ end
148
+
149
+ def self.logger=(val)
150
+ @logger = val
151
+ end
152
+
153
+ def self.max_network_retries
154
+ @max_network_retries
155
+ end
156
+
157
+ def self.max_network_retries=(val)
158
+ @max_network_retries = val.to_i
159
+ end
160
+
161
+ # Sets some basic information about the running application that's sent along
162
+ # with API requests. Useful for plugin authors to identify their plugin when
163
+ # communicating with Helio.
164
+ #
165
+ # Takes a name and optional version and plugin URL.
166
+ def self.set_app_info(name, version: nil, url: nil)
167
+ @app_info = {
168
+ name: name,
169
+ url: url,
170
+ version: version,
171
+ }
172
+ end
173
+
174
+ # DEPRECATED. Use `Util#encode_parameters` instead.
175
+ def self.uri_encode(params)
176
+ Util.encode_parameters(params)
177
+ end
178
+ private_class_method :uri_encode
179
+ class << self
180
+ extend Gem::Deprecate
181
+ deprecate :uri_encode, "Helio::Util#encode_parameters", 2016, 1
182
+ end
183
+ end
184
+
185
+ Helio.log_level = ENV["HELIO_LOG"] unless ENV["HELIO_LOG"].nil?
@@ -0,0 +1,10 @@
1
+ module Helio
2
+ module APIOperations
3
+ module Create
4
+ def create(params = {}, opts = {})
5
+ resp, opts = request(:post, resource_url, params, opts)
6
+ Util.convert_to_helio_object(resp.data, opts)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module Helio
2
+ module APIOperations
3
+ module Delete
4
+ def delete(params = {}, opts = {})
5
+ opts = Util.normalize_opts(opts)
6
+ resp, opts = request(:delete, resource_url, params, opts)
7
+ initialize_from(resp.data, opts)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ module Helio
2
+ module APIOperations
3
+ module List
4
+ def list(filters = {}, opts = {})
5
+ opts = Util.normalize_opts(opts)
6
+
7
+ resp, opts = request(:get, resource_url, filters, opts)
8
+ obj = ListObject.construct_from(resp.data, opts)
9
+
10
+ # set filters so that we can fetch the same limit, expansions, and
11
+ # predicates when accessing the next and previous pages
12
+ #
13
+ # just for general cleanliness, remove any paging options
14
+ obj.filters = filters.dup
15
+ obj.filters.delete(:ending_before)
16
+ obj.filters.delete(:starting_after)
17
+
18
+ obj
19
+ end
20
+
21
+ # The original version of #list was given the somewhat unfortunate name of
22
+ # #all, and this alias allows us to maintain backward compatibility (the
23
+ # choice was somewhat misleading in the way that it only returned a single
24
+ # page rather than all objects).
25
+ alias all list
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,61 @@
1
+ module Helio
2
+ module APIOperations
3
+ # Adds methods to help manipulate a subresource from its parent resource so
4
+ # that it's possible to do so from a static context (i.e. without a
5
+ # pre-existing collection of subresources on the parent).
6
+ #
7
+ # For examle, a transfer gains the static methods for reversals so that the
8
+ # methods `.create_reversal`, `.retrieve_reversal`, `.update_reversal`,
9
+ # etc. all become available.
10
+ module NestedResource
11
+ def nested_resource_class_methods(resource, path: nil, operations: nil)
12
+ path ||= "#{resource}s"
13
+ raise ArgumentError, "operations array required" if operations.nil?
14
+
15
+ resource_url_method = :"#{resource}s_url"
16
+ define_singleton_method(resource_url_method) do |id, nested_id = nil|
17
+ url = "#{resource_url}/#{CGI.escape(id)}/#{CGI.escape(path)}"
18
+ url += "/#{CGI.escape(nested_id)}" unless nested_id.nil?
19
+ url
20
+ end
21
+
22
+ operations.each do |operation|
23
+ case operation
24
+ when :create
25
+ define_singleton_method(:"create_#{resource}") do |id, params = {}, opts = {}|
26
+ url = send(resource_url_method, id)
27
+ resp, opts = request(:post, url, params, opts)
28
+ Util.convert_to_helio_object(resp.data, opts)
29
+ end
30
+ when :retrieve
31
+ define_singleton_method(:"retrieve_#{resource}") do |id, nested_id, opts = {}|
32
+ url = send(resource_url_method, id, nested_id)
33
+ resp, opts = request(:get, url, {}, opts)
34
+ Util.convert_to_helio_object(resp.data, opts)
35
+ end
36
+ when :update
37
+ define_singleton_method(:"update_#{resource}") do |id, nested_id, params = {}, opts = {}|
38
+ url = send(resource_url_method, id, nested_id)
39
+ resp, opts = request(:post, url, params, opts)
40
+ Util.convert_to_helio_object(resp.data, opts)
41
+ end
42
+ when :delete
43
+ define_singleton_method(:"delete_#{resource}") do |id, nested_id, params = {}, opts = {}|
44
+ url = send(resource_url_method, id, nested_id)
45
+ resp, opts = request(:delete, url, params, opts)
46
+ Util.convert_to_helio_object(resp.data, opts)
47
+ end
48
+ when :list
49
+ define_singleton_method(:"list_#{resource}s") do |id, params = {}, opts = {}|
50
+ url = send(resource_url_method, id)
51
+ resp, opts = request(:get, url, params, opts)
52
+ Util.convert_to_helio_object(resp.data, opts)
53
+ end
54
+ else
55
+ raise ArgumentError, "Unknown operation: #{operation.inspect}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,55 @@
1
+ module Helio
2
+ module APIOperations
3
+ module Request
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] ||= HelioClient.active_client
10
+
11
+ headers = opts.clone
12
+ api_token = headers.delete(:api_token)
13
+ api_base = headers.delete(:api_base)
14
+ client = headers.delete(:client)
15
+ # Assume all remaining opts must be headers
16
+
17
+ resp, opts[:api_token] = client.execute_request(
18
+ method, url,
19
+ api_base: api_base, api_token: api_token,
20
+ headers: headers, params: params
21
+ )
22
+
23
+ # Hash#select returns an array before 1.9
24
+ opts_to_persist = {}
25
+ opts.each do |k, v|
26
+ opts_to_persist[k] = v if Util::OPTS_PERSISTABLE.include?(k)
27
+ end
28
+
29
+ [resp, opts_to_persist]
30
+ end
31
+
32
+ private
33
+
34
+ def warn_on_opts_in_params(params)
35
+ Util::OPTS_USER_SPECIFIED.each do |opt|
36
+ if params.key?(opt)
37
+ $stderr.puts("WARNING: #{opt} should be in opts instead of params.")
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def self.included(base)
44
+ base.extend(ClassMethods)
45
+ end
46
+
47
+ protected
48
+
49
+ def request(method, url, params = {}, opts = {})
50
+ opts = @opts.merge(Util.normalize_opts(opts))
51
+ self.class.request(method, url, params, opts)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,85 @@
1
+ module Helio
2
+ module APIOperations
3
+ module Save
4
+ module ClassMethods
5
+ # Updates an API resource
6
+ #
7
+ # Updates the identified resource with the passed in parameters.
8
+ #
9
+ # ==== Attributes
10
+ #
11
+ # * +id+ - ID of the resource to update.
12
+ # * +params+ - A hash of parameters to pass to the API
13
+ # * +opts+ - A Hash of additional options (separate from the params /
14
+ # object values) to be added to the request. E.g. to allow for an
15
+ # idempotency_key to be passed in the request headers, or for the
16
+ # api_token to be overwritten. See {APIOperations::Request.request}.
17
+ def update(id, params = {}, opts = {})
18
+ params.each_key do |k|
19
+ if protected_fields.include?(k)
20
+ raise ArgumentError, "Cannot update protected field: #{k}"
21
+ end
22
+ end
23
+
24
+ resp, opts = request(:post, "#{resource_url}/#{id}", params, opts)
25
+ Util.convert_to_helio_object(resp.data, opts)
26
+ end
27
+ end
28
+
29
+ # Creates or updates an API resource.
30
+ #
31
+ # If the resource doesn't yet have an assigned ID and the resource is one
32
+ # that can be created, then the method attempts to create the resource.
33
+ # The resource is updated otherwise.
34
+ #
35
+ # ==== Attributes
36
+ #
37
+ # * +params+ - Overrides any parameters in the resource's serialized data
38
+ # and includes them in the create or update. If +:req_url:+ is included
39
+ # in the list, it overrides the update URL used for the create or
40
+ # update.
41
+ # * +opts+ - A Hash of additional options (separate from the params /
42
+ # object values) to be added to the request. E.g. to allow for an
43
+ # idempotency_key to be passed in the request headers, or for the
44
+ # api_token to be overwritten. See {APIOperations::Request.request}.
45
+ def save(params = {}, opts = {})
46
+ # We started unintentionally (sort of) allowing attributes sent to
47
+ # +save+ to override values used during the update. So as not to break
48
+ # the API, this makes that official here.
49
+ update_attributes(params)
50
+
51
+ # Now remove any parameters that look like object attributes.
52
+ params = params.reject { |k, _| respond_to?(k) }
53
+
54
+ values = serialize_params(self).merge(params)
55
+
56
+ # note that id gets removed here our call to #url above has already
57
+ # generated a uri for this object with an identifier baked in
58
+ values.delete(:id)
59
+
60
+ resp, opts = request(:post, save_url, values, opts)
61
+ initialize_from(resp.data, opts)
62
+ end
63
+
64
+ def self.included(base)
65
+ base.extend(ClassMethods)
66
+ end
67
+
68
+ private
69
+
70
+ def save_url
71
+ # This switch essentially allows us "upsert"-like functionality. If the
72
+ # API resource doesn't have an ID set (suggesting that it's new) and
73
+ # its class responds to .create (which comes from
74
+ # Helio::APIOperations::Create), then use the URL to create a new
75
+ # resource. Otherwise, generate a URL based on the object's identifier
76
+ # for a normal update.
77
+ if self[:id].nil? && self.class.respond_to?(:create)
78
+ self.class.resource_url
79
+ else
80
+ resource_url
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,67 @@
1
+ module Helio
2
+ class APIResource < HelioObject
3
+ include Helio::APIOperations::Request
4
+
5
+ # A flag that can be set a behavior that will cause this resource to be
6
+ # encoded and sent up along with an update of its parent resource. This is
7
+ # usually not desirable because resources are updated individually on their
8
+ # own endpoints, but there are certain cases, replacing a customer's source
9
+ # for example, where this is allowed.
10
+ attr_accessor :save_with_parent
11
+
12
+ def self.class_name
13
+ name.split("::")[-1]
14
+ end
15
+
16
+ def self.resource_url
17
+ if self == APIResource
18
+ raise NotImplementedError, "APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)"
19
+ end
20
+ resource_path = class_name.gsub(/([^\^])([A-Z])/,'\1_\2').downcase
21
+ "/#{CGI.escape(resource_path)}s"
22
+ end
23
+
24
+ # A metaprogramming call that specifies that a field of a resource can be
25
+ # its own type of API resource (say a nested card under an account for
26
+ # example), and if that resource is set, it should be transmitted to the
27
+ # API on a create or update. Doing so is not the default behavior because
28
+ # API resources should normally be persisted on their own RESTful
29
+ # endpoints.
30
+ def self.save_nested_resource(name)
31
+ define_method(:"#{name}=") do |value|
32
+ super(value)
33
+
34
+ # The parent setter will perform certain useful operations like
35
+ # converting to an APIResource if appropriate. Refresh our argument
36
+ # value to whatever it mutated to.
37
+ value = send(name)
38
+
39
+ # Note that the value may be subresource, but could also be a scalar
40
+ # (like a tokenized card's ID for example), so we check the type before
41
+ # setting #save_with_parent here.
42
+ value.save_with_parent = true if value.is_a?(APIResource)
43
+
44
+ value
45
+ end
46
+ end
47
+
48
+ def resource_url
49
+ unless (id = self["id"])
50
+ raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", "id")
51
+ end
52
+ "#{self.class.resource_url}/#{CGI.escape(id)}"
53
+ end
54
+
55
+ def refresh
56
+ resp, opts = request(:get, resource_url, @retrieve_params)
57
+ initialize_from(resp.data, opts)
58
+ end
59
+
60
+ def self.retrieve(id, opts = {})
61
+ opts = Util.normalize_opts(opts)
62
+ instance = new(id, opts)
63
+ instance.refresh
64
+ instance
65
+ end
66
+ end
67
+ end