nxt_schema 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +3 -3
  3. data/README.md +80 -38
  4. data/lib/nxt_schema.rb +17 -17
  5. data/lib/nxt_schema/dsl.rb +7 -7
  6. data/lib/nxt_schema/{application.rb → node.rb} +1 -1
  7. data/lib/nxt_schema/node/any_of.rb +21 -33
  8. data/lib/nxt_schema/node/base.rb +70 -171
  9. data/lib/nxt_schema/node/collection.rb +43 -8
  10. data/lib/nxt_schema/{application → node}/error_store.rb +5 -5
  11. data/lib/nxt_schema/{application → node}/errors/schema_error.rb +1 -1
  12. data/lib/nxt_schema/{application → node}/errors/validation_error.rb +1 -1
  13. data/lib/nxt_schema/node/leaf.rb +7 -5
  14. data/lib/nxt_schema/node/schema.rb +101 -8
  15. data/lib/nxt_schema/template/any_of.rb +50 -0
  16. data/lib/nxt_schema/template/base.rb +218 -0
  17. data/lib/nxt_schema/template/collection.rb +23 -0
  18. data/lib/nxt_schema/{node → template}/has_sub_nodes.rb +16 -10
  19. data/lib/nxt_schema/template/leaf.rb +13 -0
  20. data/lib/nxt_schema/{node → template}/maybe_evaluator.rb +1 -1
  21. data/lib/nxt_schema/{node → template}/on_evaluator.rb +1 -1
  22. data/lib/nxt_schema/template/schema.rb +22 -0
  23. data/lib/nxt_schema/{node → template}/sub_nodes.rb +1 -1
  24. data/lib/nxt_schema/{node → template}/type_resolver.rb +2 -2
  25. data/lib/nxt_schema/{node → template}/type_system_resolver.rb +1 -1
  26. data/lib/nxt_schema/version.rb +1 -1
  27. metadata +17 -17
  28. data/lib/nxt_schema/application/any_of.rb +0 -40
  29. data/lib/nxt_schema/application/base.rb +0 -116
  30. data/lib/nxt_schema/application/collection.rb +0 -57
  31. data/lib/nxt_schema/application/leaf.rb +0 -15
  32. data/lib/nxt_schema/application/schema.rb +0 -114
