swaggable 0.4.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 +7 -0
- data/.gitignore +2 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +20 -0
- data/README.md +123 -0
- data/Rakefile +13 -0
- data/assets/json-schema-draft-04.json +150 -0
- data/assets/swagger-2.0-schema.json +1495 -0
- data/lib/swaggable.rb +14 -0
- data/lib/swaggable/api_definition.rb +52 -0
- data/lib/swaggable/endpoint_definition.rb +54 -0
- data/lib/swaggable/grape_adapter.rb +84 -0
- data/lib/swaggable/parameter_definition.rb +50 -0
- data/lib/swaggable/rack_app.rb +30 -0
- data/lib/swaggable/response_definition.rb +16 -0
- data/lib/swaggable/swagger_2_serializer.rb +92 -0
- data/lib/swaggable/swagger_2_validator.rb +28 -0
- data/lib/swaggable/tag_definition.rb +27 -0
- data/lib/swaggable/version.rb +3 -0
- data/spec/assets/valid-swagger-2.0.json +1 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/swaggable/api_definition_spec.rb +169 -0
- data/spec/swaggable/endpoint_definition_spec.rb +77 -0
- data/spec/swaggable/grape_adapter_spec.rb +198 -0
- data/spec/swaggable/integration_spec.rb +100 -0
- data/spec/swaggable/parameter_definition_spec.rb +64 -0
- data/spec/swaggable/rack_app_spec.rb +49 -0
- data/spec/swaggable/response_definition_spec.rb +17 -0
- data/spec/swaggable/swagger_2_serializer_spec.rb +208 -0
- data/spec/swaggable/swagger_2_validator_spec.rb +26 -0
- data/spec/swaggable/tag_definition_spec.rb +48 -0
- data/spec/swaggable_spec.rb +7 -0
- data/swaggable.gemspec +29 -0
- metadata +161 -0
data/lib/swaggable.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'swaggable/version'
|
2
|
+
|
3
|
+
module Swaggable
|
4
|
+
autoload :ApiDefinition, 'swaggable/api_definition'
|
5
|
+
autoload :EndpointDefinition, 'swaggable/endpoint_definition'
|
6
|
+
autoload :ParameterDefinition, 'swaggable/parameter_definition'
|
7
|
+
autoload :TagDefinition, 'swaggable/tag_definition'
|
8
|
+
autoload :ResponseDefinition, 'swaggable/response_definition'
|
9
|
+
autoload :RackApp, 'swaggable/rack_app'
|
10
|
+
autoload :GrapeAdapter, 'swaggable/grape_adapter'
|
11
|
+
autoload :Swagger2Serializer, 'swaggable/swagger_2_serializer'
|
12
|
+
autoload :Swagger2Validator, 'swaggable/swagger_2_validator'
|
13
|
+
autoload :EndpointIndex, 'swaggable/endpoint_index'
|
14
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'forwarding_dsl'
|
2
|
+
require 'mini_object'
|
3
|
+
|
4
|
+
module Swaggable
|
5
|
+
class ApiDefinition
|
6
|
+
include ForwardingDsl::Getsetter
|
7
|
+
|
8
|
+
getsetter(
|
9
|
+
:version,
|
10
|
+
:title,
|
11
|
+
:description,
|
12
|
+
:base_path,
|
13
|
+
)
|
14
|
+
|
15
|
+
def initialize &block
|
16
|
+
configure(&block) if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
def endpoints &block
|
20
|
+
ForwardingDsl.run(
|
21
|
+
@endpoints ||= build_endpoints,
|
22
|
+
&block
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def tags
|
27
|
+
(endpoints.map(&:tags).reduce(:merge) || []).dup.freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.from_grape_api grape
|
31
|
+
grape_adapter.import(grape, new)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.grape_adapter
|
35
|
+
@grape_adapter ||= Swaggable::GrapeAdapter.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def configure &block
|
39
|
+
ForwardingDsl.run(self, &block)
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def build_endpoints
|
46
|
+
MiniObject::IndexedList.new.tap do |l|
|
47
|
+
l.build { EndpointDefinition.new }
|
48
|
+
l.key {|e| "#{e.verb.to_s.upcase} #{e.path}" }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'forwarding_dsl'
|
2
|
+
require 'mini_object'
|
3
|
+
|
4
|
+
module Swaggable
|
5
|
+
class EndpointDefinition
|
6
|
+
include ForwardingDsl::Getsetter
|
7
|
+
|
8
|
+
getsetter(
|
9
|
+
:path,
|
10
|
+
:verb,
|
11
|
+
:description,
|
12
|
+
:summary,
|
13
|
+
)
|
14
|
+
|
15
|
+
def initialize args = {}, &block
|
16
|
+
args.each {|k, v| self.send("#{k}=", v) }
|
17
|
+
configure(&block) if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def tags
|
21
|
+
@tags ||= MiniObject::IndexedList.new.tap do |l|
|
22
|
+
l.build { TagDefinition.new }
|
23
|
+
l.key {|e| e.name }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def parameters
|
28
|
+
@parameters ||= MiniObject::IndexedList.new.tap do |l|
|
29
|
+
l.build { ParameterDefinition.new }
|
30
|
+
l.key {|e| e.name }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def responses
|
35
|
+
@responses ||= MiniObject::IndexedList.new.tap do |l|
|
36
|
+
l.build { ResponseDefinition.new }
|
37
|
+
l.key {|e| e.status }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def consumes
|
42
|
+
@consumes ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
def produces
|
46
|
+
@produces ||= []
|
47
|
+
end
|
48
|
+
|
49
|
+
def configure &block
|
50
|
+
ForwardingDsl.run(self, &block)
|
51
|
+
self
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Swaggable
|
2
|
+
class GrapeAdapter
|
3
|
+
def import grape, api
|
4
|
+
api.version = grape.version
|
5
|
+
api.title = grape.name
|
6
|
+
api.base_path = '/'
|
7
|
+
|
8
|
+
grape.routes.each do |grape_endpoint|
|
9
|
+
api.endpoints.add_new do |api_endpoint|
|
10
|
+
import_endpoint grape_endpoint, api_endpoint, grape
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
api
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def extract_path grape_endpoint, grape
|
20
|
+
path = grape_endpoint.route_path
|
21
|
+
path = remove_format_from_path(path)
|
22
|
+
path = substitute_version_in_path(path, grape)
|
23
|
+
substitute_parameters_in_path(path, grape_endpoint)
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove_format_from_path path
|
27
|
+
path.gsub(/\(\/{0,1}\.:format\)$/,'')
|
28
|
+
end
|
29
|
+
|
30
|
+
def substitute_version_in_path path, grape
|
31
|
+
prefix = "/#{grape.prefix.to_s}".gsub(/^\/\//,'/')
|
32
|
+
prefix = '' if prefix == '/'
|
33
|
+
path.gsub(/^#{Regexp.escape prefix}\/:version/,"#{prefix}/#{grape.version}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def substitute_parameters_in_path path, grape_endpoint
|
37
|
+
path = path.dup
|
38
|
+
|
39
|
+
grape_endpoint.route_compiled.names.each do |name|
|
40
|
+
path.gsub!(/:#{name}/, "{#{name}}")
|
41
|
+
end
|
42
|
+
|
43
|
+
path
|
44
|
+
end
|
45
|
+
|
46
|
+
def parameter_from name, options, grape_endpoint
|
47
|
+
Swaggable::ParameterDefinition.new do |p|
|
48
|
+
options = {} if options == ''
|
49
|
+
|
50
|
+
p.name = name
|
51
|
+
p.type = options[:type].downcase.to_sym if options[:type]
|
52
|
+
p.required = options[:required]
|
53
|
+
p.description = options[:desc]
|
54
|
+
p.location = if grape_endpoint.route_compiled.names.include? name
|
55
|
+
:path
|
56
|
+
else
|
57
|
+
:query
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def import_endpoint grape_endpoint, api_endpoint, grape
|
63
|
+
api_endpoint.verb = grape_endpoint.route_method.downcase
|
64
|
+
api_endpoint.summary = grape_endpoint.route_description
|
65
|
+
api_endpoint.produces << "application/#{grape.format}" if grape.format
|
66
|
+
api_endpoint.path = extract_path(grape_endpoint, grape)
|
67
|
+
|
68
|
+
api_endpoint.tags.add_new do |t|
|
69
|
+
t.name = grape.name
|
70
|
+
end
|
71
|
+
|
72
|
+
grape_endpoint.route_params.each do |name, options|
|
73
|
+
api_endpoint.parameters << parameter_from(name, options, grape_endpoint)
|
74
|
+
end
|
75
|
+
|
76
|
+
(grape_endpoint.route_http_codes || []).each do |status, desc, entity|
|
77
|
+
api_endpoint.responses.add_new do |r|
|
78
|
+
r.status = status
|
79
|
+
r.description = desc
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'mini_object'
|
2
|
+
|
3
|
+
module Swaggable
|
4
|
+
class ParameterDefinition
|
5
|
+
include ForwardingDsl::Getsetter
|
6
|
+
|
7
|
+
getsetter(
|
8
|
+
:name,
|
9
|
+
:description,
|
10
|
+
:location,
|
11
|
+
:required,
|
12
|
+
:type,
|
13
|
+
)
|
14
|
+
|
15
|
+
def initialize args = {}
|
16
|
+
args.each {|k, v| self.send("#{k}=", v) }
|
17
|
+
yield self if block_given?
|
18
|
+
end
|
19
|
+
|
20
|
+
def required?
|
21
|
+
!!required
|
22
|
+
end
|
23
|
+
|
24
|
+
def location= location
|
25
|
+
unless valid_locations.include? location
|
26
|
+
raise ArgumentError.new("#{location} is not one of the valid locations: #{valid_locations.join(", ")}")
|
27
|
+
end
|
28
|
+
|
29
|
+
@location = location
|
30
|
+
end
|
31
|
+
|
32
|
+
def type= type
|
33
|
+
unless valid_types.include? type
|
34
|
+
raise ArgumentError.new("#{type} is not one of the valid types: #{valid_types.join(", ")}")
|
35
|
+
end
|
36
|
+
|
37
|
+
@type = type
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def valid_locations
|
43
|
+
[:path, :query, :header, :body, :form, nil]
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_types
|
47
|
+
[:string, :number, :integer, :boolean, :array, :file, nil]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Swaggable
|
4
|
+
class RackApp
|
5
|
+
attr_accessor(
|
6
|
+
:api_definition,
|
7
|
+
:serializer,
|
8
|
+
)
|
9
|
+
|
10
|
+
def initialize args = {}
|
11
|
+
args.each {|k,v| send "#{k}=", v }
|
12
|
+
end
|
13
|
+
|
14
|
+
def call env
|
15
|
+
[
|
16
|
+
200,
|
17
|
+
{'Content-Type' => 'application/json'},
|
18
|
+
[serializer.serialize(api_definition).to_json]
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
def serializer
|
23
|
+
@serializer ||= Swagger2Serializer.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate!
|
27
|
+
serializer.validate! api_definition
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'forwarding_dsl'
|
2
|
+
|
3
|
+
module Swaggable
|
4
|
+
class ResponseDefinition
|
5
|
+
include ForwardingDsl::Getsetter
|
6
|
+
|
7
|
+
getsetter :status
|
8
|
+
getsetter :description
|
9
|
+
|
10
|
+
def initialize args = {}
|
11
|
+
args.each {|k, v| self.send("#{k}=", v) }
|
12
|
+
yield self if block_given?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Swaggable
|
2
|
+
# Generates a Swagger 2 hash from an {ApiDefinition}.
|
3
|
+
#
|
4
|
+
# @example Basic usage
|
5
|
+
# serializer = Swagger2Serializer.new
|
6
|
+
# api_definition = ApiDefinition.new
|
7
|
+
# serializer.serialize(api_definition)
|
8
|
+
# # => {:swagger=>"2.0", :basePath=>nil, :info=>{:title=>nil, :description=>nil, :version=>nil}, :tags=>[], :paths=>{}}
|
9
|
+
#
|
10
|
+
class Swagger2Serializer
|
11
|
+
attr_accessor :tag_serializer
|
12
|
+
|
13
|
+
# Main method that given an {ApiDefinition} will return a hash to serialize
|
14
|
+
def serialize api
|
15
|
+
{
|
16
|
+
swagger: '2.0',
|
17
|
+
basePath: api.base_path,
|
18
|
+
info: serialize_info(api),
|
19
|
+
tags: api.tags.map{|t| serialize_tag t },
|
20
|
+
paths: serialize_endpoints(api.endpoints),
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def serialize_info api
|
25
|
+
{
|
26
|
+
title: api.title.to_s,
|
27
|
+
version: (api.version || '0.0.0'),
|
28
|
+
}.
|
29
|
+
tap do |h|
|
30
|
+
h[:description] = api.description if api.description
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def serialize_tag tag
|
35
|
+
{
|
36
|
+
name: tag.name,
|
37
|
+
}.
|
38
|
+
tap do |e|
|
39
|
+
e[:description] = tag.description if tag.description
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def serialize_endpoints endpoints
|
44
|
+
endpoints.inject({}) do |out, endpoint|
|
45
|
+
out[endpoint.path] ||= {}
|
46
|
+
out[endpoint.path][endpoint.verb] = serialize_endpoint(endpoint)
|
47
|
+
out
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def serialize_endpoint endpoint
|
52
|
+
{
|
53
|
+
tags: endpoint.tags.map(&:name),
|
54
|
+
consumes: endpoint.consumes,
|
55
|
+
produces: endpoint.produces,
|
56
|
+
parameters: endpoint.parameters.map{|p| serialize_parameter p },
|
57
|
+
responses: serialize_responses(endpoint.responses),
|
58
|
+
}.
|
59
|
+
tap do |e|
|
60
|
+
e[:summary] = endpoint.summary if endpoint.summary
|
61
|
+
e[:description] = endpoint.description if endpoint.description
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def serialize_parameter parameter
|
66
|
+
p = {
|
67
|
+
in: parameter.location.to_s,
|
68
|
+
name: parameter.name,
|
69
|
+
required: parameter.required?,
|
70
|
+
}
|
71
|
+
|
72
|
+
p[:type] = parameter.type || 'string'
|
73
|
+
p[:description] = parameter.description if parameter.description
|
74
|
+
p
|
75
|
+
end
|
76
|
+
|
77
|
+
def serialize_responses responses
|
78
|
+
if responses.any?
|
79
|
+
responses.inject({}) do |acc, r|
|
80
|
+
acc[r.status] = {description: r.description}
|
81
|
+
acc
|
82
|
+
end
|
83
|
+
else
|
84
|
+
{200 => {description: 'Success'}}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate! api
|
89
|
+
Swagger2Validator.validate! serialize(api)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'json-schema'
|
2
|
+
|
3
|
+
module Swaggable
|
4
|
+
class Swagger2Validator
|
5
|
+
def self.validate! swagger
|
6
|
+
preload_draft4
|
7
|
+
JSON::Validator.validate!(schema, swagger)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def self.schema
|
13
|
+
@schema = assets_dir + '/swagger-2.0-schema.json'
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.draft4
|
17
|
+
@draft4 = assets_dir + '/json-schema-draft-04.json'
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.assets_dir
|
21
|
+
File.dirname(__FILE__) + '/../../assets'
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.preload_draft4
|
25
|
+
@draft4_preloaded ||= JSON::Validator.validate!(draft4, {})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'forwarding_dsl'
|
2
|
+
|
3
|
+
module Swaggable
|
4
|
+
class TagDefinition
|
5
|
+
include ForwardingDsl::Getsetter
|
6
|
+
|
7
|
+
getsetter(
|
8
|
+
:name,
|
9
|
+
:description,
|
10
|
+
)
|
11
|
+
|
12
|
+
def initialize args = {}
|
13
|
+
args.each {|k, v| self.send("#{k}=", v) }
|
14
|
+
yield self if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def == other
|
18
|
+
self.name == other.name if other.respond_to?(:name)
|
19
|
+
end
|
20
|
+
alias eql? ==
|
21
|
+
|
22
|
+
def hash
|
23
|
+
name.hash
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|