scorpio 0.1.0 → 0.2.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 +2 -0
- data/README.md +108 -25
- data/Rakefile +2 -2
- data/documents/openapis.org/v3/schema.json +1239 -0
- data/documents/openapis.org/v3/schema.yml +794 -0
- data/lib/scorpio.rb +68 -68
- data/lib/scorpio/google_api_document.rb +18 -24
- data/lib/scorpio/json-schema-fragments.rb +1 -1
- data/lib/scorpio/json/node.rb +106 -74
- data/lib/scorpio/openapi.rb +146 -68
- data/lib/scorpio/pickle_adapter.rb +3 -3
- data/lib/scorpio/{model.rb → resource_base.rb} +187 -145
- data/lib/scorpio/schema.rb +188 -76
- data/lib/scorpio/schema_instance_base.rb +309 -0
- data/lib/scorpio/schema_instance_base/to_rb.rb +127 -0
- data/lib/scorpio/typelike_modules.rb +120 -4
- data/lib/scorpio/util.rb +83 -0
- data/lib/scorpio/util/faraday/response_media_type.rb +15 -0
- data/lib/scorpio/version.rb +1 -1
- data/scorpio.gemspec +0 -1
- metadata +10 -19
- data/lib/scorpio/schema_object_base.rb +0 -227
data/lib/scorpio.rb
CHANGED
@@ -8,7 +8,11 @@ module Scorpio
|
|
8
8
|
def self.root
|
9
9
|
@root ||= Pathname.new(__FILE__).dirname.parent.expand_path
|
10
10
|
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require "scorpio/util"
|
11
14
|
|
15
|
+
module Scorpio
|
12
16
|
# generally put in code paths that are not expected to be valid control flow paths.
|
13
17
|
# rather a NotImplementedCorrectlyError. but that's too long.
|
14
18
|
class Bug < NotImplementedError
|
@@ -17,78 +21,74 @@ module Scorpio
|
|
17
21
|
proc { |v| define_singleton_method(:error_classes_by_status) { v } }.call({})
|
18
22
|
class Error < StandardError; end
|
19
23
|
class HTTPError < Error
|
20
|
-
define_singleton_method(:status)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
class BadRequest400Error < ClientError; status(400); end
|
27
|
-
class Unauthorized401Error < ClientError; status(401); end
|
28
|
-
class PaymentRequired402Error < ClientError; status(402); end
|
29
|
-
class Forbidden403Error < ClientError; status(403); end
|
30
|
-
class NotFound404Error < ClientError; status(404); end
|
31
|
-
class MethodNotAllowed405Error < ClientError; status(405); end
|
32
|
-
class NotAcceptable406Error < ClientError; status(406); end
|
33
|
-
class ProxyAuthenticationRequired407Error < ClientError; status(407); end
|
34
|
-
class RequestTimeout408Error < ClientError; status(408); end
|
35
|
-
class Conflict409Error < ClientError; status(409); end
|
36
|
-
class Gone410Error < ClientError; status(410); end
|
37
|
-
class LengthRequired411Error < ClientError; status(411); end
|
38
|
-
class PreconditionFailed412Error < ClientError; status(412); end
|
39
|
-
class PayloadTooLarge413Error < ClientError; status(413); end
|
40
|
-
class URITooLong414Error < ClientError; status(414); end
|
41
|
-
class UnsupportedMediaType415Error < ClientError; status(415); end
|
42
|
-
class RangeNotSatisfiable416Error < ClientError; status(416); end
|
43
|
-
class ExpectationFailed417Error < ClientError; status(417); end
|
44
|
-
class ImaTeapot418Error < ClientError; status(418); end
|
45
|
-
class MisdirectedRequest421Error < ClientError; status(421); end
|
46
|
-
class UnprocessableEntity422Error < ClientError; status(422); end
|
47
|
-
class Locked423Error < ClientError; status(423); end
|
48
|
-
class FailedDependency424Error < ClientError; status(424); end
|
49
|
-
class UpgradeRequired426Error < ClientError; status(426); end
|
50
|
-
class PreconditionRequired428Error < ClientError; status(428); end
|
51
|
-
class TooManyRequests429Error < ClientError; status(429); end
|
52
|
-
class RequestHeaderFieldsTooLarge431Error < ClientError; status(431); end
|
53
|
-
class UnavailableForLegalReasons451Error < ClientError; status(451); end
|
54
|
-
|
55
|
-
class InternalServerError500Error < ServerError; status(500); end
|
56
|
-
class NotImplemented501Error < ServerError; status(501); end
|
57
|
-
class BadGateway502Error < ServerError; status(502); end
|
58
|
-
class ServiceUnavailable503Error < ServerError; status(503); end
|
59
|
-
class GatewayTimeout504Error < ServerError; status(504); end
|
60
|
-
class HTTPVersionNotSupported505Error < ServerError; status(505); end
|
61
|
-
class VariantAlsoNegotiates506Error < ServerError; status(506); end
|
62
|
-
class InsufficientStorage507Error < ServerError; status(507); end
|
63
|
-
class LoopDetected508Error < ServerError; status(508); end
|
64
|
-
class NotExtended510Error < ServerError; status(510); end
|
65
|
-
class NetworkAuthenticationRequired511Error < ServerError; status(511); end
|
66
|
-
error_classes_by_status.freeze
|
67
|
-
|
68
|
-
autoload :Model, 'scorpio/model'
|
69
|
-
autoload :OpenAPI, 'scorpio/openapi'
|
70
|
-
autoload :Google, 'scorpio/google_api_document'
|
71
|
-
autoload :JSON, 'scorpio/json'
|
72
|
-
autoload :Schema, 'scorpio/schema'
|
73
|
-
|
74
|
-
class << self
|
75
|
-
def stringify_symbol_keys(hash)
|
76
|
-
unless hash.is_a?(Hash)
|
77
|
-
raise ArgumentError, "expected argument to be a Hash; got #{hash.class}: #{hash.pretty_inspect}"
|
24
|
+
define_singleton_method(:status) do |status = nil|
|
25
|
+
if status
|
26
|
+
@status = status
|
27
|
+
Scorpio.error_classes_by_status[status] = self
|
28
|
+
else
|
29
|
+
@status
|
78
30
|
end
|
79
|
-
hash.map { |k,v| {k.is_a?(Symbol) ? k.to_s : k => v} }.inject({}, &:update)
|
80
31
|
end
|
32
|
+
attr_accessor :faraday_response, :response_object
|
81
33
|
end
|
34
|
+
# HTTP Error classes' canonical names are like Scorpio::HTTPErrors::BadRequest400Error, but can
|
35
|
+
# be referred to like Scorpio::BadRequest400Error. this is just to avoid clutter in the Scorpio
|
36
|
+
# namespace in yardoc.
|
37
|
+
module HTTPErrors
|
38
|
+
class ClientError < HTTPError; end
|
39
|
+
class ServerError < HTTPError; end
|
82
40
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
end
|
87
|
-
|
88
|
-
|
41
|
+
class BadRequest400Error < ClientError; status(400); end
|
42
|
+
class Unauthorized401Error < ClientError; status(401); end
|
43
|
+
class PaymentRequired402Error < ClientError; status(402); end
|
44
|
+
class Forbidden403Error < ClientError; status(403); end
|
45
|
+
class NotFound404Error < ClientError; status(404); end
|
46
|
+
class MethodNotAllowed405Error < ClientError; status(405); end
|
47
|
+
class NotAcceptable406Error < ClientError; status(406); end
|
48
|
+
class ProxyAuthenticationRequired407Error < ClientError; status(407); end
|
49
|
+
class RequestTimeout408Error < ClientError; status(408); end
|
50
|
+
class Conflict409Error < ClientError; status(409); end
|
51
|
+
class Gone410Error < ClientError; status(410); end
|
52
|
+
class LengthRequired411Error < ClientError; status(411); end
|
53
|
+
class PreconditionFailed412Error < ClientError; status(412); end
|
54
|
+
class PayloadTooLarge413Error < ClientError; status(413); end
|
55
|
+
class URITooLong414Error < ClientError; status(414); end
|
56
|
+
class UnsupportedMediaType415Error < ClientError; status(415); end
|
57
|
+
class RangeNotSatisfiable416Error < ClientError; status(416); end
|
58
|
+
class ExpectationFailed417Error < ClientError; status(417); end
|
59
|
+
class ImaTeapot418Error < ClientError; status(418); end
|
60
|
+
class MisdirectedRequest421Error < ClientError; status(421); end
|
61
|
+
class UnprocessableEntity422Error < ClientError; status(422); end
|
62
|
+
class Locked423Error < ClientError; status(423); end
|
63
|
+
class FailedDependency424Error < ClientError; status(424); end
|
64
|
+
class UpgradeRequired426Error < ClientError; status(426); end
|
65
|
+
class PreconditionRequired428Error < ClientError; status(428); end
|
66
|
+
class TooManyRequests429Error < ClientError; status(429); end
|
67
|
+
class RequestHeaderFieldsTooLarge431Error < ClientError; status(431); end
|
68
|
+
class UnavailableForLegalReasons451Error < ClientError; status(451); end
|
89
69
|
|
90
|
-
|
91
|
-
|
92
|
-
end
|
70
|
+
class InternalServerError500Error < ServerError; status(500); end
|
71
|
+
class NotImplemented501Error < ServerError; status(501); end
|
72
|
+
class BadGateway502Error < ServerError; status(502); end
|
73
|
+
class ServiceUnavailable503Error < ServerError; status(503); end
|
74
|
+
class GatewayTimeout504Error < ServerError; status(504); end
|
75
|
+
class HTTPVersionNotSupported505Error < ServerError; status(505); end
|
76
|
+
class VariantAlsoNegotiates506Error < ServerError; status(506); end
|
77
|
+
class InsufficientStorage507Error < ServerError; status(507); end
|
78
|
+
class LoopDetected508Error < ServerError; status(508); end
|
79
|
+
class NotExtended510Error < ServerError; status(510); end
|
80
|
+
class NetworkAuthenticationRequired511Error < ServerError; status(511); end
|
93
81
|
end
|
82
|
+
include HTTPErrors
|
83
|
+
error_classes_by_status.freeze
|
84
|
+
|
85
|
+
autoload :JSON, 'scorpio/json'
|
86
|
+
autoload :Google, 'scorpio/google_api_document'
|
87
|
+
autoload :OpenAPI, 'scorpio/openapi'
|
88
|
+
autoload :Typelike, 'scorpio/typelike_modules'
|
89
|
+
autoload :Hashlike, 'scorpio/typelike_modules'
|
90
|
+
autoload :Arraylike, 'scorpio/typelike_modules'
|
91
|
+
autoload :ResourceBase, 'scorpio/resource_base'
|
92
|
+
autoload :Schema, 'scorpio/schema'
|
93
|
+
autoload :SchemaInstanceBase, 'scorpio/schema_instance_base'
|
94
94
|
end
|
@@ -1,29 +1,27 @@
|
|
1
1
|
require 'api_hammer/ycomb'
|
2
|
-
require 'scorpio/
|
2
|
+
require 'scorpio/schema_instance_base'
|
3
3
|
|
4
4
|
module Scorpio
|
5
5
|
module Google
|
6
|
-
|
7
|
-
api_document_class = proc do |*key|
|
8
|
-
Scorpio.class_for_schema(Scorpio::JSON::Node.new_by_type(apidoc_schema_doc, ['schemas', *key]))
|
9
|
-
end
|
6
|
+
discovery_rest_description_doc = Scorpio::JSON::Node.new_by_type(::JSON.parse(Scorpio.root.join('documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest').read), [])
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
JsonSchema = api_document_class.call('JsonSchema')
|
15
|
-
RestDescription = api_document_class.call('RestDescription')
|
16
|
-
RestMethod = api_document_class.call('RestMethod')
|
17
|
-
RestResource = api_document_class.call('RestResource')
|
8
|
+
discovery_metaschema = discovery_rest_description_doc['schemas']['JsonSchema']
|
9
|
+
rest_description_schema = Scorpio.class_for_schema(discovery_metaschema).new(discovery_rest_description_doc['schemas']['RestDescription'])
|
10
|
+
discovery_rest_description = Scorpio.class_for_schema(rest_description_schema).new(discovery_rest_description_doc)
|
18
11
|
|
19
|
-
# not
|
20
|
-
|
21
|
-
|
12
|
+
# naming these is not strictly necessary, but is nice to have.
|
13
|
+
DirectoryList = Scorpio.class_for_schema(discovery_rest_description['schemas']['DirectoryList'])
|
14
|
+
JsonSchema = Scorpio.class_for_schema(discovery_rest_description['schemas']['JsonSchema'])
|
15
|
+
RestDescription = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestDescription'])
|
16
|
+
RestMethod = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestMethod'])
|
17
|
+
RestResource = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestResource'])
|
18
|
+
RestMethodRequest = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestMethod']['properties']['request'])
|
19
|
+
RestMethodResponse = Scorpio.class_for_schema(discovery_rest_description['schemas']['RestMethod']['properties']['response'])
|
22
20
|
|
23
21
|
# google does a weird thing where it defines a schema with a $ref property where a json-schema is to be used in the document (method request and response fields), instead of just setting the schema to be the json-schema schema. we'll share a module across those schema classes that really represent schemas. is this confusingly meta enough?
|
24
22
|
module SchemaLike
|
25
23
|
def to_openapi
|
26
|
-
dup_doc = ::JSON.parse(::JSON.generate(
|
24
|
+
dup_doc = ::JSON.parse(::JSON.generate(instance.content))
|
27
25
|
# openapi does not want an id field on schemas
|
28
26
|
dup_doc.delete('id')
|
29
27
|
if dup_doc['properties'].is_a?(Hash)
|
@@ -45,12 +43,12 @@ module Scorpio
|
|
45
43
|
|
46
44
|
class RestDescription
|
47
45
|
def to_openapi_document(options = {})
|
48
|
-
Scorpio::OpenAPI::Document.new(to_openapi_hash(options))
|
46
|
+
Scorpio::OpenAPI::V2::Document.new(to_openapi_hash(options))
|
49
47
|
end
|
50
48
|
|
51
49
|
def to_openapi_hash(options = {})
|
52
50
|
# we will be modifying the api document (RestDescription). clone self and modify that one.
|
53
|
-
ad = self.class.new(::JSON.parse(::JSON.generate(
|
51
|
+
ad = self.class.new(::JSON.parse(::JSON.generate(instance.document)))
|
54
52
|
ad_methods = []
|
55
53
|
if ad['methods']
|
56
54
|
ad_methods += ad['methods'].map do |mn, m|
|
@@ -80,13 +78,9 @@ module Scorpio
|
|
80
78
|
method = http_method_methods.first
|
81
79
|
unused_path_params = Addressable::Template.new(path).variables
|
82
80
|
{http_method.downcase => {}.tap do |operation|
|
83
|
-
|
81
|
+
operation['tags'] = method.resource_name ? [method.resource_name] : []
|
84
82
|
#operation['summary'] =
|
85
83
|
operation['description'] = method['description'] if method['description']
|
86
|
-
if method.resource_name && options[:x]
|
87
|
-
operation['x-resource'] = method.resource_name
|
88
|
-
operation['x-resource-method'] = method.method_name
|
89
|
-
end
|
90
84
|
#operation['externalDocs'] =
|
91
85
|
operation['operationId'] = method['id'] || (method.resource_name ? "#{method.resource_name}.#{method.method_name}" : method.method_name)
|
92
86
|
#operation['produces'] =
|
@@ -186,7 +180,7 @@ module Scorpio
|
|
186
180
|
proc do |toopenapiobject|
|
187
181
|
toopenapiobject = toopenapiobject.to_openapi if toopenapiobject.respond_to?(:to_openapi)
|
188
182
|
if toopenapiobject.respond_to?(:to_hash)
|
189
|
-
toopenapiobject.map { |
|
183
|
+
toopenapiobject.map { |k2, v2| {toopenapirec.call(k2) => toopenapirec.call(v2)} }.inject({}, &:update)
|
190
184
|
elsif toopenapiobject.respond_to?(:to_ary)
|
191
185
|
toopenapiobject.map(&toopenapirec)
|
192
186
|
elsif toopenapiobject.is_a?(Symbol)
|
data/lib/scorpio/json/node.rb
CHANGED
@@ -1,8 +1,20 @@
|
|
1
|
-
require 'scorpio/typelike_modules'
|
2
|
-
|
3
1
|
module Scorpio
|
4
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.
|
5
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.
|
6
18
|
def self.new_by_type(document, path)
|
7
19
|
node = Node.new(document, path)
|
8
20
|
content = node.content
|
@@ -15,17 +27,22 @@ module Scorpio
|
|
15
27
|
end
|
16
28
|
end
|
17
29
|
|
30
|
+
# a Node represents the content of a document at a given path.
|
18
31
|
def initialize(document, path)
|
19
|
-
raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect} (#{path.class})") unless path.is_a?(Array)
|
20
|
-
|
32
|
+
raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect.chomp} (#{path.class})") unless path.is_a?(Array)
|
33
|
+
@document = document
|
21
34
|
@path = path.dup.freeze
|
22
35
|
@pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
|
23
36
|
end
|
24
37
|
|
38
|
+
# the path of this Node within its document
|
25
39
|
attr_reader :path
|
40
|
+
# the document containing this Node at is path
|
26
41
|
attr_reader :document
|
42
|
+
# ::JSON::Schema::Pointer representing the path to this node within its document
|
27
43
|
attr_reader :pointer
|
28
44
|
|
45
|
+
# the raw content of this Node from the underlying document at this Node's path.
|
29
46
|
def content
|
30
47
|
pointer.evaluate(document)
|
31
48
|
end
|
@@ -40,7 +57,7 @@ module Scorpio
|
|
40
57
|
begin
|
41
58
|
el = content[k]
|
42
59
|
rescue TypeError => e
|
43
|
-
raise(e.class, e.message + "\nsubscripting
|
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)
|
44
61
|
end
|
45
62
|
if el.is_a?(Hash) || el.is_a?(Array)
|
46
63
|
self.class.new_by_type(node.document, node.path + [k])
|
@@ -73,26 +90,91 @@ module Scorpio
|
|
73
90
|
return self
|
74
91
|
end
|
75
92
|
|
93
|
+
# a Node at the root of the document
|
76
94
|
def document_node
|
77
95
|
Node.new_by_type(document, [])
|
78
96
|
end
|
79
97
|
|
98
|
+
# the parent of this node. if this node is the document root (its path is empty), raises
|
99
|
+
# ::JSON::Schema::Pointer::ReferenceError.
|
100
|
+
def parent_node
|
101
|
+
if path.empty?
|
102
|
+
raise(::JSON::Schema::Pointer::ReferenceError, "cannot access parent of root node: #{pretty_inspect.chomp}")
|
103
|
+
else
|
104
|
+
Node.new_by_type(document, path[0...-1])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# the pointer path to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
80
109
|
def pointer_path
|
81
110
|
pointer.pointer
|
82
111
|
end
|
112
|
+
# the pointer fragment to this node within the document, per RFC 6901 https://tools.ietf.org/html/rfc6901
|
83
113
|
def fragment
|
84
114
|
pointer.fragment
|
85
115
|
end
|
86
116
|
|
117
|
+
def as_json
|
118
|
+
Typelike.as_json(content)
|
119
|
+
end
|
120
|
+
|
121
|
+
# takes a block. the block is yielded the content of this node. the block MUST return a modified
|
122
|
+
# copy of that content (and NOT modify the object it is given).
|
123
|
+
def modified_copy
|
124
|
+
# we need to preserve the rest of the document, but modify the content at our path.
|
125
|
+
#
|
126
|
+
# this is actually a bit tricky. we can't modify the original document, obviously.
|
127
|
+
# we could do a deep copy, but that's expensive. instead, we make a copy of each array
|
128
|
+
# or hash in the path above this node. this node's content is modified by the caller, and
|
129
|
+
# that is recursively merged up to the document root. the recursion is done with a
|
130
|
+
# y combinator, for no other reason than that was a fun way to implement it.
|
131
|
+
modified_document = ycomb do |rec|
|
132
|
+
proc do |subdocument, subpath|
|
133
|
+
if subpath == []
|
134
|
+
yield(subdocument)
|
135
|
+
else
|
136
|
+
car = subpath[0]
|
137
|
+
cdr = subpath[1..-1]
|
138
|
+
if subdocument.respond_to?(:to_hash)
|
139
|
+
car_object = rec.call(subdocument[car], cdr)
|
140
|
+
if car_object.object_id == subdocument[car].object_id
|
141
|
+
subdocument
|
142
|
+
else
|
143
|
+
subdocument.merge({car => car_object})
|
144
|
+
end
|
145
|
+
elsif subdocument.respond_to?(:to_ary)
|
146
|
+
if car.is_a?(String) && car =~ /\A\d+\z/
|
147
|
+
car = car.to_i
|
148
|
+
end
|
149
|
+
unless car.is_a?(Integer)
|
150
|
+
raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
|
151
|
+
end
|
152
|
+
car_object = rec.call(subdocument[car], cdr)
|
153
|
+
if car_object.object_id == subdocument[car].object_id
|
154
|
+
subdocument
|
155
|
+
else
|
156
|
+
subdocument.dup.tap do |arr|
|
157
|
+
arr[car] = car_object
|
158
|
+
end
|
159
|
+
end
|
160
|
+
else
|
161
|
+
raise(TypeError, "bad subscript: #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect.chomp}")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end.call(document, path)
|
166
|
+
Node.new_by_type(modified_document, path)
|
167
|
+
end
|
168
|
+
|
87
169
|
def object_group_text
|
88
170
|
"fragment=#{fragment.inspect}"
|
89
171
|
end
|
90
172
|
def inspect
|
91
|
-
"\#<#{self.class.
|
173
|
+
"\#<#{self.class.inspect} #{object_group_text} #{content.inspect}>"
|
92
174
|
end
|
93
175
|
def pretty_print(q)
|
94
176
|
q.instance_exec(self) do |obj|
|
95
|
-
text "\#<#{obj.class.
|
177
|
+
text "\#<#{obj.class.inspect} #{obj.object_group_text}"
|
96
178
|
group_sub {
|
97
179
|
nest(2) {
|
98
180
|
breakable ' '
|
@@ -105,7 +187,7 @@ module Scorpio
|
|
105
187
|
end
|
106
188
|
|
107
189
|
def fingerprint
|
108
|
-
{
|
190
|
+
{is_node: self.is_a?(Scorpio::JSON::Node), document: document, path: path}
|
109
191
|
end
|
110
192
|
include FingerprintHash
|
111
193
|
end
|
@@ -116,7 +198,6 @@ module Scorpio
|
|
116
198
|
content.each_index { |i| yield self[i] }
|
117
199
|
self
|
118
200
|
end
|
119
|
-
include Enumerable
|
120
201
|
|
121
202
|
def to_ary
|
122
203
|
to_a
|
@@ -124,29 +205,27 @@ module Scorpio
|
|
124
205
|
|
125
206
|
include Arraylike
|
126
207
|
|
127
|
-
|
128
|
-
|
129
|
-
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a)
|
130
|
-
index_methods = %w(each_index empty? length size)
|
131
|
-
index_methods.each do |method_name|
|
132
|
-
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
208
|
+
def as_json # needs redefined after including Enumerable
|
209
|
+
Typelike.as_json(content)
|
133
210
|
end
|
134
211
|
|
135
|
-
# methods
|
136
|
-
#
|
137
|
-
|
138
|
-
|
139
|
-
define_method(method_name) { |*a, &b| to_a.public_send(method_name, *a, &b) }
|
212
|
+
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
|
213
|
+
# we override these methods from Arraylike
|
214
|
+
SAFE_INDEX_ONLY_METHODS.each do |method_name|
|
215
|
+
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
140
216
|
end
|
141
217
|
end
|
142
218
|
|
143
219
|
class HashNode < Node
|
144
|
-
def each
|
220
|
+
def each(&block)
|
145
221
|
return to_enum(__method__) { content.size } unless block_given?
|
146
|
-
|
222
|
+
if block.arity > 1
|
223
|
+
content.each_key { |k| yield k, self[k] }
|
224
|
+
else
|
225
|
+
content.each_key { |k| yield [k, self[k]] }
|
226
|
+
end
|
147
227
|
self
|
148
228
|
end
|
149
|
-
include Enumerable
|
150
229
|
|
151
230
|
def to_hash
|
152
231
|
inject({}) { |h, (k, v)| h[k] = v; h }
|
@@ -154,61 +233,14 @@ module Scorpio
|
|
154
233
|
|
155
234
|
include Hashlike
|
156
235
|
|
157
|
-
|
236
|
+
def as_json # needs redefined after including Enumerable
|
237
|
+
Typelike.as_json(content)
|
238
|
+
end
|
158
239
|
|
159
240
|
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
|
160
|
-
|
161
|
-
key_methods.each do |method_name|
|
241
|
+
SAFE_KEY_ONLY_METHODS.each do |method_name|
|
162
242
|
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
163
243
|
end
|
164
|
-
|
165
|
-
# methods which use key and value
|
166
|
-
hash_methods = %w(any? compact dig each_pair each_value fetch fetch_values has_value? invert rassoc reject select to_h transform_values value? values values_at)
|
167
|
-
hash_methods.each do |method_name|
|
168
|
-
define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
|
169
|
-
end
|
170
|
-
|
171
|
-
# methods that return a modified copy
|
172
|
-
def merge(other)
|
173
|
-
# we need to preserve the rest of the document, but modify the content at our path.
|
174
|
-
#
|
175
|
-
# this is actually a bit tricky. we can't modify the original document, obviously.
|
176
|
-
# we could do a deep copy, but that's expensive. instead, we make a copy of each array
|
177
|
-
# or hash in the path above this node. this node's content is merged with `other`, and
|
178
|
-
# that is recursively merged up to the document root. the recursion is done with a
|
179
|
-
# y combinator, for no other reason than that was a fun way to implement it.
|
180
|
-
merged_document = ycomb do |rec|
|
181
|
-
proc do |subdocument, subpath|
|
182
|
-
if subpath == []
|
183
|
-
subdocument.merge(other.is_a?(JSON::Node) ? other.content : other)
|
184
|
-
else
|
185
|
-
car = subpath[0]
|
186
|
-
cdr = subpath[1..-1]
|
187
|
-
if subdocument.is_a?(Array)
|
188
|
-
if car.is_a?(String) && car =~ /\A\d+\z/
|
189
|
-
car = car.to_i
|
190
|
-
end
|
191
|
-
unless car.is_a?(Integer)
|
192
|
-
raise(TypeError, "bad subscript #{car.pretty_inspect} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect}")
|
193
|
-
end
|
194
|
-
end
|
195
|
-
car_object = rec.call(subdocument[car], cdr)
|
196
|
-
if car_object == subdocument[car]
|
197
|
-
subdocument
|
198
|
-
elsif subdocument.is_a?(Hash)
|
199
|
-
subdocument.merge({car => car_object})
|
200
|
-
elsif subdocument.is_a?(Array)
|
201
|
-
subdocument.dup.tap do |arr|
|
202
|
-
arr[car] = car_object
|
203
|
-
end
|
204
|
-
else
|
205
|
-
raise(TypeError, "bad subscript: #{car.pretty_inspect} with remaining subpath: #{cdr.inspect} for content: #{subdocument.pretty_inspect}")
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end.call(document, path)
|
210
|
-
self.class.new(merged_document, path)
|
211
|
-
end
|
212
244
|
end
|
213
245
|
end
|
214
246
|
end
|