jsi 0.4.0 → 0.7.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.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +33 -0
- data/LICENSE.md +1 -1
- data/README.md +114 -42
- data/jsi.gemspec +14 -12
- data/lib/jsi/base/node.rb +183 -0
- data/lib/jsi/base.rb +388 -220
- data/lib/jsi/jsi_coder.rb +8 -7
- data/lib/jsi/metaschema.rb +0 -1
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
- data/lib/jsi/metaschema_node.rb +159 -135
- data/lib/jsi/ptr.rb +303 -0
- data/lib/jsi/schema/application/child_application/contains.rb +25 -0
- data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
- data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
- data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
- data/lib/jsi/schema/application/child_application/items.rb +18 -0
- data/lib/jsi/schema/application/child_application/properties.rb +25 -0
- data/lib/jsi/schema/application/child_application.rb +38 -0
- data/lib/jsi/schema/application/draft04.rb +8 -0
- data/lib/jsi/schema/application/draft06.rb +8 -0
- data/lib/jsi/schema/application/draft07.rb +8 -0
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
- data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
- data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
- data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
- data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
- data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
- data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
- data/lib/jsi/schema/application/inplace_application.rb +41 -0
- data/lib/jsi/schema/application.rb +12 -0
- data/lib/jsi/schema/draft04.rb +14 -0
- data/lib/jsi/schema/draft06.rb +14 -0
- data/lib/jsi/schema/draft07.rb +14 -0
- data/lib/jsi/schema/issue.rb +36 -0
- data/lib/jsi/schema/ref.rb +160 -0
- data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
- data/lib/jsi/schema/validation/array.rb +69 -0
- data/lib/jsi/schema/validation/const.rb +20 -0
- data/lib/jsi/schema/validation/contains.rb +25 -0
- data/lib/jsi/schema/validation/core.rb +39 -0
- data/lib/jsi/schema/validation/dependencies.rb +49 -0
- data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
- data/lib/jsi/schema/validation/draft04.rb +112 -0
- data/lib/jsi/schema/validation/draft06.rb +122 -0
- data/lib/jsi/schema/validation/draft07.rb +159 -0
- data/lib/jsi/schema/validation/enum.rb +25 -0
- data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
- data/lib/jsi/schema/validation/items.rb +54 -0
- data/lib/jsi/schema/validation/not.rb +20 -0
- data/lib/jsi/schema/validation/numeric.rb +121 -0
- data/lib/jsi/schema/validation/object.rb +45 -0
- data/lib/jsi/schema/validation/pattern.rb +34 -0
- data/lib/jsi/schema/validation/properties.rb +101 -0
- data/lib/jsi/schema/validation/property_names.rb +32 -0
- data/lib/jsi/schema/validation/ref.rb +40 -0
- data/lib/jsi/schema/validation/required.rb +27 -0
- data/lib/jsi/schema/validation/someof.rb +90 -0
- data/lib/jsi/schema/validation/string.rb +47 -0
- data/lib/jsi/schema/validation/type.rb +49 -0
- data/lib/jsi/schema/validation.rb +51 -0
- data/lib/jsi/schema.rb +508 -149
- data/lib/jsi/schema_classes.rb +199 -59
- data/lib/jsi/schema_registry.rb +151 -0
- data/lib/jsi/schema_set.rb +181 -0
- data/lib/jsi/simple_wrap.rb +23 -4
- data/lib/jsi/util/private/attr_struct.rb +127 -0
- data/lib/jsi/util/private.rb +204 -0
- data/lib/jsi/util/typelike.rb +229 -0
- data/lib/jsi/util.rb +89 -53
- data/lib/jsi/validation/error.rb +34 -0
- data/lib/jsi/validation/result.rb +210 -0
- data/lib/jsi/validation.rb +15 -0
- data/lib/jsi/version.rb +3 -1
- data/lib/jsi.rb +44 -14
- data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
- data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
- data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
- data/readme.rb +138 -0
- data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
- data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
- data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
- metadata +75 -122
- data/.simplecov +0 -3
- data/Rakefile.rb +0 -9
- data/lib/jsi/base/to_rb.rb +0 -128
- data/lib/jsi/json/node.rb +0 -203
- data/lib/jsi/json/pointer.rb +0 -419
- data/lib/jsi/json-schema-fragments.rb +0 -61
- data/lib/jsi/json.rb +0 -10
- data/lib/jsi/pathed_node.rb +0 -118
- data/lib/jsi/typelike_modules.rb +0 -240
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +0 -323
- data/test/base_hash_test.rb +0 -337
- data/test/base_test.rb +0 -486
- data/test/jsi_coder_test.rb +0 -85
- data/test/jsi_json_arraynode_test.rb +0 -150
- data/test/jsi_json_hashnode_test.rb +0 -132
- data/test/jsi_json_node_test.rb +0 -257
- data/test/jsi_json_pointer_test.rb +0 -102
- data/test/jsi_test.rb +0 -11
- data/test/jsi_typelike_as_json_test.rb +0 -53
- data/test/metaschema_node_test.rb +0 -19
- data/test/schema_module_test.rb +0 -21
- data/test/schema_test.rb +0 -208
- data/test/spreedly_openapi_test.rb +0 -8
- data/test/test_helper.rb +0 -97
- data/test/util_test.rb +0 -62
data/lib/jsi/simple_wrap.rb
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module JSI
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
simple_wrap_implementation = Module.new do
|
|
5
|
+
include Schema
|
|
6
|
+
include Schema::Application::ChildApplication
|
|
7
|
+
include Schema::Application::InplaceApplication
|
|
8
|
+
include Schema::Validation::Core
|
|
9
|
+
|
|
10
|
+
def internal_child_applicate_keywords(token, instance)
|
|
11
|
+
yield self
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def internal_inplace_applicate_keywords(instance, visited_refs)
|
|
15
|
+
yield self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def internal_validate_keywords(result_builder)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
simple_wrap_metaschema = MetaschemaNode.new(nil, schema_implementation_modules: [simple_wrap_implementation])
|
|
23
|
+
SimpleWrap = simple_wrap_metaschema.new_schema_module({})
|
|
8
24
|
|
|
9
25
|
# SimpleWrap is a JSI schema module which recursively wraps nested structures
|
|
10
26
|
module SimpleWrap
|
|
11
27
|
end
|
|
28
|
+
|
|
29
|
+
SimpleWrap::Implementation = simple_wrap_implementation
|
|
30
|
+
SimpleWrap::METASCHEMA = simple_wrap_metaschema
|
|
12
31
|
end
|
|
@@ -0,0 +1,127 @@
|
|
|
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.empty? ? '' : ' '}#{@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
|
+
def jsi_fingerprint
|
|
123
|
+
{class: self.class, attributes: @attributes}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
|
|
10
|
+
EMPTY_ARY = [].freeze
|
|
11
|
+
|
|
12
|
+
EMPTY_SET = Set[].freeze
|
|
13
|
+
|
|
14
|
+
# is a hash as the last argument passed to keyword params? (false in ruby 3; true before - generates
|
|
15
|
+
# a warning in 2.7 but no way to make 2.7 behave like 3 so the warning is useless)
|
|
16
|
+
#
|
|
17
|
+
# TODO remove eventually (keyword argument compatibility)
|
|
18
|
+
LAST_ARGUMENT_AS_KEYWORD_PARAMETERS = begin
|
|
19
|
+
if Object.const_defined?(:Warning)
|
|
20
|
+
warn = ::Warning.instance_method(:warn)
|
|
21
|
+
::Warning.send(:remove_method, :warn)
|
|
22
|
+
::Warning.send(:define_method, :warn) { |*, **| }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
-> (k: ) { k }[{k: nil}]
|
|
26
|
+
true
|
|
27
|
+
rescue ArgumentError
|
|
28
|
+
false
|
|
29
|
+
ensure
|
|
30
|
+
if Object.const_defined?(:Warning)
|
|
31
|
+
::Warning.send(:remove_method, :warn)
|
|
32
|
+
::Warning.send(:define_method, :warn, warn)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# is the given name ok to use as a ruby method name?
|
|
37
|
+
def ok_ruby_method_name?(name)
|
|
38
|
+
# must be a string
|
|
39
|
+
return false unless name.respond_to?(:to_str)
|
|
40
|
+
# must not begin with a digit
|
|
41
|
+
return false if name =~ /\A[0-9]/
|
|
42
|
+
# must not contain characters special to ruby syntax
|
|
43
|
+
return false if name =~ /[\\\s\#;\.,\(\)\[\]\{\}'"`%\+\-\/\*\^\|&=<>\?:!@\$~]/
|
|
44
|
+
|
|
45
|
+
return true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
|
|
49
|
+
# to define a recursive function to return the length of an array:
|
|
50
|
+
#
|
|
51
|
+
# length = ycomb do |len|
|
|
52
|
+
# proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# length.call([0])
|
|
56
|
+
# # => 1
|
|
57
|
+
#
|
|
58
|
+
# see https://en.wikipedia.org/wiki/Fixed-point_combinator#Y_combinator
|
|
59
|
+
# and chapter 9 of the little schemer, available as the sample chapter at
|
|
60
|
+
# https://felleisen.org/matthias/BTLS-index.html
|
|
61
|
+
def ycomb
|
|
62
|
+
proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def require_jmespath
|
|
66
|
+
return if instance_variable_defined?(:@jmespath_required)
|
|
67
|
+
begin
|
|
68
|
+
require 'jmespath'
|
|
69
|
+
rescue ::LoadError => e
|
|
70
|
+
# :nocov:
|
|
71
|
+
msg = [
|
|
72
|
+
"please install and/or add to your Gemfile the `jmespath` gem to use this. jmespath is not a dependency of JSI.",
|
|
73
|
+
"original error message:",
|
|
74
|
+
e.message,
|
|
75
|
+
].join("\n")
|
|
76
|
+
raise(e.class, msg, e.backtrace)
|
|
77
|
+
# :nocov:
|
|
78
|
+
end
|
|
79
|
+
hashlike = JSI::SchemaSet[].new_jsi({'test' => 0})
|
|
80
|
+
unless JMESPath.search('test', hashlike) == 0
|
|
81
|
+
# :nocov:
|
|
82
|
+
raise(::LoadError, [
|
|
83
|
+
"the loaded version of jmespath cannot be used with JSI.",
|
|
84
|
+
"jmespath is compatible with JSI objects as of version 1.5.0",
|
|
85
|
+
].join("\n"))
|
|
86
|
+
# :nocov:
|
|
87
|
+
end
|
|
88
|
+
@jmespath_required = true
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
module FingerprintHash
|
|
93
|
+
# overrides BasicObject#==
|
|
94
|
+
def ==(other)
|
|
95
|
+
__id__ == other.__id__ || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == jsi_fingerprint)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
alias_method :eql?, :==
|
|
99
|
+
|
|
100
|
+
# overrides Kernel#hash
|
|
101
|
+
def hash
|
|
102
|
+
jsi_fingerprint.hash
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class MemoMap
|
|
107
|
+
Result = AttrStruct[*%w(
|
|
108
|
+
value
|
|
109
|
+
inputs
|
|
110
|
+
inputs_hash
|
|
111
|
+
)]
|
|
112
|
+
|
|
113
|
+
class Result
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def initialize(key_by: nil, &block)
|
|
117
|
+
@key_by = key_by
|
|
118
|
+
@block = block
|
|
119
|
+
|
|
120
|
+
# each result has its own mutex to update its memoized value thread-safely
|
|
121
|
+
@result_mutexes = {}
|
|
122
|
+
# another mutex to thread-safely initialize each result mutex
|
|
123
|
+
@result_mutexes_mutex = Mutex.new
|
|
124
|
+
|
|
125
|
+
@results = {}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def [](**inputs)
|
|
129
|
+
if @key_by
|
|
130
|
+
key = @key_by.call(**inputs)
|
|
131
|
+
else
|
|
132
|
+
key = inputs
|
|
133
|
+
end
|
|
134
|
+
result_mutex = @result_mutexes_mutex.synchronize do
|
|
135
|
+
@result_mutexes[key] ||= Mutex.new
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
result_mutex.synchronize do
|
|
139
|
+
inputs_hash = inputs.hash
|
|
140
|
+
if @results.key?(key) && inputs_hash == @results[key].inputs_hash && inputs == @results[key].inputs
|
|
141
|
+
@results[key].value
|
|
142
|
+
else
|
|
143
|
+
value = @block.call(**inputs)
|
|
144
|
+
@results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
|
|
145
|
+
value
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
module Memoize
|
|
152
|
+
def self.extended(object)
|
|
153
|
+
object.send(:jsi_initialize_memos)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def jsi_initialize_memos
|
|
159
|
+
@jsi_memomaps_mutex = Mutex.new
|
|
160
|
+
@jsi_memomaps = {}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @return [Util::MemoMap]
|
|
164
|
+
def jsi_memomap(name, **options, &block)
|
|
165
|
+
raise(Bug, 'must jsi_initialize_memos') unless @jsi_memomaps
|
|
166
|
+
unless @jsi_memomaps.key?(name)
|
|
167
|
+
@jsi_memomaps_mutex.synchronize do
|
|
168
|
+
# note: this ||= appears redundant with `unless @jsi_memomaps.key?(name)`,
|
|
169
|
+
# but that check is not thread safe. this check is.
|
|
170
|
+
@jsi_memomaps[name] ||= Util::MemoMap.new(**options, &block)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
@jsi_memomaps[name]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def jsi_memoize(name, **inputs, &block)
|
|
177
|
+
jsi_memomap(name, &block)[**inputs]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
module Virtual
|
|
182
|
+
class InstantiationError < StandardError
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# this virtual class is not intended to be instantiated except by its subclasses, which override #initialize
|
|
186
|
+
def initialize
|
|
187
|
+
# :nocov:
|
|
188
|
+
raise(InstantiationError, "cannot instantiate virtual class #{self.class}")
|
|
189
|
+
# :nocov:
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# virtual_method is used to indicate that the method calling it must be implemented on the (non-virtual) subclass
|
|
193
|
+
def virtual_method
|
|
194
|
+
# :nocov:
|
|
195
|
+
raise(Bug, "class #{self.class} must implement #{caller_locations.first.label}")
|
|
196
|
+
# :nocov:
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
public
|
|
201
|
+
|
|
202
|
+
extend self
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSI
|
|
4
|
+
# @deprecated after v0.6
|
|
5
|
+
module Typelike
|
|
6
|
+
extend Util
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# a module of methods for objects which behave like Hash but are not Hash.
|
|
10
|
+
#
|
|
11
|
+
# this module is intended to be internal to JSI. no guarantees or API promises
|
|
12
|
+
# are made for non-JSI classes including this module.
|
|
13
|
+
module Util::Hashlike
|
|
14
|
+
# safe methods which can be delegated to #to_hash (which the includer is assumed to have defined).
|
|
15
|
+
# 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
|
|
16
|
+
|
|
17
|
+
# methods which do not need to access the value.
|
|
18
|
+
SAFE_KEY_ONLY_METHODS = %w(each_key empty? has_key? include? key? keys length member? size)
|
|
19
|
+
SAFE_KEY_VALUE_METHODS = %w(< <= > >= any? assoc compact dig each_pair each_value fetch fetch_values has_value? invert key merge rassoc reject select to_h to_proc transform_values value? values values_at)
|
|
20
|
+
DESTRUCTIVE_METHODS = %w(clear delete delete_if keep_if reject! replace select! shift)
|
|
21
|
+
# these return a modified copy
|
|
22
|
+
safe_modified_copy_methods = %w(compact)
|
|
23
|
+
# select and reject will return a modified copy but need the yielded block variable value from #[]
|
|
24
|
+
safe_kv_block_modified_copy_methods = %w(select reject)
|
|
25
|
+
SAFE_METHODS = SAFE_KEY_ONLY_METHODS | SAFE_KEY_VALUE_METHODS
|
|
26
|
+
custom_methods = %w(merge) # defined below
|
|
27
|
+
safe_to_hash_methods = SAFE_METHODS -
|
|
28
|
+
safe_modified_copy_methods -
|
|
29
|
+
safe_kv_block_modified_copy_methods -
|
|
30
|
+
custom_methods
|
|
31
|
+
safe_to_hash_methods.each do |method_name|
|
|
32
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
|
33
|
+
define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
|
|
34
|
+
else
|
|
35
|
+
define_method(method_name) { |*a, **kw, &b| to_hash.public_send(method_name, *a, **kw, &b) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
safe_modified_copy_methods.each do |method_name|
|
|
39
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
|
40
|
+
define_method(method_name) do |*a, &b|
|
|
41
|
+
jsi_modified_copy do |object_to_modify|
|
|
42
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
|
43
|
+
responsive_object.public_send(method_name, *a, &b)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
define_method(method_name) do |*a, **kw, &b|
|
|
48
|
+
jsi_modified_copy do |object_to_modify|
|
|
49
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
|
50
|
+
responsive_object.public_send(method_name, *a, **kw, &b)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
safe_kv_block_modified_copy_methods.each do |method_name|
|
|
56
|
+
define_method(method_name) do |**kw, &b|
|
|
57
|
+
jsi_modified_copy do |object_to_modify|
|
|
58
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
|
59
|
+
responsive_object.public_send(method_name) do |k, _v|
|
|
60
|
+
b.call(k, self[k, **kw])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# like [Hash#update](https://ruby-doc.org/core/Hash.html#method-i-update)
|
|
67
|
+
# @param other [#to_hash] the other hash to update this hash from
|
|
68
|
+
# @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
|
|
69
|
+
# is determined by calling the block with the key, its value in self and its value in other.
|
|
70
|
+
# @return self, updated with other
|
|
71
|
+
# @raise [TypeError] when `other` does not respond to #to_hash
|
|
72
|
+
def update(other, &block)
|
|
73
|
+
unless other.respond_to?(:to_hash)
|
|
74
|
+
raise(TypeError, "cannot update with argument that does not respond to #to_hash: #{other.pretty_inspect.chomp}")
|
|
75
|
+
end
|
|
76
|
+
self_respondingto_key = respond_to?(:key?) ? self : to_hash
|
|
77
|
+
other.to_hash.each_pair do |key, value|
|
|
78
|
+
if block && self_respondingto_key.key?(key)
|
|
79
|
+
value = yield(key, self[key], value)
|
|
80
|
+
end
|
|
81
|
+
self[key] = value
|
|
82
|
+
end
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
alias_method :merge!, :update
|
|
87
|
+
|
|
88
|
+
# like [Hash#merge](https://ruby-doc.org/core/Hash.html#method-i-merge)
|
|
89
|
+
# @param other [#to_hash] the other hash to merge into this
|
|
90
|
+
# @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
|
|
91
|
+
# is determined by calling the block with the key, its value in self and its value in other.
|
|
92
|
+
# @return duplicate of this hash with the other hash merged in
|
|
93
|
+
# @raise [TypeError] when `other` does not respond to #to_hash
|
|
94
|
+
def merge(other, &block)
|
|
95
|
+
dup.update(other, &block)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# basically the same #inspect as Hash, but has the class name and, if responsive,
|
|
99
|
+
# self's #jsi_object_group_text
|
|
100
|
+
# @return [String]
|
|
101
|
+
def inspect
|
|
102
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
|
103
|
+
"\#{<#{object_group_str}>#{map { |k, v| " #{k.inspect} => #{v.inspect}" }.join(',')}}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
alias_method :to_s, :inspect
|
|
107
|
+
|
|
108
|
+
# pretty-prints a representation of this hashlike to the given printer
|
|
109
|
+
# @return [void]
|
|
110
|
+
def pretty_print(q)
|
|
111
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
|
112
|
+
q.text "\#{<#{object_group_str}>"
|
|
113
|
+
q.group_sub {
|
|
114
|
+
q.nest(2) {
|
|
115
|
+
q.breakable(empty? ? '' : ' ')
|
|
116
|
+
q.seplist(self, nil, :each_pair) { |k, v|
|
|
117
|
+
q.group {
|
|
118
|
+
q.pp k
|
|
119
|
+
q.text ' => '
|
|
120
|
+
q.pp v
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
q.breakable ''
|
|
126
|
+
q.text '}'
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# a module of methods for objects which behave like Array but are not Array.
|
|
131
|
+
#
|
|
132
|
+
# this module is intended to be internal to JSI. no guarantees or API promises
|
|
133
|
+
# are made for non-JSI classes including this module.
|
|
134
|
+
module Util::Arraylike
|
|
135
|
+
# safe methods which can be delegated to #to_ary (which the includer is assumed to have defined).
|
|
136
|
+
# 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
|
|
137
|
+
|
|
138
|
+
# methods which do not need to access the element.
|
|
139
|
+
SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size)
|
|
140
|
+
# there are some ambiguous ones that are omitted, like #sort, #map / #collect.
|
|
141
|
+
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)
|
|
142
|
+
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)
|
|
143
|
+
|
|
144
|
+
# methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
|
|
145
|
+
safe_modified_copy_methods = %w(compact)
|
|
146
|
+
|
|
147
|
+
# methods that return a modified copy and do need handling of block variables
|
|
148
|
+
safe_el_block_methods = %w(reject select)
|
|
149
|
+
|
|
150
|
+
SAFE_METHODS = SAFE_INDEX_ONLY_METHODS | SAFE_INDEX_ELEMENT_METHODS
|
|
151
|
+
safe_to_ary_methods = SAFE_METHODS - safe_modified_copy_methods - safe_el_block_methods
|
|
152
|
+
safe_to_ary_methods.each do |method_name|
|
|
153
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
|
154
|
+
define_method(method_name) { |*a, &b| to_ary.public_send(method_name, *a, &b) }
|
|
155
|
+
else
|
|
156
|
+
define_method(method_name) { |*a, **kw, &b| to_ary.public_send(method_name, *a, **kw, &b) }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
safe_modified_copy_methods.each do |method_name|
|
|
160
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
|
161
|
+
define_method(method_name) do |*a, &b|
|
|
162
|
+
jsi_modified_copy do |object_to_modify|
|
|
163
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
|
164
|
+
responsive_object.public_send(method_name, *a, &b)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
else
|
|
168
|
+
define_method(method_name) do |*a, **kw, &b|
|
|
169
|
+
jsi_modified_copy do |object_to_modify|
|
|
170
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
|
171
|
+
responsive_object.public_send(method_name, *a, **kw, &b)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
safe_el_block_methods.each do |method_name|
|
|
177
|
+
define_method(method_name) do |**kw, &b|
|
|
178
|
+
jsi_modified_copy do |object_to_modify|
|
|
179
|
+
i = 0
|
|
180
|
+
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
|
181
|
+
responsive_object.public_send(method_name) do |_e|
|
|
182
|
+
b.call(self[i, **kw]).tap { i += 1 }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# see [Array#assoc](https://ruby-doc.org/core/Array.html#method-i-assoc)
|
|
189
|
+
def assoc(obj)
|
|
190
|
+
# note: assoc implemented here (instead of delegated) due to inconsistencies in whether
|
|
191
|
+
# other implementations expect each element to be an Array or to respond to #to_ary
|
|
192
|
+
detect { |e| e.respond_to?(:to_ary) and e[0] == obj }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# see [Array#rassoc](https://ruby-doc.org/core/Array.html#method-i-rassoc)
|
|
196
|
+
def rassoc(obj)
|
|
197
|
+
# note: rassoc implemented here (instead of delegated) due to inconsistencies in whether
|
|
198
|
+
# other implementations expect each element to be an Array or to respond to #to_ary
|
|
199
|
+
detect { |e| e.respond_to?(:to_ary) and e[1] == obj }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# basically the same #inspect as Array, but has the class name and, if responsive,
|
|
203
|
+
# self's #jsi_object_group_text
|
|
204
|
+
# @return [String]
|
|
205
|
+
def inspect
|
|
206
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
|
207
|
+
"\#[<#{object_group_str}>#{map { |e| ' ' + e.inspect }.join(',')}]"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
alias_method :to_s, :inspect
|
|
211
|
+
|
|
212
|
+
# pretty-prints a representation of this arraylike to the given printer
|
|
213
|
+
# @return [void]
|
|
214
|
+
def pretty_print(q)
|
|
215
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
|
216
|
+
q.text "\#[<#{object_group_str}>"
|
|
217
|
+
q.group_sub {
|
|
218
|
+
q.nest(2) {
|
|
219
|
+
q.breakable(empty? ? '' : ' ')
|
|
220
|
+
q.seplist(self, nil, :each) { |e|
|
|
221
|
+
q.pp e
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
q.breakable ''
|
|
226
|
+
q.text ']'
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|