jsi 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -24,7 +24,7 @@ module JSI
24
24
  # its schema is the metaschema.
25
25
  class MetaschemaNode
26
26
  include PathedNode
27
- include Memoize
27
+ include Util::Memoize
28
28
 
29
29
  # not every MetaschemaNode is actually an Enumerable, but it's better to include Enumerable on
30
30
  # the class than to conditionally extend the instance.
@@ -49,39 +49,39 @@ module JSI
49
49
  end
50
50
 
51
51
  instance_for_schema = node_document
52
- schema_ptr = node_ptr.reference_tokens.inject(root_schema_ptr) do |ptr0, tok|
52
+ schema_ptrs = node_ptr.reference_tokens.inject(Set.new << root_schema_ptr) do |ptrs, tok|
53
53
  if instance_for_schema.respond_to?(:to_ary)
54
- ptr1 = ptr0 && ptr0.schema_subschema_ptr_for_index(node_document, tok)
54
+ subschema_ptrs_for_token = ptrs.map do |ptr|
55
+ ptr.schema_subschema_ptrs_for_index(node_document, tok)
56
+ end.inject(Set.new, &:|)
55
57
  else
56
- ptr1 = ptr0 && ptr0.schema_subschema_ptr_for_property_name(node_document, tok)
58
+ subschema_ptrs_for_token = ptrs.map do |ptr|
59
+ ptr.schema_subschema_ptrs_for_property_name(node_document, tok)
60
+ end.inject(Set.new, &:|)
57
61
  end
58
62
  instance_for_schema = instance_for_schema[tok]
59
- ptr2 = ptr1 && ptr1.deref(node_document)
60
- ptr3 = ptr2 && ptr2.schema_match_ptr_to_instance(node_document, instance_for_schema)
61
- ptr3 # only God may judge my variable names
63
+ ptrs_for_instance = subschema_ptrs_for_token.map do |ptr|
64
+ ptr.schema_match_ptrs_to_instance(node_document, instance_for_schema)
65
+ end.inject(Set.new, &:|)
66
+ ptrs_for_instance
62
67
  end
63
68
 
64
- @schema = if schema_ptr
69
+ @jsi_schemas = schema_ptrs.map do |schema_ptr|
65
70
  if schema_ptr == node_ptr
66
71
  self
67
72
  else
68
73
  new_node(node_ptr: schema_ptr)
69
74
  end
70
- else
71
- nil
72
- end
75
+ end.to_set
73
76
 
74
- if schema_ptr
75
- if schema_ptr == metaschema_root_ptr
77
+ @jsi_schemas.each do |schema|
78
+ if schema.node_ptr == metaschema_root_ptr
76
79
  extend JSI::Schema
77
80
  end
78
- if schema_ptr == node_ptr
81
+ if schema.node_ptr == node_ptr
79
82
  extend Metaschema
80
83
  end
81
- end
82
-
83
- if @schema
84
- extend(JSI::SchemaClasses.accessor_module_for_schema(@schema, conflicting_modules: [Metaschema, Schema, MetaschemaNode, PathedArrayNode, PathedHashNode]))
84
+ extend(JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [Metaschema, Schema, MetaschemaNode, PathedArrayNode, PathedHashNode]))
85
85
  end
86
86
 
87
87
  # workarounds
@@ -106,8 +106,8 @@ module JSI
106
106
  attr_reader :metaschema_root_ptr
107
107
  # ptr to the schema of the root of the node_document
108
108
  attr_reader :root_schema_ptr
109
- # a JSI::Schema describing this MetaschemaNode
110
- attr_reader :schema
109
+ # JSI::Schemas describing this MetaschemaNode
110
+ attr_reader :jsi_schemas
111
111
 
112
112
  # @return [MetaschemaNode] document root MetaschemaNode
113
113
  def document_root_node
@@ -134,7 +134,7 @@ module JSI
134
134
  raise(NoMethodError, "cannot subcript (using token: #{token.inspect}) from content: #{node_content.pretty_inspect.chomp}")
135
135
  end
136
136
 
137
- jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
137
+ result = jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
138
138
  if token_in_range
139
139
  value_node = new_node(node_ptr: node_ptr[token])
140
140
 
