scorpio 0.1.0 → 0.2.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 +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
|