jsi 0.4.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +33 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +114 -42
  6. data/jsi.gemspec +14 -12
  7. data/lib/jsi/base/node.rb +183 -0
  8. data/lib/jsi/base.rb +388 -220
  9. data/lib/jsi/jsi_coder.rb +8 -7
  10. data/lib/jsi/metaschema.rb +0 -1
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
  12. data/lib/jsi/metaschema_node.rb +159 -135
  13. data/lib/jsi/ptr.rb +303 -0
  14. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  15. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  16. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  17. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  18. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  19. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  20. data/lib/jsi/schema/application/child_application.rb +38 -0
  21. data/lib/jsi/schema/application/draft04.rb +8 -0
  22. data/lib/jsi/schema/application/draft06.rb +8 -0
  23. data/lib/jsi/schema/application/draft07.rb +8 -0
  24. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  25. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  26. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  27. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  28. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  29. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  30. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  31. data/lib/jsi/schema/application/inplace_application.rb +41 -0
  32. data/lib/jsi/schema/application.rb +12 -0
  33. data/lib/jsi/schema/draft04.rb +14 -0
  34. data/lib/jsi/schema/draft06.rb +14 -0
  35. data/lib/jsi/schema/draft07.rb +14 -0
  36. data/lib/jsi/schema/issue.rb +36 -0
  37. data/lib/jsi/schema/ref.rb +160 -0
  38. data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
  39. data/lib/jsi/schema/validation/array.rb +69 -0
  40. data/lib/jsi/schema/validation/const.rb +20 -0
  41. data/lib/jsi/schema/validation/contains.rb +25 -0
  42. data/lib/jsi/schema/validation/core.rb +39 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +112 -0
  46. data/lib/jsi/schema/validation/draft06.rb +122 -0
  47. data/lib/jsi/schema/validation/draft07.rb +159 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +51 -0
  63. data/lib/jsi/schema.rb +508 -149
  64. data/lib/jsi/schema_classes.rb +199 -59
  65. data/lib/jsi/schema_registry.rb +151 -0
  66. data/lib/jsi/schema_set.rb +181 -0
  67. data/lib/jsi/simple_wrap.rb +23 -4
  68. data/lib/jsi/util/private/attr_struct.rb +127 -0
  69. data/lib/jsi/util/private.rb +204 -0
  70. data/lib/jsi/util/typelike.rb +229 -0
  71. data/lib/jsi/util.rb +89 -53
  72. data/lib/jsi/validation/error.rb +34 -0
  73. data/lib/jsi/validation/result.rb +210 -0
  74. data/lib/jsi/validation.rb +15 -0
  75. data/lib/jsi/version.rb +3 -1
  76. data/lib/jsi.rb +44 -14
  77. data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
  78. data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
  79. data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
  80. data/readme.rb +138 -0
  81. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  82. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  83. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  84. metadata +75 -122
  85. data/.simplecov +0 -3
  86. data/Rakefile.rb +0 -9
  87. data/lib/jsi/base/to_rb.rb +0 -128
  88. data/lib/jsi/json/node.rb +0 -203
  89. data/lib/jsi/json/pointer.rb +0 -419
  90. data/lib/jsi/json-schema-fragments.rb +0 -61
  91. data/lib/jsi/json.rb +0 -10
  92. data/lib/jsi/pathed_node.rb +0 -118
  93. data/lib/jsi/typelike_modules.rb +0 -240
  94. data/resources/icons/AGPL-3.0.png +0 -0
  95. data/test/base_array_test.rb +0 -323
  96. data/test/base_hash_test.rb +0 -337
  97. data/test/base_test.rb +0 -486
  98. data/test/jsi_coder_test.rb +0 -85
  99. data/test/jsi_json_arraynode_test.rb +0 -150
  100. data/test/jsi_json_hashnode_test.rb +0 -132
  101. data/test/jsi_json_node_test.rb +0 -257
  102. data/test/jsi_json_pointer_test.rb +0 -102
  103. data/test/jsi_test.rb +0 -11
  104. data/test/jsi_typelike_as_json_test.rb +0 -53
  105. data/test/metaschema_node_test.rb +0 -19
  106. data/test/schema_module_test.rb +0 -21
  107. data/test/schema_test.rb +0 -208
  108. data/test/spreedly_openapi_test.rb +0 -8
  109. data/test/test_helper.rb +0 -97
  110. data/test/util_test.rb +0 -62
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42c370e01587ddef37138ffb1aacc4cd6240de94c0bbd9d61e96cafcb6ba829d
4
- data.tar.gz: 9e26e8e2e8d78a05018302beff7190f33e0243d4f6899f5c3f1d046ccaaa3869
3
+ metadata.gz: '0659cc34b129717ffb69089f1d3a25ba3c3ec5364d49a6c0cc93dd5979ac7638'
4
+ data.tar.gz: 5b0298a9843a61e6f5693d46dd6d86021d53767638e1f3f84e23fff4b167276e
5
5
  SHA512:
