jsi 0.4.0 → 0.6.0

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +15 -0
  4. data/README.md +105 -38
  5. data/lib/jsi/base.rb +349 -155
  6. data/lib/jsi/jsi_coder.rb +5 -4
  7. data/lib/jsi/metaschema_node/bootstrap_schema.rb +100 -0
  8. data/lib/jsi/metaschema_node.rb +156 -129
  9. data/lib/jsi/pathed_node.rb +47 -49
  10. data/lib/jsi/ptr.rb +292 -0
  11. data/lib/jsi/schema/application/child_application/contains.rb +16 -0
  12. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  13. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  14. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  15. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  16. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  17. data/lib/jsi/schema/application/child_application.rb +40 -0
  18. data/lib/jsi/schema/application/draft04.rb +8 -0
  19. data/lib/jsi/schema/application/draft06.rb +8 -0
  20. data/lib/jsi/schema/application/draft07.rb +8 -0
  21. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  22. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  23. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  24. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  25. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  26. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  27. data/lib/jsi/schema/application/inplace_application/someof.rb +29 -0
  28. data/lib/jsi/schema/application/inplace_application.rb +46 -0
  29. data/lib/jsi/schema/application.rb +12 -0
  30. data/lib/jsi/schema/draft04.rb +14 -0
  31. data/lib/jsi/schema/draft06.rb +14 -0
  32. data/lib/jsi/schema/draft07.rb +14 -0
  33. data/lib/jsi/schema/issue.rb +36 -0
  34. data/lib/jsi/schema/ref.rb +159 -0
  35. data/lib/jsi/schema/schema_ancestor_node.rb +119 -0
  36. data/lib/jsi/schema/validation/array.rb +69 -0
  37. data/lib/jsi/schema/validation/const.rb +20 -0
  38. data/lib/jsi/schema/validation/contains.rb +25 -0
  39. data/lib/jsi/schema/validation/core.rb +39 -0
  40. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  41. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  42. data/lib/jsi/schema/validation/draft04.rb +112 -0
  43. data/lib/jsi/schema/validation/draft06.rb +122 -0
  44. data/lib/jsi/schema/validation/draft07.rb +159 -0
  45. data/lib/jsi/schema/validation/enum.rb +25 -0
  46. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  47. data/lib/jsi/schema/validation/items.rb +54 -0
  48. data/lib/jsi/schema/validation/not.rb +20 -0
  49. data/lib/jsi/schema/validation/numeric.rb +121 -0
  50. data/lib/jsi/schema/validation/object.rb +45 -0
  51. data/lib/jsi/schema/validation/pattern.rb +34 -0
  52. data/lib/jsi/schema/validation/properties.rb +101 -0
  53. data/lib/jsi/schema/validation/property_names.rb +32 -0
  54. data/lib/jsi/schema/validation/ref.rb +40 -0
  55. data/lib/jsi/schema/validation/required.rb +27 -0
  56. data/lib/jsi/schema/validation/someof.rb +90 -0
  57. data/lib/jsi/schema/validation/string.rb +47 -0
  58. data/lib/jsi/schema/validation/type.rb +49 -0
  59. data/lib/jsi/schema/validation.rb +51 -0
  60. data/lib/jsi/schema.rb +486 -133
  61. data/lib/jsi/schema_classes.rb +157 -42
  62. data/lib/jsi/schema_registry.rb +141 -0
  63. data/lib/jsi/schema_set.rb +141 -0
  64. data/lib/jsi/simple_wrap.rb +2 -2
  65. data/lib/jsi/typelike_modules.rb +52 -37
  66. data/lib/jsi/util/attr_struct.rb +106 -0
  67. data/lib/jsi/util.rb +141 -25
  68. data/lib/jsi/validation/error.rb +34 -0
  69. data/lib/jsi/validation/result.rb +210 -0
  70. data/lib/jsi/validation.rb +15 -0
  71. data/lib/jsi/version.rb +3 -1
  72. data/lib/jsi.rb +55 -9
  73. data/lib/schemas/json-schema.org/draft-04/schema.rb +8 -3
  74. data/lib/schemas/json-schema.org/draft-06/schema.rb +8 -3
  75. data/lib/schemas/json-schema.org/draft-07/schema.rb +12 -0
  76. data/readme.rb +138 -0
  77. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  78. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  79. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  80. metadata +69 -118
  81. data/.simplecov +0 -3
  82. data/Rakefile.rb +0 -9
  83. data/jsi.gemspec +0 -28
  84. data/lib/jsi/base/to_rb.rb +0 -128
  85. data/lib/jsi/json/node.rb +0 -203
  86. data/lib/jsi/json/pointer.rb +0 -419
  87. data/lib/jsi/json-schema-fragments.rb +0 -61
  88. data/lib/jsi/json.rb +0 -10
  89. data/resources/icons/AGPL-3.0.png +0 -0
  90. data/test/base_array_test.rb +0 -323
  91. data/test/base_hash_test.rb +0 -337
  92. data/test/base_test.rb +0 -486
  93. data/test/jsi_coder_test.rb +0 -85
  94. data/test/jsi_json_arraynode_test.rb +0 -150
  95. data/test/jsi_json_hashnode_test.rb +0 -132
  96. data/test/jsi_json_node_test.rb +0 -257
  97. data/test/jsi_json_pointer_test.rb +0 -102
  98. data/test/jsi_test.rb +0 -11
  99. data/test/jsi_typelike_as_json_test.rb +0 -53
  100. data/test/metaschema_node_test.rb +0 -19
  101. data/test/schema_module_test.rb +0 -21
  102. data/test/schema_test.rb +0 -208
  103. data/test/spreedly_openapi_test.rb +0 -8
  104. data/test/test_helper.rb +0 -97
  105. data/test/util_test.rb +0 -62
