scorpio 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +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'
|