swaggard 0.5.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/lib/swaggard/api_definition.rb +26 -14
- data/lib/swaggard/configuration.rb +48 -2
- data/lib/swaggard/parsers/{controllers.rb → controller.rb} +5 -7
- data/lib/swaggard/parsers/routes.rb +16 -15
- data/lib/swaggard/swagger/definition.rb +19 -6
- data/lib/swaggard/swagger/operation.rb +37 -25
- data/lib/swaggard/swagger/parameters/base.rb +6 -5
- data/lib/swaggard/swagger/parameters/body.rb +36 -10
- data/lib/swaggard/swagger/parameters/form.rb +1 -1
- data/lib/swaggard/swagger/parameters/list.rb +1 -3
- data/lib/swaggard/swagger/parameters/path.rb +17 -4
- data/lib/swaggard/swagger/parameters/query.rb +13 -8
- data/lib/swaggard/swagger/path.rb +5 -3
- data/lib/swaggard/swagger/property.rb +18 -3
- data/lib/swaggard/swagger/response.rb +25 -4
- data/lib/swaggard/swagger/response_header.rb +37 -0
- data/lib/swaggard/swagger/type.rb +13 -8
- data/lib/swaggard/version.rb +1 -3
- data/lib/swaggard.rb +69 -16
- data/lib/tasks/swaggard.rake +7 -1
- data/spec/fixtures/api.json +1 -1
- data/spec/integration/swaggard_spec.rb +0 -2
- metadata +13 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fc7e15b774e8c15ddf109dfaad4d2b3dd571e6a5527b8d237bcc89e07d5c867f
|
4
|
+
data.tar.gz: 1889e419f45111899943019b967ecb1ce094331a23b0fcc79fabd7e387c6e7ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f86d35cd4362a15b4a93bd9ac2ae4ce4a78d6bdca04823a9b3a8636903c35472944c73cde67d5e8c5ac5c05cf0c85c32aa7a25788173a52487e13aef184cc4c
|
7
|
+
data.tar.gz: 452ef43b35624b738762e396758dabf284b265d8e81fb0ef15f8e6ce07306a0c2a9204d1c1efc5a59028753c46aba6239d8912a988f7e7f7071d1772fcc30934
|
@@ -2,7 +2,6 @@ require_relative 'swagger/path'
|
|
2
2
|
|
3
3
|
module Swaggard
|
4
4
|
class ApiDefinition
|
5
|
-
|
6
5
|
attr_accessor :definitions
|
7
6
|
|
8
7
|
def initialize
|
@@ -23,32 +22,45 @@ module Swaggard
|
|
23
22
|
@definitions.concat(operation.definitions)
|
24
23
|
end
|
25
24
|
|
25
|
+
def ignore_put_if_patch!
|
26
|
+
@paths.values.each(&:ignore_put_if_patch!)
|
27
|
+
end
|
28
|
+
|
26
29
|
def to_doc
|
30
|
+
contact = { 'name' => Swaggard.configuration.contact_name }
|
31
|
+
contact['email'] = Swaggard.configuration.contact_email if Swaggard.configuration.contact_email.present?
|
32
|
+
contact['url'] = Swaggard.configuration.contact_url if Swaggard.configuration.contact_url.present?
|
33
|
+
|
34
|
+
license = { 'name' => Swaggard.configuration.license_name }
|
35
|
+
license['url'] = Swaggard.configuration.license_url if Swaggard.configuration.license_url.present?
|
36
|
+
|
27
37
|
{
|
28
38
|
'swagger' => Swaggard.configuration.swagger_version,
|
29
39
|
'info' => {
|
30
|
-
'description' => Swaggard.configuration.description,
|
31
40
|
'version' => Swaggard.configuration.api_version,
|
32
41
|
'title' => Swaggard.configuration.title,
|
42
|
+
'description' => Swaggard.configuration.description,
|
33
43
|
'termsOfService' => Swaggard.configuration.tos,
|
34
|
-
'contact'
|
35
|
-
|
36
|
-
'email' => Swaggard.configuration.contact_email,
|
37
|
-
'url' => Swaggard.configuration.contact_url
|
38
|
-
},
|
39
|
-
'license' => {
|
40
|
-
'name' => Swaggard.configuration.license_name,
|
41
|
-
'url' => Swaggard.configuration.license_url
|
42
|
-
}
|
44
|
+
'contact' => contact,
|
45
|
+
'license' => license,
|
43
46
|
},
|
44
47
|
'host' => Swaggard.configuration.host,
|
45
48
|
'basePath' => Swaggard.configuration.api_base_path,
|
46
|
-
'tags' => @tags.map { |_, tag| tag.to_doc },
|
47
49
|
'schemes' => Swaggard.configuration.schemes,
|
48
|
-
'
|
50
|
+
'consumes' => Swaggard.configuration.api_formats.map { |format| "application/#{format}" },
|
51
|
+
'produces' => Swaggard.configuration.api_formats.map { |format| "application/#{format}" },
|
52
|
+
'tags' => @tags.map { |_, tag| tag.to_doc },
|
53
|
+
'paths' => Hash[@paths.values.map { |path| [format_path(path.path), path.to_doc] }],
|
49
54
|
'definitions' => Hash[@definitions.map { |definition| [definition.id, definition.to_doc] }]
|
50
55
|
}
|
51
56
|
end
|
52
57
|
|
58
|
+
private
|
59
|
+
|
60
|
+
def format_path(path)
|
61
|
+
return path unless Swaggard.configuration.exclude_base_path_from_paths
|
62
|
+
|
63
|
+
path.gsub(Swaggard.configuration.api_base_path, '')
|
64
|
+
end
|
53
65
|
end
|
54
|
-
end
|
66
|
+
end
|
@@ -18,7 +18,12 @@ module Swaggard
|
|
18
18
|
:description, :tos, :contact_email, :contact_name, :contact_url, :host,
|
19
19
|
:authentication_type, :authentication_key, :authentication_value,
|
20
20
|
:access_username, :access_password, :default_content_type, :use_cache,
|
21
|
-
:language, :additional_parameters, :schemes
|
21
|
+
:language, :additional_parameters, :schemes, :ignore_undocumented_paths,
|
22
|
+
:license_name, :exclude_base_path_from_paths, :default_response_description,
|
23
|
+
:default_response_status_code, :excluded_paths, :path_parameter_description,
|
24
|
+
:ignore_put_if_patch_exists
|
25
|
+
|
26
|
+
attr_reader :custom_types
|
22
27
|
|
23
28
|
def swagger_version
|
24
29
|
@swagger_version ||= '2.0'
|
@@ -65,7 +70,7 @@ module Swaggard
|
|
65
70
|
end
|
66
71
|
|
67
72
|
def contact_url
|
68
|
-
@
|
73
|
+
@contact_url ||= ''
|
69
74
|
end
|
70
75
|
|
71
76
|
def license_name
|
@@ -100,6 +105,26 @@ module Swaggard
|
|
100
105
|
@default_content_type ||= ''
|
101
106
|
end
|
102
107
|
|
108
|
+
def default_response_status_code
|
109
|
+
@default_response_status_code ||= 'default'
|
110
|
+
end
|
111
|
+
|
112
|
+
def default_response_description
|
113
|
+
@default_response_description ||= 'successful operation'
|
114
|
+
end
|
115
|
+
|
116
|
+
def ignore_undocumented_paths
|
117
|
+
return @ignore_undocumented_paths unless @ignore_undocumented_paths.nil?
|
118
|
+
|
119
|
+
@ignore_undocumented_paths = false
|
120
|
+
end
|
121
|
+
|
122
|
+
def exclude_base_path_from_paths
|
123
|
+
return @exclude_base_path_from_paths unless @exclude_base_path_from_paths.nil?
|
124
|
+
|
125
|
+
@exclude_base_path_from_paths = false
|
126
|
+
end
|
127
|
+
|
103
128
|
def language
|
104
129
|
@language ||= 'en'
|
105
130
|
end
|
@@ -112,5 +137,26 @@ module Swaggard
|
|
112
137
|
@use_cache ||= false
|
113
138
|
end
|
114
139
|
|
140
|
+
def custom_types
|
141
|
+
@custom_types ||= {}
|
142
|
+
end
|
143
|
+
|
144
|
+
def excluded_paths
|
145
|
+
@excluded_paths ||= []
|
146
|
+
end
|
147
|
+
|
148
|
+
def add_custom_type(name, definition)
|
149
|
+
custom_types[name] = definition
|
150
|
+
end
|
151
|
+
|
152
|
+
def path_parameter_description
|
153
|
+
@path_parameter_description ||= ->(path_parameter) { "Scope response to #{path_parameter.name}" }
|
154
|
+
end
|
155
|
+
|
156
|
+
def ignore_put_if_patch_exists
|
157
|
+
return @ignore_put_if_patch_exists unless @ignore_put_if_patch_exists.nil?
|
158
|
+
|
159
|
+
@ignore_put_if_patch_exists = false
|
160
|
+
end
|
115
161
|
end
|
116
162
|
end
|
@@ -3,23 +3,21 @@ require_relative '../swagger/tag'
|
|
3
3
|
|
4
4
|
module Swaggard
|
5
5
|
module Parsers
|
6
|
-
class
|
6
|
+
class Controller
|
7
7
|
|
8
|
-
def run(yard_objects
|
8
|
+
def run(yard_objects)
|
9
9
|
tag = nil
|
10
|
-
operations =
|
10
|
+
operations = {}
|
11
11
|
|
12
12
|
yard_objects.each do |yard_object|
|
13
13
|
if yard_object.type == :class
|
14
14
|
tag = Swagger::Tag.new(yard_object)
|
15
15
|
elsif tag && yard_object.type == :method
|
16
|
-
|
17
|
-
operations
|
16
|
+
name = yard_object.name
|
17
|
+
operations[name.to_s] = yard_object
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
return unless operations.any?
|
22
|
-
|
23
21
|
return tag, operations
|
24
22
|
end
|
25
23
|
|
@@ -5,21 +5,22 @@ module Swaggard
|
|
5
5
|
def run(routes)
|
6
6
|
return {} unless routes
|
7
7
|
|
8
|
-
parsed_routes
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
8
|
+
routes.inject({}) do |parsed_routes, route|
|
9
|
+
path = route_path(route)
|
10
|
+
|
11
|
+
unless Swaggard.configuration.excluded_paths.any? { |excluded_path| Regexp.new(excluded_path) =~ path }
|
12
|
+
verb = route_verb(route)
|
13
|
+
|
14
|
+
parsed_routes[path] ||= {}
|
15
|
+
parsed_routes[path][verb] = {
|
16
|
+
controller: route_controller(route),
|
17
|
+
action: route_action(route),
|
18
|
+
path_params: route_path_params(route)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
parsed_routes
|
23
|
+
end.sort.to_h
|
23
24
|
end
|
24
25
|
|
25
26
|
private
|
@@ -3,24 +3,37 @@ module Swaggard
|
|
3
3
|
class Definition
|
4
4
|
|
5
5
|
attr_reader :id
|
6
|
+
attr_writer :description, :title
|
6
7
|
|
7
8
|
def initialize(id)
|
8
9
|
@id = id
|
10
|
+
@title = ''
|
9
11
|
@properties = []
|
12
|
+
@description = ''
|
10
13
|
end
|
11
14
|
|
12
15
|
def add_property(property)
|
13
16
|
@properties << property
|
14
17
|
end
|
15
18
|
|
19
|
+
def empty?
|
20
|
+
@properties.empty?
|
21
|
+
end
|
22
|
+
|
16
23
|
def to_doc
|
17
|
-
{
|
18
|
-
'
|
19
|
-
'
|
20
|
-
|
21
|
-
|
24
|
+
{}.tap do |doc|
|
25
|
+
doc['title'] = @title if @title.present?
|
26
|
+
doc['type'] = 'object'
|
27
|
+
|
28
|
+
doc['description'] = @description if @description.present?
|
29
|
+
|
30
|
+
doc['properties'] = Hash[@properties.map { |property| [property.id, property.to_doc] }]
|
31
|
+
required_properties = @properties.select(&:required?).map(&:id)
|
32
|
+
doc['required'] = required_properties if required_properties.any?
|
33
|
+
end
|
34
|
+
|
22
35
|
end
|
23
36
|
|
24
37
|
end
|
25
38
|
end
|
26
|
-
end
|
39
|
+
end
|
@@ -9,28 +9,42 @@ require_relative 'response'
|
|
9
9
|
module Swaggard
|
10
10
|
module Swagger
|
11
11
|
class Operation
|
12
|
-
|
13
12
|
attr_reader :nickname
|
14
13
|
attr_accessor :path, :parameters, :description, :http_method, :summary, :notes,
|
15
14
|
:error_responses, :tag
|
16
15
|
|
17
|
-
def initialize(yard_object, tag,
|
16
|
+
def initialize(yard_object, tag, path, verb, path_params)
|
18
17
|
@name = yard_object.name
|
19
18
|
@tag = tag
|
20
|
-
@summary = yard_object.docstring.lines.first || ''
|
19
|
+
@summary = (yard_object.docstring.lines.first || '').chomp
|
21
20
|
@parameters = []
|
22
21
|
@responses = []
|
23
|
-
|
22
|
+
|
23
|
+
@description = (yard_object.docstring.lines[1..-1] || []).map(&:chomp).reject(&:empty?).compact.join("\n")
|
24
24
|
@formats = Swaggard.configuration.api_formats
|
25
|
+
@http_method = verb
|
26
|
+
@path = path
|
27
|
+
|
28
|
+
build_path_parameters(path_params)
|
25
29
|
|
26
30
|
yard_object.tags.each do |yard_tag|
|
27
31
|
value = yard_tag.text
|
28
32
|
|
29
33
|
case yard_tag.tag_name
|
34
|
+
when 'operation_id'
|
35
|
+
@operation_id = value
|
30
36
|
when 'query_parameter'
|
31
37
|
@parameters << Parameters::Query.new(value)
|
32
38
|
when 'form_parameter'
|
33
39
|
@parameters << Parameters::Form.new(value)
|
40
|
+
when 'body_required'
|
41
|
+
body_parameter.is_required = true
|
42
|
+
when 'body_description'
|
43
|
+
body_parameter.description = value
|
44
|
+
when 'body_title'
|
45
|
+
body_parameter.title = value
|
46
|
+
when 'body_definition'
|
47
|
+
body_parameter.definition = value
|
34
48
|
when 'body_parameter'
|
35
49
|
body_parameter.add_property(value)
|
36
50
|
when 'parameter_list'
|
@@ -41,11 +55,15 @@ module Swaggard
|
|
41
55
|
success_response.status_code = value
|
42
56
|
when 'response_root'
|
43
57
|
success_response.response_root = value
|
58
|
+
when 'response_description'
|
59
|
+
success_response.description = value
|
60
|
+
when 'response_example'
|
61
|
+
success_response.add_example(value)
|
62
|
+
when 'response_header'
|
63
|
+
success_response.add_header(value)
|
44
64
|
end
|
45
65
|
end
|
46
66
|
|
47
|
-
build_path_parameters(routes)
|
48
|
-
|
49
67
|
@parameters.sort_by { |parameter| parameter.name }
|
50
68
|
|
51
69
|
@responses << success_response
|
@@ -55,20 +73,22 @@ module Swaggard
|
|
55
73
|
@path.present?
|
56
74
|
end
|
57
75
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
76
|
+
def empty?
|
77
|
+
@summary.blank? && @description.blank?
|
78
|
+
end
|
61
79
|
|
80
|
+
def to_doc
|
62
81
|
{
|
63
82
|
'tags' => [@tag.name],
|
83
|
+
'operationId' => @operation_id || @name,
|
64
84
|
'summary' => @summary,
|
65
85
|
'description' => @description,
|
66
|
-
'
|
67
|
-
|
68
|
-
'
|
69
|
-
'parameters'
|
70
|
-
'responses'
|
71
|
-
|
86
|
+
'produces' => @formats.map { |format| "application/#{format}" },
|
87
|
+
}.tap do |doc|
|
88
|
+
doc['consumes'] = @formats.map { |format| "application/#{format}" } if @body_parameter
|
89
|
+
doc['parameters'] = @parameters.map(&:to_doc)
|
90
|
+
doc['responses'] = Hash[@responses.map { |response| [response.status_code, response.to_doc] }]
|
91
|
+
end
|
72
92
|
end
|
73
93
|
|
74
94
|
def definitions
|
@@ -92,19 +112,11 @@ module Swaggard
|
|
92
112
|
@body_parameter
|
93
113
|
end
|
94
114
|
|
95
|
-
def build_path_parameters(
|
115
|
+
def build_path_parameters(path_params)
|
96
116
|
return unless @tag.controller_class
|
97
117
|
|
98
|
-
|
99
|
-
|
100
|
-
return unless route
|
101
|
-
|
102
|
-
@http_method = route[:verb]
|
103
|
-
@path = route[:path]
|
104
|
-
|
105
|
-
route[:path_params].each { |path_param| @parameters << Parameters::Path.new(path_param) }
|
118
|
+
path_params.each { |path_param| @parameters << Parameters::Path.new(self, path_param) }
|
106
119
|
end
|
107
|
-
|
108
120
|
end
|
109
121
|
end
|
110
122
|
end
|
@@ -3,19 +3,20 @@ module Swaggard
|
|
3
3
|
module Parameters
|
4
4
|
class Base
|
5
5
|
|
6
|
-
attr_reader :name
|
6
|
+
attr_reader :name, :description
|
7
|
+
attr_writer :is_required
|
7
8
|
|
8
9
|
def to_doc
|
9
10
|
{
|
10
|
-
'in' => @in,
|
11
11
|
'name' => @name,
|
12
|
-
'
|
12
|
+
'in' => @in,
|
13
13
|
'required' => @is_required,
|
14
|
-
'type' => @data_type
|
14
|
+
'type' => @data_type,
|
15
|
+
'description' => description
|
15
16
|
}
|
16
17
|
end
|
17
18
|
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
21
|
-
end
|
22
|
+
end
|
@@ -9,10 +9,12 @@ module Swaggard
|
|
9
9
|
attr_reader :definition
|
10
10
|
|
11
11
|
def initialize(operation_name)
|
12
|
-
@in
|
13
|
-
@name
|
14
|
-
@
|
15
|
-
@
|
12
|
+
@in = 'body'
|
13
|
+
@name = 'body'
|
14
|
+
@is_required = false
|
15
|
+
@description = ''
|
16
|
+
@definition = Definition.new("#{operation_name}_body")
|
17
|
+
@definition_id = @definition.id
|
16
18
|
end
|
17
19
|
|
18
20
|
def add_property(string)
|
@@ -20,17 +22,33 @@ module Swaggard
|
|
20
22
|
@definition.add_property(property)
|
21
23
|
end
|
22
24
|
|
25
|
+
def empty?
|
26
|
+
@definition.empty?
|
27
|
+
end
|
28
|
+
|
23
29
|
def to_doc
|
24
30
|
doc = super
|
25
31
|
|
26
32
|
doc.delete('type')
|
33
|
+
doc.delete('description')
|
27
34
|
|
28
|
-
doc['
|
29
|
-
doc['schema'] = { '$ref' => "#/definitions/#{@definition.id}" }
|
35
|
+
doc['schema'] = { '$ref' => "#/definitions/#{@definition_id}" }
|
30
36
|
|
31
37
|
doc
|
32
38
|
end
|
33
39
|
|
40
|
+
def description=(description)
|
41
|
+
@definition.description = description
|
42
|
+
end
|
43
|
+
|
44
|
+
def title=(title)
|
45
|
+
@definition.title = title
|
46
|
+
end
|
47
|
+
|
48
|
+
def definition=(definition)
|
49
|
+
@definition_id = definition
|
50
|
+
end
|
51
|
+
|
34
52
|
private
|
35
53
|
|
36
54
|
|
@@ -42,9 +60,14 @@ module Swaggard
|
|
42
60
|
parse(string)
|
43
61
|
end
|
44
62
|
|
63
|
+
def required?
|
64
|
+
@required
|
65
|
+
end
|
66
|
+
|
45
67
|
def to_doc
|
46
68
|
result = @type.to_doc
|
47
69
|
result['description'] = @description if @description
|
70
|
+
result['enum'] = @options if @options.present?
|
48
71
|
result
|
49
72
|
end
|
50
73
|
|
@@ -52,13 +75,16 @@ module Swaggard
|
|
52
75
|
# Example: [Array] status(required) Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
|
53
76
|
# Example: [Integer] media[media_type_id] ID of the desired media type.
|
54
77
|
def parse(string)
|
55
|
-
data_type, name,
|
56
|
-
|
57
|
-
|
78
|
+
data_type, required, name, options_and_description = string.match(/\A\[(\S*)\](!)?\s*([\w\[\]]*)\s*(.*)\Z/).captures
|
79
|
+
allow_multiple = name.gsub!('[]', '')
|
80
|
+
options, description = options_and_description.match(/\A(\[.*\])?(.*)\Z/).captures
|
81
|
+
options = options ? options.gsub(/\[?\]?\s?/, '').split(',') : []
|
58
82
|
|
59
83
|
@id = name
|
60
|
-
@description = description
|
84
|
+
@description = description if description.present?
|
61
85
|
@type = Type.new([data_type])
|
86
|
+
@required = required
|
87
|
+
@options = options
|
62
88
|
end
|
63
89
|
|
64
90
|
end
|
@@ -4,7 +4,6 @@ module Swaggard
|
|
4
4
|
module Swagger
|
5
5
|
module Parameters
|
6
6
|
class List < Base
|
7
|
-
|
8
7
|
def initialize(string)
|
9
8
|
@in = 'query'
|
10
9
|
parse(string)
|
@@ -39,8 +38,7 @@ module Swaggard
|
|
39
38
|
@data_type = data_type.downcase
|
40
39
|
@is_required = required.present?
|
41
40
|
end
|
42
|
-
|
43
41
|
end
|
44
42
|
end
|
45
43
|
end
|
46
|
-
end
|
44
|
+
end
|
@@ -5,17 +5,30 @@ module Swaggard
|
|
5
5
|
module Parameters
|
6
6
|
class Path < Base
|
7
7
|
|
8
|
-
attr_reader :name
|
8
|
+
attr_reader :operation, :name
|
9
9
|
|
10
|
-
def initialize(param_name)
|
10
|
+
def initialize(operation, param_name)
|
11
|
+
@operation = operation
|
11
12
|
@in = 'path'
|
12
13
|
@name = param_name.to_s
|
13
14
|
@data_type = 'string'
|
14
15
|
@is_required = true
|
15
|
-
@description = "Scope response to #{@name}"
|
16
16
|
end
|
17
17
|
|
18
|
+
def description
|
19
|
+
@description ||= get_description
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def get_description
|
25
|
+
if Swaggard.configuration.path_parameter_description.respond_to?(:call)
|
26
|
+
Swaggard.configuration.path_parameter_description.call(self)
|
27
|
+
else
|
28
|
+
Swaggard.configuration.path_parameter_description
|
29
|
+
end
|
30
|
+
end
|
18
31
|
end
|
19
32
|
end
|
20
33
|
end
|
21
|
-
end
|
34
|
+
end
|
@@ -11,11 +11,16 @@ module Swaggard
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def to_doc
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
doc
|
14
|
+
{
|
15
|
+
'name' => @name,
|
16
|
+
'in' => @in,
|
17
|
+
'required' => @is_required,
|
18
|
+
}.tap do |doc|
|
19
|
+
doc.merge!(@type.to_doc)
|
20
|
+
|
21
|
+
doc.merge!('enum' => @options) if @options.any?
|
22
|
+
doc.merge!('description' => description)
|
23
|
+
end
|
19
24
|
end
|
20
25
|
|
21
26
|
private
|
@@ -24,7 +29,7 @@ module Swaggard
|
|
24
29
|
# Example: [Array] status(required) Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
|
25
30
|
# Example: [Integer] media[media_type_id] ID of the desired media type.
|
26
31
|
def parse(string)
|
27
|
-
data_type,
|
32
|
+
data_type, required, name, options_and_description = string.match(/\A\[(\S*)\](!)?\s*([\w\[\]]*)\s*(.*)\Z/).captures
|
28
33
|
allow_multiple = name.gsub!('[]', '')
|
29
34
|
|
30
35
|
options, description = options_and_description.match(/\A(\[.*\])?(.*)\Z/).captures
|
@@ -32,7 +37,7 @@ module Swaggard
|
|
32
37
|
|
33
38
|
@name = name
|
34
39
|
@description = description
|
35
|
-
@
|
40
|
+
@type = Type.new([data_type])
|
36
41
|
@is_required = required.present?
|
37
42
|
@allow_multiple = allow_multiple.present?
|
38
43
|
@options = options
|
@@ -41,4 +46,4 @@ module Swaggard
|
|
41
46
|
end
|
42
47
|
end
|
43
48
|
end
|
44
|
-
end
|
49
|
+
end
|
@@ -3,7 +3,6 @@ require_relative 'operation'
|
|
3
3
|
module Swaggard
|
4
4
|
module Swagger
|
5
5
|
class Path
|
6
|
-
|
7
6
|
attr_reader :path
|
8
7
|
|
9
8
|
def initialize(path)
|
@@ -15,10 +14,13 @@ module Swaggard
|
|
15
14
|
@operations[operation.http_method.downcase] = operation
|
16
15
|
end
|
17
16
|
|
17
|
+
def ignore_put_if_patch!
|
18
|
+
@operations.delete('put') if @operations.key?('patch')
|
19
|
+
end
|
20
|
+
|
18
21
|
def to_doc
|
19
22
|
Hash[@operations.map { |http_method, operation| [http_method, operation.to_doc] }]
|
20
23
|
end
|
21
|
-
|
22
24
|
end
|
23
25
|
end
|
24
|
-
end
|
26
|
+
end
|
@@ -7,14 +7,29 @@ module Swaggard
|
|
7
7
|
attr_reader :id, :type, :description
|
8
8
|
|
9
9
|
def initialize(yard_object)
|
10
|
-
|
10
|
+
name = yard_object.name.dup
|
11
|
+
options_and_description = yard_object.text&.dup || ''
|
12
|
+
|
13
|
+
options, description = options_and_description.match(/\A(\[.*\])?(.*)\Z/).captures
|
14
|
+
options = options ? options.gsub(/\[?\]?\s?/, '').split(',') : []
|
15
|
+
description = description.strip
|
16
|
+
required = name.gsub!(/^!/, '')
|
17
|
+
|
18
|
+
@id = name
|
11
19
|
@type = Type.new(yard_object.types)
|
12
|
-
@description =
|
20
|
+
@description = description
|
21
|
+
@required = required.present?
|
22
|
+
@options = options
|
23
|
+
end
|
24
|
+
|
25
|
+
def required?
|
26
|
+
@required
|
13
27
|
end
|
14
28
|
|
15
29
|
def to_doc
|
16
30
|
result = @type.to_doc
|
17
|
-
result['description'] = @description if @description
|
31
|
+
result['description'] = @description if @description.present?
|
32
|
+
result['enum'] = @options if @options.present?
|
18
33
|
result
|
19
34
|
end
|
20
35
|
|
@@ -1,9 +1,9 @@
|
|
1
|
+
require_relative 'response_header'
|
2
|
+
|
1
3
|
module Swaggard
|
2
4
|
module Swagger
|
3
5
|
class Response
|
4
6
|
|
5
|
-
DEFAULT_STATUS_CODE = 'default'
|
6
|
-
DEFAULT_DESCRIPTION = 'successful operation'
|
7
7
|
PRIMITIVE_TYPES = %w[integer long float double string byte binary boolean date date-time password hash]
|
8
8
|
|
9
9
|
attr_writer :status_code, :description
|
@@ -11,6 +11,7 @@ module Swaggard
|
|
11
11
|
def initialize(operation_name)
|
12
12
|
@operation_name = operation_name
|
13
13
|
@response_model = ResponseModel.new
|
14
|
+
@headers = []
|
14
15
|
end
|
15
16
|
|
16
17
|
def definition
|
@@ -22,7 +23,7 @@ module Swaggard
|
|
22
23
|
end
|
23
24
|
|
24
25
|
def status_code
|
25
|
-
@status_code ||
|
26
|
+
@status_code || Swaggard.configuration.default_response_status_code
|
26
27
|
end
|
27
28
|
|
28
29
|
def response_class=(value)
|
@@ -34,8 +35,16 @@ module Swaggard
|
|
34
35
|
@response_model.id = root
|
35
36
|
end
|
36
37
|
|
38
|
+
def add_example(value)
|
39
|
+
@example = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_header(value)
|
43
|
+
@headers << ResponseHeader.new(value)
|
44
|
+
end
|
45
|
+
|
37
46
|
def description
|
38
|
-
@description ||
|
47
|
+
@description || Swaggard.configuration.default_response_description
|
39
48
|
end
|
40
49
|
|
41
50
|
def to_doc
|
@@ -47,6 +56,18 @@ module Swaggard
|
|
47
56
|
end
|
48
57
|
|
49
58
|
doc.merge!('schema' => schema) if schema
|
59
|
+
doc.merge!('examples' => example_to_doc) if @example
|
60
|
+
if @headers.any?
|
61
|
+
doc['headers'] = Hash[@headers.map { |header| [header.name, header.to_doc] }]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def example_to_doc
|
67
|
+
Swaggard.configuration.api_formats.inject({}) do |examples, format|
|
68
|
+
examples.merge!("application/#{format}" => { '$ref' => @example })
|
69
|
+
|
70
|
+
examples
|
50
71
|
end
|
51
72
|
end
|
52
73
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Swaggard
|
2
|
+
module Swagger
|
3
|
+
class ResponseHeader
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def initialize(string)
|
7
|
+
parse(string)
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_doc
|
11
|
+
{
|
12
|
+
'description' => @description,
|
13
|
+
'type' => @type,
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Example: [Array] status Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
|
20
|
+
# Example: [Array]! status Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
|
21
|
+
# Example: [Integer] media[media_type_id] ID of the desired media type.
|
22
|
+
def parse(string)
|
23
|
+
data_type, name, options_and_description = string.match(/\A\[(\S*)\]\s*([\w\-\[\]]*)\s*(.*)\Z/).captures
|
24
|
+
allow_multiple = name.gsub!('[]', '')
|
25
|
+
|
26
|
+
options, description = options_and_description.match(/\A(\[.*\])?(.*)\Z/).captures
|
27
|
+
options = options ? options.gsub(/\[?\]?\s?/, '').split(',') : []
|
28
|
+
|
29
|
+
@name = name
|
30
|
+
@description = description
|
31
|
+
@type = data_type
|
32
|
+
@allow_multiple = allow_multiple.present?
|
33
|
+
@options = options
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module Swaggard
|
2
2
|
module Swagger
|
3
3
|
class Type
|
4
|
-
|
5
4
|
BASIC_TYPES = {
|
6
5
|
'integer' => { 'type' => 'integer', 'format' => 'int32' },
|
7
6
|
'long' => { 'type' => 'integer', 'format' => 'int64' },
|
@@ -31,10 +30,6 @@ module Swaggard
|
|
31
30
|
end
|
32
31
|
end
|
33
32
|
|
34
|
-
def basic_type?
|
35
|
-
BASIC_TYPES.has_key?(@name.downcase)
|
36
|
-
end
|
37
|
-
|
38
33
|
private
|
39
34
|
|
40
35
|
def parse(types)
|
@@ -44,14 +39,24 @@ module Swaggard
|
|
44
39
|
@is_array = parts.grep(/array/i).any?
|
45
40
|
end
|
46
41
|
|
42
|
+
|
43
|
+
def basic_type?
|
44
|
+
BASIC_TYPES.has_key?(@name.downcase)
|
45
|
+
end
|
46
|
+
|
47
|
+
def custom_type?
|
48
|
+
Swaggard.configuration.custom_types.has_key?(@name)
|
49
|
+
end
|
50
|
+
|
47
51
|
def type_tag_and_name
|
48
52
|
if basic_type?
|
49
|
-
BASIC_TYPES[@name.downcase]
|
53
|
+
BASIC_TYPES[@name.downcase].dup
|
54
|
+
elsif custom_type?
|
55
|
+
Swaggard.configuration.custom_types[@name].dup
|
50
56
|
else
|
51
57
|
{ '$ref' => "#/definitions/#{name}" }
|
52
58
|
end
|
53
59
|
end
|
54
|
-
|
55
60
|
end
|
56
61
|
end
|
57
|
-
end
|
62
|
+
end
|
data/lib/swaggard/version.rb
CHANGED
data/lib/swaggard.rb
CHANGED
@@ -5,7 +5,7 @@ require 'swaggard/api_definition'
|
|
5
5
|
require 'swaggard/configuration'
|
6
6
|
require 'swaggard/engine'
|
7
7
|
|
8
|
-
require 'swaggard/parsers/
|
8
|
+
require 'swaggard/parsers/controller'
|
9
9
|
require 'swaggard/parsers/models'
|
10
10
|
require 'swaggard/parsers/routes'
|
11
11
|
|
@@ -22,22 +22,30 @@ module Swaggard
|
|
22
22
|
|
23
23
|
# Register some custom yard tags
|
24
24
|
def register_custom_yard_tags!
|
25
|
-
::YARD::Tags::Library.define_tag('Controller\'s tag',
|
25
|
+
::YARD::Tags::Library.define_tag('Controller\'s tag', :tag)
|
26
|
+
::YARD::Tags::Library.define_tag('Operation id', :operation_id)
|
26
27
|
::YARD::Tags::Library.define_tag('Query parameter', :query_parameter)
|
27
|
-
::YARD::Tags::Library.define_tag('Form parameter',
|
28
|
-
::YARD::Tags::Library.define_tag('Body
|
29
|
-
::YARD::Tags::Library.define_tag('
|
30
|
-
::YARD::Tags::Library.define_tag('
|
31
|
-
::YARD::Tags::Library.define_tag('
|
32
|
-
::YARD::Tags::Library.define_tag('
|
28
|
+
::YARD::Tags::Library.define_tag('Form parameter', :form_parameter)
|
29
|
+
::YARD::Tags::Library.define_tag('Body required', :body_required)
|
30
|
+
::YARD::Tags::Library.define_tag('Body description', :body_description)
|
31
|
+
::YARD::Tags::Library.define_tag('Body title', :body_title)
|
32
|
+
::YARD::Tags::Library.define_tag('Body definition', :body_definition)
|
33
|
+
::YARD::Tags::Library.define_tag('Body parameter', :body_parameter)
|
34
|
+
::YARD::Tags::Library.define_tag('Parameter list', :parameter_list)
|
35
|
+
::YARD::Tags::Library.define_tag('Response class', :response_class)
|
36
|
+
::YARD::Tags::Library.define_tag('Response root', :response_root)
|
37
|
+
::YARD::Tags::Library.define_tag('Response status', :response_status)
|
38
|
+
::YARD::Tags::Library.define_tag('Response description', :response_description)
|
39
|
+
::YARD::Tags::Library.define_tag('Response example', :response_example)
|
40
|
+
::YARD::Tags::Library.define_tag('Response header', :response_header)
|
33
41
|
end
|
34
42
|
|
35
|
-
def get_doc(host)
|
43
|
+
def get_doc(host = nil)
|
36
44
|
load!
|
37
45
|
|
38
46
|
doc = @api.to_doc
|
39
47
|
|
40
|
-
doc['host'] = host if doc['host'].blank?
|
48
|
+
doc['host'] = host if doc['host'].blank? && host
|
41
49
|
|
42
50
|
doc
|
43
51
|
end
|
@@ -48,22 +56,67 @@ module Swaggard
|
|
48
56
|
@api = Swaggard::ApiDefinition.new
|
49
57
|
|
50
58
|
parse_models
|
51
|
-
|
59
|
+
build_operations
|
60
|
+
|
61
|
+
@api.ignore_put_if_patch! if Swaggard.configuration.ignore_put_if_patch_exists
|
52
62
|
end
|
53
63
|
|
54
|
-
def
|
55
|
-
|
64
|
+
def build_operation(path, verb, route)
|
65
|
+
controller_name = route[:controller]
|
66
|
+
action_name = route[:action]
|
67
|
+
|
68
|
+
return unless controllers[controller_name]
|
69
|
+
|
70
|
+
controller_tag = controllers[controller_name][:tag]
|
71
|
+
|
72
|
+
return unless controller_tag
|
73
|
+
|
74
|
+
return unless controllers[controller_name][:operations]
|
75
|
+
|
76
|
+
return unless controllers[controller_name][:operations][action_name]
|
56
77
|
|
78
|
+
operation_yard_object = controllers[controller_name][:operations][action_name]
|
79
|
+
|
80
|
+
return unless operation_yard_object
|
81
|
+
|
82
|
+
operation = Swagger::Operation.new(operation_yard_object, controller_tag, path, verb, route[:path_params])
|
83
|
+
|
84
|
+
return unless operation.valid?
|
85
|
+
return if Swaggard.configuration.ignore_undocumented_paths && operation.empty?
|
86
|
+
|
87
|
+
return controller_tag, operation
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_operations
|
91
|
+
routes.each do |path, verbs|
|
92
|
+
verbs.each do |verb, route_options|
|
93
|
+
tag, operation = build_operation(path, verb, route_options)
|
94
|
+
|
95
|
+
next unless tag
|
96
|
+
|
97
|
+
@api.add_tag(tag)
|
98
|
+
@api.add_operation(operation)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def controllers
|
104
|
+
return @controllers if @controllers
|
105
|
+
|
106
|
+
parser = Parsers::Controller.new
|
107
|
+
|
108
|
+
@controllers = {}
|
57
109
|
Dir[configuration.controllers_path].each do |file|
|
58
110
|
yard_objects = get_yard_objects(file)
|
59
111
|
|
60
|
-
tag, operations = parser.run(yard_objects
|
112
|
+
tag, operations = parser.run(yard_objects)
|
61
113
|
|
62
114
|
next unless tag
|
63
115
|
|
64
|
-
@
|
65
|
-
operations.each { |operation| @api.add_operation(operation) }
|
116
|
+
@controllers[tag.controller_class.controller_path] ||= { tag: tag, operations: operations }
|
66
117
|
end
|
118
|
+
|
119
|
+
@controllers
|
67
120
|
end
|
68
121
|
|
69
122
|
def routes
|
data/lib/tasks/swaggard.rake
CHANGED
@@ -6,4 +6,10 @@ namespace :swaggard do
|
|
6
6
|
puts 'Swaggard cache has been cleared'
|
7
7
|
end
|
8
8
|
|
9
|
-
|
9
|
+
desc 'Dump swagger.json'
|
10
|
+
task :dump => :environment do
|
11
|
+
swagger = Swaggard.get_doc
|
12
|
+
File.open('public/swagger/swaggard.json', 'w') { |file| file.write(JSON.pretty_generate(swagger)) }
|
13
|
+
puts 'Swaggard dump complete'
|
14
|
+
end
|
15
|
+
end
|
data/spec/fixtures/api.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"swagger":"2.0","info":{"
|
1
|
+
{"swagger":"2.0","info":{"version":"0.1","title":"","description":"","termsOfService":"","contact":{"name":""},"license":{"name":""}},"host":"localhost:3000","basePath":"/","schemes":["https","http"],"consumes":["application/xml","application/json"],"produces":["application/xml","application/json"],"tags":[{"name":"admin/pets","description":"This document describes the API for interacting with Pet resources"},{"name":"pets","description":"This document describes the API for interacting with Pet resources"}],"paths":{"/admin/pets":{"get":{"tags":["admin/pets"],"operationId":"index","summary":"return a list of Pets","description":"","produces":["application/xml","application/json"],"parameters":[],"responses":{"default":{"description":"successful operation"}}}},"/pets":{"get":{"tags":["pets"],"operationId":"index","summary":"return a list of Pets","description":"","produces":["application/xml","application/json"],"parameters":[],"responses":{"default":{"description":"successful operation"}}}},"/pets/{id}":{"get":{"tags":["pets"],"operationId":"show","summary":"return a Pet","description":"","produces":["application/xml","application/json"],"parameters":[{"name":"id","in":"path","required":true,"type":"string","description":"Scope response to id"},{"name":"id","in":"query","required":false,"type":"integer","format":"int32","description":"The ID for the Pet"}],"responses":{"default":{"description":"successful operation"}}}}},"definitions":{}}
|
@@ -3,7 +3,6 @@ require 'spec_helper'
|
|
3
3
|
require 'json-schema'
|
4
4
|
|
5
5
|
describe Swaggard, '.get_doc' do
|
6
|
-
|
7
6
|
let(:controller_path) { File.expand_path('../../fixtures/dummy/app/controllers/**/*.rb', __FILE__) }
|
8
7
|
let(:api_json) { File.read(File.expand_path('../../fixtures/api.json', __FILE__)) }
|
9
8
|
|
@@ -19,5 +18,4 @@ describe Swaggard, '.get_doc' do
|
|
19
18
|
|
20
19
|
expect(swagger_json).to eq(api_json)
|
21
20
|
end
|
22
|
-
|
23
21
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: swaggard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adrian Gomez
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -166,7 +166,7 @@ files:
|
|
166
166
|
- lib/swaggard/api_definition.rb
|
167
167
|
- lib/swaggard/configuration.rb
|
168
168
|
- lib/swaggard/engine.rb
|
169
|
-
- lib/swaggard/parsers/
|
169
|
+
- lib/swaggard/parsers/controller.rb
|
170
170
|
- lib/swaggard/parsers/models.rb
|
171
171
|
- lib/swaggard/parsers/routes.rb
|
172
172
|
- lib/swaggard/swagger/definition.rb
|
@@ -180,6 +180,7 @@ files:
|
|
180
180
|
- lib/swaggard/swagger/path.rb
|
181
181
|
- lib/swaggard/swagger/property.rb
|
182
182
|
- lib/swaggard/swagger/response.rb
|
183
|
+
- lib/swaggard/swagger/response_header.rb
|
183
184
|
- lib/swaggard/swagger/tag.rb
|
184
185
|
- lib/swaggard/swagger/type.rb
|
185
186
|
- lib/swaggard/version.rb
|
@@ -214,20 +215,19 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
214
215
|
- !ruby/object:Gem::Version
|
215
216
|
version: '0'
|
216
217
|
requirements: []
|
217
|
-
|
218
|
-
rubygems_version: 2.5.1
|
218
|
+
rubygems_version: 3.0.3
|
219
219
|
signing_key:
|
220
220
|
specification_version: 4
|
221
221
|
summary: 'Swaggard: Swagger Rails REST API doc using yard YARD'
|
222
222
|
test_files:
|
223
|
-
- spec/
|
224
|
-
- spec/
|
225
|
-
- spec/fixtures/
|
223
|
+
- spec/spec_helper.rb
|
224
|
+
- spec/integration/swaggard_spec.rb
|
225
|
+
- spec/fixtures/swagger_schema.json
|
226
226
|
- spec/fixtures/dummy/app/controllers/pets_controller.rb
|
227
|
-
- spec/fixtures/dummy/
|
228
|
-
- spec/fixtures/dummy/
|
227
|
+
- spec/fixtures/dummy/app/controllers/application_controller.rb
|
228
|
+
- spec/fixtures/dummy/app/controllers/admin/pets_controller.rb
|
229
229
|
- spec/fixtures/dummy/config/routes.rb
|
230
|
+
- spec/fixtures/dummy/config/environments/development.rb
|
231
|
+
- spec/fixtures/dummy/config/application.rb
|
230
232
|
- spec/fixtures/dummy/log/development.log
|
231
|
-
- spec/fixtures/
|
232
|
-
- spec/integration/swaggard_spec.rb
|
233
|
-
- spec/spec_helper.rb
|
233
|
+
- spec/fixtures/api.json
|