swaggard 0.5.4 → 1.0.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 +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
|