jsi 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.simplecov +1 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +160 -0
- data/Rakefile.rb +9 -0
- data/jsi.gemspec +31 -0
- data/lib/jsi.rb +28 -0
- data/lib/jsi/base.rb +325 -0
- data/lib/jsi/base/to_rb.rb +127 -0
- data/lib/jsi/json-schema-fragments.rb +191 -0
- data/lib/jsi/json.rb +7 -0
- data/lib/jsi/json/node.rb +256 -0
- data/lib/jsi/schema.rb +249 -0
- data/lib/jsi/schema_instance_json_coder.rb +83 -0
- data/lib/jsi/struct_json_coder.rb +30 -0
- data/lib/jsi/typelike_modules.rb +164 -0
- data/lib/jsi/util.rb +103 -0
- data/lib/jsi/version.rb +3 -0
- data/test/base_array_test.rb +142 -0
- data/test/base_hash_test.rb +135 -0
- data/test/base_test.rb +395 -0
- data/test/jsi_json_arraynode_test.rb +133 -0
- data/test/jsi_json_hashnode_test.rb +117 -0
- data/test/jsi_json_node_test.rb +288 -0
- data/test/jsi_test.rb +11 -0
- data/test/schema_instance_json_coder_test.rb +122 -0
- data/test/struct_json_coder_test.rb +130 -0
- data/test/test_helper.rb +29 -0
- data/test/util_test.rb +62 -0
- metadata +155 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ec49b425e6253b4ff1bf14b1d2218fe1bd5c905194d169faab948820a2e17302
|
4
|
+
data.tar.gz: 56d79d38177b3272ba094e21def5083896a12c8157346909b739927fbe52e542
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cd3d72c29b166c0bb7b1a5970742e5538a79fda1a16359fadb35dd3b6ea65911481f40ab8e4f79ec9da56247f385ee6168ad2aeeaec0f4a6735da7370aba8005
|
7
|
+
data.tar.gz: c476fb40d83d9b98568bc067375ddf2f95953fe886284ab889fed11df51b12bd3c8c079bb6dc7e0dec52fa38175e61eb1d784b83d3b707cc984100670f8a592a
|
data/.simplecov
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
SimpleCov.start
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Ethan
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
# JSI: JSON-Schema Instantiation
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/notEthan/jsi.svg?branch=master)](https://travis-ci.org/notEthan/jsi)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/github/notEthan/jsi/badge.svg)](https://coveralls.io/github/notEthan/jsi)
|
5
|
+
|
6
|
+
JSI represents JSON-schemas as ruby classes, and schema instances as instances of those classes.
|
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.
|
9
|
+
|
10
|
+
## Example
|
11
|
+
|
12
|
+
Words are boring, let's code. Here's a schema in yaml:
|
13
|
+
|
14
|
+
```yaml
|
15
|
+
description: "A Contact"
|
16
|
+
type: "object"
|
17
|
+
properties:
|
18
|
+
name: {type: "string"}
|
19
|
+
phone:
|
20
|
+
type: "array"
|
21
|
+
items:
|
22
|
+
type: "object"
|
23
|
+
properties:
|
24
|
+
location: {type: "string"}
|
25
|
+
number: {type: "string"}
|
26
|
+
```
|
27
|
+
|
28
|
+
And here's an instance of that schema with JSI:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
Contact = JSI.class_for_schema(YAML.load_file('contact.schema.yml'))
|
32
|
+
```
|
33
|
+
|
34
|
+
This definition gives you not just the Contact class, but classes for the whole nested structure. So, if we construct an instance like:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
bill = Contact.new(name: 'bill', phone: [{location: 'home', number: '555'}], nickname: 'big b')
|
38
|
+
# => #{<Contact fragment="#">
|
39
|
+
# #{<Contact fragment="#">
|
40
|
+
# "phone" => #[<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone"] fragment="#/phone">
|
41
|
+
# #{<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone/items"] fragment="#/phone/0"> "location" => "home", "number" => "555"}
|
42
|
+
# ],
|
43
|
+
# "nickname" => "big b"
|
44
|
+
# }
|
45
|
+
```
|
46
|
+
|
47
|
+
The nested classes can be seen as `JSI::SchemaClasses[schema_id]` where schema_id is a generated value.
|
48
|
+
|
49
|
+
We get accessors for the Contact:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
bill.name
|
53
|
+
# => "bill"
|
54
|
+
```
|
55
|
+
|
56
|
+
but also nested accessors - #phone is an instance of its array-type schema, and each phone item is an instance of another object-type schema with location and number accessors:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
bill.phone.map(&:location)
|
60
|
+
# => ["home"]
|
61
|
+
```
|
62
|
+
|
63
|
+
We also get validations, as you'd expect given that's largely what json-schema exists to do:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
bill.validate
|
67
|
+
# => true
|
68
|
+
```
|
69
|
+
|
70
|
+
... and validations on the nested schema instances (#phone here), showing in this example validation failure:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
bad = Contact.new(phone: [{number: [5, 5, 5]}])
|
74
|
+
# => #{<Contact fragment="#">
|
75
|
+
# "phone" => #[<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone"] fragment="#/phone">
|
76
|
+
# #{<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone/items"] fragment="#/phone/0">
|
77
|
+
# "number" => #[<JSI::SchemaClasses["594126e3-ea3c-5d9a-b5f4-7423a8701f97#/properties/phone/items/properties/number"] fragment="#/phone/0/number"> 5, 5, 5]
|
78
|
+
# }
|
79
|
+
# ]
|
80
|
+
# }
|
81
|
+
bad.phone.fully_validate
|
82
|
+
# => ["The property '#/0/number' of type array did not match the following type: string in schema 594126e3-ea3c-5d9a-b5f4-7423a8701f97"]
|
83
|
+
```
|
84
|
+
|
85
|
+
Since the underlying instance is a ruby hash (json object), we can use it like a hash with #[] or, say, #transform_values:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
bill.transform_values(&:size)
|
89
|
+
# => {"name" => 4, "phone" => 1, "nickname" => 5}
|
90
|
+
bill['nickname']
|
91
|
+
# => "big b"
|
92
|
+
```
|
93
|
+
|
94
|
+
There's plenty more JSI has to offer, but this should give you a pretty good idea of basic usage.
|
95
|
+
|
96
|
+
## Terminology
|
97
|
+
|
98
|
+
- JSI::Base is the base class from which other classes representing JSON-Schemas inherit.
|
99
|
+
- a JSI class refers to a class representing a schema, a subclass of JSI::Base.
|
100
|
+
- "instance" is a term that is significantly overloaded in this space, so documentation will attempt to be clear what kind of instance is meant:
|
101
|
+
- a schema instance refers broadly to a data structure that is described by a json-schema.
|
102
|
+
- a JSI instance is a ruby object instantiating a JSI class. it has a method #instance which contains the underlying data.
|
103
|
+
- a schema refers to a json-schema. a JSI::Schema represents such a json-schema. a JSI class allow instantiation of such a schema.
|
104
|
+
|
105
|
+
## JSI classes
|
106
|
+
|
107
|
+
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, here's an example:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
class Contact
|
111
|
+
def full_address
|
112
|
+
address.values.join(", ")
|
113
|
+
end
|
114
|
+
def name
|
115
|
+
super + ' esq.'
|
116
|
+
end
|
117
|
+
def name=(name)
|
118
|
+
super(name.chomp(' esq.'))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
bill.name
|
123
|
+
# => "bill esq."
|
124
|
+
bill.name = 'rob esq.'
|
125
|
+
# => "rob esq."
|
126
|
+
bill.instance['name']
|
127
|
+
# => "rob"
|
128
|
+
```
|
129
|
+
|
130
|
+
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 class instead of the JSI class for this reason). You can also use [] and []=, of course, with the same effect.
|
131
|
+
|
132
|
+
## ActiveRecord serialization
|
133
|
+
|
134
|
+
A really excellent place to use JSI is when dealing with serialized columns in ActiveRecord.
|
135
|
+
|
136
|
+
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.
|
137
|
+
|
138
|
+
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.
|
139
|
+
|
140
|
+
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:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
class User < ActiveRecord::Base
|
144
|
+
serialize :contacts, JSI::SchemaInstanceJSONCoder.new(Contact, array: true)
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
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.
|
149
|
+
|
150
|
+
## Keying Hashes (JSON Objects)
|
151
|
+
|
152
|
+
Unlike Ruby, JSON only supports string keys. JSI converts symbols to strings for its internal hash keys (much like ActiveSupport::HashWithIndifferentAccess). JSI accepts symbols to refer to its string hash keys for instantiation, but does not currently transform symbols to strings everywhere else, e.g. `bill[:name]` is `nil` whereas `bill['name']` is `"bill"`.
|
153
|
+
|
154
|
+
## Contributing
|
155
|
+
|
156
|
+
Issues and pull requests are welcome on GitHub at https://github.com/notEthan/jsi.
|
157
|
+
|
158
|
+
## License
|
159
|
+
|
160
|
+
JSI is open source software available under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile.rb
ADDED
data/jsi.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "jsi/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "jsi"
|
7
|
+
spec.version = JSI::VERSION
|
8
|
+
spec.authors = ["Ethan"]
|
9
|
+
spec.email = ["ethan@unth"]
|
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"
|
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) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
# we are monkey patching json-schema with a fix that has not been merged in a timely fashion.
|
26
|
+
spec.add_dependency "json-schema", "~> 2.8"
|
27
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
28
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
29
|
+
spec.add_development_dependency "minitest-around"
|
30
|
+
spec.add_development_dependency "minitest-reporters"
|
31
|
+
end
|
data/lib/jsi.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "jsi/version"
|
2
|
+
require "pp"
|
3
|
+
require "jsi/json-schema-fragments"
|
4
|
+
require "jsi/util"
|
5
|
+
|
6
|
+
module JSI
|
7
|
+
# generally put in code paths that are not expected to be valid control flow paths.
|
8
|
+
# rather a NotImplementedCorrectlyError. but that's too long.
|
9
|
+
class Bug < NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
autoload :JSON, 'jsi/json'
|
13
|
+
autoload :Typelike, 'jsi/typelike_modules'
|
14
|
+
autoload :Hashlike, 'jsi/typelike_modules'
|
15
|
+
autoload :Arraylike, 'jsi/typelike_modules'
|
16
|
+
autoload :Schema, 'jsi/schema'
|
17
|
+
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
|
+
def self.class_for_schema(*a, &b)
|
26
|
+
SchemaClasses.class_for_schema(*a, &b)
|
27
|
+
end
|
28
|
+
end
|
data/lib/jsi/base.rb
ADDED
@@ -0,0 +1,325 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'jsi/typelike_modules'
|
3
|
+
|
4
|
+
module JSI
|
5
|
+
# base class for representing an instance of an instance described by a schema
|
6
|
+
class Base
|
7
|
+
include Memoize
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def schema_id
|
12
|
+
schema.schema_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def inspect
|
16
|
+
if !respond_to?(:schema)
|
17
|
+
super
|
18
|
+
elsif !name || name =~ /\AJSI::SchemaClasses::/
|
19
|
+
%Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
|
20
|
+
else
|
21
|
+
%Q(#{name} (#{schema_id}))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
def to_s
|
25
|
+
if !respond_to?(:schema)
|
26
|
+
super
|
27
|
+
elsif !name || name =~ /\AJSI::SchemaClasses::/
|
28
|
+
%Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
|
29
|
+
else
|
30
|
+
name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def schema_classes_const_name
|
35
|
+
name = schema.schema_id.gsub(/[^\w]/, '_')
|
36
|
+
name = 'X' + name unless name[/\A[a-zA-Z_]/]
|
37
|
+
name = name[0].upcase + name[1..-1]
|
38
|
+
name
|
39
|
+
end
|
40
|
+
|
41
|
+
def name
|
42
|
+
unless super
|
43
|
+
SchemaClasses.const_set(schema_classes_const_name, self)
|
44
|
+
end
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(instance, origin: nil)
|
50
|
+
unless respond_to?(:schema)
|
51
|
+
raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #schema. please use JSI.class_for_schema")
|
52
|
+
end
|
53
|
+
|
54
|
+
@origin = origin || self
|
55
|
+
self.instance = instance
|
56
|
+
|
57
|
+
if @instance.is_a?(JSI::JSON::HashNode)
|
58
|
+
extend BaseHash
|
59
|
+
elsif @instance.is_a?(JSI::JSON::ArrayNode)
|
60
|
+
extend BaseArray
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_reader :instance
|
65
|
+
|
66
|
+
# each is overridden by BaseHash or BaseArray when appropriate. the base
|
67
|
+
# #each is not actually implemented, along with all the methods of Enumerable.
|
68
|
+
def each
|
69
|
+
raise NoMethodError, "Enumerable methods and #each not implemented for instance that is not like a hash or array: #{instance.pretty_inspect.chomp}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def parents
|
73
|
+
parent = @origin
|
74
|
+
(@origin.instance.path.size...self.instance.path.size).map do |i|
|
75
|
+
parent.tap do
|
76
|
+
parent = parent[self.instance.path[i]]
|
77
|
+
end
|
78
|
+
end.reverse
|
79
|
+
end
|
80
|
+
def parent
|
81
|
+
parents.first
|
82
|
+
end
|
83
|
+
|
84
|
+
def deref
|
85
|
+
derefed = instance.deref
|
86
|
+
if derefed.object_id == instance.object_id
|
87
|
+
self
|
88
|
+
else
|
89
|
+
self.class.new(derefed, origin: @origin)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def modified_copy(&block)
|
94
|
+
modified_instance = instance.modified_copy(&block)
|
95
|
+
self.class.new(modified_instance, origin: @origin)
|
96
|
+
end
|
97
|
+
|
98
|
+
def fragment
|
99
|
+
instance.fragment
|
100
|
+
end
|
101
|
+
|
102
|
+
def fully_validate
|
103
|
+
schema.fully_validate(instance)
|
104
|
+
end
|
105
|
+
def validate
|
106
|
+
schema.validate(instance)
|
107
|
+
end
|
108
|
+
def validate!
|
109
|
+
schema.validate!(instance)
|
110
|
+
end
|
111
|
+
def inspect
|
112
|
+
"\#<#{self.class.to_s} #{instance.inspect}>"
|
113
|
+
end
|
114
|
+
def pretty_print(q)
|
115
|
+
q.instance_exec(self) do |obj|
|
116
|
+
text "\#<#{obj.class.to_s}"
|
117
|
+
group_sub {
|
118
|
+
nest(2) {
|
119
|
+
breakable ' '
|
120
|
+
pp obj.instance
|
121
|
+
}
|
122
|
+
}
|
123
|
+
breakable ''
|
124
|
+
text '>'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def object_group_text
|
129
|
+
instance.object_group_text
|
130
|
+
end
|
131
|
+
|
132
|
+
def as_json(*opt)
|
133
|
+
Typelike.as_json(instance, *opt)
|
134
|
+
end
|
135
|
+
|
136
|
+
def fingerprint
|
137
|
+
{class: self.class, instance: instance}
|
138
|
+
end
|
139
|
+
include FingerprintHash
|
140
|
+
|
141
|
+
private
|
142
|
+
def instance=(thing)
|
143
|
+
if instance_variable_defined?(:@instance)
|
144
|
+
raise(JSI::Bug, "overwriting instance is not supported")
|
145
|
+
end
|
146
|
+
if thing.is_a?(Base)
|
147
|
+
warn "assigning instance to a Base instance is incorrect. received: #{thing.pretty_inspect.chomp}"
|
148
|
+
@instance = JSI.deep_stringify_symbol_keys(thing.instance)
|
149
|
+
elsif thing.is_a?(JSI::JSON::Node)
|
150
|
+
@instance = JSI.deep_stringify_symbol_keys(thing)
|
151
|
+
else
|
152
|
+
@instance = JSI::JSON::Node.new_by_type(JSI.deep_stringify_symbol_keys(thing), [])
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def subscript_assign(subscript, value)
|
157
|
+
clear_memo(:[], subscript)
|
158
|
+
if value.is_a?(Base)
|
159
|
+
instance[subscript] = value.instance
|
160
|
+
else
|
161
|
+
instance[subscript] = value
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# this module is just a namespace for schema classes.
|
167
|
+
module SchemaClasses
|
168
|
+
extend Memoize
|
169
|
+
def self.[](schema_id)
|
170
|
+
@classes_by_id[schema_id]
|
171
|
+
end
|
172
|
+
@classes_by_id = {}
|
173
|
+
end
|
174
|
+
|
175
|
+
def SchemaClasses.class_for_schema(schema_object)
|
176
|
+
if schema_object.is_a?(JSI::Schema)
|
177
|
+
schema__ = schema_object
|
178
|
+
else
|
179
|
+
schema__ = JSI::Schema.new(schema_object)
|
180
|
+
end
|
181
|
+
|
182
|
+
memoize(:class_for_schema, schema__) do |schema_|
|
183
|
+
begin
|
184
|
+
begin
|
185
|
+
Class.new(Base).instance_exec(schema_) do |schema|
|
186
|
+
begin
|
187
|
+
include(JSI.module_for_schema(schema))
|
188
|
+
|
189
|
+
SchemaClasses.instance_exec(self) { |klass| @classes_by_id[klass.schema_id] = klass }
|
190
|
+
|
191
|
+
self
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def self.module_for_schema(schema_object)
|
200
|
+
if schema_object.is_a?(JSI::Schema)
|
201
|
+
schema__ = schema_object
|
202
|
+
else
|
203
|
+
schema__ = JSI::Schema.new(schema_object)
|
204
|
+
end
|
205
|
+
|
206
|
+
memoize(:module_for_schema, schema__) do |schema_|
|
207
|
+
Module.new.tap do |m|
|
208
|
+
m.instance_exec(schema_) do |schema|
|
209
|
+
define_method(:schema) { schema }
|
210
|
+
define_singleton_method(:schema) { schema }
|
211
|
+
define_singleton_method(:included) do |includer|
|
212
|
+
includer.send(:define_singleton_method, :schema) { schema }
|
213
|
+
end
|
214
|
+
|
215
|
+
define_singleton_method(:schema_id) do
|
216
|
+
schema.schema_id
|
217
|
+
end
|
218
|
+
define_singleton_method(:inspect) do
|
219
|
+
%Q(#<Module for Schema: #{schema_id}>)
|
220
|
+
end
|
221
|
+
|
222
|
+
if schema.describes_hash?
|
223
|
+
instance_method_modules = [m, Base, BaseArray, BaseHash]
|
224
|
+
instance_methods = instance_method_modules.map do |mod|
|
225
|
+
mod.instance_methods + mod.private_instance_methods
|
226
|
+
end.inject(Set.new, &:|)
|
227
|
+
accessors_to_define = schema.described_hash_property_names.map(&:to_s) - instance_methods.map(&:to_s)
|
228
|
+
accessors_to_define.each do |property_name|
|
229
|
+
define_method(property_name) do
|
230
|
+
if respond_to?(:[])
|
231
|
+
self[property_name]
|
232
|
+
else
|
233
|
+
raise(NoMethodError, "instance does not respond to []; cannot call reader `#{property_name}' for: #{pretty_inspect.chomp}")
|
234
|
+
end
|
235
|
+
end
|
236
|
+
define_method("#{property_name}=") do |value|
|
237
|
+
if respond_to?(:[]=)
|
238
|
+
self[property_name] = value
|
239
|
+
else
|
240
|
+
raise(NoMethodError, "instance does not respond to []=; cannot call writer `#{property_name}=' for: #{pretty_inspect.chomp}")
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
module BaseHash
|
251
|
+
# Hash methods
|
252
|
+
def each
|
253
|
+
return to_enum(__method__) { instance.size } unless block_given?
|
254
|
+
instance.each_key { |k| yield(k, self[k]) }
|
255
|
+
self
|
256
|
+
end
|
257
|
+
|
258
|
+
def to_hash
|
259
|
+
inject({}) { |h, (k, v)| h[k] = v; h }
|
260
|
+
end
|
261
|
+
|
262
|
+
include Hashlike
|
263
|
+
|
264
|
+
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
|
265
|
+
SAFE_KEY_ONLY_METHODS.each do |method_name|
|
266
|
+
define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
|
267
|
+
end
|
268
|
+
|
269
|
+
def [](property_name_)
|
270
|
+
memoize(:[], property_name_) do |property_name|
|
271
|
+
begin
|
272
|
+
property_schema = schema.subschema_for_property(property_name)
|
273
|
+
property_schema = property_schema && property_schema.match_to_instance(instance[property_name])
|
274
|
+
|
275
|
+
if property_schema && instance[property_name].is_a?(JSON::Node)
|
276
|
+
JSI.class_for_schema(property_schema).new(instance[property_name], origin: @origin)
|
277
|
+
else
|
278
|
+
instance[property_name]
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
def []=(property_name, value)
|
284
|
+
subscript_assign(property_name, value)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
module BaseArray
|
289
|
+
def each
|
290
|
+
return to_enum(__method__) { instance.size } unless block_given?
|
291
|
+
instance.each_index { |i| yield(self[i]) }
|
292
|
+
self
|
293
|
+
end
|
294
|
+
|
295
|
+
def to_ary
|
296
|
+
to_a
|
297
|
+
end
|
298
|
+
|
299
|
+
include Arraylike
|
300
|
+
|
301
|
+
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
|
302
|
+
# we override these methods from Arraylike
|
303
|
+
SAFE_INDEX_ONLY_METHODS.each do |method_name|
|
304
|
+
define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
|
305
|
+
end
|
306
|
+
|
307
|
+
def [](i_)
|
308
|
+
memoize(:[], i_) do |i|
|
309
|
+
begin
|
310
|
+
index_schema = schema.subschema_for_index(i)
|
311
|
+
index_schema = index_schema && index_schema.match_to_instance(instance[i])
|
312
|
+
|
313
|
+
if index_schema && instance[i].is_a?(JSON::Node)
|
314
|
+
JSI.class_for_schema(index_schema).new(instance[i], origin: @origin)
|
315
|
+
else
|
316
|
+
instance[i]
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
def []=(i, value)
|
322
|
+
subscript_assign(i, value)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|