jsi 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/LICENSE.md +613 -0
- data/README.md +62 -37
- data/jsi.gemspec +8 -12
- data/lib/jsi.rb +11 -0
- data/lib/jsi/base.rb +196 -258
- data/lib/jsi/base/to_rb.rb +2 -0
- data/lib/jsi/jsi_coder.rb +20 -15
- data/lib/jsi/json-schema-fragments.rb +2 -0
- data/lib/jsi/json.rb +2 -0
- data/lib/jsi/json/node.rb +45 -88
- data/lib/jsi/json/pointer.rb +102 -5
- data/lib/jsi/metaschema.rb +7 -0
- data/lib/jsi/metaschema_node.rb +217 -0
- data/lib/jsi/pathed_node.rb +5 -0
- data/lib/jsi/schema.rb +146 -169
- data/lib/jsi/schema_classes.rb +112 -47
- data/lib/jsi/simple_wrap.rb +8 -3
- data/lib/jsi/typelike_modules.rb +31 -39
- data/lib/jsi/util.rb +27 -47
- data/lib/jsi/version.rb +1 -1
- data/lib/schemas/json-schema.org/draft-04/schema.rb +7 -0
- data/lib/schemas/json-schema.org/draft-06/schema.rb +7 -0
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/base_array_test.rb +174 -60
- data/test/base_hash_test.rb +179 -46
- data/test/base_test.rb +163 -94
- data/test/jsi_coder_test.rb +14 -14
- data/test/jsi_json_arraynode_test.rb +10 -10
- data/test/jsi_json_hashnode_test.rb +14 -14
- data/test/jsi_json_node_test.rb +83 -136
- data/test/jsi_typelike_as_json_test.rb +1 -1
- data/test/metaschema_node_test.rb +19 -0
- data/test/schema_module_test.rb +21 -0
- data/test/schema_test.rb +40 -50
- data/test/test_helper.rb +35 -3
- data/test/util_test.rb +8 -8
- metadata +24 -16
- data/LICENSE.txt +0 -21
data/lib/jsi/schema_classes.rb
CHANGED
@@ -1,80 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
2
|
-
#
|
3
|
-
module
|
4
|
-
#
|
5
|
-
#
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
# JSI Schema Modules are extended with JSI::SchemaModule
|
5
|
+
module SchemaModule
|
6
|
+
# @return [String] absolute schema_id of the schema this module represents.
|
7
|
+
# see {Schema#schema_id}.
|
8
|
+
def schema_id
|
9
|
+
schema.schema_id
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return [String]
|
13
|
+
def inspect
|
14
|
+
idfrag = schema.schema_id || schema.node_ptr.fragment
|
15
|
+
if name
|
16
|
+
"#{name} (#{idfrag})"
|
17
|
+
else
|
18
|
+
"(JSI Schema Module: #{idfrag})"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# invokes {JSI::Schema#new_jsi} on this module's schema, passing the given instance.
|
23
|
+
# @return [JSI::Base] a JSI whose instance is the given instance
|
24
|
+
def new_jsi(instance, *a, &b)
|
25
|
+
schema.new_jsi(instance, *a, &b)
|
11
26
|
end
|
12
|
-
|
27
|
+
end
|
13
28
|
|
29
|
+
# this module is just a namespace for schema classes.
|
30
|
+
module SchemaClasses
|
14
31
|
class << self
|
15
32
|
include Memoize
|
16
33
|
|
17
34
|
# see {JSI.class_for_schema}
|
18
35
|
def class_for_schema(schema_object)
|
19
|
-
|
20
|
-
Class.new(Base).instance_exec(
|
36
|
+
jsi_memoize(:class_for_schema, JSI::Schema.from_object(schema_object)) do |schema|
|
37
|
+
Class.new(Base).instance_exec(schema) do |schema|
|
21
38
|
define_singleton_method(:schema) { schema }
|
22
39
|
define_method(:schema) { schema }
|
23
|
-
include(
|
40
|
+
include(schema.jsi_schema_module)
|
24
41
|
|
25
42
|
jsi_class = self
|
26
43
|
define_method(:jsi_class) { jsi_class }
|
27
44
|
|
28
|
-
SchemaClasses.instance_exec(self) { |klass| @classes_by_id[klass.schema_id] = klass }
|
29
|
-
|
30
45
|
self
|
31
46
|
end
|
32
47
|
end
|
33
48
|
end
|
34
49
|
|
35
|
-
# a module for the given schema, with accessor methods for any object
|
36
|
-
#
|
37
|
-
# called #schema to access the {JSI::Schema} this module represents.
|
38
|
-
#
|
39
|
-
# accessor methods are defined on these modules so that methods can be
|
40
|
-
# defined on {JSI.class_for_schema} classes without method redefinition
|
41
|
-
# warnings. additionally, these overriding instance methods can call
|
42
|
-
# `super` to invoke the normal accessor behavior.
|
50
|
+
# a module for the given schema, with accessor methods for any object property names the schema
|
51
|
+
# identifies (see {JSI::Schema#described_object_property_names}).
|
43
52
|
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
memoize(:module_for_schema, schema__, conflicting_modules) do |schema_, conflicting_modules_|
|
53
|
+
# defines a singleton method #schema to access the {JSI::Schema} this module represents, and extends
|
54
|
+
# the module with {JSI::SchemaModule}.
|
55
|
+
def module_for_schema(schema_object)
|
56
|
+
schema = JSI::Schema.from_object(schema_object)
|
57
|
+
jsi_memoize(:module_for_schema, schema) do |schema|
|
50
58
|
Module.new.tap do |m|
|
51
|
-
m.
|
59
|
+
m.module_eval do
|
52
60
|
define_singleton_method(:schema) { schema }
|
53
|
-
define_singleton_method(:schema_id) do
|
54
|
-
schema.schema_id
|
55
|
-
end
|
56
|
-
define_singleton_method(:inspect) do
|
57
|
-
%Q(#<Module for Schema: #{schema_id}>)
|
58
|
-
end
|
59
61
|
|
60
|
-
|
62
|
+
extend SchemaModule
|
63
|
+
|
64
|
+
include JSI::SchemaClasses.accessor_module_for_schema(schema, conflicting_modules: [JSI::Base, JSI::BaseArray, JSI::BaseHash])
|
65
|
+
|
66
|
+
@possibly_schema_node = schema
|
67
|
+
extend(SchemaModulePossibly)
|
68
|
+
extend(JSI::SchemaClasses.accessor_module_for_schema(schema.schema, conflicting_modules: [Module, SchemaModule, SchemaModulePossibly]))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param schema [JSI::Schema] a schema for which to define accessors for any described property names
|
75
|
+
# @param conflicting_modules [Enumerable<Module>] an array of modules (or classes) which
|
76
|
+
# may be used alongside the accessor module. methods defined by any conflicting_module
|
77
|
+
# will not be defined as accessors.
|
78
|
+
# @return [Module] a module of accessors (setters and getters) for described property names of the given
|
79
|
+
# schema
|
80
|
+
def accessor_module_for_schema(schema, conflicting_modules: )
|
81
|
+
jsi_memoize(:accessor_module_for_schema, schema, conflicting_modules) do |schema, conflicting_modules|
|
82
|
+
Module.new.tap do |m|
|
83
|
+
m.module_eval do
|
84
|
+
conflicting_instance_methods = (conflicting_modules + [m]).map do |mod|
|
61
85
|
mod.instance_methods + mod.private_instance_methods
|
62
86
|
end.inject(Set.new, &:|)
|
63
87
|
accessors_to_define = schema.described_object_property_names.map(&:to_s) - conflicting_instance_methods.map(&:to_s)
|
64
88
|
accessors_to_define.each do |property_name|
|
65
89
|
define_method(property_name) do
|
66
|
-
|
67
|
-
self[property_name]
|
68
|
-
else
|
69
|
-
raise(NoMethodError, "schema instance of class #{self.class} does not respond to []; cannot call reader '#{property_name}'. instance is #{instance.pretty_inspect.chomp}")
|
70
|
-
end
|
90
|
+
self[property_name]
|
71
91
|
end
|
72
92
|
define_method("#{property_name}=") do |value|
|
73
|
-
|
74
|
-
self[property_name] = value
|
75
|
-
else
|
76
|
-
raise(NoMethodError, "schema instance of class #{self.class} does not respond to []=; cannot call writer '#{property_name}='. instance is #{instance.pretty_inspect.chomp}")
|
77
|
-
end
|
93
|
+
self[property_name] = value
|
78
94
|
end
|
79
95
|
end
|
80
96
|
end
|
@@ -83,4 +99,53 @@ module JSI
|
|
83
99
|
end
|
84
100
|
end
|
85
101
|
end
|
102
|
+
|
103
|
+
# a JSI::Schema module and a JSI::NotASchemaModule are both a SchemaModulePossibly.
|
104
|
+
# this module provides a #[] method.
|
105
|
+
module SchemaModulePossibly
|
106
|
+
attr_reader :possibly_schema_node
|
107
|
+
|
108
|
+
# subscripting a JSI schema module or a NotASchemaModule will subscript the node, and
|
109
|
+
# if the result is a JSI::Schema, return a JSI::Schema class; if it is a PathedNode,
|
110
|
+
# return a NotASchemaModule; or if it is another value (a basic type), return that value.
|
111
|
+
#
|
112
|
+
# @param token [Object]
|
113
|
+
# @return [Class, NotASchemaModule, Object]
|
114
|
+
def [](token)
|
115
|
+
sub = @possibly_schema_node[token]
|
116
|
+
if sub.is_a?(JSI::Schema)
|
117
|
+
sub.jsi_schema_module
|
118
|
+
elsif sub.is_a?(JSI::PathedNode)
|
119
|
+
NotASchemaModule.new(sub)
|
120
|
+
else
|
121
|
+
sub
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# a schema module is a module which represents a schema. a NotASchemaModule represents
|
127
|
+
# a node in a schema's document which is not a schema, such as the 'properties'
|
128
|
+
# node (which contains schemas but is not a schema).
|
129
|
+
#
|
130
|
+
# a NotASchemaModule is extended with the module_for_schema of the node's schema.
|
131
|
+
#
|
132
|
+
# NotASchemaModule holds a node which is not a schema. when subscripted, it subscripts
|
133
|
+
# its node. if the value is a JSI::Schema, its schema module is returned. if the value
|
134
|
+
# is another node, a NotASchemaModule for that node is returned. otherwise - when the
|
135
|
+
# value is a basic type - that value itself is returned.
|
136
|
+
class NotASchemaModule
|
137
|
+
# @param node [JSI::PathedNode]
|
138
|
+
def initialize(node)
|
139
|
+
unless node.is_a?(JSI::PathedNode)
|
140
|
+
raise(TypeError, "not JSI::PathedNode: #{node.pretty_inspect.chomp}")
|
141
|
+
end
|
142
|
+
if node.is_a?(JSI::Schema)
|
143
|
+
raise(TypeError, "cannot instantiate NotASchemaModule for a JSI::Schema node: #{node.pretty_inspect.chomp}")
|
144
|
+
end
|
145
|
+
@possibly_schema_node = node
|
146
|
+
extend(JSI::SchemaClasses.accessor_module_for_schema(node.schema, conflicting_modules: [NotASchemaModule, SchemaModulePossibly]))
|
147
|
+
end
|
148
|
+
|
149
|
+
include SchemaModulePossibly
|
150
|
+
end
|
86
151
|
end
|
data/lib/jsi/simple_wrap.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
2
|
-
SimpleWrap = JSI.
|
4
|
+
SimpleWrap = JSI::Schema.new({
|
5
|
+
"additionalProperties": {"$ref": "#"},
|
6
|
+
"items": {"$ref": "#"}
|
7
|
+
}).jsi_schema_module
|
3
8
|
|
4
|
-
# SimpleWrap is a JSI
|
5
|
-
|
9
|
+
# SimpleWrap is a JSI schema module which recursively wraps nested structures
|
10
|
+
module SimpleWrap
|
6
11
|
end
|
7
12
|
end
|
data/lib/jsi/typelike_modules.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
2
4
|
# a module relating to objects that act like Hash or Array instances
|
3
5
|
module Typelike
|
@@ -136,36 +138,31 @@ module JSI
|
|
136
138
|
# @return [String] basically the same #inspect as Hash, but has the
|
137
139
|
# class name and, if responsive, self's #object_group_text
|
138
140
|
def inspect
|
139
|
-
object_group_str =
|
140
|
-
"\#{<#{
|
141
|
+
object_group_str = (respond_to?(:object_group_text) ? self.object_group_text : [self.class]).join(' ')
|
142
|
+
"\#{<#{object_group_str}>#{empty? ? '' : ' '}#{self.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}}"
|
141
143
|
end
|
142
144
|
|
143
|
-
|
144
|
-
def to_s
|
145
|
-
inspect
|
146
|
-
end
|
145
|
+
alias_method :to_s, :inspect
|
147
146
|
|
148
147
|
# pretty-prints a representation this node to the given printer
|
149
148
|
# @return [void]
|
150
149
|
def pretty_print(q)
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
pp v
|
162
|
-
}
|
150
|
+
object_group_str = (respond_to?(:object_group_text) ? object_group_text : [self.class]).join(' ')
|
151
|
+
q.text "\#{<#{object_group_str}>"
|
152
|
+
q.group_sub {
|
153
|
+
q.nest(2) {
|
154
|
+
q.breakable(any? { true } ? ' ' : '')
|
155
|
+
q.seplist(self, nil, :each_pair) { |k, v|
|
156
|
+
q.group {
|
157
|
+
q.pp k
|
158
|
+
q.text ' => '
|
159
|
+
q.pp v
|
163
160
|
}
|
164
161
|
}
|
165
162
|
}
|
166
|
-
|
167
|
-
|
168
|
-
|
163
|
+
}
|
164
|
+
q.breakable ''
|
165
|
+
q.text '}'
|
169
166
|
end
|
170
167
|
end
|
171
168
|
|
@@ -217,32 +214,27 @@ module JSI
|
|
217
214
|
# @return [String] basically the same #inspect as Array, but has the
|
218
215
|
# class name and, if responsive, self's #object_group_text
|
219
216
|
def inspect
|
220
|
-
object_group_str =
|
221
|
-
"\#[<#{
|
217
|
+
object_group_str = (respond_to?(:object_group_text) ? object_group_text : [self.class]).join(' ')
|
218
|
+
"\#[<#{object_group_str}>#{empty? ? '' : ' '}#{self.map { |e| e.inspect }.join(', ')}]"
|
222
219
|
end
|
223
220
|
|
224
|
-
|
225
|
-
def to_s
|
226
|
-
inspect
|
227
|
-
end
|
221
|
+
alias_method :to_s, :inspect
|
228
222
|
|
229
223
|
# pretty-prints a representation this node to the given printer
|
230
224
|
# @return [void]
|
231
225
|
def pretty_print(q)
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
pp e
|
240
|
-
}
|
226
|
+
object_group_str = (respond_to?(:object_group_text) ? object_group_text : [self.class]).join(' ')
|
227
|
+
q.text "\#[<#{object_group_str}>"
|
228
|
+
q.group_sub {
|
229
|
+
q.nest(2) {
|
230
|
+
q.breakable(any? { true } ? ' ' : '')
|
231
|
+
q.seplist(self, nil, :each) { |e|
|
232
|
+
q.pp e
|
241
233
|
}
|
242
234
|
}
|
243
|
-
|
244
|
-
|
245
|
-
|
235
|
+
}
|
236
|
+
q.breakable ''
|
237
|
+
q.text ']'
|
246
238
|
end
|
247
239
|
end
|
248
240
|
end
|
data/lib/jsi/util.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSI
|
2
4
|
module Util
|
3
5
|
# a proc which does nothing
|
@@ -17,68 +19,44 @@ module JSI
|
|
17
19
|
# @param hash [#to_hash] the hash from which to convert symbol keys to strings
|
18
20
|
# @return [same class as the param `hash`, or Hash if the former cannot be done] a
|
19
21
|
# hash(-like) instance containing no symbol keys
|
20
|
-
def stringify_symbol_keys(
|
21
|
-
unless
|
22
|
-
raise(ArgumentError, "expected argument to be a hash; got #{
|
22
|
+
def stringify_symbol_keys(hashlike)
|
23
|
+
unless hashlike.respond_to?(:to_hash)
|
24
|
+
raise(ArgumentError, "expected argument to be a hash; got #{hashlike.class.inspect}: #{hashlike.pretty_inspect.chomp}")
|
23
25
|
end
|
24
|
-
JSI::Typelike.modified_copy(
|
25
|
-
changed = false
|
26
|
+
JSI::Typelike.modified_copy(hashlike) do |hash|
|
26
27
|
out = {}
|
27
|
-
|
28
|
-
|
29
|
-
changed = true
|
30
|
-
k = k.to_s
|
31
|
-
end
|
32
|
-
out[k] = v
|
28
|
+
hash.each do |k, v|
|
29
|
+
out[k.is_a?(Symbol) ? k.to_s : k] = v
|
33
30
|
end
|
34
|
-
|
31
|
+
out
|
35
32
|
end
|
36
33
|
end
|
37
34
|
|
38
35
|
def deep_stringify_symbol_keys(object)
|
39
36
|
if object.respond_to?(:to_hash)
|
40
37
|
JSI::Typelike.modified_copy(object) do |hash|
|
41
|
-
changed = false
|
42
38
|
out = {}
|
43
39
|
(hash.respond_to?(:each) ? hash : hash.to_hash).each do |k, v|
|
44
|
-
|
45
|
-
changed = true
|
46
|
-
k = k.to_s
|
47
|
-
end
|
48
|
-
out_k = deep_stringify_symbol_keys(k)
|
49
|
-
out_v = deep_stringify_symbol_keys(v)
|
50
|
-
changed = true if out_k.object_id != k.object_id
|
51
|
-
changed = true if out_v.object_id != v.object_id
|
52
|
-
out[out_k] = out_v
|
40
|
+
out[k.is_a?(Symbol) ? k.to_s : deep_stringify_symbol_keys(k)] = deep_stringify_symbol_keys(v)
|
53
41
|
end
|
54
|
-
|
42
|
+
out
|
55
43
|
end
|
56
44
|
elsif object.respond_to?(:to_ary)
|
57
45
|
JSI::Typelike.modified_copy(object) do |ary|
|
58
|
-
|
59
|
-
|
60
|
-
out_e = deep_stringify_symbol_keys(e)
|
61
|
-
changed = true if out_e.object_id != e.object_id
|
62
|
-
out_e
|
46
|
+
(ary.respond_to?(:each) ? ary : ary.to_ary).map do |e|
|
47
|
+
deep_stringify_symbol_keys(e)
|
63
48
|
end
|
64
|
-
changed ? out : ary
|
65
49
|
end
|
66
50
|
else
|
67
51
|
object
|
68
52
|
end
|
69
53
|
end
|
70
54
|
|
71
|
-
# @param object_group_text [Array<String>]
|
72
|
-
# @return [String]
|
73
|
-
def object_group_str(object_group_text)
|
74
|
-
object_group_text.compact.map { |t| " #{t}" }.join('')
|
75
|
-
end
|
76
|
-
|
77
55
|
# this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
|
78
56
|
# to define a recursive function to return the length of an array:
|
79
57
|
#
|
80
58
|
# length = ycomb do |len|
|
81
|
-
# proc{|list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
|
59
|
+
# proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
|
82
60
|
# end
|
83
61
|
#
|
84
62
|
# see https://secure.wikimedia.org/wikipedia/en/wiki/Fixed_point_combinator#Y_combinator
|
@@ -92,33 +70,35 @@ module JSI
|
|
92
70
|
extend Util
|
93
71
|
|
94
72
|
module FingerprintHash
|
73
|
+
# overrides BasicObject#==
|
95
74
|
def ==(other)
|
96
|
-
object_id == other.object_id || (other.respond_to?(:
|
75
|
+
object_id == other.object_id || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
|
97
76
|
end
|
98
77
|
|
99
78
|
alias_method :eql?, :==
|
100
79
|
|
80
|
+
# overrides Kernel#hash
|
101
81
|
def hash
|
102
|
-
|
82
|
+
jsi_fingerprint.hash
|
103
83
|
end
|
104
84
|
end
|
105
85
|
|
106
86
|
module Memoize
|
107
|
-
def
|
108
|
-
@
|
109
|
-
@
|
87
|
+
def jsi_memoize(key, *args_)
|
88
|
+
@jsi_memos ||= {}
|
89
|
+
@jsi_memos[key] ||= Hash.new do |h, args|
|
110
90
|
h[args] = yield(*args)
|
111
91
|
end
|
112
|
-
@
|
92
|
+
@jsi_memos[key][args_]
|
113
93
|
end
|
114
94
|
|
115
|
-
def
|
116
|
-
@
|
117
|
-
if @
|
95
|
+
def jsi_clear_memo(key, *args)
|
96
|
+
@jsi_memos ||= {}
|
97
|
+
if @jsi_memos[key]
|
118
98
|
if args.empty?
|
119
|
-
@
|
99
|
+
@jsi_memos[key].clear
|
120
100
|
else
|
121
|
-
@
|
101
|
+
@jsi_memos[key].delete(args)
|
122
102
|
end
|
123
103
|
end
|
124
104
|
end
|