jsi 0.6.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +6 -1
- data/CHANGELOG.md +33 -0
- data/LICENSE.md +1 -1
- data/README.md +29 -23
- data/jsi.gemspec +29 -0
- data/lib/jsi/base/mutability.rb +44 -0
- data/lib/jsi/base/node.rb +348 -0
- data/lib/jsi/base.rb +497 -339
- data/lib/jsi/jsi_coder.rb +19 -17
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +61 -26
- data/lib/jsi/metaschema_node.rb +161 -133
- data/lib/jsi/ptr.rb +80 -47
- data/lib/jsi/schema/application/child_application/contains.rb +11 -2
- data/lib/jsi/schema/application/child_application/draft04.rb +0 -1
- data/lib/jsi/schema/application/child_application/draft06.rb +0 -1
- data/lib/jsi/schema/application/child_application/draft07.rb +0 -1
- data/lib/jsi/schema/application/child_application/items.rb +3 -3
- data/lib/jsi/schema/application/child_application/properties.rb +3 -3
- data/lib/jsi/schema/application/child_application.rb +0 -27
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +1 -1
- data/lib/jsi/schema/application/inplace_application/draft04.rb +0 -1
- data/lib/jsi/schema/application/inplace_application/draft06.rb +0 -1
- data/lib/jsi/schema/application/inplace_application/draft07.rb +0 -1
- data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +3 -3
- data/lib/jsi/schema/application/inplace_application/ref.rb +2 -2
- data/lib/jsi/schema/application/inplace_application/someof.rb +26 -11
- data/lib/jsi/schema/application/inplace_application.rb +0 -32
- data/lib/jsi/schema/draft04.rb +0 -1
- data/lib/jsi/schema/draft06.rb +0 -1
- data/lib/jsi/schema/draft07.rb +0 -1
- data/lib/jsi/schema/ref.rb +46 -19
- data/lib/jsi/schema/schema_ancestor_node.rb +69 -66
- data/lib/jsi/schema/validation/array.rb +3 -3
- data/lib/jsi/schema/validation/const.rb +1 -1
- data/lib/jsi/schema/validation/contains.rb +2 -2
- data/lib/jsi/schema/validation/dependencies.rb +1 -1
- data/lib/jsi/schema/validation/draft04/minmax.rb +8 -6
- data/lib/jsi/schema/validation/draft04.rb +0 -2
- data/lib/jsi/schema/validation/draft06.rb +0 -2
- data/lib/jsi/schema/validation/draft07.rb +0 -2
- data/lib/jsi/schema/validation/enum.rb +1 -1
- data/lib/jsi/schema/validation/ifthenelse.rb +5 -5
- data/lib/jsi/schema/validation/items.rb +7 -7
- data/lib/jsi/schema/validation/not.rb +1 -1
- data/lib/jsi/schema/validation/numeric.rb +5 -5
- data/lib/jsi/schema/validation/object.rb +2 -2
- data/lib/jsi/schema/validation/pattern.rb +2 -2
- data/lib/jsi/schema/validation/properties.rb +7 -7
- data/lib/jsi/schema/validation/property_names.rb +1 -1
- data/lib/jsi/schema/validation/ref.rb +2 -2
- data/lib/jsi/schema/validation/required.rb +1 -1
- data/lib/jsi/schema/validation/someof.rb +3 -3
- data/lib/jsi/schema/validation/string.rb +2 -2
- data/lib/jsi/schema/validation/type.rb +1 -1
- data/lib/jsi/schema/validation.rb +1 -3
- data/lib/jsi/schema.rb +443 -226
- data/lib/jsi/schema_classes.rb +241 -147
- data/lib/jsi/schema_registry.rb +78 -19
- data/lib/jsi/schema_set.rb +114 -28
- data/lib/jsi/simple_wrap.rb +18 -4
- data/lib/jsi/util/private/attr_struct.rb +141 -0
- data/lib/jsi/util/private/memo_map.rb +75 -0
- data/lib/jsi/util/private.rb +185 -0
- data/lib/jsi/{typelike_modules.rb → util/typelike.rb} +79 -105
- data/lib/jsi/util.rb +157 -153
- data/lib/jsi/validation/error.rb +4 -0
- data/lib/jsi/validation/result.rb +18 -32
- data/lib/jsi/version.rb +1 -1
- data/lib/jsi.rb +65 -39
- data/lib/schemas/json-schema.org/draft-04/schema.rb +160 -3
- data/lib/schemas/json-schema.org/draft-06/schema.rb +162 -3
- data/lib/schemas/json-schema.org/draft-07/schema.rb +189 -3
- metadata +27 -11
- data/lib/jsi/metaschema.rb +0 -7
- data/lib/jsi/pathed_node.rb +0 -116
- data/lib/jsi/schema/validation/core.rb +0 -39
- data/lib/jsi/util/attr_struct.rb +0 -106
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e18f4241f5f0013f657912295cb85db30095223a25030da3bbb690e68a5954d9
|
4
|
+
data.tar.gz: '083cbd8436424eeab11c36aeabb42f27ad9072d44450a0b8bfca391503131f9e'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1030a1013ae2209a7c6b1df083efe2466a670e1f644d64fd349475ba07331c182629c41a77d4d7220aaffcfb669f0c13bc5c49e6cb23a4f231747fbdc4b5ec7a
|
7
|
+
data.tar.gz: d9f24dec46a507e9d2f40046e891acec2bbef92150e72dd29bb1245618bb6be0ea9beb140cc4f0a465f187bd65e6e52c68bf82d906f8df0edaff67f062dd4c15
|
data/.yardopts
CHANGED
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
|
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
|
-
|
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
|
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.
|
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
|
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::
|
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
|
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::
|
156
|
-
| Draft 6 | `http://json-schema.org/draft-06/schema#` | {JSI::
|
157
|
-
| Draft 7 | `http://json-schema.org/draft-07/schema#` | {JSI::
|
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
|
-
|
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).
|
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
|
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
|
-
##
|
262
|
+
## Meta-Schemas
|
257
263
|
|
258
|
-
A
|
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
|
266
|
+
In JSI, a schema is generally a JSI::Base instance whose schemas include a meta-schema.
|
261
267
|
|
262
|
-
A self-descriptive
|
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
|
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
|
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
|