nxt_schema 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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +86 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +376 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/lib/nxt_schema/callable.rb +74 -0
  13. data/lib/nxt_schema/callable_or_value.rb +72 -0
  14. data/lib/nxt_schema/dsl.rb +38 -0
  15. data/lib/nxt_schema/error_messages/en.yaml +15 -0
  16. data/lib/nxt_schema/error_messages.rb +40 -0
  17. data/lib/nxt_schema/errors/error.rb +7 -0
  18. data/lib/nxt_schema/errors/invalid_options_error.rb +5 -0
  19. data/lib/nxt_schema/errors/schema_not_applied_error.rb +5 -0
  20. data/lib/nxt_schema/errors.rb +4 -0
  21. data/lib/nxt_schema/node/base.rb +318 -0
  22. data/lib/nxt_schema/node/collection.rb +73 -0
  23. data/lib/nxt_schema/node/constructor.rb +9 -0
  24. data/lib/nxt_schema/node/default_value_evaluator.rb +20 -0
  25. data/lib/nxt_schema/node/error.rb +13 -0
  26. data/lib/nxt_schema/node/has_subnodes.rb +97 -0
  27. data/lib/nxt_schema/node/leaf.rb +43 -0
  28. data/lib/nxt_schema/node/maybe_evaluator.rb +23 -0
  29. data/lib/nxt_schema/node/schema.rb +147 -0
  30. data/lib/nxt_schema/node/template_store.rb +15 -0
  31. data/lib/nxt_schema/node/type_resolver.rb +24 -0
  32. data/lib/nxt_schema/node/validate_with_proxy.rb +41 -0
  33. data/lib/nxt_schema/node.rb +5 -0
  34. data/lib/nxt_schema/registry.rb +85 -0
  35. data/lib/nxt_schema/types.rb +10 -0
  36. data/lib/nxt_schema/undefined.rb +7 -0
  37. data/lib/nxt_schema/validators/attribute.rb +34 -0
  38. data/lib/nxt_schema/validators/equality.rb +33 -0
  39. data/lib/nxt_schema/validators/excluded.rb +23 -0
  40. data/lib/nxt_schema/validators/excludes.rb +23 -0
  41. data/lib/nxt_schema/validators/greater_than.rb +23 -0
  42. data/lib/nxt_schema/validators/greater_than_or_equal.rb +23 -0
  43. data/lib/nxt_schema/validators/included.rb +23 -0
  44. data/lib/nxt_schema/validators/includes.rb +23 -0
  45. data/lib/nxt_schema/validators/less_than.rb +23 -0
  46. data/lib/nxt_schema/validators/less_than_or_equal.rb +23 -0
  47. data/lib/nxt_schema/validators/optional_node.rb +26 -0
  48. data/lib/nxt_schema/validators/pattern.rb +24 -0
  49. data/lib/nxt_schema/validators/query.rb +28 -0
  50. data/lib/nxt_schema/validators/registry.rb +11 -0
  51. data/lib/nxt_schema/validators/validator.rb +17 -0
  52. data/lib/nxt_schema/version.rb +3 -0
  53. data/lib/nxt_schema.rb +69 -0
  54. data/nxt_schema.gemspec +46 -0
  55. metadata +211 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '09685f892e03e96e13235c9f5d98c78ddbacd8118a47062c3a4417530cd2a3b0'