@@ -148,6 +148,7 @@ module JSI
148
148
  nil
149
149
  end
150
150
  end
151
+ result
151
152
  end
152
153
 
153
154
  # if this MetaschemaNode is a $ref then the $ref is followed. otherwise this MetaschemaNode is returned.
@@ -186,13 +187,13 @@ module JSI
186
187
 
187
188
  # @return [Array<String>]
188
189
  def object_group_text
189
- if schema
190
- class_n_schema = "#{self.class} (#{schema.node_ptr.fragment})"
190
+ if jsi_schemas.any?
191
+ class_n_schemas = "#{self.class} (#{jsi_schemas.map { |s| s.node_ptr.uri }.join(' ')})"
191
192
  else
192
- class_n_schema = self.class.to_s
193
+ class_n_schemas = self.class.to_s
193
194
  end
194
195
  [
195
- class_n_schema,
196
+ class_n_schemas,
196
197
  is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
197
198
  *(node_content.respond_to?(:object_group_text) ? node_content.object_group_text : []),
198
199
  ].compact
@@ -202,7 +203,7 @@ module JSI
202
203
  def jsi_fingerprint
203
204
  {class: self.class, node_document: node_document}.merge(our_initialize_params)
204
205
  end
205
- include FingerprintHash
206
+ include Util::FingerprintHash
206
207
 
207
208
  private
208
209
 
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSI
4
- # JSI::Schema represents a JSON Schema. initialized from a Hash-like schema
5
- # object, JSI::Schema is a relatively simple class to abstract useful methods
6
- # applied to a JSON Schema.
4
+ # JSI::Schema is a module which extends instances which represent JSON schemas.
5
+ #
6
+ # the content of an instance which is a JSI::Schema (referred to in this context as schema_content) is
7
+ # expected to be a Hash (JSON object) or a Boolean.
7
8
  module Schema
8
9
  class Error < StandardError
9
10
  end
@@ -12,8 +13,6 @@ module JSI
12
13
  class NotASchemaError < Error
13
14
  end
14
15
 
15
- include Memoize
16
-
17
16
  # JSI::Schema::DescribesSchema: a schema which describes another schema. this module
18
17
  # extends a JSI::Schema instance and indicates that JSIs which instantiate the schema
19
18
  # are themselves also schemas.
@@ -109,14 +108,13 @@ module JSI
109
108
  parent_auri = Addressable::URI.parse(parent_id)
110
109
  if parent_auri.fragment
111
110
  # add onto the fragment
112
- parent_id_path = JSI::JSON::Pointer.from_fragment('#' + parent_auri.fragment).reference_tokens
111
+ parent_id_path = JSI::JSON::Pointer.from_fragment(parent_auri.fragment).reference_tokens
113
112
  path_from_id_node = parent_id_path + path_from_id_node
114
113
  parent_auri.fragment = nil
115
114
  #else: no fragment so parent_id good as is
116
115
  end
117
116
 
118
- fragment = JSI::JSON::Pointer.new(path_from_id_node).fragment
119
- schema_id = parent_auri.to_s + fragment
117
+ schema_id = parent_auri.merge(fragment: JSI::JSON::Pointer.new(path_from_id_node).fragment).to_s
120
118
 
121
119
  schema_id
122
120
  else
@@ -130,17 +128,20 @@ module JSI
130
128
  JSI::SchemaClasses.module_for_schema(self)
131
129
  end
132
130
 
133
- # @return [Class subclassing JSI::Base] shortcut for JSI.class_for_schema(schema)
131
+ # @return [Class < JSI::Base] a JSI class for this one schema
134
132
  def jsi_schema_class
135
- JSI.class_for_schema(self)
133
+ JSI.class_for_schemas(Set[self])
136
134
  end
137
135
 
138
- # calls #new on the class for this schema with the given arguments. for parameters,
139
- # see JSI::Base#initialize documentation.
136
+ # instantiates the given other_instance as a JSI::Base class for schemas matched from this schema to the
137
+ # other_instance.
138
+ #
139
+ # any parameters are passed to JSI::Base#initialize, but none are normally used.
140
140
  #
