jsi 0.7.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +29 -20
  5. data/jsi.gemspec +2 -3
  6. data/lib/jsi/base/mutability.rb +44 -0
  7. data/lib/jsi/base/node.rb +199 -34
  8. data/lib/jsi/base.rb +412 -228
  9. data/lib/jsi/jsi_coder.rb +18 -16
  10. data/lib/jsi/metaschema_node/bootstrap_schema.rb +57 -23
  11. data/lib/jsi/metaschema_node.rb +138 -107
  12. data/lib/jsi/ptr.rb +59 -37
  13. data/lib/jsi/schema/application/child_application/draft04.rb +0 -1
  14. data/lib/jsi/schema/application/child_application/draft06.rb +0 -1
  15. data/lib/jsi/schema/application/child_application/draft07.rb +0 -1
  16. data/lib/jsi/schema/application/child_application.rb +0 -25
  17. data/lib/jsi/schema/application/inplace_application/draft04.rb +0 -1
  18. data/lib/jsi/schema/application/inplace_application/draft06.rb +0 -1
  19. data/lib/jsi/schema/application/inplace_application/draft07.rb +0 -1
  20. data/lib/jsi/schema/application/inplace_application/ref.rb +1 -1
  21. data/lib/jsi/schema/application/inplace_application/someof.rb +1 -1
  22. data/lib/jsi/schema/application/inplace_application.rb +0 -27
  23. data/lib/jsi/schema/draft04.rb +0 -1
  24. data/lib/jsi/schema/draft06.rb +0 -1
  25. data/lib/jsi/schema/draft07.rb +0 -1
  26. data/lib/jsi/schema/ref.rb +44 -18
  27. data/lib/jsi/schema/schema_ancestor_node.rb +65 -56
  28. data/lib/jsi/schema/validation/contains.rb +1 -1
  29. data/lib/jsi/schema/validation/draft04/minmax.rb +2 -0
  30. data/lib/jsi/schema/validation/draft04.rb +0 -2
  31. data/lib/jsi/schema/validation/draft06.rb +0 -2
  32. data/lib/jsi/schema/validation/draft07.rb +0 -2
  33. data/lib/jsi/schema/validation/items.rb +3 -3
  34. data/lib/jsi/schema/validation/pattern.rb +1 -1
  35. data/lib/jsi/schema/validation/properties.rb +4 -4
  36. data/lib/jsi/schema/validation/ref.rb +1 -1
  37. data/lib/jsi/schema/validation.rb +0 -2
  38. data/lib/jsi/schema.rb +408 -198
  39. data/lib/jsi/schema_classes.rb +196 -127
  40. data/lib/jsi/schema_registry.rb +66 -17
  41. data/lib/jsi/schema_set.rb +76 -30
  42. data/lib/jsi/simple_wrap.rb +2 -7
  43. data/lib/jsi/util/private/attr_struct.rb +28 -14
  44. data/lib/jsi/util/private/memo_map.rb +75 -0
  45. data/lib/jsi/util/private.rb +73 -92
  46. data/lib/jsi/util/typelike.rb +28 -28
  47. data/lib/jsi/util.rb +120 -36
  48. data/lib/jsi/validation/error.rb +4 -0
  49. data/lib/jsi/validation/result.rb +18 -32
  50. data/lib/jsi/version.rb +1 -1
  51. data/lib/jsi.rb +67 -25
  52. data/lib/schemas/json-schema.org/draft-04/schema.rb +159 -4
  53. data/lib/schemas/json-schema.org/draft-06/schema.rb +161 -4
  54. data/lib/schemas/json-schema.org/draft-07/schema.rb +188 -4
  55. data/readme.rb +1 -1
  56. metadata +19 -5
  57. data/lib/jsi/metaschema.rb +0 -6
  58. data/lib/jsi/schema/validation/core.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0659cc34b129717ffb69089f1d3a25ba3c3ec5364d49a6c0cc93dd5979ac7638'
4
- data.tar.gz: 5b0298a9843a61e6f5693d46dd6d86021d53767638e1f3f84e23fff4b167276e
3
+ metadata.gz: daf35895a6186a3d3123b037f917912ff65ea03c876da9dd5cb40e2a9b079641
4
+ data.tar.gz: b14524e9eb8796385fdd2ec96bcbb5129c2e80ad9eabb39d60637b9c6078d1c8
5
5
  SHA512:
