jsi 0.6.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/CHANGELOG.md +18 -0
- data/LICENSE.md +1 -1
- data/README.md +11 -6
- data/jsi.gemspec +30 -0
- data/lib/jsi/base/node.rb +183 -0
- data/lib/jsi/base.rb +135 -161
- data/lib/jsi/jsi_coder.rb +3 -3
- data/lib/jsi/metaschema.rb +0 -1
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +9 -8
- data/lib/jsi/metaschema_node.rb +48 -51
- data/lib/jsi/ptr.rb +28 -17
- data/lib/jsi/schema/application/child_application/contains.rb +11 -2
- data/lib/jsi/schema/application/child_application/items.rb +3 -3
- data/lib/jsi/schema/application/child_application/properties.rb +3 -3
- data/lib/jsi/schema/application/child_application.rb +1 -3
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +1 -1
- data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +3 -3
- data/lib/jsi/schema/application/inplace_application/ref.rb +1 -1
- data/lib/jsi/schema/application/inplace_application/someof.rb +26 -11
- data/lib/jsi/schema/application/inplace_application.rb +1 -6
- data/lib/jsi/schema/ref.rb +3 -2
- data/lib/jsi/schema/schema_ancestor_node.rb +11 -17
- data/lib/jsi/schema/validation/array.rb +3 -3
- data/lib/jsi/schema/validation/const.rb +1 -1
- data/lib/jsi/schema/validation/contains.rb +1 -1
- data/lib/jsi/schema/validation/dependencies.rb +1 -1
- data/lib/jsi/schema/validation/draft04/minmax.rb +6 -6
- data/lib/jsi/schema/validation/enum.rb +1 -1
- data/lib/jsi/schema/validation/ifthenelse.rb +5 -5
- data/lib/jsi/schema/validation/items.rb +4 -4
- data/lib/jsi/schema/validation/not.rb +1 -1
- data/lib/jsi/schema/validation/numeric.rb +5 -5
- data/lib/jsi/schema/validation/object.rb +2 -2
- data/lib/jsi/schema/validation/pattern.rb +1 -1
- data/lib/jsi/schema/validation/properties.rb +3 -3
- data/lib/jsi/schema/validation/property_names.rb +1 -1
- data/lib/jsi/schema/validation/ref.rb +1 -1
- data/lib/jsi/schema/validation/required.rb +1 -1
- data/lib/jsi/schema/validation/someof.rb +3 -3
- data/lib/jsi/schema/validation/string.rb +2 -2
- data/lib/jsi/schema/validation/type.rb +1 -1
- data/lib/jsi/schema/validation.rb +1 -1
- data/lib/jsi/schema.rb +91 -85
- data/lib/jsi/schema_classes.rb +70 -45
- data/lib/jsi/schema_registry.rb +15 -5
- data/lib/jsi/schema_set.rb +42 -2
- data/lib/jsi/simple_wrap.rb +23 -4
- data/lib/jsi/util/{attr_struct.rb → private/attr_struct.rb} +40 -19
- data/lib/jsi/util/private.rb +204 -0
- data/lib/jsi/{typelike_modules.rb → util/typelike.rb} +56 -82
- data/lib/jsi/util.rb +68 -148
- data/lib/jsi/version.rb +1 -1
- data/lib/jsi.rb +1 -17
- data/lib/schemas/json-schema.org/draft-04/schema.rb +3 -1
- data/lib/schemas/json-schema.org/draft-06/schema.rb +3 -1
- data/lib/schemas/json-schema.org/draft-07/schema.rb +3 -1
- metadata +11 -9
- data/lib/jsi/pathed_node.rb +0 -116
data/lib/jsi/schema_set.rb
CHANGED
@@ -42,12 +42,21 @@ module JSI
|
|
42
42
|
# @yieldreturn [JSI::Schema]
|
43
43
|
# @raise [JSI::Schema::NotASchemaError]
|
44
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
|
+
|
45
54
|
super
|
46
55
|
|
47
56
|
not_schemas = reject { |s| s.is_a?(Schema) }
|
48
57
|
if !not_schemas.empty?
|
49
58
|
raise(Schema::NotASchemaError, [
|
50
|
-
"
|
59
|
+
"#{SchemaSet} initialized with non-schema objects:",
|
51
60
|
*not_schemas.map { |ns| ns.pretty_inspect.chomp },
|
52
61
|
].join("\n"))
|
53
62
|
end
|
@@ -71,7 +80,10 @@ module JSI
|
|
71
80
|
)
|
72
81
|
applied_schemas = inplace_applicator_schemas(instance)
|
73
82
|
|
74
|
-
|
83
|
+
jsi_class = JSI::SchemaClasses.class_for_schemas(applied_schemas,
|
84
|
+
includes: SchemaClasses.includes_for(instance),
|
85
|
+
)
|
86
|
+
jsi = jsi_class.new(instance,
|
75
87
|
jsi_schema_base_uri: uri,
|
76
88
|
)
|
77
89
|
|
@@ -102,6 +114,32 @@ module JSI
|
|
102
114
|
nil
|
103
115
|
end
|
104
116
|
|
117
|
+
# a set of child applicator subschemas of each schema in this set which apply to the child
|
118
|
+
# of the given instance on the given token.
|
119
|
+
# (see {Schema::Application::ChildApplication#child_applicator_schemas})
|
120
|
+
#
|
121
|
+
# @param instance (see Schema::Application::ChildApplication#child_applicator_schemas)
|
122
|
+
# @return [JSI::SchemaSet]
|
123
|
+
def child_applicator_schemas(token, instance)
|
124
|
+
SchemaSet.new(each_child_applicator_schema(token, instance))
|
125
|
+
end
|
126
|
+
|
127
|
+
# yields each child applicator schema which applies to the child of
|
128
|
+
# the given instance on the given token.
|
129
|
+
#
|
130
|
+
# @param (see Schema::Application::ChildApplication#child_applicator_schemas)
|
131
|
+
# @yield [JSI::Schema]
|
132
|
+
# @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
|
133
|
+
def each_child_applicator_schema(token, instance, &block)
|
134
|
+
return to_enum(__method__, token, instance) unless block
|
135
|
+
|
136
|
+
each do |schema|
|
137
|
+
schema.each_child_applicator_schema(token, instance, &block)
|
138
|
+
end
|
139
|
+
|
140
|
+
nil
|
141
|
+
end
|
142
|
+
|
105
143
|
# validates the given instance against our schemas
|
106
144
|
#
|
107
145
|
# @param instance [Object] the instance to validate against our schemas
|
@@ -123,6 +161,8 @@ module JSI
|
|
123
161
|
"#{self.class}[#{map(&:inspect).join(", ")}]"
|
124
162
|
end
|
125
163
|
|
164
|
+
alias_method :to_s, :inspect
|
165
|
+
|
126
166
|
def pretty_print(q)
|
127
167
|
q.text self.class.to_s
|
128
168
|
q.text '['
|
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
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module JSI
|
4
|
-
module Util
|
4
|
+
module Util::Private
|
5
5
|
# like a Struct, but stores all the attributes in one @attributes Hash, instead of individual instance
|
6
6
|
# variables for each attribute.
|
7
7
|
# this tends to be easier to work with and more flexible. keys which are symbols are converted to strings.
|
@@ -15,22 +15,18 @@ module JSI
|
|
15
15
|
class << self
|
16
16
|
# creates a AttrStruct subclass with the given attribute keys.
|
17
17
|
# @param attribute_keys [Enumerable<String, Symbol>]
|
18
|
-
def
|
19
|
-
unless self == AttrStruct
|
20
|
-
# :nocov:
|
21
|
-
raise(NotImplementedError, "AttrStruct multiple inheritance not supported")
|
22
|
-
# :nocov:
|
23
|
-
end
|
24
|
-
|
18
|
+
def subclass(*attribute_keys)
|
25
19
|
bad = attribute_keys.reject { |key| key.respond_to?(:to_str) || key.is_a?(Symbol) }
|
26
20
|
unless bad.empty?
|
27
21
|
raise ArgumentError, "attribute keys must be String or Symbol; got keys: #{bad.map(&:inspect).join(', ')}"
|
28
22
|
end
|
29
|
-
attribute_keys = attribute_keys.map { |key| key
|
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 }
|
30
29
|
|
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
30
|
attribute_keys.each do |attribute_key|
|
35
31
|
# reader
|
36
32
|
klass.send(:define_method, attribute_key) do
|
@@ -44,28 +40,46 @@ module JSI
|
|
44
40
|
end
|
45
41
|
end
|
46
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
|
47
60
|
end
|
48
61
|
|
49
62
|
def initialize(attributes = {})
|
50
63
|
unless attributes.respond_to?(:to_hash)
|
51
64
|
raise(TypeError, "expected attributes to be a Hash; got: #{attributes.inspect}")
|
52
65
|
end
|
53
|
-
attributes =
|
54
|
-
|
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) }
|
55
71
|
unless bad.empty?
|
56
72
|
raise UndefinedAttributeKey, "undefined attribute keys: #{bad.map(&:inspect).join(', ')}"
|
57
73
|
end
|
58
|
-
@attributes = attributes
|
59
74
|
end
|
60
75
|
|
61
76
|
def [](key)
|
62
|
-
key
|
63
|
-
@attributes[key]
|
77
|
+
@attributes[key.is_a?(Symbol) ? key.to_s : key]
|
64
78
|
end
|
65
79
|
|
66
80
|
def []=(key, value)
|
67
|
-
key =
|
68
|
-
unless
|
81
|
+
key = self.class.convert_key(key)
|
82
|
+
unless attribute_keys.include?(key)
|
69
83
|
raise UndefinedAttributeKey, "undefined attribute key: #{key.inspect}"
|
70
84
|
end
|
71
85
|
@attributes[key] = value
|
@@ -76,6 +90,8 @@ module JSI
|
|
76
90
|
"\#<#{self.class.name}#{@attributes.empty? ? '' : ' '}#{@attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>"
|
77
91
|
end
|
78
92
|
|
93
|
+
alias_method :to_s, :inspect
|
94
|
+
|
79
95
|
# pretty-prints a representation of self to the given printer
|
80
96
|
# @return [void]
|
81
97
|
def pretty_print(q)
|
@@ -97,6 +113,11 @@ module JSI
|
|
97
113
|
q.text '>'
|
98
114
|
end
|
99
115
|
|
116
|
+
# (see AttrStruct.attribute_keys)
|
117
|
+
def attribute_keys
|
118
|
+
self.class.attribute_keys
|
119
|
+
end
|
120
|
+
|
100
121
|
include FingerprintHash
|
101
122
|
def jsi_fingerprint
|
102
123
|
{class: self.class, attributes: @attributes}
|
@@ -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
|
@@ -1,72 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module JSI
|
4
|
-
#
|
4
|
+
# @deprecated after v0.6
|
5
5
|
module Typelike
|
6
|
-
|
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.
|
9
|
-
#
|
10
|
-
# the given block must result in a modified copy of its block parameter
|
11
|
-
# (not destructively modifying the yielded content).
|
12
|
-
#
|
13
|
-
# @yield [Object] the content of the given object. the block should result
|
14
|
-
# in a (nondestructively) modified copy of this.
|
15
|
-
# @return [object.class] modified copy of the given object
|
16
|
-
def self.modified_copy(object, &block)
|
17
|
-
if object.respond_to?(:jsi_modified_copy)
|
18
|
-
object.jsi_modified_copy(&block)
|
19
|
-
else
|
20
|
-
return yield(object)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# recursive method to express the given argument object in json-compatible
|
25
|
-
# types of Hash, Array, and basic types of String/boolean/numeric/nil. this
|
26
|
-
# will raise TypeError if an object is given that is not a type that seems
|
27
|
-
# to be expressable as json.
|
28
|
-
#
|
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
|
31
|
-
# not naturally expressable in JSON, and prefer not to load its
|
32
|
-
# monkey-patching.
|
33
|
-
#
|
34
|
-
# @param object [Object] the object to be converted to jsonifiability
|
35
|
-
# @return [Array, Hash, String, Boolean, NilClass, Numeric] jsonifiable
|
36
|
-
# expression of param object
|
37
|
-
# @raise [TypeError] when the object (or an object nested with a hash or
|
38
|
-
# array of object) cannot be expressed as json
|
39
|
-
def self.as_json(object, *opt)
|
40
|
-
if object.is_a?(JSI::PathedNode)
|
41
|
-
as_json(object.jsi_node_content, *opt)
|
42
|
-
elsif object.respond_to?(:to_hash)
|
43
|
-
(object.respond_to?(:map) ? object : object.to_hash).map do |k, v|
|
44
|
-
unless k.is_a?(Symbol) || k.respond_to?(:to_str)
|
45
|
-
raise(TypeError, "json object (hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
|
46
|
-
end
|
47
|
-
{k.to_s => as_json(v, *opt)}
|
48
|
-
end.inject({}, &:update)
|
49
|
-
elsif object.respond_to?(:to_ary)
|
50
|
-
(object.respond_to?(:map) ? object : object.to_ary).map { |e| as_json(e, *opt) }
|
51
|
-
elsif [String, TrueClass, FalseClass, NilClass, Numeric].any? { |c| object.is_a?(c) }
|
52
|
-
object
|
53
|
-
elsif object.is_a?(Symbol)
|
54
|
-
object.to_s
|
55
|
-
elsif object.is_a?(Set)
|
56
|
-
as_json(object.to_a, *opt)
|
57
|
-
elsif object.respond_to?(:as_json)
|
58
|
-
as_json(object.as_json(*opt), *opt)
|
59
|
-
else
|
60
|
-
raise(TypeError, "cannot express object as json: #{object.pretty_inspect.chomp}")
|
61
|
-
end
|
62
|
-
end
|
6
|
+
extend Util
|
63
7
|
end
|
64
8
|
|
65
9
|
# a module of methods for objects which behave like Hash but are not Hash.
|
66
10
|
#
|
67
11
|
# this module is intended to be internal to JSI. no guarantees or API promises
|
68
12
|
# are made for non-JSI classes including this module.
|
69
|
-
module Hashlike
|
13
|
+
module Util::Hashlike
|
70
14
|
# safe methods which can be delegated to #to_hash (which the includer is assumed to have defined).
|
71
15
|
# 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
|
72
16
|
|
@@ -79,24 +23,41 @@ module JSI
|
|
79
23
|
# select and reject will return a modified copy but need the yielded block variable value from #[]
|
80
24
|
safe_kv_block_modified_copy_methods = %w(select reject)
|
81
25
|
SAFE_METHODS = SAFE_KEY_ONLY_METHODS | SAFE_KEY_VALUE_METHODS
|
82
|
-
|
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
|
83
31
|
safe_to_hash_methods.each do |method_name|
|
84
|
-
|
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
|
85
37
|
end
|
86
38
|
safe_modified_copy_methods.each do |method_name|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
91
52
|
end
|
92
53
|
end
|
93
54
|
end
|
94
55
|
safe_kv_block_modified_copy_methods.each do |method_name|
|
95
|
-
define_method(method_name) do
|
56
|
+
define_method(method_name) do |**kw, &b|
|
96
57
|
jsi_modified_copy do |object_to_modify|
|
97
58
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
|
98
59
|
responsive_object.public_send(method_name) do |k, _v|
|
99
|
-
b.call(k, self[k,
|
60
|
+
b.call(k, self[k, **kw])
|
100
61
|
end
|
101
62
|
end
|
102
63
|
end
|
@@ -112,7 +73,7 @@ module JSI
|
|
112
73
|
unless other.respond_to?(:to_hash)
|
113
74
|
raise(TypeError, "cannot update with argument that does not respond to #to_hash: #{other.pretty_inspect.chomp}")
|
114
75
|
end
|
115
|
-
self_respondingto_key =
|
76
|
+
self_respondingto_key = respond_to?(:key?) ? self : to_hash
|
116
77
|
other.to_hash.each_pair do |key, value|
|
117
78
|
if block && self_respondingto_key.key?(key)
|
118
79
|
value = yield(key, self[key], value)
|
@@ -138,8 +99,8 @@ module JSI
|
|
138
99
|
# self's #jsi_object_group_text
|
139
100
|
# @return [String]
|
140
101
|
def inspect
|
141
|
-
object_group_str = (respond_to?(:jsi_object_group_text) ?
|
142
|
-
"\#{<#{object_group_str}>#{
|
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(',')}}"
|
143
104
|
end
|
144
105
|
|
145
106
|
alias_method :to_s, :inspect
|
@@ -147,7 +108,7 @@ module JSI
|
|
147
108
|
# pretty-prints a representation of this hashlike to the given printer
|
148
109
|
# @return [void]
|
149
110
|
def pretty_print(q)
|
150
|
-
object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
|
111
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
151
112
|
q.text "\#{<#{object_group_str}>"
|
152
113
|
q.group_sub {
|
153
114
|
q.nest(2) {
|
@@ -170,7 +131,7 @@ module JSI
|
|
170
131
|
#
|
171
132
|
# this module is intended to be internal to JSI. no guarantees or API promises
|
172
133
|
# are made for non-JSI classes including this module.
|
173
|
-
module Arraylike
|
134
|
+
module Util::Arraylike
|
174
135
|
# safe methods which can be delegated to #to_ary (which the includer is assumed to have defined).
|
175
136
|
# 'safe' means, in this context, nondestructive - methods which do not modify the receiver.
|
176
137
|
|
@@ -189,23 +150,36 @@ module JSI
|
|
189
150
|
SAFE_METHODS = SAFE_INDEX_ONLY_METHODS | SAFE_INDEX_ELEMENT_METHODS
|
190
151
|
safe_to_ary_methods = SAFE_METHODS - safe_modified_copy_methods - safe_el_block_methods
|
191
152
|
safe_to_ary_methods.each do |method_name|
|
192
|
-
|
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
|
193
158
|
end
|
194
159
|
safe_modified_copy_methods.each do |method_name|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
199
173
|
end
|
200
174
|
end
|
201
175
|
end
|
202
176
|
safe_el_block_methods.each do |method_name|
|
203
|
-
define_method(method_name) do
|
177
|
+
define_method(method_name) do |**kw, &b|
|
204
178
|
jsi_modified_copy do |object_to_modify|
|
205
179
|
i = 0
|
206
180
|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
|
207
181
|
responsive_object.public_send(method_name) do |_e|
|
208
|
-
b.call(self[i,
|
182
|
+
b.call(self[i, **kw]).tap { i += 1 }
|
209
183
|
end
|
210
184
|
end
|
211
185
|
end
|
@@ -229,8 +203,8 @@ module JSI
|
|
229
203
|
# self's #jsi_object_group_text
|
230
204
|
# @return [String]
|
231
205
|
def inspect
|
232
|
-
object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
|
233
|
-
"\#[<#{object_group_str}>#{
|
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(',')}]"
|
234
208
|
end
|
235
209
|
|
236
210
|
alias_method :to_s, :inspect
|
@@ -238,7 +212,7 @@ module JSI
|
|
238
212
|
# pretty-prints a representation of this arraylike to the given printer
|
239
213
|
# @return [void]
|
240
214
|
def pretty_print(q)
|
241
|
-
object_group_str = (respond_to?(:jsi_object_group_text) ? jsi_object_group_text : [self.class]).join(' ')
|
215
|
+
object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
|
242
216
|
q.text "\#[<#{object_group_str}>"
|
243
217
|
q.group_sub {
|
244
218
|
q.nest(2) {
|