@@ -0,0 +1,50 @@
1
+ module NxtSchema
2
+ module Template
3
+ class AnyOf < Base
4
+ include HasSubNodes
5
+
6
+ def initialize(name:, type: nil, parent_node:, **options, &block)
7
+ super
8
+ ensure_sub_nodes_present
9
+ end
10
+
11
+ def collection(name = sub_nodes.count, type = NxtSchema::Template::Collection::DEFAULT_TYPE, **options, &block)
12
+ super
13
+ end
14
+
15
+ def schema(name = sub_nodes.count, type = NxtSchema::Template::Schema::DEFAULT_TYPE, **options, &block)
16
+ super
17
+ end
18
+
19
+ def node(name = sub_nodes.count, node_or_type_of_node = nil, **options, &block)
20
+ super
21
+ end
22
+
23
+ def on(*args)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def maybe(*args)
28
+ raise NotImplementedError
29
+ end
30
+
31
+ private
32
+
33
+ def resolve_type(name_or_type)
34
+ nil
35
+ end
36
+
37
+ def resolve_optional_option
38
+ return unless options.key?(:optional)
39
+
40
+ raise InvalidOptions, "The optional option is not available for nodes of type #{self.class.name}"
41
+ end
42
+
43
+ def resolve_omnipresent_option
44
+ return unless options.key?(:omnipresent)
45
+
46
+ raise InvalidOptions, "The omnipresent option is not available for nodes of type #{self.class.name}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,218 @@
1
+ module NxtSchema
2
+ module Template
3
+ class Base
4
+ def initialize(name:, type:, parent_node:, **options, &block)
5
+ resolve_name(name)
6
+
7
+ @parent_node = parent_node
8
+ @options = options
9
+ @is_root_node = parent_node.nil?
10
+ @root_node = parent_node.nil? ? self : parent_node.root_node
11
+ @path = resolve_path
12
+ @on_evaluators = []
13
+ @maybe_evaluators = []
14
+ @validations = []
15
+ @configuration = block
16
+
17
+ resolve_key_transformer
18
+ resolve_context
19
+ resolve_optional_option
20
+ resolve_omnipresent_option
21
+ resolve_type_system
22
+ resolve_type(type)
23
+ resolve_additional_keys_strategy
24
+ application_class # memoize
25
+ configure(&block) if block_given?
26
+ end
27
+
28
+ attr_accessor :name,
29
+ :parent_node,
30
+ :options,
31
+ :type,
32
+ :root_node,
33
+ :additional_keys_strategy
34
+
35
+ attr_reader :type_system,
36
+ :path,
37
+ :context,
38
+ :meta,
39
+ :on_evaluators,
40
+ :maybe_evaluators,
41
+ :validations,
42
+ :configuration,
43
+ :key_transformer
44
+
45
+ def apply(input: MissingInput.new, context: self.context, parent: nil, error_key: nil)
46
+ build_application(input: input, context: context, parent: parent, error_key: error_key).call
47
+ end
48
+
49
+ def apply!(input: MissingInput.new, context: self.context, parent: nil, error_key: nil)
50
+ result = build_application(input: input, context: context, parent: parent, error_key: error_key).call
51
+ return result if parent || result.errors.empty?
52
+
53
+ raise NxtSchema::Errors::Invalid.new(result)
54
+ end
55
+
56
+ def build_application(input: MissingInput.new, context: self.context, parent: nil, error_key: nil)
57
+ application_class.new(
58
+ node: self,
59
+ input: input,
60
+ parent: parent,
61
+ context: context,
62
+ error_key: error_key
63
+ )
64
+ end
65
+
66
+ def root_node?
67
+ @is_root_node
68
+ end
69
+
70
+ def optional?
71
+ @optional
72
+ end
73
+
74
+ def omnipresent?
75
+ @omnipresent
76
+ end
77
+
78
+ def default(value = NxtSchema::MissingInput.new, &block)
79
+ value = missing_input?(value) ? block : value
80
+ condition = ->(input) { missing_input?(input) || input.nil? }
81
+ on(condition, value)
82
+
83
+ self
84
+ end
85
+
86
+ def on(condition, value = NxtSchema::MissingInput.new, &block)
87
+ value = missing_input?(value) ? block : value
88
+ on_evaluators << OnEvaluator.new(condition: condition, value: value)
89
+
90
+ self
91
+ end
92
+
93
+ def maybe(value = NxtSchema::MissingInput.new, &block)
94
+ value = missing_input?(value) ? block : value
95
+ maybe_evaluators << MaybeEvaluator.new(value: value)
96
+
97
+ self
98
+ end
99
+
100
+ def validate(key = NxtSchema::MissingInput.new, *args, &block)
101
+ # TODO: This does not really work with all kinds of chaining combinations yet!
102
+
103
+ validator = if key.is_a?(Symbol)
104
+ validator(key, *args)
105
+ elsif key.respond_to?(:call)
106
+ key
107
+ elsif block_given?
108
+ if key.is_a?(NxtSchema::MissingInput)
109
+ block
110
+ else
111
+ configure(&block)
112
+ end
113
+ else
114
+ raise ArgumentError, "Don't know how to resolve validator from: #{key} with: #{args} #{block}"
115
+ end
116
+
117
+ register_validator(validator)
118
+
119
+ self
120
+ end
121
+
122
+ def validate_with(&block)
123
+ proxy = ->(node) { NxtSchema::Validator::ValidateWithProxy.new(node).validate(&block) }
124
+ register_validator(proxy)
125
+ end
126
+
127
+ private
128
+
129
+ attr_writer :path, :meta, :context, :on_evaluators, :maybe_evaluators
130
+
131
+ def validator(key, *args)
132
+ Validators::REGISTRY.resolve!(key).new(*args).build
133
+ end
134
+
135
+ def register_validator(validator)
136
+ validations << validator
137
+ end
138
+
139
+ def resolve_type(name_or_type)
140
+ @type = root_node.send(:type_resolver).resolve(type_system, name_or_type)
141
+ end
142
+
143
+ def resolve_type_system
144
+ @type_system = TypeSystemResolver.new(node: self).call
145
+ end
146
+
147
+ def type_resolver
148
+ @type_resolver ||= begin
149
+ root_node? ? TypeResolver.new : (raise NoMethodError, 'type_resolver is only available on root node')
150
+ end
151
+ end
152
+
153
+ def application_class
154
+ @application_class ||= "NxtSchema::Node::#{self.class.name.demodulize}".constantize
155
+ end
156
+
157
+ def configure(&block)
158
+ if block.arity == 1
159
+ block.call(self)
160
+ else
161
+ instance_exec(&block)
162
+ end
163
+ end
164
+
165
+ def resolve_additional_keys_strategy
166
+ @additional_keys_strategy = options.fetch(:additional_keys) do
167
+ parent_node&.send(:additional_keys_strategy) || :allow
168
+ end
169
+ end
170
+
171
+ def resolve_optional_option
172
+ optional = options.fetch(:optional, false)
173
+ raise Errors::InvalidOptions, 'Optional nodes are only available within schemas' if optional && !parent_node.is_a?(Schema)
174
+ raise Errors::InvalidOptions, "Can't make omnipresent node optional" if optional && omnipresent?
175
+
176
+ if optional.respond_to?(:call)
177
+ # When a node is conditionally optional we make it optional and add a validator to the parent to check
178
+ # that it's there when the option does not apply.
179
+ optional_node_validator = validator(:optional_node, optional, name)
180
+ parent_node.send(:register_validator, optional_node_validator)
181
+ @optional = true
182
+ else
183
+ @optional = optional
184
+ end
185
+ end
186
+
187
+ def resolve_omnipresent_option
188
+ omnipresent = options.fetch(:omnipresent, false)
189
+ raise Errors::InvalidOptions, 'Omnipresent nodes are only available within schemas' if omnipresent && !parent_node.is_a?(Schema)
190
+ raise Errors::InvalidOptions, "Can't make omnipresent node optional" if optional? && omnipresent
191
+
192
+ @omnipresent = omnipresent
193
+ end
194
+
195
+ def resolve_path
196
+ self.path = root_node? ? name : "#{parent_node.path}.#{name}"
197
+ end
198
+
199
+ def resolve_context
200
+ self.context = options.fetch(:context) { parent_node&.send(:context) }
201
+ end
202
+
203
+ def missing_input?(value)
204
+ value.is_a? MissingInput
205
+ end
206
+
207
+ def resolve_key_transformer
208
+ @key_transformer = options.fetch(:transform_keys) { parent_node&.key_transformer || ->(key) { key.to_sym } }
209
+ end
210
+
211
+ def resolve_name(name)
212
+ raise ArgumentError, 'Name can either be a symbol or an integer' unless name.class.in?([Symbol, Integer])
213
+
214
+ @name = name
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Template
3
+ class Collection < Template::Base
4
+ include HasSubNodes
5
+
6
+ DEFAULT_TYPE = NxtSchema::Types::Strict::Array
7
+
8
+ def initialize(name:, type: DEFAULT_TYPE, parent_node:, **options, &block)
9
+ super
10
+ ensure_sub_nodes_present
11
+ end
12
+
13
+ private
14
+
15
+ def add_sub_node(node)
16
+ # TODO: Spec that this raises
17
+ raise ArgumentError, "It's not possible to define multiple nodes within a collection" unless sub_nodes.empty?
18
+
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,8 +1,8 @@
1
1
  module NxtSchema