141
- # @return [JSI::Base] a JSI whose schema is this schema and whose instance is the given instance
141
+ # @return [JSI::Base] a JSI whose instance is the given instance and whose schemas are matched from this
142
+ # schema.
142
143
  def new_jsi(other_instance, *a, &b)
143
- JSI.class_for_schema(match_to_instance(other_instance)).new(other_instance, *a, &b)
144
+ JSI.class_for_schemas(match_to_instance(other_instance)).new(other_instance, *a, &b)
144
145
  end
145
146
 
146
147
  # @return [Boolean] does this schema itself describe a schema?
@@ -148,60 +149,48 @@ module JSI
148
149
  is_a?(JSI::Schema::DescribesSchema)
149
150
  end
150
151
 
151
- # if this schema is a oneOf, allOf, anyOf schema, #match_to_instance finds
152
- # one of the subschemas that matches the given instance and returns it. if
153
- # there are no matching *Of schemas, this schema is returned.
152
+ # checks this schema for applicators ($ref, allOf, etc.) which should be applied to the given instance.
153
+ # returns these as a Set of {JSI::Schema}s.
154
154
  #
155
- # @param other_instance [Object] the instance to which to attempt to match *Of subschemas
156
- # @return [JSI::Schema] a matched subschema, or this schema (self)
155
+ # the returned set will contain this schema itself, unless this schema contains a $ref keyword.
156
+ #
157
+ # @param other_instance [Object] the instance to check any applicators against
158
+ # @return [Set<JSI::Schema>] matched applicator schemas
157
159
  def match_to_instance(other_instance)
158
- ptr = node_ptr
159
- ptr = ptr.deref(node_document)
160
- ptr = ptr.schema_match_ptr_to_instance(node_document, other_instance)
161
- if ptr
160
+ node_ptr.schema_match_ptrs_to_instance(node_document, other_instance).map do |ptr|
162
161
  ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
163
- else
164
- self
165
- end
162
+ end.to_set
166
163
  end
167
164
 
168
- # @param property_name [String] the property name for which to find a subschema
169
- # @return [JSI::Schema, nil] a subschema from `properties`, `patternProperties`, or `additionalProperties` for the given token
170
- def subschema_for_property(property_name)
171
- jsi_memoize(:subschema_for_property, property_name) do |property_name|
172
- ptr = node_ptr
173
- ptr = ptr.deref(node_document)
174
- ptr = ptr.schema_subschema_ptr_for_property_name(node_document, property_name)
175
- if ptr
176
- ptr = ptr.deref(node_document)
165
+ # returns a set of subschemas of this schema for the given property name, from keywords
166
+ # `properties`, `patternProperties`, and `additionalProperties`.
167
+ #
168
+ # @param property_name [String] the property name for which to find subschemas
169
+ # @return [Set<JSI::Schema>] subschemas of this schema for the given property_name
170
+ def subschemas_for_property_name(property_name)
171
+ jsi_memoize(:subschemas_for_property_name, property_name) do |property_name|
172
+ node_ptr.schema_subschema_ptrs_for_property_name(node_document, property_name).map do |ptr|
177
173
  ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
178
- else
179
- nil
180
- end
174
+ end.to_set
181
175
  end
182
176
  end
183
177
 
184
- # @param index [Integer] the array index for which to find a subschema
185
- # @return [JSI::Schema, nil] a subschema from `items` or `additionalItems` for the given token
186
- def subschema_for_index(index)
187
- jsi_memoize(:subschema_for_index, index) do |index|
188
- ptr = node_ptr
189
- ptr = ptr.deref(node_document)
190
- ptr = ptr.schema_subschema_ptr_for_index(node_document, index)
191
- if ptr
192
- ptr = ptr.deref(node_document)
178
+ # returns a set of subschemas of this schema for the given array index, from keywords
179
+ # `items` and `additionalItems`.
180
+ #
181
+ # @param index [Integer] the array index for which to find subschemas
182
+ # @return [Set<JSI::Schema>] subschemas of this schema for the given array index
183
+ def subschemas_for_index(index)
184
+ jsi_memoize(:subschemas_for_index, index) do |index|
185
+ node_ptr.schema_subschema_ptrs_for_index(node_document, index).map do |ptr|
193
186
  ptr.evaluate(document_root_node).tap { |subschema| jsi_ensure_subschema_is_schema(subschema, ptr) }
