jsi 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -1
  3. data/CHANGELOG.md +33 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +29 -23
  6. data/jsi.gemspec +29 -0
  7. data/lib/jsi/base/mutability.rb +44 -0
  8. data/lib/jsi/base/node.rb +348 -0
  9. data/lib/jsi/base.rb +497 -339
  10. data/lib/jsi/jsi_coder.rb +19 -17
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +61 -26
  12. data/lib/jsi/metaschema_node.rb +161 -133
  13. data/lib/jsi/ptr.rb +80 -47
  14. data/lib/jsi/schema/application/child_application/contains.rb +11 -2
  15. data/lib/jsi/schema/application/child_application/draft04.rb +0 -1
  16. data/lib/jsi/schema/application/child_application/draft06.rb +0 -1
  17. data/lib/jsi/schema/application/child_application/draft07.rb +0 -1
  18. data/lib/jsi/schema/application/child_application/items.rb +3 -3
  19. data/lib/jsi/schema/application/child_application/properties.rb +3 -3
  20. data/lib/jsi/schema/application/child_application.rb +0 -27
  21. data/lib/jsi/schema/application/inplace_application/dependencies.rb +1 -1
  22. data/lib/jsi/schema/application/inplace_application/draft04.rb +0 -1
  23. data/lib/jsi/schema/application/inplace_application/draft06.rb +0 -1
  24. data/lib/jsi/schema/application/inplace_application/draft07.rb +0 -1
  25. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +3 -3
  26. data/lib/jsi/schema/application/inplace_application/ref.rb +2 -2
  27. data/lib/jsi/schema/application/inplace_application/someof.rb +26 -11
  28. data/lib/jsi/schema/application/inplace_application.rb +0 -32
  29. data/lib/jsi/schema/draft04.rb +0 -1
  30. data/lib/jsi/schema/draft06.rb +0 -1
  31. data/lib/jsi/schema/draft07.rb +0 -1
  32. data/lib/jsi/schema/ref.rb +46 -19
  33. data/lib/jsi/schema/schema_ancestor_node.rb +69 -66
  34. data/lib/jsi/schema/validation/array.rb +3 -3
  35. data/lib/jsi/schema/validation/const.rb +1 -1
  36. data/lib/jsi/schema/validation/contains.rb +2 -2
  37. data/lib/jsi/schema/validation/dependencies.rb +1 -1
  38. data/lib/jsi/schema/validation/draft04/minmax.rb +8 -6
  39. data/lib/jsi/schema/validation/draft04.rb +0 -2
  40. data/lib/jsi/schema/validation/draft06.rb +0 -2
  41. data/lib/jsi/schema/validation/draft07.rb +0 -2
  42. data/lib/jsi/schema/validation/enum.rb +1 -1
  43. data/lib/jsi/schema/validation/ifthenelse.rb +5 -5
  44. data/lib/jsi/schema/validation/items.rb +7 -7
  45. data/lib/jsi/schema/validation/not.rb +1 -1
  46. data/lib/jsi/schema/validation/numeric.rb +5 -5
  47. data/lib/jsi/schema/validation/object.rb +2 -2
  48. data/lib/jsi/schema/validation/pattern.rb +2 -2
  49. data/lib/jsi/schema/validation/properties.rb +7 -7
  50. data/lib/jsi/schema/validation/property_names.rb +1 -1
  51. data/lib/jsi/schema/validation/ref.rb +2 -2
  52. data/lib/jsi/schema/validation/required.rb +1 -1
  53. data/lib/jsi/schema/validation/someof.rb +3 -3
  54. data/lib/jsi/schema/validation/string.rb +2 -2
  55. data/lib/jsi/schema/validation/type.rb +1 -1
  56. data/lib/jsi/schema/validation.rb +1 -3
  57. data/lib/jsi/schema.rb +443 -226
  58. data/lib/jsi/schema_classes.rb +241 -147
  59. data/lib/jsi/schema_registry.rb +78 -19
  60. data/lib/jsi/schema_set.rb +114 -28
  61. data/lib/jsi/simple_wrap.rb +18 -4
  62. data/lib/jsi/util/private/attr_struct.rb +141 -0
  63. data/lib/jsi/util/private/memo_map.rb +75 -0
  64. data/lib/jsi/util/private.rb +185 -0
  65. data/lib/jsi/{typelike_modules.rb → util/typelike.rb} +79 -105
  66. data/lib/jsi/util.rb +157 -153
  67. data/lib/jsi/validation/error.rb +4 -0
  68. data/lib/jsi/validation/result.rb +18 -32
  69. data/lib/jsi/version.rb +1 -1
  70. data/lib/jsi.rb +65 -39
  71. data/lib/schemas/json-schema.org/draft-04/schema.rb +160 -3
  72. data/lib/schemas/json-schema.org/draft-06/schema.rb +162 -3
  73. data/lib/schemas/json-schema.org/draft-07/schema.rb +189 -3
  74. metadata +27 -11
  75. data/lib/jsi/metaschema.rb +0 -7
  76. data/lib/jsi/pathed_node.rb +0 -116
  77. data/lib/jsi/schema/validation/core.rb +0 -39
  78. data/lib/jsi/util/attr_struct.rb +0 -106
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e03bfff724257e11f9001d2845666d25f317b3761d5b62dc39816ad3fa3c28e
4
- data.tar.gz: d315e1424f2bd8eef67a2ad562c7668b6b8582b9bda25c8ba74cc892b1a9b1e4
3
+ metadata.gz: e18f4241f5f0013f657912295cb85db30095223a25030da3bbb690e68a5954d9
4
+ data.tar.gz: '083cbd8436424eeab11c36aeabb42f27ad9072d44450a0b8bfca391503131f9e'
5
5
  SHA512:
