zuora-ruby 0.2.0 → 0.3.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.rubocop.yml +11 -1
  4. data/README.md +101 -172
  5. data/bin/console +2 -2
  6. data/lib/utils/schema_model.rb +194 -0
  7. data/lib/zuora.rb +30 -72
  8. data/lib/zuora/calls/amend.rb +68 -0
  9. data/lib/zuora/calls/create.rb +9 -0
  10. data/lib/zuora/calls/delete.rb +20 -0
  11. data/lib/zuora/calls/generate.rb +22 -0
  12. data/lib/zuora/calls/login.rb +21 -0
  13. data/lib/zuora/calls/query.rb +15 -0
  14. data/lib/zuora/calls/subscribe.rb +75 -0
  15. data/lib/zuora/calls/update.rb +9 -0
  16. data/lib/zuora/calls/upsert.rb +29 -0
  17. data/lib/zuora/client.rb +84 -94
  18. data/lib/zuora/dispatcher.rb +45 -0
  19. data/lib/zuora/object.rb +5 -0
  20. data/lib/zuora/response.rb +50 -0
  21. data/lib/zuora/utils/envelope.rb +98 -0
  22. data/lib/zuora/version.rb +1 -4
  23. data/zuora_ruby.gemspec +10 -11
  24. metadata +57 -67
  25. data/lib/zuora/models.rb +0 -11
  26. data/lib/zuora/models/account.rb +0 -64
  27. data/lib/zuora/models/card_holder.rb +0 -54
  28. data/lib/zuora/models/contact.rb +0 -71
  29. data/lib/zuora/models/dirty.rb +0 -192
  30. data/lib/zuora/models/payment_method.rb +0 -1
  31. data/lib/zuora/models/payment_methods/credit_card.rb +0 -37
  32. data/lib/zuora/models/rate_plan.rb +0 -17
  33. data/lib/zuora/models/rate_plan_charge.rb +0 -119
  34. data/lib/zuora/models/subscription.rb +0 -80
  35. data/lib/zuora/models/tier.rb +0 -27
  36. data/lib/zuora/models/validation_predicates.rb +0 -29
  37. data/lib/zuora/resources.rb +0 -6
  38. data/lib/zuora/resources/accounts.rb +0 -20
  39. data/lib/zuora/resources/payment_methods.rb +0 -1
  40. data/lib/zuora/resources/payment_methods/credit_card.rb +0 -24
  41. data/lib/zuora/resources/subscriptions.rb +0 -17
  42. data/lib/zuora/serializers.rb +0 -1
  43. data/lib/zuora/serializers/attribute.rb +0 -35
  44. data/lib/zuora/serializers/noop.rb +0 -18
  45. data/zuora/fixtures/vcr_cassettes/account_create_.yml +0 -111
  46. data/zuora/fixtures/vcr_cassettes/account_update_.yml +0 -113
  47. data/zuora/fixtures/vcr_cassettes/subscription_create_.yml +0 -114
  48. data/zuora/fixtures/vcr_cassettes/subscription_update_.yml +0 -114
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: da6c115de2be80e51003cee17072c6c06e578199
4
- data.tar.gz: 6e48747729c511bd9fd2892eb8d97b1b9a3786a8
3
+ metadata.gz: 59ac3797046acdb3f30dbd498d3f5368024d6c8f
4
+ data.tar.gz: c747cfe5f6c0e2dfeeb46da7a0279761e201ecba
5
5
  SHA512:
6
- metadata.gz: c1ea116b905657af8fef4d060239583187eefa02fbceb623ce1676a083efd3a2cb46916cec0c0beed5cedab0ca802abf3ccf42aab24b1bf3d2613744ae00e713
7
- data.tar.gz: 7bdeb438f2581dfa8dc1aa9d01c008ba8cfdad614831bf0de642ef56a3e6ecf197b81a2919082d735dc5c7c62bfcefa3c6af858210e90062482587aabfc932fd
6
+ metadata.gz: a2e9865623c907ad008b942f39fb831979f6fe65f8b7d159f4176ae074ea41867e38883d77e891922e1e3e70da7685a3ad617dbe9cbf6aedfefceba8513f2b56
7
+ data.tar.gz: ded935331529eecc8ce365652be19e7fadb252c949e919d7e0bca07af74211d7f1cf5cc4264c3dd2d721bfdefcdb1893007ba3b91e25e678298047a70d1797a7
data/.gitignore CHANGED
@@ -9,4 +9,5 @@
9
9
  /tmp/
10
10
  .env
11
11
  .byebug_history
12
- zuora-ruby*.gem
12
+ zuora-ruby*.gem
13
+ xml-debug/
@@ -3,6 +3,7 @@ AllCops:
3
3
  - 'db/**/*'
4
4
  - 'config/**/*'
5
5
  - 'vendor/**/*'
6
+ - 'scratchpad.rb'
6
7
 
7
8
  Style/Encoding:
8
9
  Enabled: true
@@ -43,4 +44,13 @@ Style/MultilineOperationIndentation:
43
44
  EnforcedStyle: indented
44
45
  SupportedStyles:
45
46
  - aligned
