jsi 0.2.1 → 0.3.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 +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
|