6
- metadata.gz: e62b9bf206486a77a3d1202388ddcd0fb29d326abf5a0b19fd6e74454ae05a494e4f88dcddaa42c7fc0b8b071877a406f213cde4589b21383778d9c713aa91b7
7
- data.tar.gz: a07b6d591b0cd8bfe28d5923018931f4ee8fc3dba393b240dbefd74a26e813e0a759ecab482be4da7a7e2fa3d4c74a0542c98fcef35364a1a07684e0dc24d2f0
6
+ metadata.gz: 1030a1013ae2209a7c6b1df083efe2466a670e1f644d64fd349475ba07331c182629c41a77d4d7220aaffcfb669f0c13bc5c49e6cb23a4f231747fbdc4b5ec7a
7
+ data.tar.gz: d9f24dec46a507e9d2f40046e891acec2bbef92150e72dd29bb1245618bb6be0ea9beb140cc4f0a465f187bd65e6e52c68bf82d906f8df0edaff67f062dd4c15
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,36 @@
1
+ # v0.8.0
2
+
3
+ - Immutable JSIs with new_jsi param `mutable`
4
+ - JSIs are still mutable by default, but in the next release they will default to immutable
5
+ - Base#jsi_indicated_schemas
6
+ - Base::StringNode
7
+ - rename metaschema modules /JSONSchemaOrgDraft0X/JSONSchemaDraft0X/
8
+ - terminology: /Metaschema/Meta-Schema/ and /metaschema/meta-schema/ (where hyphen is allowed)
9
+ - Base::HashNode#jsi_each_propertyName
10
+ - new_schema and/or new_jsi params register, schema_registry, stringify_symbol_keys, to_immutable
11
+ - new_schema block param will module_exec on schema module
12
+ - Base#[] param use_default default false, overridable
13
+ - SchemaModule::Connects, SchemaModule::Connection
14
+ - rm Schema#jsi_schema_instance_modules
15
+
16
+ # v0.7.0
17
+
18
+ - JSI::Base instances include Array/Hash-like modules on subclasses rather than extending each instance; are only Enumerable when appropriate instead of always
19
+ - PathedHashNode -> Base::HashNode, PathedArrayNode -> Base::ArrayNode, PathedNode merged with Base
20
+ - change application of conditional schemas to instances which do not validate, always apply them
21
+ - fix nomenclature: child is immediately below parent; descendent is anywhere at/below an ancestor
22
+ - deprecate previous misnamed methods
23
+ - Base#jsi_descendent_node, Base#jsi_ancestor_nodes
24
+ - add Schema#describes_schema!, deprecate Schema#jsi_schema_instance_modules
25
+ - Schema#keyword?
26
+ - Base#jmespath_search
27
+ - MetaschemaNode keeps its jsi_root_node (reducing an enormous number of unnecessary instantiations of MetaschemaNode)
28
+ - /metaschema_instance_modules/schema_implementation_modules/
29
+ - separate JSI::Util (public) and JSI::Util::Private
30
+ - support ruby 3
31
+ - Schema.default_metaschema is nil unless set by the application
32
+ - deprecate JSI::Typelike module, merged with Util; Arraylike -> Util::Arraylike, Hashlike -> Util::Hashlike
33
+
1
34
  # v0.6.0
