jsi 0.0.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +3 -1
  3. data/CHANGELOG.md +48 -0
  4. data/LICENSE.md +613 -0
  5. data/README.md +84 -45
  6. data/jsi.gemspec +11 -14
  7. data/lib/jsi.rb +31 -12
  8. data/lib/jsi/base.rb +310 -344
  9. data/lib/jsi/base/to_rb.rb +2 -0
  10. data/lib/jsi/jsi_coder.rb +91 -0
  11. data/lib/jsi/json-schema-fragments.rb +3 -135
  12. data/lib/jsi/json.rb +3 -0
  13. data/lib/jsi/json/node.rb +72 -197
  14. data/lib/jsi/json/pointer.rb +419 -0
  15. data/lib/jsi/metaschema.rb +7 -0
  16. data/lib/jsi/metaschema_node.rb +218 -0
  17. data/lib/jsi/pathed_node.rb +118 -0
  18. data/lib/jsi/schema.rb +168 -223
  19. data/lib/jsi/schema_classes.rb +158 -0
  20. data/lib/jsi/simple_wrap.rb +12 -0
  21. data/lib/jsi/typelike_modules.rb +71 -45
  22. data/lib/jsi/util.rb +47 -57
  23. data/lib/jsi/version.rb +1 -1
  24. data/lib/schemas/json-schema.org/draft-04/schema.rb +7 -0
  25. data/lib/schemas/json-schema.org/draft-06/schema.rb +7 -0
  26. data/resources/icons/AGPL-3.0.png +0 -0
  27. data/test/base_array_test.rb +210 -84
  28. data/test/base_hash_test.rb +201 -58
  29. data/test/base_test.rb +212 -121
  30. data/test/jsi_coder_test.rb +85 -0
  31. data/test/jsi_json_arraynode_test.rb +26 -25
  32. data/test/jsi_json_hashnode_test.rb +40 -39
  33. data/test/jsi_json_node_test.rb +95 -126
  34. data/test/jsi_json_pointer_test.rb +102 -0
  35. data/test/jsi_typelike_as_json_test.rb +53 -0
  36. data/test/metaschema_node_test.rb +19 -0
  37. data/test/schema_module_test.rb +21 -0
  38. data/test/schema_test.rb +109 -97
  39. data/test/spreedly_openapi_test.rb +8 -0
  40. data/test/test_helper.rb +42 -8
  41. data/test/util_test.rb +14 -14
  42. metadata +54 -25
  43. data/LICENSE.txt +0 -21
  44. data/lib/jsi/schema_instance_json_coder.rb +0 -83
  45. data/lib/jsi/struct_json_coder.rb +0 -30
  46. data/test/schema_instance_json_coder_test.rb +0 -121
  47. data/test/struct_json_coder_test.rb +0 -130
data/README.md CHANGED
@@ -1,11 +1,13 @@
1
- # JSI: JSON-Schema Instantiation
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 represents JSON-schemas as ruby classes, and schema instances as instances of those classes.
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
- A JSI class aims to be a fairly unobtrusive wrapper around its instance. It adds accessors for known property names, validation methods, and a few 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.
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
- And here's the class for that schema from JSI:
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 = JSI.class_for_schema(YAML.load_file('contact.schema.yml'))
32
- # you can copy/paste this line instead, to follow along in irb:
33
- Contact = JSI.class_for_schema({"description" => "A Contact", "type" => "object", "properties" => {"name" => {"type" => "string"}, "phone" => {"type" => "array", "items" => {"type" => "object", "properties" => {"location" => {"type" => "string"}, "number" => {"type" => "string"}}}}}})
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
- This definition gives you not just the Contact class, but classes for the whole nested structure. So, if we construct an instance like:
54
+ So, if we construct an instance like:
37
55
 
