swaggable 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|