jsi 0.2.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 +36 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +153 -52
  6. data/lib/jsi/base.rb +485 -338
  7. data/lib/jsi/jsi_coder.rb +24 -18
  8. data/lib/jsi/metaschema.rb +7 -0
  9. data/lib/jsi/metaschema_node/bootstrap_schema.rb +100 -0
  10. data/lib/jsi/metaschema_node.rb +245 -0
  11. data/lib/jsi/pathed_node.rb +49 -46
  12. data/lib/jsi/ptr.rb +292 -0
  13. data/lib/jsi/schema/application/child_application/contains.rb +16 -0
  14. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  15. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  16. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  17. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  18. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  19. data/lib/jsi/schema/application/child_application.rb +40 -0
  20. data/lib/jsi/schema/application/draft04.rb +8 -0
  21. data/lib/jsi/schema/application/draft06.rb +8 -0
  22. data/lib/jsi/schema/application/draft07.rb +8 -0
  23. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  24. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  25. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  26. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  27. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  28. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  29. data/lib/jsi/schema/application/inplace_application/someof.rb +29 -0
  30. data/lib/jsi/schema/application/inplace_application.rb +46 -0
  31. data/lib/jsi/schema/application.rb +12 -0
  32. data/lib/jsi/schema/draft04.rb +14 -0
  33. data/lib/jsi/schema/draft06.rb +14 -0
  34. data/lib/jsi/schema/draft07.rb +14 -0
  35. data/lib/jsi/schema/issue.rb +36 -0
  36. data/lib/jsi/schema/ref.rb +159 -0
  37. data/lib/jsi/schema/schema_ancestor_node.rb +119 -0
  38. data/lib/jsi/schema/validation/array.rb +69 -0
  39. data/lib/jsi/schema/validation/const.rb +20 -0
  40. data/lib/jsi/schema/validation/contains.rb +25 -0
  41. data/lib/jsi/schema/validation/core.rb +39 -0
  42. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  43. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  44. data/lib/jsi/schema/validation/draft04.rb +112 -0
  45. data/lib/jsi/schema/validation/draft06.rb +122 -0
  46. data/lib/jsi/schema/validation/draft07.rb +159 -0
  47. data/lib/jsi/schema/validation/enum.rb +25 -0
  48. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  49. data/lib/jsi/schema/validation/items.rb +54 -0
  50. data/lib/jsi/schema/validation/not.rb +20 -0
  51. data/lib/jsi/schema/validation/numeric.rb +121 -0
  52. data/lib/jsi/schema/validation/object.rb +45 -0
  53. data/lib/jsi/schema/validation/pattern.rb +34 -0
  54. data/lib/jsi/schema/validation/properties.rb +101 -0
  55. data/lib/jsi/schema/validation/property_names.rb +32 -0
  56. data/lib/jsi/schema/validation/ref.rb +40 -0
  57. data/lib/jsi/schema/validation/required.rb +27 -0
  58. data/lib/jsi/schema/validation/someof.rb +90 -0
  59. data/lib/jsi/schema/validation/string.rb +47 -0
  60. data/lib/jsi/schema/validation/type.rb +49 -0
  61. data/lib/jsi/schema/validation.rb +51 -0
  62. data/lib/jsi/schema.rb +528 -233
  63. data/lib/jsi/schema_classes.rb +238 -51
  64. data/lib/jsi/schema_registry.rb +141 -0
  65. data/lib/jsi/schema_set.rb +141 -0
  66. data/lib/jsi/simple_wrap.rb +8 -3
  67. data/lib/jsi/typelike_modules.rb +75 -68
  68. data/lib/jsi/util/attr_struct.rb +106 -0
  69. data/lib/jsi/util.rb +167 -64
  70. data/lib/jsi/validation/error.rb +34 -0
  71. data/lib/jsi/validation/result.rb +210 -0
  72. data/lib/jsi/validation.rb +15 -0
  73. data/lib/jsi/version.rb +3 -1
  74. data/lib/jsi.rb +72 -9
  75. data/lib/schemas/json-schema.org/draft-04/schema.rb +12 -0
  76. data/lib/schemas/json-schema.org/draft-06/schema.rb +12 -0
  77. data/lib/schemas/json-schema.org/draft-07/schema.rb +12 -0
  78. data/readme.rb +138 -0
  79. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  80. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  81. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  82. metadata +80 -107
  83. data/.simplecov +0 -1
  84. data/LICENSE.txt +0 -21
  85. data/Rakefile.rb +0 -9
  86. data/jsi.gemspec +0 -31
  87. data/lib/jsi/base/to_rb.rb +0 -126
  88. data/lib/jsi/json/node.rb +0 -243
  89. data/lib/jsi/json/pointer.rb +0 -330
  90. data/lib/jsi/json-schema-fragments.rb +0 -59
  91. data/lib/jsi/json.rb +0 -8
  92. data/test/base_array_test.rb +0 -209
  93. data/test/base_hash_test.rb +0 -204
  94. data/test/base_test.rb +0 -422
  95. data/test/jsi_coder_test.rb +0 -85
  96. data/test/jsi_json_arraynode_test.rb +0 -150
  97. data/test/jsi_json_hashnode_test.rb +0 -132
  98. data/test/jsi_json_node_test.rb +0 -310
  99. data/test/jsi_json_pointer_test.rb +0 -106
  100. data/test/jsi_test.rb +0 -11
  101. data/test/jsi_typelike_as_json_test.rb +0 -53
  102. data/test/schema_test.rb +0 -196
  103. data/test/spreedly_openapi_test.rb +0 -8
  104. data/test/test_helper.rb +0 -63
  105. data/test/util_test.rb +0 -62
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
2
4
  # a module relating to objects that act like Hash or Array instances
