unit-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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +15 -0
  5. data/.travis.yml +7 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +78 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +51 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/lib/unit-ruby/application_form.rb +14 -0
  15. data/lib/unit-ruby/atm_location.rb +41 -0
  16. data/lib/unit-ruby/deposit_account.rb +28 -0
  17. data/lib/unit-ruby/individual_application.rb +33 -0
  18. data/lib/unit-ruby/individual_customer.rb +28 -0
  19. data/lib/unit-ruby/individual_debit_card.rb +25 -0
  20. data/lib/unit-ruby/institution.rb +19 -0
  21. data/lib/unit-ruby/types/address.rb +32 -0
  22. data/lib/unit-ruby/types/application_form_prefill.rb +61 -0
  23. data/lib/unit-ruby/types/application_form_settings_override.rb +57 -0
  24. data/lib/unit-ruby/types/array.rb +21 -0
  25. data/lib/unit-ruby/types/boolean.rb +16 -0
  26. data/lib/unit-ruby/types/coordinates.rb +23 -0
  27. data/lib/unit-ruby/types/date.rb +11 -0
  28. data/lib/unit-ruby/types/date_time.rb +11 -0
  29. data/lib/unit-ruby/types/decimal.rb +9 -0
  30. data/lib/unit-ruby/types/float.rb +9 -0
  31. data/lib/unit-ruby/types/full_name.rb +23 -0
  32. data/lib/unit-ruby/types/hash.rb +21 -0
  33. data/lib/unit-ruby/types/integer.rb +9 -0
  34. data/lib/unit-ruby/types/phone.rb +23 -0
  35. data/lib/unit-ruby/types/string.rb +9 -0
  36. data/lib/unit-ruby/util/api_resource.rb +179 -0
  37. data/lib/unit-ruby/util/connection.rb +72 -0
  38. data/lib/unit-ruby/util/error.rb +16 -0
  39. data/lib/unit-ruby/util/resource_operations.rb +82 -0
  40. data/lib/unit-ruby/util/schema.rb +16 -0
  41. data/lib/unit-ruby/version.rb +3 -0
  42. data/lib/unit-ruby.rb +41 -0
  43. data/unit-ruby.gemspec +44 -0
  44. metadata +160 -0
