jsi-dev 0.0.0.pre.commonmarker

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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +8 -0
  3. data/CHANGELOG.md +101 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +303 -0
  6. data/docs/glossary.md +281 -0
  7. data/jsi.gemspec +30 -0
  8. data/lib/jsi/base/node.rb +373 -0
  9. data/lib/jsi/base.rb +738 -0
  10. data/lib/jsi/jsi_coder.rb +92 -0
  11. data/lib/jsi/metaschema.rb +6 -0
  12. data/lib/jsi/metaschema_node/bootstrap_schema.rb +126 -0
  13. data/lib/jsi/metaschema_node.rb +262 -0
  14. data/lib/jsi/ptr.rb +314 -0
  15. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  16. data/lib/jsi/schema/application/child_application/draft04.rb +21 -0
  17. data/lib/jsi/schema/application/child_application/draft06.rb +28 -0
  18. data/lib/jsi/schema/application/child_application/draft07.rb +28 -0
  19. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  20. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  21. data/lib/jsi/schema/application/child_application.rb +13 -0
  22. data/lib/jsi/schema/application/draft04.rb +8 -0
  23. data/lib/jsi/schema/application/draft06.rb +8 -0
  24. data/lib/jsi/schema/application/draft07.rb +8 -0
  25. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  26. data/lib/jsi/schema/application/inplace_application/draft04.rb +25 -0
  27. data/lib/jsi/schema/application/inplace_application/draft06.rb +26 -0
  28. data/lib/jsi/schema/application/inplace_application/draft07.rb +32 -0
  29. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  30. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  31. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  32. data/lib/jsi/schema/application/inplace_application.rb +14 -0
  33. data/lib/jsi/schema/application.rb +12 -0
  34. data/lib/jsi/schema/draft04.rb +13 -0
  35. data/lib/jsi/schema/draft06.rb +13 -0
  36. data/lib/jsi/schema/draft07.rb +13 -0
  37. data/lib/jsi/schema/issue.rb +36 -0
  38. data/lib/jsi/schema/ref.rb +183 -0
  39. data/lib/jsi/schema/schema_ancestor_node.rb +122 -0
  40. data/lib/jsi/schema/validation/array.rb +69 -0
  41. data/lib/jsi/schema/validation/const.rb +20 -0
  42. data/lib/jsi/schema/validation/contains.rb +25 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +110 -0
  46. data/lib/jsi/schema/validation/draft06.rb +120 -0
  47. data/lib/jsi/schema/validation/draft07.rb +157 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +49 -0
  63. data/lib/jsi/schema.rb +792 -0
  64. data/lib/jsi/schema_classes.rb +357 -0
  65. data/lib/jsi/schema_registry.rb +190 -0
  66. data/lib/jsi/schema_set.rb +219 -0
  67. data/lib/jsi/simple_wrap.rb +26 -0
  68. data/lib/jsi/util/private/attr_struct.rb +130 -0
  69. data/lib/jsi/util/private/memo_map.rb +75 -0
  70. data/lib/jsi/util/private.rb +202 -0
  71. data/lib/jsi/util/typelike.rb +225 -0
  72. data/lib/jsi/util.rb +227 -0
  73. data/lib/jsi/validation/error.rb +34 -0
  74. data/lib/jsi/validation/result.rb +212 -0
  75. data/lib/jsi/validation.rb +15 -0
  76. data/lib/jsi/version.rb +5 -0
  77. data/lib/jsi.rb +105 -0
  78. data/lib/schemas/json-schema.org/draft-04/schema.rb +169 -0
  79. data/lib/schemas/json-schema.org/draft-06/schema.rb +171 -0
  80. data/lib/schemas/json-schema.org/draft-07/schema.rb +198 -0
  81. data/readme.rb +138 -0
  82. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  83. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  84. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  85. metadata +155 -0
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # a Set of JSI Schemas. always frozen.
5
+ #
6
+ # any schema instance is described by a set of schemas.
7
+ class SchemaSet < ::Set
8
+ class << self
9
+ # builds a SchemaSet from a mutable Set which is added to by the given block
10
+ #
11
+ # @yield [Set] a Set to which the block may add schemas
12
+ # @return [SchemaSet]
13
+ def build
14
+ mutable_set = Set.new
15
+ yield mutable_set
16
+ new(mutable_set)
17
+ end
18
+
19
+ # ensures the given param becomes a SchemaSet. returns the param if it is already SchemaSet, otherwise
20
+ # initializes a SchemaSet from it.
21
+ #
22
+ # @param schemas [SchemaSet, Enumerable] the object to ensure becomes a SchemaSet
23
+ # @return [SchemaSet] the given SchemaSet, or a SchemaSet initialized from the given Enumerable
24
+ # @raise [ArgumentError] when the schemas param is not an Enumerable
25
+ # @raise [Schema::NotASchemaError] when the schemas param contains objects which are not Schemas
26
+ def ensure_schema_set(schemas)
27
+ if schemas.is_a?(SchemaSet)
28
+ schemas
29
+ else
30
+ new(schemas)
31
+ end
32
+ end
33
+ end
34
+
35
+ # initializes a SchemaSet from the given enum and freezes it.
36
+ #
37
+ # if a block is given, each element of the enum is passed to it, and the result must be a Schema.
38
+ # if no block is given, the enum must contain only Schemas.
39
+ #
40
+ # @param enum [#each] the schemas to be included in the SchemaSet, or items to be passed to the block
41
+ # @yieldparam yields each element of enum for preprocessing into a Schema
42
+ # @yieldreturn [JSI::Schema]
43
+ # @raise [JSI::Schema::NotASchemaError]
44
+ def initialize(enum, &block)
45
+ if enum.is_a?(Schema)
46
+ raise(ArgumentError, [
47
+ "#{SchemaSet} initialized with a #{Schema}",
48
+ "you probably meant to pass that to #{SchemaSet}[]",
49
+ "or to wrap that schema in a Set or Array for #{SchemaSet}.new",
50
+ "given: #{enum.pretty_inspect.chomp}",
51
+ ].join("\n"))
52
+ end
53
+
54
+ unless enum.is_a?(Enumerable)
55
+ raise(ArgumentError, "#{SchemaSet} initialized with non-Enumerable: #{enum.pretty_inspect.chomp}")
56
+ end
57
+
58
+ super
59
+
60
+ not_schemas = reject { |s| s.is_a?(Schema) }
61
+ if !not_schemas.empty?
62
+ raise(Schema::NotASchemaError, [
63
+ "#{SchemaSet} initialized with non-schema objects:",
64
+ *not_schemas.map { |ns| ns.pretty_inspect.chomp },
65
+ ].join("\n"))
66
+ end
67
+
68
+ freeze
69
+ end
70
+
71
+ # Instantiates a new JSI whose content comes from the given `instance` param.
72
+ # This SchemaSet indicates the schemas of the JSI - its schemas are inplace
73
+ # applicators of this set's schemas which apply to the given instance.
74
+ #
75
+ # @param instance [Object] the instance to be represented as a JSI
76
+ # @param uri [#to_str, Addressable::URI] The retrieval URI of the instance.
77
+ #
78
+ # It is rare that this needs to be specified, and only useful for instances which contain schemas.
79
+ # See {Schema::DescribesSchema#new_schema}'s `uri` param documentation.
80
+ # @param register [Boolean] Whether schema resources in the instantiated JSI will be registered
81
+ # in the schema registry indicated by param `schema_registry`.
82
+ # This is only useful when the JSI is a schema or contains schemas.
83
+ # The JSI's root will be registered with the `uri` param, if specified, whether or not the
84
+ # root is a schema.
85
+ # @param schema_registry [SchemaRegistry, nil] The registry to use for references to other schemas and,
86
+ # depending on `register` and `uri` params, to register this JSI and/or any contained schemas with
87
+ # declared URIs.
88
+ # @param stringify_symbol_keys [Boolean] Whether the instance content will have any Symbol keys of Hashes
89
+ # replaced with Strings (recursively through the document).
90
+ # Replacement is done on a copy; the given instance is not modified.
91
+ # @return [JSI::Base subclass] a JSI whose content comes from the given instance and whose schemas are
92
+ # inplace applicators of the schemas in this set.
93
+ def new_jsi(instance,
94
+ uri: nil,
95
+ register: false,
96
+ schema_registry: JSI.schema_registry,
97
+ stringify_symbol_keys: false
98
+ )
99
+ if stringify_symbol_keys
100
+ instance = Util.deep_stringify_symbol_keys(instance)
101
+ end
102
+
103
+ applied_schemas = inplace_applicator_schemas(instance)
104
+
105
+ if uri
106
+ unless uri.respond_to?(:to_str)
107
+ raise(TypeError, "uri must be string or Addressable::URI; got: #{uri.inspect}")
108
+ end
109
+ uri = Util.uri(uri)
110
+ unless uri.absolute? && !uri.fragment
111
+ raise(ArgumentError, "uri must be an absolute URI with no fragment; got: #{uri.inspect}")
112
+ end
113
+ end
114
+
115
+ jsi_class = JSI::SchemaClasses.class_for_schemas(applied_schemas,
116
+ includes: SchemaClasses.includes_for(instance),
117
+ )
118
+ jsi = jsi_class.new(instance,
119
+ jsi_indicated_schemas: self,
120
+ jsi_schema_base_uri: uri,
121
+ jsi_schema_registry: schema_registry,
122
+ )
123
+
124
+ if register && schema_registry
125
+ schema_registry.register(jsi)
126
+ end
127
+
128
+ jsi
129
+ end
130
+
131
+ # a set of inplace applicator schemas of each schema in this set which apply to the given instance.
132
+ # (see {Schema#inplace_applicator_schemas})
133
+ #
134
+ # @param instance (see Schema#inplace_applicator_schemas)
135
+ # @return [JSI::SchemaSet]
136
+ def inplace_applicator_schemas(instance)
137
+ SchemaSet.new(each_inplace_applicator_schema(instance))
138
+ end
139
+
140
+ # yields each inplace applicator schema which applies to the given instance.
141
+ #
142
+ # @param instance (see Schema#inplace_applicator_schemas)
143
+ # @yield [JSI::Schema]
144
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
145
+ def each_inplace_applicator_schema(instance, &block)
146
+ return to_enum(__method__, instance) unless block
147
+
148
+ each do |schema|
149
+ schema.each_inplace_applicator_schema(instance, &block)
150
+ end
151
+
152
+ nil
153
+ end
154
+
155
+ # a set of child applicator subschemas of each schema in this set which apply to the child
156
+ # of the given instance on the given token.
157
+ # (see {Schema#child_applicator_schemas})
158
+ #
159
+ # @param instance (see Schema#child_applicator_schemas)
160
+ # @return [JSI::SchemaSet]
161
+ def child_applicator_schemas(token, instance)
162
+ SchemaSet.new(each_child_applicator_schema(token, instance))
163
+ end
164
+
165
+ # yields each child applicator schema which applies to the child of
166
+ # the given instance on the given token.
167
+ #
168
+ # @param (see Schema#child_applicator_schemas)
169
+ # @yield [JSI::Schema]
170
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
171
+ def each_child_applicator_schema(token, instance, &block)
172
+ return to_enum(__method__, token, instance) unless block
173
+
174
+ each do |schema|
175
+ schema.each_child_applicator_schema(token, instance, &block)
176
+ end
177
+
178
+ nil
179
+ end
180
+
181
+ # validates the given instance against our schemas
182
+ #
183
+ # @param instance [Object] the instance to validate against our schemas
184
+ # @return [JSI::Validation::Result]
185
+ def instance_validate(instance)
186
+ results = map { |schema| schema.instance_validate(instance) }
187
+ results.inject(Validation::FullResult.new, &:merge).freeze
188
+ end
189
+
190
+ # whether the given instance is valid against our schemas
191
+ # @param instance [Object] the instance to validate against our schemas
192
+ # @return [Boolean]
193
+ def instance_valid?(instance)
194
+ all? { |schema| schema.instance_valid?(instance) }
195
+ end
196
+
197
+ # @return [String]
198
+ def inspect
199
+ -"#{self.class}[#{map(&:inspect).join(", ")}]"
200
+ end
201
+
202
+ alias_method :to_s, :inspect
203
+
204
+ def pretty_print(q)
205
+ q.text self.class.to_s
206
+ q.text '['
207
+ q.group_sub {
208
+ q.nest(2) {
209
+ q.breakable('')
210
+ q.seplist(self, nil, :each) { |e|
211
+ q.pp e
212
+ }
213
+ }
214
+ }
215
+ q.breakable ''
216
+ q.text ']'
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ simple_wrap_implementation = Module.new do
5
+ def internal_child_applicate_keywords(token, instance)
6
+ yield self
7
+ end
8
+
9
+ def internal_inplace_applicate_keywords(instance, visited_refs)
10
+ yield self
11
+ end
12
+
13
+ def internal_validate_keywords(result_builder)
14
+ end
15
+ end
16
+
17
+ simple_wrap_metaschema = JSI.new_metaschema(nil, schema_implementation_modules: [simple_wrap_implementation])
18
+ SimpleWrap = simple_wrap_metaschema.new_schema_module({})
19
+
20
+ # SimpleWrap is a JSI schema module which recursively wraps nested structures
21
+ module SimpleWrap
22
+ end
23
+
24
+ SimpleWrap::Implementation = simple_wrap_implementation
25
+ SimpleWrap::METASCHEMA = simple_wrap_metaschema
26
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Util::Private
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 subclass(*attribute_keys)
19
+ bad = attribute_keys.reject { |key| key.respond_to?(:to_str) || key.is_a?(Symbol) }
20
+ unless bad.empty?
21
+ raise ArgumentError, "attribute keys must be String or Symbol; got keys: #{bad.map(&:inspect).join(', ')}"
22
+ end
23
+ attribute_keys = attribute_keys.map { |key| convert_key(key) }
24
+
25
+ all_attribute_keys = (self.attribute_keys + attribute_keys).freeze
26
+
27
+ Class.new(self).tap do |klass|
28
+ klass.define_singleton_method(:attribute_keys) { all_attribute_keys }
29
+
30
+ attribute_keys.each do |attribute_key|
31
+ # reader
32
+ klass.send(:define_method, attribute_key) do
33
+ @attributes[attribute_key]
34
+ end
35
+
36
+ # writer
37
+ klass.send(:define_method, "#{attribute_key}=") do |value|
38
+ @attributes[attribute_key] = value
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ alias_method :[], :subclass
45
+
46
+ # the attribute keys defined for this class
47
+ # @return [Set<String>]
48
+ def attribute_keys
49
+ # empty for AttrStruct itself; redefined on each subclass
50
+ Util::Private::EMPTY_SET
51
+ end
52
+
53
+ # returns a frozen string, given a string or symbol.
54
+ # returns anything else as-is for the caller to handle.
55
+ # @api private
56
+ def convert_key(key)
57
+ # TODO use Symbol#name when available on supported rubies
58
+ key.is_a?(Symbol) ? key.to_s.freeze : key.frozen? ? key : key.is_a?(String) ? key.dup.freeze : key
59
+ end
60
+ end
61
+
62
+ def initialize(attributes = {})
63
+ unless attributes.respond_to?(:to_hash)
64
+ raise(TypeError, "expected attributes to be a Hash; got: #{attributes.inspect}")
65
+ end
66
+ @attributes = {}
67
+ attributes.to_hash.each do |k, v|
68
+ @attributes[self.class.convert_key(k)] = v
69
+ end
70
+ bad = @attributes.keys.reject { |k| attribute_keys.include?(k) }
71
+ unless bad.empty?
72
+ raise UndefinedAttributeKey, "undefined attribute keys: #{bad.map(&:inspect).join(', ')}"
73
+ end
74
+ end
75
+
76
+ def [](key)
77
+ @attributes[key.is_a?(Symbol) ? key.to_s : key]
78
+ end
79
+
80
+ def []=(key, value)
81
+ key = self.class.convert_key(key)
82
+ unless attribute_keys.include?(key)
83
+ raise UndefinedAttributeKey, "undefined attribute key: #{key.inspect}"
84
+ end
85
+ @attributes[key] = value
86
+ end
87
+
88
+ # @return [String]
89
+ def inspect
90
+ -"\#<#{self.class.name}#{@attributes.map { |k, v| " #{k}: #{v.inspect}" }.join(',')}>"
91
+ end
92
+
93
+ alias_method :to_s, :inspect
94
+
95
+ # pretty-prints a representation of self to the given printer
96
+ # @return [void]
97
+ def pretty_print(q)
98
+ q.text '#<'
99
+ q.text self.class.name
100
+ q.group_sub {
101
+ q.nest(2) {
102
+ q.breakable(@attributes.empty? ? '' : ' ')
103
+ q.seplist(@attributes, nil, :each_pair) { |k, v|
104
+ q.group {
105
+ q.text k
106
+ q.text ': '
107
+ q.pp v
108
+ }
109
+ }
110
+ }
111
+ }
112
+ q.breakable ''
113
+ q.text '>'
114
+ end
115
+
116
+ # (see AttrStruct.attribute_keys)
117
+ def attribute_keys
118
+ self.class.attribute_keys
119
+ end
120
+
121
+ include FingerprintHash
122
+
123
+ # see {Util::Private::FingerprintHash}
124
+ # @api private
125
+ def jsi_fingerprint
126
+ {class: self.class, attributes: @attributes}
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Util::Private
5
+ class MemoMap
6
+ def initialize(key_by: nil, &block)
7
+ @key_by = key_by
8
+ @block = block || raise(ArgumentError, "no block given")
9
+
10
+ # each result has its own mutex to update its memoized value thread-safely
11
+ @result_mutexes = {}
12
+ # another mutex to thread-safely initialize each result mutex
13
+ @result_mutexes_mutex = Mutex.new
14
+
15
+ @results = {}
16
+ end
17
+
18
+ def key_for(inputs)
19
+ if @key_by
20
+ @key_by.call(**inputs)
21
+ else
22
+ inputs
23
+ end
24
+ end
25
+ end
26
+
27
+ class MemoMap::Mutable < MemoMap
28
+ Result = AttrStruct[*%w(
29
+ value
30
+ inputs
31
+ inputs_hash
32
+ )]
33
+
34
+ class Result
35
+ end
36
+
37
+ def [](**inputs)
38
+ key = key_for(inputs)
39
+
40
+ result_mutex = @result_mutexes_mutex.synchronize do
41
+ @result_mutexes[key] ||= Mutex.new
42
+ end
43
+
44
+ result_mutex.synchronize do
45
+ inputs_hash = inputs.hash
46
+ if @results.key?(key) && inputs_hash == @results[key].inputs_hash && inputs == @results[key].inputs
47
+ @results[key].value
48
+ else
49
+ value = @block.call(**inputs)
50
+ @results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
51
+ value
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ class MemoMap::Immutable < MemoMap
58
+ def [](**inputs)
59
+ key = key_for(inputs)
60
+
61
+ result_mutex = @result_mutexes_mutex.synchronize do
62
+ @result_mutexes[key] ||= Mutex.new
63
+ end
64
+
65
+ result_mutex.synchronize do
66
+ if @results.key?(key)
67
+ @results[key]
68
+ else
69
+ @results[key] = @block.call(**inputs)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # JSI::Util::Private classes, modules, constants, and methods are internal, and will be added and removed without warning.
5
+ #
6
+ # @api private
7
+ module Util::Private
8
+ autoload :AttrStruct, 'jsi/util/private/attr_struct'
9
+ autoload :MemoMap, 'jsi/util/private/memo_map'
10
+
11
+ extend self
12
+
13
+ EMPTY_ARY = [].freeze
14
+
15
+ EMPTY_SET = Set[].freeze
16
+
17
+ CLASSES_ALWAYS_FROZEN = Set[TrueClass, FalseClass, NilClass, Integer, Float, BigDecimal, Rational, Symbol].freeze
18
+
19
+ # is a hash as the last argument passed to keyword params? (false in ruby 3; true before - generates
20
+ # a warning in 2.7 but no way to make 2.7 behave like 3 so the warning is useless)
21
+ #
22
+ # TODO remove eventually (keyword argument compatibility)
23
+ LAST_ARGUMENT_AS_KEYWORD_PARAMETERS = begin
24
+ if Object.const_defined?(:Warning)
25
+ warn = ::Warning.instance_method(:warn)
26
+ ::Warning.send(:remove_method, :warn)
27
+ ::Warning.send(:define_method, :warn) { |*, **| }
28
+ end
29
+
30
+ -> (k: ) { k }[{k: nil}]
31
+ true
32
+ rescue ArgumentError
33
+ false
34
+ ensure
35
+ if Object.const_defined?(:Warning)
36
+ ::Warning.send(:remove_method, :warn)
37
+ ::Warning.send(:define_method, :warn, warn)
38
+ end
39
+ end
40
+
41
+ # we won't use #to_json on classes where it is defined by
42
+ # JSON::Ext::Generator::GeneratorMethods / JSON::Pure::Generator::GeneratorMethods
43
+ # this is a bit of a kluge and disregards any singleton class to_json, but it will do.
44
+ USE_TO_JSON_METHOD = Hash.new do |h, klass|
45
+ h[klass] = klass.method_defined?(:to_json) &&
46
+ klass.instance_method(:to_json).owner.name !~ /\AJSON:.*:GeneratorMethods\b/
47
+ end
48
+
49
+ RUBY_REJECT_NAME_CODEPOINTS = [
50
+ 0..31, # C0 control chars
51
+ %q( !"#$%&'()*+,-./:;<=>?@[\\]^`{|}~).each_codepoint, # printable special chars (note: "_" not included)
52
+ 127..159, # C1 control chars
53
+ ].inject(Set[], &:merge).freeze
54
+
55
+ RUBY_REJECT_NAME_RE = Regexp.new('[' + Regexp.escape(RUBY_REJECT_NAME_CODEPOINTS.to_a.pack('U*')) + ']+').freeze
56
+
57
+ # is the given name ok to use as a ruby method name?
58
+ def ok_ruby_method_name?(name)
59
+ # must be a string
60
+ return false unless name.respond_to?(:to_str)
61
+ # must not begin with a digit
62
+ return false if name =~ /\A[0-9]/
63
+ # must not contain special or control characters
64
+ return false if name =~ RUBY_REJECT_NAME_RE
65
+
66
+ return true
67
+ end
68
+
69
+ def const_name_from_parts(parts, join: '')
70
+ parts = parts.map do |part|
71
+ part = part.dup
72
+ part[/\A[^a-zA-Z]*/] = ''
73
+ part[0] = part[0].upcase if part[0]
74
+ part.gsub!(RUBY_REJECT_NAME_RE, '_')
75
+ part
76
+ end
77
+ if !parts.all?(&:empty?)
78
+ parts.reject(&:empty?).join(join).freeze
79
+ else
80
+ nil
81
+ end
82
+ end
83
+
84
+ # string or URI → frozen URI
85
+ # @return [Addressable::URI]
86
+ def uri(uri)
87
+ if uri.is_a?(Addressable::URI)
88
+ if uri.frozen?
89
+ uri
90
+ else
91
+ uri.dup.freeze
92
+ end
93
+ else
94
+ Addressable::URI.parse(uri).freeze
95
+ end
96
+ end
97
+
98
+ # this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
99
+ # to define a recursive function to return the length of an array:
100
+ #
101
+ # length = ycomb do |len|
102
+ # proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
103
+ # end
104
+ #
105
+ # length.call([0])
106
+ # # => 1
107
+ #
108
+ # see https://en.wikipedia.org/wiki/Fixed-point_combinator#Y_combinator
109
+ # and chapter 9 of the little schemer, available as the sample chapter at
110
+ # https://felleisen.org/matthias/BTLS-index.html
111
+ def ycomb
112
+ proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
113
+ end
114
+
115
+ def require_jmespath
116
+ return if instance_variable_defined?(:@jmespath_required)
117
+ begin
118
+ require 'jmespath'
119
+ rescue ::LoadError => e
120
+ # :nocov:
121
+ msg = [
122
+ "please install and/or add to your Gemfile the `jmespath` gem to use this. jmespath is not a dependency of JSI.",
123
+ "original error message:",
124
+ e.message,
125
+ ].join("\n")
126
+ raise(e.class, msg, e.backtrace)
127
+ # :nocov:
128
+ end
129
+ hashlike = JSI::SchemaSet[].new_jsi({'test' => 0})
130
+ unless JMESPath.search('test', hashlike) == 0
131
+ # :nocov:
132
+ raise(::LoadError, [
133
+ "the loaded version of jmespath cannot be used with JSI.",
134
+ "jmespath is compatible with JSI objects as of version 1.5.0",
135
+ ].join("\n"))
136
+ # :nocov:
137
+ end
138
+ @jmespath_required = true
139
+ nil
140
+ end
141
+
142
+ # Defines equality methods and #hash (for Hash / Set), based on a method #jsi_fingerprint
143
+ # implemented by the includer. #jsi_fingerprint is to include the class and any properties
144
+ # of the instance which constitute its identity.
145
+ module FingerprintHash
146
+ # overrides BasicObject#==
147
+ def ==(other)
148
+ __id__ == other.__id__ || (other.is_a?(FingerprintHash) && jsi_fingerprint == other.jsi_fingerprint)
149
+ end
150
+
151
+ alias_method :eql?, :==
152
+
153
+ # overrides Kernel#hash
154
+ def hash
155
+ jsi_fingerprint.hash
156
+ end
157
+ end
158
+
159
+ module FingerprintHash::Immutable
160
+ include FingerprintHash
161
+
162
+ def ==(other)
163
+ return true if __id__ == other.__id__
164
+ return false unless other.is_a?(FingerprintHash)
165
+ # FingerprintHash::Immutable#hash being memoized, comparing that is basically free.
166
+ # not done with FingerprintHash, its #hash can be expensive.
167
+ return false if other.is_a?(FingerprintHash::Immutable) && hash != other.hash
168
+ jsi_fingerprint == other.jsi_fingerprint
169
+ end
170
+
171
+ alias_method :eql?, :==
172
+
173
+ def hash
174
+ @jsi_fingerprint_hash ||= jsi_fingerprint.hash
175
+ end
176
+
177
+ def freeze
178
+ hash
179
+ super
180
+ end
181
+ end
182
+
183
+ module Virtual
184
+ class InstantiationError < StandardError
185
+ end
186
+
187
+ # this virtual class is not intended to be instantiated except by its subclasses, which override #initialize
188
+ def initialize
189
+ # :nocov:
190
+ raise(InstantiationError, "cannot instantiate virtual class #{self.class}")
191
+ # :nocov:
192
+ end
193
+
194
+ # virtual_method is used to indicate that the method calling it must be implemented on the (non-virtual) subclass
195
+ def virtual_method
196
+ # :nocov:
197
+ raise(Bug, "class #{self.class} must implement #{caller_locations.first.label}")
198
+ # :nocov:
199
+ end
200
+ end
201
+ end
202
+ end