38
56
  ```ruby
39
- bill = Contact.new('name' => 'bill', 'phone' => [{'location' => 'home', 'number' => '555'}], 'nickname' => 'big b')
40
- # => #{<Contact fragment="#">
41
- # #{<Contact fragment="#">
42
- # "phone" => #[<JSI::SchemaClasses["1f97#/properties/phone"] fragment="#/phone">
43
- # #{<JSI::SchemaClasses["1f97#/properties/phone/items"] fragment="#/phone/0"> "location" => "home", "number" => "555"}
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.new('phone' => [{'number' => [5, 5, 5]}])
78
- # => #{<Contact fragment="#">
79
- # "phone" => #[<JSI::SchemaClasses["1f97#/properties/phone"] fragment="#/phone">
80
- # #{<JSI::SchemaClasses["1f97#/properties/phone/items"] fragment="#/phone/0">
81
- # "number" => #[<JSI::SchemaClasses["1f97#/properties/phone/items/properties/number"] fragment="#/phone/0/number"> 5, 5, 5]
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 1f97"]
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 #[] or, say, #transform_values:
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 from which other classes representing JSON-Schemas inherit.
105
- - a JSI class refers to a class representing a schema, a subclass of JSI::Base.
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 json-schema.
108
- - a JSI instance (or just "a JSI") is a ruby object instantiating a JSI class. it has a method #instance which contains the underlying data.
109
- - a schema refers to a json-schema. a JSI::Schema represents such a json-schema. a JSI class allows instantiation of such a schema.
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 classes
134
+ ## JSI and Object Oriented Programming
112
135
 
113
- A JSI class (that is, subclass of JSI::Base) is a starting point but obviously you want your own methods, so you reopen the class as you would any other. referring back to the Example section above, we reopen the Contact class:
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
- class Contact
117
- def full_address
118
- address.values.join(", ")
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 (these accessor methods are defined on an included module instead of the JSI class for this reason). You can also use [] and []=, of course, with the same effect.
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
- If you want to add methods to a subschema, get the class_for_schema for that schema and open up that class. You can leave the class anonymous, as in this example:
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['properties']['phone']['items']
142
- JSI.class_for_schema(phone_schema).class_eval do
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 class, this works:
176
+ If you want to name the module, this works:
152
177
 
153
178
  ```ruby
154
- Phone = JSI.class_for_schema(Contact.schema['properties']['phone']['items'])
155
- class Phone
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 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.
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 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.
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 SchemaInstanceJSONCoder. The objects in your database are simple json types, and your ruby classes are extensible and have the accessors you get from a JSI class hierarchy. Here's an example:
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 :contacts, JSI::SchemaInstanceJSONCoder.new(Contact, array: true)
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 json type in the database, with Contact's accessors, validations, and user-defined instance methods.
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
- JSI is open source software available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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.
@@ -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-Schema instantiation"
12
- spec.description = "JSI represents json-schemas as ruby classes and json-schema instances as instances of those classes"
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 = "MIT"
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
- Dir.chdir(File.expand_path('..', __FILE__)) do
18
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(ignore_files_re) }
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", "~> 10.0"
28
- spec.add_development_dependency "minitest", "~> 5.0"
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 :BaseArray, 'jsi/base'
19
- autoload :BaseHash, 'jsi/base'
20
- autoload :SchemaClasses, 'jsi/base'
21
- autoload :ObjectJSONCoder, 'jsi/schema_instance_json_coder'
22
- autoload :StructJSONCoder, 'jsi/struct_json_coder'
23
- autoload :SchemaInstanceJSONCoder, 'jsi/schema_instance_json_coder'
24
-
25
- # @return [Class subclassing JSI::Base] a JSI class which represents the
26
- # given schema. instances of the class represent JSON Schema instances
27
- # for the given schema.
28
- def self.class_for_schema(*a, &b)
29
- SchemaClasses.class_for_schema(*a, &b)
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
@@ -1,5 +1,4 @@
1
- require 'json'
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
- # @return [String] absolute schema_id of the schema this class represents.
19
- # see {Schema#schema_id}.
20
- def schema_id
21
- schema.schema_id
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
- # @return [String] a string representing the class, with schema_id
25
- def inspect
26
- if !respond_to?(:schema)
27
- super
28
- elsif !name || name =~ /\AJSI::SchemaClasses::/
29
- %Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
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 - a class name if one
36
- # was explicitly defined, otherwise a reference to JSI::SchemaClasses
37
- def to_s
38
- if !respond_to?(:schema)
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
- name
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
- # @return [String] a name for a constant for this class, generated from the
48
- # schema_id. only used if the class is not assigned to another constant.
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
- name = schema.schema_id.gsub(/[^\w]/, '_')
51
- name = 'X' + name unless name[/\A[a-zA-Z_]/]
52
- name = name[0].upcase + name[1..-1]
53
- name
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 super || SchemaClasses.const_defined?(schema_classes_const_name)
59
- SchemaClasses.const_set(schema_classes_const_name, self)
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
- # initializes this JSI from the given instance. the instance will be
66
- # wrapped as a {JSI::JSON::Node JSI::JSON::Node} (unless what you pass is
67
- # a Node already).
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 ancestor [JSI::Base] for internal use, specifies an ancestor
71
- # from which this JSI originated to calculate #parents
72
- def initialize(instance, ancestor: nil)
73
- unless respond_to?(:schema)
74
- raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #schema. please use JSI.class_for_schema")
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
- @ancestor = ancestor || self
78
- self.instance = instance
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
- if @instance.is_a?(JSI::JSON::HashNode)
81
- extend BaseHash
82
- elsif @instance.is_a?(JSI::JSON::ArrayNode)
83
- extend BaseArray
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 the json-schema. this is a JSI::JSON::Node.
88
- attr_reader :instance
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
- # a JSI which is an ancestor of this
91
- attr_reader :ancestor
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
- # each is overridden by BaseHash or BaseArray when appropriate. the base
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: #{instance.pretty_inspect.chomp}"
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. empty if this
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 parents
105
- parent = @ancestor
106
- (@ancestor.instance.path.size...self.instance.path.size).map do |i|
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[self.instance.path[i]]
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(s) are known.
209
+ # the immediate parent of this JSI. nil if there is no parent.
114
210
  #
