zuora-ruby 0.2.0 → 0.3.0

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