3
5
  module Typelike
4
- # yields the content of the given param `object`. for objects which have a
5
- # #modified_copy method of their own (JSI::Base, JSI::JSON::Node) that
6
- # method is invoked with the given block. otherwise the given object itself
7
- # 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.
8
9
  #
9
10
  # the given block must result in a modified copy of its block parameter
10
11
  # (not destructively modifying the yielded content).
@@ -13,8 +14,8 @@ module JSI
13
14
  # in a (nondestructively) modified copy of this.
14
15
  # @return [object.class] modified copy of the given object
15
16
  def self.modified_copy(object, &block)
16
- if object.respond_to?(:modified_copy)
17
- object.modified_copy(&block)
17
+ if object.respond_to?(:jsi_modified_copy)
18
+ object.jsi_modified_copy(&block)
18
19
  else
19
20
  return yield(object)
20
21
  end
@@ -25,8 +26,8 @@ module JSI
25
26
  # will raise TypeError if an object is given that is not a type that seems
26
27
  # to be expressable as json.
27
28
  #
28
- # similar effect could be achieved by requiring 'json/add/core' and using
29
- # #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
30
31
  # not naturally expressable in JSON, and prefer not to load its
31
32
  # monkey-patching.
32
33
  #
@@ -37,7 +38,7 @@ module JSI
37
38
  # array of object) cannot be expressed as json
38
39
  def self.as_json(object, *opt)
39
40
  if object.is_a?(JSI::PathedNode)
40
- as_json(object.node_content, *opt)
41
+ as_json(object.jsi_node_content, *opt)
41
42
  elsif object.respond_to?(:to_hash)
42
43
  (object.respond_to?(:map) ? object : object.to_hash).map do |k, v|
43
44
  unless k.is_a?(Symbol) || k.respond_to?(:to_str)
@@ -84,7 +85,7 @@ module JSI
84
85
  end
85
86
  safe_modified_copy_methods.each do |method_name|
86
87
  define_method(method_name) do |*a, &b|
87
- modified_copy do |object_to_modify|
88
+ jsi_modified_copy do |object_to_modify|
88
89
  responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
89
90
  responsive_object.public_send(method_name, *a, &b)
90
91
  end
@@ -92,19 +93,19 @@ module JSI
92
93
  end
93
94
  safe_kv_block_modified_copy_methods.each do |method_name|
94
95
  define_method(method_name) do |*a, &b|
95
- modified_copy do |object_to_modify|
96
+ jsi_modified_copy do |object_to_modify|
96
97
  responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
