scorpio 0.4.6 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- model = models_by_schema[object.schema]
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
@@ -1,5 +1,7 @@
1
1
  module Scorpio
2
- class Response < ::Ur::Response
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)
@@ -1,5 +1,16 @@
1
1
  module Scorpio
2
- class Ur < ::Ur
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
@@ -1,3 +1,3 @@
1
1
  module Scorpio
2
- VERSION = "0.4.6"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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.3.0"
24
- spec.add_dependency "ur", "~> 0.1.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", "~> 10.0"
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
@@ -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