115
211
  # @return [JSI::Base, nil]
116
- def parent
117
- parents.first
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
- derefed = instance.deref
126
- if derefed.object_id == instance.object_id
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
- modified_instance = instance.modified_copy(&block)
142
- self.class.new(modified_instance, ancestor: @ancestor)
143
- end
144
-
145
- def fragment
146
- instance.fragment
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<String>] array of schema validation error messages for this instance
150
- def fully_validate
151
- schema.fully_validate(instance)
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.validate(instance)
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.validate!(instance)
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
- "\#<#{self.class.to_s} #{instance.inspect}>"
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.instance_exec(self) do |obj|
177
- text "\#<#{obj.class.to_s}"
178
- group_sub {
179
- nest(2) {
180
- breakable ' '
181
- pp obj.instance
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
- breakable ''
185
- text '>'
186
- end
369
+ }
370
+ q.breakable ''
371
+ q.text '>'
187
372
  end
188
373
 
189
- # @return [String] the instance's object_group_text
374
+ # @return [Array<String>]
190
375
  def object_group_text
191
- instance.object_group_text
192
- end
193
-
194
- # @return [Object] a jsonifiable representation of the instance
195
- def as_json(*opt)
196
- Typelike.as_json(instance, *opt)
197
- end
198
-
199
- # @return [Object] an opaque fingerprint of this JSI for FingerprintHash
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
- instance_method_modules = [m, Base, BaseArray, BaseHash]
307
- instance_methods = instance_method_modules.map do |mod|
308
- mod.instance_methods + mod.private_instance_methods
309
- end.inject(Set.new, &:|)
310
- accessors_to_define = schema.described_object_property_names.map(&:to_s) - instance_methods.map(&:to_s)
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
- # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
355
- SAFE_KEY_ONLY_METHODS.each do |method_name|
356
- define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
357
- end
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
- include Arraylike
417
-
418
- # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
419
- # we override these methods from Arraylike
420
- SAFE_INDEX_ONLY_METHODS.each do |method_name|
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] returns the instance's subscript value at the given index
425
- # i_. if there is a subschema defined for that index on this JSI's schema,
426
- # returns the instance's subscript as a JSI instiation of that subschema.
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
- # assigns the given index of the instance to the given value.
452
- # if the value is a JSI, its instance is assigned.
453
- # @param i [Object] the array index to assign
454
- # @param value [Object] the value to be assigned to the given subscript i
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