97
- responsive_object.public_send(method_name, *a) do |k, _v|
98
- b.call(k, self[k])
98
+ responsive_object.public_send(method_name) do |k, _v|
99
+ b.call(k, self[k, *a])
99
100
  end
100
101
  end
101
102
  end
102
103
  end
103
104
 
104
- # the same as Hash#update
105
+ # like [Hash#update](https://ruby-doc.org/core/Hash.html#method-i-update)
105
106
  # @param other [#to_hash] the other hash to update this hash from
106
107
  # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
107
- # 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.
108
109
  # @return self, updated with other
109
110
  # @raise [TypeError] when `other` does not respond to #to_hash
110
111
  def update(other, &block)
@@ -113,7 +114,7 @@ module JSI
113
114
  end
114
115
  self_respondingto_key = self.respond_to?(:key?) ? self : to_hash
115
116
  other.to_hash.each_pair do |key, value|
116
- if block_given? && self_respondingto_key.key?(key)
117
+ if block && self_respondingto_key.key?(key)
117
118
  value = yield(key, self[key], value)
118
119
  end
119
120
  self[key] = value
@@ -123,49 +124,45 @@ module JSI
123
124
 
124
125
  alias_method :merge!, :update
125
126
 
126
- # the same as Hash#merge
127
+ # like [Hash#merge](https://ruby-doc.org/core/Hash.html#method-i-merge)
127
128
  # @param other [#to_hash] the other hash to merge into this
128
129
  # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
129
- # 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.
130
131
  # @return duplicate of this hash with the other hash merged in
131
132
  # @raise [TypeError] when `other` does not respond to #to_hash
132
133
  def merge(other, &block)
133
134
  dup.update(other, &block)
134
135
  end
135
136
 
136
- # @return [String] basically the same #inspect as Hash, but has the
137
- # 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]
138
140
  def inspect
139
- object_group_text = respond_to?(:object_group_text) ? ' ' + self.object_group_text : ''
140
- "\#{<#{self.class}#{object_group_text}>#{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(',')}}"
141
143
  end
142
144
 
143
- # @return [String] see #inspect
144
- def to_s
145
- inspect
146
- end
145
+ alias_method :to_s, :inspect
147
146
 
148
- # pretty-prints a representation this node to the given printer
147
+ # pretty-prints a representation of this hashlike to the given printer
149
148
  # @return [void]
150
149
  def pretty_print(q)
151
- q.instance_exec(self) do |obj|
152
- object_group_text = obj.respond_to?(:object_group_text) ? ' ' + obj.object_group_text : ''
153
- text "\#{<#{obj.class}#{object_group_text}>"
154
- group_sub {
155
- nest(2) {
156
- breakable(obj.any? { true } ? ' ' : '')
157
- seplist(obj, nil, :each_pair) { |k, v|
158
- group {
159
- pp k
160
- text ' => '
161
- pp v
162
- }
150
+ object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
151
+ q.text "\#{<#{object_group_str}>"
152
+ q.group_sub {
153
+ q.nest(2) {
154
+ q.breakable(empty? ? '' : ' ')
155
+ q.seplist(self, nil, :each_pair) { |k, v|
156
+ q.group {
157
+ q.pp k
158
+ q.text ' => '
159
+ q.pp v
163
160
  }
164
161
  }
165
162
  }
166
- breakable ''
167
- text '}'
168
- end
163
+ }
164
+ q.breakable ''
165
+ q.text '}'
169
166
  end
170
167
  end
171
168
 
@@ -180,7 +177,7 @@ module JSI
180
177
  # methods which do not need to access the element.
181
178
  SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size)
182
179
  # there are some ambiguous ones that are omitted, like #sort, #map / #collect.
183
- 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)
184
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)
185
182
 
186
183
  # methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
@@ -196,7 +193,7 @@ module JSI
196
193
  end
197
194
  safe_modified_copy_methods.each do |method_name|
198
195
  define_method(method_name) do |*a, &b|
199
- modified_copy do |object_to_modify|
196
+ jsi_modified_copy do |object_to_modify|
200
197
  responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
201
198
  responsive_object.public_send(method_name, *a, &b)