@@ -3,10 +3,9 @@
3
3
  module JSI
4
4
  # a module relating to objects that act like Hash or Array instances
5
5
  module Typelike
6
- # yields the content of the given param `object`. for objects which have a
7
- # #modified_copy method of their own (JSI::Base, JSI::JSON::Node) that
8
- # method is invoked with the given block. otherwise the given object itself
9
- # is yielded.
6
+ # yields the content of the given param `object`. for objects which have a #jsi_modified_copy
7
+ # method of their own (JSI::Base, JSI::MetaschemaNode) that method is invoked with the given
8
+ # block. otherwise the given object itself is yielded.
10
9
  #
11
10
  # the given block must result in a modified copy of its block parameter
12
11
  # (not destructively modifying the yielded content).
@@ -15,8 +14,8 @@ module JSI
15
14
  # in a (nondestructively) modified copy of this.
16
15
  # @return [object.class] modified copy of the given object
17
16
  def self.modified_copy(object, &block)
18
- if object.respond_to?(:modified_copy)
19
- object.modified_copy(&block)
17
+ if object.respond_to?(:jsi_modified_copy)
18
+ object.jsi_modified_copy(&block)
20
19
  else
21
20
  return yield(object)
22
21
  end
@@ -27,8 +26,8 @@ module JSI
27
26
  # will raise TypeError if an object is given that is not a type that seems
28
27
  # to be expressable as json.
29
28
  #
30
- # similar effect could be achieved by requiring 'json/add/core' and using
31
- # #as_json, but I don't much care for how it represents classes that are
29
+ # similar effect could be achieved by requiring 'json/add/core' and using #as_json,
30
+ # but I don't much care for how it represents classes that are
32
31
  # not naturally expressable in JSON, and prefer not to load its
33
32
  # monkey-patching.
34
33
  #
@@ -39,7 +38,7 @@ module JSI
39
38
  # array of object) cannot be expressed as json
40
39
  def self.as_json(object, *opt)
41
40
  if object.is_a?(JSI::PathedNode)