6
- metadata.gz: 8b68dcbb479756b8fa58a43f7a8e83accda02a8b0aa87617dabb6623a4b1a8aa05f94cb03de601cf7bf88c943ce62e42963b533ef125729847159e3b775f32c2
7
- data.tar.gz: 49997a70900b9d250e4888bcf3e5470e68b116c3a558651db9e5551f73027ea4abd18b0585d23f5201b15e8d80f1d72b46488d1870ac530d40368c7577d3b048
6
+ metadata.gz: 005c27210c96f9b613f7f6959e86e17b41380c5366fe14f69cd047498fb5bfc8256b8404aa6c5f2ced04882b94edd1c9b62ff79c48001b7388b4b4faadf6c5b1
7
+ data.tar.gz: 9eccdec2038200c340df2365a3b179da83fd900deb8b4f8f21aa5e4430f785d80afa929e8d718e80850d463fd5d2cd0e0982f7b99352b9e4109cf7c781e07c74
data/.yardopts CHANGED
@@ -1 +1,6 @@
1
- --main README.md --markup=markdown --no-private {lib}/**/*.rb
1
+ --main README.md
2
+ --markup=markdown
3
+ --markup-provider=commonmarker
4
+ --no-private
5
+ --hide-void-return
6
+ {lib}/**/*.rb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # v0.8.1
2
+
3
+ - JSIs are immutable by default
4
+
5
+ # v0.8.0
6
+
7
+ - Immutable JSIs with new_jsi param `mutable`
8
+ - JSIs are still mutable by default, but in the next release they will default to immutable
9
+ - Base#jsi_indicated_schemas
10
+ - Base::StringNode
11
+ - rename metaschema modules /JSONSchemaOrgDraft0X/JSONSchemaDraft0X/
12
+ - terminology: /Metaschema/Meta-Schema/ and /metaschema/meta-schema/ (where hyphen is allowed)
13
+ - Base::HashNode#jsi_each_propertyName
14
+ - new_schema and/or new_jsi params register, schema_registry, stringify_symbol_keys, to_immutable
15
+ - new_schema block param will module_exec on schema module
16
+ - Base#[] param use_default default false, overridable
17
+ - SchemaModule::Connects, SchemaModule::Connection
18
+ - rm Schema#jsi_schema_instance_modules
19
+
1
20
  # v0.7.0
2
21
 
3
22
  - JSI::Base instances include Array/Hash-like modules on subclasses rather than extending each instance; are only Enumerable when appropriate instead of always
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  JSI offers an Object-Oriented representation for JSON data using JSON Schemas. Given your JSON Schemas, JSI constructs Ruby modules and classes which are used to instantiate your JSON data. These modules let you use JSON with all the niceties of OOP such as property accessors and application-defined instance methods.
7
7
 
