jsi 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +5 -3
- data/lib/jsi.rb +1 -3
- data/lib/jsi/base.rb +7 -1
- data/lib/jsi/jsi_coder.rb +86 -0
- data/lib/jsi/json-schema-fragments.rb +1 -135
- data/lib/jsi/json.rb +1 -0
- data/lib/jsi/json/node.rb +5 -5
- data/lib/jsi/json/pointer.rb +136 -0
- data/lib/jsi/schema.rb +4 -7
- data/lib/jsi/typelike_modules.rb +7 -1
- data/lib/jsi/version.rb +1 -1
- data/test/jsi_coder_test.rb +85 -0
- data/test/jsi_json_node_test.rb +3 -3
- data/test/jsi_json_pointer_test.rb +98 -0
- data/test/jsi_typelike_as_json_test.rb +53 -0
- metadata +10 -8
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e9899a9ae6a661558974001eba14223c9e497849d3bf3d8e3c6ee9fffd4fcc6d
|
4
|
+
data.tar.gz: 8cfcc01a0167bc251235d7376e793e50bdba92e39f706edbb7bd05386a035352
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e4ed68443ce9a25db4ddeb18c80f4aea51b4dbb819600afc98c30ab6453ddf8dbc7589079cb7e6743802a31cc4ec53bc32b1da6796a10334e8cc2b6182d37cd4
|
7
|
+
data.tar.gz: 64e8c93a243bc54e0226b8020e20b475c151d349d0b2769bf745b0ba6019f31cf6de63c4edaa36baea89da790814654e92c68154c5a63ade881bef4a18fac133
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
# v0.1.0
|
2
|
+
|
3
|
+
- JSI::JSON::Pointer replaces monkey-patched-in ::JSON::Schema::Pointer
|
4
|
+
- JSI::JSICoder replaces JSI::SchemaInstanceJSONCoder / ObjectJSONCoder
|
5
|
+
- remove JSI::StructJSONCoder
|
6
|
+
- misc improvements to code, doc, tests
|
7
|
+
|
1
8
|
# v0.0.4
|
2
9
|
|
3
10
|
- minor bugfixes / improvements
|
data/README.md
CHANGED
@@ -48,7 +48,7 @@ bill = Contact.new('name' => 'bill', 'phone' => [{'location' => 'home', 'number'
|
|
48
48
|
|
49
49
|
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
50
|
|
51
|
-
The nested classes can be seen as `JSI::SchemaClasses[schema_id]` where schema_id is a generated value.
|
51
|
+
The nested classes can be seen in the #inspect output as `JSI::SchemaClasses[schema_id]` where schema_id is a generated value.
|
52
52
|
|
53
53
|
We get accessors for the Contact:
|
54
54
|
|
@@ -169,16 +169,18 @@ Let's say you're sticking to json types in the database - you have to do so if y
|
|
169
169
|
|
170
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.
|
171
171
|
|
172
|
-
JSI gives you the best of both with
|
172
|
+
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 class. Here's an example:
|
173
173
|
|
174
174
|
```ruby
|
175
175
|
class User < ActiveRecord::Base
|
176
|
-
serialize :
|
176
|
+
serialize :contact_info, JSI::JSICoder.new(Contact)
|
177
177
|
end
|
178
178
|
```
|
179
179
|
|
180
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.
|
181
181
|
|
182
|
+
See the gem [`arms`](https://github.com/notEthan/arms) if you wish to serialize the dumped JSON-compatible objects further as text.
|
183
|
+
|
182
184
|
## Keying Hashes (JSON Objects)
|
183
185
|
|
184
186
|
Unlike Ruby, JSON only supports string keys. It is recommended to use strings as hash keys for all JSI instances, but JSI does not enforce this, nor does it do any key conversion. It should be possible to use ActiveSupport::HashWithIndifferentAccess as the instance of a JSI in order to gain the benefits that offers over a plain hash. This is not tested behavior, but JSI should behave correctly with any instance that responds to #to_hash.
|
data/lib/jsi.rb
CHANGED
@@ -18,9 +18,7 @@ module JSI
|
|
18
18
|
autoload :BaseArray, 'jsi/base'
|
19
19
|
autoload :BaseHash, 'jsi/base'
|
20
20
|
autoload :SchemaClasses, 'jsi/base'
|
21
|
-
autoload :
|
22
|
-
autoload :StructJSONCoder, 'jsi/struct_json_coder'
|
23
|
-
autoload :SchemaInstanceJSONCoder, 'jsi/schema_instance_json_coder'
|
21
|
+
autoload :JSICoder, 'jsi/jsi_coder'
|
24
22
|
|
25
23
|
# @return [Class subclassing JSI::Base] a JSI class which represents the
|
26
24
|
# given schema. instances of the class represent JSON Schema instances
|
data/lib/jsi/base.rb
CHANGED
@@ -15,6 +15,8 @@ module JSI
|
|
15
15
|
include Enumerable
|
16
16
|
|
17
17
|
class << self
|
18
|
+
attr_accessor :in_schema_classes
|
19
|
+
|
18
20
|
# @return [String] absolute schema_id of the schema this class represents.
|
19
21
|
# see {Schema#schema_id}.
|
20
22
|
def schema_id
|
@@ -23,10 +25,13 @@ module JSI
|
|
23
25
|
|
24
26
|
# @return [String] a string representing the class, with schema_id
|
25
27
|
def inspect
|
28
|
+
name # see #name for side effects
|
26
29
|
if !respond_to?(:schema)
|
27
30
|
super
|
28
|
-
elsif
|
31
|
+
elsif in_schema_classes
|
29
32
|
%Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
|
33
|
+
elsif !name
|
34
|
+
%Q(#<Class for Schema: #{schema_id}>)
|
30
35
|
else
|
31
36
|
%Q(#{name} (#{schema_id}))
|
32
37
|
end
|
@@ -57,6 +62,7 @@ module JSI
|
|
57
62
|
def name
|
58
63
|
unless super || SchemaClasses.const_defined?(schema_classes_const_name)
|
59
64
|
SchemaClasses.const_set(schema_classes_const_name, self)
|
65
|
+
self.in_schema_classes = true
|
60
66
|
end
|
61
67
|
super
|
62
68
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module JSI
|
2
|
+
# this is an ActiveRecord serialization coder intended to serialize between
|
3
|
+
# JSON-compatible objects on the database side, and a JSI instance loaded on
|
4
|
+
# the model attribute.
|
5
|
+
#
|
6
|
+
# on its own this coder is useful with a JSON database column. in order to
|
7
|
+
# serialize further to a string of JSON, or to YAML, the gem `arms` allows
|
8
|
+
# coders to be chained together. for example, for a table `foos` and a column
|
9
|
+
# `preferences_json` which is an actual json column, and `preferences_txt`
|
10
|
+
# which is a string:
|
11
|
+
#
|
12
|
+
# Preferences = JSI.class_for_schema(preferences_json_schema)
|
13
|
+
# class Foo < ActiveRecord::Base
|
14
|
+
# # as a single serializer, loads a Preferences instance from a json column
|
15
|
+
# serialize 'preferences', JSI::JSICoder.new(Preferences)
|
16
|
+
#
|
17
|
+
# # for a text column, arms_serialize will go from JSI to JSON-compatible
|
18
|
+
# # objects to a string. the symbol `:jsi` is a shortcut for JSI::JSICoder.
|
19
|
+
# arms_serialize 'preferences', [:jsi, Preferences], :json
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# the column data may be either a single instance of the loaded class
|
23
|
+
# (represented as one json object) or an array of them (represented as a json
|
24
|
+
# array of json objects), indicated by the keyword argument `array`.
|
25
|
+
class JSICoder
|
26
|
+
# @param loaded_class [Class] the JSI::Base subclass which #load will instantiate
|
27
|
+
# @param array [Boolean] whether the dumped data represent one instance of loaded_class,
|
28
|
+
# or an array of them. note that it may be preferable to have loaded_class simply be
|
29
|
+
# an array schema class.
|
30
|
+
def initialize(loaded_class, array: false)
|
31
|
+
@loaded_class = loaded_class
|
32
|
+
@array = array
|
33
|
+
end
|
34
|
+
|
35
|
+
# loads the database column to instances of #loaded_class
|
36
|
+
#
|
37
|
+
# @param data [Object, Array, nil] the dumped schema instance(s) of the JSI(s)
|
38
|
+
# @return [loaded_class instance, Array<loaded_class instance>, nil] the JSI or JSIs
|
39
|
+
# containing the schema instance(s), or nil if data is nil
|
40
|
+
def load(data)
|
41
|
+
return nil if data.nil?
|
42
|
+
object = if @array
|
43
|
+
unless data.respond_to?(:to_ary)
|
44
|
+
raise TypeError, "expected array-like column data; got: #{data.class}: #{data.inspect}"
|
45
|
+
end
|
46
|
+
data.map { |el| load_object(el) }
|
47
|
+
else
|
48
|
+
load_object(data)
|
49
|
+
end
|
50
|
+
object
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param object [loaded_class instance, Array<loaded_class instance>, nil] the JSI or array
|
54
|
+
# of JSIs containing the schema instance(s)
|
55
|
+
# @return [Object, Array, nil] the schema instance(s) of the JSI(s), or nil if object is nil
|
56
|
+
def dump(object)
|
57
|
+
return nil if object.nil?
|
58
|
+
jsonifiable = begin
|
59
|
+
if @array
|
60
|
+
unless object.respond_to?(:to_ary)
|
61
|
+
raise(TypeError, "expected array-like attribute; got: #{object.class}: #{object.inspect}")
|
62
|
+
end
|
63
|
+
object.map do |el|
|
64
|
+
dump_object(el)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
dump_object(object)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
jsonifiable
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
# @param data [Object]
|
75
|
+
# @return [loaded_class]
|
76
|
+
def load_object(data)
|
77
|
+
@loaded_class.new(data)
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param object [loaded_class]
|
81
|
+
# @return [Object]
|
82
|
+
def dump_object(object)
|
83
|
+
JSI::Typelike.as_json(object)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -2,140 +2,6 @@ require "json-schema"
|
|
2
2
|
|
3
3
|
# apply the changes from https://github.com/ruby-json-schema/json-schema/pull/382
|
4
4
|
|
5
|
-
# json-schema/pointer.rb
|
6
|
-
require 'addressable/uri'
|
7
|
-
|
8
|
-
module JSON
|
9
|
-
class Schema
|
10
|
-
# a JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901
|
11
|
-
class Pointer
|
12
|
-
class Error < JSON::Schema::SchemaError
|
13
|
-
end
|
14
|
-
class PointerSyntaxError < Error
|
15
|
-
end
|
16
|
-
class ReferenceError < Error
|
17
|
-
end
|
18
|
-
|
19
|
-
# parse a fragment to an array of reference tokens
|
20
|
-
#
|
21
|
-
# #/foo/bar
|
22
|
-
#
|
23
|
-
# => ['foo', 'bar']
|
24
|
-
#
|
25
|
-
# #/foo%20bar
|
26
|
-
#
|
27
|
-
# => ['foo bar']
|
28
|
-
def self.parse_fragment(fragment)
|
29
|
-
fragment = Addressable::URI.unescape(fragment)
|
30
|
-
match = fragment.match(/\A#/)
|
31
|
-
if match
|
32
|
-
parse_pointer(match.post_match)
|
33
|
-
else
|
34
|
-
raise(PointerSyntaxError, "Invalid fragment syntax in #{fragment.inspect}: fragment must begin with #")
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
# parse a pointer to an array of reference tokens
|
39
|
-
#
|
40
|
-
# /foo
|
41
|
-
#
|
42
|
-
# => ['foo']
|
43
|
-
#
|
44
|
-
# /foo~0bar/baz~1qux
|
45
|
-
#
|
46
|
-
# => ['foo~bar', 'baz/qux']
|
47
|
-
def self.parse_pointer(pointer_string)
|
48
|
-
tokens = pointer_string.split('/', -1).map! do |piece|
|
49
|
-
piece.gsub('~1', '/').gsub('~0', '~')
|
50
|
-
end
|
51
|
-
if tokens[0] == ''
|
52
|
-
tokens[1..-1]
|
53
|
-
elsif tokens.empty?
|
54
|
-
tokens
|
55
|
-
else
|
56
|
-
raise(PointerSyntaxError, "Invalid pointer syntax in #{pointer_string.inspect}: pointer must begin with /")
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
# initializes a JSON::Schema::Pointer from the given representation.
|
61
|
-
#
|
62
|
-
# type may be one of:
|
63
|
-
#
|
64
|
-
# - :fragment - the representation is a fragment containing a pointer (starting with #)
|
65
|
-
# - :pointer - the representation is a pointer (starting with /)
|
66
|
-
# - :reference_tokens - the representation is an array of tokens referencing a path in a document
|
67
|
-
def initialize(type, representation)
|
68
|
-
@type = type
|
69
|
-
if type == :reference_tokens
|
70
|
-
reference_tokens = representation
|
71
|
-
elsif type == :fragment
|
72
|
-
reference_tokens = self.class.parse_fragment(representation)
|
73
|
-
elsif type == :pointer
|
74
|
-
reference_tokens = self.class.parse_pointer(representation)
|
75
|
-
else
|
76
|
-
raise ArgumentError, "invalid initialization type: #{type.inspect} with representation #{representation.inspect}"
|
77
|
-
end
|
78
|
-
@reference_tokens = reference_tokens.map(&:freeze).freeze
|
79
|
-
end
|
80
|
-
|
81
|
-
attr_reader :reference_tokens
|
82
|
-
|
83
|
-
# takes a root json document and evaluates this pointer through the document, returning the value
|
84
|
-
# pointed to by this pointer.
|
85
|
-
def evaluate(document)
|
86
|
-
res = reference_tokens.inject(document) do |value, token|
|
87
|
-
if value.respond_to?(:to_ary)
|
88
|
-
if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
|
89
|
-
token = token.to_i
|
90
|
-
end
|
91
|
-
unless token.is_a?(Integer)
|
92
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
|
93
|
-
end
|
94
|
-
unless (0...(value.respond_to?(:size) ? value : value.to_ary).size).include?(token)
|
95
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
|
96
|
-
end
|
97
|
-
(value.respond_to?(:[]) ? value : value.to_ary)[token]
|
98
|
-
elsif value.respond_to?(:to_hash)
|
99
|
-
unless (value.respond_to?(:key?) ? value : value.to_hash).key?(token)
|
100
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
|
101
|
-
end
|
102
|
-
(value.respond_to?(:[]) ? value : value.to_hash)[token]
|
103
|
-
else
|
104
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
|
105
|
-
end
|
106
|
-
end
|
107
|
-
res
|
108
|
-
end
|
109
|
-
|
110
|
-
# the pointer string representation of this Pointer
|
111
|
-
def pointer
|
112
|
-
reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
|
113
|
-
end
|
114
|
-
|
115
|
-
# the fragment string representation of this Pointer
|
116
|
-
def fragment
|
117
|
-
'#' + Addressable::URI.escape(pointer)
|
118
|
-
end
|
119
|
-
|
120
|
-
def to_s
|
121
|
-
"#<#{self.class.inspect} #{@type} = #{representation_s}>"
|
122
|
-
end
|
123
|
-
|
124
|
-
private
|
125
|
-
|
126
|
-
def representation_s
|
127
|
-
if @type == :fragment
|
128
|
-
fragment
|
129
|
-
elsif @type == :pointer
|
130
|
-
pointer
|
131
|
-
else
|
132
|
-
reference_tokens.inspect
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
5
|
# json-schema/validator.rb
|
140
6
|
|
141
7
|
module JSON
|
@@ -177,7 +43,7 @@ module JSON
|
|
177
43
|
def schema_from_fragment(base_schema, fragment)
|
178
44
|
schema_uri = base_schema.uri
|
179
45
|
|
180
|
-
pointer = JSON::
|
46
|
+
pointer = JSI::JSON::Pointer.new(:fragment, fragment)
|
181
47
|
|
182
48
|
base_schema = JSON::Schema.new(pointer.evaluate(base_schema.schema), schema_uri, @options[:version])
|
183
49
|
|
data/lib/jsi/json.rb
CHANGED
data/lib/jsi/json/node.rb
CHANGED
@@ -52,14 +52,14 @@ module JSI
|
|
52
52
|
end
|
53
53
|
@document = document
|
54
54
|
@path = path.to_ary.dup.freeze
|
55
|
-
@pointer = ::JSON::
|
55
|
+
@pointer = JSI::JSON::Pointer.new(:reference_tokens, path)
|
56
56
|
end
|
57
57
|
|
58
58
|
# the path of this Node within its document
|
59
59
|
attr_reader :path
|
60
60
|
# the document containing this Node at is path
|
61
61
|
attr_reader :document
|
62
|
-
# ::JSON::
|
62
|
+
# JSI::JSON::Pointer representing the path to this node within its document
|
63
63
|
attr_reader :pointer
|
64
64
|
|
65
65
|
# the raw content of this Node from the underlying document at this Node's path.
|
@@ -129,7 +129,7 @@ module JSI
|
|
129
129
|
return self unless ref.is_a?(String)
|
130
130
|
|
131
131
|
if ref[/\A#/]
|
132
|
-
return self.class.new_by_type(document, ::JSON::
|
132
|
+
return self.class.new_by_type(document, JSI::JSON::Pointer.parse_fragment(ref)).deref
|
133
133
|
end
|
134
134
|
|
135
135
|
# HAX for how google does refs and ids
|
@@ -153,10 +153,10 @@ module JSI
|
|
153
153
|
end
|
154
154
|
|
155
155
|
# the parent of this node. if this node is the document root (its path is empty), raises
|
156
|
-
# ::JSON::
|
156
|
+
# JSI::JSON::Pointer::ReferenceError.
|
157
157
|
def parent_node
|
158
158
|
if path.empty?
|
159
|
-
raise(::JSON::
|
159
|
+
raise(JSI::JSON::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}")
|
160
160
|
else
|
161
161
|
Node.new_by_type(document, path[0...-1])
|
162
162
|
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
|
3
|
+
module JSI
|
4
|
+
module JSON
|
5
|
+
# a JSON Pointer, as described by RFC 6901 https://tools.ietf.org/html/rfc6901
|
6
|
+
class Pointer
|
7
|
+
class Error < StandardError
|
8
|
+
end
|
9
|
+
class PointerSyntaxError < Error
|
10
|
+
end
|
11
|
+
class ReferenceError < Error
|
12
|
+
end
|
13
|
+
|
14
|
+
# parse a fragment to an array of reference tokens
|
15
|
+
#
|
16
|
+
# #/foo/bar
|
17
|
+
#
|
18
|
+
# => ['foo', 'bar']
|
19
|
+
#
|
20
|
+
# #/foo%20bar
|
21
|
+
#
|
22
|
+
# => ['foo bar']
|
23
|
+
def self.parse_fragment(fragment)
|
24
|
+
fragment = Addressable::URI.unescape(fragment)
|
25
|
+
match = fragment.match(/\A#/)
|
26
|
+
if match
|
27
|
+
parse_pointer(match.post_match)
|
28
|
+
else
|
29
|
+
raise(PointerSyntaxError, "Invalid fragment syntax in #{fragment.inspect}: fragment must begin with #")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# parse a pointer to an array of reference tokens
|
34
|
+
#
|
35
|
+
# /foo
|
36
|
+
#
|
37
|
+
# => ['foo']
|
38
|
+
#
|
39
|
+
# /foo~0bar/baz~1qux
|
40
|
+
#
|
41
|
+
# => ['foo~bar', 'baz/qux']
|
42
|
+
def self.parse_pointer(pointer_string)
|
43
|
+
tokens = pointer_string.split('/', -1).map! do |piece|
|
44
|
+
piece.gsub('~1', '/').gsub('~0', '~')
|
45
|
+
end
|
46
|
+
if tokens[0] == ''
|
47
|
+
tokens[1..-1]
|
48
|
+
elsif tokens.empty?
|
49
|
+
tokens
|
50
|
+
else
|
51
|
+
raise(PointerSyntaxError, "Invalid pointer syntax in #{pointer_string.inspect}: pointer must begin with /")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# initializes a JSI::JSON::Pointer from the given representation.
|
56
|
+
#
|
57
|
+
# type may be one of:
|
58
|
+
#
|
59
|
+
# - :fragment - the representation is a fragment containing a pointer (starting with #)
|
60
|
+
# - :pointer - the representation is a pointer (starting with /)
|
61
|
+
# - :reference_tokens - the representation is an array of tokens referencing a path in a document
|
62
|
+
def initialize(type, representation)
|
63
|
+
@type = type
|
64
|
+
if type == :reference_tokens
|
65
|
+
reference_tokens = representation
|
66
|
+
elsif type == :fragment
|
67
|
+
reference_tokens = self.class.parse_fragment(representation)
|
68
|
+
elsif type == :pointer
|
69
|
+
reference_tokens = self.class.parse_pointer(representation)
|
70
|
+
else
|
71
|
+
raise ArgumentError, "invalid initialization type: #{type.inspect} with representation #{representation.inspect}"
|
72
|
+
end
|
73
|
+
@reference_tokens = reference_tokens.map(&:freeze).freeze
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_reader :reference_tokens
|
77
|
+
|
78
|
+
# takes a root json document and evaluates this pointer through the document, returning the value
|
79
|
+
# pointed to by this pointer.
|
80
|
+
#
|
81
|
+
# @param document [#to_ary, #to_hash] the document against which we will evaluate this pointer
|
82
|
+
# @return [Object] the content of the document pointed to by this pointer
|
83
|
+
# @raise [JSI::JSON::Pointer::ReferenceError] the document does not contain the path this pointer references
|
84
|
+
def evaluate(document)
|
85
|
+
res = reference_tokens.inject(document) do |value, token|
|
86
|
+
if value.respond_to?(:to_ary)
|
87
|
+
if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
|
88
|
+
token = token.to_i
|
89
|
+
end
|
90
|
+
unless token.is_a?(Integer)
|
91
|
+
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
|
92
|
+
end
|
93
|
+
unless (0...(value.respond_to?(:size) ? value : value.to_ary).size).include?(token)
|
94
|
+
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
|
95
|
+
end
|
96
|
+
(value.respond_to?(:[]) ? value : value.to_ary)[token]
|
97
|
+
elsif value.respond_to?(:to_hash)
|
98
|
+
unless (value.respond_to?(:key?) ? value : value.to_hash).key?(token)
|
99
|
+
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
|
100
|
+
end
|
101
|
+
(value.respond_to?(:[]) ? value : value.to_hash)[token]
|
102
|
+
else
|
103
|
+
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
res
|
107
|
+
end
|
108
|
+
|
109
|
+
# the pointer string representation of this Pointer
|
110
|
+
def pointer
|
111
|
+
reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
|
112
|
+
end
|
113
|
+
|
114
|
+
# the fragment string representation of this Pointer
|
115
|
+
def fragment
|
116
|
+
'#' + Addressable::URI.escape(pointer)
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_s
|
120
|
+
"#<#{self.class.inspect} #{@type} = #{representation_s}>"
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def representation_s
|
126
|
+
if @type == :fragment
|
127
|
+
fragment
|
128
|
+
elsif @type == :pointer
|
129
|
+
pointer
|
130
|
+
else
|
131
|
+
reference_tokens.inspect
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/lib/jsi/schema.rb
CHANGED
@@ -97,13 +97,13 @@ module JSI
|
|
97
97
|
end
|
98
98
|
if parent_auri.fragment
|
99
99
|
# add onto the fragment
|
100
|
-
parent_id_path = ::JSON::
|
100
|
+
parent_id_path = JSI::JSON::Pointer.new(:fragment, '#' + parent_auri.fragment).reference_tokens
|
101
101
|
path_from_id_node = parent_id_path + path_from_id_node
|
102
102
|
parent_auri.fragment = nil
|
103
103
|
#else: no fragment so parent_id good as is
|
104
104
|
end
|
105
105
|
|
106
|
-
fragment = ::JSON::
|
106
|
+
fragment = JSI::JSON::Pointer.new(:reference_tokens, path_from_id_node).fragment
|
107
107
|
schema_id = parent_auri.to_s + fragment
|
108
108
|
|
109
109
|
schema_id
|
@@ -124,12 +124,9 @@ module JSI
|
|
124
124
|
def match_to_instance(instance)
|
125
125
|
# matching oneOf is good here. one schema for one instance.
|
126
126
|
# matching anyOf is okay. there could be more than one schema matched. it's often just one. if more
|
127
|
-
# than one is a match,
|
128
|
-
# matching allOf is questionable. all of the schemas must be matched but we just return the first match.
|
129
|
-
# there isn't really a better answer with the current implementation. merging the schemas together
|
130
|
-
# is a thought but is not practical.
|
127
|
+
# than one is a match, you just get the first one.
|
131
128
|
instance = instance.deref if instance.is_a?(JSI::JSON::Node)
|
132
|
-
%w(oneOf
|
129
|
+
%w(oneOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |someof_key|
|
133
130
|
schema_node[someof_key].map(&:deref).map do |someof_node|
|
134
131
|
someof_schema = self.class.new(someof_node)
|
135
132
|
if someof_schema.validate(instance)
|
data/lib/jsi/typelike_modules.rb
CHANGED
@@ -36,7 +36,13 @@ module JSI
|
|
36
36
|
# @raise [TypeError] when the object (or an object nested with a hash or
|
37
37
|
# array of object) cannot be expressed as json
|
38
38
|
def self.as_json(object, *opt)
|
39
|
-
if object.
|
39
|
+
if object.is_a?(JSI::Schema)
|
40
|
+
as_json(object.schema_object, *opt)
|
41
|
+
elsif object.is_a?(JSI::Base)
|
42
|
+
as_json(object.instance, *opt)
|
43
|
+
elsif object.is_a?(JSI::JSON::Node)
|
44
|
+
as_json(object.content, *opt)
|
45
|
+
elsif object.respond_to?(:to_hash)
|
40
46
|
(object.respond_to?(:map) ? object : object.to_hash).map do |k, v|
|
41
47
|
unless k.is_a?(Symbol) || k.respond_to?(:to_str)
|
42
48
|
raise(TypeError, "json object (hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
|
data/lib/jsi/version.rb
CHANGED
@@ -0,0 +1,85 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe JSI::JSICoder do
|
4
|
+
let(:schema) do
|
5
|
+
{properties: {foo: {}, bar: {}}}
|
6
|
+
end
|
7
|
+
let(:schema_instance_class) { JSI.class_for_schema(schema) }
|
8
|
+
let(:options) { {} }
|
9
|
+
let(:schema_instance_json_coder) { JSI::JSICoder.new(schema_instance_class, options) }
|
10
|
+
describe 'json' do
|
11
|
+
describe 'load' do
|
12
|
+
it 'loads nil' do
|
13
|
+
assert_nil(schema_instance_json_coder.load(nil))
|
14
|
+
end
|
15
|
+
it 'loads a hash' do
|
16
|
+
assert_equal(schema_instance_class.new('foo' => 'bar'), schema_instance_json_coder.load({"foo" => "bar"}))
|
17
|
+
end
|
18
|
+
it 'loads something else' do
|
19
|
+
assert_equal(schema_instance_class.new([[]]), schema_instance_json_coder.load([[]]))
|
20
|
+
end
|
21
|
+
describe 'array' do
|
22
|
+
let(:options) { {array: true} }
|
23
|
+
it 'loads an array of hashes' do
|
24
|
+
data = [{"foo" => "bar"}, {"foo" => "baz"}]
|
25
|
+
assert_equal([schema_instance_class.new('foo' => 'bar'), schema_instance_class.new('foo' => 'baz')], schema_instance_json_coder.load(data))
|
26
|
+
end
|
27
|
+
it 'loads an empty array' do
|
28
|
+
assert_equal([], schema_instance_json_coder.load([]))
|
29
|
+
end
|
30
|
+
it 'loads a not an array' do
|
31
|
+
assert_raises(TypeError) do
|
32
|
+
schema_instance_json_coder.load(Object.new)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
describe 'array schema' do
|
37
|
+
let(:schema) { {items: {properties: {foo: {}, bar: {}}}} }
|
38
|
+
it 'loads an array of hashes' do
|
39
|
+
data = [{"foo" => "bar"}, {"foo" => "baz"}]
|
40
|
+
assert_equal(schema_instance_class.new([{'foo' => 'bar'}, {'foo' => 'baz'}]), schema_instance_json_coder.load(data))
|
41
|
+
end
|
42
|
+
it 'loads an empty array' do
|
43
|
+
assert_equal(schema_instance_class.new([]), schema_instance_json_coder.load([]))
|
44
|
+
end
|
45
|
+
it 'loads a not an array' do
|
46
|
+
instance = Object.new
|
47
|
+
assert_equal(schema_instance_class.new(instance), schema_instance_json_coder.load(instance))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
describe 'dump' do
|
52
|
+
it 'dumps nil' do
|
53
|
+
assert_nil(schema_instance_json_coder.dump(nil))
|
54
|
+
end
|
55
|
+
it 'dumps a schema_instance_class' do
|
56
|
+
assert_equal({"foo" => "x", "bar" => "y"}, schema_instance_json_coder.dump(schema_instance_class.new(foo: 'x', bar: 'y')))
|
57
|
+
end
|
58
|
+
it 'dumps something else' do
|
59
|
+
assert_raises(TypeError) do
|
60
|
+
schema_instance_json_coder.dump(Object.new)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
it 'dumps some of the keys of a schema_instance_class after loading in a partial one' do
|
64
|
+
schema_instance_class = schema_instance_json_coder.load({'foo' => 'who'})
|
65
|
+
assert_equal({'foo' => 'who'}, schema_instance_json_coder.dump(schema_instance_class))
|
66
|
+
schema_instance_class.bar = 'whar'
|
67
|
+
assert_equal({'foo' => 'who', 'bar' => 'whar'}, schema_instance_json_coder.dump(schema_instance_class))
|
68
|
+
end
|
69
|
+
describe 'array' do
|
70
|
+
let(:options) { {array: true} }
|
71
|
+
it 'dumps an array of schema_instances' do
|
72
|
+
schema_instances = [schema_instance_class.new(foo: 'x', bar: 'y'), schema_instance_class.new(foo: 'z', bar: 'q')]
|
73
|
+
assert_equal([{"foo" => "x", "bar" => "y"}, {"foo" => "z", "bar" => "q"}], schema_instance_json_coder.dump(schema_instances))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
describe 'array schema' do
|
77
|
+
let(:schema) { {items: {properties: {foo: {}, bar: {}}}} }
|
78
|
+
it 'dumps a schema_instance array' do
|
79
|
+
schema_instances = schema_instance_class.new([{foo: 'x', bar: 'y'}, {foo: 'z', bar: 'q'}])
|
80
|
+
assert_equal([{"foo" => "x", "bar" => "y"}, {"foo" => "z", "bar" => "q"}], schema_instance_json_coder.dump(schema_instances))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/test/jsi_json_node_test.rb
CHANGED
@@ -30,8 +30,8 @@ describe JSI::JSON::Node do
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
describe '#pointer' do
|
33
|
-
it 'is a ::JSON::
|
34
|
-
assert_instance_of(::JSON::
|
33
|
+
it 'is a JSI::JSON::Pointer' do
|
34
|
+
assert_instance_of(JSI::JSON::Pointer, JSI::JSON::Node.new({}, []).pointer)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
describe '#content' do
|
@@ -170,7 +170,7 @@ describe JSI::JSON::Node do
|
|
170
170
|
assert_equal([], root_from_sub.path)
|
171
171
|
assert_equal({'a' => {'b' => []}}, root_from_sub.content)
|
172
172
|
assert_equal(node, root_from_sub)
|
173
|
-
err = assert_raises(::JSON::
|
173
|
+
err = assert_raises(JSI::JSON::Pointer::ReferenceError) do
|
174
174
|
root_from_sub.parent_node
|
175
175
|
end
|
176
176
|
assert_match(/\Acannot access parent of root node: #\{<JSI::JSON::HashNode/, err.message)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe JSI::JSON::Pointer do
|
4
|
+
# For example, given the document
|
5
|
+
let(:document) do
|
6
|
+
{
|
7
|
+
"foo" => ["bar", "baz"],
|
8
|
+
"" => 0,
|
9
|
+
"a/b" => 1,
|
10
|
+
"c%d" => 2,
|
11
|
+
"e^f" => 3,
|
12
|
+
"g|h" => 4,
|
13
|
+
"i\\j" => 5,
|
14
|
+
"k\"l" => 6,
|
15
|
+
" " => 7,
|
16
|
+
"m~n" => 8,
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'initialize from pointer' do
|
21
|
+
it 'parses' do
|
22
|
+
# The following strings evaluate to the accompanying values:
|
23
|
+
evaluations = [
|
24
|
+
"" , document,
|
25
|
+
"/foo" , ["bar", "baz"],
|
26
|
+
"/foo/0", "bar",
|
27
|
+
"/" , 0,
|
28
|
+
"/a~1b" , 1,
|
29
|
+
"/c%d" , 2,
|
30
|
+
"/e^f" , 3,
|
31
|
+
"/g|h" , 4,
|
32
|
+
"/i\\j" , 5,
|
33
|
+
"/k\"l" , 6,
|
34
|
+
"/ " , 7,
|
35
|
+
"/m~0n" , 8,
|
36
|
+
]
|
37
|
+
evaluations.each_slice(2) do |pointer, value|
|
38
|
+
assert_equal(value, JSI::JSON::Pointer.new(:pointer, pointer).evaluate(document))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'raises for invalid syntax' do
|
43
|
+
err = assert_raises(JSI::JSON::Pointer::PointerSyntaxError) do
|
44
|
+
JSI::JSON::Pointer.new(:pointer, "this does not begin with slash").evaluate(document)
|
45
|
+
end
|
46
|
+
assert_equal("Invalid pointer syntax in \"this does not begin with slash\": pointer must begin with /", err.message)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
describe 'initialize from fragment' do
|
50
|
+
# For example, given the document
|
51
|
+
let(:document) do
|
52
|
+
{
|
53
|
+
"foo" => ["bar", "baz"],
|
54
|
+
"" => 0,
|
55
|
+
"a/b" => 1,
|
56
|
+
"c%d" => 2,
|
57
|
+
"e^f" => 3,
|
58
|
+
"g|h" => 4,
|
59
|
+
"i\\j" => 5,
|
60
|
+
"k\"l" => 6,
|
61
|
+
" " => 7,
|
62
|
+
"m~n" => 8,
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'parses' do
|
67
|
+
# the following URI fragment identifiers evaluate to the accompanying values:
|
68
|
+
evaluations = [
|
69
|
+
'#', document,
|
70
|
+
'#/foo', ["bar", "baz"],
|
71
|
+
'#/foo/0', "bar",
|
72
|
+
'#/', 0,
|
73
|
+
'#/a~1b', 1,
|
74
|
+
'#/c%25d', 2,
|
75
|
+
'#/e%5Ef', 3,
|
76
|
+
'#/g%7Ch', 4,
|
77
|
+
'#/i%5Cj', 5,
|
78
|
+
'#/k%22l', 6,
|
79
|
+
'#/%20', 7,
|
80
|
+
'#/m~0n', 8,
|
81
|
+
]
|
82
|
+
evaluations.each_slice(2) do |fragment, value|
|
83
|
+
assert_equal(value, JSI::JSON::Pointer.new(:fragment, fragment).evaluate(document))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'raises for invalid syntax' do
|
88
|
+
err = assert_raises(JSI::JSON::Pointer::PointerSyntaxError) do
|
89
|
+
JSI::JSON::Pointer.new(:fragment, "this does not begin with #").evaluate(document)
|
90
|
+
end
|
91
|
+
assert_equal("Invalid fragment syntax in \"this does not begin with #\": fragment must begin with #", err.message)
|
92
|
+
err = assert_raises(JSI::JSON::Pointer::PointerSyntaxError) do
|
93
|
+
JSI::JSON::Pointer.new(:fragment, "#this does not begin with slash").evaluate(document)
|
94
|
+
end
|
95
|
+
assert_equal("Invalid pointer syntax in \"this does not begin with slash\": pointer must begin with /", err.message)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class JSONifiable
|
4
|
+
def initialize(object)
|
5
|
+
@object = object
|
6
|
+
end
|
7
|
+
def as_json
|
8
|
+
@object
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe JSI::Typelike do
|
13
|
+
describe 'as_json' do
|
14
|
+
it 'expresses as json' do
|
15
|
+
assert_equal({}, JSI::Typelike.as_json({}))
|
16
|
+
assert_equal([], JSI::Typelike.as_json([]))
|
17
|
+
|
18
|
+
# symbols to string
|
19
|
+
assert_equal(['a'], JSI::Typelike.as_json([:a]))
|
20
|
+
|
21
|
+
# set
|
22
|
+
assert_equal(['a'], JSI::Typelike.as_json(Set.new(['a'])))
|
23
|
+
|
24
|
+
# responds to #to_hash / #to_ary but naught else
|
25
|
+
assert_equal({'a' => 'b'}, JSI::Typelike.as_json(SortOfHash.new({'a' => 'b'})))
|
26
|
+
assert_equal(['a'], JSI::Typelike.as_json(SortOfArray.new(['a'])))
|
27
|
+
|
28
|
+
# symbol keys to string
|
29
|
+
assert_equal({'a' => 'b'}, JSI::Typelike.as_json({a: 'b'}))
|
30
|
+
# non string/symbol key
|
31
|
+
err = assert_raises(TypeError) { JSI::Typelike.as_json({nil => 0}) }
|
32
|
+
assert_equal('json object (hash) cannot be keyed with: nil', err.message)
|
33
|
+
|
34
|
+
# schema
|
35
|
+
schema = JSI::Schema.from_object({'type' => 'array'})
|
36
|
+
assert_equal({'type' => 'array'}, JSI::Typelike.as_json(schema))
|
37
|
+
|
38
|
+
# JSI
|
39
|
+
assert_equal(['a'], JSI::Typelike.as_json(JSI.class_for_schema(schema).new(['a'])))
|
40
|
+
|
41
|
+
# JSON::Node
|
42
|
+
assert_equal(['a'], JSI::Typelike.as_json(JSI::JSON::Node.new_doc(['a'])))
|
43
|
+
|
44
|
+
# #as_json
|
45
|
+
assert_equal(['a'], JSI::Typelike.as_json(JSONifiable.new(['a'])))
|
46
|
+
|
47
|
+
# not jsonifiable
|
48
|
+
object = Object.new
|
49
|
+
err = assert_raises(TypeError) { JSI::Typelike.as_json(object) }
|
50
|
+
assert_equal("cannot express object as json: #{object.pretty_inspect.chomp}", err.message)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ethan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json-schema
|
@@ -98,25 +98,26 @@ files:
|
|
98
98
|
- lib/jsi.rb
|
99
99
|
- lib/jsi/base.rb
|
100
100
|
- lib/jsi/base/to_rb.rb
|
101
|
+
- lib/jsi/jsi_coder.rb
|
101
102
|
- lib/jsi/json-schema-fragments.rb
|
102
103
|
- lib/jsi/json.rb
|
103
104
|
- lib/jsi/json/node.rb
|
105
|
+
- lib/jsi/json/pointer.rb
|
104
106
|
- lib/jsi/schema.rb
|
105
|
-
- lib/jsi/schema_instance_json_coder.rb
|
106
|
-
- lib/jsi/struct_json_coder.rb
|
107
107
|
- lib/jsi/typelike_modules.rb
|
108
108
|
- lib/jsi/util.rb
|
109
109
|
- lib/jsi/version.rb
|
110
110
|
- test/base_array_test.rb
|
111
111
|
- test/base_hash_test.rb
|
112
112
|
- test/base_test.rb
|
113
|
+
- test/jsi_coder_test.rb
|
113
114
|
- test/jsi_json_arraynode_test.rb
|
114
115
|
- test/jsi_json_hashnode_test.rb
|
115
116
|
- test/jsi_json_node_test.rb
|
117
|
+
- test/jsi_json_pointer_test.rb
|
116
118
|
- test/jsi_test.rb
|
117
|
-
- test/
|
119
|
+
- test/jsi_typelike_as_json_test.rb
|
118
120
|
- test/schema_test.rb
|
119
|
-
- test/struct_json_coder_test.rb
|
120
121
|
- test/test_helper.rb
|
121
122
|
- test/util_test.rb
|
122
123
|
homepage: https://github.com/notEthan/jsi
|
@@ -147,12 +148,13 @@ test_files:
|
|
147
148
|
- test/base_array_test.rb
|
148
149
|
- test/base_hash_test.rb
|
149
150
|
- test/base_test.rb
|
151
|
+
- test/jsi_coder_test.rb
|
150
152
|
- test/jsi_json_arraynode_test.rb
|
151
153
|
- test/jsi_json_hashnode_test.rb
|
152
154
|
- test/jsi_json_node_test.rb
|
155
|
+
- test/jsi_json_pointer_test.rb
|
153
156
|
- test/jsi_test.rb
|
154
|
-
- test/
|
157
|
+
- test/jsi_typelike_as_json_test.rb
|
155
158
|
- test/schema_test.rb
|
156
|
-
- test/struct_json_coder_test.rb
|
157
159
|
- test/test_helper.rb
|
158
160
|
- test/util_test.rb
|
@@ -1,83 +0,0 @@
|
|
1
|
-
module JSI
|
2
|
-
# this is a ActiveRecord serialization class intended to store JSON in the
|
3
|
-
# database column and expose a ruby class once loaded on a model instance.
|
4
|
-
# this allows for better ruby idioms to access to properties, and definition
|
5
|
-
# of related methods on the loaded class.
|
6
|
-
#
|
7
|
-
# the first argument, `loaded_class`, is the class which will be used to
|
8
|
-
# instantiate the column data. properties of the loaded class will correspond
|
9
|
-
# to keys of the json object in the database.
|
10
|
-
#
|
11
|
-
# the column data may be either a single instance of the loaded class
|
12
|
-
# (represented as one json object) or an array of them (represented as a json
|
13
|
-
# array of json objects), indicated by the keyword argument `array`.
|
14
|
-
#
|
15
|
-
# the column behind the attribute may be an actual JSON column (postgres json
|
16
|
-
# or jsonb - hstore should work too if you only have string attributes) or a
|
17
|
-
# serialized string, indicated by the keyword argument `string`.
|
18
|
-
class ObjectJSONCoder
|
19
|
-
class Error < StandardError
|
20
|
-
end
|
21
|
-
class LoadError < Error
|
22
|
-
end
|
23
|
-
class DumpError < Error
|
24
|
-
end
|
25
|
-
|
26
|
-
def initialize(loaded_class, string: false, array: false, next_coder: nil)
|
27
|
-
@loaded_class = loaded_class
|
28
|
-
# this notes the order of the keys as they were in the json, used by dump_object to generate
|
29
|
-
# json that is equivalent to the json/jsonifiable that came in, so that AR's #changed_attributes
|
30
|
-
# can tell whether the attribute has been changed.
|
31
|
-
@loaded_class.send(:attr_accessor, :object_json_coder_keys_order)
|
32
|
-
@string = string
|
33
|
-
@array = array
|
34
|
-
@next_coder = next_coder
|
35
|
-
end
|
36
|
-
|
37
|
-
def load(column_data)
|
38
|
-
return nil if column_data.nil?
|
39
|
-
data = @string ? ::JSON.parse(column_data) : column_data
|
40
|
-
object = if @array
|
41
|
-
unless data.respond_to?(:to_ary)
|
42
|
-
raise TypeError, "expected array-like column data; got: #{data.class}: #{data.inspect}"
|
43
|
-
end
|
44
|
-
data.map { |el| load_object(el) }
|
45
|
-
else
|
46
|
-
load_object(data)
|
47
|
-
end
|
48
|
-
object = @next_coder.load(object) if @next_coder
|
49
|
-
object
|
50
|
-
end
|
51
|
-
|
52
|
-
def dump(object)
|
53
|
-
object = @next_coder.dump(object) if @next_coder
|
54
|
-
return nil if object.nil?
|
55
|
-
jsonifiable = begin
|
56
|
-
if @array
|
57
|
-
unless object.respond_to?(:to_ary)
|
58
|
-
raise DumpError, "expected array-like attribute; got: #{object.class}: #{object.inspect}"
|
59
|
-
end
|
60
|
-
object.map do |el|
|
61
|
-
dump_object(el)
|
62
|
-
end
|
63
|
-
else
|
64
|
-
dump_object(object)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
@string ? ::JSON.generate(jsonifiable) : jsonifiable
|
68
|
-
end
|
69
|
-
end
|
70
|
-
# this is a ActiveRecord serialization class intended to store JSON in the
|
71
|
-
# database column and expose a given JSI::Base subclass once loaded
|
72
|
-
# on a model instance.
|
73
|
-
class SchemaInstanceJSONCoder < ObjectJSONCoder
|
74
|
-
private
|
75
|
-
def load_object(data)
|
76
|
-
@loaded_class.new(data)
|
77
|
-
end
|
78
|
-
|
79
|
-
def dump_object(object)
|
80
|
-
JSI::Typelike.as_json(object)
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
module JSI
|
2
|
-
# this is a ActiveRecord serialization class intended to store JSON in the
|
3
|
-
# database column and expose a Struct subclass once loaded on a model instance.
|
4
|
-
class StructJSONCoder < ObjectJSONCoder
|
5
|
-
private
|
6
|
-
def load_object(data)
|
7
|
-
if data.is_a?(Hash)
|
8
|
-
good_keys = @loaded_class.members.map(&:to_s)
|
9
|
-
bad_keys = data.keys - good_keys
|
10
|
-
unless bad_keys.empty?
|
11
|
-
raise LoadError, "expected keys #{good_keys}; got unrecognized keys: #{bad_keys}"
|
12
|
-
end
|
13
|
-
instance = @loaded_class.new(*@loaded_class.members.map { |m| data[m.to_s] })
|
14
|
-
instance.object_json_coder_keys_order = data.keys
|
15
|
-
instance
|
16
|
-
else
|
17
|
-
raise LoadError, "expected instance(s) of #{Hash}; got: #{data.class}: #{data.inspect}"
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def dump_object(object)
|
22
|
-
if object.is_a?(@loaded_class)
|
23
|
-
keys = (object.object_json_coder_keys_order || []) | @loaded_class.members.map(&:to_s)
|
24
|
-
keys.map { |member| {member => object[member]} }.inject({}, &:update)
|
25
|
-
else
|
26
|
-
raise TypeError, "expected instance(s) of #{@loaded_class}; got: #{object.class}: #{object.inspect}"
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
@@ -1,121 +0,0 @@
|
|
1
|
-
require_relative 'test_helper'
|
2
|
-
|
3
|
-
describe JSI::SchemaInstanceJSONCoder do
|
4
|
-
let(:schema_instance_class) { JSI.class_for_schema(properties: {foo: {}, bar: {}}) }
|
5
|
-
let(:options) { {} }
|
6
|
-
let(:schema_instance_json_coder) { JSI::SchemaInstanceJSONCoder.new(schema_instance_class, options) }
|
7
|
-
describe 'json' do
|
8
|
-
describe 'load' do
|
9
|
-
it 'loads nil' do
|
10
|
-
assert_nil(schema_instance_json_coder.load(nil))
|
11
|
-
end
|
12
|
-
it 'loads a hash' do
|
13
|
-
assert_equal(schema_instance_class.new('foo' => 'bar'), schema_instance_json_coder.load({"foo" => "bar"}))
|
14
|
-
end
|
15
|
-
it 'loads something else' do
|
16
|
-
assert_equal(schema_instance_class.new([[]]), schema_instance_json_coder.load([[]]))
|
17
|
-
end
|
18
|
-
describe 'array' do
|
19
|
-
let(:options) { {array: true} }
|
20
|
-
it 'loads an array of hashes' do
|
21
|
-
data = [{"foo" => "bar"}, {"foo" => "baz"}]
|
22
|
-
assert_equal([schema_instance_class.new('foo' => 'bar'), schema_instance_class.new('foo' => 'baz')], schema_instance_json_coder.load(data))
|
23
|
-
end
|
24
|
-
it 'loads an empty array' do
|
25
|
-
assert_equal([], schema_instance_json_coder.load([]))
|
26
|
-
end
|
27
|
-
it 'loads a not an array' do
|
28
|
-
assert_raises(TypeError) do
|
29
|
-
schema_instance_json_coder.load(Object.new)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
describe 'dump' do
|
35
|
-
it 'dumps nil' do
|
36
|
-
assert_nil(schema_instance_json_coder.dump(nil))
|
37
|
-
end
|
38
|
-
it 'dumps a schema_instance_class' do
|
39
|
-
assert_equal({"foo" => "x", "bar" => "y"}, schema_instance_json_coder.dump(schema_instance_class.new(foo: 'x', bar: 'y')))
|
40
|
-
end
|
41
|
-
it 'dumps something else' do
|
42
|
-
assert_raises(TypeError) do
|
43
|
-
schema_instance_json_coder.dump(Object.new)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
it 'dumps some of the keys of a schema_instance_class after loading in a partial one' do
|
47
|
-
schema_instance_class = schema_instance_json_coder.load({'foo' => 'who'})
|
48
|
-
assert_equal({'foo' => 'who'}, schema_instance_json_coder.dump(schema_instance_class))
|
49
|
-
schema_instance_class.bar = 'whar'
|
50
|
-
assert_equal({'foo' => 'who', 'bar' => 'whar'}, schema_instance_json_coder.dump(schema_instance_class))
|
51
|
-
end
|
52
|
-
describe 'array' do
|
53
|
-
let(:options) { {array: true} }
|
54
|
-
it 'dumps an array of schema_instances' do
|
55
|
-
schema_instances = [schema_instance_class.new(foo: 'x', bar: 'y'), schema_instance_class.new(foo: 'z', bar: 'q')]
|
56
|
-
assert_equal([{"foo" => "x", "bar" => "y"}, {"foo" => "z", "bar" => "q"}], schema_instance_json_coder.dump(schema_instances))
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
describe 'string' do
|
62
|
-
let(:options) { {string: true} }
|
63
|
-
describe 'load' do
|
64
|
-
it 'loads nil' do
|
65
|
-
assert_nil(schema_instance_json_coder.load(nil))
|
66
|
-
end
|
67
|
-
it 'loads a hash' do
|
68
|
-
assert_equal(schema_instance_class.new('foo' => 'bar'), schema_instance_json_coder.load('{"foo": "bar"}'))
|
69
|
-
end
|
70
|
-
it 'loads something else' do
|
71
|
-
assert_equal(schema_instance_class.new([[]]), schema_instance_json_coder.load('[[]]'))
|
72
|
-
end
|
73
|
-
it 'loads something that is not a json string' do
|
74
|
-
assert_raises(::JSON::ParserError) do
|
75
|
-
schema_instance_json_coder.load('??')
|
76
|
-
end
|
77
|
-
end
|
78
|
-
describe 'array' do
|
79
|
-
let(:options) { {string: true, array: true} }
|
80
|
-
it 'loads an array of hashes' do
|
81
|
-
data = '[{"foo": "bar"}, {"foo": "baz"}]'
|
82
|
-
assert_equal([schema_instance_class.new('foo' => 'bar'), schema_instance_class.new('foo' => 'baz')], schema_instance_json_coder.load(data))
|
83
|
-
end
|
84
|
-
it 'loads an empty array' do
|
85
|
-
assert_equal([], schema_instance_json_coder.load('[]'))
|
86
|
-
end
|
87
|
-
it 'loads a not an array' do
|
88
|
-
assert_raises(TypeError) do
|
89
|
-
schema_instance_json_coder.load('{}')
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
describe 'dump' do
|
95
|
-
it 'dumps nil' do
|
96
|
-
assert_nil(schema_instance_json_coder.dump(nil))
|
97
|
-
end
|
98
|
-
it 'dumps a schema_instance_class' do
|
99
|
-
assert_equal('{"foo":"x","bar":"y"}', schema_instance_json_coder.dump(schema_instance_class.new(foo: 'x', bar: 'y')))
|
100
|
-
end
|
101
|
-
it 'dumps something else' do
|
102
|
-
assert_raises(TypeError) do
|
103
|
-
schema_instance_json_coder.dump(Object.new)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
it 'dumps some of the keys of a schema_instance_class after loading in a partial one' do
|
107
|
-
schema_instance_class = schema_instance_json_coder.load('{"foo": "who"}')
|
108
|
-
assert_equal("{\"foo\":\"who\"}", schema_instance_json_coder.dump(schema_instance_class))
|
109
|
-
schema_instance_class.bar = 'whar'
|
110
|
-
assert_equal("{\"foo\":\"who\",\"bar\":\"whar\"}", schema_instance_json_coder.dump(schema_instance_class))
|
111
|
-
end
|
112
|
-
describe 'array' do
|
113
|
-
let(:options) { {string: true, array: true} }
|
114
|
-
it 'dumps an array of schema_instances' do
|
115
|
-
schema_instances = [schema_instance_class.new(foo: 'x', bar: 'y'), schema_instance_class.new(foo: 'z', bar: 'q')]
|
116
|
-
assert_equal('[{"foo":"x","bar":"y"},{"foo":"z","bar":"q"}]', schema_instance_json_coder.dump(schema_instances))
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
@@ -1,130 +0,0 @@
|
|
1
|
-
require_relative 'test_helper'
|
2
|
-
|
3
|
-
describe JSI::StructJSONCoder do
|
4
|
-
let(:struct) { Struct.new(:foo, :bar) }
|
5
|
-
let(:options) { {} }
|
6
|
-
let(:struct_json_coder) { JSI::StructJSONCoder.new(struct, options) }
|
7
|
-
describe 'json' do
|
8
|
-
describe 'load' do
|
9
|
-
it 'loads nil' do
|
10
|
-
assert_nil(struct_json_coder.load(nil))
|
11
|
-
end
|
12
|
-
it 'loads a hash' do
|
13
|
-
assert_equal(struct.new('bar'), struct_json_coder.load({"foo" => "bar"}))
|
14
|
-
end
|
15
|
-
it 'loads something else' do
|
16
|
-
assert_raises(JSI::StructJSONCoder::LoadError) do
|
17
|
-
struct_json_coder.load([[]])
|
18
|
-
end
|
19
|
-
end
|
20
|
-
it 'loads unrecognized keys' do
|
21
|
-
assert_raises(JSI::StructJSONCoder::LoadError) do
|
22
|
-
struct_json_coder.load({"uhoh" => "spaghettio"})
|
23
|
-
end
|
24
|
-
end
|
25
|
-
describe 'array' do
|
26
|
-
let(:options) { {array: true} }
|
27
|
-
it 'loads an array of hashes' do
|
28
|
-
data = [{"foo" => "bar"}, {"foo" => "baz"}]
|
29
|
-
assert_equal([struct.new('bar'), struct.new('baz')], struct_json_coder.load(data))
|
30
|
-
end
|
31
|
-
it 'loads an empty array' do
|
32
|
-
assert_equal([], struct_json_coder.load([]))
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
describe 'dump' do
|
37
|
-
it 'dumps nil' do
|
38
|
-
assert_nil(struct_json_coder.dump(nil))
|
39
|
-
end
|
40
|
-
it 'dumps a struct' do
|
41
|
-
assert_equal({"foo" => "x", "bar" => "y"}, struct_json_coder.dump(struct.new('x', 'y')))
|
42
|
-
end
|
43
|
-
it 'dumps something else' do
|
44
|
-
assert_raises(TypeError) do
|
45
|
-
struct_json_coder.dump(Object.new)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
it 'dumps all the keys of a struct after loading in a partial one' do
|
49
|
-
struct = struct_json_coder.load({'foo' => 'who'})
|
50
|
-
assert_equal({'foo' => 'who', 'bar' => nil}, struct_json_coder.dump(struct))
|
51
|
-
struct.bar = 'whar'
|
52
|
-
assert_equal({'foo' => 'who', 'bar' => 'whar'}, struct_json_coder.dump(struct))
|
53
|
-
end
|
54
|
-
describe 'array' do
|
55
|
-
let(:options) { {array: true} }
|
56
|
-
it 'dumps an array of structs' do
|
57
|
-
structs = [struct.new('x', 'y'), struct.new('z', 'q')]
|
58
|
-
assert_equal([{"foo" => "x", "bar" => "y"}, {"foo" => "z", "bar" => "q"}], struct_json_coder.dump(structs))
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
describe 'string' do
|
64
|
-
let(:options) { {string: true} }
|
65
|
-
describe 'load' do
|
66
|
-
it 'loads nil' do
|
67
|
-
assert_nil(struct_json_coder.load(nil))
|
68
|
-
end
|
69
|
-
it 'loads a hash' do
|
70
|
-
assert_equal(struct.new('bar'), struct_json_coder.load('{"foo": "bar"}'))
|
71
|
-
end
|
72
|
-
it 'loads something else' do
|
73
|
-
assert_raises(JSI::StructJSONCoder::LoadError) do
|
74
|
-
struct_json_coder.load('[[]]')
|
75
|
-
end
|
76
|
-
end
|
77
|
-
it 'loads something that is not a json string' do
|
78
|
-
assert_raises(JSON::ParserError) do
|
79
|
-
struct_json_coder.load('??')
|
80
|
-
end
|
81
|
-
end
|
82
|
-
it 'loads unrecognized keys' do
|
83
|
-
assert_raises(JSI::StructJSONCoder::LoadError) do
|
84
|
-
struct_json_coder.load('{"uhoh": "spaghettio"}')
|
85
|
-
end
|
86
|
-
end
|
87
|
-
describe 'array' do
|
88
|
-
let(:options) { {string: true, array: true} }
|
89
|
-
it 'loads an array of hashes' do
|
90
|
-
data = '[{"foo": "bar"}, {"foo": "baz"}]'
|
91
|
-
assert_equal([struct.new('bar'), struct.new('baz')], struct_json_coder.load(data))
|
92
|
-
end
|
93
|
-
it 'loads an empty array' do
|
94
|
-
assert_equal([], struct_json_coder.load('[]'))
|
95
|
-
end
|
96
|
-
it 'loads a not an array' do
|
97
|
-
assert_raises(TypeError) do
|
98
|
-
struct_json_coder.load('{}')
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
describe 'dump' do
|
104
|
-
it 'dumps nil' do
|
105
|
-
assert_nil(struct_json_coder.dump(nil))
|
106
|
-
end
|
107
|
-
it 'dumps a struct' do
|
108
|
-
assert_equal('{"foo":"x","bar":"y"}', struct_json_coder.dump(struct.new('x', 'y')))
|
109
|
-
end
|
110
|
-
it 'dumps something else' do
|
111
|
-
assert_raises(TypeError) do
|
112
|
-
struct_json_coder.dump(Object.new)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
it 'dumps all the keys of a struct after loading in a partial one' do
|
116
|
-
struct = struct_json_coder.load('{"foo": "who"}')
|
117
|
-
assert_equal("{\"foo\":\"who\",\"bar\":null}", struct_json_coder.dump(struct))
|
118
|
-
struct.bar = 'whar'
|
119
|
-
assert_equal("{\"foo\":\"who\",\"bar\":\"whar\"}", struct_json_coder.dump(struct))
|
120
|
-
end
|
121
|
-
describe 'array' do
|
122
|
-
let(:options) { {string: true, array: true} }
|
123
|
-
it 'dumps an array of structs' do
|
124
|
-
structs = [struct.new('x', 'y'), struct.new('z', 'q')]
|
125
|
-
assert_equal('[{"foo":"x","bar":"y"},{"foo":"z","bar":"q"}]', struct_json_coder.dump(structs))
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|