jsi 0.0.4 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +3 -1
- data/CHANGELOG.md +48 -0
- data/LICENSE.md +613 -0
- data/README.md +84 -45
- data/jsi.gemspec +11 -14
- data/lib/jsi.rb +31 -12
- data/lib/jsi/base.rb +310 -344
- data/lib/jsi/base/to_rb.rb +2 -0
- data/lib/jsi/jsi_coder.rb +91 -0
- data/lib/jsi/json-schema-fragments.rb +3 -135
- data/lib/jsi/json.rb +3 -0
- data/lib/jsi/json/node.rb +72 -197
- data/lib/jsi/json/pointer.rb +419 -0
- data/lib/jsi/metaschema.rb +7 -0
- data/lib/jsi/metaschema_node.rb +218 -0
- data/lib/jsi/pathed_node.rb +118 -0
- data/lib/jsi/schema.rb +168 -223
- data/lib/jsi/schema_classes.rb +158 -0
- data/lib/jsi/simple_wrap.rb +12 -0
- data/lib/jsi/typelike_modules.rb +71 -45
- data/lib/jsi/util.rb +47 -57
- data/lib/jsi/version.rb +1 -1
- data/lib/schemas/json-schema.org/draft-04/schema.rb +7 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +7 -0
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +210 -84
- data/test/base_hash_test.rb +201 -58
- data/test/base_test.rb +212 -121
- data/test/jsi_coder_test.rb +85 -0
- data/test/jsi_json_arraynode_test.rb +26 -25
- data/test/jsi_json_hashnode_test.rb +40 -39
- data/test/jsi_json_node_test.rb +95 -126
- data/test/jsi_json_pointer_test.rb +102 -0
- data/test/jsi_typelike_as_json_test.rb +53 -0
- data/test/metaschema_node_test.rb +19 -0
- data/test/schema_module_test.rb +21 -0
- data/test/schema_test.rb +109 -97
- data/test/spreedly_openapi_test.rb +8 -0
- data/test/test_helper.rb +42 -8
- data/test/util_test.rb +14 -14
- metadata +54 -25
- data/LICENSE.txt +0 -21
- data/lib/jsi/schema_instance_json_coder.rb +0 -83
- data/lib/jsi/struct_json_coder.rb +0 -30
- data/test/schema_instance_json_coder_test.rb +0 -121
- data/test/struct_json_coder_test.rb +0 -130
data/README.md
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
-
# JSI: JSON
|
1
|
+
# JSI: JSON Schema Instantiation
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/notEthan/jsi.svg?branch=master)](https://travis-ci.org/notEthan/jsi)
|
4
4
|
[![Coverage Status](https://coveralls.io/repos/github/notEthan/jsi/badge.svg)](https://coveralls.io/github/notEthan/jsi)
|
5
5
|
|
6
|
-
JSI
|
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
|
-
|
8
|
+
To learn more about JSON Schema see [https://json-schema.org/]().
|
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`.
|
9
11
|
|
10
12
|
## Example
|
11
13
|
|
@@ -19,28 +21,48 @@ properties:
|
|
19
21
|
phone:
|
20
22
|
type: "array"
|
21
23
|
items:
|
24
|
+
description: "A phone number"
|
22
25
|
type: "object"
|
23
26
|
properties:
|
24
27
|
location: {type: "string"}
|
25
28
|
number: {type: "string"}
|
26
29
|
```
|
27
30
|
|
28
|
-
|
31
|
+
Using that schema, we instantiate a JSI::Schema to represent it:
|
32
|
+
|
33
|
+
```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"}}}}}})
|
36
|
+
```
|
37
|
+
|
38
|
+
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.
|
29
39
|
|
30
40
|
```ruby
|
31
|
-
Contact =
|
32
|
-
|
33
|
-
|
41
|
+
Contact = contact_schema.jsi_schema_module
|
42
|
+
```
|
43
|
+
|
44
|
+
To instantiate the schema, we need some JSON data (expressed here as YAML)
|
45
|
+
|
46
|
+
```yaml
|
47
|
+
name: bill
|
48
|
+
phone:
|
49
|
+
- location: home
|
50
|
+
number: "555"
|
51
|
+
nickname: big b
|
34
52
|
```
|
35
53
|
|
36
|
-
|
54
|
+
So, if we construct an instance like:
|
37
55
|
|
38
56
|
```ruby
|
39
|
-
|
40
|
-
|
41
|
-
# #{<Contact
|
42
|
-
# "
|
43
|
-
#
|
57
|
+
# this would usually use a YAML.load/JSON.parse/whatever; it's inlined for copypastability.
|
58
|
+
bill = Contact.new_jsi({"name" => "bill", "phone" => [{"location" => "home", "number" => "555"}], "nickname" => "big b"})
|
59
|
+
# => #{<JSI (Contact)>
|
60
|
+
# "name" => "bill",
|
61
|
+
# "phone" => #[<JSI>
|
62
|
+
# #{<JSI>
|
63
|
+
# "location" => "home",
|
64
|
+
# "number" => "555"
|
65
|
+
# }
|
44
66
|
# ],
|
45
67
|
# "nickname" => "big b"
|
46
68
|
# }
|
@@ -48,8 +70,6 @@ bill = Contact.new('name' => 'bill', 'phone' => [{'location' => 'home', 'number'
|
|
48
70
|
|
49
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.
|
50
72
|
|
51
|
-
The nested classes can be seen as `JSI::SchemaClasses[schema_id]` where schema_id is a generated value.
|
52
|
-
|
53
73
|
We get accessors for the Contact:
|
54
74
|
|
55
75
|
```ruby
|
@@ -74,23 +94,25 @@ bill.validate
|
|
74
94
|
... and validations on the nested schema instances (#phone here), showing in this example validation failure:
|
75
95
|
|
76
96
|
```ruby
|
77
|
-
bad = Contact.
|
78
|
-
# => #{<Contact
|
79
|
-
# "phone" => #[<JSI
|
80
|
-
# #{<JSI
|
81
|
-
# "number" => #[<JSI
|
97
|
+
bad = Contact.new_jsi({'phone' => [{'number' => [5, 5, 5]}]})
|
98
|
+
# => #{<JSI (Contact)>
|
99
|
+
# "phone" => #[<JSI>
|
100
|
+
# #{<JSI>
|
101
|
+
# "number" => #[<JSI> 5, 5, 5]
|
82
102
|
# }
|
83
103
|
# ]
|
84
104
|
# }
|
85
105
|
bad.phone.fully_validate
|
86
|
-
# => ["The property '#/0/number' of type array did not match the following type: string in schema
|
106
|
+
# => ["The property '#/0/number' of type array did not match the following type: string in schema 594126e3"]
|
87
107
|
```
|
88
108
|
|
89
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.
|
90
110
|
|
91
|
-
Since the underlying instance is a ruby hash (json object), we can use it like a hash with
|
111
|
+
Since the underlying instance is a ruby hash (json object), we can use it like a hash with `#[]` or, say, `#transform_values`:
|
92
112
|
|
93
113
|
```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.
|
94
116
|
bill.transform_values(&:size)
|
95
117
|
# => {"name" => 4, "phone" => 1, "nickname" => 5}
|
96
118
|
bill['nickname']
|
@@ -101,21 +123,22 @@ There's plenty more JSI has to offer, but this should give you a pretty good ide
|
|
101
123
|
|
102
124
|
## Terminology and Concepts
|
103
125
|
|
104
|
-
- JSI::Base is the base class
|
105
|
-
- a JSI class
|
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.
|
106
129
|
- "instance" is a term that is significantly overloaded in this space, so documentation will attempt to be clear what kind of instance is meant:
|
107
|
-
- a schema instance refers broadly to a data structure that is described by a
|
108
|
-
- a JSI instance (or just "a JSI") is a ruby object instantiating a JSI class. it has a method
|
109
|
-
- a schema refers to a
|
130
|
+
- 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).
|
110
133
|
|
111
|
-
## JSI
|
134
|
+
## JSI and Object Oriented Programming
|
112
135
|
|
113
|
-
|
136
|
+
Instantiating your schema is a starting point. But, since the major point of object-oriented programming is applying methods to your objects, of course you want to be able to define your own methods. To do this we reopen the JSI module we defined. Referring back to the Example section above, we reopen the `Contact` module:
|
114
137
|
|
115
138
|
```ruby
|
116
|
-
|
117
|
-
def
|
118
|
-
|
139
|
+
module Contact
|
140
|
+
def phone_numbers
|
141
|
+
phone.map(&:number)
|
119
142
|
end
|
120
143
|
def name
|
121
144
|
super + ' esq.'
|
@@ -131,15 +154,17 @@ bill.name = 'rob esq.'
|
|
131
154
|
# => "rob esq."
|
132
155
|
bill['name']
|
133
156
|
# => "rob"
|
157
|
+
bill.phone_numbers
|
158
|
+
# => ["555"]
|
134
159
|
```
|
135
160
|
|
136
|
-
Note the use of `super` - you can call to accessors defined by JSI and make your accessors act as wrappers
|
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.
|
137
162
|
|
138
|
-
|
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.
|
139
164
|
|
140
165
|
```ruby
|
141
|
-
phone_schema = Contact.schema
|
142
|
-
|
166
|
+
phone_schema = Contact.schema.properties['phone'].items
|
167
|
+
phone_schema.jsi_schema_module.module_eval do
|
143
168
|
def number_with_dashes
|
144
169
|
number.split(//).join('-')
|
145
170
|
end
|
@@ -148,11 +173,11 @@ bill.phone.first.number_with_dashes
|
|
148
173
|
# => "5-5-5"
|
149
174
|
```
|
150
175
|
|
151
|
-
If you want to name the
|
176
|
+
If you want to name the module, this works:
|
152
177
|
|
153
178
|
```ruby
|
154
|
-
|
155
|
-
|
179
|
+
ContactPhone = Contact.schema.properties['phone'].items.jsi_schema_module
|
180
|
+
module ContactPhone
|
156
181
|
def number_with_dashes
|
157
182
|
number.split(//).join('-')
|
158
183
|
end
|
@@ -161,23 +186,33 @@ end
|
|
161
186
|
|
162
187
|
Either syntax is slightly cumbersome and a better syntax is in the works.
|
163
188
|
|
189
|
+
## Metaschemas
|
190
|
+
|
191
|
+
A metaschema is a schema which describes schemas. Likewise, a schema is an instance of a metaschema.
|
192
|
+
|
193
|
+
In JSI, a schema is generally a JSI::Base instance whose schema is a metaschema.
|
194
|
+
|
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).
|
196
|
+
|
164
197
|
## ActiveRecord serialization
|
165
198
|
|
166
199
|
A really excellent place to use JSI is when dealing with serialized columns in ActiveRecord.
|
167
200
|
|
168
|
-
Let's say you're sticking to
|
201
|
+
Let's say you're sticking to JSON types in the database - you have to do so if you're using JSON columns, or JSON serialization, and if you have dealt with arbitrary yaml- or marshal-serialized objects in ruby, you have probably found that approach has its shortcomings when the implementation of your classes changes.
|
169
202
|
|
170
|
-
But if your database contains
|
203
|
+
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.
|
171
204
|
|
172
|
-
JSI gives you the best of both with
|
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:
|
173
206
|
|
174
207
|
```ruby
|
175
208
|
class User < ActiveRecord::Base
|
176
|
-
serialize :
|
209
|
+
serialize :contact_info, JSI::JSICoder.new(Contact)
|
177
210
|
end
|
178
211
|
```
|
179
212
|
|
180
|
-
Now `user.contacts` will return an array of Contact instances, from the
|
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.
|
214
|
+
|
215
|
+
See the gem [`arms`](https://github.com/notEthan/arms) if you wish to serialize the dumped JSON-compatible objects further as text.
|
181
216
|
|
182
217
|
## Keying Hashes (JSON Objects)
|
183
218
|
|
@@ -189,4 +224,8 @@ Issues and pull requests are welcome on GitHub at https://github.com/notEthan/js
|
|
189
224
|
|
190
225
|
## License
|
191
226
|
|
192
|
-
|
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)
|
228
|
+
|
229
|
+
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
|
+
|
231
|
+
Unlike the MIT or BSD licenses more commonly used with Ruby gems, this license requires that if you modify JSI and propagate your changes, e.g. by including it in a web application, your modified version must be publicly available. The common path of forking on Github should satisfy this requirement.
|
data/jsi.gemspec
CHANGED
@@ -6,26 +6,23 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "jsi"
|
7
7
|
spec.version = JSI::VERSION
|
8
8
|
spec.authors = ["Ethan"]
|
9
|
-
spec.email = ["ethan@unth"]
|
9
|
+
spec.email = ["ethan.jsi@unth.net"]
|
10
10
|
|
11
|
-
spec.summary = "JSI: JSON
|
12
|
-
spec.description = "JSI
|
11
|
+
spec.summary = "JSI: JSON Schema Instantiation"
|
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
|
-
spec.license = "
|
15
|
-
ignore_files
|
16
|
-
ignore_files_re
|
17
|
-
|
18
|
-
|
19
|
-
spec.test_files = `git ls-files -z test`.split("\x0")
|
20
|
-
end
|
21
|
-
spec.bindir = "exe"
|
22
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
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")
|
23
19
|
spec.require_paths = ["lib"]
|
24
20
|
|
25
21
|
# we are monkey patching json-schema with a fix that has not been merged in a timely fashion.
|
26
22
|
spec.add_dependency "json-schema", "~> 2.8"
|
27
|
-
spec.add_development_dependency "rake"
|
28
|
-
spec.add_development_dependency "minitest"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "minitest"
|
29
25
|
spec.add_development_dependency "minitest-around"
|
30
26
|
spec.add_development_dependency "minitest-reporters"
|
27
|
+
spec.add_development_dependency "scorpio"
|
31
28
|
end
|
data/lib/jsi.rb
CHANGED
@@ -1,31 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "jsi/version"
|
2
4
|
require "pp"
|
5
|
+
require "set"
|
6
|
+
require "json"
|
7
|
+
require "pathname"
|
8
|
+
require "addressable/uri"
|
9
|
+
|
3
10
|
require "jsi/json-schema-fragments"
|
11
|
+
|
4
12
|
require "jsi/util"
|
13
|
+
require "jsi/typelike_modules"
|
5
14
|
|
6
15
|
module JSI
|
7
16
|
# generally put in code paths that are not expected to be valid control flow paths.
|
8
17
|
# rather a NotImplementedCorrectlyError. but that's too long.
|
18
|
+
#
|
19
|
+
# if you've found this class because JSI has raised this error, please open an issue with the backtrace
|
20
|
+
# and any context you can provide at https://github.com/notEthan/jsi/issues
|
9
21
|
class Bug < NotImplementedError
|
10
22
|
end
|
11
23
|
|
24
|
+
ROOT_PATH = Pathname.new(__FILE__).dirname.parent.expand_path
|
25
|
+
RESOURCES_PATH = ROOT_PATH.join('resources')
|
26
|
+
|
12
27
|
autoload :JSON, 'jsi/json'
|
28
|
+
autoload :PathedNode, 'jsi/pathed_node'
|
13
29
|
autoload :Typelike, 'jsi/typelike_modules'
|
14
30
|
autoload :Hashlike, 'jsi/typelike_modules'
|
15
31
|
autoload :Arraylike, 'jsi/typelike_modules'
|
16
32
|
autoload :Schema, 'jsi/schema'
|
17
33
|
autoload :Base, 'jsi/base'
|
18
|
-
autoload :
|
19
|
-
autoload :
|
20
|
-
autoload :SchemaClasses, 'jsi/
|
21
|
-
autoload :
|
22
|
-
|
23
|
-
autoload :
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
34
|
+
autoload :Metaschema, 'jsi/metaschema'
|
35
|
+
autoload :MetaschemaNode, 'jsi/metaschema_node'
|
36
|
+
autoload :SchemaClasses, 'jsi/schema_classes'
|
37
|
+
autoload :JSICoder, 'jsi/jsi_coder'
|
38
|
+
|
39
|
+
autoload :JSONSchemaOrgDraft04, 'schemas/json-schema.org/draft-04/schema'
|
40
|
+
autoload :JSONSchemaOrgDraft06, 'schemas/json-schema.org/draft-06/schema'
|
41
|
+
|
42
|
+
autoload :SimpleWrap, 'jsi/simple_wrap'
|
43
|
+
|
44
|
+
# @param schemas [Enumerable<JSI::Schema, #to_hash, Boolean>] schemas to represent with the class
|
45
|
+
# @return [Class subclassing JSI::Base] a JSI class which represents the given schemas.
|
46
|
+
# an instance of the class represents a JSON Schema instance described by all of the given schemas.
|
47
|
+
def self.class_for_schemas(*schemas)
|
48
|
+
SchemaClasses.class_for_schemas(*schemas)
|
30
49
|
end
|
31
50
|
end
|
data/lib/jsi/base.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
require 'jsi/typelike_modules'
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module JSI
|
5
4
|
# the base class for representing and instantiating a JSON Schema.
|
@@ -11,123 +10,302 @@ module JSI
|
|
11
10
|
# are dynamically created for schemas using {JSI.class_for_schema}, and these
|
12
11
|
# are what are used to instantiate and represent JSON schema instances.
|
13
12
|
class Base
|
14
|
-
include Memoize
|
13
|
+
include Util::Memoize
|
15
14
|
include Enumerable
|
15
|
+
include PathedNode
|
16
|
+
class CannotSubscriptError < StandardError
|
17
|
+
end
|
16
18
|
|
17
19
|
class << self
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
21
|
-
|
20
|
+
# JSI::Base.new_jsi behaves the same as .new, and is defined for compatibility so you may call #new_jsi
|
21
|
+
# on any of a JSI::Schema, a JSI::SchemaModule, or a JSI schema class.
|
22
|
+
# @return [JSI::Base] a JSI whose instance is the given instance
|
23
|
+
def new_jsi(instance, *a, &b)
|
24
|
+
new(instance, *a, &b)
|
22
25
|
end
|
23
26
|
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
else
|
31
|
-
%Q(#{name} (#{schema_id}))
|
32
|
-
end
|
27
|
+
# is the constant JSI::SchemaClasses::{self.schema_classes_const_name} defined?
|
28
|
+
# (if so, we will prefer to use something more human-readable than that ugly mess.)
|
29
|
+
def in_schema_classes
|
30
|
+
# #name sets @in_schema_classes
|
31
|
+
name
|
32
|
+
@in_schema_classes
|
33
33
|
end
|
34
34
|
|
35
|
-
# @return [String] a string representing the class
|
36
|
-
#
|
37
|
-
def
|
38
|
-
if !respond_to?(:
|
35
|
+
# @return [String] a string representing the class, indicating the schemas represented by their module
|
36
|
+
# name or a URI
|
37
|
+
def inspect
|
38
|
+
if !respond_to?(:jsi_class_schemas)
|
39
39
|
super
|
40
|
-
elsif !name || name =~ /\AJSI::SchemaClasses::/
|
41
|
-
%Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
|
42
40
|
else
|
43
|
-
|
41
|
+
schema_names = jsi_class_schemas.map do |schema|
|
42
|
+
mod = schema.jsi_schema_module
|
43
|
+
if mod.name && schema.schema_id
|
44
|
+
"#{mod.name} (#{schema.schema_id})"
|
45
|
+
elsif mod.name
|
46
|
+
mod.name
|
47
|
+
elsif schema.schema_id
|
48
|
+
schema.schema_id
|
49
|
+
else
|
50
|
+
schema.node_ptr.uri
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
if name && !in_schema_classes
|
55
|
+
if jsi_class_schemas.empty?
|
56
|
+
"#{name} (0 schemas)"
|
57
|
+
else
|
58
|
+
"#{name} (#{schema_names.join(', ')})"
|
59
|
+
end
|
60
|
+
else
|
61
|
+
if schema_names.empty?
|
62
|
+
"(JSI Schema Class for 0 schemas)"
|
63
|
+
else
|
64
|
+
"(JSI Schema Class: #{schema_names.join(', ')})"
|
65
|
+
end
|
66
|
+
end
|
44
67
|
end
|
45
68
|
end
|
46
69
|
|
47
|
-
|
48
|
-
|
70
|
+
alias_method :to_s, :inspect
|
71
|
+
|
72
|
+
# @return [String, nil] a name for a constant for this class, generated from the constant name
|
73
|
+
# or schema id of each schema this class represents. nil if any represented schema has no constant
|
74
|
+
# name or schema id.
|
49
75
|
def schema_classes_const_name
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
76
|
+
if respond_to?(:jsi_class_schemas)
|
77
|
+
schema_names = jsi_class_schemas.map do |schema|
|
78
|
+
if schema.jsi_schema_module.name
|
79
|
+
schema.jsi_schema_module.name
|
80
|
+
elsif schema.schema_id
|
81
|
+
schema.schema_id
|
82
|
+
else
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
if !schema_names.any?(&:nil?) && !schema_names.empty?
|
87
|
+
schema_names.sort.map { |n| 'X' + n.gsub(/[^\w]/, '_') }.join('')
|
88
|
+
end
|
89
|
+
end
|
54
90
|
end
|
55
91
|
|
56
92
|
# @return [String] a constant name of this class
|
57
93
|
def name
|
58
|
-
unless
|
59
|
-
|
94
|
+
unless instance_variable_defined?(:@in_schema_classes)
|
95
|
+
const_name = schema_classes_const_name
|
96
|
+
if super || !const_name || SchemaClasses.const_defined?(const_name)
|
97
|
+
@in_schema_classes = false
|
98
|
+
else
|
99
|
+
SchemaClasses.const_set(const_name, self)
|
100
|
+
@in_schema_classes = true
|
101
|
+
end
|
60
102
|
end
|
61
103
|
super
|
62
104
|
end
|
63
105
|
end
|
64
106
|
|
65
|
-
#
|
66
|
-
#
|
67
|
-
|
107
|
+
# NOINSTANCE is a magic value passed to #initialize when instantiating a JSI
|
108
|
+
# from a document and JSON Pointer.
|
109
|
+
NOINSTANCE = Object.new.tap { |o| [:inspect, :to_s].each(&(-> (s, m) { o.define_singleton_method(m) { s } }.curry.([JSI::Base.name, 'NOINSTANCE'].join('::')))) }
|
110
|
+
|
111
|
+
# initializes this JSI from the given instance - instance is most commonly
|
112
|
+
# a parsed JSON document consisting of Hash, Array, or sometimes a basic
|
113
|
+
# type, but this is in no way enforced and a JSI may wrap any object.
|
68
114
|
#
|
69
115
|
# @param instance [Object] the JSON Schema instance being represented
|
70
|
-
# @param
|
71
|
-
#
|
72
|
-
|
73
|
-
|
74
|
-
|
116
|
+
# @param jsi_document [Object] for internal use. the instance may be specified as a
|
117
|
+
# node in the `jsi_document` param, pointed to by `jsi_ptr`. the param `instance`
|
118
|
+
# MUST be `NOINSTANCE` to use the jsi_document + jsi_ptr form. `jsi_document` MUST
|
119
|
+
# NOT be passed if `instance` is anything other than `NOINSTANCE`.
|
120
|
+
# @param jsi_ptr [JSI::JSON::Pointer] for internal use. a JSON pointer specifying
|
121
|
+
# the path of this instance in the `jsi_document` param. `jsi_ptr` must be passed
|
122
|
+
# iff `jsi_document` is passed, i.e. when `instance` is `NOINSTANCE`
|
123
|
+
# @param jsi_root_node [JSI::Base] for internal use, specifies the JSI at the root of the document
|
124
|
+
def initialize(instance, jsi_document: nil, jsi_ptr: nil, jsi_root_node: nil)
|
125
|
+
unless respond_to?(:jsi_schemas)
|
126
|
+
raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #jsi_schemas. it is recommended to instantiate JSIs from a schema using JSI::Schema#new_jsi.")
|
127
|
+
end
|
128
|
+
|
129
|
+
if instance.is_a?(JSI::Schema)
|
130
|
+
raise(TypeError, "assigning a schema to a #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
|
131
|
+
elsif instance.is_a?(JSI::Base)
|
132
|
+
raise(TypeError, "assigning another JSI::Base instance to a #{self.class.inspect} instance is incorrect. received: #{instance.pretty_inspect.chomp}")
|
133
|
+
end
|
134
|
+
|
135
|
+
if instance == NOINSTANCE
|
136
|
+
@jsi_document = jsi_document
|
137
|
+
unless jsi_ptr.is_a?(JSI::JSON::Pointer)
|
138
|
+
raise(TypeError, "jsi_ptr must be a JSI::JSON::Pointer; got: #{jsi_ptr.inspect}")
|
139
|
+
end
|
140
|
+
@jsi_ptr = jsi_ptr
|
141
|
+
if @jsi_ptr.root?
|
142
|
+
raise(Bug, "jsi_root_node cannot be specified for root JSI") if jsi_root_node
|
143
|
+
@jsi_root_node = self
|
144
|
+
else
|
145
|
+
if !jsi_root_node.is_a?(JSI::Base)
|
146
|
+
raise(TypeError, "jsi_root_node must be a JSI::Base; got: #{jsi_root_node.inspect}")
|
147
|
+
end
|
148
|
+
if !jsi_root_node.jsi_ptr.root?
|
149
|
+
raise(Bug, "jsi_root_node ptr #{jsi_root_node.jsi_ptr.inspect} is not root")
|
150
|
+
end
|
151
|
+
@jsi_root_node = jsi_root_node
|
152
|
+
end
|
153
|
+
else
|
154
|
+
raise(Bug, 'incorrect usage') if jsi_document || jsi_ptr || jsi_root_node
|
155
|
+
@jsi_document = instance
|
156
|
+
@jsi_ptr = JSI::JSON::Pointer[]
|
157
|
+
@jsi_root_node = self
|
75
158
|
end
|
76
159
|
|
77
|
-
|
78
|
-
|
160
|
+
if self.jsi_instance.respond_to?(:to_hash)
|
161
|
+
extend PathedHashNode
|
162
|
+
elsif self.jsi_instance.respond_to?(:to_ary)
|
163
|
+
extend PathedArrayNode
|
164
|
+
end
|
79
165
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
166
|
+
jsi_schemas.each do |schema|
|
167
|
+
if schema.describes_schema?
|
168
|
+
extend JSI::Schema
|
169
|
+
end
|
84
170
|
end
|
85
171
|
end
|
86
172
|
|
87
|
-
# the instance of
|
88
|
-
attr_reader :
|
173
|
+
# document containing the instance of this JSI
|
174
|
+
attr_reader :jsi_document
|
175
|
+
|
176
|
+
# JSI::JSON::Pointer pointing to this JSI's instance within the jsi_document
|
177
|
+
attr_reader :jsi_ptr
|
178
|
+
|
179
|
+
# the JSI at the root of this JSI's document
|
180
|
+
attr_reader :jsi_root_node
|
89
181
|
|
90
|
-
|
91
|
-
|
182
|
+
alias_method :node_document, :jsi_document
|
183
|
+
alias_method :node_ptr, :jsi_ptr
|
184
|
+
alias_method :document_root_node, :jsi_root_node
|
92
185
|
|
93
|
-
#
|
186
|
+
# the instance of the json-schema - the underlying JSON data used to instantiate this JSI
|
187
|
+
alias_method :jsi_instance, :node_content
|
188
|
+
alias_method :instance, :node_content
|
189
|
+
|
190
|
+
# each is overridden by PathedHashNode or PathedArrayNode when appropriate. the base
|
94
191
|
# #each is not actually implemented, along with all the methods of Enumerable.
|
95
192
|
def each
|
96
|
-
raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{
|
193
|
+
raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{jsi_instance.pretty_inspect.chomp}"
|
97
194
|
end
|
98
195
|
|
99
|
-
# an array of JSI instances above this one in the document.
|
100
|
-
# JSI is at the root or was instantiated from a source that does not have
|
101
|
-
# a document (e.g. a plain hash or array).
|
196
|
+
# an array of JSI instances above this one in the document.
|
102
197
|
#
|
103
198
|
# @return [Array<JSI::Base>]
|
104
|
-
def
|
105
|
-
parent =
|
106
|
-
|
199
|
+
def parent_jsis
|
200
|
+
parent = jsi_root_node
|
201
|
+
|
202
|
+
jsi_ptr.reference_tokens.map do |token|
|
107
203
|
parent.tap do
|
108
|
-
parent = parent[
|
204
|
+
parent = parent[token]
|
109
205
|
end
|
110
206
|
end.reverse
|
111
207
|
end
|
112
208
|
|
113
|
-
# the immediate parent of this JSI. nil if no parent
|
209
|
+
# the immediate parent of this JSI. nil if there is no parent.
|
114
210
|
#
|
115
211
|
# @return [JSI::Base, nil]
|
116
|
-
def
|
117
|
-
|
212
|
+
def parent_jsi
|
213
|
+
parent_jsis.first
|
214
|
+
end
|
215
|
+
|
216
|
+
alias_method :parent_node, :parent_jsi
|
217
|
+
|
218
|
+
# @deprecated
|
219
|
+
alias_method :parents, :parent_jsis
|
220
|
+
# @deprecated
|
221
|
+
alias_method :parent, :parent_jsi
|
222
|
+
|
223
|
+
# @param token [String, Integer, Object] the token to subscript
|
224
|
+
# @return [JSI::Base, Object] the instance's subscript value at the given token.
|
225
|
+
# if this JSI's schemas define subschemas which apply for the given token, and the value is complex,
|
226
|
+
# returns the subscript value as a JSI instantiation of those subschemas. otherwise, the plain instance
|
227
|
+
# value is returned.
|
228
|
+
def [](token)
|
229
|
+
if respond_to?(:to_hash)
|
230
|
+
token_in_range = node_content_hash_pubsend(:key?, token)
|
231
|
+
value = node_content_hash_pubsend(:[], token)
|
232
|
+
elsif respond_to?(:to_ary)
|
233
|
+
token_in_range = node_content_ary_pubsend(:each_index).include?(token)
|
234
|
+
value = node_content_ary_pubsend(:[], token)
|
235
|
+
else
|
236
|
+
raise(CannotSubscriptError, "cannot subcript (using token: #{token.inspect}) from instance: #{jsi_instance.pretty_inspect.chomp}")
|
237
|
+
end
|
238
|
+
|
239
|
+
result = jsi_memoize(:[], token, value, token_in_range) do |token, value, token_in_range|
|
240
|
+
if respond_to?(:to_ary)
|
241
|
+
token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_index(token) }.inject(Set.new, &:|)
|
242
|
+
else
|
243
|
+
token_schemas = jsi_schemas.map { |schema| schema.subschemas_for_property_name(token) }.inject(Set.new, &:|)
|
244
|
+
end
|
245
|
+
token_schemas = token_schemas.map { |schema| schema.match_to_instance(value) }.inject(Set.new, &:|)
|
246
|
+
|
247
|
+
if token_in_range
|
248
|
+
complex_value = token_schemas.any? && (value.respond_to?(:to_hash) || value.respond_to?(:to_ary))
|
249
|
+
schema_value = token_schemas.any? { |token_schema| token_schema.describes_schema? }
|
250
|
+
|
251
|
+
if complex_value || schema_value
|
252
|
+
JSI::SchemaClasses.class_for_schemas(token_schemas).new(Base::NOINSTANCE, jsi_document: @jsi_document, jsi_ptr: @jsi_ptr[token], jsi_root_node: @jsi_root_node)
|
253
|
+
else
|
254
|
+
value
|
255
|
+
end
|
256
|
+
else
|
257
|
+
defaults = Set.new
|
258
|
+
token_schemas.each do |token_schema|
|
259
|
+
if token_schema.respond_to?(:to_hash) && token_schema.key?('default')
|
260
|
+
defaults << token_schema['default']
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
if defaults.size == 1
|
265
|
+
# use the default value
|
266
|
+
# we are using #dup so that we get a modified copy of self, in which we set dup[token]=default.
|
267
|
+
dup.tap { |o| o[token] = defaults.first }[token]
|
268
|
+
else
|
269
|
+
# I kind of want to just return nil here. the preferred mechanism for
|
270
|
+
# a JSI's default value should be its schema. but returning nil ignores
|
271
|
+
# any value returned by Hash#default/#default_proc. there's no compelling
|
272
|
+
# reason not to support both, so I'll return that.
|
273
|
+
value
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
result
|
278
|
+
end
|
279
|
+
|
280
|
+
# assigns the subscript of the instance identified by the given token to the given value.
|
281
|
+
# if the value is a JSI, its instance is assigned instead of the JSI value itself.
|
282
|
+
#
|
283
|
+
# @param token [String, Integer, Object] token identifying the subscript to assign
|
284
|
+
# @param value [JSI::Base, Object] the value to be assigned
|
285
|
+
def []=(token, value)
|
286
|
+
unless respond_to?(:to_hash) || respond_to?(:to_ary)
|
287
|
+
raise(NoMethodError, "cannot assign subcript (using token: #{token.inspect}) to instance: #{jsi_instance.pretty_inspect.chomp}")
|
288
|
+
end
|
289
|
+
jsi_clear_memo(:[])
|
290
|
+
if value.is_a?(Base)
|
291
|
+
self[token] = value.jsi_instance
|
292
|
+
else
|
293
|
+
jsi_instance[token] = value
|
294
|
+
end
|
118
295
|
end
|
119
296
|
|
120
297
|
# if this JSI is a $ref then the $ref is followed. otherwise this JSI
|
121
298
|
# is returned.
|
122
299
|
#
|
300
|
+
# @yield [JSI::Base] if a block is given (optional), this will yield a deref'd JSI. if this
|
301
|
+
# JSI is not a $ref object, the block is not called. if we are a $ref which cannot be followed
|
302
|
+
# (e.g. a $ref to an external document, which is not yet supported), the block is not called.
|
123
303
|
# @return [JSI::Base, self]
|
124
|
-
def deref
|
125
|
-
|
126
|
-
|
127
|
-
self
|
128
|
-
else
|
129
|
-
self.class.new(derefed, ancestor: @ancestor)
|
304
|
+
def deref(&block)
|
305
|
+
node_ptr_deref do |deref_ptr|
|
306
|
+
deref_ptr.evaluate(jsi_root_node).tap(&(block || Util::NOOP))
|
130
307
|
end
|
308
|
+
return self
|
131
309
|
end
|
132
310
|
|
133
311
|
# yields the content of the underlying instance. the block must result in
|
@@ -138,22 +316,25 @@ module JSI
|
|
138
316
|
# in a (nondestructively) modified copy of this.
|
139
317
|
# @return [JSI::Base subclass the same as self] the modified copy of self
|
140
318
|
def modified_copy(&block)
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
319
|
+
if node_ptr.root?
|
320
|
+
modified_document = @jsi_ptr.modified_document_copy(@jsi_document, &block)
|
321
|
+
self.class.new(Base::NOINSTANCE, jsi_document: modified_document, jsi_ptr: @jsi_ptr)
|
322
|
+
else
|
323
|
+
modified_jsi_root_node = @jsi_root_node.modified_copy do |root|
|
324
|
+
@jsi_ptr.modified_document_copy(root, &block)
|
325
|
+
end
|
326
|
+
self.class.new(Base::NOINSTANCE, jsi_document: modified_jsi_root_node.node_document, jsi_ptr: @jsi_ptr, jsi_root_node: modified_jsi_root_node)
|
327
|
+
end
|
147
328
|
end
|
148
329
|
|
149
|
-
# @return [Array
|
150
|
-
def fully_validate
|
151
|
-
schema.
|
330
|
+
# @return [Array] array of schema validation errors for this instance
|
331
|
+
def fully_validate(errors_as_objects: false)
|
332
|
+
jsi_schemas.map { |schema| schema.fully_validate_instance(jsi_instance, errors_as_objects: errors_as_objects) }.inject([], &:+)
|
152
333
|
end
|
153
334
|
|
154
335
|
# @return [true, false] whether the instance validates against its schema
|
155
336
|
def validate
|
156
|
-
schema.
|
337
|
+
jsi_schemas.all? { |schema| schema.validate_instance(jsi_instance) }
|
157
338
|
end
|
158
339
|
|
159
340
|
# @return [true] if this method does not raise, it returns true to
|
@@ -161,299 +342,84 @@ module JSI
|
|
161
342
|
# @raise [::JSON::Schema::ValidationError] raises if the instance has
|
162
343
|
# validation errors
|
163
344
|
def validate!
|
164
|
-
schema.
|
345
|
+
jsi_schemas.each { |schema| schema.validate_instance!(jsi_instance) }
|
346
|
+
true
|
347
|
+
end
|
348
|
+
|
349
|
+
def dup
|
350
|
+
modified_copy(&:dup)
|
165
351
|
end
|
166
352
|
|
167
353
|
# @return [String] a string representing this JSI, indicating its class
|
168
354
|
# and inspecting its instance
|
169
355
|
def inspect
|
170
|
-
"\#<#{
|
356
|
+
"\#<#{object_group_text.join(' ')} #{jsi_instance.inspect}>"
|
171
357
|
end
|
172
358
|
|
173
359
|
# pretty-prints a representation this JSI to the given printer
|
174
360
|
# @return [void]
|
175
361
|
def pretty_print(q)
|
176
|
-
q.
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
}
|
362
|
+
q.text '#<'
|
363
|
+
q.text object_group_text.join(' ')
|
364
|
+
q.group_sub {
|
365
|
+
q.nest(2) {
|
366
|
+
q.breakable ' '
|
367
|
+
q.pp jsi_instance
|
183
368
|
}
|
184
|
-
|
185
|
-
|
186
|
-
|
369
|
+
}
|
370
|
+
q.breakable ''
|
371
|
+
q.text '>'
|
187
372
|
end
|
188
373
|
|
189
|
-
# @return [String]
|
374
|
+
# @return [Array<String>]
|
190
375
|
def object_group_text
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
def fingerprint
|
201
|
-
{class: self.class, instance: instance}
|
202
|
-
end
|
203
|
-
include FingerprintHash
|
204
|
-
|
205
|
-
private
|
206
|
-
|
207
|
-
# assigns @instance to the given thing, raising if the thing is not appropriate for a JSI instance
|
208
|
-
# @param thing [Object] a JSON schema instance for this class's schema
|
209
|
-
def instance=(thing)
|
210
|
-
if instance_variable_defined?(:@instance)
|
211
|
-
raise(JSI::Bug, "overwriting instance is not supported")
|
212
|
-
end
|
213
|
-
if thing.is_a?(Base)
|
214
|
-
warn "assigning instance to a Base instance is incorrect. received: #{thing.pretty_inspect.chomp}"
|
215
|
-
@instance = thing.instance
|
216
|
-
elsif thing.is_a?(JSI::JSON::Node)
|
217
|
-
@instance = thing
|
218
|
-
else
|
219
|
-
@instance = JSI::JSON::Node.new_doc(thing)
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
# assigns a subscript, taking care of memoization and unwrapping a JSI if given.
|
224
|
-
# @param subscript [Object] the bit between the [ and ]
|
225
|
-
# @param value [JSI::Base, Object] the value to be assigned
|
226
|
-
def subscript_assign(subscript, value)
|
227
|
-
clear_memo(:[], subscript)
|
228
|
-
if value.is_a?(Base)
|
229
|
-
instance[subscript] = value.instance
|
230
|
-
else
|
231
|
-
instance[subscript] = value
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
# this is an instance method in order to allow subclasses of JSI classes to
|
236
|
-
# override it to point to other subclasses corresponding to other schemas.
|
237
|
-
def class_for_schema(schema)
|
238
|
-
JSI.class_for_schema(schema)
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
# this module is just a namespace for schema classes.
|
243
|
-
module SchemaClasses
|
244
|
-
extend Memoize
|
245
|
-
|
246
|
-
# JSI::SchemaClasses[schema_id] returns a class for the schema with the
|
247
|
-
# given id, the same class as returned from JSI.class_for_schema.
|
248
|
-
#
|
249
|
-
# @param schema_id [String] absolute schema id as returned by {JSI::Schema#schema_id}
|
250
|
-
# @return [Class subclassing JSI::Base] the class for that schema
|
251
|
-
def self.[](schema_id)
|
252
|
-
@classes_by_id[schema_id]
|
253
|
-
end
|
254
|
-
@classes_by_id = {}
|
255
|
-
end
|
256
|
-
|
257
|
-
# see {JSI.class_for_schema}
|
258
|
-
def SchemaClasses.class_for_schema(schema_object)
|
259
|
-
memoize(:class_for_schema, JSI::Schema.from_object(schema_object)) do |schema_|
|
260
|
-
begin
|
261
|
-
begin
|
262
|
-
Class.new(Base).instance_exec(schema_) do |schema|
|
263
|
-
begin
|
264
|
-
include(JSI::SchemaClasses.module_for_schema(schema))
|
265
|
-
|
266
|
-
SchemaClasses.instance_exec(self) { |klass| @classes_by_id[klass.schema_id] = klass }
|
267
|
-
|
268
|
-
self
|
269
|
-
end
|
270
|
-
end
|
271
|
-
end
|
272
|
-
end
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
# a module for the given schema, with accessor methods for any object
|
277
|
-
# property names the schema identifies. also has class and instance
|
278
|
-
# methods called #schema to access the {JSI::Schema} this module
|
279
|
-
# represents.
|
280
|
-
#
|
281
|
-
# accessor methods are defined on these modules so that methods can be
|
282
|
-
# defined on {JSI.class_for_schema} classes without method redefinition
|
283
|
-
# warnings. additionally, these overriding instance methods can call
|
284
|
-
# `super` to invoke the normal accessor behavior.
|
285
|
-
#
|
286
|
-
# no property names that are the same as existing method names on the JSI
|
287
|
-
# class will be defined. users should use #[] and #[]= to access properties
|
288
|
-
# whose names conflict with existing methods.
|
289
|
-
def SchemaClasses.module_for_schema(schema_object)
|
290
|
-
memoize(:module_for_schema, JSI::Schema.from_object(schema_object)) do |schema_|
|
291
|
-
Module.new.tap do |m|
|
292
|
-
m.instance_exec(schema_) do |schema|
|
293
|
-
define_method(:schema) { schema }
|
294
|
-
define_singleton_method(:schema) { schema }
|
295
|
-
define_singleton_method(:included) do |includer|
|
296
|
-
includer.send(:define_singleton_method, :schema) { schema }
|
297
|
-
end
|
298
|
-
|
299
|
-
define_singleton_method(:schema_id) do
|
300
|
-
schema.schema_id
|
301
|
-
end
|
302
|
-
define_singleton_method(:inspect) do
|
303
|
-
%Q(#<Module for Schema: #{schema_id}>)
|
376
|
+
class_name = self.class.name unless self.class.in_schema_classes
|
377
|
+
class_txt = begin
|
378
|
+
if class_name
|
379
|
+
# ignore ID
|
380
|
+
schema_module_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name }.compact
|
381
|
+
if schema_module_names.empty?
|
382
|
+
class_name
|
383
|
+
else
|
384
|
+
"#{class_name} (#{schema_module_names.join(', ')})"
|
304
385
|
end
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
accessors_to_define.each do |property_name|
|
312
|
-
define_method(property_name) do
|
313
|
-
if respond_to?(:[])
|
314
|
-
self[property_name]
|
315
|
-
else
|
316
|
-
raise(NoMethodError, "instance does not respond to []; cannot call reader `#{property_name}' for: #{pretty_inspect.chomp}")
|
317
|
-
end
|
318
|
-
end
|
319
|
-
define_method("#{property_name}=") do |value|
|
320
|
-
if respond_to?(:[]=)
|
321
|
-
self[property_name] = value
|
322
|
-
else
|
323
|
-
raise(NoMethodError, "instance does not respond to []=; cannot call writer `#{property_name}=' for: #{pretty_inspect.chomp}")
|
324
|
-
end
|
325
|
-
end
|
386
|
+
else
|
387
|
+
schema_names = jsi_schemas.map { |schema| schema.jsi_schema_module.name || schema.schema_id }.compact
|
388
|
+
if schema_names.empty?
|
389
|
+
"JSI"
|
390
|
+
else
|
391
|
+
"JSI (#{schema_names.join(', ')})"
|
326
392
|
end
|
327
393
|
end
|
328
394
|
end
|
329
|
-
end
|
330
|
-
end
|
331
|
-
|
332
|
-
# module extending a {JSI::Base} object when its schema instance is Hash-like (responds to #to_hash)
|
333
|
-
module BaseHash
|
334
|
-
# yields each key and value of this JSI.
|
335
|
-
# each yielded key is the same as a key of the instance, and each yielded
|
336
|
-
# value is the result of self[key] (see #[]).
|
337
|
-
# returns an Enumerator if no block is given.
|
338
|
-
# @yield [Object, Object] each key and value of this JSI hash
|
339
|
-
# @return [self, Enumerator]
|
340
|
-
def each
|
341
|
-
return to_enum(__method__) { instance.size } unless block_given?
|
342
|
-
instance.each_key { |k| yield(k, self[k]) }
|
343
|
-
self
|
344
|
-
end
|
345
|
-
|
346
|
-
# @return [Hash] a hash in which each key is a key of the instance hash and
|
347
|
-
# each value is the result of self[key] (see #[]).
|
348
|
-
def to_hash
|
349
|
-
inject({}) { |h, (k, v)| h[k] = v; h }
|
350
|
-
end
|
351
|
-
|
352
|
-
include Hashlike
|
353
395
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
# @return [JSI::Base, Object] the instance's subscript value at the given
|
360
|
-
# key property_name_. if there is a subschema defined for that property
|
361
|
-
# on this JSI's schema, returns the instance's subscript as a JSI
|
362
|
-
# instiation of that subschema.
|
363
|
-
def [](property_name_)
|
364
|
-
memoize(:[], property_name_) do |property_name|
|
365
|
-
begin
|
366
|
-
property_schema = schema.subschema_for_property(property_name)
|
367
|
-
property_schema = property_schema && property_schema.match_to_instance(instance[property_name])
|
368
|
-
|
369
|
-
if !instance.key?(property_name) && property_schema && property_schema.schema_object.key?('default')
|
370
|
-
# use the default value
|
371
|
-
default = property_schema.schema_object['default']
|
372
|
-
if default.respond_to?(:to_hash) || default.respond_to?(:to_ary)
|
373
|
-
class_for_schema(property_schema).new(default, ancestor: @ancestor)
|
374
|
-
else
|
375
|
-
default
|
376
|
-
end
|
377
|
-
elsif property_schema && (instance[property_name].respond_to?(:to_hash) || instance[property_name].respond_to?(:to_ary))
|
378
|
-
class_for_schema(property_schema).new(instance[property_name], ancestor: @ancestor)
|
379
|
-
else
|
380
|
-
instance[property_name]
|
381
|
-
end
|
396
|
+
if (is_a?(PathedArrayNode) || is_a?(PathedHashNode)) && ![Array, Hash].include?(node_content.class)
|
397
|
+
if node_content.respond_to?(:object_group_text)
|
398
|
+
node_content_txt = node_content.object_group_text
|
399
|
+
else
|
400
|
+
node_content_txt = [node_content.class.to_s]
|
382
401
|
end
|
402
|
+
else
|
403
|
+
node_content_txt = []
|
383
404
|
end
|
384
|
-
end
|
385
|
-
|
386
|
-
# assigns the given property name of the instance to the given value.
|
387
|
-
# if the value is a JSI, its instance is assigned.
|
388
|
-
# @param property_name [Object] this should generally be a String, but JSI
|
389
|
-
# does not enforce any constraint on it.
|
390
|
-
# @param value [Object] the value to be assigned to the given subscript
|
391
|
-
# property_name
|
392
|
-
def []=(property_name, value)
|
393
|
-
subscript_assign(property_name, value)
|
394
|
-
end
|
395
|
-
end
|
396
|
-
|
397
|
-
# module extending a {JSI::Base} object when its schema instance is Array-like (responds to #to_ary)
|
398
|
-
module BaseArray
|
399
|
-
# yields each element. each yielded element is the result of self[index]
|
400
|
-
# for each index of the instance (see #[]).
|
401
|
-
# returns an Enumerator if no block is given.
|
402
|
-
# @yield [Object] each element of this JSI array
|
403
|
-
# @return [self, Enumerator]
|
404
|
-
def each
|
405
|
-
return to_enum(__method__) { instance.size } unless block_given?
|
406
|
-
instance.each_index { |i| yield(self[i]) }
|
407
|
-
self
|
408
|
-
end
|
409
|
-
|
410
|
-
# @return [Array] an array, the same size as the instance, in which the
|
411
|
-
# element at each index is the result of self[index] (see #[])
|
412
|
-
def to_ary
|
413
|
-
to_a
|
414
|
-
end
|
415
405
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
|
406
|
+
[
|
407
|
+
class_txt,
|
408
|
+
is_a?(Metaschema) ? "Metaschema" : is_a?(Schema) ? "Schema" : nil,
|
409
|
+
*node_content_txt,
|
410
|
+
].compact
|
422
411
|
end
|
423
412
|
|
424
|
-
# @return [Object]
|
425
|
-
|
426
|
-
|
427
|
-
# @param i_ the array index to subscript
|
428
|
-
def [](i_)
|
429
|
-
memoize(:[], i_) do |i|
|
430
|
-
begin
|
431
|
-
index_schema = schema.subschema_for_index(i)
|
432
|
-
index_schema = index_schema && index_schema.match_to_instance(instance[i])
|
433
|
-
|
434
|
-
if !instance.each_index.to_a.include?(i) && index_schema && index_schema.schema_object.key?('default')
|
435
|
-
# use the default value
|
436
|
-
default = index_schema.schema_object['default']
|
437
|
-
if default.respond_to?(:to_hash) || default.respond_to?(:to_ary)
|
438
|
-
class_for_schema(index_schema).new(default, ancestor: @ancestor)
|
439
|
-
else
|
440
|
-
default
|
441
|
-
end
|
442
|
-
elsif index_schema && (instance[i].respond_to?(:to_hash) || instance[i].respond_to?(:to_ary))
|
443
|
-
class_for_schema(index_schema).new(instance[i], ancestor: @ancestor)
|
444
|
-
else
|
445
|
-
instance[i]
|
446
|
-
end
|
447
|
-
end
|
448
|
-
end
|
413
|
+
# @return [Object] a jsonifiable representation of the instance
|
414
|
+
def as_json(*opt)
|
415
|
+
Typelike.as_json(jsi_instance, *opt)
|
449
416
|
end
|
450
417
|
|
451
|
-
#
|
452
|
-
# if the
|
453
|
-
|
454
|
-
|
455
|
-
def []=(i, value)
|
456
|
-
subscript_assign(i, value)
|
418
|
+
# @return [Object] an opaque fingerprint of this JSI for FingerprintHash. JSIs are equal
|
419
|
+
# if their instances are equal, and if the JSIs are of the same JSI class or subclass.
|
420
|
+
def jsi_fingerprint
|
421
|
+
{class: jsi_class, jsi_document: jsi_document, jsi_ptr: jsi_ptr}
|
457
422
|
end
|
423
|
+
include Util::FingerprintHash
|
458
424
|
end
|
459
425
|
end
|