jsi 0.2.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 +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