jsi 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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) {