jsi 0.4.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +33 -0
- data/LICENSE.md +1 -1
- data/README.md +114 -42
- data/jsi.gemspec +14 -12
- data/lib/jsi/base/node.rb +183 -0
- data/lib/jsi/base.rb +388 -220
- data/lib/jsi/jsi_coder.rb +8 -7
- data/lib/jsi/metaschema.rb +0 -1
- data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
- data/lib/jsi/metaschema_node.rb +159 -135
- data/lib/jsi/ptr.rb +303 -0
- data/lib/jsi/schema/application/child_application/contains.rb +25 -0
- data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
- data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
- data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
- data/lib/jsi/schema/application/child_application/items.rb +18 -0
- data/lib/jsi/schema/application/child_application/properties.rb +25 -0
- data/lib/jsi/schema/application/child_application.rb +38 -0
- data/lib/jsi/schema/application/draft04.rb +8 -0
- data/lib/jsi/schema/application/draft06.rb +8 -0
- data/lib/jsi/schema/application/draft07.rb +8 -0
- data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
- data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
- data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
- data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
- data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
- data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
- data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
- data/lib/jsi/schema/application/inplace_application.rb +41 -0
- data/lib/jsi/schema/application.rb +12 -0
- data/lib/jsi/schema/draft04.rb +14 -0
- data/lib/jsi/schema/draft06.rb +14 -0
- data/lib/jsi/schema/draft07.rb +14 -0
- data/lib/jsi/schema/issue.rb +36 -0
- data/lib/jsi/schema/ref.rb +160 -0
- data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
- data/lib/jsi/schema/validation/array.rb +69 -0
- data/lib/jsi/schema/validation/const.rb +20 -0
- data/lib/jsi/schema/validation/contains.rb +25 -0
- data/lib/jsi/schema/validation/core.rb +39 -0
- data/lib/jsi/schema/validation/dependencies.rb +49 -0
- data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
- data/lib/jsi/schema/validation/draft04.rb +112 -0
- data/lib/jsi/schema/validation/draft06.rb +122 -0
- data/lib/jsi/schema/validation/draft07.rb +159 -0
- data/lib/jsi/schema/validation/enum.rb +25 -0
- data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
- data/lib/jsi/schema/validation/items.rb +54 -0
- data/lib/jsi/schema/validation/not.rb +20 -0
- data/lib/jsi/schema/validation/numeric.rb +121 -0
- data/lib/jsi/schema/validation/object.rb +45 -0
- data/lib/jsi/schema/validation/pattern.rb +34 -0
- data/lib/jsi/schema/validation/properties.rb +101 -0
- data/lib/jsi/schema/validation/property_names.rb +32 -0
- data/lib/jsi/schema/validation/ref.rb +40 -0
- data/lib/jsi/schema/validation/required.rb +27 -0
- data/lib/jsi/schema/validation/someof.rb +90 -0
- data/lib/jsi/schema/validation/string.rb +47 -0
- data/lib/jsi/schema/validation/type.rb +49 -0
- data/lib/jsi/schema/validation.rb +51 -0
- data/lib/jsi/schema.rb +508 -149
- data/lib/jsi/schema_classes.rb +199 -59
- data/lib/jsi/schema_registry.rb +151 -0
- data/lib/jsi/schema_set.rb +181 -0
- data/lib/jsi/simple_wrap.rb +23 -4
- data/lib/jsi/util/private/attr_struct.rb +127 -0
- data/lib/jsi/util/private.rb +204 -0
- data/lib/jsi/util/typelike.rb +229 -0
- data/lib/jsi/util.rb +89 -53
- data/lib/jsi/validation/error.rb +34 -0
- data/lib/jsi/validation/result.rb +210 -0
- data/lib/jsi/validation.rb +15 -0
- data/lib/jsi/version.rb +3 -1
- data/lib/jsi.rb +44 -14
- data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
- data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
- data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
- data/readme.rb +138 -0
- data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
- data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
- data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
- metadata +75 -122
- data/.simplecov +0 -3
- data/Rakefile.rb +0 -9
- data/lib/jsi/base/to_rb.rb +0 -128
- data/lib/jsi/json/node.rb +0 -203
- data/lib/jsi/json/pointer.rb +0 -419
- data/lib/jsi/json-schema-fragments.rb +0 -61
- data/lib/jsi/json.rb +0 -10
- data/lib/jsi/pathed_node.rb +0 -118
- data/lib/jsi/typelike_modules.rb +0 -240
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +0 -323
- data/test/base_hash_test.rb +0 -337
- data/test/base_test.rb +0 -486
- data/test/jsi_coder_test.rb +0 -85
- data/test/jsi_json_arraynode_test.rb +0 -150
- data/test/jsi_json_hashnode_test.rb +0 -132
- data/test/jsi_json_node_test.rb +0 -257
- data/test/jsi_json_pointer_test.rb +0 -102
- data/test/jsi_test.rb +0 -11
- data/test/jsi_typelike_as_json_test.rb +0 -53
- data/test/metaschema_node_test.rb +0 -19
- data/test/schema_module_test.rb +0 -21
- data/test/schema_test.rb +0 -208
- data/test/spreedly_openapi_test.rb +0 -8
- data/test/test_helper.rb +0 -97
- data/test/util_test.rb +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0659cc34b129717ffb69089f1d3a25ba3c3ec5364d49a6c0cc93dd5979ac7638'
|
4
|
+
data.tar.gz: 5b0298a9843a61e6f5693d46dd6d86021d53767638e1f3f84e23fff4b167276e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b68dcbb479756b8fa58a43f7a8e83accda02a8b0aa87617dabb6623a4b1a8aa05f94cb03de601cf7bf88c943ce62e42963b533ef125729847159e3b775f32c2
|
7
|
+
data.tar.gz: 49997a70900b9d250e4888bcf3e5470e68b116c3a558651db9e5551f73027ea4abd18b0585d23f5201b15e8d80f1d72b46488d1870ac530d40368c7577d3b048
|
data/.yardopts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
--main README.md --markup=markdown {lib}/**/*.rb
|
1
|
+
--main README.md --markup=markdown --no-private {lib}/**/*.rb
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,36 @@
|
|
1
|
+
# v0.7.0
|
2
|
+
|
3
|
+
- JSI::Base instances include Array/Hash-like modules on subclasses rather than extending each instance; are only Enumerable when appropriate instead of always
|
4
|
+
- PathedHashNode -> Base::HashNode, PathedArrayNode -> Base::ArrayNode, PathedNode merged with Base
|
5
|
+
- change application of conditional schemas to instances which do not validate, always apply them
|
6
|
+
- fix nomenclature: child is immediately below parent; descendent is anywhere at/below an ancestor
|
7
|
+
- deprecate previous misnamed methods
|
8
|
+
- Base#jsi_descendent_node, Base#jsi_ancestor_nodes
|
9
|
+
- add Schema#describes_schema!, deprecate Schema#jsi_schema_instance_modules
|
10
|
+
- Schema#keyword?
|
11
|
+
- Base#jmespath_search
|
12
|
+
- MetaschemaNode keeps its jsi_root_node (reducing an enormous number of unnecessary instantiations of MetaschemaNode)
|
13
|
+
- /metaschema_instance_modules/schema_implementation_modules/
|
14
|
+
- separate JSI::Util (public) and JSI::Util::Private
|
15
|
+
- support ruby 3
|
16
|
+
- Schema.default_metaschema is nil unless set by the application
|
17
|
+
- deprecate JSI::Typelike module, merged with Util; Arraylike -> Util::Arraylike, Hashlike -> Util::Hashlike
|
18
|
+
|
19
|
+
# v0.6.0
|
20
|
+
|
21
|
+
- initial validation; remove gem `json-schema` dependency
|
22
|
+
- initial schema issue collection
|
23
|
+
- JSI.new_schema / new_schema_module
|
24
|
+
- JSI::SchemaSet
|
25
|
+
- JSI::SchemaRegistry
|
26
|
+
- JSI::Schema::Ref
|
27
|
+
- Base#jsi_each_child_node, jsi_select_children_node_first, jsi_select_children_leaf_first
|
28
|
+
- JSI::JSON::Pointer → JSI::Ptr
|
29
|
+
- deprecate JSI.class_for_schemas
|
30
|
+
- remove test/development files from gem
|
31
|
+
- remove JSI::JSON::Node
|
32
|
+
- as always too much more to list
|
33
|
+
|
1
34
|
# v0.4.0
|
2
35
|
|
3
36
|
- a JSI::Base has multiple jsi_schemas https://github.com/notEthan/jsi/pull/88
|
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,19 +1,24 @@
|
|
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 [https://json-schema.org/]().
|
8
|
+
To learn more about JSON Schema see [https://json-schema.org/](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
|
+
|
12
|
+
A JSI instance aims to offer a fairly unobtrusive wrapper around its JSON data, which is usually a Hash (JSON Object) or Array described by one or more JSON Schemas. JSI instances have accessors for property names described by schemas, schema validation, and other nice things. Mostly though, you use a JSI as you would use its underlying data, calling the same methods (e.g. `#[]`, `#map`, `#repeated_permutation`) and passing it to anything that duck-types expecting `#to_ary` or `#to_hash`.
|
13
|
+
|
14
|
+
Note: The canonical location of this README is on [RubyDoc](http://rubydoc.info/gems/jsi/). When viewed on [Github](https://github.com/notEthan/jsi/), it may be inconsistent with the latest released gem, and Yardoc links will not work.
|
11
15
|
|
12
16
|
## Example
|
13
17
|
|
14
18
|
Words are boring, let's code. Here's a schema in yaml:
|
15
19
|
|
16
20
|
```yaml
|
21
|
+
$schema: "http://json-schema.org/draft-07/schema"
|
17
22
|
description: "A Contact"
|
18
23
|
type: "object"
|
19
24
|
properties:
|
@@ -28,11 +33,11 @@ properties:
|
|
28
33
|
number: {type: "string"}
|
29
34
|
```
|
30
35
|
|
31
|
-
|
36
|
+
We pass that to {JSI.new_schema} which will instantiate a JSI Schema which represents it:
|
32
37
|
|
33
38
|
```ruby
|
34
|
-
# this would usually
|
35
|
-
contact_schema = JSI
|
39
|
+
# this would usually load YAML or JSON; the schema object is inlined for copypastability.
|
40
|
+
contact_schema = JSI.new_schema({"$schema" => "http://json-schema.org/draft-07/schema", "description" => "A Contact", "type" => "object", "properties" => {"name" => {"type" => "string"}, "phone" => {"type" => "array", "items" => {"type" => "object", "properties" => {"location" => {"type" => "string"}, "number" => {"type" => "string"}}}}}})
|
36
41
|
```
|
37
42
|
|
38
43
|
We name the module that JSI will use when instantiating a contact. Named modules are better to work with, and JSI will indicate the names of schema modules in its `#inspect` output.
|
@@ -41,6 +46,8 @@ We name the module that JSI will use when instantiating a contact. Named modules
|
|
41
46
|
Contact = contact_schema.jsi_schema_module
|
42
47
|
```
|
43
48
|
|
49
|
+
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.
|
50
|
+
|
44
51
|
To instantiate the schema, we need some JSON data (expressed here as YAML)
|
45
52
|
|
46
53
|
```yaml
|
@@ -54,12 +61,12 @@ nickname: big b
|
|
54
61
|
So, if we construct an instance like:
|
55
62
|
|
56
63
|
```ruby
|
57
|
-
# this would usually
|
64
|
+
# this would usually load YAML or JSON; the schema instance is inlined for copypastability.
|
58
65
|
bill = Contact.new_jsi({"name" => "bill", "phone" => [{"location" => "home", "number" => "555"}], "nickname" => "big b"})
|
59
66
|
# => #{<JSI (Contact)>
|
60
67
|
# "name" => "bill",
|
61
|
-
# "phone" => #[<JSI>
|
62
|
-
# #{<JSI>
|
68
|
+
# "phone" => #[<JSI (Contact.properties["phone"])>
|
69
|
+
# #{<JSI (Contact.properties["phone"].items)>
|
63
70
|
# "location" => "home",
|
64
71
|
# "number" => "555"
|
65
72
|
# }
|
@@ -68,7 +75,7 @@ bill = Contact.new_jsi({"name" => "bill", "phone" => [{"location" => "home", "nu
|
|
68
75
|
# }
|
69
76
|
```
|
70
77
|
|
71
|
-
Note that the keys are strings. JSI, being designed with JSON in mind, is geared toward string keys.
|
78
|
+
Note that the hash keys are strings. JSI, being designed with JSON in mind, is geared toward string keys.
|
72
79
|
|
73
80
|
We get accessors for the Contact:
|
74
81
|
|
@@ -77,7 +84,7 @@ bill.name
|
|
77
84
|
# => "bill"
|
78
85
|
```
|
79
86
|
|
80
|
-
but also nested accessors -
|
87
|
+
but also nested accessors - `#phone` is an instance of its array-type schema, and each phone item is an instance of another object-type schema with `#location` and `#number` accessors:
|
81
88
|
|
82
89
|
```ruby
|
83
90
|
bill.phone.map(&:location)
|
@@ -87,34 +94,43 @@ bill.phone.map(&:location)
|
|
87
94
|
We also get validations, as you'd expect given that's largely what json-schema exists to do:
|
88
95
|
|
89
96
|
```ruby
|
90
|
-
bill.
|
97
|
+
bill.jsi_valid?
|
91
98
|
# => true
|
92
99
|
```
|
93
100
|
|
94
|
-
... and validations on the nested schema instances (
|
101
|
+
... and validations on the nested schema instances (`#phone` here), showing in this example validation failure on /phone/0/number:
|
95
102
|
|
96
103
|
```ruby
|
97
104
|
bad = Contact.new_jsi({'phone' => [{'number' => [5, 5, 5]}]})
|
98
105
|
# => #{<JSI (Contact)>
|
99
|
-
# "phone" => #[<JSI>
|
100
|
-
# #{<JSI>
|
101
|
-
# "number" => #[<JSI> 5, 5, 5]
|
106
|
+
# "phone" => #[<JSI (Contact.properties["phone"])>
|
107
|
+
# #{<JSI (Contact.properties["phone"].items)>
|
108
|
+
# "number" => #[<JSI (Contact.properties["phone"].items.properties["number"])> 5, 5, 5]
|
102
109
|
# }
|
103
110
|
# ]
|
104
111
|
# }
|
105
|
-
bad.phone.
|
106
|
-
# =>
|
112
|
+
bad.phone.jsi_validate
|
113
|
+
# => #<JSI::Validation::FullResult
|
114
|
+
# @validation_errors=
|
115
|
+
# #<Set: {#<JSI::Validation::Error
|
116
|
+
# message: "instance type does not match `type` value",
|
117
|
+
# keyword: "type",
|
118
|
+
# schema: #{<JSI (JSI::JSONSchemaOrgDraft07) Schema> "type" => "string"},
|
119
|
+
# instance_ptr: JSI::Ptr["phone", 0, "number"],
|
120
|
+
# instance_document: {"phone"=>[{"number"=>[5, 5, 5]}]}
|
121
|
+
# >,
|
122
|
+
# ...
|
123
|
+
# >
|
107
124
|
```
|
108
125
|
|
109
|
-
These validations are done by the [`json-schema` gem](https://github.com/ruby-json-schema/json-schema) - JSI does not do validations on its own.
|
110
|
-
|
111
126
|
Since the underlying instance is a ruby hash (json object), we can use it like a hash with `#[]` or, say, `#transform_values`:
|
112
127
|
|
113
128
|
```ruby
|
114
|
-
# note that #size here is actually referring to multiple different methods;
|
115
|
-
# it is String#size but for phone it is Array#size.
|
129
|
+
# note that #size here is actually referring to multiple different methods;
|
130
|
+
# for name and nickname it is String#size but for phone it is Array#size.
|
116
131
|
bill.transform_values(&:size)
|
117
132
|
# => {"name" => 4, "phone" => 1, "nickname" => 5}
|
133
|
+
|
118
134
|
bill['nickname']
|
119
135
|
# => "big b"
|
120
136
|
```
|
@@ -123,13 +139,24 @@ There's plenty more JSI has to offer, but this should give you a pretty good ide
|
|
123
139
|
|
124
140
|
## Terminology and Concepts
|
125
141
|
|
126
|
-
- `JSI::Base` is the base class for each JSI class representing
|
127
|
-
- a "JSI
|
128
|
-
- a "JSI
|
142
|
+
- `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`.
|
144
|
+
- 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
|
+
- 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.
|
129
146
|
- "instance" is a term that is significantly overloaded in this space, so documentation will attempt to be clear what kind of instance is meant:
|
130
147
|
- a schema instance refers broadly to a data structure that is described by a JSON schema.
|
131
|
-
- a JSI instance (or just "a JSI") is a ruby object instantiating a JSI class.
|
132
|
-
-
|
148
|
+
- a JSI instance (or just "a JSI") is a ruby object instantiating a JSI schema class (subclass of `JSI::Base`). This wraps the content of the schema instance (see `JSI::Base#jsi_instance`), and ties it to the schemas which describe the instance (`JSI::Base#jsi_schemas`).
|
149
|
+
- "schema" refers to either a parsed JSON schema (generally a ruby Hash) or a JSI schema.
|
150
|
+
|
151
|
+
## Supported specification versions
|
152
|
+
|
153
|
+
JSI supports these JSON Schema specification versions:
|
154
|
+
|
155
|
+
| Version | `$schema` URI | JSI Schema Module |
|
156
|
+
| --- | --- | --- |
|
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} |
|
133
160
|
|
134
161
|
## JSI and Object Oriented Programming
|
135
162
|
|
@@ -148,23 +175,34 @@ module Contact
|
|
148
175
|
end
|
149
176
|
end
|
150
177
|
|
178
|
+
bill.phone_numbers
|
179
|
+
# => ["555"]
|
180
|
+
|
151
181
|
bill.name
|
152
182
|
# => "bill esq."
|
153
183
|
bill.name = 'rob esq.'
|
154
184
|
# => "rob esq."
|
155
185
|
bill['name']
|
156
186
|
# => "rob"
|
157
|
-
bill.phone_numbers
|
158
|
-
# => ["555"]
|
159
187
|
```
|
160
188
|
|
161
|
-
|
189
|
+
`#phone_numbers` is a new method returning each number in the `phone` array - pretty straightforward.
|
190
|
+
|
191
|
+
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`.
|
162
192
|
|
163
|
-
|
193
|
+
Working with subschemas is just about as easy as with root schemas.
|
194
|
+
|
195
|
+
You can subscript or use property accessors on a JSI schema module to refer to the schema modules of its subschemas, e.g.:
|
164
196
|
|
165
197
|
```ruby
|
166
|
-
|
167
|
-
|
198
|
+
Contact.properties['phone'].items
|
199
|
+
# => Contact.properties["phone"].items (JSI Schema Module)
|
200
|
+
```
|
201
|
+
|
202
|
+
Opening a subschema module with [`module_exec`](https://ruby-doc.org/core/Module.html#method-i-module_exec), you can add methods to instances of the subschema.
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
Contact.properties['phone'].items.module_exec do
|
168
206
|
def number_with_dashes
|
169
207
|
number.split(//).join('-')
|
170
208
|
end
|
@@ -173,10 +211,24 @@ bill.phone.first.number_with_dashes
|
|
173
211
|
# => "5-5-5"
|
174
212
|
```
|
175
213
|
|
176
|
-
|
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.
|
177
216
|
|
178
217
|
```ruby
|
179
|
-
|
218
|
+
module Contact
|
219
|
+
Phone = properties['phone'].items
|
220
|
+
module Phone
|
221
|
+
def number_with_dashes
|
222
|
+
number.split(//).join('-')
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
However, that is only a convention, and a flat namespace works fine too.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
ContactPhone = Contact.properties['phone'].items
|
180
232
|
module ContactPhone
|
181
233
|
def number_with_dashes
|
182
234
|
number.split(//).join('-')
|
@@ -184,15 +236,35 @@ module ContactPhone
|
|
184
236
|
end
|
185
237
|
```
|
186
238
|
|
187
|
-
|
239
|
+
### A note on Classes
|
240
|
+
|
241
|
+
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
|
+
|
243
|
+
## Registration
|
244
|
+
|
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}.
|
246
|
+
|
247
|
+
Schemas instantiated with `.new_schema`, and their subschemas, are automatically registered with `JSI.schema_registry` if they identify an absolute URI.
|
248
|
+
|
249
|
+
Schemas can automatically be lazily loaded by registering a block which instantiates them with {JSI::SchemaRegistry#autoload_uri} (see its documentation).
|
250
|
+
|
251
|
+
## Validation
|
252
|
+
|
253
|
+
JSI implements all required features, and many optional features, for validation according to supported JSON Schema specifications. To validate instances, see methods {JSI::Base#jsi_validate}, {JSI::Base#jsi_valid?}, {JSI::Schema#instance_validate}, {JSI::Schema#instance_valid?}.
|
254
|
+
|
255
|
+
The following optional features are not completely supported:
|
256
|
+
|
257
|
+
- The `format` keyword does not perform any validation.
|
258
|
+
- 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
|
+
- Keywords `contentMediaType` and `contentEncoding` do not perform validation.
|
188
260
|
|
189
261
|
## Metaschemas
|
190
262
|
|
191
263
|
A metaschema is a schema which describes schemas. Likewise, a schema is an instance of a metaschema.
|
192
264
|
|
193
|
-
In JSI, a schema is generally a JSI::Base instance whose
|
265
|
+
In JSI, a schema is generally a JSI::Base instance whose schemas include a metaschema.
|
194
266
|
|
195
|
-
A self-descriptive metaschema - most commonly one of the JSON schema draft metaschemas - is an object whose
|
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.
|
196
268
|
|
197
269
|
## ActiveRecord serialization
|
198
270
|
|
@@ -202,7 +274,7 @@ Let's say you're sticking to JSON types in the database - you have to do so if y
|
|
202
274
|
|
203
275
|
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.
|
204
276
|
|
205
|
-
JSI gives you the best of both with JSICoder. This coder dumps objects which are simple JSON types, and loads instances of a specified
|
277
|
+
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:
|
206
278
|
|
207
279
|
```ruby
|
208
280
|
class User < ActiveRecord::Base
|
@@ -210,13 +282,13 @@ class User < ActiveRecord::Base
|
|
210
282
|
end
|
211
283
|
```
|
212
284
|
|
213
|
-
Now `user.
|
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.
|
214
286
|
|
215
287
|
See the gem [`arms`](https://github.com/notEthan/arms) if you wish to serialize the dumped JSON-compatible objects further as text.
|
216
288
|
|
217
289
|
## Keying Hashes (JSON Objects)
|
218
290
|
|
219
|
-
Unlike Ruby, JSON only supports string keys. It is recommended to use strings as hash keys for all JSI instances, but JSI does not enforce this, nor does it do any key conversion.
|
291
|
+
Unlike Ruby, JSON only supports string keys. It is recommended to use strings as hash keys for all JSI instances, but JSI does not enforce this, nor does it do any key conversion. You may also use [ActiveSupport::HashWithIndifferentAccess](https://api.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html) as the instance of a JSI in order to gain the benefits that offers over a plain hash. Note that activesupport is not a dependency of jsi and would be required separately for this.
|
220
292
|
|
221
293
|
## Contributing
|
222
294
|
|
@@ -224,7 +296,7 @@ Issues and pull requests are welcome on GitHub at https://github.com/notEthan/js
|
|
224
296
|
|
225
297
|
## License
|
226
298
|
|
227
|
-
[<img align="right" src="https://github.com/notEthan/jsi/raw/
|
299
|
+
[<img align="right" src="https://github.com/notEthan/jsi/raw/v0.3.0/resources/icons/AGPL-3.0.png">](https://www.gnu.org/licenses/agpl-3.0.html)
|
228
300
|
|
229
301
|
JSI is licensed under the terms of the [GNU Affero General Public License version 3](https://www.gnu.org/licenses/agpl-3.0.html).
|
230
302
|
|
data/jsi.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
lib = File.expand_path("
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
3
|
require "jsi/version"
|
4
4
|
|
@@ -12,17 +12,19 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.description = "JSI offers an Object-Oriented representation for JSON data using JSON Schemas"
|
13
13
|
spec.homepage = "https://github.com/notEthan/jsi"
|
14
14
|
spec.license = "AGPL-3.0"
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
|
16
|
+
spec.files = [
|
17
|
+
'LICENSE.md',
|
18
|
+
'CHANGELOG.md',
|
19
|
+
'README.md',
|
20
|
+
'readme.rb',
|
21
|
+
'.yardopts',
|
22
|
+
'jsi.gemspec',
|
23
|
+
*Dir['lib/**/*'],
|
24
|
+
*Dir['\\{resources\\}/schemas/**/*'],
|
25
|
+
].reject { |f| File.lstat(f).ftype == 'directory' }
|
26
|
+
|
19
27
|
spec.require_paths = ["lib"]
|
20
28
|
|
21
|
-
|
22
|
-
spec.add_dependency "json-schema", "~> 2.8"
|
23
|
-
spec.add_development_dependency "rake"
|
24
|
-
spec.add_development_dependency "minitest"
|
25
|
-
spec.add_development_dependency "minitest-around"
|
26
|
-
spec.add_development_dependency "minitest-reporters"
|
27
|
-
spec.add_development_dependency "scorpio"
|
29
|
+
spec.add_dependency "addressable", '~> 2.3'
|
28
30
|
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
module Base::Enumerable
|
5
|
+
include ::Enumerable
|
6
|
+
|
7
|
+
# an Array containing each item in this JSI.
|
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
|
18
|
+
end
|
19
|
+
ary
|
20
|
+
end
|
21
|
+
|
22
|
+
alias_method :entries, :to_a
|
23
|
+
|
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)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
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
|
38
|
+
|
39
|
+
# yields each hash key and value of this node.
|
40
|
+
#
|
41
|
+
# each yielded key is a key of the instance hash, and each yielded value is the result of {Base#[]}.
|
42
|
+
#
|
43
|
+
# returns an Enumerator if no block is given.
|
44
|
+
#
|
45
|
+
# @param kw keyword arguments are passed to {Base#[]}
|
46
|
+
# @yield [Object, Object] each key and value of this hash node
|
47
|
+
# @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
|
48
|
+
def each(**kw, &block)
|
49
|
+
return to_enum(__method__, **kw) { jsi_node_content_hash_pubsend(:size) } unless block
|
50
|
+
if block.arity > 1
|
51
|
+
jsi_node_content_hash_pubsend(:each_key) { |k| yield k, self[k, **kw] }
|
52
|
+
else
|
53
|
+
jsi_node_content_hash_pubsend(:each_key) { |k| yield [k, self[k, **kw]] }
|
54
|
+
end
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# a hash in which each key is a key of the instance hash and each value is the result of {Base#[]}
|
59
|
+
# @param kw keyword arguments are passed to {Base#[]}
|
60
|
+
# @return [Hash]
|
61
|
+
def to_hash(**kw)
|
62
|
+
{}.tap { |h| jsi_node_content_hash_pubsend(:each_key) { |k| h[k] = self[k, **kw] } }
|
63
|
+
end
|
64
|
+
|
65
|
+
include Util::Hashlike
|
66
|
+
|
67
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
68
|
+
# invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash
|
69
|
+
# @param method_name [String, Symbol]
|
70
|
+
# @param a positional arguments are passed to the invocation of method_name
|
71
|
+
# @param b block is passed to the invocation of method_name
|
72
|
+
# @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash
|
73
|
+
def jsi_node_content_hash_pubsend(method_name, *a, &b)
|
74
|
+
if jsi_node_content.respond_to?(method_name)
|
75
|
+
jsi_node_content.public_send(method_name, *a, &b)
|
76
|
+
else
|
77
|
+
jsi_node_content.to_hash.public_send(method_name, *a, &b)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
else
|
81
|
+
# invokes the method with the given name on the jsi_node_content (if defined) or its #to_hash
|
82
|
+
# @param method_name [String, Symbol]
|
83
|
+
# @param a positional arguments are passed to the invocation of method_name
|
84
|
+
# @param kw keyword arguments are passed to the invocation of method_name
|
85
|
+
# @param b block is passed to the invocation of method_name
|
86
|
+
# @return [Object] the result of calling method method_name on the jsi_node_content or its #to_hash
|
87
|
+
def jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
|
88
|
+
if jsi_node_content.respond_to?(method_name)
|
89
|
+
jsi_node_content.public_send(method_name, *a, **kw, &b)
|
90
|
+
else
|
91
|
+
jsi_node_content.to_hash.public_send(method_name, *a, **kw, &b)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
|
97
|
+
SAFE_KEY_ONLY_METHODS.each do |method_name|
|
98
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
99
|
+
define_method(method_name) do |*a, &b|
|
100
|
+
jsi_node_content_hash_pubsend(method_name, *a, &b)
|
101
|
+
end
|
102
|
+
else
|
103
|
+
define_method(method_name) do |*a, **kw, &b|
|
104
|
+
jsi_node_content_hash_pubsend(method_name, *a, **kw, &b)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# module extending a {JSI::Base} object when its instance (its {Base#jsi_node_content})
|
111
|
+
# is an Array (or responds to `#to_ary`)
|
112
|
+
module Base::ArrayNode
|
113
|
+
include Base::Enumerable
|
114
|
+
|
115
|
+
# yields each array element of this node.
|
116
|
+
#
|
117
|
+
# each yielded element is the result of {Base#[]} for each index of the instance array.
|
118
|
+
#
|
119
|
+
# returns an Enumerator if no block is given.
|
120
|
+
#
|
121
|
+
# @param kw keyword arguments are passed to {Base#[]}
|
122
|
+
# @yield [Object] each element of this array node
|
123
|
+
# @return [self, Enumerator] an Enumerator if invoked without a block; otherwise self
|
124
|
+
def each(**kw, &block)
|
125
|
+
return to_enum(__method__, **kw) { jsi_node_content_ary_pubsend(:size) } unless block
|
126
|
+
jsi_node_content_ary_pubsend(:each_index) { |i| yield(self[i, **kw]) }
|
127
|
+
self
|
128
|
+
end
|
129
|
+
|
130
|
+
# an array, the same size as the instance array, in which the element at each index is the
|
131
|
+
# result of {Base#[]}.
|
132
|
+
# @param kw keyword arguments are passed to {Base#[]}
|
133
|
+
# @return [Array]
|
134
|
+
def to_ary(**kw)
|
135
|
+
to_a(**kw)
|
136
|
+
end
|
137
|
+
|
138
|
+
include Util::Arraylike
|
139
|
+
|
140
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
141
|
+
# invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary
|
142
|
+
# @param method_name [String, Symbol]
|
143
|
+
# @param a positional arguments are passed to the invocation of method_name
|
144
|
+
# @param b block is passed to the invocation of method_name
|
145
|
+
# @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary
|
146
|
+
def jsi_node_content_ary_pubsend(method_name, *a, &b)
|
147
|
+
if jsi_node_content.respond_to?(method_name)
|
148
|
+
jsi_node_content.public_send(method_name, *a, &b)
|
149
|
+
else
|
150
|
+
jsi_node_content.to_ary.public_send(method_name, *a, &b)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
else
|
154
|
+
# invokes the method with the given name on the jsi_node_content (if defined) or its #to_ary
|
155
|
+
# @param method_name [String, Symbol]
|
156
|
+
# @param a positional arguments are passed to the invocation of method_name
|
157
|
+
# @param kw keyword arguments are passed to the invocation of method_name
|
158
|
+
# @param b block is passed to the invocation of method_name
|
159
|
+
# @return [Object] the result of calling method method_name on the jsi_node_content or its #to_ary
|
160
|
+
def jsi_node_content_ary_pubsend(method_name, *a, **kw, &b)
|
161
|
+
if jsi_node_content.respond_to?(method_name)
|
162
|
+
jsi_node_content.public_send(method_name, *a, **kw, &b)
|
163
|
+
else
|
164
|
+
jsi_node_content.to_ary.public_send(method_name, *a, **kw, &b)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
|
170
|
+
# we override these methods from Arraylike
|
171
|
+
SAFE_INDEX_ONLY_METHODS.each do |method_name|
|
172
|
+
if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
|
173
|
+
define_method(method_name) do |*a, &b|
|
174
|
+
jsi_node_content_ary_pubsend(method_name, *a, &b)
|
175
|
+
end
|
176
|
+
else
|
177
|
+
define_method(method_name) do |*a, **kw, &b|
|
178
|
+
jsi_node_content_ary_pubsend(method_name, *a, **kw, &b)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|