jsi-dev 0.0.0.pre.commonmarker

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