scorpio 0.2.3 → 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 +5 -0
- data/README.md +4 -2
- data/lib/scorpio.rb +9 -22
- data/lib/scorpio/google_api_document.rb +13 -16
- data/lib/scorpio/openapi.rb +141 -143
- data/lib/scorpio/openapi/document.rb +167 -0
- data/lib/scorpio/openapi/operation.rb +208 -0
- data/lib/scorpio/openapi/operations_scope.rb +29 -0
- data/lib/scorpio/openapi/v3/server.rb +32 -0
- data/lib/scorpio/request.rb +227 -0
- data/lib/scorpio/resource_base.rb +148 -182
- data/lib/scorpio/response.rb +34 -0
- data/lib/scorpio/ur.rb +33 -0
- data/lib/scorpio/version.rb +1 -1
- data/scorpio.gemspec +3 -4
- metadata +32 -36
- data/lib/scorpio/json-schema-fragments.rb +0 -191
- data/lib/scorpio/json.rb +0 -5
- data/lib/scorpio/json/node.rb +0 -256
- data/lib/scorpio/schema.rb +0 -249
- data/lib/scorpio/schema_instance_base.rb +0 -325
- data/lib/scorpio/schema_instance_base/to_rb.rb +0 -127
- data/lib/scorpio/schema_instance_json_coder.rb +0 -83
- data/lib/scorpio/struct_json_coder.rb +0 -30
- data/lib/scorpio/typelike_modules.rb +0 -164
- data/lib/scorpio/util.rb +0 -89
- data/lib/scorpio/util/faraday/response_media_type.rb +0 -15
@@ -0,0 +1,34 @@
|
|
1
|
+
module Scorpio
|
2
|
+
class Response < ::Ur::Response
|
3
|
+
def response_schema
|
4
|
+
ur.scorpio_request.operation.response_schema(status: status, media_type: media_type)
|
5
|
+
end
|
6
|
+
|
7
|
+
def body_object
|
8
|
+
# TODO handle media types like `application/schema-instance+json` or vendor things like github's
|
9
|
+
if media_type == 'application/json'
|
10
|
+
if body.empty?
|
11
|
+
# an empty body isn't valid json, of course, but we'll just return nil for it.
|
12
|
+
body_object = nil
|
13
|
+
else
|
14
|
+
begin
|
15
|
+
body_object = ::JSON.parse(body)
|
16
|
+
#rescue ::JSON::ParserError
|
17
|
+
# TODO
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
if response_schema && (body_object.respond_to?(:to_hash) || body_object.respond_to?(:to_ary))
|
22
|
+
body_object = JSI.class_for_schema(response_schema).new(JSI::JSON::Node.new_doc(body_object))
|
23
|
+
end
|
24
|
+
|
25
|
+
body_object
|
26
|
+
elsif media_type == 'text/plain'
|
27
|
+
body
|
28
|
+
else
|
29
|
+
# we will return the body if we do not have a supported parsing. for now.
|
30
|
+
body
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/scorpio/ur.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Scorpio
|
2
|
+
class Ur < ::Ur
|
3
|
+
attr_accessor :scorpio_request
|
4
|
+
|
5
|
+
def class_for_schema(schema)
|
6
|
+
jsi_class_for_schema = super
|
7
|
+
if jsi_class_for_schema == ::Ur::Response
|
8
|
+
Scorpio::Response
|
9
|
+
else
|
10
|
+
jsi_class_for_schema
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def raise_on_http_error
|
15
|
+
error_class = Scorpio.error_classes_by_status[response.status]
|
16
|
+
error_class ||= if (400..499).include?(response.status)
|
17
|
+
ClientError
|
18
|
+
elsif (500..599).include?(response.status)
|
19
|
+
ServerError
|
20
|
+
elsif !response.success?
|
21
|
+
HTTPError
|
22
|
+
end
|
23
|
+
if error_class
|
24
|
+
message = "Error calling operation #{scorpio_request.operation.operationId} on #{self}:\n" + response.body
|
25
|
+
raise(error_class.new(message).tap do |e|
|
26
|
+
e.ur = self
|
27
|
+
e.response_object = response.body_object
|
28
|
+
end)
|
29
|
+
end
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/scorpio/version.rb
CHANGED
data/scorpio.gemspec
CHANGED
@@ -20,11 +20,9 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
|
23
|
+
spec.add_dependency "jsi", "~> 0.0.3"
|
24
|
+
spec.add_dependency "ur", "~> 0.0.2"
|
23
25
|
spec.add_dependency "faraday"
|
24
|
-
# we are monkey patching json-schema with a fix that has not been merged in a timely fashion.
|
25
|
-
spec.add_dependency "json-schema", "~> 2.8"
|
26
|
-
spec.add_dependency "api_hammer"
|
27
|
-
spec.add_development_dependency "bundler", "~> 1.12"
|
28
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
29
27
|
spec.add_development_dependency "minitest", "~> 5.0"
|
30
28
|
spec.add_development_dependency "minitest-around"
|
@@ -33,6 +31,7 @@ Gem::Specification.new do |spec|
|
|
33
31
|
spec.add_development_dependency "rack", "~> 1.0"
|
34
32
|
spec.add_development_dependency "rack-accept"
|
35
33
|
spec.add_development_dependency "rack-test"
|
34
|
+
spec.add_development_dependency "api_hammer"
|
36
35
|
spec.add_development_dependency "activerecord"
|
37
36
|
spec.add_development_dependency "sqlite3"
|
38
37
|
spec.add_development_dependency "database_cleaner"
|
metadata
CHANGED
@@ -1,45 +1,45 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scorpio
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ethan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-01-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: jsi
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.0.3
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 0.0.3
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: ur
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.0.2
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.0.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: faraday
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -52,20 +52,6 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: bundler
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '1.12'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '1.12'
|
69
55
|
- !ruby/object:Gem::Dependency
|
70
56
|
name: rake
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -178,6 +164,20 @@ dependencies:
|
|
178
164
|
- - ">="
|
179
165
|
- !ruby/object:Gem::Version
|
180
166
|
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: api_hammer
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
181
|
- !ruby/object:Gem::Dependency
|
182
182
|
name: activerecord
|
183
183
|
requirement: !ruby/object:Gem::Requirement
|
@@ -255,20 +255,16 @@ files:
|
|
255
255
|
- documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest.yml
|
256
256
|
- lib/scorpio.rb
|
257
257
|
- lib/scorpio/google_api_document.rb
|
258
|
-
- lib/scorpio/json-schema-fragments.rb
|
259
|
-
- lib/scorpio/json.rb
|
260
|
-
- lib/scorpio/json/node.rb
|
261
258
|
- lib/scorpio/openapi.rb
|
259
|
+
- lib/scorpio/openapi/document.rb
|
260
|
+
- lib/scorpio/openapi/operation.rb
|
261
|
+
- lib/scorpio/openapi/operations_scope.rb
|
262
|
+
- lib/scorpio/openapi/v3/server.rb
|
262
263
|
- lib/scorpio/pickle_adapter.rb
|
264
|
+
- lib/scorpio/request.rb
|
263
265
|
- lib/scorpio/resource_base.rb
|
264
|
-
- lib/scorpio/
|
265
|
-
- lib/scorpio/
|
266
|
-
- lib/scorpio/schema_instance_base/to_rb.rb
|
267
|
-
- lib/scorpio/schema_instance_json_coder.rb
|
268
|
-
- lib/scorpio/struct_json_coder.rb
|
269
|
-
- lib/scorpio/typelike_modules.rb
|
270
|
-
- lib/scorpio/util.rb
|
271
|
-
- lib/scorpio/util/faraday/response_media_type.rb
|
266
|
+
- lib/scorpio/response.rb
|
267
|
+
- lib/scorpio/ur.rb
|
272
268
|
- lib/scorpio/version.rb
|
273
269
|
- scorpio.gemspec
|
274
270
|
homepage: https://github.com/notEthan/scorpio
|
@@ -1,191 +0,0 @@
|
|
1
|
-
require "json-schema"
|
2
|
-
|
3
|
-
# apply the changes from https://github.com/ruby-json-schema/json-schema/pull/382
|
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
|
-
reference_tokens.inject(document) do |value, token|
|
87
|
-
if value.is_a?(Array)
|
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.size).include?(token)
|
95
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
|
96
|
-
end
|
97
|
-
elsif value.is_a?(Hash)
|
98
|
-
unless value.key?(token)
|
99
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
|
100
|
-
end
|
101
|
-
else
|
102
|
-
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
|
103
|
-
end
|
104
|
-
value[token]
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
# the pointer string representation of this Pointer
|
109
|
-
def pointer
|
110
|
-
reference_tokens.map { |t| '/' + t.to_s.gsub('~', '~0').gsub('/', '~1') }.join('')
|
111
|
-
end
|
112
|
-
|
113
|
-
# the fragment string representation of this Pointer
|
114
|
-
def fragment
|
115
|
-
'#' + Addressable::URI.escape(pointer)
|
116
|
-
end
|
117
|
-
|
118
|
-
def to_s
|
119
|
-
"#<#{self.class.inspect} #{@type} = #{representation_s}>"
|
120
|
-
end
|
121
|
-
|
122
|
-
private
|
123
|
-
|
124
|
-
def representation_s
|
125
|
-
if @type == :fragment
|
126
|
-
fragment
|
127
|
-
elsif @type == :pointer
|
128
|
-
pointer
|
129
|
-
else
|
130
|
-
reference_tokens.inspect
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
# json-schema/validator.rb
|
138
|
-
|
139
|
-
module JSON
|
140
|
-
class Validator
|
141
|
-
def initialize(schema_data, data, opts={})
|
142
|
-
@options = @@default_opts.clone.merge(opts)
|
143
|
-
@errors = []
|
144
|
-
|
145
|
-
validator = self.class.validator_for_name(@options[:version])
|
146
|
-
@options[:version] = validator
|
147
|
-
@options[:schema_reader] ||= self.class.schema_reader
|
148
|
-
|
149
|
-
@validation_options = @options[:record_errors] ? {:record_errors => true} : {}
|
150
|
-
@validation_options[:insert_defaults] = true if @options[:insert_defaults]
|
151
|
-
@validation_options[:strict] = true if @options[:strict] == true
|
152
|
-
@validation_options[:clear_cache] = true if !@@cache_schemas || @options[:clear_cache]
|
153
|
-
|
154
|
-
@@mutex.synchronize { @base_schema = initialize_schema(schema_data) }
|
155
|
-
@original_data = data
|
156
|
-
@data = initialize_data(data)
|
157
|
-
@@mutex.synchronize { build_schemas(@base_schema) }
|
158
|
-
|
159
|
-
# If the :fragment option is set, try and validate against the fragment
|
160
|
-
if opts[:fragment]
|
161
|
-
@base_schema = schema_from_fragment(@base_schema, opts[:fragment])
|
162
|
-
end
|
163
|
-
|
164
|
-
# validate the schema, if requested
|
165
|
-
if @options[:validate_schema]
|
166
|
-
if @base_schema.schema["$schema"]
|
167
|
-
base_validator = self.class.validator_for_name(@base_schema.schema["$schema"])
|
168
|
-
end
|
169
|
-
metaschema = base_validator ? base_validator.metaschema : validator.metaschema
|
170
|
-
# Don't clear the cache during metaschema validation!
|
171
|
-
self.class.validate!(metaschema, @base_schema.schema, {:clear_cache => false})
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
def schema_from_fragment(base_schema, fragment)
|
176
|
-
schema_uri = base_schema.uri
|
177
|
-
|
178
|
-
pointer = JSON::Schema::Pointer.new(:fragment, fragment)
|
179
|
-
|
180
|
-
base_schema = JSON::Schema.new(pointer.evaluate(base_schema.schema), schema_uri, @options[:version])
|
181
|
-
|
182
|
-
if @options[:list]
|
183
|
-
base_schema.to_array_schema
|
184
|
-
elsif base_schema.is_a?(Hash)
|
185
|
-
JSON::Schema.new(base_schema, schema_uri, @options[:version])
|
186
|
-
else
|
187
|
-
base_schema
|
188
|
-
end
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
data/lib/scorpio/json.rb
DELETED
data/lib/scorpio/json/node.rb
DELETED
@@ -1,256 +0,0 @@
|
|
1
|
-
module Scorpio
|
2
|
-
module JSON
|
3
|
-
# Scorpio::JSON::Node is an abstraction of a node within a JSON document.
|
4
|
-
# it aims to act like the underlying data type of the node's content
|
5
|
-
# (Hash or Array, generally) in most cases, defining methods of Hash
|
6
|
-
# and Array which delegate to the content. However, destructive methods
|
7
|
-
# are not defined, as modifying the content of a node would change it
|
8
|
-
# for any other nodes in the document that contain or refer to it.
|
9
|
-
#
|
10
|
-
# methods that return a modified copy such as #merge are defined, and
|
11
|
-
# return a copy of the document with the content of the node modified.
|
12
|
-
# the original node's document and content are untouched.
|
13
|
-
class Node
|
14
|
-
# if the content of the document at the given path is a Hash, returns
|
15
|
-
# a HashNode; if an Array, returns ArrayNode. otherwise returns a
|
16
|
-
# regular Node, though, for the most part this will be called with Hash
|
17
|
-
# or Array content.
|
18
|
-
def self.new_by_type(document, path)
|
19
|
-
node = Node.new(document, path)
|
20
|
-
content = node.content
|
21
|
-
if content.is_a?(Hash)
|
22
|
-
HashNode.new(document, path)
|
23
|
-
elsif content.is_a?(Array)
|
24
|
-
ArrayNode.new(document, path)
|
25
|
-
else
|
26
|
-
node
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# a Node represents the content of a document at a given path.
|
31
|
-
def initialize(document, path)
|
32
|
-
raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})") unless path.is_a?(Array)
|
33
|
-
@document = document
|
34
|
-
@path = path.dup.freeze
|
35
|
-
@pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
|
36
|
-
end
|
37
|
-
|
38
|
-
# the path of this Node within its document
|
39
|
-
attr_reader :path
|
40
|
-
# the document containing this Node at is path
|
41
|
-
attr_reader :document
|
42
|
-
# ::JSON::Schema::Pointer representing the path to this node within its document
|
43
|
-
attr_reader :pointer
|
44
|
-
|
45
|
-
# the raw content of this Node from the underlying document at this Node's path.
|
46
|
-
def content
|
47
|
-
pointer.evaluate(document)
|
48
|
-
end
|
49
|
-
|
50
|
-
def [](k)
|
51
|
-
node = self
|
52
|
-
content = node.content
|
53
|
-
if content.is_a?(Hash) && !content.key?(k)
|
54
|
-
node = node.deref
|
55
|
-
content = node.content
|
56
|
-
end
|
57
|
-
begin
|
58
|
-
el = content[k]
|
59
|
-
rescue TypeError => e
|
60
|
-
raise(e.class, e.message + "\nsubscripting with #{k.pretty_inspect.chomp} (#{k.class}) from #{content.class.inspect}. self is: #{pretty_inspect.chomp}", e.backtrace)
|
61
|
-
end
|
62
|
-
if el.is_a?(Hash) || el.is_a?(Array)
|
63
|
-
self.class.new_by_type(node.document, node.path + [k])
|
64
|
-
else
|
65
|
-
el
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def []=(k, v)
|
70
|
-
if v.is_a?(Node)
|
71
|
-
content[k] = v.content
|
72
|
-
else
|
73
|
-
content[k] = v
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def deref
|
78
|
-
content = self.content
|
79
|
-
|
80
|
-
return self unless content.is_a?(Hash) && content['$ref'].is_a?(String)
|
81
|
-
|
82
|
-
if content['$ref'][/\A#/]
|
83
|
-
return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(content['$ref'])).deref
|
84
|
-
end
|
85
|
-
|
86
|
-
# HAX for how google does refs and ids
|
87
|
-
if document_node['schemas'].respond_to?(:to_hash)
|
88
|
-
if document_node['schemas'][content['$ref']]
|
89
|
-
return document_node['schemas'][content['$ref']]
|
90
|
-
end
|
91
|
-
_, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == content['$ref'] }
|
92
|
-
if deref_by_id
|
93
|
-
return deref_by_id
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
#raise(NotImplementedError, "cannot dereference #{content['$ref']}") # TODO
|
98
|
-
return self
|
99
|
-
end
|
100
|
-
|
101
|
-
# a Node at the root of the document
|
102
|
-
def document_node
|
103
|
-
Node.new_by_type(document, [])
|
104
|
-
end
|
105
|
-
|
106
|
-
# the parent of this node. if this node is the document root (its path is empty), raises
|
107
|
-
# ::JSON::Schema::Pointer::ReferenceError.
|
108
|
-
def parent_node
|
109
|
-
if path.empty?
|
110
|
-
raise(::JSON::Schema::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}")
|
111
|
-
else
|
112
|
-
Node.new_by_type(document, path[0...-1])
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
# the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
117
|
-
def pointer_path
|
118
|
-
pointer.pointer
|
119
|
-
end
|
120
|
-
# the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
121
|
-
def fragment
|
122
|
-
pointer.fragment
|
123
|
-
end
|
124
|
-
|
125
|
-
def as_json(*opt)
|
126
|
-
Typelike.as_json(content, *opt)
|
127
|
-
end
|
128
|
-
|
129
|
-
# takes a block. the block is yielded the content of this node. the block MUST return a modified
|
130
|
-
# copy of that content (and NOT modify the object it is given).
|
131
|
-
def modified_copy
|
132
|
-
# we need to preserve the rest of the document, but modify the content at our path.
|
133
|
-
#
|
134
|
-
# this is actually a bit tricky. we can't modify the original document, obviously.
|
135
|
-
# we could do a deep copy, but that's expensive. instead, we make a copy of each array
|
136
|
-
# or hash in the path above this node. this node's content is modified by the caller, and
|
137
|
-
# that is recursively merged up to the document root. the recursion is done with a
|
138
|
-
# y combinator, for no other reason than that was a fun way to implement it.
|
139
|
-
modified_document = ycomb do |rec|
|
140
|
-
proc do |subdocument, subpath|
|
141
|
-
if subpath == []
|
142
|
-
yield(subdocument)
|
143
|
-
else
|
144
|
-
car = subpath[0]
|
145
|
-
cdr = subpath[1..-1]
|
146
|
-
if subdocument.respond_to?(:to_hash)
|
147
|
-
car_object = rec.call(subdocument[car], cdr)
|
148
|
-
if car_object.object_id == subdocument[car].object_id
|
149
|
-
subdocument
|
150
|
-
else
|
151
|
-
subdocument.merge({car => car_object})
|
152
|
-
end
|
153
|
-
elsif subdocument.respond_to?(:to_ary)
|
154
|
-
if car.is_a?(String) && car =~ /\A\d+\z/
|
155
|
-
car = car.to_i
|
156
|
-
end
|
157
|
-
unless car.is_a?(Integer)
|
158
|
-
raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
|
159
|
-
end
|
160
|
-
car_object = rec.call(subdocument[car], cdr)
|
161
|
-
if car_object.object_id == subdocument[car].object_id
|
162
|
-
subdocument
|
163
|
-
else
|
164
|
-
subdocument.dup.tap do |arr|
|
165
|
-
arr[car] = car_object
|
166
|
-
end
|
167
|
-
end
|
168
|
-
else
|
169
|
-
raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end.call(document, path)
|
174
|
-
Node.new_by_type(modified_document, path)
|
175
|
-
end
|
176
|
-
|
177
|
-
def object_group_text
|
178
|
-
"fragment=#{fragment.inspect}"
|
179
|
-
end
|
180
|
-
def inspect
|
181
|
-
"\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>"
|
182
|
-
end
|
183
|
-
def pretty_print(q)
|
184
|
-
q.instance_exec(self) do |obj|
|
185
|
-
text "\#<#{obj.class.inspect} #{obj.object_group_text}"
|
186
|
-
group_sub {
|
187
|
-
nest(2) {
|
188
|
-
breakable ' '
|
189
|
-
pp obj.content
|
190
|
-
}
|
191
|
-
}
|
192
|
-
breakable ''
|
193
|
-
text '>'
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
def fingerprint
|
198
|
-
{is_node: self.is_a?(Scorpio::JSON::Node), document: document, path: path}
|
199
|
-
end
|
200
|
-
include FingerprintHash
|
201
|
-
end
|
202
|
-
|
203
|
-
class ArrayNode < Node
|
204
|
-
def each
|
205
|
-
return to_enum(__method__) { content.size } unless block_given?
|
206
|
-
content.each_index { |i| yield self[i] }
|
207
|
-
self
|
208
|
-
end
|
209
|
-
|
210
|
-
def to_ary
|
211
|
-
to_a
|
212
|
-
end
|
213
|
-
|
214
|
-
include Enumerable
|
215
|
-
include Arraylike
|
216
|
-
|
217
|
-
def as_json(*opt) # needs redefined after including Enumerable
|
218
|
-
Typelike.as_json(content, *opt)
|
219
|
-
end
|
220
|
-
|
221
|
-
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
|
222
|
-
# we override these methods from Arraylike
|
223
|
-
SAFE_INDEX_ONLY_METHODS.each do |method_name|
|
224
|
-
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
225
|
-
end
|
226
|
-
end
|
227
|
-
|
228
|
-
class HashNode < Node
|
229
|
-
def each(&block)
|
230
|
-
return to_enum(__method__) { content.size } unless block_given?
|
231
|
-
if block.arity > 1
|
232
|
-
content.each_key { |k| yield k, self[k] }
|
233
|
-
else
|
234
|
-
content.each_key { |k| yield [k, self[k]] }
|
235
|
-
end
|
236
|
-
self
|
237
|
-
end
|
238
|
-
|
239
|
-
def to_hash
|
240
|
-
inject({}) { |h, (k, v)| h[k] = v; h }
|
241
|
-
end
|
242
|
-
|
243
|
-
include Enumerable
|
244
|
-
include Hashlike
|
245
|
-
|
246
|
-
def as_json(*opt) # needs redefined after including Enumerable
|
247
|
-
Typelike.as_json(content, *opt)
|
248
|
-
end
|
249
|
-
|
250
|
-
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
|
251
|
-
SAFE_KEY_ONLY_METHODS.each do |method_name|
|
252
|
-
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
253
|
-
end
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|