scorpio 0.4.6 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +1 -1
- data/lib/scorpio/google_api_document.rb +16 -9
- data/lib/scorpio/openapi.rb +125 -131
- data/lib/scorpio/openapi/document.rb +4 -4
- data/lib/scorpio/openapi/operation.rb +2 -2
- data/lib/scorpio/openapi/operations_scope.rb +1 -1
- data/lib/scorpio/openapi/v3/server.rb +1 -1
- data/lib/scorpio/request.rb +13 -9
- data/lib/scorpio/resource_base.rb +9 -2
- data/lib/scorpio/response.rb +3 -1
- data/lib/scorpio/ur.rb +12 -15
- data/lib/scorpio/version.rb +1 -1
- data/scorpio.gemspec +4 -3
- data/test/blog.openapi2.yml +113 -0
- data/test/blog.openapi3.yml +131 -0
- data/test/blog.rb +117 -0
- data/test/blog.rest_description.yml +67 -0
- data/test/blog_scorpio_models.rb +49 -0
- data/test/scorpio_test.rb +105 -0
- data/test/test_helper.rb +86 -0
- metadata +25 -11
@@ -423,7 +423,14 @@ module Scorpio
|
|
423
423
|
|
424
424
|
def response_object_to_instances(object, initialize_options = {})
|
425
425
|
if object.is_a?(JSI::Base)
|
426
|
-
|
426
|
+
models = object.jsi_schemas.map { |schema| models_by_schema[schema] }
|
427
|
+
if models.size == 0
|
428
|
+
model = nil
|
429
|
+
elsif models.size == 1
|
430
|
+
model = models.first
|
431
|
+
else
|
432
|
+
raise(Scorpio::OpenAPI::Error, "multiple models indicated by response JSI. models: #{models.inspect}; jsi: #{jsi.pretty_inspect.chomp}")
|
433
|
+
end
|
427
434
|
end
|
428
435
|
|
429
436
|
if object.respond_to?(:to_hash)
|
@@ -522,6 +529,6 @@ module Scorpio
|
|
522
529
|
def jsi_fingerprint
|
523
530
|
{class: self.class, attributes: JSI::Typelike.as_json(@attributes)}
|
524
531
|
end
|
525
|
-
include JSI::FingerprintHash
|
532
|
+
include JSI::Util::FingerprintHash
|
526
533
|
end
|
527
534
|
end
|
data/lib/scorpio/response.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module Scorpio
|
2
|
-
|
2
|
+
Response = Scorpio::Ur.properties['response']
|
3
|
+
|
4
|
+
module Response
|
3
5
|
# @return [::JSI::Schema] the schema for this response according to its OpenAPI doc
|
4
6
|
def response_schema
|
5
7
|
ur.scorpio_request.operation.response_schema(status: status, media_type: media_type)
|
data/lib/scorpio/ur.rb
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
module Scorpio
|
2
|
-
|
2
|
+
# Scorpio::Ur is a JSI Schema module with which scorpio extends the ::Ur schema module
|
3
|
+
Ur = JSI::Schema.new({
|
4
|
+
'$id' => 'https://schemas.jsi.unth.net/ur',
|
5
|
+
'properties' => {
|
6
|
+
'request' => {},
|
7
|
+
'response' => {},
|
8
|
+
}
|
9
|
+
}).jsi_schema_module
|
10
|
+
|
11
|
+
-> { Scorpio::Response }.() # invoke autoload
|
12
|
+
|
13
|
+
module Ur
|
3
14
|
attr_accessor :scorpio_request
|
4
15
|
|
5
16
|
# raises a subclass of Scorpio::HTTPError if the response has an error status.
|
@@ -28,19 +39,5 @@ module Scorpio
|
|
28
39
|
end
|
29
40
|
nil
|
30
41
|
end
|
31
|
-
|
32
|
-
private
|
33
|
-
# overrides JSI::Base#class_for_schema to use Scorpio::Response instead of ::Ur::Response.
|
34
|
-
# maybe a Scorpio::Ur::Request in the future if I need to extend that ... or Scorpio::Request
|
35
|
-
# if I decide to make that subclass ::Ur::Request. not sure if that's a good idea or a terrible
|
36
|
-
# idea.
|
37
|
-
def class_for_schema(schema)
|
38
|
-
jsi_class_for_schema = super
|
39
|
-
if jsi_class_for_schema == ::Ur::Response
|
40
|
-
Scorpio::Response
|
41
|
-
else
|
42
|
-
jsi_class_for_schema
|
43
|
-
end
|
44
|
-
end
|
45
42
|
end
|
46
43
|
end
|
data/lib/scorpio/version.rb
CHANGED
data/scorpio.gemspec
CHANGED
@@ -16,14 +16,15 @@ Gem::Specification.new do |spec|
|
|
16
16
|
ignore_files = %w(.gitignore .travis.yml Gemfile test)
|
17
17
|
ignore_files_re = %r{\A(#{ignore_files.map { |f| Regexp.escape(f) }.join('|')})(/|\z)}
|
18
18
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(ignore_files_re) }
|
19
|
+
spec.test_files = `git ls-files -z test`.split("\x0")
|
19
20
|
spec.bindir = "exe"
|
20
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
22
|
spec.require_paths = ["lib"]
|
22
23
|
|
23
|
-
spec.add_dependency "jsi", "~> 0.
|
24
|
-
spec.add_dependency "ur", "~> 0.
|
24
|
+
spec.add_dependency "jsi", "~> 0.4.0"
|
25
|
+
spec.add_dependency "ur", "~> 0.2.0"
|
25
26
|
spec.add_dependency "faraday"
|
26
|
-
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rake"
|
27
28
|
spec.add_development_dependency "minitest", "~> 5.0"
|
28
29
|
spec.add_development_dependency "minitest-around"
|
29
30
|
spec.add_development_dependency "minitest-reporters"
|
@@ -0,0 +1,113 @@
|
|
1
|
+
---
|
2
|
+
swagger: '2.0'
|
3
|
+
info:
|
4
|
+
title: Scorpio Blog
|
5
|
+
description: REST service for the Scorpio Blog
|
6
|
+
version: ''
|
7
|
+
contact: {}
|
8
|
+
host: blog
|
9
|
+
basePath: "/v1"
|
10
|
+
schemes: ['https']
|
11
|
+
consumes:
|
12
|
+
- application/json
|
13
|
+
produces:
|
14
|
+
- application/json
|
15
|
+
paths:
|
16
|
+
"/articles":
|
17
|
+
get:
|
18
|
+
tags:
|
19
|
+
- articles
|
20
|
+
operationId: articles.index
|
21
|
+
responses:
|
22
|
+
default:
|
23
|
+
description: default response
|
24
|
+
schema:
|
25
|
+
type: array
|
26
|
+
items:
|
27
|
+
"$ref": "#/definitions/articles"
|
28
|
+
post:
|
29
|
+
tags:
|
30
|
+
- articles
|
31
|
+
operationId: articles.post
|
32
|
+
parameters:
|
33
|
+
- name: body
|
34
|
+
in: body
|
35
|
+
required: true
|
36
|
+
schema:
|
37
|
+
"$ref": "#/definitions/articles"
|
38
|
+
responses:
|
39
|
+
default:
|
40
|
+
description: default response
|
41
|
+
schema:
|
42
|
+
"$ref": "#/definitions/articles"
|
43
|
+
"/articles_with_root":
|
44
|
+
get:
|
45
|
+
tags:
|
46
|
+
- articles
|
47
|
+
operationId: articles.index_with_root
|
48
|
+
responses:
|
49
|
+
default:
|
50
|
+
description: default response
|
51
|
+
schema:
|
52
|
+
type: object
|
53
|
+
properties:
|
54
|
+
articles:
|
55
|
+
type: array
|
56
|
+
items:
|
57
|
+
"$ref": "#/definitions/articles"
|
58
|
+
best_article:
|
59
|
+
"$ref": "#/definitions/articles"
|
60
|
+
version:
|
61
|
+
type: string
|
62
|
+
"/articles/{id}":
|
63
|
+
get:
|
64
|
+
tags:
|
65
|
+
- articles
|
66
|
+
operationId: articles.read
|
67
|
+
parameters:
|
68
|
+
- name: id
|
69
|
+
in: path
|
70
|
+
required: true
|
71
|
+
type: string
|
72
|
+
responses:
|
73
|
+
default:
|
74
|
+
description: default response
|
75
|
+
schema:
|
76
|
+
"$ref": "#/definitions/articles"
|
77
|
+
patch:
|
78
|
+
tags:
|
79
|
+
- articles
|
80
|
+
operationId: articles.patch
|
81
|
+
parameters:
|
82
|
+
- name: id
|
83
|
+
in: path
|
84
|
+
required: true
|
85
|
+
type: string
|
86
|
+
- name: body
|
87
|
+
in: body
|
88
|
+
required: true
|
89
|
+
schema:
|
90
|
+
"$ref": "#/definitions/articles"
|
91
|
+
responses:
|
92
|
+
default:
|
93
|
+
description: default response
|
94
|
+
schema:
|
95
|
+
"$ref": "#/definitions/articles"
|
96
|
+
"/clean":
|
97
|
+
post:
|
98
|
+
tags:
|
99
|
+
- clean
|
100
|
+
operationId: clean
|
101
|
+
responses:
|
102
|
+
default:
|
103
|
+
description: default response
|
104
|
+
definitions:
|
105
|
+
articles:
|
106
|
+
type: object
|
107
|
+
properties:
|
108
|
+
id:
|
109
|
+
type: integer
|
110
|
+
title:
|
111
|
+
type: string
|
112
|
+
author_id:
|
113
|
+
type: integer
|
@@ -0,0 +1,131 @@
|
|
1
|
+
openapi: 3.0.0
|
2
|
+
servers:
|
3
|
+
- url: "{scheme}://{host}:{port}/{basePath}"
|
4
|
+
variables:
|
5
|
+
scheme:
|
6
|
+
default: https
|
7
|
+
host:
|
8
|
+
default: blog.scorpio
|
9
|
+
port:
|
10
|
+
default: '443'
|
11
|
+
basePath:
|
12
|
+
enum:
|
13
|
+
- v1
|
14
|
+
default: v1
|
15
|
+
info:
|
16
|
+
title: Scorpio Blog
|
17
|
+
description: REST service for the Scorpio Blog
|
18
|
+
version: ''
|
19
|
+
contact: {}
|
20
|
+
paths:
|
21
|
+
/articles:
|
22
|
+
get:
|
23
|
+
tags:
|
24
|
+
- articles
|
25
|
+
operationId: articles.index
|
26
|
+
responses:
|
27
|
+
default:
|
28
|
+
description: default response
|
29
|
+
content:
|
30
|
+
application/json:
|
31
|
+
schema:
|
32
|
+
type: array
|
33
|
+
items:
|
34
|
+
$ref: '#/components/schemas/articles'
|
35
|
+
post:
|
36
|
+
tags:
|
37
|
+
- articles
|
38
|
+
operationId: articles.post
|
39
|
+
responses:
|
40
|
+
default:
|
41
|
+
description: default response
|
42
|
+
content:
|
43
|
+
application/json:
|
44
|
+
schema:
|
45
|
+
$ref: '#/components/schemas/articles'
|
46
|
+
requestBody:
|
47
|
+
$ref: '#/components/requestBodies/articles'
|
48
|
+
/articles_with_root:
|
49
|
+
get:
|
50
|
+
tags:
|
51
|
+
- articles
|
52
|
+
operationId: articles.index_with_root
|
53
|
+
responses:
|
54
|
+
default:
|
55
|
+
description: default response
|
56
|
+
content:
|
57
|
+
application/json:
|
58
|
+
schema:
|
59
|
+
type: object
|
60
|
+
properties:
|
61
|
+
articles:
|
62
|
+
type: array
|
63
|
+
items:
|
64
|
+
$ref: '#/components/schemas/articles'
|
65
|
+
best_article:
|
66
|
+
$ref: '#/components/schemas/articles'
|
67
|
+
version:
|
68
|
+
type: string
|
69
|
+
'/articles/{id}':
|
70
|
+
get:
|
71
|
+
tags:
|
72
|
+
- articles
|
73
|
+
operationId: articles.read
|
74
|
+
parameters:
|
75
|
+
- name: id
|
76
|
+
in: path
|
77
|
+
required: true
|
78
|
+
schema:
|
79
|
+
type: string
|
80
|
+
responses:
|
81
|
+
default:
|
82
|
+
description: default response
|
83
|
+
content:
|
84
|
+
application/json:
|
85
|
+
schema:
|
86
|
+
$ref: '#/components/schemas/articles'
|
87
|
+
patch:
|
88
|
+
tags:
|
89
|
+
- articles
|
90
|
+
operationId: articles.patch
|
91
|
+
parameters:
|
92
|
+
- name: id
|
93
|
+
in: path
|
94
|
+
required: true
|
95
|
+
schema:
|
96
|
+
type: string
|
97
|
+
responses:
|
98
|
+
default:
|
99
|
+
description: default response
|
100
|
+
content:
|
101
|
+
application/json:
|
102
|
+
schema:
|
103
|
+
$ref: '#/components/schemas/articles'
|
104
|
+
requestBody:
|
105
|
+
$ref: '#/components/requestBodies/articles'
|
106
|
+
/clean:
|
107
|
+
post:
|
108
|
+
tags:
|
109
|
+
- clean
|
110
|
+
operationId: clean
|
111
|
+
responses:
|
112
|
+
default:
|
113
|
+
description: default response
|
114
|
+
components:
|
115
|
+
requestBodies:
|
116
|
+
articles:
|
117
|
+
content:
|
118
|
+
application/json:
|
119
|
+
schema:
|
120
|
+
$ref: '#/components/schemas/articles'
|
121
|
+
required: true
|
122
|
+
schemas:
|
123
|
+
articles:
|
124
|
+
type: object
|
125
|
+
properties:
|
126
|
+
id:
|
127
|
+
type: integer
|
128
|
+
title:
|
129
|
+
type: string
|
130
|
+
author_id:
|
131
|
+
type: integer
|
data/test/blog.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'api_hammer'
|
3
|
+
require 'rack/accept'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
# app
|
7
|
+
|
8
|
+
class Blog < Sinatra::Base
|
9
|
+
include ApiHammer::Sinatra
|
10
|
+
self.supported_media_types = ['application/json']
|
11
|
+
set :static, false
|
12
|
+
disable :protection
|
13
|
+
logpath = Pathname.new('log/test.log')
|
14
|
+
FileUtils.mkdir_p(logpath.dirname)
|
15
|
+
set :logger, ::Logger.new(logpath)
|
16
|
+
logger.level = ::Logger::INFO
|
17
|
+
define_method(:logger) { self.class.logger }
|
18
|
+
use_with_lint ApiHammer::RequestLogger, logger
|
19
|
+
|
20
|
+
# prevent sinatra from using Sinatra::ShowExceptions so we can use ShowTextExceptions instead
|
21
|
+
set :show_exceptions, false
|
22
|
+
# allow errors to bubble past sinatra up to ShowTextExceptions
|
23
|
+
set :raise_errors, true
|
24
|
+
# ShowTextExceptions rescues ruby exceptions and gives a response of 500 with text/plain
|
25
|
+
use_with_lint ApiHammer::ShowTextExceptions, :full_error => true, :logger => logger
|
26
|
+
end
|
27
|
+
|
28
|
+
# models
|
29
|
+
|
30
|
+
require 'active_record'
|
31
|
+
ActiveRecord::Base.logger = Blog.logger
|
32
|
+
dbpath = Pathname.new('tmp/blog.sqlite3')
|
33
|
+
FileUtils.mkdir_p(dbpath.dirname)
|
34
|
+
dbpath.unlink if dbpath.exist?
|
35
|
+
ActiveRecord::Base.establish_connection(
|
36
|
+
:adapter => "sqlite3",
|
37
|
+
:database => dbpath
|
38
|
+
)
|
39
|
+
|
40
|
+
ActiveRecord::Schema.define do
|
41
|
+
create_table :articles do |table|
|
42
|
+
table.column :title, :string
|
43
|
+
table.column :author_id, :integer
|
44
|
+
end
|
45
|
+
|
46
|
+
create_table :authors do |table|
|
47
|
+
table.column :name, :string
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# we will namespace the models under Blog so that the top-level namespace
|
52
|
+
# can be used by the scorpio model classes
|
53
|
+
class Blog
|
54
|
+
class Article < ActiveRecord::Base
|
55
|
+
# validates_enthusiasm_of :title
|
56
|
+
validate { errors.add(:title, "with gusto!") if title && !title[/!\z/] }
|
57
|
+
end
|
58
|
+
class Author < ActiveRecord::Base
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# controllers
|
63
|
+
|
64
|
+
class Blog
|
65
|
+
get '/v1/articles' do
|
66
|
+
check_accept
|
67
|
+
|
68
|
+
articles = Blog::Article.all
|
69
|
+
format_response(200, articles.map(&:serializable_hash))
|
70
|
+
end
|
71
|
+
get '/v1/articles_with_root' do
|
72
|
+
check_accept
|
73
|
+
|
74
|
+
articles = Blog::Article.all
|
75
|
+
body = {
|
76
|
+
# this is on the response schema, an array with items whose id indicates they are articles
|
77
|
+
'articles' => articles.map(&:serializable_hash),
|
78
|
+
# in the response schema, a single article
|
79
|
+
'best_article' => articles.last.serializable_hash,
|
80
|
+
# this is on the response schema, not indicating it is an article
|
81
|
+
'version' => 'v1',
|
82
|
+
# this is not in the response schema at all
|
83
|
+
'note' => 'hi!',
|
84
|
+
}
|
85
|
+
format_response(200, body)
|
86
|
+
end
|
87
|
+
get '/v1/articles/:id' do |id|
|
88
|
+
article = find_or_halt(Blog::Article, id: id)
|
89
|
+
format_response(200, article.serializable_hash)
|
90
|
+
end
|
91
|
+
post '/v1/articles' do
|
92
|
+
article = Blog::Article.create(parsed_body)
|
93
|
+
if article.persisted?
|
94
|
+
format_response(200, article.serializable_hash)
|
95
|
+
else
|
96
|
+
halt_unprocessable_entity(article.errors.messages)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
patch '/v1/articles/:id' do |id|
|
100
|
+
article_attrs = parsed_body
|
101
|
+
article = find_or_halt(Blog::Article, id: id)
|
102
|
+
|
103
|
+
article.assign_attributes(article_attrs)
|
104
|
+
saved = article.save
|
105
|
+
if saved
|
106
|
+
format_response(200, article.serializable_hash)
|
107
|
+
else
|
108
|
+
halt_unprocessable_entity(article.errors.messages)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
require 'database_cleaner'
|
113
|
+
post '/v1/clean' do
|
114
|
+
DatabaseCleaner.clean_with(:truncation)
|
115
|
+
format_response(200, nil)
|
116
|
+
end
|
117
|
+
end
|