nxt_schema 0.1.0

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