6
- metadata.gz: 75c5220df82e30b9332fba96d10463ee07cc52ff60cf8dc44963dd70c2472d279180659ab782098b5bcc9066dd000e07f60b1e7fa616215bfe6e6bfa11eddcb4
7
- data.tar.gz: 848ac231235bebf262502fb7966265ab6c5e477bd6bb4acab038793dd5727a357f70482a3092e161cecc76eff4b126d4cdd7841965747fc2429ee76b5121128c
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 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,19 +1,24 @@
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/]().
8
+ To learn more about JSON Schema see [https://json-schema.org/](https://json-schema.org/).
9
9
 
10
- A JSI class aims to be a fairly unobtrusive wrapper around its instance - "instance" here meaning the JSON data, usually a Hash or Array, which instantiate the JSON Schema. JSI schema modules and classes add accessors for property names described by its schema, 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`.
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
- Using that schema, we instantiate a JSI::Schema to represent it:
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 use a YAML.load/JSON.parse/whatever; it's inlined for copypastability.
35
- contact_schema = JSI::Schema.new({"description" => "A Contact", "type" => "object", "properties" => {"name" => {"type" => "string"}, "phone" => {"type" => "array", "items" => {"type" => "object", "properties" => {"location" => {"type" => "string"}, "number" => {"type" => "string"}}}}}})
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 use a YAML.load/JSON.parse/whatever; it's inlined for copypastability.
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. Symbol keys will not match to schema properties, and so act the same as any other key not recognized from the schema.
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 - #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:
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.validate
97
+ bill.jsi_valid?
91
98
  # => true
92
99
  ```
93
100
 
94
- ... and validations on the nested schema instances (#phone here), showing in this example validation failure:
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.fully_validate
106
- # => ["The property '#/0/number' of type array did not match the following type: string in schema 594126e3"]
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; for name and nickname
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 a JSON Schema.
127
- - a "JSI class" is a subclass of `JSI::Base` representing a JSON schema.
128
- - a "JSI schema module" is a module representing a schema, included on a JSI class.
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. it has a method `#jsi_instance` which contains the underlying data.
132
- - a schema refers to a JSON schema. `JSI::Schema` is a module which extends schemas. A schema is usually a `JSI::Base` instance, and that schema JSI's schema is a metaschema (see the sections on Metaschemas below).
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
- 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.
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
- You can also add methods to a subschema using the same method `#jsi_schema_module` which we used to define the `Contact` module above.
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
- phone_schema = Contact.schema.properties['phone'].items
167
- phone_schema.jsi_schema_module.module_eval do
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
- If you want to name the module, this works:
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
- ContactPhone = Contact.schema.properties['phone'].items.jsi_schema_module
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
- Either syntax is slightly cumbersome and a better syntax is in the works.
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 schema is a metaschema.
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 schema is itself. This is instantiated in JSI as a JSI::MetaschemaNode (not a JSI::Base).
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 JSI schema. Here's an example:
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.contacts` will return an array of Contact instances, from the JSON type in the database, with Contact's accessors, validations, and user-defined instance methods.
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. It should be possible to use ActiveSupport::HashWithIndifferentAccess as the instance of a JSI in order to gain the benefits that offers over a plain hash. This is not tested behavior, but JSI should behave correctly with any instance that responds to #to_hash.
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/master/resources/icons/AGPL-3.0.png">](https://www.gnu.org/licenses/agpl-3.0.html)
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("../lib", __FILE__)
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
- ignore_files = %w(.gitignore .travis.yml Gemfile test)
16
- ignore_files_re = %r{\A(#{ignore_files.map { |f| Regexp.escape(f) }.join('|')})(/|\z)}
17
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(ignore_files_re) }
18
- spec.test_files = `git ls-files -z test`.split("\x0")
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
- # we are monkey patching json-schema with a fix that has not been merged in a timely fashion.
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