42
- as_json(object.node_content, *opt)
41
+ as_json(object.jsi_node_content, *opt)
43
42
  elsif object.respond_to?(:to_hash)
44
43
  (object.respond_to?(:map) ? object : object.to_hash).map do |k, v|
45
44
  unless k.is_a?(Symbol) || k.respond_to?(:to_str)
@@ -86,7 +85,7 @@ module JSI
86
85
  end
87
86
  safe_modified_copy_methods.each do |method_name|
88
87
  define_method(method_name) do |*a, &b|
89
- modified_copy do |object_to_modify|
88
+ jsi_modified_copy do |object_to_modify|
90
89
  responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
91
90
  responsive_object.public_send(method_name, *a, &b)
92
91
  end
@@ -94,19 +93,19 @@ module JSI
94
93
  end
95
94
  safe_kv_block_modified_copy_methods.each do |method_name|
96
95
  define_method(method_name) do |*a, &b|
97
- modified_copy do |object_to_modify|
96
+ jsi_modified_copy do |object_to_modify|
98
97
  responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
99
- responsive_object.public_send(method_name, *a) do |k, _v|
100
- b.call(k, self[k])
98
+ responsive_object.public_send(method_name) do |k, _v|
99
+ b.call(k, self[k, *a])
101
100
  end
102
101
  end
103
102
  end
104
103
  end
105
104
 
106
- # the same as Hash#update
105
+ # like [Hash#update](https://ruby-doc.org/core/Hash.html#method-i-update)
107
106
  # @param other [#to_hash] the other hash to update this hash from
108
107
  # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
109
- # is determined by calling the block with the key, its value in hsh and its value in other_hash.
108
+ # is determined by calling the block with the key, its value in self and its value in other.
110
109
  # @return self, updated with other
111
110
  # @raise [TypeError] when `other` does not respond to #to_hash
112
111
  def update(other, &block)
@@ -115,7 +114,7 @@ module JSI
115
114
  end
116
115
  self_respondingto_key = self.respond_to?(:key?) ? self : to_hash
117
116
  other.to_hash.each_pair do |key, value|
118
- if block_given? && self_respondingto_key.key?(key)
117
+ if block && self_respondingto_key.key?(key)
119
118
  value = yield(key, self[key], value)
120
119
  end
121
120
  self[key] = value
@@ -125,33 +124,34 @@ module JSI
125
124
 
126
125
  alias_method :merge!, :update
127
126
 
128
- # the same as Hash#merge
127
+ # like [Hash#merge](https://ruby-doc.org/core/Hash.html#method-i-merge)
129
128
  # @param other [#to_hash] the other hash to merge into this
130
129
  # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
131
- # is determined by calling the block with the key, its value in hsh and its value in other_hash.
130
+ # is determined by calling the block with the key, its value in self and its value in other.
132
131
  # @return duplicate of this hash with the other hash merged in
133
132
  # @raise [TypeError] when `other` does not respond to #to_hash
134
133
  def merge(other, &block)
135
134
  dup.update(other, &block)
136
135
  end
137
136
 
138
- # @return [String] basically the same #inspect as Hash, but has the
139
- # class name and, if responsive, self's #object_group_text
137
+ # basically the same #inspect as Hash, but has the class name and, if responsive,
138
+ # self's #jsi_object_group_text
139
+ # @return [String]
140
140
  def inspect
141
- object_group_str = (respond_to?(:object_group_text) ? self.object_group_text : [self.class]).join(' ')
142
- "\#{<#{object_group_str}>#{empty? ? '' : ' '}#{self.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}}"
141
+ object_group_str = (respond_to?(:jsi_object_group_text) ? self.jsi_object_group_text : [self.class]).join(' ')
142
+ "\#{<#{object_group_str}>#{self.map { |k, v| " #{k.inspect} => #{v.inspect}" }.join(',')}}"
143
143
  end
144
144
 
145
145
  alias_method :to_s, :inspect
