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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +11 -6
  5. data/jsi.gemspec +30 -0
  6. data/lib/jsi/base/node.rb +183 -0
  7. data/lib/jsi/base.rb +135 -161
  8. data/lib/jsi/jsi_coder.rb +3 -3
  9. data/lib/jsi/metaschema.rb +0 -1
  10. data/lib/jsi/metaschema_node/bootstrap_schema.rb +9 -8
  11. data/lib/jsi/metaschema_node.rb +48 -51
  12. data/lib/jsi/ptr.rb +28 -17
  13. data/lib/jsi/schema/application/child_application/contains.rb +11 -2
  14. data/lib/jsi/schema/application/child_application/items.rb +3 -3
  15. data/lib/jsi/schema/application/child_application/properties.rb +3 -3
  16. data/lib/jsi/schema/application/child_application.rb +1 -3
  17. data/lib/jsi/schema/application/inplace_application/dependencies.rb +1 -1
  18. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +3 -3
  19. data/lib/jsi/schema/application/inplace_application/ref.rb +1 -1
  20. data/lib/jsi/schema/application/inplace_application/someof.rb +26 -11
  21. data/lib/jsi/schema/application/inplace_application.rb +1 -6
  22. data/lib/jsi/schema/ref.rb +3 -2
  23. data/lib/jsi/schema/schema_ancestor_node.rb +11 -17
  24. data/lib/jsi/schema/validation/array.rb +3 -3
  25. data/lib/jsi/schema/validation/const.rb +1 -1
  26. data/lib/jsi/schema/validation/contains.rb +1 -1
  27. data/lib/jsi/schema/validation/dependencies.rb +1 -1
  28. data/lib/jsi/schema/validation/draft04/minmax.rb +6 -6
  29. data/lib/jsi/schema/validation/enum.rb +1 -1
  30. data/lib/jsi/schema/validation/ifthenelse.rb +5 -5
  31. data/lib/jsi/schema/validation/items.rb +4 -4
  32. data/lib/jsi/schema/validation/not.rb +1 -1
  33. data/lib/jsi/schema/validation/numeric.rb +5 -5
  34. data/lib/jsi/schema/validation/object.rb +2 -2
  35. data/lib/jsi/schema/validation/pattern.rb +1 -1
  36. data/lib/jsi/schema/validation/properties.rb +3 -3
  37. data/lib/jsi/schema/validation/property_names.rb +1 -1
  38. data/lib/jsi/schema/validation/ref.rb +1 -1
  39. data/lib/jsi/schema/validation/required.rb +1 -1
  40. data/lib/jsi/schema/validation/someof.rb +3 -3
  41. data/lib/jsi/schema/validation/string.rb +2 -2
  42. data/lib/jsi/schema/validation/type.rb +1 -1
  43. data/lib/jsi/schema/validation.rb +1 -1
  44. data/lib/jsi/schema.rb +91 -85
  45. data/lib/jsi/schema_classes.rb +70 -45
  46. data/lib/jsi/schema_registry.rb +15 -5
  47. data/lib/jsi/schema_set.rb +42 -2
  48. data/lib/jsi/simple_wrap.rb +23 -4
  49. data/lib/jsi/util/{attr_struct.rb → private/attr_struct.rb} +40 -19
  50. data/lib/jsi/util/private.rb +204 -0
  51. data/lib/jsi/{typelike_modules.rb → util/typelike.rb} +56 -82
  52. data/lib/jsi/util.rb +68 -148
  53. data/lib/jsi/version.rb +1 -1
  54. data/lib/jsi.rb +1 -17
  55. data/lib/schemas/json-schema.org/draft-04/schema.rb +3 -1
  56. data/lib/schemas/json-schema.org/draft-06/schema.rb +3 -1
  57. data/lib/schemas/json-schema.org/draft-07/schema.rb +3 -1
  58. metadata +11 -9
  59. data/lib/jsi/pathed_node.rb +0 -116
@@ -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
- "JSI::SchemaSet initialized with non-schema objects:",
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
- jsi = JSI::SchemaClasses.class_for_schemas(applied_schemas).new(instance,
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 '['
@@ -1,12 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSI
4
- SimpleWrap = JSI::JSONSchemaOrgDraft06.new_schema_module({
5
- "additionalProperties": {"$ref": "#"},
6
- "items": {"$ref": "#"}
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 [](*attribute_keys)
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.is_a?(Symbol) ? key.to_s : 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 = 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) }
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 = key.to_s if key.is_a?(Symbol)
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 = key.to_s if key.is_a?(Symbol)
68
- unless self.attribute_keys.include?(key)
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
- # a module relating to objects that act like Hash or Array instances
4
+ # @deprecated after v0.6
5
5
  module Typelike
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.
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
- safe_to_hash_methods = SAFE_METHODS - safe_modified_copy_methods - safe_kv_block_modified_copy_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
83
31
  safe_to_hash_methods.each do |method_name|
84
- define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
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
- define_method(method_name) do |*a, &b|
88
- jsi_modified_copy do |object_to_modify|
89
- responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
90
- responsive_object.public_send(method_name, *a, &b)
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 |*a, &b|
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, *a])
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 = self.respond_to?(:key?) ? self : to_hash
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) ? self.jsi_object_group_text : [self.class]).join(' ')
142
- "\#{<#{object_group_str}>#{self.map { |k, v| " #{k.inspect} => #{v.inspect}" }.join(',')}}"
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
- define_method(method_name) { |*a, &b| to_ary.public_send(method_name, *a, &b) }
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
- define_method(method_name) do |*a, &b|
196
- jsi_modified_copy do |object_to_modify|
197
- responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
198
- responsive_object.public_send(method_name, *a, &b)
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 |*a, &b|
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, *a]).tap { i += 1 }
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}>#{self.map { |e| ' ' + e.inspect }.join(',')}]"
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) {