@@ -0,0 +1,21 @@
1
+ module Unit
2
+ module Types
3
+ class Array
4
+ attr_reader :items
5
+
6
+ def initialize(items)
7
+ @items = items || []
8
+ end
9
+
10
+ def self.cast(val)
11
+ return val if val.is_a? self
12
+
13
+ new(val)
14
+ end
15
+
16
+ def as_json_api
17
+ items
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ module Unit
2
+ module Types
3
+ class Boolean
4
+ def self.cast(value)
5
+ return nil if value.nil?
6
+
7
+ case value
8
+ when 'false', '0', 0, false
9
+ false
10
+ when 'true', '1', 1, true
11
+ true
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ module Unit
2
+ module Types
3
+ class Coordinates
4
+ attr_reader :longitude, :latitude
5
+
6
+ def initialize(longitude:, latitude:)
7
+ @longitude = longitude
8
+ @latitude = latitude
9
+ end
10
+
11
+ def self.cast(val)
12
+ return val if val.is_a? self
13
+ return nil if val.nil?
14
+
15
+ new(longitude: val[:longitude], latitude: val[:latitude])
16
+ end
17
+
18
+ def as_json_api
19
+ { longitude: longitude, latitude: latitude }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ module Unit
2
+ module Types
3
+ class Date
4
+ def self.cast(value)
5
+ return nil if value.nil?
6
+
7
+ ::Date.parse(value).strftime('%F')
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Unit
2
+ module Types
3
+ class DateTime
4
+ def self.cast(value)
5
+ return nil if value.nil?
6
+
7
+ ::Date.parse(value).iso8601
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Unit
2
+ module Types
3
+ class Decimal
4
+ def self.cast(value)
5
+ value&.to_d
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Unit
2
+ module Types
3
+ class Float
4
+ def self.cast(value)
5
+ value&.to_f
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module Unit
2
+ module Types
3
+ class FullName
4
+ attr_reader :first, :last
5
+
6
+ def initialize(first:, last:)
7
+ @first = first
8
+ @last = last
9
+ end
10
+
11
+ def self.cast(val)
12
+ return val if val.is_a? self
13
+ return nil if val.nil?
14
+
15
+ new(first: val[:first], last: val[:last])
16
+ end
17
+
18
+ def as_json_api
19
+ { first: first, last: last }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module Unit
2
+ module Types
3
+ class Hash
4
+ attr_reader :items
5
+
6
+ def initialize(items)
7
+ @items = items || {}
8
+ end
9
+
10
+ def self.cast(val)
11
+ return val if val.is_a? self
12
+
13
+ new(val)
14
+ end
15
+
16
+ def as_json_api
17
+ items
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module Unit
2
+ module Types
3
+ class Integer
4
+ def self.cast(value)
5
+ value&.to_i
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module Unit
2
+ module Types
3
+ class Phone
4
+ attr_reader :country_code, :number
5
+
6
+ def initialize(country_code:, number:)
7
+ @country_code = country_code
8
+ @number = number
9
+ end
10
+
11
+ def self.cast(val)
12
+ return val if val.is_a? self
13
+ return nil if val.nil?
14
+
15
+ new(country_code: val[:country_code], number: val[:number])
16
+ end
17
+
18
+ def as_json_api
19
+ { country_code: country_code, number: number }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ module Unit
2
+ module Types
3
+ class String
4
+ def self.cast(value)
5
+ value&.to_s
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,179 @@
1
+ module Unit
2
+ module Util
3
+ class APIResource
4
+ attr_accessor :id, :type
5
+
6
+ def initialize(attributes = {})
7
+ clear_attributes!
8
+ mark_as_clean!
9
+
10
+ attributes.each do |key, value|
11
+ send("#{key}=", value)
12
+ end
13
+ end
14
+
15
+ # Creates a base http connection to the API
16
+ #
17
+ def self.connection
18
+ @connection ||= Connection.new
19
+ end
20
+
21
+ # Defines the schema for a resource's attributes
22
+ #
23
+ def self.schema
24
+ @schema ||= Util::Schema.new
25
+ end
26
+
27
+ def schema
28
+ self.class.schema
29
+ end
30
+
31
+ # Declares a new attribute by name and adds it to the schema
32
+ #
33
+ # @param name [Symbol] the name of the attribute
34
+ # @param type [Class] the object type
35
+ # @param readonly [Boolean] excludes the attribute from the request when creating a resource
36
+ def self.attribute(name, type = nil, readonly: false)
37
+ schema.add(name, type, readonly: readonly)
38
+
39
+ attr_accessor name
40
+
41
+ define_method("#{name}=") do |value|
42
+ previous_value = send(name)
43
+ new_value = type.cast(value)
44
+
45
+ instance_variable_set("@#{name}", new_value)
46
+
47
+ mark_attribute_as_dirty(name) if new_value != previous_value
48
+ new_value
49
+ end
50
+ end
51
+
52
+ def relationships
53
+ @relationships ||= {}
54
+ end
55
+
56
+ attr_writer :relationships
57
+
58
+ # Sets the base path for this resource
59
+ #
60
+ # Usage:
61
+ # class Customer < Unit::Resource
62
+ # path '/customers'
63
+ # end
64
+ def self.path(route = nil)
65
+ return @path if route.nil?
66
+
67
+ @path = route
68
+ end
69
+
70
+ def self.resource_path(id)
71
+ "#{path}/#{id}"
72
+ end
73
+
74
+ def self.resources_path
75
+ path
76
+ end
77
+
78
+ # The JSON:API type for this resource
79
+ def resource_type
80
+ self.class.name.split('::').last.camelize(:lower)
81
+ end
82
+
83
+ def resource_path
84
+ "#{self.class.path}/#{id}"
85
+ end
86
+
87
+ # Creates an association to a related resource
88
+ # This will create a helper method to traverse into a resource's related resource(s)
89
+ def self.belongs_to(resource_name, class_name: nil)
90
+ class_name ||= resource_name.to_s.camelize
91
+
92
+ define_method(resource_name) do
93
+ relationship_id = relationships[resource_name][:data]&.fetch(:id)
94
+
95
+ return nil unless relationship_id
96
+
97
+ Kernel.const_get(class_name).find(relationship_id)
98
+ end
99
+
100
+ define_method("#{resource_name}=") do |resource|
101
+ relationships[resource_name] = {
102
+ data: { type: resource_name, id: resource.id }
103
+ }
104
+ end
105
+ end
106
+
107
+ # Hyrdates an instance of the resource from data returned from the API
108
+ def self.build_resource_from_json_api(data_item)
109
+ new.tap do |resource|
110
+ resource.mark_as_clean!
111
+ resource.update_resource_from_json_api(data_item)
112
+ end
113
+ end
114
+
115
+ def update_resource_from_json_api(data)
116
+ self.id = data[:id]
117
+ self.type = data[:type]
118
+ self.relationships = data[:relationships]
119
+
120
+ clear_attributes!
121
+
122
+ data[:attributes].each { |key, value| update_attribute(key, value) }
123
+
124
+ mark_as_clean!
125
+ end
126
+
127
+ # Represents this resource's attributes
128
+ #
129
+ # @return [Hash] Representation of this resource's attributes as a hash
130
+ def attributes
131
+ self.class.schema.attributes.each_with_object({}) do |schema_attribute, h|
132
+ h[schema_attribute.name] = send(schema_attribute.name)
133
+ end
134
+ end
135
+
136
+ # Represents this resource for serialization (create/update)
137
+ #
138
+ # @return [Hash] Representation of this object as JSONAPI object
139
+ def as_json_api
140
+ self.class.schema.attributes.each_with_object({}) do |schema_attribute, h|
141
+ next if schema_attribute.readonly
142
+
143
+ val = send(schema_attribute.name)
144
+
145
+ # serialize the value if it is a complex type
146
+ val = val.as_json_api if val.respond_to? :as_json_api
147
+
148
+ h[schema_attribute.name] = val
149
+ end
150
+ end
151
+
152
+ def dirty?
153
+ dirty_attributes.any?
154
+ end
155
+
156
+ def dirty_attributes
157
+ @dirty_attributes ||= []
158
+ end
159
+
160
+ def mark_attribute_as_dirty(name)
161
+ dirty_attributes << name
162
+ end
163
+
164
+ def mark_as_clean!
165
+ @dirty_attributes = []
166
+ end
167
+
168
+ def clear_attributes!
169
+ self.class.schema.attributes.each do |attribute|
170
+ update_attribute(attribute.name, nil)
171
+ end
172
+ end
173
+
174
+ def update_attribute(name, value)
175
+ send("#{name}=", value)
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,72 @@
1
+ module Unit
2
+ module Util
3
+ class Connection
4
+ class << self
5
+ attr_accessor :api_key, :base_url
6
+ end
7
+
8
+ attr_reader :connection
9
+
10
+ def initialize
11
+ @connection = Faraday.new(self.class.base_url) do |f|
12
+ f.headers['Authorization'] = "Bearer #{self.class.api_key}"
13
+ f.request :json # encode req bodies as JSON
14
+ f.request :retry # retry transient failures
15
+ f.response :follow_redirects # follow redirects
16
+ f.response :json # decode response bodies as JSON
17
+ end
18
+ end
19
+
20
+ # Executes a GET request to the API
21
+ #
22
+ # @return the resource (or array of resources) returned from the API
23
+ def get(path, params = nil)
24
+ response = connection.get(path, params)
25
+
26
+ handle_errors(response)
27
+
28
+ from_json_api(response.body)
29
+ end
30
+
31
+ # Executes a POST request to the API
32
+ #
33
+ # @return [Unit::APIResource] a new instance of the resource
34
+ def post(path, data = nil)
35
+ response = connection.post do |req|
36
+ req.url path
37
+ req.headers['Content-Type'] = 'application/vnd.api+json'
38
+ req.body = data.deep_transform_keys! { |key| key.to_s.camelize(:lower) } if data
39
+ end
40
+
41
+ handle_errors(response)
42
+
43
+ from_json_api(response.body)
44
+ end
45
+
46
+ # Executes a PATCH request to the API
47
+ def patch(path, data = nil)
48
+ response = connection.patch do |req|
49
+ req.url path
50
+ req.headers['Content-Type'] = 'application/vnd.api+json'
51
+ req.body = data.deep_transform_keys! { |key| key.to_s.camelize(:lower) } if data
52
+ end
53
+
54
+ handle_errors(response)
55
+
56
+ from_json_api(response.body)
57
+ end
58
+
59
+ def from_json_api(response_body)
60
+ response_body.deep_transform_keys do |key|
61
+ key.to_s.underscore.to_sym
62
+ end.fetch(:data)
63
+ end
64
+
65
+ def handle_errors(response)
66
+ return if response.success?
67
+
68
+ raise(Error, response.body)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,16 @@
1
+ module Unit
2
+ module Util
3
+ class Error < StandardError
4
+ attr_accessor :title, :status, :details, :detail
5
+
6
+ def initialize(api_response)
7
+ error = api_response['errors'].first
8
+ @title = error['title']
9
+ @status = error['status']
10
+ @details = error['details'] || error['detail']
11
+
12
+ super(@details)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,82 @@
1
+ module Unit
2
+ module Util
3
+ module ResourceOperations
4
+ module Find
5
+ def self.included(klass)
6
+ klass.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def find(id)
11
+ located_resource = connection.get(resource_path(id))
12
+
13
+ build_resource_from_json_api(located_resource)
14
+ end
15
+ end
16
+ end
17
+
18
+ module Create
19
+ def self.included(klass)
20
+ klass.extend(ClassMethods)
21
+ end
22
+
23
+ module ClassMethods
24
+ def create(attributes)
25
+ resource = new(attributes)
26
+
27
+ data = {
28
+ type: resource.resource_type,
29
+ attributes: resource.as_json_api.slice(*resource.dirty_attributes)
30
+ }
31
+ unless resource.relationships.empty?
32
+ data[:relationships] =
33
+ resource.relationships
34
+ end
35
+
36
+ created_resource = connection.post(resources_path, { data: data })
37
+
38
+ build_resource_from_json_api(created_resource)
39
+ end
40
+ end
41
+ end
42
+
43
+ module List
44
+ def self.included(klass)
45
+ klass.extend(ClassMethods)
46
+ end
47
+
48
+ module ClassMethods
49
+ # List resources
50
+ #
51
+ # @param where [Hash] Optional. Filters to apply to the list.
52
+ # @param limit [Integer] Optional. Maximum number of resources that will be returned. Maximum is 1000 resources.
53
+ # @param offset [Integer] Optional. Number of resources to skip
54
+ # @param sort [String] Optional. sort: 'createdAt' for ascending order or sort: '-createdAt' (leading minus sign) for descending order
55
+ def list(where: {}, limit: 100, offset: 0, sort: nil)
56
+ params = { filter: where, page: { offset: offset, limit: limit },
57
+ sort: sort }.compact
58
+ resources = connection.get(resources_path, params)
59
+
60
+ resources.map { |resource| build_resource_from_json_api(resource) }
61
+ end
62
+ end
63
+ end
64
+
65
+ module Save
66
+ def save
67
+ updated_resource = self.class.connection.patch(
68
+ resource_path,
69
+ {
70
+ data: {
71
+ type: resource_type,
72
+ attributes: as_json_api.slice(*dirty_attributes)
73
+ }
74
+ }
75
+ )
76
+
77
+ update_resource_from_json_api(updated_resource)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,16 @@
1
+ module Unit
2
+ module Util
3
+ class Schema
4
+ Attribute = Struct.new(:name, :type, :readonly)
5
+
6
+ def initialize
7
+ @attributes = []
8
+ end
9
+
10
+ def add(name, type, readonly: false)
11
+ @attributes << Attribute.new(name, type, readonly)
12
+ end
13
+ attr_reader :attributes
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Unit
2
+ VERSION = '0.1.0'
3
+ end
data/lib/unit-ruby.rb ADDED
@@ -0,0 +1,41 @@
1
+ require 'unit-ruby/util/connection'
2
+ require 'unit-ruby/util/error'
3
+ require 'unit-ruby/util/api_resource'
4
+ require 'unit-ruby/util/resource_operations'
5
+ require 'unit-ruby/util/schema'
6
+
7
+ require 'unit-ruby/types/address'
8
+ require 'unit-ruby/types/application_form_prefill'
9
+ require 'unit-ruby/types/application_form_settings_override'
10
+ require 'unit-ruby/types/array'
11
+ require 'unit-ruby/types/boolean'
12
+ require 'unit-ruby/types/coordinates'
13
+ require 'unit-ruby/types/date_time'
14
+ require 'unit-ruby/types/date'
15
+ require 'unit-ruby/types/decimal'
16
+ require 'unit-ruby/types/float'
17
+ require 'unit-ruby/types/full_name'
18
+ require 'unit-ruby/types/hash'
19
+ require 'unit-ruby/types/integer'
20
+ require 'unit-ruby/types/phone'
21
+ require 'unit-ruby/types/string'
22
+
23
+ require 'unit-ruby/application_form'
24
+ require 'unit-ruby/atm_location'
25
+ require 'unit-ruby/deposit_account'
26
+ require 'unit-ruby/individual_application'
27
+ require 'unit-ruby/individual_customer'
28
+ require 'unit-ruby/individual_debit_card'
29
+ require 'unit-ruby/institution'
30
+ require 'unit-ruby/version'
31
+
32
+ module Unit
33
+ # Usage:
34
+ # Unit.configure do |config|
35
+ # config.api_key = '<your api key>'
36
+ # config.base_url = 'https://api.s.unit.sh'
37
+ # end
38
+ def self.configure
39
+ yield(Util::Connection)
40
+ end
41
+ end
data/unit-ruby.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'unit-ruby/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'unit-ruby'
7
+ spec.version = Unit::VERSION
8
+ spec.authors = ['Chloe Isacke']
9
+ spec.email = ['chloe@retirable.com']
10
+
11
+ spec.summary = 'A Ruby gem for communicating with the Unit API.'
12
+ spec.homepage = 'https://github.com/retirable/unit-ruby'
13
+ spec.license = 'MIT'
14
+
15
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
16
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
17
+ if spec.respond_to?(:metadata)
18
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
19
+
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = 'https://github.com/retirable/unit-ruby'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/retirable/unit-ruby/blob/main/CHANGELOG.md'
23
+ else
24
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
25
+ 'public gem pushes.'
26
+ end
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+ spec.bindir = 'exe'
34
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ['lib']
36
+
37
+ spec.add_dependency 'faraday', '~> 1.8.0'
38
+
39
+ spec.add_development_dependency 'bundler', '~> 1.17'
40
+ spec.add_development_dependency 'rake', '~> 13.0'
41
+ spec.add_development_dependency 'rspec', '~> 3.0'
42
+ spec.add_development_dependency 'rubocop', '~> 1.24.1'
43
+ spec.metadata['rubygems_mfa_required'] = 'true'
44
+ end