202
199
  end
@@ -204,45 +201,55 @@ module JSI
204
201
  end
205
202
  safe_el_block_methods.each do |method_name|
206
203
  define_method(method_name) do |*a, &b|
207
- modified_copy do |object_to_modify|
204
+ jsi_modified_copy do |object_to_modify|
208
205
  i = 0
209
206
  responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
210
- responsive_object.public_send(method_name, *a) do |_e|
211
- 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 }
212
209
  end
213
210
  end
214
211
  end
215
212
  end
216
213
 
217
- # @return [String] basically the same #inspect as Array, but has the
218
- # class name and, if responsive, self's #object_group_text
219
- def inspect
220
- object_group_text = respond_to?(:object_group_text) ? ' ' + self.object_group_text : ''
221
- "\#[<#{self.class}#{object_group_text}>#{empty? ? '' : ' '}#{self.map { |e| e.inspect }.join(', ')}]"
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 }
222
226
  end
223
227
 
224
- # @return [String] see #inspect
225
- def to_s
226
- inspect
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]
231
+ def inspect
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(',')}]"
227
234
  end
228
235
 
229
- # pretty-prints a representation this node to the given printer
236
+ alias_method :to_s, :inspect
237
+
238
+ # pretty-prints a representation of this arraylike to the given printer
230
239
  # @return [void]
231
240
  def pretty_print(q)
