scorpio 0.0.4 → 0.1.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/README.md +37 -14
- data/bin/documents_to_yml.rb +26 -0
- data/documents/swagger.io/v2/schema.json +1607 -0
- data/documents/swagger.io/v2/schema.yml +1079 -0
- data/documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest +684 -0
- data/{getRest.yml → documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest.yml} +34 -72
- data/lib/scorpio.rb +30 -1
- data/lib/scorpio/google_api_document.rb +232 -0
- data/lib/scorpio/json-schema-fragments.rb +191 -0
- data/lib/scorpio/json.rb +5 -0
- data/lib/scorpio/json/node.rb +214 -0
- data/lib/scorpio/model.rb +178 -91
- data/lib/scorpio/openapi.rb +81 -0
- data/lib/scorpio/schema.rb +133 -0
- data/lib/scorpio/schema_object_base.rb +227 -0
- data/lib/scorpio/typelike_modules.rb +52 -0
- data/lib/scorpio/version.rb +1 -1
- data/scorpio.gemspec +2 -1
- metadata +20 -8
@@ -0,0 +1,191 @@
|
|
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.name} #{@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
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'scorpio/typelike_modules'
|
2
|
+
|
3
|
+
module Scorpio
|
4
|
+
module JSON
|
5
|
+
class Node
|
6
|
+
def self.new_by_type(document, path)
|
7
|
+
node = Node.new(document, path)
|
8
|
+
content = node.content
|
9
|
+
if content.is_a?(Hash)
|
10
|
+
HashNode.new(document, path)
|
11
|
+
elsif content.is_a?(Array)
|
12
|
+
ArrayNode.new(document, path)
|
13
|
+
else
|
14
|
+
node
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(document, path)
|
19
|
+
raise(ArgumentError, "path must be an array. got: #{path.pretty_inspect} (#{path.class})") unless path.is_a?(Array)
|
20
|
+
define_singleton_method(:document) { document }
|
21
|
+
@path = path.dup.freeze
|
22
|
+
@pointer = ::JSON::Schema::Pointer.new(:reference_tokens, path)
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :path
|
26
|
+
attr_reader :document
|
27
|
+
attr_reader :pointer
|
28
|
+
|
29
|
+
def content
|
30
|
+
pointer.evaluate(document)
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](k)
|
34
|
+
node = self
|
35
|
+
content = node.content
|
36
|
+
if content.is_a?(Hash) && !content.key?(k)
|
37
|
+
node = node.deref
|
38
|
+
content = node.content
|
39
|
+
end
|
40
|
+
begin
|
41
|
+
el = content[k]
|
42
|
+
rescue TypeError => e
|
43
|
+
raise(e.class, e.message + "\nsubscripting from #{content.pretty_inspect} (#{content.class}): #{k.pretty_inspect} (#{k.class})", e.backtrace)
|
44
|
+
end
|
45
|
+
if el.is_a?(Hash) || el.is_a?(Array)
|
46
|
+
self.class.new_by_type(node.document, node.path + [k])
|
47
|
+
else
|
48
|
+
el
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def deref
|
53
|
+
content = self.content
|
54
|
+
|
55
|
+
return self unless content.is_a?(Hash) && content['$ref'].is_a?(String)
|
56
|
+
|
57
|
+
if content['$ref'][/\A#/]
|
58
|
+
return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(content['$ref'])).deref
|
59
|
+
end
|
60
|
+
|
61
|
+
# HAX for how google does refs and ids
|
62
|
+
if document_node['schemas'].respond_to?(:to_hash)
|
63
|
+
if document_node['schemas'][content['$ref']]
|
64
|
+
return document_node['schemas'][content['$ref']]
|
65
|
+
end
|
66
|
+
_, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == content['$ref'] }
|
67
|
+
if deref_by_id
|
68
|
+
return deref_by_id
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
#raise(NotImplementedError, "cannot dereference #{content['$ref']}") # TODO
|
73
|
+
return self
|
74
|
+
end
|
75
|
+
|
76
|
+
def document_node
|
77
|
+
Node.new_by_type(document, [])
|
78
|
+
end
|
79
|
+
|
80
|
+
def pointer_path
|
81
|
+
pointer.pointer
|
82
|
+
end
|
83
|
+
def fragment
|
84
|
+
pointer.fragment
|
85
|
+
end
|
86
|
+
|
87
|
+
def object_group_text
|
88
|
+
"fragment=#{fragment.inspect}"
|
89
|
+
end
|
90
|
+
def inspect
|
91
|
+
"\#<#{self.class.name} #{object_group_text} #{content.inspect}>"
|
92
|
+
end
|
93
|
+
def pretty_print(q)
|
94
|
+
q.instance_exec(self) do |obj|
|
95
|
+
text "\#<#{obj.class.name} #{object_group_text}"
|
96
|
+
group_sub {
|
97
|
+
nest(2) {
|
98
|
+
breakable ' '
|
99
|
+
pp obj.content
|
100
|
+
}
|
101
|
+
}
|
102
|
+
breakable ''
|
103
|
+
text '>'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def fingerprint
|
108
|
+
{class: self.class, document: document, path: path}
|
109
|
+
end
|
110
|
+
include FingerprintHash
|
111
|
+
end
|
112
|
+
|
113
|
+
class ArrayNode < Node
|
114
|
+
def each
|
115
|
+
return to_enum(__method__) { content.size } unless block_given?
|
116
|
+
content.each_index { |i| yield self[i] }
|
117
|
+
self
|
118
|
+
end
|
119
|
+
include Enumerable
|
120
|
+
|
121
|
+
def to_ary
|
122
|
+
to_a
|
123
|
+
end
|
124
|
+
|
125
|
+
include Arraylike
|
126
|
+
|
127
|
+
# array methods - define only those which do not modify the array.
|
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) }
|
133
|
+
end
|
134
|
+
|
135
|
+
# methods which use index and value.
|
136
|
+
# flatten is omitted. flatten should not exist.
|
137
|
+
array_methods = %w(& | * + - <=> abbrev assoc at bsearch bsearch_index combination compact count cycle dig fetch index first include? join last pack permutation rassoc repeated_combination reject reverse reverse_each rindex rotate sample select shelljoin shuffle slice sort take take_while transpose uniq values_at zip)
|
138
|
+
array_methods.each do |method_name|
|
139
|
+
define_method(method_name) { |*a, &b| to_a.public_send(method_name, *a, &b) }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class HashNode < Node
|
144
|
+
def each
|
145
|
+
return to_enum(__method__) { content.size } unless block_given?
|
146
|
+
content.each_key { |k| yield k, self[k] }
|
147
|
+
self
|
148
|
+
end
|
149
|
+
include Enumerable
|
150
|
+
|
151
|
+
def to_hash
|
152
|
+
inject({}) { |h, (k, v)| h[k] = v; h }
|
153
|
+
end
|
154
|
+
|
155
|
+
include Hashlike
|
156
|
+
|
157
|
+
# hash methods - define only those which do not modify the hash.
|
158
|
+
|
159
|
+
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
|
160
|
+
key_methods = %w(each_key empty? include? has_key? key key? keys length member? size)
|
161
|
+
key_methods.each do |method_name|
|
162
|
+
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
|
163
|
+
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
|
+
end
|
213
|
+
end
|
214
|
+
end
|
data/lib/scorpio/model.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'addressable/template'
|
2
|
-
require 'json-schema'
|
3
2
|
require 'faraday_middleware'
|
4
3
|
|
5
4
|
module Scorpio
|
@@ -12,18 +11,27 @@ module Scorpio
|
|
12
11
|
class << self
|
13
12
|
def define_inheritable_accessor(accessor, options = {})
|
14
13
|
if options[:default_getter]
|
14
|
+
# the value before the field is set (overwritten) is the result of the default_getter proc
|
15
15
|
define_singleton_method(accessor, &options[:default_getter])
|
16
16
|
else
|
17
|
+
# the value before the field is set (overwritten) is the default_value (which is nil if not specified)
|
17
18
|
default_value = options[:default_value]
|
18
19
|
define_singleton_method(accessor) { default_value }
|
19
20
|
end
|
21
|
+
# field setter method. redefines the getter, replacing the method with one that returns the
|
22
|
+
# setter's argument (that being inherited to the scope of the define_method(accessor) block
|
20
23
|
define_singleton_method(:"#{accessor}=") do |value|
|
24
|
+
# the setter operates on the singleton class of the receiver (self)
|
21
25
|
singleton_class.instance_exec(value, self) do |value_, klass|
|
26
|
+
# remove a previous getter. NameError is raised if a getter is not defined on this class;
|
27
|
+
# this may be ignored.
|
22
28
|
begin
|
23
29
|
remove_method(accessor)
|
24
30
|
rescue NameError
|
25
31
|
end
|
32
|
+
# getter method
|
26
33
|
define_method(accessor) { value_ }
|
34
|
+
# invoke on_set callback defined on the class
|
27
35
|
if options[:on_set]
|
28
36
|
klass.instance_exec(&options[:on_set])
|
29
37
|
end
|
@@ -34,46 +42,58 @@ module Scorpio
|
|
34
42
|
end
|
35
43
|
end
|
36
44
|
end
|
37
|
-
|
38
|
-
|
45
|
+
# the class on which the openapi document is defined. subclasses use the openapi document set on this class
|
46
|
+
# (except in the unlikely event it is overwritten by a subclass)
|
47
|
+
define_inheritable_accessor(:openapi_document_class)
|
48
|
+
# the openapi document
|
49
|
+
define_inheritable_accessor(:openapi_document, on_set: proc { self.openapi_document_class = self })
|
39
50
|
define_inheritable_accessor(:resource_name, update_methods: true)
|
40
|
-
define_inheritable_accessor(:
|
41
|
-
|
42
|
-
|
43
|
-
|
51
|
+
define_inheritable_accessor(:definition_keys, default_value: [], update_methods: true, on_set: proc do
|
52
|
+
definition_keys.each do |key|
|
53
|
+
schema_as_key = schemas_by_key[key]
|
54
|
+
schema_as_key = schema_as_key.object if schema_as_key.is_a?(Scorpio::OpenAPI::Schema)
|
55
|
+
schema_as_key = schema_as_key.content if schema_as_key.is_a?(Scorpio::JSON::Node)
|
56
|
+
|
57
|
+
openapi_document_class.models_by_schema = openapi_document_class.models_by_schema.merge(schema_as_key => self)
|
44
58
|
end
|
45
59
|
end)
|
46
60
|
define_inheritable_accessor(:schemas_by_key, default_value: {})
|
61
|
+
define_inheritable_accessor(:schemas_by_path)
|
47
62
|
define_inheritable_accessor(:schemas_by_id, default_value: {})
|
48
|
-
define_inheritable_accessor(:
|
49
|
-
define_inheritable_accessor(:models_by_schema_key, default_value: {})
|
63
|
+
define_inheritable_accessor(:models_by_schema, default_value: {})
|
50
64
|
define_inheritable_accessor(:base_url)
|
51
65
|
|
52
66
|
define_inheritable_accessor(:faraday_request_middleware, default_value: [])
|
53
67
|
define_inheritable_accessor(:faraday_adapter, default_getter: proc { Faraday.default_adapter })
|
54
68
|
define_inheritable_accessor(:faraday_response_middleware, default_value: [])
|
55
69
|
class << self
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
70
|
+
def set_openapi_document(openapi_document)
|
71
|
+
if openapi_document.is_a?(Hash)
|
72
|
+
openapi_document = OpenAPI::Document.new(openapi_document)
|
73
|
+
end
|
74
|
+
openapi_document.paths.each do |path, path_item|
|
75
|
+
path_item.each do |http_method, operation|
|
76
|
+
next if http_method == 'parameters' # parameters is not an operation. TOOD maybe just select the keys that are http methods?
|
77
|
+
unless operation.is_a?(Scorpio::OpenAPI::Operation)
|
78
|
+
raise("bad operation at #{operation.fragment}: #{operation.pretty_inspect}")
|
79
|
+
end
|
80
|
+
operation.path = path
|
81
|
+
operation.http_method = http_method
|
63
82
|
end
|
64
|
-
rest['schemas']['RestDescription']
|
65
83
|
end
|
66
|
-
end
|
67
84
|
|
68
|
-
|
69
|
-
|
70
|
-
self.
|
71
|
-
|
72
|
-
|
73
|
-
|
85
|
+
openapi_document.validate!
|
86
|
+
self.schemas_by_path = {}
|
87
|
+
self.schemas_by_key = {}
|
88
|
+
self.schemas_by_id = {}
|
89
|
+
self.openapi_document = openapi_document
|
90
|
+
(openapi_document.definitions || {}).each do |schema_key, schema|
|
91
|
+
if schema['id']
|
92
|
+
# this isn't actually allowed by openapi's definition. whatever.
|
93
|
+
self.schemas_by_id = self.schemas_by_id.merge(schema['id'] => schema)
|
74
94
|
end
|
75
|
-
|
76
|
-
schemas_by_key
|
95
|
+
self.schemas_by_path = self.schemas_by_path.merge(schema.object.fragment => schema)
|
96
|
+
self.schemas_by_key = self.schemas_by_key.merge(schema_key => schema)
|
77
97
|
end
|
78
98
|
update_dynamic_methods
|
79
99
|
end
|
@@ -84,9 +104,9 @@ module Scorpio
|
|
84
104
|
end
|
85
105
|
|
86
106
|
def all_schema_properties
|
87
|
-
schemas_by_key.select { |k, _|
|
107
|
+
schemas_by_key.select { |k, _| definition_keys.include?(k) }.map do |schema_key, schema|
|
88
108
|
unless schema['type'] == 'object'
|
89
|
-
raise "
|
109
|
+
raise "definition key #{schema_key} for #{self} is not of type object - type must be object for Scorpio Model to represent this schema" # TODO class
|
90
110
|
end
|
91
111
|
schema['properties'].keys
|
92
112
|
end.inject([], &:|)
|
@@ -107,49 +127,87 @@ module Scorpio
|
|
107
127
|
end
|
108
128
|
end
|
109
129
|
|
130
|
+
def operation_for_resource_class?(operation)
|
131
|
+
return false unless resource_name
|
132
|
+
|
133
|
+
return true if operation['x-resource'] == self.resource_name
|
134
|
+
|
135
|
+
return true if operation.operationId =~ /\A#{Regexp.escape(resource_name)}\.(\w+)\z/
|
136
|
+
|
137
|
+
request_schema = operation.body_parameter['schema'] if operation.body_parameter
|
138
|
+
if request_schema && schemas_by_key.any? { |key, as| as == request_schema && definition_keys.include?(key) }
|
139
|
+
return true
|
140
|
+
end
|
141
|
+
|
142
|
+
return false
|
143
|
+
end
|
144
|
+
|
145
|
+
def operation_for_resource_instance?(operation)
|
146
|
+
return false unless operation_for_resource_class?(operation)
|
147
|
+
|
148
|
+
request_schema = operation.body_parameter['schema'] if operation.body_parameter
|
149
|
+
|
150
|
+
# define an instance method if the request schema is for this model
|
151
|
+
request_resource_is_self = request_schema &&
|
152
|
+
schemas_by_key.any? { |key, as| as == request_schema && definition_keys.include?(key) }
|
153
|
+
|
154
|
+
# also define an instance method depending on certain attributes the request description
|
155
|
+
# might have in common with the model's schema attributes
|
156
|
+
request_attributes = []
|
157
|
+
# if the path has attributes in common with model schema attributes, we'll define on
|
158
|
+
# instance method
|
159
|
+
request_attributes |= Addressable::Template.new(operation.path).variables
|
160
|
+
# TODO if the method request schema has attributes in common with the model schema attributes,
|
161
|
+
# should we define an instance method?
|
162
|
+
#request_attributes |= request_schema && request_schema['type'] == 'object' && request_schema['properties'] ?
|
163
|
+
# request_schema['properties'].keys : []
|
164
|
+
# TODO if the method parameters have attributes in common with the model schema attributes,
|
165
|
+
# should we define an instance method?
|
166
|
+
#request_attributes |= method_desc['parameters'] ? method_desc['parameters'].keys : []
|
167
|
+
|
168
|
+
schema_attributes = definition_keys.map do |schema_key|
|
169
|
+
schema = schemas_by_key[schema_key]
|
170
|
+
schema['type'] == 'object' && schema['properties'] ? schema['properties'].keys : []
|
171
|
+
end.inject([], &:|)
|
172
|
+
|
173
|
+
return request_resource_is_self || (request_attributes & schema_attributes).any?
|
174
|
+
end
|
175
|
+
|
176
|
+
def method_names_by_operation
|
177
|
+
@method_names_by_operation ||= Hash.new do |h, operation|
|
178
|
+
h[operation] = begin
|
179
|
+
raise(ArgumentError, operation.pretty_inspect) unless operation.is_a?(Scorpio::OpenAPI::Operation)
|
180
|
+
if operation['x-resource-method']
|
181
|
+
method_name = operation['x-resource-method']
|
182
|
+
elsif resource_name && operation.operationId =~ /\A#{Regexp.escape(resource_name)}\.(\w+)\z/
|
183
|
+
method_name = $1
|
184
|
+
else
|
185
|
+
method_name = operation.operationId || raise("no operationId on operation: #{operation.pretty_inspect}")
|
186
|
+
end
|
187
|
+
method_name = '_' + method_name unless method_name[/\A[a-zA-Z_]/]
|
188
|
+
method_name.gsub(/[^\w]/, '_')
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
110
193
|
def update_class_and_instance_api_methods
|
111
|
-
|
112
|
-
|
113
|
-
|
194
|
+
openapi_document.paths.each do |path, path_item|
|
195
|
+
path_item.each do |http_method, operation|
|
196
|
+
next if http_method == 'parameters' # parameters is not an operation. TOOD maybe just select the keys that are http methods?
|
197
|
+
operation.path = path
|
198
|
+
operation.http_method = http_method
|
199
|
+
method_name = method_names_by_operation[operation]
|
114
200
|
# class method
|
115
|
-
|
201
|
+
if operation_for_resource_class?(operation) && !respond_to?(method_name)
|
116
202
|
define_singleton_method(method_name) do |call_params = nil|
|
117
|
-
|
203
|
+
call_operation(operation, call_params: call_params)
|
118
204
|
end
|
119
205
|
end
|
120
206
|
|
121
207
|
# instance method
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
# define an instance method if the request schema is for this model
|
126
|
-
request_resource_is_self = request_schema &&
|
127
|
-
request_schema['id'] &&
|
128
|
-
schemas_by_key.any? { |key, as| as['id'] == request_schema['id'] && schema_keys.include?(key) }
|
129
|
-
|
130
|
-
# also define an instance method depending on certain attributes the request description
|
131
|
-
# might have in common with the model's schema attributes
|
132
|
-
request_attributes = []
|
133
|
-
# if the path has attributes in common with model schema attributes, we'll define on
|
134
|
-
# instance method
|
135
|
-
request_attributes |= Addressable::Template.new(method_desc['path']).variables
|
136
|
-
# TODO if the method request schema has attributes in common with the model schema attributes,
|
137
|
-
# should we define an instance method?
|
138
|
-
#request_attributes |= request_schema && request_schema['type'] == 'object' && request_schema['properties'] ?
|
139
|
-
# request_schema['properties'].keys : []
|
140
|
-
# TODO if the method parameters have attributes in common with the model schema attributes,
|
141
|
-
# should we define an instance method?
|
142
|
-
#request_attributes |= method_desc['parameters'] ? method_desc['parameters'].keys : []
|
143
|
-
|
144
|
-
schema_attributes = schema_keys.map do |schema_key|
|
145
|
-
schema = schemas_by_key[schema_key]
|
146
|
-
schema['type'] == 'object' && schema['properties'] ? schema['properties'].keys : []
|
147
|
-
end.inject([], &:|)
|
148
|
-
|
149
|
-
if request_resource_is_self || (request_attributes & schema_attributes).any?
|
150
|
-
define_method(method_name) do |call_params = nil|
|
151
|
-
call_api_method(method_name, call_params: call_params)
|
152
|
-
end
|
208
|
+
if operation_for_resource_instance?(operation) && !method_defined?(method_name)
|
209
|
+
define_method(method_name) do |call_params = nil|
|
210
|
+
call_operation(operation, call_params: call_params)
|
153
211
|
end
|
154
212
|
end
|
155
213
|
end
|
@@ -157,6 +215,8 @@ module Scorpio
|
|
157
215
|
end
|
158
216
|
|
159
217
|
def deref_schema(schema)
|
218
|
+
schema = schema.object if schema.is_a?(Scorpio::SchemaObjectBase)
|
219
|
+
schema = schema.deref if schema.is_a?(Scorpio::JSON::Node)
|
160
220
|
schema && schemas_by_id[schema['$ref']] || schema
|
161
221
|
end
|
162
222
|
|
@@ -188,22 +248,21 @@ module Scorpio
|
|
188
248
|
end
|
189
249
|
end
|
190
250
|
|
191
|
-
def
|
251
|
+
def call_operation(operation, call_params: nil, model_attributes: nil)
|
192
252
|
call_params = Scorpio.stringify_symbol_keys(call_params) if call_params.is_a?(Hash)
|
193
253
|
model_attributes = Scorpio.stringify_symbol_keys(model_attributes || {})
|
194
|
-
|
195
|
-
|
196
|
-
path_template = Addressable::Template.new(method_desc['path'])
|
254
|
+
http_method = operation.http_method.downcase.to_sym
|
255
|
+
path_template = Addressable::Template.new(operation.path)
|
197
256
|
template_params = model_attributes
|
198
257
|
template_params = template_params.merge(call_params) if call_params.is_a?(Hash)
|
199
258
|
missing_variables = path_template.variables - template_params.keys
|
200
259
|
if missing_variables.any?
|
201
|
-
raise(ArgumentError, "path #{
|
260
|
+
raise(ArgumentError, "path #{operation.path} for operation #{operation.operationId} requires attributes " +
|
202
261
|
"which were missing: #{missing_variables.inspect}")
|
203
262
|
end
|
204
263
|
empty_variables = path_template.variables.select { |v| template_params[v].to_s.empty? }
|
205
264
|
if empty_variables.any?
|
206
|
-
raise(ArgumentError, "path #{
|
265
|
+
raise(ArgumentError, "path #{operation.path} for operation #{operation.operationId} requires attributes " +
|
207
266
|
"which were empty: #{empty_variables.inspect}")
|
208
267
|
end
|
209
268
|
path = path_template.expand(template_params)
|
@@ -215,8 +274,7 @@ module Scorpio
|
|
215
274
|
other_params.reject! { |k, _| path_template.variables.include?(k) }
|
216
275
|
end
|
217
276
|
|
218
|
-
|
219
|
-
request_schema = deref_schema(method_desc['request'])
|
277
|
+
request_schema = operation.body_parameter && deref_schema(operation.body_parameter['schema'])
|
220
278
|
if request_schema
|
221
279
|
# TODO deal with model_attributes / call_params better in nested whatever
|
222
280
|
if call_params.nil?
|
@@ -253,13 +311,21 @@ module Scorpio
|
|
253
311
|
HTTPError
|
254
312
|
end
|
255
313
|
if error_class
|
256
|
-
message = "Error calling #{
|
314
|
+
message = "Error calling operation #{operation.operationId} on #{self}:\n" + (response.env[:raw_body] || response.env.body)
|
257
315
|
raise error_class.new(message).tap { |e| e.response = response }
|
258
316
|
end
|
259
317
|
|
260
|
-
|
261
|
-
|
262
|
-
|
318
|
+
if operation.responses
|
319
|
+
_, operation_response = operation.responses.detect { |k, v| k.to_s == response.status.to_s }
|
320
|
+
operation_response ||= operation.responses['default']
|
321
|
+
response_schema = operation_response.schema if operation_response
|
322
|
+
end
|
323
|
+
initialize_options = {
|
324
|
+
'persisted' => true,
|
325
|
+
'source' => {'operationId' => operation.operationId, 'call_params' => call_params, 'url' => url.to_s},
|
326
|
+
'response' => response,
|
327
|
+
}
|
328
|
+
response_object_to_instances(response.body, response_schema, initialize_options)
|
263
329
|
end
|
264
330
|
|
265
331
|
def request_body_for_schema(object, schema)
|
@@ -333,7 +399,7 @@ module Scorpio
|
|
333
399
|
end
|
334
400
|
|
335
401
|
def request_schema_fail(object, schema)
|
336
|
-
raise(RequestSchemaFailure, "object does not conform to schema.\nobject = #{object.
|
402
|
+
raise(RequestSchemaFailure, "object does not conform to schema.\nobject = #{object.pretty_inspect}\nschema = #{::JSON.pretty_generate(schema, quirks_mode: true)}")
|
337
403
|
end
|
338
404
|
|
339
405
|
def response_object_to_instances(object, schema, initialize_options = {})
|
@@ -351,7 +417,10 @@ module Scorpio
|
|
351
417
|
schema['additionalProperties']
|
352
418
|
{key => response_object_to_instances(value, schema_for_value, initialize_options)}
|
353
419
|
end.inject(object.class.new, &:update)
|
354
|
-
|
420
|
+
schema_as_key = schema
|
421
|
+
schema_as_key = schema_as_key.object if schema_as_key.is_a?(Scorpio::OpenAPI::Schema)
|
422
|
+
schema_as_key = schema_as_key.content if schema_as_key.is_a?(Scorpio::JSON::Node)
|
423
|
+
model = models_by_schema[schema_as_key]
|
355
424
|
if model
|
356
425
|
model.new(out, initialize_options)
|
357
426
|
else
|
@@ -391,24 +460,26 @@ module Scorpio
|
|
391
460
|
@attributes[key] = value
|
392
461
|
end
|
393
462
|
|
394
|
-
def
|
395
|
-
|
463
|
+
def call_api_method(method_name, call_params: nil)
|
464
|
+
operation = self.class.method_names_by_operation.invert[method_name] || raise(ArgumentError)
|
465
|
+
call_operation(operation, call_params: call_params)
|
396
466
|
end
|
397
467
|
|
398
|
-
def
|
399
|
-
response = self.class.
|
468
|
+
def call_operation(operation, call_params: nil)
|
469
|
+
response = self.class.call_operation(operation, call_params: call_params, model_attributes: self.attributes)
|
400
470
|
|
401
471
|
# if we're making a POST or PUT and the request schema is this resource, we'll assume that
|
402
472
|
# the request is persisting this resource
|
403
|
-
|
404
|
-
request_schema = self.class.deref_schema(api_method['request'])
|
473
|
+
request_schema = operation.body_parameter && self.class.deref_schema(operation.body_parameter['schema'])
|
405
474
|
request_resource_is_self = request_schema &&
|
406
475
|
request_schema['id'] &&
|
407
|
-
self.class.schemas_by_key.any? { |key, as| as['id'] == request_schema['id'] && self.class.
|
408
|
-
|
476
|
+
self.class.schemas_by_key.any? { |key, as| (as['id'] ? as['id'] == request_schema['id'] : as == request_schema) && self.class.definition_keys.include?(key) }
|
477
|
+
if @options['response'] && @options['response'].status && operation.responses
|
478
|
+
_, response_schema = operation.responses.detect { |k, v| k.to_s == @options['response'].status.to_s }
|
479
|
+
end
|
480
|
+
response_schema = self.class.deref_schema(response_schema)
|
409
481
|
response_resource_is_self = response_schema &&
|
410
|
-
response_schema['id'] &&
|
411
|
-
self.class.schemas_by_key.any? { |key, as| as['id'] == response_schema['id'] && self.class.schema_keys.include?(key) }
|
482
|
+
self.class.schemas_by_key.any? { |key, as| (as['id'] ? as['id'] == response_schema['id'] : as == response_schema) && self.class.definition_keys.include?(key) }
|
412
483
|
if request_resource_is_self && %w(PUT POST).include?(api_method['httpMethod'])
|
413
484
|
@persisted = true
|
414
485
|
|
@@ -425,10 +496,26 @@ module Scorpio
|
|
425
496
|
@attributes
|
426
497
|
end
|
427
498
|
|
428
|
-
|
499
|
+
def inspect
|
500
|
+
"\#<#{self.class.name} #{attributes.inspect}>"
|
501
|
+
end
|
502
|
+
def pretty_print(q)
|
503
|
+
q.instance_exec(self) do |obj|
|
504
|
+
text "\#<#{obj.class.name}"
|
505
|
+
group_sub {
|
506
|
+
nest(2) {
|
507
|
+
breakable ' '
|
508
|
+
pp obj.attributes
|
509
|
+
}
|
510
|
+
}
|
511
|
+
breakable ''
|
512
|
+
text '>'
|
513
|
+
end
|
514
|
+
end
|
429
515
|
|
430
|
-
def
|
431
|
-
@attributes
|
516
|
+
def fingerprint
|
517
|
+
{class: self.class, attributes: @attributes}
|
432
518
|
end
|
519
|
+
include FingerprintHash
|
433
520
|
end
|
434
521
|
end
|