2
35
 
3
36
  - initial validation; remove gem `json-schema` dependency
data/LICENSE.md CHANGED
@@ -2,7 +2,7 @@ Copright © [Ethan](https://github.com/notEthan/) <ethan.jsi@unth.net>
2
2
 
3
3
  [<img align="right" src="https://github.com/notEthan/jsi/raw/master/resources/icons/AGPL-3.0.png">](https://www.gnu.org/licenses/agpl-3.0.html)
4
4
 
5
- JSI is Open Source Software licensed under the terms of the [GNU Affero General Public License version 3](https://www.gnu.org/licenses/agpl-3.0.html).
5
+ JSI is licensed under the terms of the [GNU Affero General Public License version 3](https://www.gnu.org/licenses/agpl-3.0.html).
6
6
 
7
7
  GNU Affero General Public License
8
8
  =================================
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # JSI: JSON Schema Instantiation
2
2
 
3
- [![Build Status](https://travis-ci.org/notEthan/jsi.svg?branch=master)](https://travis-ci.org/notEthan/jsi)
3
+ ![Test CI Status](https://github.com/notEthan/jsi/actions/workflows/test.yml/badge.svg?branch=stable)
4
4
  [![Coverage Status](https://coveralls.io/repos/github/notEthan/jsi/badge.svg)](https://coveralls.io/github/notEthan/jsi)
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"
@@ -46,6 +48,8 @@ We name the module that JSI will use when instantiating a contact. Named modules
46
48
  Contact = contact_schema.jsi_schema_module
47
49
  ```
48
50
 
51
+ Note: it is more concise to instantiate the schema module with the shortcut {JSI.new_schema_module}, i.e. `Contact = JSI.new_schema_module(...)`. This example includes the intermediate step to help show all that is happening.
52
+
49
53
  To instantiate the schema, we need some JSON data (expressed here as YAML)
50
54
 
51
55
  ```yaml
@@ -89,7 +93,7 @@ bill.phone.map(&:location)
89
93
  # => ["home"]
90
94
  ```
91
95
 
92
- We also get validations, as you'd expect given that's largely what json-schema exists to do:
96
+ We also get validations, as you'd expect given that's largely what JSON Schema exists to do:
93
97
 
94
98
  ```ruby
95
99
  bill.jsi_valid?
@@ -113,7 +117,7 @@ bad.phone.jsi_validate
113
117
  # #<Set: {#<JSI::Validation::Error
114
118
  # message: "instance type does not match `type` value",
115
119
  # keyword: "type",
116
- # schema: #{<JSI (JSI::JSONSchemaOrgDraft07) Schema> "type" => "string"},
120
+ # schema: #{<JSI (JSI::JSONSchemaDraft07) Schema> "type" => "string"},
117
121
  # instance_ptr: JSI::Ptr["phone", 0, "number"],
118
122
  # instance_document: {"phone"=>[{"number"=>[5, 5, 5]}]}
119
123
  # >,
@@ -138,7 +142,7 @@ There's plenty more JSI has to offer, but this should give you a pretty good ide
138
142
  ## Terminology and Concepts
139
143
 
140
144
  - `JSI::Base` is the base class for each JSI schema class representing instances of JSON Schemas.
141
- - 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`.
145
+ - 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`.
142
146
  - 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.
143
147
  - 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.
144
148
  - "instance" is a term that is significantly overloaded in this space, so documentation will attempt to be clear what kind of instance is meant:
@@ -152,9 +156,9 @@ JSI supports these JSON Schema specification versions:
152
156
 
153
157
  | Version | `$schema` URI | JSI Schema Module |
154
158
  | --- | --- | --- |
155
- | Draft 4 | `http://json-schema.org/draft-04/schema#` | {JSI::JSONSchemaOrgDraft04} |
156
- | Draft 6 | `http://json-schema.org/draft-06/schema#` | {JSI::JSONSchemaOrgDraft06} |
157
- | Draft 7 | `http://json-schema.org/draft-07/schema#` | {JSI::JSONSchemaOrgDraft07} |
159
+ | Draft 4 | `http://json-schema.org/draft-04/schema#` | {JSI::JSONSchemaDraft04} |
160
+ | Draft 6 | `http://json-schema.org/draft-06/schema#` | {JSI::JSONSchemaDraft06} |
161
+ | Draft 7 | `http://json-schema.org/draft-07/schema#` | {JSI::JSONSchemaDraft07} |
158
162
 
159
163
  ## JSI and Object Oriented Programming
160
164
 
@@ -173,19 +177,22 @@ module Contact
173
177
  end
174
178
  end
175
179
 
180
+ bill.phone_numbers
181
+ # => ["555"]
182
+
176
183
  bill.name
177
184
  # => "bill esq."
178
185
  bill.name = 'rob esq.'
179
186
  # => "rob esq."
180
187
  bill['name']
181
188
  # => "rob"
182
- bill.phone_numbers
183
- # => ["555"]
184
189
  ```
185
190
 
186
- Note the use of `super` - you can call to accessors defined by JSI and make your accessors act as wrappers. You can alternatively use `[]` and `[]=` with the same effect.
191
+ `#phone_numbers` is a new method returning each number in the `phone` array - pretty straightforward.
192
+
193
+ 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`.
187
194
 
188
- Working with subschemas is just about as easy as with root schemas.
195
+ Working with subschemas to add methods is just about as easy as with root schemas.
189
196
 
190
197
  You can subscript or use property accessors on a JSI schema module to refer to the schema modules of its subschemas, e.g.:
191
198
 
@@ -206,8 +213,7 @@ bill.phone.first.number_with_dashes
206
213
  # => "5-5-5"
207
214
  ```
208
215
 
209
- A recommended convention for naming subschemas is to define them in the namespace of the module of their
210
- parent schema. The module can then be opened to add methods to the subschema's module.
216
+ 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.
211
217
 
212
218
  ```ruby
213
219
  module Contact
@@ -237,9 +243,9 @@ The classes used to instantiate JSIs are dynamically generated subclasses of JSI
237
243
 
238
244
  ## Registration
239
245
 
240
- 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}.
246
+ 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}.
241
247
 
242
- Schemas instantiated with `.new_schema`, and their subschemas, are automatically registered with `JSI.schema_registry` if they identify an absolute URI.
248
+ 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`.
243
249
 
244
250
  Schemas can automatically be lazily loaded by registering a block which instantiates them with {JSI::SchemaRegistry#autoload_uri} (see its documentation).
245
251
 
@@ -253,13 +259,13 @@ The following optional features are not completely supported:
253
259
  - 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 `$`.
254
260
  - Keywords `contentMediaType` and `contentEncoding` do not perform validation.
255
261
 
256
- ## Metaschemas
262
+ ## Meta-Schemas
257
263
 
258
- A metaschema is a schema which describes schemas. Likewise, a schema is an instance of a metaschema.
264
+ A meta-schema is a schema that describes schemas. Likewise, a schema is an instance of a meta-schema.
259
265
 
260
- In JSI, a schema is generally a JSI::Base instance whose schemas include a metaschema.
266
+ In JSI, a schema is generally a JSI::Base instance whose schemas include a meta-schema.
261
267
 
262
- 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.
268
+ 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.
263
269
 
264
270
  ## ActiveRecord serialization
265
271
 
@@ -269,7 +275,7 @@ Let's say you're sticking to JSON types in the database - you have to do so if y
269
275
 
270
276
  But if your database contains JSON, then your deserialized objects in ruby are likewise Hash / Array / basic types. You have to use subscripts instead of accessors, and you don't have any way to add methods to your data types.
271
277
 
272
- JSI gives you the best of both with JSICoder. This coder dumps objects which are simple JSON types, and loads instances of a specified JSI schema. Here's an example, supposing a `users` table with a JSON column `contact_info`:
278
+ JSI gives you the best of both with {JSI::JSICoder}. This coder dumps objects which are simple JSON types, and loads instances of a specified JSON Schema. Here's an example, supposing a `users` table with a JSON column `contact_info` to be instantiated using the `Contact` schema module defined in the Example section above:
273
279
 
274
280
  ```ruby
275
281
  class User < ActiveRecord::Base
@@ -277,7 +283,7 @@ class User < ActiveRecord::Base
277
283
  end
278
284
  ```
279
285
 
280
- 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.
286
+ 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.
281
287
 
282
288
  See the gem [`arms`](https://github.com/notEthan/arms) if you wish to serialize the dumped JSON-compatible objects further as text.
283
289
 
data/jsi.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ require_relative "lib/jsi/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "jsi"
5
+ spec.version = JSI::VERSION
6
+ spec.authors = ["Ethan"]
7
+ spec.email = ["ethan.jsi@unth.net"]
8
+
9
+ spec.summary = "JSI: JSON Schema Instantiation"
10
+ spec.description = "JSI offers an Object-Oriented representation for JSON data using JSON Schemas"
11
+ spec.homepage = "https://github.com/notEthan/jsi"
12
+ spec.license = "AGPL-3.0"
13
+
14
+ spec.files = [
15
+ 'LICENSE.md',
16
+ 'CHANGELOG.md',
17
+ 'README.md',
18
+ 'readme.rb',
19
+ '.yardopts',
20
+ 'jsi.gemspec',
21
+ *Dir['lib/**/*'],
22
+ *Dir['\\{resources\\}/schemas/**/*'],
23
+ ].reject { |f| File.lstat(f).ftype == 'directory' }
24
+
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "addressable", '~> 2.3'
28
+ spec.add_dependency "bigdecimal"
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
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ module 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
+ #
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)
23
+ end
24
+
25
+ nil
26
+ end
27
+
28
+ # See {Base#jsi_hash?}. Always true for HashNode.
29
+ def jsi_hash?
30
+ true
31
+ end
32
+
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
38
+ end
39
+
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
66
+
67
+ # yields each Hash key (JSON object property name) and value of this node.
68
+ #
69
+ # each yielded key is a key of the instance hash, and each yielded value is the result of {Base#[]}.
70
+ #
71
+ # @param kw keyword arguments are passed to {Base#[]}
72
+ # @yield [Object, Object] each key and value of this hash node
73
+ # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
74
+ def each(**kw, &block)
75
+ return to_enum(__method__, **kw) { jsi_node_content_hash_pubsend(:size) } unless block
76
+ if block.arity > 1
77
+ jsi_node_content_hash_pubsend(:each_key) { |k| yield k, self[k, **kw] }
78
+ else
79
+ jsi_node_content_hash_pubsend(:each_key) { |k| yield [k, self[k, **kw]] }
80
+ end
81
+ self
82
+ end
83
+
84
+ # a hash in which each key is a key of the instance hash and each value is the result of {Base#[]}
85
+ # @param kw keyword arguments are passed to {Base#[]}
86
+ # @return [Hash]
87
+ def to_hash(**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
104
+ end
105
+
106
+ include Util::Hashlike
107
+
108
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
109
+ # invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash
110
+ # @param method_name [String, Symbol]
111
+ # @param a positional arguments are passed to the invocation of method_name
112
+ # @param b block is passed to the invocation of method_name
113
+ # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash
114
+ def jsi_node_content_hash_pubsend(method_name, *a, &b)
115
+ if jsi_node_content.respond_to?(method_name)
116
+ jsi_node_content.public_send(method_name, *a, &b)
117
+ else
118
+ jsi_node_content.to_hash.public_send(method_name, *a, &b)
119
+ end
120
+ end
121
+ else
122
+ # invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash
123
+ # @param method_name [String, Symbol]
124
+ # @param a positional arguments are passed to the invocation of method_name
125
+ # @param kw keyword arguments are passed to the invocation of method_name
126
+ # @param b block is passed to the invocation of method_name
127
+ # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash
128
+ def jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
129
+ if jsi_node_content.respond_to?(method_name)
130
+ jsi_node_content.public_send(method_name, *a, **kw, &b)
131
+ else
132
+ jsi_node_content.to_hash.public_send(method_name, *a, **kw, &b)
133
+ end
134
+ end
135
+ end
136
+
137
+ # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
138
+ SAFE_KEY_ONLY_METHODS.each do |method_name|
139
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
140
+ define_method(method_name) do |*a, &b|
141
+ jsi_node_content_hash_pubsend(method_name, *a, &b)
142
+ end
143
+ else
144
+ define_method(method_name) do |*a, **kw, &b|
145
+ jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ # module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content})
152
+ # is an Array (or responds to `#to_ary`)
153
+ module Base::ArrayNode
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
242
+
243
+ # yields each array element of this node.
244
+ #
245
+ # each yielded element is the result of {Base#[]} for each index of the instance array.
246
+ #
247
+ # @param kw keyword arguments are passed to {Base#[]}
248
+ # @yield [Object] each element of this array node
249
+ # @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
250
+ def each(**kw, &block)
251
+ return to_enum(__method__, **kw) { jsi_node_content_ary_pubsend(:size) } unless block
252
+ jsi_node_content_ary_pubsend(:each_index) { |i| yield(self[i, **kw]) }
253
+ self
254
+ end
255
+
256
+ # an array, the same size as the instance array, in which the element at each index is the
257
+ # result of {Base#[]}.
258
+ # @param kw keyword arguments are passed to {Base#[]}
259
+ # @return [Array]
260
+ def to_ary(**kw)
261
+ to_a(**kw)
262
+ end
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
+
269
+ include Util::Arraylike
270
+
271
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
272
+ # invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary
273
+ # @param method_name [String, Symbol]
274
+ # @param a positional arguments are passed to the invocation of method_name
275
+ # @param b block is passed to the invocation of method_name
276
+ # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary
277
+ def jsi_node_content_ary_pubsend(method_name, *a, &b)
278
+ if jsi_node_content.respond_to?(method_name)
279
+ jsi_node_content.public_send(method_name, *a, &b)
280
+ else
281
+ jsi_node_content.to_ary.public_send(method_name, *a, &b)
282
+ end
283
+ end
284
+ else
285
+ # invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary
286
+ # @param method_name [String, Symbol]
287
+ # @param a positional arguments are passed to the invocation of method_name
288
+ # @param kw keyword arguments are passed to the invocation of method_name
289
+ # @param b block is passed to the invocation of method_name
290
+ # @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary
291
+ def jsi_node_content_ary_pubsend(method_name, *a, **kw, &b)
292
+ if jsi_node_content.respond_to?(method_name)
293
+ jsi_node_content.public_send(method_name, *a, **kw, &b)
294
+ else
295
+ jsi_node_content.to_ary.public_send(method_name, *a, **kw, &b)
296
+ end
297
+ end
298
+ end
299
+
300
+ # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
301
+ # we override these methods from Arraylike
302
+ SAFE_INDEX_ONLY_METHODS.each do |method_name|
303
+ if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
304
+ define_method(method_name) do |*a, &b|
305
+ jsi_node_content_ary_pubsend(method_name, *a, &b)
306
+ end
307
+ else
308
+ define_method(method_name) do |*a, **kw, &b|
309
+ jsi_node_content_ary_pubsend(method_name, *a, **kw, &b)
310
+ end
311
+ end
312
+ end
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
348
+ end