232
- q.instance_exec(self) do |obj|
233
- object_group_text = obj.respond_to?(:object_group_text) ? ' ' + obj.object_group_text : ''
234
- text "\#[<#{obj.class}#{object_group_text}>"
235
- group_sub {
236
- nest(2) {
237
- breakable(obj.any? { true } ? ' ' : '')
238
- seplist(obj, nil, :each) { |e|
239
- pp e
240
- }
241
+ object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
242
+ q.text "\#[<#{object_group_str}>"
243
+ q.group_sub {
244
+ q.nest(2) {
245
+ q.breakable(empty? ? '' : ' ')
246
+ q.seplist(self, nil, :each) { |e|
247
+ q.pp e
241
248
  }
242
249
  }
243
- breakable ''
244
- text ']'
245
- end
250
+ }
251
+ q.breakable ''
252
+ q.text ']'
246
253
  end
247
254
  end
248
255
  end
@@ -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,9 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSI
4
+ # JSI::Util classes, modules, constants, and methods are internal, and will be added and removed without warning.
5
+ #
6
+ # @api private
2
7
  module Util
3
- # a proc which does nothing
4
- NOOP = -> (*_) { }
8
+ autoload :AttrStruct, 'jsi/util/attr_struct'
5
9
 
6
- # 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
7
11
  # converted to strings. behavior on collisions is undefined (but in the
8
12
  # future could take a block like
9
13
  # ActiveSupport::HashWithIndifferentAccess#update)
@@ -14,108 +18,207 @@ module JSI
14
18
  # the return if you need to ensure it is not the same instance as the
15
19
  # argument instance.
16
20
  #
17
- # @param hash [#to_hash] the hash from which to convert symbol keys to strings
21
+ # @param hashlike [#to_hash] the hash from which to convert symbol keys to strings
18
22
  # @return [same class as the param `hash`, or Hash if the former cannot be done] a
19
23
  # hash(-like) instance containing no symbol keys
20
- def stringify_symbol_keys(hash)
21
- unless hash.respond_to?(:to_hash)
22
- raise(ArgumentError, "expected argument to be a hash; got #{hash.class.inspect}: #{hash.pretty_inspect.chomp}")
24
+ def stringify_symbol_keys(hashlike)
25
+ unless hashlike.respond_to?(:to_hash)
26
+ raise(ArgumentError, "expected argument to be a hash; got #{hashlike.class.inspect}: #{hashlike.pretty_inspect.chomp}")
23
27
  end
24
- JSI::Typelike.modified_copy(hash) do |hash_|
25
- changed = false
28
+ JSI::Typelike.modified_copy(hashlike) do |hash|
26
29
  out = {}
27
- hash_.each do |k, v|
28
- if k.is_a?(Symbol)
29
- changed = true
30
- k = k.to_s
31
- end
32
- out[k] = v
30
+ hash.each do |k, v|
31
+ out[k.is_a?(Symbol) ? k.to_s : k] = v
33
32
  end
34
- changed ? out : hash_
33
+ out
35
34
  end
36
35
  end
37
36
 
38
37
  def deep_stringify_symbol_keys(object)
39
38
  if object.respond_to?(:to_hash)
40
39
  JSI::Typelike.modified_copy(object) do |hash|
41
- changed = false
42
40
  out = {}
43
41
  (hash.respond_to?(:each) ? hash : hash.to_hash).each do |k, v|
44
- if k.is_a?(Symbol)
45
- changed = true
46
- k = k.to_s
47
- end
48
- out_k = deep_stringify_symbol_keys(k)
49
- out_v = deep_stringify_symbol_keys(v)
50
- changed = true if out_k.object_id != k.object_id
51
- changed = true if out_v.object_id != v.object_id
52
- out[out_k] = out_v
42
+ out[k.is_a?(Symbol) ? k.to_s : deep_stringify_symbol_keys(k)] = deep_stringify_symbol_keys(v)
53
43
  end
54
- changed ? out : hash
44
+ out
55
45
  end
56
46
  elsif object.respond_to?(:to_ary)
57
47
  JSI::Typelike.modified_copy(object) do |ary|
58
- changed = false
59
- out = (ary.respond_to?(:each) ? ary : ary.to_ary).map do |e|
60
- out_e = deep_stringify_symbol_keys(e)
61
- changed = true if out_e.object_id != e.object_id
62
- out_e
48
+ (ary.respond_to?(:each) ? ary : ary.to_ary).map do |e|
49
+ deep_stringify_symbol_keys(e)
63
50
  end
64
- changed ? out : ary
65
51
  end
66
52
  else
67
53
  object
68
54
  end
69
55
  end
70
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
+
71
93
  # this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
72
94
  # to define a recursive function to return the length of an array:
73
95
  #
74
- # length = ycomb do |len|
75
- # proc{|list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
76
- # 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
77
102
  #
78
- # see https://secure.wikimedia.org/wikipedia/en/wiki/Fixed_point_combinator#Y_combinator
79
- # 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
80
106
  def ycomb
81
107
  proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
82
108
  end
83
- module_function :ycomb
84
- end
85
- public
86
- extend Util
87
109
 
88
- module FingerprintHash
89
- def ==(other)
90
- object_id == other.object_id || (other.respond_to?(:fingerprint) && other.fingerprint == self.fingerprint)
91
- end
110
+ module FingerprintHash
111
+ # overrides BasicObject#==
112
+ def ==(other)
113
+ __id__ == other.__id__ || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
114
+ end
92
115
 
93
- alias_method :eql?, :==
116
+ alias_method :eql?, :==
94
117
 
95
- def hash
96
- fingerprint.hash
118
+ # overrides Kernel#hash
119
+ def hash
120
+ jsi_fingerprint.hash
121
+ end
97
122
  end
98
- end
99
123
 
100
- module Memoize
101
- def memoize(key, *args_)
102
- @memos ||= {}
103
- @memos[key] ||= Hash.new do |h, args|
104
- h[args] = yield(*args)
124
+ class MemoMap
125
+ Result = Util::AttrStruct[*%w(
126
+ value
127
+ inputs
128
+ inputs_hash
129
+ )]
130
+
131
+ class Result
105
132
  end
106
- @memos[key][args_]
107
- end
108
133
 
109
- def clear_memo(key, *args)
110
- @memos ||= {}
111
- if @memos[key]
112
- if args.empty?
113
- @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)
114
149
  else
115
- @memos[key].delete(args)
150
+ key = inputs
151
+ end
152
+ result_mutex = @result_mutexes_mutex.synchronize do
153
+ @result_mutexes[key] ||= Mutex.new
116
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
160
+ else
161
+ value = @block.call(*inputs)
162
+ @results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
163
+ value
164
+ end
165
+ end
166
+ end
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]
117
196
  end
118
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
119
221
  end
120
- extend Memoize
222
+ public
223
+ extend Util
121
224
  end