194
- else
195
- nil
196
- end
187
+ end.to_set
197
188
  end
198
189
  end
199
190
 
200
- # @return [Set] any object property names this schema indicates may be
201
- # present on its instances. this includes, if present: keys of this
202
- # schema's "properties" object; entries of this schema's array of
203
- # "required" property keys. if this schema has allOf subschemas, those
204
- # schemas are checked (recursively) for their described object property names.
191
+ # @return [Set] any object property names this schema indicates may be present on its instances.
192
+ # this includes any keys of this schema's "properties" object and any entries of this schema's
193
+ # array of "required" property keys.
205
194
  def described_object_property_names
206
195
  jsi_memoize(:described_object_property_names) do
207
196
  Set.new.tap do |property_names|
@@ -211,12 +200,6 @@ module JSI
211
200
  if node_content.respond_to?(:to_hash) && node_content['required'].respond_to?(:to_ary)
212
201
  property_names.merge(node_content['required'].to_ary)
213
202
  end
214
- # we should look at dependencies (TODO).
215
- if respond_to?(:to_hash) && self['allOf'].respond_to?(:to_ary)
216
- self['allOf'].select{ |s| s.is_a?(JSI::Schema) }.map(&:deref).map do |allOf_schema|
217
- property_names.merge(allOf_schema.described_object_property_names)
218
- end
219
- end
220
203
  end
221
204
  end
222
205
  end
@@ -11,11 +11,11 @@ module JSI
11
11
 
12
12
  # @return [String]
13
13
  def inspect
14
- idfrag = schema.schema_id || schema.node_ptr.fragment
14
+ uri = schema.schema_id || schema.node_ptr.uri
15
15
  if name
16
- "#{name} (#{idfrag})"
16
+ "#{name} (#{uri})"
17
17
  else
18
- "(JSI Schema Module: #{idfrag})"
18
+ "(JSI Schema Module: #{uri})"
19
19
  end
20
20
  end
21
21
 
@@ -29,16 +29,16 @@ module JSI
29
29
  # this module is just a namespace for schema classes.
30
30
  module SchemaClasses
31
31
  class << self
32
- include Memoize
33
-
34
- # see {JSI.class_for_schema}
35
- def class_for_schema(schema_object)
36
- jsi_memoize(:class_for_schema, JSI::Schema.from_object(schema_object)) do |schema|
37
- Class.new(Base).instance_exec(schema) do |schema|
38
- define_singleton_method(:schema) { schema }
39
- define_method(:schema) { schema }
40
- include(schema.jsi_schema_module)
41
-
32
+ include Util::Memoize
33
+
34
+ # see {JSI.class_for_schemas}
35
+ def class_for_schemas(schema_objects)
36
+ schemas = schema_objects.map { |schema_object| JSI::Schema.from_object(schema_object) }.to_set
37
+ jsi_memoize(:class_for_schemas, schemas) do |schemas|
38
+ Class.new(Base).instance_exec(schemas) do |schemas|
39
+ define_singleton_method(:jsi_class_schemas) { schemas }
40
+ define_method(:jsi_schemas) { schemas }
41
+ schemas.each { |schema| include(schema.jsi_schema_module) }
42
42
  jsi_class = self
43
43
  define_method(:jsi_class) { jsi_class }
44
44
 
@@ -61,11 +61,13 @@ module JSI
61
61
 
62
62
  extend SchemaModule
63
63
 
64
- include JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [JSI::Base, JSI::BaseArray, JSI::BaseHash])
64
+ include JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [JSI::Base, JSI::PathedArrayNode, JSI::PathedHashNode])
65
65
 
66
66
  @possibly_schema_node = schema
67
67
  extend(SchemaModulePossibly)
68
- extend(JSI::SchemaClasses.accessor_module_for_schema(schema.schema, conflicting_modules: [Module, SchemaModule, SchemaModulePossibly]))
68
+ schema.jsi_schemas.each do |schema_schema|
69
+ extend(JSI::SchemaClasses.accessor_module_for_schema(schema_schema, conflicting_modules: [Module, SchemaModule, SchemaModulePossibly]))
70
+ end
69
71
  end
70
72
  end
71
73
  end
