jsi-dev 0.0.0.pre.commonmarker

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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +8 -0
  3. data/CHANGELOG.md +101 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +303 -0
  6. data/docs/glossary.md +281 -0
  7. data/jsi.gemspec +30 -0
  8. data/lib/jsi/base/node.rb +373 -0
  9. data/lib/jsi/base.rb +738 -0
  10. data/lib/jsi/jsi_coder.rb +92 -0
  11. data/lib/jsi/metaschema.rb +6 -0
  12. data/lib/jsi/metaschema_node/bootstrap_schema.rb +126 -0
  13. data/lib/jsi/metaschema_node.rb +262 -0
  14. data/lib/jsi/ptr.rb +314 -0
  15. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  16. data/lib/jsi/schema/application/child_application/draft04.rb +21 -0
  17. data/lib/jsi/schema/application/child_application/draft06.rb +28 -0
  18. data/lib/jsi/schema/application/child_application/draft07.rb +28 -0
  19. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  20. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  21. data/lib/jsi/schema/application/child_application.rb +13 -0
  22. data/lib/jsi/schema/application/draft04.rb +8 -0
  23. data/lib/jsi/schema/application/draft06.rb +8 -0
  24. data/lib/jsi/schema/application/draft07.rb +8 -0
  25. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  26. data/lib/jsi/schema/application/inplace_application/draft04.rb +25 -0
  27. data/lib/jsi/schema/application/inplace_application/draft06.rb +26 -0
  28. data/lib/jsi/schema/application/inplace_application/draft07.rb +32 -0
  29. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  30. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  31. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  32. data/lib/jsi/schema/application/inplace_application.rb +14 -0
  33. data/lib/jsi/schema/application.rb +12 -0
  34. data/lib/jsi/schema/draft04.rb +13 -0
  35. data/lib/jsi/schema/draft06.rb +13 -0
  36. data/lib/jsi/schema/draft07.rb +13 -0
  37. data/lib/jsi/schema/issue.rb +36 -0
  38. data/lib/jsi/schema/ref.rb +183 -0
  39. data/lib/jsi/schema/schema_ancestor_node.rb +122 -0
  40. data/lib/jsi/schema/validation/array.rb +69 -0
  41. data/lib/jsi/schema/validation/const.rb +20 -0
  42. data/lib/jsi/schema/validation/contains.rb +25 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +110 -0
  46. data/lib/jsi/schema/validation/draft06.rb +120 -0
  47. data/lib/jsi/schema/validation/draft07.rb +157 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +49 -0
  63. data/lib/jsi/schema.rb +792 -0
  64. data/lib/jsi/schema_classes.rb +357 -0
  65. data/lib/jsi/schema_registry.rb +190 -0
  66. data/lib/jsi/schema_set.rb +219 -0
  67. data/lib/jsi/simple_wrap.rb +26 -0
  68. data/lib/jsi/util/private/attr_struct.rb +130 -0
  69. data/lib/jsi/util/private/memo_map.rb +75 -0
  70. data/lib/jsi/util/private.rb +202 -0
  71. data/lib/jsi/util/typelike.rb +225 -0
  72. data/lib/jsi/util.rb +227 -0
  73. data/lib/jsi/validation/error.rb +34 -0
  74. data/lib/jsi/validation/result.rb +212 -0
  75. data/lib/jsi/validation.rb +15 -0
  76. data/lib/jsi/version.rb +5 -0
  77. data/lib/jsi.rb +105 -0
  78. data/lib/schemas/json-schema.org/draft-04/schema.rb +169 -0
  79. data/lib/schemas/json-schema.org/draft-06/schema.rb +171 -0
  80. data/lib/schemas/json-schema.org/draft-07/schema.rb +198 -0
  81. data/readme.rb +138 -0
  82. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  83. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  84. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  85. metadata +155 -0
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # a module of methods for objects which behave like Hash but are not Hash.
5
+ #
6
+ # this module is intended to be internal to JSI. no guarantees or API promises
7
+ # are made for non-JSI classes including this module.
8
+ module Util::Hashlike
9
+ # safe methods which can be delegated to #to_hash (which the includer is assumed to have defined).
10
+ # 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
11
+
12
+ # methods which do not need to access the value.
13
+ SAFE_KEY_ONLY_METHODS = %w(each_key empty? has_key? include? key? keys length member? size).map(&:freeze).freeze
14
+ SAFE_KEY_VALUE_METHODS = %w(< <= > >= any? assoc compact dig each_pair each_value fetch fetch_values has_value? invert key merge rassoc reject select to_h to_proc transform_values value? values values_at).map(&:freeze).freeze
15
+ DESTRUCTIVE_METHODS = %w(clear delete delete_if keep_if reject! replace select! shift).map(&:freeze).freeze
16
+ # these return a modified copy
17
+ safe_modified_copy_methods = %w(compact)
18
+ # select and reject will return a modified copy but need the yielded block variable value from #[]
19
+ safe_kv_block_modified_copy_methods = %w(select reject)
20
+ SAFE_METHODS = SAFE_KEY_ONLY_METHODS | SAFE_KEY_VALUE_METHODS
21
+ custom_methods = %w(merge) # defined below
22
+ safe_to_hash_methods = SAFE_METHODS -
23
+ safe_modified_copy_methods -
24
+ safe_kv_block_modified_copy_methods -
25
+ custom_methods
26
+ safe_to_hash_methods.each do |method_name|
27
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
28
+ define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
29
+ else
30
+ define_method(method_name) { |*a, **kw, &b| to_hash.public_send(method_name, *a, **kw, &b) }
31
+ end
32
+ end
33
+ safe_modified_copy_methods.each do |method_name|
34
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
35
+ define_method(method_name) do |*a, &b|
36
+ jsi_modified_copy do |object_to_modify|
37
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
38
+ responsive_object.public_send(method_name, *a, &b)
39
+ end
40
+ end
41
+ else
42
+ define_method(method_name) do |*a, **kw, &b|
43
+ jsi_modified_copy do |object_to_modify|
44
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
45
+ responsive_object.public_send(method_name, *a, **kw, &b)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ safe_kv_block_modified_copy_methods.each do |method_name|
51
+ define_method(method_name) do |**kw, &b|
52
+ jsi_modified_copy do |object_to_modify|
53
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
54
+ responsive_object.public_send(method_name) do |k, _v|
55
+ b.call(k, self[k, **kw])
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # like [Hash#update](https://ruby-doc.org/core/Hash.html#method-i-update)
62
+ # @param other [#to_hash] the other hash to update this hash from
63
+ # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
64
+ # is determined by calling the block with the key, its value in self and its value in other.
65
+ # @return self, updated with other
66
+ # @raise [TypeError] when `other` does not respond to #to_hash
67
+ def update(other, &block)
68
+ unless other.respond_to?(:to_hash)
69
+ raise(TypeError, "cannot update with argument that does not respond to #to_hash: #{other.pretty_inspect.chomp}")
70
+ end
71
+ other.to_hash.each_pair do |key, value|
72
+ if block && key?(key)
73
+ value = yield(key, self[key], value)
74
+ end
75
+ self[key] = value
76
+ end
77
+ self
78
+ end
79
+
80
+ alias_method :merge!, :update
81
+
82
+ # like [Hash#merge](https://ruby-doc.org/core/Hash.html#method-i-merge)
83
+ # @param other [#to_hash] the other hash to merge into this
84
+ # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
85
+ # is determined by calling the block with the key, its value in self and its value in other.
86
+ # @return duplicate of this hash with the other hash merged in
87
+ # @raise [TypeError] when `other` does not respond to #to_hash
88
+ def merge(other, &block)
89
+ jsi_modified_copy do |instance|
90
+ instance.merge(other.is_a?(Base) ? other.jsi_node_content : other, &block)
91
+ end
92
+ end
93
+
94
+ # basically the same #inspect as Hash, but has the class name and, if responsive,
95
+ # self's #jsi_object_group_text
96
+ # @return [String]
97
+ def inspect
98
+ object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
99
+ -"\#{<#{object_group_str}>#{map { |k, v| " #{k.inspect} => #{v.inspect}" }.join(',')}}"
100
+ end
101
+
102
+ alias_method :to_s, :inspect
103
+
104
+ # pretty-prints a representation of this hashlike to the given printer
105
+ # @return [void]
106
+ def pretty_print(q)
107
+ object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
108
+ q.text "\#{<#{object_group_str}>"
109
+ q.group_sub {
110
+ q.nest(2) {
111
+ q.breakable ' ' if !empty?
112
+ q.seplist(self, nil, :each_pair) { |k, v|
113
+ q.group {
114
+ q.pp k
115
+ q.text ' => '
116
+ q.pp v
117
+ }
118
+ }
119
+ }
120
+ }
121
+ q.breakable '' if !empty?
122
+ q.text '}'
123
+ end
124
+ end
125
+
126
+ # a module of methods for objects which behave like Array but are not Array.
127
+ #
128
+ # this module is intended to be internal to JSI. no guarantees or API promises
129
+ # are made for non-JSI classes including this module.
130
+ module Util::Arraylike
131
+ # safe methods which can be delegated to #to_ary (which the includer is assumed to have defined).
132
+ # 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
133
+
134
+ # methods which do not need to access the element.
135
+ SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size).map(&:freeze).freeze
136
+ # there are some ambiguous ones that are omitted, like #sort, #map / #collect.
137
+ SAFE_INDEX_ELEMENT_METHODS = %w(| & * + - <=> abbrev at bsearch bsearch_index combination compact count cycle dig drop drop_while fetch find_index first include? index join last pack permutation product reject repeated_combination repeated_permutation reverse reverse_each rindex rotate sample select shelljoin shuffle slice sort take take_while transpose uniq values_at zip).map(&:freeze).freeze
138
+ DESTRUCTIVE_METHODS = %w(<< clear collect! compact! concat delete delete_at delete_if fill flatten! insert keep_if map! pop push reject! replace reverse! rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift).map(&:freeze).freeze
139
+
140
+ # methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
141
+ safe_modified_copy_methods = %w(compact)
142
+
143
+ # methods that return a modified copy and do need handling of block variables
144
+ safe_el_block_methods = %w(reject select)
145
+
146
+ SAFE_METHODS = SAFE_INDEX_ONLY_METHODS | SAFE_INDEX_ELEMENT_METHODS
147
+ safe_to_ary_methods = SAFE_METHODS - safe_modified_copy_methods - safe_el_block_methods
148
+ safe_to_ary_methods.each do |method_name|
149
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
150
+ define_method(method_name) { |*a, &b| to_ary.public_send(method_name, *a, &b) }
151
+ else
152
+ define_method(method_name) { |*a, **kw, &b| to_ary.public_send(method_name, *a, **kw, &b) }
153
+ end
154
+ end
155
+ safe_modified_copy_methods.each do |method_name|
156
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
157
+ define_method(method_name) do |*a, &b|
158
+ jsi_modified_copy do |object_to_modify|
159
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
160
+ responsive_object.public_send(method_name, *a, &b)
161
+ end
162
+ end
163
+ else
164
+ define_method(method_name) do |*a, **kw, &b|
165
+ jsi_modified_copy do |object_to_modify|
166
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
167
+ responsive_object.public_send(method_name, *a, **kw, &b)
168
+ end
169
+ end
170
+ end
171
+ end
172
+ safe_el_block_methods.each do |method_name|
173
+ define_method(method_name) do |**kw, &b|
174
+ jsi_modified_copy do |object_to_modify|
175
+ i = 0
176
+ responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
177
+ responsive_object.public_send(method_name) do |_e|
178
+ b.call(self[i, **kw]).tap { i += 1 }
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ # see [Array#assoc](https://ruby-doc.org/core/Array.html#method-i-assoc)
185
+ def assoc(obj)
186
+ # note: assoc implemented here (instead of delegated) due to inconsistencies in whether
187
+ # other implementations expect each element to be an Array or to respond to #to_ary
188
+ detect { |e| e.respond_to?(:to_ary) and e[0] == obj }
189
+ end
190
+
191
+ # see [Array#rassoc](https://ruby-doc.org/core/Array.html#method-i-rassoc)
192
+ def rassoc(obj)
193
+ # note: rassoc implemented here (instead of delegated) due to inconsistencies in whether
194
+ # other implementations expect each element to be an Array or to respond to #to_ary
195
+ detect { |e| e.respond_to?(:to_ary) and e[1] == obj }
196
+ end
197
+
198
+ # basically the same #inspect as Array, but has the class name and, if responsive,
199
+ # self's #jsi_object_group_text
200
+ # @return [String]
201
+ def inspect
202
+ object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
203
+ -"\#[<#{object_group_str}>#{map { |e| ' ' + e.inspect }.join(',')}]"
204
+ end
205
+
206
+ alias_method :to_s, :inspect
207
+
208
+ # pretty-prints a representation of this arraylike to the given printer
209
+ # @return [void]
210
+ def pretty_print(q)
211
+ object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
212
+ q.text "\#[<#{object_group_str}>"
213
+ q.group_sub {
214
+ q.nest(2) {
215
+ q.breakable ' ' if !empty?
216
+ q.seplist(self, nil, :each) { |e|
217
+ q.pp e
218
+ }
219
+ }
220
+ }
221
+ q.breakable '' if !empty?
222
+ q.text ']'
223
+ end
224
+ end
225
+ end
data/lib/jsi/util.rb ADDED
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # JSI::Util contains public utilities
5
+ module Util
6
+ autoload :Private, 'jsi/util/private'
7
+
8
+ include Private
9
+
10
+ extend self
11
+
12
+ autoload :Arraylike, 'jsi/util/typelike'
13
+ autoload :Hashlike, 'jsi/util/typelike'
14
+
15
+ # yields the content of the given param `object`. for objects which have a #jsi_modified_copy
16
+ # method of their own (JSI::Base, JSI::MetaschemaNode) that method is invoked with the given
17
+ # block. otherwise the given object itself is yielded.
18
+ #
19
+ # the given block must result in a modified copy of its block parameter
20
+ # (not destructively modifying the yielded content).
21
+ #
22
+ # @yield [Object] the content of the given object. the block should result
23
+ # in a (nondestructively) modified copy of this.
24
+ # @return [object.class] modified copy of the given object
25
+ def modified_copy(object, &block)
26
+ if object.respond_to?(:jsi_modified_copy)
27
+ object.jsi_modified_copy(&block)
28
+ else
29
+ yield(object)
30
+ end
31
+ end
32
+
33
+ # A structure like the given `object`, recursively coerced to JSON-compatible types.
34
+ #
35
+ # - Structures of Hash, Array, and basic types of String/number/boolean/nil are returned as-is.
36
+ # - If the object responds to `#as_json`, that method is used, passing any given options.
37
+ # - If the object supports [implicit conversion](https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html)
38
+ # with `#to_hash`, `#to_ary`, `#to_str`, or `#to_int`, that is used.
39
+ # - Set becomes Array; Symbol becomes String.
40
+ # - Types with no known coersion to JSON-compatible raise TypeError.
41
+ #
42
+ # @param object [Object]
43
+ # @return [Array, Hash, String, Integer, Float, Boolean, NilClass] a JSON-compatible structure like the given `object`
44
+ # @raise [TypeError] If the object cannot be coerced to a JSON-compatible structure
45
+ def as_json(object, options = {})
46
+ type_err = proc { raise(TypeError, "cannot express object as json: #{object.pretty_inspect.chomp}") }
47
+ if object.respond_to?(:as_json)
48
+ options.empty? ? object.as_json : object.as_json(**options) # TODO remove eventually (keyword argument compatibility)
49
+ elsif object.is_a?(Addressable::URI)
50
+ object.to_s
51
+ elsif object.respond_to?(:to_hash) && (object_to_hash = object.to_hash).is_a?(Hash)
52
+ result = {}
53
+ object_to_hash.each_pair do |k, v|
54
+ ks = k.is_a?(String) ? k :
55
+ k.is_a?(Symbol) ? k.to_s :
56
+ k.respond_to?(:to_str) && (kstr = k.to_str).is_a?(String) ? kstr :
57
+ raise(TypeError, "json object (hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
58
+ result[ks] = as_json(v, **options)
59
+ end
60
+ result
61
+ elsif object.respond_to?(:to_ary) && (object_to_ary = object.to_ary).is_a?(Array)
62
+ object_to_ary.map { |e| as_json(e, **options) }
63
+ elsif [String, Integer, TrueClass, FalseClass, NilClass].any? { |c| object.is_a?(c) }
64
+ object
65
+ elsif object.is_a?(Float)
66
+ type_err.call unless object.finite?
67
+ object
68
+ elsif object.is_a?(Symbol)
69
+ object.to_s
70
+ elsif object.is_a?(Set)
71
+ as_json(object.to_a, **options)
72
+ elsif object.respond_to?(:to_str) && (object_to_str = object.to_str).is_a?(String)
73
+ object_to_str
74
+ elsif object.respond_to?(:to_int) && (object_to_int = object.to_int).is_a?(Integer)
75
+ object_to_int
76
+ else
77
+ type_err.call
78
+ end
79
+ end
80
+
81
+ # A JSON encoded string of the given object.
82
+ #
83
+ # - If the object has a `#to_json` method that isn't defined by the stdlib `json` gem,
84
+ # that method is used, passing any given options.
85
+ # - Otherwise, JSON is generated using {as_json} to coerce to compatible types.
86
+ # @return [String]
87
+ def to_json(object, options = {})
88
+ if USE_TO_JSON_METHOD[object.class]
89
+ options.empty? ? object.to_json : object.to_json(**options) # TODO remove eventually (keyword argument compatibility)
90
+ else
91
+ JSON.generate(as_json(object, **options))
92
+ end
93
+ end
94
+
95
+ # a hash copied from the given hashlike, in which any symbol keys are
96
+ # converted to strings. behavior on collisions is undefined (but in the
97
+ # future could take a block like
98
+ # ActiveSupport::HashWithIndifferentAccess#update)
99
+ #
100
+ # at the moment it is undefined whether the returned hash is the same
101
+ # instance as the `hash` param. if `hash` is already a hash which contains
102
+ # no symbol keys, this method MAY return that same instance. use #dup on
103
+ # the return if you need to ensure it is not the same instance as the
104
+ # argument instance.
105
+ #
106
+ # @param hashlike [#to_hash] the hash from which to convert symbol keys to strings
107
+ # @return [same class as the param `hash`, or Hash if the former cannot be done] a
108
+ # hash(-like) instance containing no symbol keys
109
+ def stringify_symbol_keys(hashlike)
110
+ unless hashlike.respond_to?(:to_hash)
111
+ raise(ArgumentError, "expected argument to be a hash; got #{hashlike.class.inspect}: #{hashlike.pretty_inspect.chomp}")
112
+ end
113
+ JSI::Util.modified_copy(hashlike) do |hash|
114
+ out = {}
115
+ hash.each do |k, v|
116
+ out[k.is_a?(Symbol) ? k.to_s : k] = v
117
+ end
118
+ out
119
+ end
120
+ end
121
+
122
+ def deep_stringify_symbol_keys(object)
123
+ if object.respond_to?(:to_hash) && !object.is_a?(Addressable::URI)
124
+ JSI::Util.modified_copy(object) do |hash|
125
+ out = {}
126
+ (hash.respond_to?(:each) ? hash : hash.to_hash).each do |k, v|
127
+ out[k.is_a?(Symbol) ? k.to_s.freeze : deep_stringify_symbol_keys(k)] = deep_stringify_symbol_keys(v)
128
+ end
129
+ out
130
+ end
131
+ elsif object.respond_to?(:to_ary)
132
+ JSI::Util.modified_copy(object) do |ary|
133
+ (ary.respond_to?(:each) ? ary : ary.to_ary).map do |e|
134
+ deep_stringify_symbol_keys(e)
135
+ end
136
+ end
137
+ else
138
+ object
139
+ end
140
+ end
141
+
142
+ # returns an object which is equal to the param object, and is recursively frozen.
143
+ # the given object is not modified.
144
+ def deep_to_frozen(object, not_implemented: nil)
145
+ dtf = proc { |o| deep_to_frozen(o, not_implemented: not_implemented) }
146
+ if object.instance_of?(Hash)
147
+ out = {}
148
+ identical = object.frozen?
149
+ object.each do |k, v|
150
+ fk = dtf[k]
151
+ fv = dtf[v]
152
+ identical &&= fk.__id__ == k.__id__
153
+ identical &&= fv.__id__ == v.__id__
154
+ out[fk] = fv
155
+ end
156
+ if !object.default.nil?
157
+ out.default = dtf[object.default]
158
+ identical &&= out.default.__id__ == object.default.__id__
159
+ end
160
+ if object.default_proc
161
+ raise(ArgumentError, "cannot make immutable copy of a Hash with default_proc")
162
+ end
163
+ if identical
164
+ object
165
+ else
166
+ out.freeze
167
+ end
168
+ elsif object.instance_of?(Array)
169
+ identical = object.frozen?
170
+ out = Array.new(object.size)
171
+ object.each_with_index do |e, i|
172
+ fe = dtf[e]
173
+ identical &&= fe.__id__ == e.__id__
174
+ out[i] = fe
175
+ end
176
+ if identical
177
+ object
178
+ else
179
+ out.freeze
180
+ end
181
+ elsif object.instance_of?(String)
182
+ if object.frozen?
183
+ object
184
+ else
185
+ object.dup.freeze
186
+ end
187
+ elsif CLASSES_ALWAYS_FROZEN.any? { |c| object.is_a?(c) } # note: `is_a?`, not `instance_of?`, here because instance_of?(Integer) is false until Fixnum/Bignum is gone. this is fine here; there is no concern of subclasses of CLASSES_ALWAYS_FROZEN duping/freezing differently (as with e.g. ActiveSupport::HashWithIndifferentAccess)
188
+ object
189
+ else
190
+ if not_implemented
191
+ not_implemented.call(object)
192
+ else
193
+ raise(NotImplementedError, [
194
+ "deep_to_frozen not implemented for class: #{object.class}",
195
+ "object: #{object.pretty_inspect.chomp}",
196
+ ].join("\n"))
197
+ end
198
+ end
199
+ end
200
+
201
+ # ensures the given param becomes a frozen Set of Modules.
202
+ # returns the param if it is already that, otherwise initializes and freezes such a Set.
203
+ #
204
+ # @param modules [Set, Enumerable] the object to ensure becomes a frozen Set of Modules
205
+ # @return [Set] frozen Set containing the given modules
206
+ # @raise [ArgumentError] when the modules param is not an Enumerable
207
+ # @raise [Schema::NotASchemaError] when the modules param contains objects which are not Schemas
208
+ def ensure_module_set(modules)
209
+ if modules.is_a?(Set) && modules.frozen?
210
+ set = modules
211
+ elsif modules.is_a?(Enumerable)
212
+ set = Set.new(modules).freeze
213
+ else
214
+ raise(TypeError, "not given an Enumerable of Modules")
215
+ end
216
+ not_modules = set.reject { |s| s.is_a?(Module) }
217
+ if !not_modules.empty?
218
+ raise(TypeError, [
219
+ "ensure_module_set given non-Module objects:",
220
+ *not_modules.map { |ns| ns.pretty_inspect.chomp },
221
+ ].join("\n"))
222
+ end
223
+
224
+ set
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Validation
5
+ Error = Util::AttrStruct[*%w(
6
+ message
7
+ keyword
8
+ schema
9
+ instance_ptr
10
+ instance_document
11
+ )]
12
+
13
+ # a validation error of a schema instance against a schema
14
+ #
15
+ # @!attribute message
16
+ # a message describing the error
17
+ # @return [String]
18
+ # @!attribute keyword
19
+ # the keyword of the schema which failed to validate.
20
+ # this may be absent if the error is not from a schema keyword (i.e, `false` schema).
21
+ # @return [String]
22
+ # @!attribute schema
23
+ # the schema against which the instance failed to validate
24
+ # @return [JSI::Schema]
25
+ # @!attribute instance_ptr
26
+ # pointer to the instance in instance_document
27
+ # @return [JSI::Ptr]
28
+ # @!attribute instance_document
29
+ # document containing the instance at instance_ptr
30
+ # @return [Object]
31
+ class Error
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Validation
5
+ # a result of validating an instance against schemas which describe it.
6
+ # virtual base class.
7
+ class Result
8
+ include Util::Virtual
9
+
10
+ Builder = Util::AttrStruct[*%w(
11
+ result
12
+ schema
13
+ instance_ptr
14
+ instance_document
15
+ validate_only
16
+ visited_refs
17
+ )]
18
+
19
+ # @private
20
+ # a structure used to build a Result. virtual base class.
21
+ class Builder
22
+ def instance
23
+ instance_ptr.evaluate(instance_document)
24
+ end
25
+
26
+ def schema_issue(*_)
27
+ virtual_method
28
+ end
29
+
30
+ def schema_error(message, keyword = nil)
31
+ schema_issue(:error, message, keyword)
32
+ end
33
+
34
+ def schema_warning(message, keyword = nil)
35
+ schema_issue(:warning, message, keyword)
36
+ end
37
+
38
+ # @param subschema_ptr [JSI::Ptr, #to_ary]
39
+ # @return [JSI::Validation::Result]
40
+ def inplace_subschema_validate(subschema_ptr)
41
+ subresult = schema.subschema(subschema_ptr).internal_validate_instance(
42
+ instance_ptr,
43
+ instance_document,
44
+ validate_only: validate_only,
45
+ visited_refs: visited_refs,
46
+ )
47
+ merge_schema_issues(subresult)
48
+ subresult
49
+ end
50
+
51
+ # @param instance_child_token [String, Integer]
52
+ # @param subschema_ptr [JSI::Ptr, #to_ary]
53
+ # @return [JSI::Validation::Result]
54
+ def child_subschema_validate(instance_child_token, subschema_ptr)
55
+ subresult = schema.subschema(subschema_ptr).internal_validate_instance(
56
+ instance_ptr[instance_child_token],
57
+ instance_document,
58
+ validate_only: validate_only,
59
+ )
60
+ merge_schema_issues(subresult)
61
+ subresult
62
+ end
63
+
64
+ # @param other_result [JSI::Validation::Result]
65
+ # @return [void]
66
+ def merge_schema_issues(other_result)
67
+ unless validate_only
68
+ # schema_issues are always merged from subschema results (not depending on validation results)
69
+ result.schema_issues.merge(other_result.schema_issues)
70
+ end
71
+ nil
72
+ end
73
+ end
74
+
75
+ def builder(schema, instance_ptr, instance_document, validate_only, visited_refs)
76
+ self.class::Builder.new(
77
+ result: self,
78
+ schema: schema,
79
+ instance_ptr: instance_ptr,
80
+ instance_document: instance_document,
81
+ validate_only: validate_only,
82
+ visited_refs: visited_refs,
83
+ )
84
+ end
85
+
86
+ # is the instance valid against its schemas?
87
+ # @return [Boolean]
88
+ def valid?
89
+ # :nocov:
90
+ virtual_method
91
+ # :nocov:
92
+ end
93
+
94
+ include Util::FingerprintHash
95
+ end
96
+
97
+ # a full result of validating an instance against its schemas, with each validation error
98
+ class FullResult < Result
99
+ # @private
100
+ class Builder < Result::Builder
101
+ def validate(
102
+ valid,
103
+ message,
104
+ keyword: nil,
105
+ results: []
106
+ )
107
+ results.each { |res| result.schema_issues.merge(res.schema_issues) }
108
+ if !valid
109
+ results.each { |res| result.validation_errors.merge(res.validation_errors) }
110
+ result.validation_errors << Validation::Error.new({
111
+ message: message,
112
+ keyword: keyword,
113
+ schema: schema,
114
+ instance_ptr: instance_ptr,
115
+ instance_document: instance_document,
116
+ })
117
+ end
118
+ end
119
+
120
+ def schema_issue(level, message, keyword = nil)
121
+ result.schema_issues << Schema::Issue.new({
122
+ level: level,
123
+ message: message,
124
+ keyword: keyword,
125
+ schema: schema,
126
+ })
127
+ end
128
+ end
129
+
130
+ def initialize
131
+ @validation_errors = Set.new
132
+ @schema_issues = Set.new
133
+ end
134
+
135
+ attr_reader :validation_errors
136
+ attr_reader :schema_issues
137
+
138
+ def valid?
139
+ validation_errors.empty?
140
+ end
141
+
142
+ def freeze
143
+ @validation_errors.each(&:freeze)
144
+ @schema_issues.each(&:freeze)
145
+ @validation_errors.freeze
146
+ @schema_issues.freeze
147
+ super
148
+ end
149
+
150
+ def merge(result)
151
+ unless result.is_a?(FullResult)
152
+ raise(TypeError, "not a #{FullResult.name}: #{result.pretty_inspect.chomp}")
153
+ end
154
+ validation_errors.merge(result.validation_errors)
155
+ schema_issues.merge(result.schema_issues)
156
+ self
157
+ end
158
+
159
+ def +(result)
160
+ FullResult.new.merge(self).merge(result)
161
+ end
162
+
163
+ # see {Util::Private::FingerprintHash}
164
+ # @api private
165
+ def jsi_fingerprint
166
+ {
167
+ class: self.class,
168
+ validation_errors: validation_errors,
169
+ schema_issues: schema_issues,
170
+ }
171
+ end
172
+ end
173
+
174
+ # a result indicating only whether an instance is valid against its schemas
175
+ class ValidityResult < Result
176
+ # @private
177
+ class Builder < Result::Builder
178
+ def validate(
179
+ valid,
180
+ message,
181
+ keyword: nil,
182
+ results: []
183
+ )
184
+ if !valid
185
+ throw(:jsi_validation_result, INVALID)
186
+ end
187
+ end
188
+
189
+ def schema_issue(*_)
190
+ # noop
191
+ end
192
+ end
193
+
194
+ def initialize(valid)
195
+ @valid = valid
196
+ end
197
+
198
+ def valid?
199
+ @valid
200
+ end
201
+
202
+ # see {Util::Private::FingerprintHash}
203
+ # @api private
204
+ def jsi_fingerprint
205
+ {
206
+ class: self.class,
207
+ valid: valid?,
208
+ }
209
+ end
210
+ end
211
+ end
212
+ end