scorpio 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,79 +1,157 @@
1
- require 'scorpio/schema_object_base'
1
+ require 'scorpio/schema_instance_base'
2
2
 
3
3
  module Scorpio
4
4
  module OpenAPI
5
- openapi_schema_doc = ::JSON.parse(Scorpio.root.join('documents/swagger.io/v2/schema.json').read)
6
- openapi_class = proc do |*key|
7
- Scorpio.class_for_schema(Scorpio::JSON::Node.new_by_type(openapi_schema_doc, key))
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
- Document = openapi_class.call()
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
- # naming these is not strictly necessary, but is nice to have.
13
- # generated: puts Scorpio::OpenAPI::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}')" }
14
- Info = openapi_class.call('definitions', 'info')
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
- class Operation
70
- attr_accessor :path
71
- attr_accessor :http_method
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
- # there should only be one body parameter; this returns it
74
- def body_parameter
75
- (parameters || []).detect do |parameter|
76
- parameter['in'] == 'body'
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 Model
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::Model }
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 'faraday_middleware'
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 Model
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(:openapi_document, on_set: proc { self.openapi_document_class = self })
50
- define_inheritable_accessor(:resource_name, update_methods: true)
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)
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
- define_inheritable_accessor(:base_url)
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 set_openapi_document(openapi_document)
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
- 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)
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
- schemas_by_key.select { |k, _| definition_keys.include?(k) }.map do |schema_key, schema|
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 resource_name
166
+ return false unless tag_name
132
167
 
133
- return true if operation['x-resource'] == self.resource_name
168
+ return true if operation.tags.respond_to?(:to_ary) && operation.tags.include?(tag_name)
134
169
 
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) }
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 = 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([], &:|)
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
- 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/
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 || raise("no operationId on operation: #{operation.pretty_inspect}")
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
- url = Addressable::URI.parse(base_url) + path
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
- request_schema = operation.body_parameter && deref_schema(operation.body_parameter['schema'])
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.update(call_params)
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
- response = connection.run_request(http_method, url, body, nil)
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 error_class.new(message).tap { |e| e.response = response }
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(response.body, response_schema, initialize_options)
381
+ response_object_to_instances(response_object, initialize_options)
329
382
  end
330
383
 
331
384
  def request_body_for_schema(object, schema)
332
- schema = deref_schema(schema)
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.represent_for_schema(schema), schema)
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 jsonifiable
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, schema, initialize_options = {})
406
- schema = deref_schema(schema)
407
- if schema
408
- if schema['type'] == 'object' && MODULES_FOR_JSON_SCHEMA_TYPES['object'].any? { |m| object.is_a?(m) }
409
- out = object.map do |key, value|
410
- schema_for_value = schema['properties'] && schema['properties'][key] ||
411
- if schema['patternProperties']
412
- _, pattern_schema = schema['patternProperties'].detect do |pattern, _|
413
- key =~ Regexp.new(pattern)
414
- end
415
- pattern_schema
416
- end ||
417
- schema['additionalProperties']
418
- {key => response_object_to_instances(value, schema_for_value, initialize_options)}
419
- end.inject(object.class.new, &:update)
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]
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, schema['items'], initialize_options)
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
- request_schema = operation.body_parameter && self.class.deref_schema(operation.body_parameter['schema'])
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
- _, response_schema = operation.responses.detect { |k, v| k.to_s == @options['response'].status.to_s }
522
+ _, response_schema_node = operation.responses.detect { |k, v| k.to_s == @options['response'].status.to_s }
479
523
  end
480
- response_schema = self.class.deref_schema(response_schema)
481
- response_resource_is_self = response_schema &&
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) }
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
- # TODO
495
- def represent_for_schema(schema)
496
- @attributes
537
+ def as_json
538
+ Typelike.as_json(@attributes)
497
539
  end
498
540
 
499
541
  def inspect
500
- "\#<#{self.class.name} #{attributes.inspect}>"
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.name}"
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