helio-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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