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.
- checksums.yaml +7 -0
- data/.gitattributes +4 -0
- data/.gitignore +7 -0
- data/.rubocop.yml +20 -0
- data/.rubocop_todo.yml +60 -0
- data/CHANGELOG.md +5 -0
- data/CONTRIBUTORS +1 -0
- data/Gemfile +39 -0
- data/LICENSE +22 -0
- data/README.md +154 -0
- data/Rakefile +34 -0
- data/VERSION +1 -0
- data/bin/helio-console +14 -0
- data/helio-ruby.gemspec +22 -0
- data/lib/data/ca-certificates.crt +4043 -0
- data/lib/helio-ruby.rb +185 -0
- data/lib/helio/api_operations/create.rb +10 -0
- data/lib/helio/api_operations/delete.rb +11 -0
- data/lib/helio/api_operations/list.rb +28 -0
- data/lib/helio/api_operations/nested_resource.rb +61 -0
- data/lib/helio/api_operations/request.rb +55 -0
- data/lib/helio/api_operations/save.rb +85 -0
- data/lib/helio/api_resource.rb +67 -0
- data/lib/helio/customer_list.rb +25 -0
- data/lib/helio/errors.rb +109 -0
- data/lib/helio/helio_client.rb +542 -0
- data/lib/helio/helio_object.rb +475 -0
- data/lib/helio/helio_response.rb +48 -0
- data/lib/helio/list_object.rb +103 -0
- data/lib/helio/participant.rb +9 -0
- data/lib/helio/singleton_api_resource.rb +20 -0
- data/lib/helio/util.rb +401 -0
- data/lib/helio/version.rb +3 -0
- metadata +91 -0
data/lib/helio-ruby.rb
ADDED
@@ -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,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
|