4
+ data.tar.gz: ec1b647208395d39dc5f61639dcfdfa1ea65caadfa9cd9011414ed9a5a5d9430
5
+ SHA512:
6
+ metadata.gz: 56489e75d59dc6f21f8237fcebc14236113c2e14cb47adb986989f819d0937353b1dd37763b522737923e3a87966eda2bf339f42055383fc8d174d536f1d32e5
7
+ data.tar.gz: 1f595737e27ff2c874c807b2339eee3781799a64cac27bd3724642898db6200ec60279edd72b50f100d3a52ff4f8ef155e97392b8738186258d8fd93dbdd65b0
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ vendor/cache/
10
+ .idea/
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.1
7
+ before_install: gem install bundler -v 1.17.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in nxt_schema.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,86 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ nxt_schema (0.1.0)
5
+ activesupport
6
+ dry-types
7
+ nxt_registry
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activesupport (6.0.2.1)
13
+ concurrent-ruby (~> 1.0, >= 1.0.2)
14
+ i18n (>= 0.7, < 2)
15
+ minitest (~> 5.1)
16
+ tzinfo (~> 1.1)
17
+ zeitwerk (~> 2.2)
18
+ coderay (1.1.2)
19
+ concurrent-ruby (1.1.6)
20
+ diff-lcs (1.3)
21
+ dry-configurable (0.11.2)
22
+ concurrent-ruby (~> 1.0)
23
+ dry-core (~> 0.4, >= 0.4.7)
24
+ dry-equalizer (~> 0.2)
25
+ dry-container (0.7.2)
26
+ concurrent-ruby (~> 1.0)
27
+ dry-configurable (~> 0.1, >= 0.1.3)
28
+ dry-core (0.4.9)
29
+ concurrent-ruby (~> 1.0)
30
+ dry-equalizer (0.3.0)
31
+ dry-inflector (0.2.0)
32
+ dry-logic (1.0.6)
33
+ concurrent-ruby (~> 1.0)
34
+ dry-core (~> 0.2)
35
+ dry-equalizer (~> 0.2)
36
+ dry-types (1.3.1)
37
+ concurrent-ruby (~> 1.0)
38
+ dry-container (~> 0.3)
39
+ dry-core (~> 0.4, >= 0.4.4)
40
+ dry-equalizer (~> 0.3)
41
+ dry-inflector (~> 0.1, >= 0.1.2)
42
+ dry-logic (~> 1.0, >= 1.0.2)
43
+ hirb (0.7.3)
44
+ i18n (1.8.2)
45
+ concurrent-ruby (~> 1.0)
46
+ method_profiler (2.0.1)
47
+ hirb (>= 0.6.0)
48
+ method_source (0.9.2)
49
+ minitest (5.14.0)
50
+ nxt_registry (0.1.4)
51
+ activesupport
52
+ pry (0.12.2)
53
+ coderay (~> 1.1.0)
54
+ method_source (~> 0.9.0)
55
+ rake (12.3.3)
56
+ rspec (3.8.0)
57
+ rspec-core (~> 3.8.0)
58
+ rspec-expectations (~> 3.8.0)
59
+ rspec-mocks (~> 3.8.0)
60
+ rspec-core (3.8.2)
61
+ rspec-support (~> 3.8.0)
62
+ rspec-expectations (3.8.4)
63
+ diff-lcs (>= 1.2.0, < 2.0)
64
+ rspec-support (~> 3.8.0)
65
+ rspec-mocks (3.8.1)
66
+ diff-lcs (>= 1.2.0, < 2.0)
67
+ rspec-support (~> 3.8.0)
68
+ rspec-support (3.8.2)
69
+ thread_safe (0.3.6)
70
+ tzinfo (1.2.6)
71
+ thread_safe (~> 0.1)
72
+ zeitwerk (2.2.2)
73
+
74
+ PLATFORMS
75
+ ruby
76
+
77
+ DEPENDENCIES
78
+ bundler (~> 1.17)
79
+ method_profiler
80
+ nxt_schema!
81
+ pry
82
+ rake (~> 12.3.3)
83
+ rspec (~> 3.0)
84
+
85
+ BUNDLED WITH
86
+ 1.17.2
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Andreas Robecke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,376 @@
1
+ # NxtSchema
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'nxt_schema'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install nxt_schema
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ # Schema with hash root
23
+ schema = NxtSchema.root(:company) do
24
+ requires(:name, :String)
25
+ requires(:value, :Integer).maybe(nil)
26
+ present(:stock_options, :Bool).default(false)
27
+
28
+ schema(:address) do
29
+ requires(:street, :String)
30
+ requires(:street_number, :Integer)
31
+ end
32
+
33
+ nodes(:employees) do
34
+ hash(:employee) do
35
+ POSITIONS = %w[senior junior intern]
36
+
37
+ requires(:first_name, :String)
38
+ requires(:last_name, :String)
39
+ optional(:email, :String).validate(:format, /\A.*@.*\z/)
40
+ requires(:position, NxtSchema::Types::Enums[*POSITIONS])
41
+ end
42
+ end
43
+ end
44
+
45
+ # Schema with array root
46
+ schema = NxtSchema.roots(:companies) do
47
+ schema(:company) do
48
+ requires(:name, :String)
49
+ requires(:value, :Integer).maybe(nil)
50
+ end
51
+ end
52
+
53
+ schema.apply(your: 'values here')
54
+ schema.errors # { 'name.spaced.key': ['all the errors'] }
55
+ ```
56
+
57
+ ### DSL
58
+
59
+ Create a new schema with `NxtSchema.root { ... }` or in case you have an array node as root,
60
+ use `NxtSchema.roots { ... }`. Within the schema you can create node simply with the `node(name, type_or_node, **options)`
61
+ method. Each node requires a name and a type and accepts additional options. Node are required per default.
62
+ But you can make them optional by providing the optional option.
63
+
64
+ #### Nodes
65
+
66
+ ```ruby
67
+ NxtSchema.root do
68
+ node(:first_name, :String)
69
+ node(:last_name, :String, optional: true)
70
+ node(:email, :String, presence: true)
71
+ end
72
+ ```
73
+
74
+ In order to make the schema more readable you can make use of several predicate aliases to create required, optional or
75
+ (omni)present nodes.
76
+
77
+ #### Predicate aliases
78
+
79
+ ```ruby
80
+ NxtSchema.root do
81
+ required(:first_name, :String)
82
+ optional(:last_name, :String)
83
+ present(:email, :String)
84
+ end
85
+ ```
86
+
87
+ ### Nodes
88
+
89
+ The following types of nodes exist
90
+
91
+ #### Schema Nodes
92
+
93
+ ```ruby
94
+ # Create schema nodes with:
95
+ required(:test, :Schema) do ... end
96
+ schema(:test) do ... end
97
+ hash(:test) do ... end
98
+ ```
99
+
100
+ #### Collection Nodes
101
+
102
+ ```ruby
103
+ # Create collection (array) nodes with:
104
+ required(:test, :Collection) do ... end
105
+
106
+ nodes(:test) do
107
+ # For type checking of array items you can simply add a node with the expected type.
108
+ # As always you need to give it a name. This would result in an array of string items
109
+ required(:item, :String)
110
+ end
111
+
112
+ array(:test) do ... end
113
+ ```
114
+
115
+ #### Leaf Nodes
116
+
117
+ ```ruby
118
+ # Create leaf nodes with a basic type
119
+ required(:test, :String) do ... end
120
+ ```
121
+
122
+ #### Struct Nodes
123
+
124
+ ```ruby
125
+ # Create structs from hash inputs
126
+ struct(:test) do ... end
127
+ ```
128
+
129
+ ### Types
130
+
131
+ The type system is built with dry-types from the amazing https://dry-rb.org/ eco system. Even though dry-types also
132
+ offers features such as default values for types as well as maybe types, these features are built directly into
133
+ NxtSchema. Dry.rb also has a gem for schemas and another one dedicated to validations. You should probably
134
+ check those out! However, in NxtSchema every node has a type and you can either provide a symbol that will be resolved
135
+ through the type system of the schema. But you can also directly provide an instance of dry type and thus use your
136
+ custom types.
137
+
138
+ #### Default type system
139
+
140
+ You can tell your schema which default type system it should use. Dry-Types comes with a few built in type systems.
141
+ Per default NxtSchema will use nominal types if not specified otherwise. If the type cannot be resolved from the default
142
+ type system that was specified, NxtSchema will again try to fallback to nominal types. In theory you can provide
143
+ a separate type system per node if that's what you want :-D
144
+
145
+ ```ruby
146
+ NxtSchema.root do
147
+ required(:test, :String) # The :String will resolve to NxtSchema::Types::Nominal::String
148
+ end
149
+
150
+ NxtSchema.root(type_system: NxtSchema::Types::JSON) do
151
+ required(:test, :Date) # The :Date will resolve to NxtSchema::Types::JSON::Date
152
+ # When the type does not exist in the default type system (there is non JSON::String) we fallback to nominal types
153
+ required(:test, :String)
154
+ end
155
+ ```
156
+
157
+ #### NxtSchema.params
158
+
159
+ NxtSchema.params will give you a schema as root node with NxtSchema::Types::Params as default type system.
160
+ This is suitable to validate and coerce your query params.
161
+
162
+ ```ruby
163
+ NxtSchema.params do
164
+ required(:effective_at, :DateTime) # would resolve to Types::Params::DateTime
165
+ required(:test, :String) # The :String will resolve to NxtSchema::Types::Nominal::String
166
+ required(:advanced, NxtSchema::Types::Params::Bool) # long version of required(:advanced, :Bool)
167
+ end
168
+ ```
169
+
170
+ #### Custom types
171
+
172
+ You can also register custom types. In order to check out all the cool things you can do with dry types you should
173
+ check out dry-types on https://dry-rb.org. But here is how you can add a type to the `NxtSchema::Types` module.
174
+
175
+ ```ruby
176
+ NxtSchema.register_type(
177
+ :MyCustomStrippedString,
178
+ NxtSchema::Types::Strict::String.constructor(->(string) { string&.strip })
179
+ )
180
+
181
+ # once registered you can use the type in your schema
182
+
183
+ NxtSchema.root(:company) do
184
+ required(:name, NxtSchema::Types::MyCustomStrippedString)
185
+ end
186
+ ```
187
+
188
+ ### Values
189
+
190
+ #### Default values
191
+
192
+ ```ruby
193
+ # Define default values as options or with the default method
194
+ required(:test, :String).default(value_or_proc)
195
+ required(:test, :String, default: value_or_proc) do ... end
196
+ ```
197
+
198
+ #### Maybe values
199
+
200
+ Allow specific values that are not being coerced
201
+
202
+ ```ruby
203
+ # Define maybe values (values that do not match the type)
204
+ required(:test, :String).maybe(value_or_proc)
205
+ required(:test, :String, maybe: value_or_proc) do ... end
206
+ ```
207
+
208
+ ### Validations
209
+
210
+ NxtSchema comes with a simple validation system and ships with a small set of useful validators. Every node in a schema
211
+ implements the `:validate` method. Similar to ActiveModel::Validations it allows you to simply add errors to a node
212
+ based on some condition.
213
+
214
+ ```ruby
215
+ # Simple validation
216
+ required(:test, :String).validate -> (node, value) { node.add_error("#{value} is not valid") if value == 'not allowed' }
217
+ # Built in validations
218
+ required(:test, :String).validate(:attribute, :size, ->(s) { s < 7 })
219
+ required(:test, :String).validate(:equality, 'same')
220
+ required(:test, :String).validate(:excluded, %w[not_allowed]) # excluded in the target: %w[not_allowed]
221
+ required(:test, :String).validate(:included, %w[allowed]) # included in the target: %w[allowed]
222
+ required(:test, :Array).validate(:excludes, 'excluded') # array value itself must exclude 'excluded'
223
+ required(:test, :Array).validate(:includes, 'included') # array value itself must include 'included'
224
+ required(:test, :Integer).validate(:greater_than, 1)
225
+ required(:test, :Integer).validate(:greater_than_or_equal, 1)
226
+ required(:test, :Integer).validate(:less_than, 1)
227
+ required(:test, :Integer).validate(:less_than_or_equal, 1)
228
+ required(:test, :String).validate(:pattern, /\A.*@.*\z/)
229
+ required(:test, :String).validate(:query, :present?)
230
+ ```
231
+
232
+ #### Custom validators
233
+
234
+ You can also register your custom validators. Therefore you can simply implement a class that returns a lambda on build.
235
+ This lambda will be called with the node the validations runs on and it's input value. Except this, you are free in
236
+ how your validator can be used. Check out the specs for some examples.
237
+
238
+ ```ruby
239
+ class MyCustomExclusionValidator
240
+ def initialize(target)
241
+ @target = target
242
+ end
243
+
244
+ attr_reader :target
245
+
246
+ def build
247
+ lambda do |node, value|
248
+ if target.exclude?(value)
249
+ true
250
+ else
251
+ node.add_error("#{target} should not contain #{value}")
252
+ false # validators must return false in the bad case (add_error already does this as per default)
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ # Register your validators
259
+ NxtSchema.register_validator(MyCustomExclusionValidator, :my_custom_exclusion_validator)
260
+
261
+ # and then simply reference it with the key you've registered it
262
+ schema = NxtSchema.root(:company) do
263
+ requires(:name, :String).validate(:my_custom_exclusion_validator, %w[lemonade])
264
+ end
265
+
266
+ schema.apply(name: 'lemonade').valid? # => false
267
+ ```
268
+
269
+ #### Validation messages
270
+
271
+ - Allow to specify a path to translations
272
+ - Add translated errors
273
+ - Interpolate with actual vs. expected
274
+
275
+ #### Combining validators with custom logic
276
+
277
+ `node(:test, String).validate(...)` basically adds a validator to the node. Of course you can add multiple validators.
278
+ But that means that they will all be executed and errors aggregated. If you want your validator to only run in case
279
+ another was false, you can use `:validat_with do ... end` in order to combine validators based on custom logic.
280
+
281
+ ```ruby
282
+ NxtSchema.root do
283
+ required(:test, :Integer).validate_with do
284
+ validator(:greater_than, 5) &&
285
+ validator(:greater_than, 6) &&
286
+ validator(:greater_than, 7)
287
+ end
288
+ end
289
+ ```
290
+
291
+ This has one drawback however. Let's say your test value is 4. This would only run your first validator and then exit
292
+ from the logic since validators are combined with &&. In this example it might not make much sense, but it basically
293
+ means that you might not have the full validation errors when combining validations with `:validate_with`
294
+
295
+
296
+ ### Schema options
297
+
298
+ #### Optional keys strategies
299
+
300
+ Schemas in NxtSchema only look at the keys that you have defined in your schema, others are ignored per default.
301
+ You can change this behaviour by providing a strategy for the `:additional_keys` option.
302
+
303
+ ```ruby
304
+ # This will simply ignore any other key except test
305
+ NxtSchema.root(additional_keys: :ignore) do
306
+ required(:test, :String)
307
+ end
308
+
309
+ # This would give you an error in case you apply anything other than { test: '...' }
310
+ NxtSchema.root(additional_keys: :restrict) do
311
+ required(:test, :String)
312
+ end
313
+
314
+ # This will merge other keys into your output
315
+ schema = NxtSchema.root(additional_keys: :allow) do
316
+ required(:test, :String)
317
+ end
318
+
319
+ schema.apply(test: 'getsafe', other: 'Heidelberg')
320
+ schema.valid? # => true
321
+ schema.value # => { test: 'getsafe', other: 'Heidelberg' }
322
+ ```
323
+
324
+ #### Transform keys
325
+
326
+ You may want to transform the keys from your input. Therefore specify the transform_keys option. This might be useful
327
+ when you want your schema to return only symbolized keys for example.
328
+
329
+ ```ruby
330
+ schema = NxtSchema.root(transform_keys: :to_sym) do
331
+ required(:test, :String)
332
+ end
333
+
334
+ schema.apply('test' => 'getsafe') # => {:test=>"getsafe"}
335
+ schema.apply(test: 'getsafe') # => {:test=>"getsafe"}
336
+ ```
337
+
338
+ #### Adding meta data to nodes
339
+
340
+ You want to give nodes an ID or some other meta data? You can use the meta method on nodes for adding additional
341
+ information onto any node.
342
+
343
+ ```ruby
344
+ schema = NxtSchema.root do
345
+ ERROR_MESSAGES = {
346
+ test: 'This is always broken'
347
+ }
348
+
349
+ required(:test, :String).meta(ERROR_MESSAGES).validate ->(node) { node.add_error(node.meta.fetch(node.name)) }
350
+ end
351
+
352
+ schema.apply(test: 'getsafe')
353
+ schema.error # {"root.test"=>["This is always broken"]}
354
+ ```
355
+
356
+ ## Development
357
+
358
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
359
+
360
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
361
+
362
+ ## Contributing
363
+
364
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/nxt_schema.
365
+
366
+ ## License
367
+
368
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
369
+
370
+ # TODO:
371
+
372
+ - Explain the difference between array nodes and typed array nodes
373
+ - Should we translate coercion errors as well?
374
+ - Test the different scenarios of merging schemas array, hash, ...
375
+ - Structure Errors
376
+ - NxtSchema::Json => Use json types, maybe even parse Json with Oj
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "nxt_schema"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,74 @@
1
+ module NxtSchema
2
+ class Callable
3
+ def initialize(callee)
4
+ @callee = callee
5
+
6
+ if callee.is_a?(Symbol)
7
+ self.type = :method
8
+ elsif callee.respond_to?(:call)
9
+ self.type = :proc
10
+ self.context = callee.binding
11
+ else
12
+ raise ArgumentError, "Callee is nor symbol nor a proc: #{callee}"
13
+ end
14
+ end
15
+
16
+ def bind!(execution_context)
17
+ self.context = execution_context
18
+ ensure_context_not_missing
19
+ self
20
+ end
21
+
22
+ def bind(execution_context = nil)
23
+ return self if context
24
+
25
+ self.context = execution_context
26
+ ensure_context_not_missing
27
+ self
28
+ end
29
+
30
+ # NOTE: Currently we only allow arguments! Not keyword args or **options
31
+ # If we would allow **options and we would pass a hash as the only argument it would
32
+ # automatically be parsed as the options!
33
+ def call(*args)
34
+ ensure_context_not_missing
35
+
36
+ args = args.take(arity)
37
+
38
+ if method?
39
+ context.send(callee, *args)
40
+ else
41
+ context.instance_exec(*args, &callee)
42
+ end
43
+ end
44
+
45
+ def arity
46
+ if proc?
47
+ callee.arity
48
+ elsif method?
49
+ method = context.send(:method, callee)
50
+ method.arity
51
+ else
52
+ raise ArgumentError, "Can't resolve arity from #{callee}"
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def proc?
59
+ type == :proc
60
+ end
61
+
62
+ def method?
63
+ type == :method
64
+ end
65
+
66
+ def ensure_context_not_missing
67
+ return if context
68
+
69
+ raise ArgumentError, "Missing context: #{context}"
70
+ end
71
+
72
+ attr_accessor :context, :callee, :type
73
+ end
74
+ end
@@ -0,0 +1,72 @@
1
+ module NxtSchema
2
+ class CallableOrValue
3
+ def initialize(callee)
4
+ @callee = callee
5
+
6
+ if callee.is_a?(Symbol)
7
+ self.type = :method
8
+ elsif callee.respond_to?(:call)
9
+ self.type = :proc
10
+ self.context = callee.binding
11
+ else
12
+ self.type = :value
13
+ end
14
+ end
15
+
16
+ def bind(execution_context = nil)
17
+ self.context = execution_context
18
+ ensure_context_not_missing
19
+ self
20
+ end
21
+
22
+ # NOTE: Currently we only allow arguments! Not keyword args or **options
23
+ # If we would allow **options and we would pass a hash as the only argument it would
24
+ # automatically be parsed as the options!
25
+ def call(*args)
26
+ return callee if value?
27
+
28
+ ensure_context_not_missing
29
+
30
+ args = args.take(arity)
31
+
32
+ if method?
33
+ context.send(callee, *args)
34
+ else
35
+ context.instance_exec(*args, &callee)
36
+ end
37
+ end
38
+
39
+ def arity
40
+ if proc?
41
+ callee.arity
42
+ elsif method?
43
+ method = context.send(:method, callee)
44
+ method.arity
45
+ else
46
+ raise ArgumentError, "Can't resolve arity from #{callee}"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def proc?
53
+ type == :proc
54
+ end
55
+
56
+ def method?
57
+ type == :method
58
+ end
59
+
60
+ def value?
61
+ type == :value
62
+ end
63
+
64
+ def ensure_context_not_missing
65
+ return if context
66
+
67
+ raise ArgumentError, "Missing context: #{context}"
68
+ end
69
+
70
+ attr_accessor :context, :callee, :type
71
+ end
72
+ end