8
- To learn more about JSON Schema see [https://json-schema.org/](https://json-schema.org/).
8
+ To learn more about JSON Schema see <https://json-schema.org/>.
9
9
 
10
10
  JSI marries object-oriented programming with JSON Schemas by associating a module with each schema, and extending every instance described by a schema with that module. When an application adds methods to a schema module, those methods can be used on its instances.
11
11
 
@@ -15,7 +15,9 @@ Note: The canonical location of this README is on [RubyDoc](http://rubydoc.info/
15
15
 
16
16
  ## Example
17
17
 
18
- Words are boring, let's code. Here's a schema in yaml:
18
+ Words are boring, let's code. You can follow along from the code blocks - install the gem (`gem install jsi`), load an irb (`irb -r jsi`), and copy/paste/hack.
19
+
20
+ Here's a schema in yaml:
19
21
 
20
22
  ```yaml
21
23
  $schema: "http://json-schema.org/draft-07/schema"
@@ -61,8 +63,12 @@ nickname: big b
61
63
  So, if we construct an instance like:
62
64
 
63
65
  ```ruby
64
- # this would usually load YAML or JSON; the schema instance is inlined for copypastability.
65
- bill = Contact.new_jsi({"name" => "bill", "phone" => [{"location" => "home", "number" => "555"}], "nickname" => "big b"})
66
+ bill = Contact.new_jsi(
67
+ # this would typically load JSON or YAML; the schema instance is inlined for copypastability.
68
+ {"name" => "bill", "phone" => [{"location" => "home", "number" => "555"}], "nickname" => "big b"},
69
+ # note: bill is mutable to demonstrate setters below; the default is immutable.
70
+ mutable: true
71
+ )
66
72
  # => #{<JSI (Contact)>
67
73
  # "name" => "bill",
68
74
  # "phone" => #[<JSI (Contact.properties["phone"])>
@@ -91,7 +97,7 @@ bill.phone.map(&:location)
91
97
  # => ["home"]
92
98
  ```
93
99
 
94
- We also get validations, as you'd expect given that's largely what json-schema exists to do:
100
+ We also get validations, as you'd expect given that's largely what JSON Schema exists to do:
95
101
 
96
102
  ```ruby
97
103
  bill.jsi_valid?
@@ -115,7 +121,7 @@ bad.phone.jsi_validate
115
121
  # #<Set: {#<JSI::Validation::Error
116
122
  # message: "instance type does not match `type` value",
117
123
  # keyword: "type",
118
- # schema: #{<JSI (JSI::JSONSchemaOrgDraft07) Schema> "type" => "string"},
124
+ # schema: #{<JSI (JSI::JSONSchemaDraft07) Schema> "type" => "string"},
119
125
  # instance_ptr: JSI::Ptr["phone", 0, "number"],
120
126
  # instance_document: {"phone"=>[{"number"=>[5, 5, 5]}]}
121
127
  # >,
@@ -140,7 +146,7 @@ There's plenty more JSI has to offer, but this should give you a pretty good ide
140
146
  ## Terminology and Concepts
141
147
 
142
148
  - `JSI::Base` is the base class for each JSI schema class representing instances of JSON Schemas.
143
- - a "JSI Schema" is a JSON Schema, instantiated as (usually) a JSI::Base described by a metaschema (see the sections on Metaschemas below). a JSI Schema is an instance of the module `JSI::Schema`.
149
+ - a "JSI Schema" is a JSON Schema, instantiated as (usually) a JSI::Base described by a meta-schema (see the section on meta-schemas below). A JSI Schema is an instance of the module `JSI::Schema`.
144
150
  - a "JSI Schema Module" is a module which represents one schema, dynamically created by that Schema. Instances of that schema are extended with its JSI schema module. applications may reopen these modules to add functionality to JSI instances described by a given schema.
145
151
  - a "JSI schema class" is a subclass of `JSI::Base` representing one or more JSON schemas. Instances of such a class are described by all of the represented schemas. A JSI schema class includes the JSI schema module of each represented schema.
146
152
  - "instance" is a term that is significantly overloaded in this space, so documentation will attempt to be clear what kind of instance is meant:
@@ -154,9 +160,9 @@ JSI supports these JSON Schema specification versions:
154
160
 
155
161
  | Version | `$schema` URI | JSI Schema Module |
156
162
  | --- | --- | --- |
157
- | Draft 4 | `http://json-schema.org/draft-04/schema#` | {JSI::JSONSchemaOrgDraft04} |
158
- | Draft 6 | `http://json-schema.org/draft-06/schema#` | {JSI::JSONSchemaOrgDraft06} |
159
- | Draft 7 | `http://json-schema.org/draft-07/schema#` | {JSI::JSONSchemaOrgDraft07} |
163
+ | Draft 4 | `http://json-schema.org/draft-04/schema#` | {JSI::JSONSchemaDraft04} |
164
+ | Draft 6 | `http://json-schema.org/draft-06/schema#` | {JSI::JSONSchemaDraft06} |
165
+ | Draft 7 | `http://json-schema.org/draft-07/schema#` | {JSI::JSONSchemaDraft07} |
160
166
 
161
167
  ## JSI and Object Oriented Programming
162
168
 
@@ -190,7 +196,7 @@ bill['name']
190
196
 
191
197
  For `#name` and `#name=`, we're overriding existing accessor methods. note the use of `super` - this invokes the accessor methods defined by JSI which these override. You could alternatively use `self['name']` and `self['name']=` in these methods, with the same effect as `super`.
192
198
 
193
- Working with subschemas is just about as easy as with root schemas.
199
+ Working with subschemas to add methods is just about as easy as with root schemas.
194
200
 
195
201
  You can subscript or use property accessors on a JSI schema module to refer to the schema modules of its subschemas, e.g.:
196
202
 
@@ -211,8 +217,7 @@ bill.phone.first.number_with_dashes
211
217
  # => "5-5-5"
212
218
  ```
213
219
 
214
- A recommended convention for naming subschemas is to define them in the namespace of the module of their
215
- parent schema. The module can then be opened to add methods to the subschema's module.
220
+ A recommended convention for naming subschemas is to define them in the namespace of the module of their parent schema. The module can then be opened to add methods to the subschema's module.
216
221
 
217
222
  ```ruby
218
223
  module Contact
@@ -240,11 +245,15 @@ end
240
245
 
241
246
  The classes used to instantiate JSIs are dynamically generated subclasses of JSI::Base which include the JSI Schema Module of each schema describing the given instance. These are mostly intended to be ignored: applications aren't expected to instantiate these directly (rather, `#new_jsi` on a Schema or Schema Module is intended), and they are not intended for subclassing or method definition (applications should instead define methods on a schema's {JSI::Schema#jsi_schema_module}).
242
247
 
248
+ ## Mutability
249
+
250
+ JSI instances are immutable by default. Mutable JSIs may be instantiated using the `mutable` param of `new_jsi`. Immutable JSIs are much more performant, because mutation may change what schemas apply to nodes in a document, and checking for that is costly. It is not recommended to instantiate large documents as mutable; their JSI instances become unusably slow.
251
+
243
252
  ## Registration
244
253
 
245
- In order for references across documents (generally from a `$ref` schema keyword) to resolve, JSI provides a registry which associates URIs with schemas (or resources containing schemas). This registry is accessible on {JSI.schema_registry} and is a {JSI::SchemaRegistry}.
254
+ In order for references across documents (generally from a `$ref` schema keyword) to resolve, JSI provides a registry (a {JSI::SchemaRegistry}) which associates URIs with schemas (or resources containing schemas). The default registry is accessible on {JSI.schema_registry}.
246
255
 
247
- Schemas instantiated with `.new_schema`, and their subschemas, are automatically registered with `JSI.schema_registry` if they identify an absolute URI.
256
+ Schemas instantiated with `.new_schema`, and their subschemas, are by default registered with `JSI.schema_registry` if they are identified by an absolute URI. This can be controlled by params `register` and `schema_registry`.
248
257
 
249
258
  Schemas can automatically be lazily loaded by registering a block which instantiates them with {JSI::SchemaRegistry#autoload_uri} (see its documentation).
250
259
 
@@ -258,13 +267,13 @@ The following optional features are not completely supported:
258
267
  - Regular expressions are interpreted by Ruby's Regexp class, whereas JSON Schema recommends interpreting these as ECMA 262 regular expressions. Certain expressions behave differently, particularly `^` and `$`.
259
268
  - Keywords `contentMediaType` and `contentEncoding` do not perform validation.
260
269
 
261
- ## Metaschemas
270
+ ## Meta-Schemas
262
271
 
263
- A metaschema is a schema which describes schemas. Likewise, a schema is an instance of a metaschema.
272
+ A meta-schema is a schema that describes schemas. Likewise, a schema is an instance of a meta-schema.
264
273
 
265
- In JSI, a schema is generally a JSI::Base instance whose schemas include a metaschema.
274
+ In JSI, a schema is generally a JSI::Base instance whose schemas include a meta-schema.
266
275
 
267
- A self-descriptive metaschema - most commonly one of the JSON schema draft metaschemas - is an object whose schemas include itself. This is instantiated in JSI as a JSI::MetaschemaNode, a special subclass of JSI::Base.
276
+ A self-descriptive meta-schema - most commonly one of the JSON schema draft meta-schemas - is an object whose schemas include itself. This is instantiated in JSI as a JSI::MetaSchemaNode, a special subclass of JSI::Base.
268
277
 
269
278
  ## ActiveRecord serialization
270
279
 
@@ -282,7 +291,7 @@ class User < ActiveRecord::Base
282
291
  end
283
292
  ```
284
293
 
285
- Now `user.contact_info` will be instantiated as a `Contact` JSI instance, from the JSON type in the database, with Contact's accessors, validations, and user-defined instance methods.
294
+ Now `user.contact_info` will be instantiated as a `Contact` JSI instance, from the JSON type in the database, with Contact's accessors, validations, and application-defined instance methods.
286
295
 
287
296
  See the gem [`arms`](https://github.com/notEthan/arms) if you wish to serialize the dumped JSON-compatible objects further as text.
288
297
 
data/jsi.gemspec CHANGED
@@ -1,6 +1,4 @@
1
- lib = File.expand_path("lib", __dir__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require "jsi/version"
1
+ require_relative "lib/jsi/version"
4
2
 
5
3
  Gem::Specification.new do |spec|
6
4
  spec.name = "jsi"
@@ -27,4 +25,5 @@ Gem::Specification.new do |spec|
27
25
  spec.require_paths = ["lib"]
28
26
 
29
27
  spec.add_dependency "addressable", '~> 2.3'
28
+ spec.add_dependency "bigdecimal"
30
29
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Base::Mutable
5
+ include(Util::FingerprintHash)
6
+
7
+ def jsi_node_content
8
+ jsi_ptr.evaluate(jsi_document)
9
+ end
10
+
11
+ def jsi_mutable?
12
+ true
13
+ end
14
+
15
+ private
16
+
17
+ def jsi_mutability_initialize
18
+ end
19
+
20
+ def jsi_memomap_class
21
+ Util::MemoMap::Mutable
22
+ end
23
+ end
24
+
25
+ module Base::Immutable
26
+ include(Util::FingerprintHash::Immutable)
27
+
28
+ attr_reader(:jsi_node_content)
29
+
30
+ def jsi_mutable?
31
+ false
32
+ end
33
+
34
+ private
35
+
36
+ def jsi_mutability_initialize
37
+ @jsi_node_content = @jsi_ptr.evaluate(@jsi_document)
38
+ end
39
+
40
+ def jsi_memomap_class
41
+ Util::MemoMap::Immutable
42
+ end
43
+ end
44
+ end
data/lib/jsi/base/node.rb CHANGED
@@ -1,47 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSI
4
- module Base::Enumerable
5
- include ::Enumerable
6
-
7
- # an Array containing each item in this JSI.
4
+ # module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content})
5
+ # is a Hash (or responds to `#to_hash`)
6
+ module Base::HashNode
7
+ # instantiates and yields each property name (hash key) as a JSI described by any `propertyNames` schemas.
8
8
  #
9
- # @param kw keyword arguments are passed to {Base#[]} - see its keyword params
10
- # @return [Array]
11
- def to_a(**kw)
12
- # TODO remove eventually (keyword argument compatibility)
13
- # discard when all supported ruby versions Enumerable#to_a delegate keywords to #each (3.0.1 breaks; 2.7.x warns)
14
- # https://bugs.ruby-lang.org/issues/18289
15
- ary = []
16
- each(**kw) do |e|
17
- ary << e
9
+ # @yield [JSI::Base]
10
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
11
+ def jsi_each_propertyName
12
+ return to_enum(__method__) { jsi_node_content_hash_pubsend(:size) } unless block_given?
13
+
14
+ property_schemas = SchemaSet.build do |schemas|
15
+ jsi_schemas.each do |s|
16
+ if s.keyword?('propertyNames') && s['propertyNames'].is_a?(Schema)
17
+ schemas << s['propertyNames']
18
+ end
19
+ end
20
+ end
21
+ jsi_node_content_hash_pubsend(:each_key) do |key|
22
+ yield property_schemas.new_jsi(key)
18
23
  end
19
- ary
24
+
25
+ nil
20
26
  end
21
27
 
22
- alias_method :entries, :to_a
28
+ # See {Base#jsi_hash?}. Always true for HashNode.
29
+ def jsi_hash?
30
+ true
31
+ end
23
32
 
24
- # a jsonifiable representation of the node content
25
- # @return [Object]
26
- def as_json(*opt)
27
- # include Enumerable (above) means, if ActiveSupport is loaded, its undesirable #as_json is included
28
- # https://github.com/rails/rails/blob/v7.0.0/activesupport/lib/active_support/core_ext/object/json.rb#L139-L143
29
- # although Base#as_json does clobber activesupport's, I want as_json defined correctly on the module too.
30
- Typelike.as_json(jsi_node_content, *opt)
33
+ # Yields each key - see {Base#jsi_each_child_token}
34
+ def jsi_each_child_token(&block)
35
+ return to_enum(__method__) { jsi_node_content_hash_pubsend(:size) } unless block
36
+ jsi_node_content_hash_pubsend(:each_key, &block)
37
+ nil
31
38
  end
32
- end
33
39
 
34
- # module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content})
35
- # is a Hash (or responds to `#to_hash`)
36
- module Base::HashNode
37
- include Base::Enumerable
40
+ # See {Base#jsi_child_token_in_range?}
41
+ def jsi_child_token_in_range?(token)
42
+ jsi_node_content_hash_pubsend(:key?, token)
43
+ end
44
+
45
+ # See {Base#jsi_node_content_child}
46
+ def jsi_node_content_child(token)
47
+ # I could check token_in_range? and return nil here (as ArrayNode does).
48
+ # without that check, if the instance defines Hash#default or #default_proc, that result is returned.
49
+ # the preferred mechanism for a JSI's default value should be its schema.
50
+ # but there's no compelling reason not to support both, so I'll return what #[] returns.
51
+ jsi_node_content_hash_pubsend(:[], token)
52
+ end
53
+
54
+ # See {Base#[]}
55
+ def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
56
+ if jsi_node_content_hash_pubsend(:key?, token)
57
+ jsi_child(token, as_jsi: as_jsi)
58
+ else
59
+ if use_default
60
+ jsi_default_child(token, as_jsi: as_jsi)
61
+ else
62
+ nil
63
+ end
64
+ end
65
+ end
38
66
 
39
- # yields each hash key and value of this node.
67
+ # yields each Hash key (JSON object property name) and value of this node.
40
68
  #
41
69
  # each yielded key is a key of the instance hash, and each yielded value is the result of {Base#[]}.
42
70
  #
43
- # returns an Enumerator if no block is given.
44
- #
45
71
  # @param kw keyword arguments are passed to {Base#[]}
46
72
  # @yield [Object, Object] each key and value of this hash node
47
73
  # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
@@ -59,7 +85,22 @@ module JSI
59
85
  # @param kw keyword arguments are passed to {Base#[]}
60
86
  # @return [Hash]
61
87
  def to_hash(**kw)
62
- {}.tap { |h| jsi_node_content_hash_pubsend(:each_key) { |k| h[k] = self[k, **kw] } }
88
+ hash = {}
89
+ jsi_node_content_hash_pubsend(:each_key) { |k| hash[k] = self[k, **kw] }
90
+ hash.freeze
91
+ end
92
+
93
+ # See {Base#as_json}
94
+ def as_json(options = {})
95
+ hash = {}
96
+ each_key do |k|
97
+ ks = k.is_a?(String) ? k :
98
+ k.is_a?(Symbol) ? k.to_s :
99
+ k.respond_to?(:to_str) && (kstr = k.to_str).is_a?(String) ? kstr :
100
+ raise(TypeError, "JSON object (Hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
101
+ hash[ks] = jsi_child_node(k).as_json(**options)
102
+ end
103
+ hash
63
104
  end
64
105
 
65
106
  include Util::Hashlike
@@ -110,14 +151,99 @@ module JSI
110
151
  # module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content})
111
152
  # is an Array (or responds to `#to_ary`)
112
153
  module Base::ArrayNode
113
- include Base::Enumerable
154
+ # See {Base#jsi_array?}. Always true for ArrayNode.
155
+ def jsi_array?
156
+ true
157
+ end
158
+
159
+ # Yields each index - see {Base#jsi_each_child_token}
160
+ def jsi_each_child_token(&block)
161
+ return to_enum(__method__) { jsi_node_content_ary_pubsend(:size) } unless block
162
+ jsi_node_content_ary_pubsend(:each_index, &block)
163
+ nil
164
+ end
165
+
166
+ # See {Base#jsi_child_token_in_range?}
167
+ def jsi_child_token_in_range?(token)
168
+ token.is_a?(Integer) && token >= 0 && token < jsi_node_content_ary_pubsend(:size)
169
+ end
170
+
171
+ # See {Base#jsi_node_content_child}
172
+ def jsi_node_content_child(token)
173
+ # we check token_in_range? here (unlike HashNode) because we do not want to pass
174
+ # negative indices, Ranges, or non-Integers to Array#[]
175
+ if jsi_child_token_in_range?(token)
176
+ jsi_node_content_ary_pubsend(:[], token)
177
+ else
178
+ nil
179
+ end
180
+ end
181
+
182
+ # See {Base#[]}
183
+ def [](token, as_jsi: jsi_child_as_jsi_default, use_default: jsi_child_use_default_default)
184
+ size = jsi_node_content_ary_pubsend(:size)
185
+ if token.is_a?(Integer)
186
+ if token < 0
187
+ if token < -size
188
+ nil
189
+ else
190
+ jsi_child(token + size, as_jsi: as_jsi)
191
+ end
192
+ else
193
+ if token < size
194
+ jsi_child(token, as_jsi: as_jsi)
195
+ else
196
+ if use_default
197
+ jsi_default_child(token, as_jsi: as_jsi)
198
+ else
199
+ nil
200
+ end
201
+ end
202
+ end
203
+ elsif token.is_a?(Range)
204
+ type_err = proc do
205
+ raise(TypeError, [
206
+ "given range does not contain Integers",
207
+ "range: #{token.inspect}",
208
+ ].join("\n"))
209
+ end
210
+
211
+ start_idx = token.begin
212
+ if start_idx.is_a?(Integer)
213
+ start_idx += size if start_idx < 0
214
+ return Util::EMPTY_ARY if start_idx == size
215
+ return nil if start_idx < 0 || start_idx > size
216
+ elsif start_idx.nil?
217
+ start_idx = 0
218
+ else
219
+ type_err.call
220
+ end
221
+
222
+ end_idx = token.end
223
+ if end_idx.is_a?(Integer)
224
+ end_idx += size if end_idx < 0
225
+ end_idx += 1 unless token.exclude_end?
226
+ end_idx = size if end_idx > size
227
+ return Util::EMPTY_ARY if start_idx >= end_idx
228
+ elsif end_idx.nil?
229
+ end_idx = size
230
+ else
231
+ type_err.call
232
+ end
233
+
234
+ (start_idx...end_idx).map { |i| jsi_child(i, as_jsi: as_jsi) }.freeze
235
+ else
236
+ raise(TypeError, [
237
+ "expected `token` param to be an Integer or Range",
238
+ "token: #{token.inspect}",
239
+ ].join("\n"))
240
+ end
241
+ end
114
242
 
115
243
  # yields each array element of this node.
116
244
  #
117
245
  # each yielded element is the result of {Base#[]} for each index of the instance array.
118
246
  #
119
- # returns an Enumerator if no block is given.
120
- #
121
247
  # @param kw keyword arguments are passed to {Base#[]}
122
248
  # @yield [Object] each element of this array node
123
249
  # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
@@ -135,6 +261,11 @@ module JSI
135
261
  to_a(**kw)
136
262
  end
137
263
 
264
+ # See {Base#as_json}
265
+ def as_json(options = {})
266
+ each_index.map { |i| jsi_child_node(i).as_json(**options) }
267
+ end
268
+
138
269
  include Util::Arraylike
139
270
 
140
271
  if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
@@ -180,4 +311,38 @@ module JSI
180
311
  end
181
312
  end
182
313
  end
314
+
315
+ module Base::StringNode
316
+ delegate_methods = %w(% * + << =~ [] []=
317
+ ascii_only? b byteindex byterindex bytes bytesize byteslice bytesplice capitalize capitalize!
318
+ casecmp casecmp? center chars chomp chomp! chop chop! chr clear codepoints concat count delete delete!
319
+ delete_prefix delete_prefix! delete_suffix delete_suffix! downcase downcase!
320
+ each_byte each_char each_codepoint each_grapheme_cluster each_line
321
+ empty? encode encode! encoding end_with? force_encoding getbyte grapheme_clusters gsub gsub! hex
322
+ include? index insert intern length lines ljust lstrip lstrip! match match? next next! oct ord
323
+ partition prepend replace reverse reverse! rindex rjust rpartition rstrip rstrip! scan scrub scrub!
324
+ setbyte size slice slice! split squeeze squeeze! start_with? strip strip! sub sub! succ succ! sum
325
+ swapcase swapcase! to_c to_f to_i to_r to_s to_str to_sym tr tr! tr_s tr_s!
326
+ unicode_normalize unicode_normalize! unicode_normalized? unpack unpack1 upcase upcase! upto valid_encoding?
327
+ )
328
+ delegate_methods.each do |method_name|
329
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
330
+ define_method(method_name) do |*a, &b|
331
+ if jsi_node_content.respond_to?(method_name)
332
+ jsi_node_content.public_send(method_name, *a, &b)
333
+ else
334
+ jsi_node_content.to_str.public_send(method_name, *a, &b)
335
+ end
336
+ end
337
+ else
338
+ define_method(method_name) do |*a, **kw, &b|
339
+ if jsi_node_content.respond_to?(method_name)
340
+ jsi_node_content.public_send(method_name, *a, **kw, &b)
341
+ else
342
+ jsi_node_content.to_str.public_send(method_name, *a, **kw, &b)
343
+ end
344
+ end
345
+ end
346
+ end
347
+ end
183
348
  end