jsi 0.4.0 → 0.6.0

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