2
- module Node
2
+ module Template
3
3
  module HasSubNodes
4
- def collection(name, type = NxtSchema::Node::Collection::DEFAULT_TYPE, **options, &block)
5
- node = NxtSchema::Node::Collection.new(
4
+ def collection(name, type = NxtSchema::Template::Collection::DEFAULT_TYPE, **options, &block)
5
+ node = NxtSchema::Template::Collection.new(
6
6
  name: name,
7
7
  type: type,
8
8
  parent_node: self,
@@ -15,8 +15,8 @@ module NxtSchema
15
15
 
16
16
  alias nodes collection
17
17
 
18
- def schema(name, type = NxtSchema::Node::Schema::DEFAULT_TYPE, **options, &block)
19
- node = NxtSchema::Node::Schema.new(
18
+ def schema(name, type = NxtSchema::Template::Schema::DEFAULT_TYPE, **options, &block)
19
+ node = NxtSchema::Template::Schema.new(
20
20
  name: name,
21
21
  type: type,
22
22
  parent_node: self,
@@ -28,7 +28,7 @@ module NxtSchema
28
28
  end
29
29
 
30
30
  def any_of(name, **options, &block)
31
- node = NxtSchema::Node::AnyOf.new(
31
+ node = NxtSchema::Template::AnyOf.new(
32
32
  name: name,
33
33
  parent_node: self,
34
34
  **options,
@@ -39,18 +39,18 @@ module NxtSchema
39
39
  end
40
40
 
41
41
  def node(name, node_or_type_of_node, **options, &block)
42
- node = if node_or_type_of_node.is_a?(NxtSchema::Node::Base)
42
+ node = if node_or_type_of_node.is_a?(NxtSchema::Template::Base)
43
43
  raise ArgumentError, "Can't provide a block along with a node" if block.present?
44
44
 
45
45
  node_or_type_of_node.class.new(
46
46
  name: name,
47
47
  type: node_or_type_of_node.type,
48
48
  parent_node: self,
49
- **node_or_type_of_node.options.merge(options), # Does this make sense to merge options here?
49
+ **node_or_type_of_node.options.merge(options),
50
50
  &node_or_type_of_node.configuration
51
51
  )
52
52
  else
53
- NxtSchema::Node::Leaf.new(
53
+ NxtSchema::Template::Leaf.new(
54
54
  name: name,
55
55
  type: node_or_type_of_node,
56
56
  parent_node: self,
@@ -70,12 +70,18 @@ module NxtSchema
70
70
  end
71
71
 
72
72
  def sub_nodes
73
- @sub_nodes ||= Node::SubNodes.new
73
+ @sub_nodes ||= Template::SubNodes.new
74
74
  end
75
75
 
76
76
  def [](key)
77
77
  sub_nodes[key]
78
78
  end
79
+
80
+ def ensure_sub_nodes_present
81
+ return if sub_nodes.any?
82
+
83
+ raise NxtSchema::Errors::InvalidOptions, "#{self.class.name} must have sub nodes"
84
+ end
79
85
  end
80
86
  end
81
87
  end
@@ -0,0 +1,13 @@
1
+ module NxtSchema
2
+ module Template
3
+ class Leaf < Template::Base
4
+ def initialize(name:, type: :String, parent_node:, **options, &block)
5
+ super
6
+ end
7
+
8
+ def leaf?
9
+ true
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  module NxtSchema
2
- module Node
2
+ module Template
3
3
  class MaybeEvaluator
4
4
  def initialize(value:)
5
5
  @value = value
@@ -1,5 +1,5 @@
1
1
  module NxtSchema
2
- module Node
2
+ module Template
3
3
  class OnEvaluator
4
4
  def initialize(condition:, value:)
5
5
  @condition = condition
@@ -0,0 +1,22 @@
1
+ module NxtSchema
2
+ module Template
3
+ class Schema < Template::Base
4
+ include HasSubNodes
5
+
6
+ DEFAULT_TYPE = NxtSchema::Types::Strict::Hash
7
+
8
+ def initialize(name:, type: DEFAULT_TYPE, parent_node:, **options, &block)
9
+ super
10
+ ensure_sub_nodes_present
11
+ end
12
+
13
+ def optional(name, node_or_type_of_node, **options, &block)
14
+ node(name, node_or_type_of_node, **options.merge(optional: true), &block)
15
+ end
16
+
17
+ def omnipresent(name, node_or_type_of_node, **options, &block)
18
+ node(name, node_or_type_of_node, **options.merge(omnipresent: true), &block)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  module NxtSchema
2
- module Node
2
+ module Template
3
3
  class SubNodes < ::Hash
4
4
  def initialize
5
5
  super
@@ -1,5 +1,5 @@
1
1
  module NxtSchema
2
- module Node
2
+ module Template
3
3
  class TypeResolver
4
4
  def resolve(type_system, type)
5
5
  @resolve ||= {}
@@ -21,4 +21,4 @@ module NxtSchema
21
21
  end
22
22
  end
23
23
  end
24
- end
24
+ end
@@ -1,5 +1,5 @@
1
1
  module NxtSchema
2
- module Node
2
+ module Template
3
3
  class TypeSystemResolver
4
4
  include NxtInit
5
5
  attr_init :node