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
@@ -0,0 +1,67 @@
|
|
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
|
@@ -0,0 +1,49 @@
|
|
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
|
@@ -0,0 +1,105 @@
|
|
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
ADDED
@@ -0,0 +1,86 @@
|
|
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'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scorpio
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ethan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jsi
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.4.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.4.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: ur
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.
|
33
|
+
version: 0.2.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.
|
40
|
+
version: 0.2.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: faraday
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -56,16 +56,16 @@ dependencies:
|
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: minitest
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -283,6 +283,13 @@ files:
|
|
283
283
|
- lib/scorpio/version.rb
|
284
284
|
- resources/icons/AGPL-3.0.png
|
285
285
|
- scorpio.gemspec
|
286
|
+
- test/blog.openapi2.yml
|
287
|
+
- test/blog.openapi3.yml
|
288
|
+
- test/blog.rb
|
289
|
+
- test/blog.rest_description.yml
|
290
|
+
- test/blog_scorpio_models.rb
|
291
|
+
- test/scorpio_test.rb
|
292
|
+
- test/test_helper.rb
|
286
293
|
homepage: https://github.com/notEthan/scorpio
|
287
294
|
licenses:
|
288
295
|
- AGPL-3.0
|
@@ -306,4 +313,11 @@ rubygems_version: 3.0.6
|
|
306
313
|
signing_key:
|
307
314
|
specification_version: 4
|
308
315
|
summary: Scorpio REST client
|
309
|
-
test_files:
|
316
|
+
test_files:
|
317
|
+
- test/blog.openapi2.yml
|
318
|
+
- test/blog.openapi3.yml
|
319
|
+
- test/blog.rb
|
320
|
+
- test/blog.rest_description.yml
|
321
|
+
- test/blog_scorpio_models.rb
|
322
|
+
- test/scorpio_test.rb
|
323
|
+
- test/test_helper.rb
|