@@ -78,6 +80,9 @@ module JSI
78
80
  # @return [Module] a module of accessors (setters and getters) for described property names of the given
79
81
  # schema
80
82
  def accessor_module_for_schema(schema, conflicting_modules: )
83
+ unless schema.is_a?(JSI::Schema)
84
+ raise(JSI::Schema::NotASchemaError, "not a schema: #{schema.pretty_inspect.chomp}")
85
+ end
81
86
  jsi_memoize(:accessor_module_for_schema, schema, conflicting_modules) do |schema, conflicting_modules|
82
87
  Module.new.tap do |m|
83
88
  m.module_eval do
@@ -143,7 +148,9 @@ module JSI
143
148
  raise(TypeError, "cannot instantiate NotASchemaModule for a JSI::Schema node: #{node.pretty_inspect.chomp}")
144
149
  end
145
150
  @possibly_schema_node = node
146
- extend(JSI::SchemaClasses.accessor_module_for_schema(node.schema, conflicting_modules: [NotASchemaModule, SchemaModulePossibly]))
151
+ node.jsi_schemas.each do |schema|
152
+ extend(JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [NotASchemaModule, SchemaModulePossibly]))
153
+ end
147
154
  end
148
155
 
149
156
  include SchemaModulePossibly
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSI
4
+ # JSI::Util classes, modules, constants, and methods are INTERNAL and will be added and removed without warning.
5
+ # do not rely on them.
4
6
  module Util
5
7
  # a proc which does nothing
6
8
  NOOP = -> (*_) { }
@@ -16,7 +18,7 @@ module JSI
16
18
  # the return if you need to ensure it is not the same instance as the
17
19
  # argument instance.
18
20
  #
19
- # @param hash [#to_hash] the hash from which to convert symbol keys to strings
21
+ # @param hashlike [#to_hash] the hash from which to convert symbol keys to strings
20
22
  # @return [same class as the param `hash`, or Hash if the former cannot be done] a
21
23
  # hash(-like) instance containing no symbol keys
22
24
  def stringify_symbol_keys(hashlike)
@@ -65,43 +67,42 @@ module JSI
65
67
  proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
66
68
  end
67
69
  module_function :ycomb
68
- end
69
- public
70
- extend Util
71
70
 
72
- module FingerprintHash
73
- # overrides BasicObject#==
74
- def ==(other)
75
- object_id == other.object_id || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
76
- end
71
+ module FingerprintHash
72
+ # overrides BasicObject#==
73
+ def ==(other)
74
+ object_id == other.object_id || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
75
+ end
77
76
 
78
- alias_method :eql?, :==
77
+ alias_method :eql?, :==
79
78
 
80
- # overrides Kernel#hash
81
- def hash
82
- jsi_fingerprint.hash
79
+ # overrides Kernel#hash
80
+ def hash
81
+ jsi_fingerprint.hash
82
+ end
83
83
  end
84
- end
85
84
 
86
- module Memoize
87
- def jsi_memoize(key, *args_)
88
- @jsi_memos ||= {}
89
- @jsi_memos[key] ||= Hash.new do |h, args|
90
- h[args] = yield(*args)
85
+ module Memoize
86
+ def jsi_memoize(key, *args_)
87
+ @jsi_memos ||= {}
88
+ @jsi_memos[key] ||= Hash.new do |h, args|
89
+ h[args] = yield(*args)
90
+ end
91
+ @jsi_memos[key][args_]
91
92
  end
92
- @jsi_memos[key][args_]
93
- end
94
93
 
95
- def jsi_clear_memo(key, *args)
96
- @jsi_memos ||= {}
97
- if @jsi_memos[key]
98
- if args.empty?
99
- @jsi_memos[key].clear
100
- else
101
- @jsi_memos[key].delete(args)
94
+ def jsi_clear_memo(key, *args)
95
+ @jsi_memos ||= {}
96
+ if @jsi_memos[key]
97
+ if args.empty?
98
+ @jsi_memos[key].clear
99
+ else
100
+ @jsi_memos[key].delete(args)
101
+ end
102
102
  end
103
103
  end
104
104
  end
105
105
  end
106
- extend Memoize
106
+ public
107
+ extend Util
107
108
  end