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/openapi.rb
CHANGED
@@ -1,79 +1,157 @@
|
|
1
|
-
require 'scorpio/
|
1
|
+
require 'scorpio/schema_instance_base'
|
2
2
|
|
3
3
|
module Scorpio
|
4
4
|
module OpenAPI
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
module V3
|
6
|
+
openapi_schema = Scorpio::Schema.new(::JSON.parse(Scorpio.root.join('documents/openapis.org/v3/schema.json').read))
|
7
|
+
openapi_class = proc do |*key|
|
8
|
+
Scorpio.class_for_schema(key.inject(openapi_schema, &:[]))
|
9
|
+
end
|
10
|
+
|
11
|
+
Document = openapi_class.call()
|
12
|
+
|
13
|
+
# naming these is not strictly necessary, but is nice to have.
|
14
|
+
# generated: puts openapi_schema_doc['definitions'].select { |k,v| ['object', nil].include?(v['type']) }.keys.map { |k| "#{k[0].upcase}#{k[1..-1]} = openapi_class.call('definitions', '#{k}')" }
|
15
|
+
Info = openapi_class.call('definitions', 'info')
|
16
|
+
Contact = openapi_class.call('definitions', 'contact')
|
17
|
+
License = openapi_class.call('definitions', 'license')
|
18
|
+
Server = openapi_class.call('definitions', 'server')
|
19
|
+
ServerVariable = openapi_class.call('definitions', 'serverVariable')
|
20
|
+
Components = openapi_class.call('definitions', 'components')
|
21
|
+
Paths = openapi_class.call('definitions', 'paths')
|
22
|
+
PathItem = openapi_class.call('definitions', 'pathItem')
|
23
|
+
Operation = openapi_class.call('definitions', 'operation')
|
24
|
+
ExternalDocs = openapi_class.call('definitions', 'externalDocs')
|
25
|
+
Parameter = openapi_class.call('definitions', 'parameter')
|
26
|
+
RequestBody = openapi_class.call('definitions', 'requestBody')
|
27
|
+
MediaType = openapi_class.call('definitions', 'mediaType')
|
28
|
+
Encoding = openapi_class.call('definitions', 'encoding')
|
29
|
+
Responses = openapi_class.call('definitions', 'responses')
|
30
|
+
Response = openapi_class.call('definitions', 'response')
|
31
|
+
Callback = openapi_class.call('definitions', 'callback')
|
32
|
+
Example = openapi_class.call('definitions', 'example')
|
33
|
+
Link = openapi_class.call('definitions', 'link')
|
34
|
+
Header = openapi_class.call('definitions', 'header')
|
35
|
+
Tag = openapi_class.call('definitions', 'tag')
|
36
|
+
Examples = openapi_class.call('definitions', 'examples')
|
37
|
+
Reference = openapi_class.call('definitions', 'reference')
|
38
|
+
Schema = openapi_class.call('definitions', 'schema')
|
39
|
+
Discriminator = openapi_class.call('definitions', 'discriminator')
|
40
|
+
Xml = openapi_class.call('definitions', 'xml')
|
41
|
+
SecurityScheme = openapi_class.call('definitions', 'securityScheme')
|
42
|
+
OauthFlows = openapi_class.call('definitions', 'oauthFlows')
|
43
|
+
OauthFlow = openapi_class.call('definitions', 'oauthFlow')
|
44
|
+
SecurityRequirement = openapi_class.call('definitions', 'securityRequirement')
|
45
|
+
AnyOrExpression = openapi_class.call('definitions', 'anyOrExpression')
|
46
|
+
CallbackOrReference = openapi_class.call('definitions', 'callbackOrReference')
|
47
|
+
ExampleOrReference = openapi_class.call('definitions', 'exampleOrReference')
|
48
|
+
HeaderOrReference = openapi_class.call('definitions', 'headerOrReference')
|
49
|
+
LinkOrReference = openapi_class.call('definitions', 'linkOrReference')
|
50
|
+
ParameterOrReference = openapi_class.call('definitions', 'parameterOrReference')
|
51
|
+
RequestBodyOrReference = openapi_class.call('definitions', 'requestBodyOrReference')
|
52
|
+
ResponseOrReference = openapi_class.call('definitions', 'responseOrReference')
|
53
|
+
SchemaOrReference = openapi_class.call('definitions', 'schemaOrReference')
|
54
|
+
SecuritySchemeOrReference = openapi_class.call('definitions', 'securitySchemeOrReference')
|
55
|
+
AnysOrExpressions = openapi_class.call('definitions', 'anysOrExpressions')
|
56
|
+
CallbacksOrReferences = openapi_class.call('definitions', 'callbacksOrReferences')
|
57
|
+
Encodings = openapi_class.call('definitions', 'encodings')
|
58
|
+
ExamplesOrReferences = openapi_class.call('definitions', 'examplesOrReferences')
|
59
|
+
HeadersOrReferences = openapi_class.call('definitions', 'headersOrReferences')
|
60
|
+
LinksOrReferences = openapi_class.call('definitions', 'linksOrReferences')
|
61
|
+
MediaTypes = openapi_class.call('definitions', 'mediaTypes')
|
62
|
+
ParametersOrReferences = openapi_class.call('definitions', 'parametersOrReferences')
|
63
|
+
RequestBodiesOrReferences = openapi_class.call('definitions', 'requestBodiesOrReferences')
|
64
|
+
ResponsesOrReferences = openapi_class.call('definitions', 'responsesOrReferences')
|
65
|
+
SchemasOrReferences = openapi_class.call('definitions', 'schemasOrReferences')
|
66
|
+
SecuritySchemesOrReferences = openapi_class.call('definitions', 'securitySchemesOrReferences')
|
67
|
+
ServerVariables = openapi_class.call('definitions', 'serverVariables')
|
68
|
+
Strings = openapi_class.call('definitions', 'strings')
|
69
|
+
Object = openapi_class.call('definitions', 'object')
|
70
|
+
Any = openapi_class.call('definitions', 'any')
|
71
|
+
Expression = openapi_class.call('definitions', 'expression')
|
72
|
+
SpecificationExtension = openapi_class.call('definitions', 'specificationExtension')
|
73
|
+
DefaultType = openapi_class.call('definitions', 'defaultType')
|
8
74
|
end
|
75
|
+
module V2
|
76
|
+
openapi_schema = Scorpio::Schema.new(::JSON.parse(Scorpio.root.join('documents/swagger.io/v2/schema.json').read))
|
77
|
+
openapi_class = proc do |*key|
|
78
|
+
Scorpio.class_for_schema(key.inject(openapi_schema, &:[]))
|
79
|
+
end
|
80
|
+
|
81
|
+
Document = openapi_class.call()
|
9
82
|
|
10
|
-
|
83
|
+
# naming these is not strictly necessary, but is nice to have.
|
84
|
+
# generated: puts Scorpio::OpenAPI::V2::Document.schema_document['definitions'].select { |k,v| ['object', nil].include?(v['type']) }.keys.map { |k| "#{k[0].upcase}#{k[1..-1]} = openapi_class.call('definitions', '#{k}')" }
|
85
|
+
Info = openapi_class.call('definitions', 'info')
|
86
|
+
Contact = openapi_class.call('definitions', 'contact')
|
87
|
+
License = openapi_class.call('definitions', 'license')
|
88
|
+
Paths = openapi_class.call('definitions', 'paths')
|
89
|
+
Definitions = openapi_class.call('definitions', 'definitions')
|
90
|
+
ParameterDefinitions = openapi_class.call('definitions', 'parameterDefinitions')
|
91
|
+
ResponseDefinitions = openapi_class.call('definitions', 'responseDefinitions')
|
92
|
+
ExternalDocs = openapi_class.call('definitions', 'externalDocs')
|
93
|
+
Examples = openapi_class.call('definitions', 'examples')
|
94
|
+
Operation = openapi_class.call('definitions', 'operation')
|
95
|
+
PathItem = openapi_class.call('definitions', 'pathItem')
|
96
|
+
Responses = openapi_class.call('definitions', 'responses')
|
97
|
+
ResponseValue = openapi_class.call('definitions', 'responseValue')
|
98
|
+
Response = openapi_class.call('definitions', 'response')
|
99
|
+
Headers = openapi_class.call('definitions', 'headers')
|
100
|
+
Header = openapi_class.call('definitions', 'header')
|
101
|
+
VendorExtension = openapi_class.call('definitions', 'vendorExtension')
|
102
|
+
BodyParameter = openapi_class.call('definitions', 'bodyParameter')
|
103
|
+
HeaderParameterSubSchema = openapi_class.call('definitions', 'headerParameterSubSchema')
|
104
|
+
QueryParameterSubSchema = openapi_class.call('definitions', 'queryParameterSubSchema')
|
105
|
+
FormDataParameterSubSchema = openapi_class.call('definitions', 'formDataParameterSubSchema')
|
106
|
+
PathParameterSubSchema = openapi_class.call('definitions', 'pathParameterSubSchema')
|
107
|
+
NonBodyParameter = openapi_class.call('definitions', 'nonBodyParameter')
|
108
|
+
Parameter = openapi_class.call('definitions', 'parameter')
|
109
|
+
Schema = openapi_class.call('definitions', 'schema')
|
110
|
+
FileSchema = openapi_class.call('definitions', 'fileSchema')
|
111
|
+
PrimitivesItems = openapi_class.call('definitions', 'primitivesItems')
|
112
|
+
SecurityRequirement = openapi_class.call('definitions', 'securityRequirement')
|
113
|
+
Xml = openapi_class.call('definitions', 'xml')
|
114
|
+
Tag = openapi_class.call('definitions', 'tag')
|
115
|
+
SecurityDefinitions = openapi_class.call('definitions', 'securityDefinitions')
|
116
|
+
BasicAuthenticationSecurity = openapi_class.call('definitions', 'basicAuthenticationSecurity')
|
117
|
+
ApiKeySecurity = openapi_class.call('definitions', 'apiKeySecurity')
|
118
|
+
Oauth2ImplicitSecurity = openapi_class.call('definitions', 'oauth2ImplicitSecurity')
|
119
|
+
Oauth2PasswordSecurity = openapi_class.call('definitions', 'oauth2PasswordSecurity')
|
120
|
+
Oauth2ApplicationSecurity = openapi_class.call('definitions', 'oauth2ApplicationSecurity')
|
121
|
+
Oauth2AccessCodeSecurity = openapi_class.call('definitions', 'oauth2AccessCodeSecurity')
|
122
|
+
Oauth2Scopes = openapi_class.call('definitions', 'oauth2Scopes')
|
123
|
+
Title = openapi_class.call('definitions', 'title')
|
124
|
+
Description = openapi_class.call('definitions', 'description')
|
125
|
+
Default = openapi_class.call('definitions', 'default')
|
126
|
+
MultipleOf = openapi_class.call('definitions', 'multipleOf')
|
127
|
+
Maximum = openapi_class.call('definitions', 'maximum')
|
128
|
+
ExclusiveMaximum = openapi_class.call('definitions', 'exclusiveMaximum')
|
129
|
+
Minimum = openapi_class.call('definitions', 'minimum')
|
130
|
+
ExclusiveMinimum = openapi_class.call('definitions', 'exclusiveMinimum')
|
131
|
+
MaxLength = openapi_class.call('definitions', 'maxLength')
|
132
|
+
MinLength = openapi_class.call('definitions', 'minLength')
|
133
|
+
Pattern = openapi_class.call('definitions', 'pattern')
|
134
|
+
MaxItems = openapi_class.call('definitions', 'maxItems')
|
135
|
+
MinItems = openapi_class.call('definitions', 'minItems')
|
136
|
+
UniqueItems = openapi_class.call('definitions', 'uniqueItems')
|
137
|
+
Enum = openapi_class.call('definitions', 'enum')
|
138
|
+
JsonReference = openapi_class.call('definitions', 'jsonReference')
|
11
139
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
Contact = openapi_class.call('definitions', 'contact')
|
16
|
-
License = openapi_class.call('definitions', 'license')
|
17
|
-
Paths = openapi_class.call('definitions', 'paths')
|
18
|
-
Definitions = openapi_class.call('definitions', 'definitions')
|
19
|
-
ParameterDefinitions = openapi_class.call('definitions', 'parameterDefinitions')
|
20
|
-
ResponseDefinitions = openapi_class.call('definitions', 'responseDefinitions')
|
21
|
-
ExternalDocs = openapi_class.call('definitions', 'externalDocs')
|
22
|
-
Examples = openapi_class.call('definitions', 'examples')
|
23
|
-
Operation = openapi_class.call('definitions', 'operation')
|
24
|
-
PathItem = openapi_class.call('definitions', 'pathItem')
|
25
|
-
Responses = openapi_class.call('definitions', 'responses')
|
26
|
-
ResponseValue = openapi_class.call('definitions', 'responseValue')
|
27
|
-
Response = openapi_class.call('definitions', 'response')
|
28
|
-
Headers = openapi_class.call('definitions', 'headers')
|
29
|
-
Header = openapi_class.call('definitions', 'header')
|
30
|
-
VendorExtension = openapi_class.call('definitions', 'vendorExtension')
|
31
|
-
BodyParameter = openapi_class.call('definitions', 'bodyParameter')
|
32
|
-
HeaderParameterSubSchema = openapi_class.call('definitions', 'headerParameterSubSchema')
|
33
|
-
QueryParameterSubSchema = openapi_class.call('definitions', 'queryParameterSubSchema')
|
34
|
-
FormDataParameterSubSchema = openapi_class.call('definitions', 'formDataParameterSubSchema')
|
35
|
-
PathParameterSubSchema = openapi_class.call('definitions', 'pathParameterSubSchema')
|
36
|
-
NonBodyParameter = openapi_class.call('definitions', 'nonBodyParameter')
|
37
|
-
Parameter = openapi_class.call('definitions', 'parameter')
|
38
|
-
Schema = openapi_class.call('definitions', 'schema')
|
39
|
-
FileSchema = openapi_class.call('definitions', 'fileSchema')
|
40
|
-
PrimitivesItems = openapi_class.call('definitions', 'primitivesItems')
|
41
|
-
SecurityRequirement = openapi_class.call('definitions', 'securityRequirement')
|
42
|
-
Xml = openapi_class.call('definitions', 'xml')
|
43
|
-
Tag = openapi_class.call('definitions', 'tag')
|
44
|
-
SecurityDefinitions = openapi_class.call('definitions', 'securityDefinitions')
|
45
|
-
BasicAuthenticationSecurity = openapi_class.call('definitions', 'basicAuthenticationSecurity')
|
46
|
-
ApiKeySecurity = openapi_class.call('definitions', 'apiKeySecurity')
|
47
|
-
Oauth2ImplicitSecurity = openapi_class.call('definitions', 'oauth2ImplicitSecurity')
|
48
|
-
Oauth2PasswordSecurity = openapi_class.call('definitions', 'oauth2PasswordSecurity')
|
49
|
-
Oauth2ApplicationSecurity = openapi_class.call('definitions', 'oauth2ApplicationSecurity')
|
50
|
-
Oauth2AccessCodeSecurity = openapi_class.call('definitions', 'oauth2AccessCodeSecurity')
|
51
|
-
Oauth2Scopes = openapi_class.call('definitions', 'oauth2Scopes')
|
52
|
-
Title = openapi_class.call('definitions', 'title')
|
53
|
-
Description = openapi_class.call('definitions', 'description')
|
54
|
-
Default = openapi_class.call('definitions', 'default')
|
55
|
-
MultipleOf = openapi_class.call('definitions', 'multipleOf')
|
56
|
-
Maximum = openapi_class.call('definitions', 'maximum')
|
57
|
-
ExclusiveMaximum = openapi_class.call('definitions', 'exclusiveMaximum')
|
58
|
-
Minimum = openapi_class.call('definitions', 'minimum')
|
59
|
-
ExclusiveMinimum = openapi_class.call('definitions', 'exclusiveMinimum')
|
60
|
-
MaxLength = openapi_class.call('definitions', 'maxLength')
|
61
|
-
MinLength = openapi_class.call('definitions', 'minLength')
|
62
|
-
Pattern = openapi_class.call('definitions', 'pattern')
|
63
|
-
MaxItems = openapi_class.call('definitions', 'maxItems')
|
64
|
-
MinItems = openapi_class.call('definitions', 'minItems')
|
65
|
-
UniqueItems = openapi_class.call('definitions', 'uniqueItems')
|
66
|
-
Enum = openapi_class.call('definitions', 'enum')
|
67
|
-
JsonReference = openapi_class.call('definitions', 'jsonReference')
|
140
|
+
class Operation
|
141
|
+
attr_accessor :path
|
142
|
+
attr_accessor :http_method
|
68
143
|
|
69
|
-
|
70
|
-
|
71
|
-
|
144
|
+
# there should only be one body parameter; this returns it
|
145
|
+
def body_parameter
|
146
|
+
(parameters || []).detect do |parameter|
|
147
|
+
parameter['in'] == 'body'
|
148
|
+
end
|
149
|
+
end
|
72
150
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
151
|
+
def request_schema
|
152
|
+
if body_parameter && body_parameter['schema']
|
153
|
+
Scorpio::Schema.new(body_parameter['schema'])
|
154
|
+
end
|
77
155
|
end
|
78
156
|
end
|
79
157
|
end
|
@@ -2,7 +2,7 @@ require 'scorpio'
|
|
2
2
|
require 'pickle'
|
3
3
|
|
4
4
|
module Scorpio
|
5
|
-
class
|
5
|
+
class ResourceBase
|
6
6
|
module PickleAdapter
|
7
7
|
include ::Pickle::Adapter::Base
|
8
8
|
|
@@ -10,12 +10,12 @@ module Scorpio
|
|
10
10
|
#
|
11
11
|
# all of the Scorpio models MUST be loaded before this gets called.
|
12
12
|
def self.model_classes
|
13
|
-
ObjectSpace.each_object(Class).select { |klass| klass < ::Scorpio::
|
13
|
+
ObjectSpace.each_object(Class).select { |klass| klass < ::Scorpio::ResourceBase }
|
14
14
|
end
|
15
15
|
|
16
16
|
# get a list of column names for a given class
|
17
17
|
def self.column_names(klass)
|
18
|
-
klass.all_schema_properties
|
18
|
+
klass.all_schema_properties.to_a
|
19
19
|
end
|
20
20
|
|
21
21
|
# Get an instance by id of the model
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'addressable/template'
|
2
|
-
require '
|
2
|
+
require 'faraday'
|
3
|
+
require 'scorpio/util/faraday/response_media_type'
|
3
4
|
|
4
5
|
module Scorpio
|
5
6
|
# see also Faraday::Env::MethodsWithBodies
|
@@ -7,7 +8,7 @@ module Scorpio
|
|
7
8
|
class RequestSchemaFailure < Error
|
8
9
|
end
|
9
10
|
|
10
|
-
class
|
11
|
+
class ResourceBase
|
11
12
|
class << self
|
12
13
|
def define_inheritable_accessor(accessor, options = {})
|
13
14
|
if options[:default_getter]
|
@@ -36,9 +37,6 @@ module Scorpio
|
|
36
37
|
klass.instance_exec(&options[:on_set])
|
37
38
|
end
|
38
39
|
end
|
39
|
-
if options[:update_methods]
|
40
|
-
update_dynamic_methods
|
41
|
-
end
|
42
40
|
end
|
43
41
|
end
|
44
42
|
end
|
@@ -46,35 +44,88 @@ module Scorpio
|
|
46
44
|
# (except in the unlikely event it is overwritten by a subclass)
|
47
45
|
define_inheritable_accessor(:openapi_document_class)
|
48
46
|
# the openapi document
|
49
|
-
define_inheritable_accessor(:
|
50
|
-
define_inheritable_accessor(:
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
47
|
+
define_inheritable_accessor(:tag_name, on_set: -> { update_dynamic_methods })
|
48
|
+
define_inheritable_accessor(:represented_schemas, default_value: [], on_set: proc do
|
49
|
+
unless represented_schemas.respond_to?(:to_ary)
|
50
|
+
raise(TypeError, "represented_schemas must be an array. received: #{represented_schemas.pretty_inspect.chomp}")
|
51
|
+
end
|
52
|
+
if represented_schemas.all? { |s| s.is_a?(Scorpio::Schema) }
|
53
|
+
represented_schemas.each do |schema|
|
54
|
+
openapi_document_class.models_by_schema = openapi_document_class.models_by_schema.merge(schema => self)
|
55
|
+
end
|
56
|
+
update_dynamic_methods
|
57
|
+
else
|
58
|
+
self.represented_schemas = self.represented_schemas.map do |schema|
|
59
|
+
unless schema.is_a?(Scorpio::Schema)
|
60
|
+
schema = Scorpio::Schema.new(schema)
|
61
|
+
end
|
62
|
+
unless schema['type'].nil? || schema.describes_hash?
|
63
|
+
raise(TypeError, "given schema for #{self.inspect} not of type object - type must be object for Scorpio ResourceBase to represent this schema. schema is: #{schema.pretty_inspect.chomp}")
|
64
|
+
end
|
65
|
+
schema
|
66
|
+
end
|
58
67
|
end
|
59
68
|
end)
|
60
|
-
define_inheritable_accessor(:schemas_by_key, default_value: {})
|
61
|
-
define_inheritable_accessor(:schemas_by_path)
|
62
|
-
define_inheritable_accessor(:schemas_by_id, default_value: {})
|
63
69
|
define_inheritable_accessor(:models_by_schema, default_value: {})
|
64
|
-
|
70
|
+
# the base url to which paths are appended.
|
71
|
+
# by default this looks at the openapi document's schemes, picking https or http first.
|
72
|
+
# it looks at the openapi_document's host and basePath.
|
73
|
+
# a model overriding this MUST include the openapi document's basePath if defined, e.g.
|
74
|
+
# class MyModel
|
75
|
+
# self.base_url = File.join('https://example.com/', openapi_document.basePath)
|
76
|
+
# end
|
77
|
+
define_inheritable_accessor(:base_url, default_getter: -> {
|
78
|
+
if openapi_document.schemes.nil?
|
79
|
+
scheme = 'https'
|
80
|
+
elsif openapi_document.schemes.respond_to?(:to_ary)
|
81
|
+
# prefer https, then http, then anything else since we probably don't support.
|
82
|
+
scheme = openapi_document.schemes.sort_by { |s| ['https', 'http'].index(s) || (1.0 / 0) }.first
|
83
|
+
end
|
84
|
+
if openapi_document.host && scheme
|
85
|
+
Addressable::URI.new(
|
86
|
+
scheme: scheme,
|
87
|
+
host: openapi_document.host,
|
88
|
+
path: openapi_document.basePath,
|
89
|
+
).to_s
|
90
|
+
end
|
91
|
+
})
|
65
92
|
|
93
|
+
define_inheritable_accessor(:user_agent, default_getter: -> {
|
94
|
+
"Scorpio/#{Scorpio::VERSION} (https://github.com/notEthan/scorpio) Faraday/#{Faraday::VERSION} Ruby/#{RUBY_VERSION}"
|
95
|
+
})
|
66
96
|
define_inheritable_accessor(:faraday_request_middleware, default_value: [])
|
67
97
|
define_inheritable_accessor(:faraday_adapter, default_getter: proc { Faraday.default_adapter })
|
68
98
|
define_inheritable_accessor(:faraday_response_middleware, default_value: [])
|
69
99
|
class << self
|
70
|
-
def
|
100
|
+
def openapi_document
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def openapi_document=(openapi_document)
|
105
|
+
self.openapi_document_class = self
|
106
|
+
|
71
107
|
if openapi_document.is_a?(Hash)
|
72
|
-
openapi_document = OpenAPI::Document.new(openapi_document)
|
108
|
+
openapi_document = OpenAPI::V2::Document.new(openapi_document)
|
109
|
+
end
|
110
|
+
|
111
|
+
begin
|
112
|
+
singleton_class.instance_exec { remove_method(:openapi_document) }
|
113
|
+
rescue NameError
|
73
114
|
end
|
115
|
+
define_singleton_method(:openapi_document) { openapi_document }
|
116
|
+
define_singleton_method(:openapi_document=) do
|
117
|
+
if self == openapi_document_class
|
118
|
+
raise(ArgumentError, "openapi_document may only be set once on #{self.inspect}")
|
119
|
+
else
|
120
|
+
raise(ArgumentError, "openapi_document may not be overridden on subclass #{self.inspect} after it was set on #{openapi_document_class.inspect}")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
update_dynamic_methods
|
124
|
+
|
74
125
|
openapi_document.paths.each do |path, path_item|
|
75
126
|
path_item.each do |http_method, operation|
|
76
127
|
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)
|
128
|
+
unless operation.is_a?(Scorpio::OpenAPI::V2::Operation)
|
78
129
|
raise("bad operation at #{operation.fragment}: #{operation.pretty_inspect}")
|
79
130
|
end
|
80
131
|
operation.path = path
|
@@ -83,18 +134,7 @@ module Scorpio
|
|
83
134
|
end
|
84
135
|
|
85
136
|
openapi_document.validate!
|
86
|
-
|
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)
|
94
|
-
end
|
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)
|
97
|
-
end
|
137
|
+
|
98
138
|
update_dynamic_methods
|
99
139
|
end
|
100
140
|
|
@@ -104,12 +144,7 @@ module Scorpio
|
|
104
144
|
end
|
105
145
|
|
106
146
|
def all_schema_properties
|
107
|
-
|
108
|
-
unless schema['type'] == 'object'
|
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
|
110
|
-
end
|
111
|
-
schema['properties'].keys
|
112
|
-
end.inject([], &:|)
|
147
|
+
represented_schemas.map(&:described_hash_property_names).inject(Set.new, &:|)
|
113
148
|
end
|
114
149
|
|
115
150
|
def update_instance_accessors
|
@@ -128,14 +163,11 @@ module Scorpio
|
|
128
163
|
end
|
129
164
|
|
130
165
|
def operation_for_resource_class?(operation)
|
131
|
-
return false unless
|
166
|
+
return false unless tag_name
|
132
167
|
|
133
|
-
return true if operation
|
168
|
+
return true if operation.tags.respond_to?(:to_ary) && operation.tags.include?(tag_name)
|
134
169
|
|
135
|
-
|
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) }
|
170
|
+
if operation.request_schema && represented_schemas.include?(operation.request_schema)
|
139
171
|
return true
|
140
172
|
end
|
141
173
|
|
@@ -145,11 +177,8 @@ module Scorpio
|
|
145
177
|
def operation_for_resource_instance?(operation)
|
146
178
|
return false unless operation_for_resource_class?(operation)
|
147
179
|
|
148
|
-
request_schema = operation.body_parameter['schema'] if operation.body_parameter
|
149
|
-
|
150
180
|
# 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) }
|
181
|
+
request_resource_is_self = operation.request_schema && represented_schemas.include?(operation.request_schema)
|
153
182
|
|
154
183
|
# also define an instance method depending on certain attributes the request description
|
155
184
|
# might have in common with the model's schema attributes
|
@@ -165,27 +194,21 @@ module Scorpio
|
|
165
194
|
# should we define an instance method?
|
166
195
|
#request_attributes |= method_desc['parameters'] ? method_desc['parameters'].keys : []
|
167
196
|
|
168
|
-
schema_attributes =
|
169
|
-
schema = schemas_by_key[schema_key]
|
170
|
-
schema['type'] == 'object' && schema['properties'] ? schema['properties'].keys : []
|
171
|
-
end.inject([], &:|)
|
197
|
+
schema_attributes = represented_schemas.map(&:described_hash_property_names).inject(Set.new, &:|)
|
172
198
|
|
173
|
-
return request_resource_is_self || (request_attributes & schema_attributes).any?
|
199
|
+
return request_resource_is_self || (request_attributes & schema_attributes.to_a).any?
|
174
200
|
end
|
175
201
|
|
176
202
|
def method_names_by_operation
|
177
203
|
@method_names_by_operation ||= Hash.new do |h, operation|
|
178
204
|
h[operation] = begin
|
179
|
-
raise(ArgumentError, operation.pretty_inspect) unless operation.is_a?(Scorpio::OpenAPI::Operation)
|
180
|
-
|
181
|
-
|
182
|
-
elsif resource_name && operation.operationId =~ /\A#{Regexp.escape(resource_name)}\.(\w+)\z/
|
205
|
+
raise(ArgumentError, operation.pretty_inspect) unless operation.is_a?(Scorpio::OpenAPI::V2::Operation)
|
206
|
+
|
207
|
+
if operation.tags.respond_to?(:to_ary) && operation.tags.include?(tag_name) && operation.operationId =~ /\A#{Regexp.escape(tag_name)}\.(\w+)\z/
|
183
208
|
method_name = $1
|
184
209
|
else
|
185
|
-
method_name = operation.operationId
|
210
|
+
method_name = operation.operationId
|
186
211
|
end
|
187
|
-
method_name = '_' + method_name unless method_name[/\A[a-zA-Z_]/]
|
188
|
-
method_name.gsub(/[^\w]/, '_')
|
189
212
|
end
|
190
213
|
end
|
191
214
|
end
|
@@ -214,36 +237,14 @@ module Scorpio
|
|
214
237
|
end
|
215
238
|
end
|
216
239
|
|
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)
|
220
|
-
schema && schemas_by_id[schema['$ref']] || schema
|
221
|
-
end
|
222
|
-
|
223
|
-
MODULES_FOR_JSON_SCHEMA_TYPES = {
|
224
|
-
'object' => [Hash],
|
225
|
-
'array' => [Array, Set],
|
226
|
-
'string' => [String],
|
227
|
-
'integer' => [Integer],
|
228
|
-
'number' => [Numeric],
|
229
|
-
'boolean' => [TrueClass, FalseClass],
|
230
|
-
'null' => [NilClass],
|
231
|
-
}
|
232
|
-
|
233
240
|
def connection
|
234
|
-
Faraday.new do |c|
|
235
|
-
unless faraday_request_middleware.any? { |m| [*m].first == :json }
|
236
|
-
c.request :json
|
237
|
-
end
|
241
|
+
Faraday.new(:headers => {'User-Agent' => user_agent}) do |c|
|
238
242
|
faraday_request_middleware.each do |m|
|
239
243
|
c.request(*m)
|
240
244
|
end
|
241
245
|
faraday_response_middleware.each do |m|
|
242
246
|
c.response(*m)
|
243
247
|
end
|
244
|
-
unless faraday_response_middleware.any? { |m| [*m].first == :json }
|
245
|
-
c.response :json, :content_type => /\bjson$/, :preserve_raw => true
|
246
|
-
end
|
247
248
|
c.adapter(*faraday_adapter)
|
248
249
|
end
|
249
250
|
end
|
@@ -266,7 +267,10 @@ module Scorpio
|
|
266
267
|
"which were empty: #{empty_variables.inspect}")
|
267
268
|
end
|
268
269
|
path = path_template.expand(template_params)
|
269
|
-
|
270
|
+
# we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
|
271
|
+
# we use File.join just to deal with consecutive slashes.
|
272
|
+
url = File.join(base_url, path)
|
273
|
+
url = Addressable::URI.parse(url)
|
270
274
|
# assume that call_params must be included somewhere. model_attributes are a source of required things
|
271
275
|
# but not required to be here.
|
272
276
|
other_params = call_params
|
@@ -274,14 +278,13 @@ module Scorpio
|
|
274
278
|
other_params.reject! { |k, _| path_template.variables.include?(k) }
|
275
279
|
end
|
276
280
|
|
277
|
-
|
278
|
-
if request_schema
|
281
|
+
if operation.request_schema
|
279
282
|
# TODO deal with model_attributes / call_params better in nested whatever
|
280
283
|
if call_params.nil?
|
281
|
-
body = request_body_for_schema(model_attributes, request_schema)
|
284
|
+
body = request_body_for_schema(model_attributes, operation.request_schema)
|
282
285
|
elsif call_params.is_a?(Hash)
|
283
|
-
body = request_body_for_schema(model_attributes.merge(call_params), request_schema)
|
284
|
-
body.
|
286
|
+
body = request_body_for_schema(model_attributes.merge(call_params), operation.request_schema)
|
287
|
+
body = body.merge(call_params) # TODO
|
285
288
|
else
|
286
289
|
body = call_params
|
287
290
|
end
|
@@ -300,7 +303,59 @@ module Scorpio
|
|
300
303
|
end
|
301
304
|
end
|
302
305
|
|
303
|
-
|
306
|
+
request_headers = {}
|
307
|
+
|
308
|
+
if METHODS_WITH_BODIES.any? { |m| m.to_s == http_method.downcase.to_s } && body != nil
|
309
|
+
consumes = operation.consumes || openapi_document.consumes || []
|
310
|
+
if consumes.include?("application/json") || (!body.respond_to?(:to_str) && consumes.empty?)
|
311
|
+
# if we have a body that's not a string and no indication of how to serialize it, we guess json.
|
312
|
+
request_headers['Content-Type'] = "application/json"
|
313
|
+
unless body.respond_to?(:to_str)
|
314
|
+
body = ::JSON.pretty_generate(Typelike.as_json(body))
|
315
|
+
end
|
316
|
+
elsif consumes.include?("application/x-www-form-urlencoded")
|
317
|
+
request_headers['Content-Type'] = "application/x-www-form-urlencoded"
|
318
|
+
unless body.respond_to?(:to_str)
|
319
|
+
body = URI.encode_www_form(body)
|
320
|
+
end
|
321
|
+
elsif body.is_a?(String)
|
322
|
+
if consumes.size == 1
|
323
|
+
request_headers['Content-Type'] = consumes.first
|
324
|
+
end
|
325
|
+
else
|
326
|
+
raise("do not know how to serialize for #{consumes.inspect}: #{body.pretty_inspect.chomp}")
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
response = connection.run_request(http_method, url, body, request_headers)
|
331
|
+
|
332
|
+
if response.media_type == 'application/json'
|
333
|
+
if response.body.empty?
|
334
|
+
response_object = nil
|
335
|
+
else
|
336
|
+
begin
|
337
|
+
response_object = ::JSON.parse(response.body)
|
338
|
+
rescue ::JSON::ParserError
|
339
|
+
# TODO warn
|
340
|
+
response_object = response.body
|
341
|
+
end
|
342
|
+
end
|
343
|
+
else
|
344
|
+
response_object = response.body
|
345
|
+
end
|
346
|
+
|
347
|
+
if operation.responses
|
348
|
+
_, operation_response = operation.responses.detect { |k, v| k.to_s == response.status.to_s }
|
349
|
+
operation_response ||= operation.responses['default']
|
350
|
+
response_schema = operation_response['schema'] if operation_response
|
351
|
+
end
|
352
|
+
if response_schema
|
353
|
+
# not too sure about this, but I don't think it makes sense to instantiate things that are
|
354
|
+
# not hash or array as a SchemaInstanceBase
|
355
|
+
if response_object.respond_to?(:to_hash) || response_object.respond_to?(:to_ary)
|
356
|
+
response_object = Scorpio.class_for_schema(response_schema).new(response_object)
|
357
|
+
end
|
358
|
+
end
|
304
359
|
|
305
360
|
error_class = Scorpio.error_classes_by_status[response.status]
|
306
361
|
error_class ||= if (400..499).include?(response.status)
|
@@ -312,27 +367,28 @@ module Scorpio
|
|
312
367
|
end
|
313
368
|
if error_class
|
314
369
|
message = "Error calling operation #{operation.operationId} on #{self}:\n" + (response.env[:raw_body] || response.env.body)
|
315
|
-
raise
|
370
|
+
raise(error_class.new(message).tap do |e|
|
371
|
+
e.faraday_response = response
|
372
|
+
e.response_object = response_object
|
373
|
+
end)
|
316
374
|
end
|
317
375
|
|
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
376
|
initialize_options = {
|
324
377
|
'persisted' => true,
|
325
378
|
'source' => {'operationId' => operation.operationId, 'call_params' => call_params, 'url' => url.to_s},
|
326
379
|
'response' => response,
|
327
380
|
}
|
328
|
-
response_object_to_instances(
|
381
|
+
response_object_to_instances(response_object, initialize_options)
|
329
382
|
end
|
330
383
|
|
331
384
|
def request_body_for_schema(object, schema)
|
332
|
-
|
333
|
-
if object.is_a?(Scorpio::Model)
|
385
|
+
if object.is_a?(Scorpio::ResourceBase)
|
334
386
|
# TODO request_schema_fail unless schema is for given model type
|
335
|
-
request_body_for_schema(object.
|
387
|
+
request_body_for_schema(object.attributes, schema)
|
388
|
+
elsif object.is_a?(Scorpio::SchemaInstanceBase)
|
389
|
+
request_body_for_schema(object.instance, schema)
|
390
|
+
elsif object.is_a?(Scorpio::JSON::Node)
|
391
|
+
request_body_for_schema(object.content, schema)
|
336
392
|
else
|
337
393
|
if object.is_a?(Hash)
|
338
394
|
object.map do |key, value|
|
@@ -391,7 +447,7 @@ module Scorpio
|
|
391
447
|
request_body_for_schema(el, subschema)
|
392
448
|
end
|
393
449
|
else
|
394
|
-
# TODO maybe raise on anything not
|
450
|
+
# TODO maybe raise on anything not serializable
|
395
451
|
# TODO check conformance to schema, request_schema_fail if not
|
396
452
|
object
|
397
453
|
end
|
@@ -402,36 +458,27 @@ module Scorpio
|
|
402
458
|
raise(RequestSchemaFailure, "object does not conform to schema.\nobject = #{object.pretty_inspect}\nschema = #{::JSON.pretty_generate(schema, quirks_mode: true)}")
|
403
459
|
end
|
404
460
|
|
405
|
-
def response_object_to_instances(object,
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
model = models_by_schema[schema_as_key]
|
424
|
-
if model
|
425
|
-
model.new(out, initialize_options)
|
426
|
-
else
|
427
|
-
out
|
428
|
-
end
|
429
|
-
elsif schema['type'] == 'array' && MODULES_FOR_JSON_SCHEMA_TYPES['array'].any? { |m| object.is_a?(m) }
|
461
|
+
def response_object_to_instances(object, initialize_options = {})
|
462
|
+
if object.is_a?(SchemaInstanceBase)
|
463
|
+
model = models_by_schema[object.schema]
|
464
|
+
end
|
465
|
+
|
466
|
+
if object.respond_to?(:to_hash)
|
467
|
+
out = Typelike.modified_copy(object) do
|
468
|
+
object.map do |key, value|
|
469
|
+
{key => response_object_to_instances(value, initialize_options)}
|
470
|
+
end.inject({}, &:update)
|
471
|
+
end
|
472
|
+
if model
|
473
|
+
model.new(out, initialize_options)
|
474
|
+
else
|
475
|
+
out
|
476
|
+
end
|
477
|
+
elsif object.respond_to?(:to_ary)
|
478
|
+
Typelike.modified_copy(object) do
|
430
479
|
object.map do |element|
|
431
|
-
response_object_to_instances(element,
|
480
|
+
response_object_to_instances(element, initialize_options)
|
432
481
|
end
|
433
|
-
else
|
434
|
-
object
|
435
482
|
end
|
436
483
|
else
|
437
484
|
object
|
@@ -470,17 +517,13 @@ module Scorpio
|
|
470
517
|
|
471
518
|
# if we're making a POST or PUT and the request schema is this resource, we'll assume that
|
472
519
|
# the request is persisting this resource
|
473
|
-
|
474
|
-
request_resource_is_self = request_schema &&
|
475
|
-
request_schema['id'] &&
|
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) }
|
520
|
+
request_resource_is_self = operation.request_schema && self.class.represented_schemas.include?(operation.request_schema)
|
477
521
|
if @options['response'] && @options['response'].status && operation.responses
|
478
|
-
_,
|
522
|
+
_, response_schema_node = operation.responses.detect { |k, v| k.to_s == @options['response'].status.to_s }
|
479
523
|
end
|
480
|
-
response_schema =
|
481
|
-
response_resource_is_self = response_schema &&
|
482
|
-
|
483
|
-
if request_resource_is_self && %w(PUT POST).include?(api_method['httpMethod'])
|
524
|
+
response_schema = Scorpio::Schema.new(response_schema_node) if response_schema_node
|
525
|
+
response_resource_is_self = response_schema && self.class.represented_schemas.include?(response_schema)
|
526
|
+
if request_resource_is_self && %w(put post).include?(operation.http_method.to_s.downcase)
|
484
527
|
@persisted = true
|
485
528
|
|
486
529
|
if response_resource_is_self
|
@@ -491,17 +534,16 @@ module Scorpio
|
|
491
534
|
response
|
492
535
|
end
|
493
536
|
|
494
|
-
|
495
|
-
|
496
|
-
@attributes
|
537
|
+
def as_json
|
538
|
+
Typelike.as_json(@attributes)
|
497
539
|
end
|
498
540
|
|
499
541
|
def inspect
|
500
|
-
"\#<#{self.class.
|
542
|
+
"\#<#{self.class.inspect} #{attributes.inspect}>"
|
501
543
|
end
|
502
544
|
def pretty_print(q)
|
503
545
|
q.instance_exec(self) do |obj|
|
504
|
-
text "\#<#{obj.class.
|
546
|
+
text "\#<#{obj.class.inspect}"
|
505
547
|
group_sub {
|
506
548
|
nest(2) {
|
507
549
|
breakable ' '
|
@@ -514,7 +556,7 @@ module Scorpio
|
|
514
556
|
end
|
515
557
|
|
516
558
|
def fingerprint
|
517
|
-
{class: self.class, attributes: @attributes}
|
559
|
+
{class: self.class, attributes: Typelike.as_json(@attributes)}
|
518
560
|
end
|
519
561
|
include FingerprintHash
|
520
562
|
end
|