46
- - indented
47
+ - indented
48
+
49
+ Style/ClassAndModuleChildren:
50
+ Enabled: false
51
+
52
+ Metrics/ClassLength:
53
+ Enabled: false
54
+
55
+ Style/EachWithObject:
56
+ Enabled: false
data/README.md CHANGED
@@ -2,195 +2,115 @@
2
2
  [![Code Climate](https://codeclimate.com/repos/569444dfa3d810003a00313f/badges/416bae00acf65d690efe/gpa.svg)](https://codeclimate.com/repos/569444dfa3d810003a00313f/feed)
3
3
  [![Test Coverage](https://codeclimate.com/repos/569444dfa3d810003a00313f/badges/416bae00acf65d690efe/coverage.svg)](https://codeclimate.com/repos/569444dfa3d810003a00313f/coverage)
4
4
 
5
- # Zuora REST API: Ruby Client
5
+ # Zuora SOAP API Client
6
6
 
7
- This library implements a Ruby client wrapping Zuora's REST API.
7
+ ## Features
8
+ * HTTP client to Zuora SOAP API
9
+ * Authentication and session storage
10
+ * SOAP XML request constructors from Ruby data
11
+ * Support for custom Zuora fields
12
+ * Light validation of SOAP call parameters
13
+ * Light wrapper over response, providing a Ruby object interface over Zuora's returned XML response
8
14
 
9
- ### Model
10
- A base module called `DirtyValidAttr` provides `dirty_model_attr`
11
- * **Accessors**: attribute name provides getters and setters, as in `attr_accessor`
12
- * **Validations** `valid: max_length(3) `
13
- * Includes a library of predicate higher order validation functions
14
- * **Coercions** `coerce: ->(value) { value.to_s } `
15
- * **Type Checks** `type: String`
16
- * **Required Attributes**: `:required: true`
15
+ ## Usage
17
16
 
18
- ### Resource
19
- * **HTTP requests**: that are authenticated with provided credentials
20
- * **Zuora API endpoints**:
21
17
 
22
- ### Utilities
23
- - **Serialization**: Ruby <=> JSON serializer provided, just provide a module or class that respnds to `.serialize(hash)`
18
+ ### Client
24
19
 
25
- ### Development: Specs and Testing
26
- - **Factories**: for generating sample valid and invalid data that works with the API
27
- - **Unit**: factories for testing model valdiations
28
- - **Integration**: Tests against or memoized (via `VCR`) HTTP responses
29
-
30
- ## Quickstart
20
+ Create a client
31
21
  ```ruby
32
- # Connect
33
- client = Zuora::Client.new(username, password)
34
- # Create a model
35
- account = Zuora::Models::Account.new(...)
36
- serializer = Zuora::Serializers::Attribute
37
- # Low level HTTP API
38
- client.get('/rest/v1/accounts', serializer.serialze account)
39
- # High Level Resource API
40
- Zuora::Resources::Account.create! client, account, serializer
22
+ client = Zuora::Client.new(<username>, <password>)
41
23
  ```
42
- ## Key Features & Concepts
43
- 1. ***Client:*** Create a client by providing username and password.
44
- This authenticates and stores the returned session cookie
45
- used in subsequent requests. An optional third, truthy value enables Sandbox instead of production mode.
46
24
 
47
- 2. ***HTTP:***
48
- Use `client.<get|post|put>(url, params)` to make HTTP requests via the authenticated client. Request and response body will be converted to/from Ruby via `farraday_middleware`.
25
+ Execute a SOAP request. All Zuora calls are supported: `.create()`, `.update()`, `.amend()`, `.generate()`, `.delete()`, `subscribe()`.
49
26
 
50
- 3. ***Models:*** Ruby interface for constructing valid Zuora objects.
51
- - Documentation coming soon. In the mean time, check comments in `Zuora::Models::Dirty`.
27
+ ### Quick Reference
28
+ See examples below and integration specs for full interfaces.
29
+ ```ruby
30
+ client.call! :query, "SELECT Id FROM Account"
31
+ client.call! :create, type: :Account, objects: [{}, {}]
32
+ client.call! :update, type: :Invoice, objects: [{ id: '123' }, { id: '123' }]
33
+ client.call! :delete, ids: ['123', '456']
34
+ client.call! :amend, amendments: {}, amend_options: {}, :preview_options: {}
35
+ client.call! :subscribe,
36
+ account: {}
37
+ payment_method: {}
38
+ bill_to_contact: {}
39
+ sold_to_contact: {}
40
+ subscribe_options: {}
41
+ subscription: {}
42
+ rate_plan: {}
43
+ ```
52
44
 
53
- 4. **Serializers:** Recursive data transformations for mapping between formats; a Ruby -> JSON serializer is included; `snake_case` attributes are transformed into JSON `lowerCamelCase` recursively in a nested structure.
54
- - ex. `Zuora::Serializers::Attribute.serialize account`
45
+ #### Create Example
55
46
 
56
- 5. **Resources:** Wraps Zuora REST API endpoints. Hand a valid model and (optionally) a serializer to a Resource to trigger a request. Request will be made for valid models only. An exception will be raised if the model is invalid. Otherwise, a `Farraday::Response` object will be returned (responding to `.status`, `.headers`, and `.body`).
47
+ ```ruby
48
+ response = client.call! :create,
49
+ type: :BillRun,
50
+ objects: [{
51
+ invoice_date: '2016-03-01',
52
+ target_date: '2016-03-01'
53
+ }]
54
+ ```
55
+ This would generate SOAP XML, make, and return an authenticated SOAP request.
56
+ ```xml
57
+ <?xml version="1.0"?>
58
+ <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:api="http://api.zuora.com/" xmlns:obj="http://object.api.zuora.com/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
59
+ <soapenv:Header>
60
+ <api:SessionHeader>
61
+ <api:session><!-- SESSION TOKEN HERE --></api:session>
62
+ </api:SessionHeader>
63
+ </soapenv:Header>
64
+ <soapenv:Body>
65
+ <api:create>
66
+ <api:zObjects xsi:type="obj:BillRun">
67
+ <obj:InvoiceDate>2016-03-01</obj:InvoiceDate>
68
+ <obj:TargetDate>2016-03-01</obj:TargetDate>
69
+ </api:zObjects>
70
+ </api:create>
71
+ </soapenv:Body>
72
+ </soapenv:Envelope>
73
+ ```
57
74
 
58
- 6. **Factories:** Factories are set up for easily constructing Zuora requests in development (via `factory_girl`)
75
+ A response object is returned. You can access the raw response:
59
76
  ```ruby
60
- account = create :account, :credit_card => create(:credit_card),
61
- :sold_to_contact => create(:contact),
62
- :bill_to_contact => create(:contact)
63
-
64
- account.valid? # => true
77
+ response.raw.body
78
+ ```
79
+ ```xml
80
+ <?xml version="1.0" encoding="UTF-8"?>
81
+ <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
82
+ <soapenv:Body>
83
+ <api:createResponse xmlns:api="http://api.zuora.com/">
84
+ <api:result>
85
+ <api:Id>2c92c0f9526913e301526a7863df4647</api:Id>
86
+ <api:Success>true</api:Success>
87
+ </api:result>
88
+ </api:createResponse>
89
+ </soapenv:Body>
90
+ </soapenv:Envelope>
65
91
  ```
66
- 7. **Test coverage:** Unit and integration specs coverage via `rspec`. Coming soon: HTTP response caching (using `VCR`)
67
-
68
- ## Models
69
- Models implement (recursive, nested) Zuora validations using `ActiveModel::Model` and soon, dirty attribute tracking via `ActiveModel::Dirty`
70
- * Account
71
- * CardHolder
72
- * Contact
73
- * PaymentMethod::CreditCard
74
- * RatePlan
75
- * RatePlanCharge
76
- * Subscription
77
- * Tier
78
-
79
- ## Resources
80
- In module `Zuora::Resources::`
81
- * `Account.create!` **[working]**
82
- * `Account.update!` **[working]**
83
- * `Subscription.create!` **[working]**
84
- * `Subscription.update!` **[working]**
85
- * `Subscription.cancel!` [in progress]
86
- * `PaymentMethod.update!` [in progress]
87
-
88
- ## Examples
89
- ### Creating an Account
92
+
93
+ The `.to_h` method provides an interface that can be navigated as a hash or object.
90
94
 
91
95
  ```ruby
92
- username = 'your@username.com'
93
- password = 'super_secure_password'
94
-
95
- client = Zuora::Client.new(username, password, true) # true for sandbox
96
-
97
- account = Zuora::Models::Account.new(
98
- :name => 'Abc',
99
- :auto_pay => true,
100
- :currency => 'USD',
101
- :bill_cycle_day => '0',
102
- :payment_term => 'Net 30',
103
- :bill_to_contact => Zuora::Models::Contact.new(
104
- :first_name => 'Abc',
105
- :last_name => 'Def',
106
- :address_1 => '123 Main St',
107
- :city => 'Palm Springs',
108
- :state => 'FL',
109
- :zip_code => '90210',
110
- :country => 'US'
111
- ),
112
- :sold_to_contact => Zuora::Models::Contact.new(
113
- :first_name => 'Abc',
114
- :last_name => 'Def',
115
- :country => 'US'
116
- ),
117
- :credit_card => Zuora::Models::PaymentMethod.new(
118
- :card_type => 'Visa',
119
- :card_number => '4111111111111111',
120
- :expiration_month => '03',
121
- :expiration_year => '2017',
122
- :security_code => '122',
123
- )
124
- )
125
-
126
- # Create an account in one of two ways:
127
-
128
- serializer = Zuora::Serializers::Attribute
129
-
130
- # Using the low-level API exposed by `Client`
131
- response = client.post('/rest/v1/accounts', serializer.serialize accont)
132
-
133
- # or using the higher-level resource API
134
- response = Zuora::Resources::Accounts.create!(client, account, serializer)
135
-
136
- # Le response
137
-
138
- pp response
139
-
140
- #<Faraday::Response:0x007f8033b05f08
141
- @env=
142
- #<struct Faraday::Env
143
- method=:post,
144
- body=
145
- {"success"=>true,
146
- "accountId"=>"2c92c0fa521b466c0152250822741a71",
147
- "accountNumber"=>"A00000038",
148
- "paymentMethodId"=>"2c92c0fa521b466c0152250829c81a7b"},
149
- url=#<URI::HTTPS https://apisandbox-api.zuora.com/rest/v1/accounts>,
150
- request=
151
- #<struct Faraday::RequestOptions
152
- params_encoder=nil,
153
- proxy=nil,
154
- bind=nil,
155
- timeout=nil,
156
- open_timeout=nil,
157
- boundary=nil,
158
- oauth=nil>,
159
- request_headers=
160
- {"User-Agent"=>"Faraday v0.9.2",
161
- "Content-Type"=>"application/json",
162
- "Cookie"=>
163
- "ZSession=LBToVw72ZCAQLjdZ9Ksj8rx2BlP3NbgmMYwCzuf_slSJqIhMbJjdQ1T-4otbdfjUOImQ_XJOCbJgdCd7jHmGsnnJyG49NyRkI7FVKOukVQtdJssJ5n1xAXJeVjxj3qj97iiIZp697v3G2w86iCTN6kWycUlSVezBElbC8_EhScbx8YmaP4QJxXRIFHHdOQPq3IN-9ezk21Cpq3fdXn6s0fIPMU7NUFj7-kD4dcYNBAyd7i2fJVAIV31mXNBH2MuU;"},
164
- ssl=
165
- #<struct Faraday::SSLOptions
166
- verify=false,
167
- ca_file=nil,
168
- ca_path=nil,
169
- verify_mode=nil,
170
- cert_store=nil,
171
- client_cert=nil,
172
- client_key=nil,
173
- certificate=nil,
174
- private_key=nil,
175
- verify_depth=nil,
176
- version=nil>,
177
- parallel_manager=nil,
178
- params=nil,
179
- response=#<Faraday::Response:0x007f8033b05f08 ...>,
180
- response_headers=
181
- {"server"=>"Zuora App",
182
- "content-type"=>"application/json;charset=utf-8",
183
- "expires"=>"Sat, 09 Jan 2016 06:17:18 GMT",
184
- "cache-control"=>"max-age=0, no-cache, no-store",
185
- "pragma"=>"no-cache",
186
- "date"=>"Sat, 09 Jan 2016 06:17:18 GMT",
187
- "content-length"=>"165",
188
- "connection"=>"close",
189
- "set-cookie"=>
190
- "ZSession=dOz9WgdPQbb9J9wzwhuR_t1j9feD4dYBUEZ_sjK6pS9KAaJtPdKN-jAivNELsaANWMJrvHW_1eLxT7XqzjLVBJKzLDJT7_0ucvzcrwNcwMW8mUGpeUhQQu_h2HzNH1kZjc1HX6pfw-BH66BafLemLIdqL75ifmglk8YuTOf_wTg54GsovkrgJCAp9zferw6pYHkZoQUXyH7zmUmmWvMAZ1ZVamhLOf1P3FrrHaw6eIiUj0ehlKvrtxB-GHIgYxh6; Path=/; Secure; HttpOnly"},
191
- status=200>,
192
- @on_complete_callbacks=[]>
96
+ response.to_h.envelope.body.create_response.result
97
+ => { id: '2c92c0f9526913e301526a7863df4647', success: true }
98
+ ```
99
+
100
+ #### .subscribe() example
101
+ Subscribe is a very large call that involves a lot of data. See the integration spec `spec/zuora/integration/subscription_spec.rb` for full example
102
+
103
+ ```ruby
104
+ response = client.call! :subscribe,
105
+ account: {...},
106
+ payment_method: {...},
107
+ bill_to_contact: {...},
108
+ sold_to_contact: {...},
109
+ subscription: {...},
110
+ rate_plan: {...}
111
+
193
112
  ```
113
+
194
114
  # Changelog
195
115
  * **[0.1.0 - 2016-01-12]** Initial release
196
116
  * **[0.2.0] - 2016-01-14]** Models
@@ -205,6 +125,15 @@ pp response
205
125
  - Adds VCR for mocking out HTTP requests
206
126
  - Adds integration specs for `Subscribe` `create!` and `update!` and `Account` `create!` and `update!`
207
127
 
128
+ * **[0.3.0 2016-1-28]** Focus on SOAP API, simpify client library feature set
129
+ - Redesign API, eliminate previous Model constructs
130
+ - Implement SOAP API Client, as it provides fuller functionality than REST
131
+ - Focus on constructing + composing hash-like Ruby objects into XML SOAP requests
132
+ - Add support for custom fields
133
+ - Remove object-level validations; relies on Zuora's own error responses. Light validations on call constructors.
134
+ - Provide object/hash lookup capabilities on Zuora Responses
135
+ - See integration specs for full interface
136
+
208
137
  # Commit rights
209
138
  Anyone who has a patch accepted may request commit rights. Please do so inside the pull request post-merge.
210
139
 
@@ -12,10 +12,10 @@ require 'zuora'
12
12
  # Pry.start
13
13
 
14
14
  require 'factory_girl'
15
- FactoryGirl.definition_file_paths = ['spec/zuora/factories']
15
+ FactoryGirl.definition_file_paths = ['spec/factories']
16
16
  FactoryGirl.find_definitions
17
17
  # Short hand factory girl syntax
18
- include FactoryGirl::Syntax::Methods
18
+ # include FactoryGirl::Syntax::Methods
19
19
 
20
20
  require 'irb'
21
21
  IRB.start
@@ -0,0 +1,194 @@
1
+ require 'byebug'
2
+
3
+ module SchemaModel
4
+ def self.included(base)
5
+ base.include InstanceMethods
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ # Dynamically configures accessors, dirty tracking, validation,
11
+ # and serialization methods given definition in opts
12
+ # @param [Object] _name - name of schema
13
+ # @param [Hash] opts - See below
14
+ #
15
+ # class AwesomeClass
16
+ # schema :my_schema,
17
+ # id: {
18
+ # type: Numeric, # value will be checked using is_a?
19
+ # valid: -> (v) { v > 0 }, # value will be validated by calling this
20
+ # schema: [ChildClass] # single or collection recursive checks
21
+ # doc: 'Id, number greater than 1' # documentation string
22
+ # }
23
+ # end
24
+ #
25
+ # a = AwesomeClass.new(id: 1)
26
+ # a.valid? => true
27
+ # a.errors => {}
28
+ #
29
+ def schema(_name, opts = {})
30
+ define_method(:definition) { opts }
31
+
32
+ opts.each do |k, definition|
33
+ # Reader
34
+ attr_reader k
35
+
36
+ # Writer
37
+ define_writer! k, definition
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Helper for dynamically defining writer method
44
+ # @param [Symbol] k - name of attribute
45
+ # @param [Hash] definition - See docstring for schema above
46
+ def define_writer!(k, definition)
47
+ define_method("#{k}=") do |value|
48
+ # Recursively convert hash and array of hash to schematized objects
49
+ value = ensure_schema value, definition[:schema]
50
+
51
+ # Initial value
52
+ instance_variable_set "@#{k}", value
53
+
54
+ # Dirty tracking
55
+ self.changed_attributes ||= Set.new
56
+ self.changed_attributes << k
57
+ end
58
+ end
59
+ end
60
+
61
+ module InstanceMethods
62
+ attr_accessor :changed_attributes
63
+
64
+ def initialize(attrs = {})
65
+ attrs.each do |attr, v|
66
+ send("#{attr}=", v)
67
+ end
68
+ end
69
+
70
+ def errors
71
+ check definition, self
72
+ end
73
+
74
+ def valid?
75
+ errors.empty?
76
+ end
77
+
78
+ def to_json
79
+ return nil unless changed_attributes
80
+ Hash[
81
+ changed_attributes.map { |attr| serialize_attr(attr) }
82
+ ]
83
+ end
84
+
85
+ private
86
+
87
+ # @param [Symbol] attr
88
+ # @return [Array]
89
+ def serialize_attr(attr)
90
+ value = send(attr)
91
+ value = if value.is_a?(Hash) || value.is_a?(SchemaModel)
92
+ value.to_json
93
+ elsif value.is_a?(Array)
94
+ value.map(&:to_json)
95
+ else
96
+ value
97
+ end
98
+
99
+ [attr.to_s.camelize(:lower), value]
100
+ end
101
+
102
+ # Given a schema and a value which may be a single record or collection,
103
+ # collect and return any errors.
104
+ # @param [SchemaModel] child_schema - A schema object class
105
+ # @param [Object] value - Array of models or single model
106
+ # @return [Object] Array of errors hashes, or one hash.
107
+ # Structure matches 'value' input
108
+ def check_children(child_schema, value)
109
+ return unless child_schema && value.present?
110
+
111
+ if value.is_a? Array
112
+ value.map(&:errors).reject(&:empty?)
113
+ else
114
+ value.errors
115
+ end
116
+ end
117
+
118
+ # Checks that value is of correct type
119
+ # @param [Maybe Class] type - type to check using value.is_a?(type)
120
+ # @param [Object] value - value to check
121
+ # @return [Maybe String] error message
122
+ def check_type(type, value)
123
+ return unless type && value && !value.is_a?(type)
124
+
125
+ "should be of type #{type} but is of type #{value.class}"
126
+ end
127
+
128
+ # Checks that required field meets validation
129
+ # @param [Boolean or Callable] valid - callable validation fn or boolean
130
+ # function will be called with value
131
+ # @param [Object] value - value to check
132
+ # @return [Maybe String] error message
133
+ def check_validation(valid, value)
134
+ return unless valid && value
135
+
136
+ passes_validation = begin
137
+ valid.call(value)
138
+ rescue
139
+ false
140
+ end
141
+ passes_validation ? nil : 'is invalid'
142
+ end
143
+
144
+ # Mutates errors, adding in error messages scoped to the attribute and key
145
+ # @param [Maybe Hash] errors -
146
+ # @param [Symbol] attr - name of attribute under check
147
+ # @param [Symbol] key - name of validation step
148
+ # @param [Object] val - data to append
149
+ def append!(errors, attr, key, val)
150
+ return unless val.present?
151
+
152
+ errors ||= {}
153
+ errors[attr] ||= {}
154
+ errors[attr][key] = val
155
+ end
156
+
157
+ # @param [Hash] schema
158
+ # @param [Hash|Object] data
159
+ # @return [Hash]
160
+ def check(schema, data)
161
+ schema.reduce({}) do |errors, (attr, defn)|
162
+ # Destructuring
163
+ child_schema, type = defn.values_at :schema, :type
164
+
165
+ # Get the value for this attribute
166
+ value = data.send attr
167
+
168
+ # Add error messages
169
+ append! errors, attr, :child, check_children(child_schema, value)
170
+ append! errors, attr, :type, check_type(type, value)
171
+ errors
172
+ end
173
+ end
174
+
175
+ # Constructs new instance(s) of provided Schema model from hash or
176
+ # array of hash values. Allows for modeling of has_one and has_many.
177
+ # @param [Array of Hashes or SchemaModels] value
178
+ # @param [SchemaModel] child_schema
179
+
180
+ def ensure_schema(value, child_schema)
181
+ if value.present? && child_schema.present?
182
+ value = if child_schema.is_a?(Array)
183
+ value.map do |item|
184
+ item.is_a?(SchemaModel) ? item : child_schema[0].new(item)
185
+ end
186
+ else
187
+ value.is_a?(SchemaModel) ? value : child_schema.new(value)
188
+ end
189
+ end
190
+
191
+ value
192
+ end
193
+ end
194
+ end