scorpio 0.5.0 → 0.6.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 +5 -0
- data/LICENSE.md +1 -1
- data/README.md +26 -17
- data/lib/scorpio/google_api_document.rb +9 -1
- data/lib/scorpio/openapi/document.rb +4 -2
- data/lib/scorpio/openapi/operation.rb +90 -40
- data/lib/scorpio/openapi/operations_scope.rb +13 -11
- data/lib/scorpio/openapi/reference.rb +27 -2
- data/lib/scorpio/openapi/tag.rb +15 -0
- data/lib/scorpio/openapi/v3/server.rb +3 -1
- data/lib/scorpio/openapi.rb +55 -17
- data/lib/scorpio/pickle_adapter.rb +2 -0
- data/lib/scorpio/request.rb +47 -32
- data/lib/scorpio/resource_base.rb +234 -201
- data/lib/scorpio/response.rb +6 -4
- data/lib/scorpio/ur.rb +7 -3
- data/lib/scorpio/version.rb +3 -1
- data/lib/scorpio.rb +5 -6
- data/scorpio.gemspec +15 -23
- metadata +21 -220
- data/.simplecov +0 -1
- data/Rakefile +0 -10
- data/bin/documents_to_yml.rb +0 -33
- data/resources/icons/AGPL-3.0.png +0 -0
- data/test/blog.openapi2.yml +0 -113
- data/test/blog.openapi3.yml +0 -131
- data/test/blog.rb +0 -117
- data/test/blog.rest_description.yml +0 -67
- data/test/blog_scorpio_models.rb +0 -49
- data/test/scorpio_test.rb +0 -105
- data/test/test_helper.rb +0 -86
data/test/blog.rb
DELETED
@@ -1,117 +0,0 @@
|
|
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
|
@@ -1,67 +0,0 @@
|
|
1
|
-
discoveryVersion: v1
|
2
|
-
name: blog
|
3
|
-
title: "Scorpio Blog"
|
4
|
-
description: "REST service for the Scorpio Blog"
|
5
|
-
documentationLink: https://github.com/notEthan/scorpio
|
6
|
-
servicePath: /v1
|
7
|
-
resources:
|
8
|
-
articles:
|
9
|
-
methods:
|
10
|
-
index:
|
11
|
-
path: articles
|
12
|
-
httpMethod: GET
|
13
|
-
response:
|
14
|
-
type: array
|
15
|
-
items:
|
16
|
-
$ref: https://blog.example.com/schemas/articles/v1.0.0
|
17
|
-
index_with_root:
|
18
|
-
path: articles_with_root
|
19
|
-
httpMethod: GET
|
20
|
-
response:
|
21
|
-
type: object
|
22
|
-
properties:
|
23
|
-
articles:
|
24
|
-
type: array
|
25
|
-
items:
|
26
|
-
$ref: https://blog.example.com/schemas/articles/v1.0.0
|
27
|
-
best_article:
|
28
|
-
$ref: https://blog.example.com/schemas/articles/v1.0.0
|
29
|
-
version:
|
30
|
-
type: string
|
31
|
-
read:
|
32
|
-
path: articles/{id}
|
33
|
-
httpMethod: GET
|
34
|
-
response:
|
35
|
-
$ref: https://blog.example.com/schemas/articles/v1.0.0
|
36
|
-
post:
|
37
|
-
path: articles
|
38
|
-
httpMethod: POST
|
39
|
-
request:
|
40
|
-
$ref: https://blog.example.com/schemas/articles/v1.0.0
|
41
|
-
response:
|
42
|
-
$ref: https://blog.example.com/schemas/articles/v1.0.0
|
43
|
-
patch:
|
44
|
-
path: articles/{id}
|
45
|
-
httpMethod: PATCH
|
46
|
-
request:
|
47
|
-
$ref: https://blog.example.com/schemas/articles/v1.0.0
|
48
|
-
response:
|
49
|
-
$ref: https://blog.example.com/schemas/articles/v1.0.0
|
50
|
-
clean:
|
51
|
-
methods:
|
52
|
-
clean:
|
53
|
-
path: clean
|
54
|
-
httpMethod: POST
|
55
|
-
response:
|
56
|
-
{}
|
57
|
-
schemas:
|
58
|
-
articles:
|
59
|
-
id: https://blog.example.com/schemas/articles/v1.0.0
|
60
|
-
type: object
|
61
|
-
properties:
|
62
|
-
id:
|
63
|
-
type: integer
|
64
|
-
title:
|
65
|
-
type: string
|
66
|
-
author_id:
|
67
|
-
type: integer
|
data/test/blog_scorpio_models.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
require 'logger'
|
2
|
-
require 'api_hammer'
|
3
|
-
|
4
|
-
# this is a virtual model to parent models representing resources of the blog. it sets
|
5
|
-
# up connection information including base url, custom middleware or adapter for faraday.
|
6
|
-
# it describes the API by setting the API document, but this class itself represents no
|
7
|
-
# resources - it sets no resource_name and defines no schema_keys.
|
8
|
-
class BlogModel < Scorpio::ResourceBase
|
9
|
-
define_inheritable_accessor(:logger)
|
10
|
-
logpath = Pathname.new('log/test.log')
|
11
|
-
FileUtils.mkdir_p(logpath.dirname)
|
12
|
-
self.logger = ::Logger.new(logpath)
|
13
|
-
|
14
|
-
if ENV['SCORPIO_API_DESCRIPTION_FORMAT'] == 'rest_description'
|
15
|
-
self.openapi_document = Scorpio::Google::RestDescription.new_jsi(YAML.load_file('test/blog.rest_description.yml')).to_openapi_document
|
16
|
-
self.base_url = File.join("http://localhost:#{$blog_port || raise(Bug)}/", openapi_document.basePath)
|
17
|
-
elsif ENV['SCORPIO_API_DESCRIPTION_FORMAT'] == 'openapi2'
|
18
|
-
self.openapi_document = YAML.load_file('test/blog.openapi2.yml')
|
19
|
-
self.base_url = File.join("http://localhost:#{$blog_port || raise(Bug)}/", openapi_document.basePath)
|
20
|
-
elsif ENV['SCORPIO_API_DESCRIPTION_FORMAT'] == 'openapi3' || ENV['SCORPIO_API_DESCRIPTION_FORMAT'].nil?
|
21
|
-
self.openapi_document = YAML.load_file('test/blog.openapi3.yml')
|
22
|
-
self.server_variables = {
|
23
|
-
'scheme' => 'http',
|
24
|
-
'host' => 'localhost',
|
25
|
-
'port' => $blog_port || raise(Bug, '$blog_port is nil'),
|
26
|
-
}
|
27
|
-
else
|
28
|
-
abort("bad SCORPIO_API_DESCRIPTION_FORMAT")
|
29
|
-
end
|
30
|
-
self.faraday_builder = -> (conn) {
|
31
|
-
conn.request(:api_hammer_request_logger, logger)
|
32
|
-
}
|
33
|
-
end
|
34
|
-
|
35
|
-
# this is a model of Article, a resource of the blog API. it sets the resource_name
|
36
|
-
# to the key of the 'resources' section of the API (described by the api document
|
37
|
-
# specified to BlogModel)
|
38
|
-
class Article < BlogModel
|
39
|
-
self.tag_name = 'articles'
|
40
|
-
if openapi_document.v2?
|
41
|
-
self.represented_schemas = [openapi_document.definitions['articles']]
|
42
|
-
else
|
43
|
-
self.represented_schemas = [openapi_document.components.schemas['articles']]
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
class BlogClean < BlogModel
|
48
|
-
self.tag_name = 'clean'
|
49
|
-
end
|
data/test/scorpio_test.rb
DELETED
@@ -1,105 +0,0 @@
|
|
1
|
-
require_relative 'test_helper'
|
2
|
-
|
3
|
-
class ScorpioTest < Minitest::Test
|
4
|
-
def test_that_it_has_a_version_number
|
5
|
-
refute_nil ::Scorpio::VERSION
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
describe 'blog' do
|
10
|
-
let(:blog_article) { Article.post('title' => "sports!") }
|
11
|
-
|
12
|
-
it 'indexes articles' do
|
13
|
-
blog_article
|
14
|
-
|
15
|
-
articles = Article.index
|
16
|
-
|
17
|
-
assert_equal(1, articles.size)
|
18
|
-
article = articles[0]
|
19
|
-
assert_equal(1, article['id'])
|
20
|
-
assert(article.is_a?(Article))
|
21
|
-
assert_equal('sports!', article['title'])
|
22
|
-
assert_equal('sports!', article.title)
|
23
|
-
end
|
24
|
-
it 'indexes articles with root' do
|
25
|
-
blog_article
|
26
|
-
|
27
|
-
articles = Article.index_with_root
|
28
|
-
assert_respond_to(articles, :to_hash)
|
29
|
-
assert_equal('v1', articles['version'])
|
30
|
-
assert_equal('hi!', articles['note'])
|
31
|
-
assert_instance_of(Article, articles['best_article'])
|
32
|
-
assert_equal(articles['articles'].last, articles['best_article'])
|
33
|
-
assert_equal(1, articles['articles'].size)
|
34
|
-
article = articles['articles'][0]
|
35
|
-
assert_equal(1, article['id'])
|
36
|
-
assert(article.is_a?(Article))
|
37
|
-
assert_equal('sports!', article['title'])
|
38
|
-
assert_equal('sports!', article.title)
|
39
|
-
end
|
40
|
-
it 'reads an article' do
|
41
|
-
blog_article
|
42
|
-
article = Article.read(id: blog_article.id)
|
43
|
-
assert(article.is_a?(Article))
|
44
|
-
assert_equal('sports!', article['title'])
|
45
|
-
assert_equal('sports!', article.title)
|
46
|
-
end
|
47
|
-
it 'tries to read an article without a required path variable' do
|
48
|
-
blog_article
|
49
|
-
e = assert_raises(ArgumentError) do
|
50
|
-
Article.read({})
|
51
|
-
end
|
52
|
-
assert_equal('path /articles/{id} for operation articles.read requires path_params which were missing: ["id"]',
|
53
|
-
e.message)
|
54
|
-
e = assert_raises(ArgumentError) do
|
55
|
-
Article.read({id: ''})
|
56
|
-
end
|
57
|
-
assert_equal('path /articles/{id} for operation articles.read requires path_params which were empty: ["id"]',
|
58
|
-
e.message)
|
59
|
-
end
|
60
|
-
it 'tries to read a nonexistent article' do
|
61
|
-
err = assert_raises(Scorpio::NotFound404Error) do
|
62
|
-
Article.read(id: 99)
|
63
|
-
end
|
64
|
-
assert_equal({"article" => ["Unknown article! id: 99"]}, JSI::Typelike.as_json(err.response_object['errors']))
|
65
|
-
assert_match(/Unknown article! id: 99/, err.message)
|
66
|
-
end
|
67
|
-
it 'updates an article on the class' do
|
68
|
-
blog_article
|
69
|
-
Article.patch({id: blog_article.id, title: 'politics!'})
|
70
|
-
assert_equal('politics!', Article.read(id: blog_article.id).title)
|
71
|
-
end
|
72
|
-
it 'updates an article on the instance' do
|
73
|
-
blog_article
|
74
|
-
article = Article.read(id: blog_article.id)
|
75
|
-
article.title = 'politics!'
|
76
|
-
article.patch
|
77
|
-
assert_equal('politics!', Article.read(id: blog_article.id).title)
|
78
|
-
end
|
79
|
-
it 'updates an article with an unsuccessful response' do
|
80
|
-
blog_article
|
81
|
-
err = assert_raises(Scorpio::UnprocessableEntity422Error) do
|
82
|
-
Article.patch({id: blog_article.id, title: 'politics?'})
|
83
|
-
end
|
84
|
-
assert_equal({"title" => ["with gusto!"]}, JSI::Typelike.as_json(err.response_object['errors']))
|
85
|
-
assert_match(/with gusto!/, err.message)
|
86
|
-
assert_equal('sports!', Article.read(id: blog_article.id).title)
|
87
|
-
end
|
88
|
-
it 'instantiates an article with bad argument' do
|
89
|
-
assert_raises(ArgumentError) { Article.new("foo") }
|
90
|
-
end
|
91
|
-
it 'reports schema failure when the request does not match the request schema' do
|
92
|
-
# TODO handle blame
|
93
|
-
assert_raises(Scorpio::HTTPErrors::UnprocessableEntity422Error) do
|
94
|
-
# title is supposed to be a string
|
95
|
-
Article.post('title' => {'music' => '!'})
|
96
|
-
end
|
97
|
-
end
|
98
|
-
it 'checks equality' do
|
99
|
-
assert_equal(Article.read(id: blog_article.id), Article.read(id: blog_article.id))
|
100
|
-
end
|
101
|
-
it 'consistently keys a hash' do
|
102
|
-
hash = {Article.read(id: blog_article.id) => 0}
|
103
|
-
assert_equal(0, hash[Article.read(id: blog_article.id)])
|
104
|
-
end
|
105
|
-
end
|
data/test/test_helper.rb
DELETED
@@ -1,86 +0,0 @@
|
|
1
|
-
require 'coveralls'
|
2
|
-
if Coveralls.will_run?
|
3
|
-
Coveralls.wear!
|
4
|
-
end
|
5
|
-
|
6
|
-
require 'simplecov'
|
7
|
-
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
8
|
-
require 'scorpio'
|
9
|
-
|
10
|
-
require 'bundler/setup'
|
11
|
-
|
12
|
-
# NO EXPECTATIONS
|
13
|
-
ENV["MT_NO_EXPECTATIONS"] = ''
|
14
|
-
|
15
|
-
require 'minitest/autorun'
|
16
|
-
require 'minitest/around/spec'
|
17
|
-
require 'minitest/reporters'
|
18
|
-
|
19
|
-
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
20
|
-
|
21
|
-
require 'byebug'
|
22
|
-
|
23
|
-
class ScorpioSpec < Minitest::Spec
|
24
|
-
if ENV['SCORPIO_TEST_ALPHA']
|
25
|
-
# :nocov:
|
26
|
-
define_singleton_method(:test_order) { :alpha }
|
27
|
-
# :nocov:
|
28
|
-
end
|
29
|
-
|
30
|
-
around do |test|
|
31
|
-
test.call
|
32
|
-
BlogClean.clean
|
33
|
-
end
|
34
|
-
|
35
|
-
def assert_equal exp, act, msg = nil
|
36
|
-
msg = message(msg, E) { diff exp, act }
|
37
|
-
assert exp == act, msg
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# register this to be the base class for specs instead of Minitest::Spec
|
42
|
-
Minitest::Spec.register_spec_type(//, ScorpioSpec)
|
43
|
-
|
44
|
-
# boot the blog application in a different process
|
45
|
-
|
46
|
-
# find a free port
|
47
|
-
server = TCPServer.new(0)
|
48
|
-
$blog_port = server.addr[1]
|
49
|
-
server.close
|
50
|
-
|
51
|
-
$blog_pid = fork do
|
52
|
-
require_relative 'blog'
|
53
|
-
|
54
|
-
STDOUT.reopen(Scorpio.root.join('log/blog_webrick_stdout.log').open('a'))
|
55
|
-
STDERR.reopen(Scorpio.root.join('log/blog_webrick_stderr.log').open('a'))
|
56
|
-
|
57
|
-
trap('INT') { ::Rack::Handler::WEBrick.shutdown }
|
58
|
-
|
59
|
-
::Rack::Handler::WEBrick.run(::Blog, Port: $blog_port)
|
60
|
-
end
|
61
|
-
|
62
|
-
# wait for the server to become responsive
|
63
|
-
running = false
|
64
|
-
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
65
|
-
timeout = 30
|
66
|
-
while !running
|
67
|
-
require 'socket'
|
68
|
-
begin
|
69
|
-
sock=TCPSocket.new('localhost', $blog_port)
|
70
|
-
running = true
|
71
|
-
sock.close
|
72
|
-
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
|
73
|
-
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > started + timeout
|
74
|
-
raise $!.class, "Failed to connect to the server on port #{$blog_port} after #{timeout} seconds.\n\n#{$!.message}", $!.backtrace
|
75
|
-
end
|
76
|
-
sleep 2**-2
|
77
|
-
STDOUT.write('.')
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
Minitest.after_run do
|
82
|
-
Process.kill('INT', $blog_pid)
|
83
|
-
Process.waitpid
|
84
|
-
end
|
85
|
-
|
86
|
-
require_relative 'blog_scorpio_models'
|