146
146
 
147
- # pretty-prints a representation this node to the given printer
147
+ # pretty-prints a representation of this hashlike to the given printer
148
148
  # @return [void]
149
149
  def pretty_print(q)
150
- object_group_str = (respond_to?(:object_group_text) ? object_group_text : [self.class]).join(' ')
150
+ object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
151
151
  q.text "\#{<#{object_group_str}>"
152
152
  q.group_sub {
153
153
  q.nest(2) {
154
- q.breakable(any? { true } ? ' ' : '')
154
+ q.breakable(empty? ? '' : ' ')
155
155
  q.seplist(self, nil, :each_pair) { |k, v|
156
156
  q.group {
157
157
  q.pp k
@@ -177,7 +177,7 @@ module JSI
177
177
  # methods which do not need to access the element.
178
178
  SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size)
179
179
  # there are some ambiguous ones that are omitted, like #sort, #map / #collect.
180
- SAFE_INDEX_ELEMENT_METHODS = %w(| & * + - <=> abbrev assoc 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)
180
+ 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)
181
181
  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)
182
182
 
183
183
  # methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
@@ -193,7 +193,7 @@ module JSI
193
193
  end
194
194
  safe_modified_copy_methods.each do |method_name|
195
195
  define_method(method_name) do |*a, &b|
196
- modified_copy do |object_to_modify|
196
+ jsi_modified_copy do |object_to_modify|
197
197
  responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
198
198
  responsive_object.public_send(method_name, *a, &b)
199
199
  end
@@ -201,33 +201,48 @@ module JSI
201
201
  end
202
202
  safe_el_block_methods.each do |method_name|
203
203
  define_method(method_name) do |*a, &b|
204
- modified_copy do |object_to_modify|
204
+ jsi_modified_copy do |object_to_modify|
205
205
  i = 0
206
206
  responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
207
- responsive_object.public_send(method_name, *a) do |_e|
208
- b.call(self[i]).tap { i += 1 }
207
+ responsive_object.public_send(method_name) do |_e|
208
+ b.call(self[i, *a]).tap { i += 1 }
209
209
  end
210
210
  end
211
211
  end
212
212
  end
213
213
 
214
- # @return [String] basically the same #inspect as Array, but has the
215
- # class name and, if responsive, self's #object_group_text
214
+ # see [Array#assoc](https://ruby-doc.org/core/Array.html#method-i-assoc)
215
+ def assoc(obj)
216
+ # note: assoc implemented here (instead of delegated) due to inconsistencies in whether
217
+ # other implementations expect each element to be an Array or to respond to #to_ary
218
+ detect { |e| e.respond_to?(:to_ary) and e[0] == obj }
219
+ end
220
+
221
+ # see [Array#rassoc](https://ruby-doc.org/core/Array.html#method-i-rassoc)
222
+ def rassoc(obj)
223
+ # note: rassoc implemented here (instead of delegated) due to inconsistencies in whether
224
+ # other implementations expect each element to be an Array or to respond to #to_ary
225
+ detect { |e| e.respond_to?(:to_ary) and e[1] == obj }
226
+ end
227
+
228
+ # basically the same #inspect as Array, but has the class name and, if responsive,
229
+ # self's #jsi_object_group_text
230
+ # @return [String]
216
231
  def inspect
217
- object_group_str = (respond_to?(:object_group_text) ? object_group_text : [self.class]).join(' ')
218
- "\#[<#{object_group_str}>#{empty? ? '' : ' '}#{self.map { |e| e.inspect }.join(', ')}]"
232
+ object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
233
+ "\#[<#{object_group_str}>#{self.map { |e| ' ' + e.inspect }.join(',')}]"
219
234
  end
220
235
 
221
236
  alias_method :to_s, :inspect
222
237
 
223
- # pretty-prints a representation this node to the given printer
238
+ # pretty-prints a representation of this arraylike to the given printer
224
239
  # @return [void]
225
240
  def pretty_print(q)
226
- object_group_str = (respond_to?(:object_group_text) ? object_group_text : [self.class]).join(' ')
241
+ object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
227
242
  q.text "\#[<#{object_group_str}>"
228
243
  q.group_sub {
229
244
  q.nest(2) {
230
- q.breakable(any? { true } ? ' ' : '')
245
+ q.breakable(empty? ? '' : ' ')
231
246
  q.seplist(self, nil, :each) { |e|
232
247
  q.pp e
233
248
  }
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Util
5
+ # like a Struct, but stores all the attributes in one @attributes Hash, instead of individual instance
6
+ # variables for each attribute.
7
+ # this tends to be easier to work with and more flexible. keys which are symbols are converted to strings.
8
+ class AttrStruct
9
+ class AttrStructError < StandardError
10
+ end
11
+
12
+ class UndefinedAttributeKey < AttrStructError
13
+ end
14
+
15
+ class << self
16
+ # creates a AttrStruct subclass with the given attribute keys.
17
+ # @param attribute_keys [Enumerable<String, Symbol>]
18
+ def [](*attribute_keys)
19
+ unless self == AttrStruct
20
+ # :nocov:
21
+ raise(NotImplementedError, "AttrStruct multiple inheritance not supported")
22
+ # :nocov:
23
+ end
24
+
25
+ bad = attribute_keys.reject { |key| key.respond_to?(:to_str) || key.is_a?(Symbol) }
26
+ unless bad.empty?
27
+ raise ArgumentError, "attribute keys must be String or Symbol; got keys: #{bad.map(&:inspect).join(', ')}"
28
+ end
29
+ attribute_keys = attribute_keys.map { |key| key.is_a?(Symbol) ? key.to_s : key }
30
+
31
+ Class.new(AttrStruct).tap do |klass|
32
+ klass.define_singleton_method(:attribute_keys) { attribute_keys }
33
+ klass.send(:define_method, :attribute_keys) { attribute_keys }
34
+ attribute_keys.each do |attribute_key|
35
+ # reader
36
+ klass.send(:define_method, attribute_key) do
37
+ @attributes[attribute_key]
38
+ end
39
+
40
+ # writer
41
+ klass.send(:define_method, "#{attribute_key}=") do |value|
42
+ @attributes[attribute_key] = value
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def initialize(attributes = {})
50
+ unless attributes.respond_to?(:to_hash)
51
+ raise(TypeError, "expected attributes to be a Hash; got: #{attributes.inspect}")
52
+ end
53
+ attributes = attributes.map { |k, v| {k.is_a?(Symbol) ? k.to_s : k => v} }.inject({}, &:update)
54
+ bad = attributes.keys.reject { |k| self.attribute_keys.include?(k) }
55
+ unless bad.empty?
56
+ raise UndefinedAttributeKey, "undefined attribute keys: #{bad.map(&:inspect).join(', ')}"
57
+ end
58
+ @attributes = attributes
59
+ end
60
+
61
+ def [](key)
62
+ key = key.to_s if key.is_a?(Symbol)
63
+ @attributes[key]
64
+ end
65
+
66
+ def []=(key, value)
67
+ key = key.to_s if key.is_a?(Symbol)
68
+ unless self.attribute_keys.include?(key)
69
+ raise UndefinedAttributeKey, "undefined attribute key: #{key.inspect}"
70
+ end
71
+ @attributes[key] = value
72
+ end
73
+
74
+ # @return [String]
75
+ def inspect
76
+ "\#<#{self.class.name}#{@attributes.empty? ? '' : ' '}#{@attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>"
77
+ end
78
+
79
+ # pretty-prints a representation of self to the given printer
80
+ # @return [void]
81
+ def pretty_print(q)
82
+ q.text '#<'
83
+ q.text self.class.name
84
+ q.group_sub {
85
+ q.nest(2) {
86
+ q.breakable(@attributes.empty? ? '' : ' ')
87
+ q.seplist(@attributes, nil, :each_pair) { |k, v|
88
+ q.group {
89
+ q.text k
90
+ q.text ': '
91
+ q.pp v
92
+ }
93
+ }
94
+ }
95
+ }
96
+ q.breakable ''
97
+ q.text '>'
98
+ end
99
+
100
+ include FingerprintHash
101
+ def jsi_fingerprint
102
+ {class: self.class, attributes: @attributes}
103
+ end
104
+ end
105
+ end
106
+ end
data/lib/jsi/util.rb CHANGED
@@ -1,13 +1,13 @@
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
+ # JSI::Util classes, modules, constants, and methods are internal, and will be added and removed without warning.
5
+ #
6
+ # @api private
6
7
  module Util
7
- # a proc which does nothing
8
- NOOP = -> (*_) { }
8
+ autoload :AttrStruct, 'jsi/util/attr_struct'
9
9
 
10
- # returns a version of the given hash, in which any symbol keys are
10
+ # a hash copied from the given hashlike, in which any symbol keys are
11
11
  # converted to strings. behavior on collisions is undefined (but in the
12
12
  # future could take a block like
13
13
  # ActiveSupport::HashWithIndifferentAccess#update)
@@ -54,24 +54,63 @@ module JSI
54
54
  end
55
55
  end
56
56
 
57
+ # ensures the given param becomes a frozen Set of Modules.
58
+ # returns the param if it is already that, otherwise initializes and freezes such a Set.
59
+ #
60
+ # @param modules [Set, Enumerable] the object to ensure becomes a frozen Set of Modules
61
+ # @return [Set] frozen Set containing the given modules
62
+ # @raise [ArgumentError] when the modules param is not an Enumerable
63
+ # @raise [Schema::NotASchemaError] when the modules param contains objects which are not Schemas
64
+ def ensure_module_set(modules)
65
+ if modules.is_a?(Set) && modules.frozen?
66
+ set = modules
67
+ else
68
+ set = Set.new(modules).freeze
69
+ end
70
+ not_modules = set.reject { |s| s.is_a?(Module) }
71
+ if !not_modules.empty?
72
+ raise(TypeError, [
73
+ "ensure_module_set given non-Module objects:",
74
+ *not_modules.map { |ns| ns.pretty_inspect.chomp },
75
+ ].join("\n"))
76
+ end
77
+
78
+ set
79
+ end
80
+
81
+ # is the given name ok to use as a ruby method name?
82
+ def ok_ruby_method_name?(name)
83
+ # must be a string
84
+ return false unless name.respond_to?(:to_str)
85
+ # must not begin with a digit
86
+ return false if name =~ /\A[0-9]/
87
+ # must not contain characters special to ruby syntax
88
+ return false if name =~ /[\\\s\#;\.,\(\)\[\]\{\}'"`%\+\-\/\*\^\|&=<>\?:!@\$~]/
89
+
90
+ return true
91
+ end
92
+
57
93
  # this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
58
94
  # to define a recursive function to return the length of an array:
59
95
  #
60
- # length = ycomb do |len|
61
- # proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
62
- # end
96
+ # length = ycomb do |len|
97
+ # proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
98
+ # end
99
+ #
100
+ # length.call([0])
101
+ # # => 1
63
102
  #
64
- # see https://secure.wikimedia.org/wikipedia/en/wiki/Fixed_point_combinator#Y_combinator
65
- # and chapter 9 of the little schemer, available as the sample chapter at http://www.ccs.neu.edu/home/matthias/BTLS/
103
+ # see https://en.wikipedia.org/wiki/Fixed-point_combinator#Y_combinator
104
+ # and chapter 9 of the little schemer, available as the sample chapter at
105
+ # https://felleisen.org/matthias/BTLS-index.html
66
106
  def ycomb
67
107
  proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
68
108
  end
69
- module_function :ycomb
70
109
 
71
110
  module FingerprintHash
72
111
  # overrides BasicObject#==
73
112
  def ==(other)
74
- object_id == other.object_id || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
113
+ __id__ == other.__id__ || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
75
114
  end
76
115
 
77
116
  alias_method :eql?, :==
@@ -82,26 +121,103 @@ module JSI
82
121
  end
83
122
  end
84
123
 
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_]
124
+ class MemoMap
125
+ Result = Util::AttrStruct[*%w(
126
+ value
127
+ inputs
128
+ inputs_hash
129
+ )]
130
+
131
+ class Result
92
132
  end
93
133
 
94
- def jsi_clear_memo(key, *args)
95
- @jsi_memos ||= {}
96
- if @jsi_memos[key]
97
- if args.empty?
98
- @jsi_memos[key].clear
134
+ def initialize(key_by: nil, &block)
135
+ @key_by = key_by
136
+ @block = block
137
+
138
+ # each result has its own mutex to update its memoized value thread-safely
139
+ @result_mutexes = {}
140
+ # another mutex to thread-safely initialize each result mutex
141
+ @result_mutexes_mutex = Mutex.new
142
+
143
+ @results = {}
144
+ end
145
+
146
+ def [](*inputs)
147
+ if @key_by
148
+ key = @key_by.call(*inputs)
149
+ else
150
+ key = inputs
151
+ end
152
+ result_mutex = @result_mutexes_mutex.synchronize do
153
+ @result_mutexes[key] ||= Mutex.new
154
+ end
155
+
156
+ result_mutex.synchronize do
157
+ inputs_hash = inputs.hash
158
+ if @results.key?(key) && inputs_hash == @results[key].inputs_hash && inputs == @results[key].inputs
159
+ @results[key].value
99
160
  else
100
- @jsi_memos[key].delete(args)
161
+ value = @block.call(*inputs)
162
+ @results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
163
+ value
101
164
  end
102
165
  end
103
166
  end
104
167
  end
168
+
169
+ module Memoize
170
+ def self.extended(object)
171
+ object.send(:jsi_initialize_memos)
172
+ end
173
+
174
+ private
175
+
176
+ def jsi_initialize_memos
177
+ @jsi_memomaps_mutex = Mutex.new
178
+ @jsi_memomaps = {}
179
+ end
180
+
181
+ # @return [Util::MemoMap]
182
+ def jsi_memomap(name, **options, &block)
183
+ raise(Bug, 'must jsi_initialize_memos') unless @jsi_memomaps
184
+ unless @jsi_memomaps.key?(name)
185
+ @jsi_memomaps_mutex.synchronize do
186
+ # note: this ||= appears redundant with `unless @jsi_memomaps.key?(name)`,
187
+ # but that check is not thread safe. this check is.
188
+ @jsi_memomaps[name] ||= Util::MemoMap.new(**options, &block)
189
+ end
190
+ end
191
+ @jsi_memomaps[name]
192
+ end
193
+
194
+ def jsi_memoize(name, *inputs, &block)
195
+ jsi_memomap(name, &block)[*inputs]
196
+ end
197
+ end
198
+
199
+ module Virtual
200
+ class InstantiationError < StandardError
201
+ end
202
+
203
+ # this virtual class is not intended to be instantiated except by its subclasses, which override #initialize
204
+ def initialize
205
+ # :nocov:
206
+ raise(InstantiationError, "cannot instantiate virtual class #{self.class}")
207
+ # :nocov:
208
+ end
209
+
210
+ # virtual_method is used to indicate that the method calling it must be implemented on the (non-virtual) subclass
211
+ def virtual_method
212
+ # :nocov:
213
+ raise(Bug, "class #{self.class} must implement #{caller_locations.first.label}")
214
+ # :nocov:
215
+ end
216
+ end
217
+
218
+ public
219
+
220
+ extend self
105
221
  end
106
222
  public
107
223
  extend Util
@@ -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