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.
@@ -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