swaggard 2.0.0 → 4.0.1

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -132
  3. data/app/views/swaggard/swagger/index.html.erb +51 -53
  4. data/lib/swaggard/api_definition.rb +42 -28
  5. data/lib/swaggard/configuration.rb +1 -5
  6. data/lib/swaggard/engine.rb +0 -32
  7. data/lib/swaggard/swagger/operation.rb +32 -12
  8. data/lib/swaggard/swagger/parameters/base.rb +7 -6
  9. data/lib/swaggard/swagger/parameters/body.rb +14 -11
  10. data/lib/swaggard/swagger/parameters/form.rb +10 -4
  11. data/lib/swaggard/swagger/parameters/list.rb +5 -8
  12. data/lib/swaggard/swagger/parameters/query.rb +15 -12
  13. data/lib/swaggard/swagger/response.rb +15 -8
  14. data/lib/swaggard/swagger/response_header.rb +1 -3
  15. data/lib/swaggard/swagger/type.rb +6 -2
  16. data/lib/swaggard/version.rb +1 -1
  17. data/lib/swaggard.rb +9 -1
  18. data/spec/fixtures/api.json +111 -1
  19. data/spec/fixtures/dummy/app/controllers/pets_controller.rb +1 -0
  20. data/spec/fixtures/dummy/config/application.rb +0 -1
  21. data/spec/fixtures/openapi_3_1_schema.json +1440 -0
  22. data/spec/integration/openapi_spec.rb +28 -0
  23. data/spec/integration/swaggard_spec.rb +1 -1
  24. metadata +8 -43
  25. data/app/assets/fonts/swaggard/DroidSans-Bold.ttf +0 -0
  26. data/app/assets/fonts/swaggard/DroidSans.ttf +0 -0
  27. data/app/assets/javascripts/swaggard/application.js +0 -28
  28. data/app/assets/javascripts/swaggard/lang/ca.js +0 -53
  29. data/app/assets/javascripts/swaggard/lang/el.js +0 -56
  30. data/app/assets/javascripts/swaggard/lang/en.js +0 -56
  31. data/app/assets/javascripts/swaggard/lang/es.js +0 -53
  32. data/app/assets/javascripts/swaggard/lang/fr.js +0 -54
  33. data/app/assets/javascripts/swaggard/lang/geo.js +0 -56
  34. data/app/assets/javascripts/swaggard/lang/it.js +0 -52
  35. data/app/assets/javascripts/swaggard/lang/ja.js +0 -56
  36. data/app/assets/javascripts/swaggard/lang/ko-kr.js +0 -53
  37. data/app/assets/javascripts/swaggard/lang/pl.js +0 -53
  38. data/app/assets/javascripts/swaggard/lang/pt.js +0 -53
  39. data/app/assets/javascripts/swaggard/lang/ru.js +0 -56
  40. data/app/assets/javascripts/swaggard/lang/tr.js +0 -53
  41. data/app/assets/javascripts/swaggard/lang/zh-cn.js +0 -56
  42. data/app/assets/javascripts/swaggard/lib/backbone-min.js +0 -1
  43. data/app/assets/javascripts/swaggard/lib/handlebars-4.0.5.js +0 -3
  44. data/app/assets/javascripts/swaggard/lib/highlight.9.1.0.pack.js +0 -1
  45. data/app/assets/javascripts/swaggard/lib/highlight.9.1.0.pack_extended.js +0 -1
  46. data/app/assets/javascripts/swaggard/lib/jquery-1.8.0.min.js +0 -3
  47. data/app/assets/javascripts/swaggard/lib/jquery.ba-bbq.min.js +0 -1
  48. data/app/assets/javascripts/swaggard/lib/jquery.slideto.min.js +0 -1
  49. data/app/assets/javascripts/swaggard/lib/jquery.wiggle.min.js +0 -1
  50. data/app/assets/javascripts/swaggard/lib/jsoneditor.min.js +0 -5
  51. data/app/assets/javascripts/swaggard/lib/lodash.min.js +0 -2
  52. data/app/assets/javascripts/swaggard/lib/marked.js +0 -1
  53. data/app/assets/javascripts/swaggard/lib/object-assign-pollyfill.js +0 -1
  54. data/app/assets/javascripts/swaggard/lib/swagger-oauth.js +0 -1
  55. data/app/assets/javascripts/swaggard/swaggard.js +0 -72
  56. data/app/assets/javascripts/swaggard/swagger-ui.js +0 -25378
  57. data/app/assets/javascripts/swaggard/translator.js +0 -39
  58. data/app/assets/stylesheets/swaggard/application.css +0 -16
  59. data/app/assets/stylesheets/swaggard/application_print.css +0 -15
  60. data/app/assets/stylesheets/swaggard/print.css.scss +0 -4
  61. data/app/assets/stylesheets/swaggard/reset.css +0 -1
  62. data/app/assets/stylesheets/swaggard/screen.css.scss +0 -4
  63. data/app/assets/stylesheets/swaggard/typography.css.scss +0 -0
@@ -14,14 +14,13 @@ module Swaggard
14
14
  :error_responses, :tag
15
15
 
16
16
  def initialize(yard_object, tag, path, verb, path_params)
17
- @name = yard_object.name
17
+ @name = yard_object.name.to_s
18
18
  @tag = tag
19
19
  @summary = (yard_object.docstring.lines.first || '').chomp
20
20
  @parameters = []
21
21
  @responses = []
22
22
 
23
23
  @description = (yard_object.docstring.lines[1..-1] || []).map(&:chomp).reject(&:empty?).compact.join("\n")
24
- @formats = Swaggard.configuration.api_formats
25
24
  @http_method = verb
26
25
  @path = path
27
26
 
@@ -78,17 +77,26 @@ module Swaggard
78
77
  end
79
78
 
80
79
  def to_doc
81
- {
82
- 'tags' => [@tag.name],
83
- 'operationId' => @operation_id || @name,
84
- 'summary' => @summary,
85
- 'description' => @description,
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] }]
80
+ regular_params = @parameters.reject { |p| p.is_a?(Parameters::Body) || p.is_a?(Parameters::Form) }
81
+ body_param = @parameters.find { |p| p.is_a?(Parameters::Body) }
82
+ form_params = @parameters.select { |p| p.is_a?(Parameters::Form) }
83
+
84
+ doc = {
85
+ 'tags' => [@tag.name],
86
+ 'operationId' => @operation_id || @name,
87
+ 'summary' => @summary,
88
+ 'description' => @description,
89
+ 'parameters' => regular_params.map(&:to_doc),
90
+ 'responses' => Hash[@responses.map { |response| [response.status_code, response.to_doc] }]
91
+ }
92
+
93
+ if body_param && !body_param.empty?
94
+ doc['requestBody'] = body_param.to_request_body
95
+ elsif form_params.any?
96
+ doc['requestBody'] = build_form_request_body(form_params)
91
97
  end
98
+
99
+ doc
92
100
  end
93
101
 
94
102
  def definitions
@@ -117,6 +125,18 @@ module Swaggard
117
125
  @body_parameter
118
126
  end
119
127
 
128
+ def build_form_request_body(form_params)
129
+ properties = form_params.each_with_object({}) do |param, props|
130
+ props[param.name] = param.form_property_doc
131
+ end
132
+ required = form_params.select(&:is_required?).map(&:name)
133
+
134
+ schema = { 'type' => 'object', 'properties' => properties }
135
+ schema['required'] = required if required.any?
136
+
137
+ { 'content' => { 'application/x-www-form-urlencoded' => { 'schema' => schema } } }
138
+ end
139
+
120
140
  def build_path_parameters(path_params)
121
141
  return unless @tag.controller_class
122
142
 
@@ -7,13 +7,14 @@ module Swaggard
7
7
  attr_writer :is_required
8
8
 
9
9
  def to_doc
10
- {
11
- 'name' => @name,
12
- 'in' => @in,
13
- 'required' => @is_required,
14
- 'type' => @data_type,
15
- 'description' => description
10
+ doc = {
11
+ 'name' => @name,
12
+ 'in' => @in,
13
+ 'required' => @is_required,
14
+ 'description' => description
16
15
  }
16
+ doc['schema'] = { 'type' => @data_type } if @data_type
17
+ doc
17
18
  end
18
19
 
19
20
  end
@@ -25,15 +25,15 @@ module Swaggard
25
25
  @definition.empty?
26
26
  end
27
27
 
28
- def to_doc
29
- doc = super
30
-
31
- doc.delete('type')
32
- doc.delete('description')
33
-
34
- doc['schema'] = { '$ref' => "#/definitions/#{@definition_id}" }
35
-
36
- doc
28
+ def to_request_body
29
+ {
30
+ 'required' => @is_required,
31
+ 'content' => {
32
+ 'application/json' => {
33
+ 'schema' => { '$ref' => "#/components/schemas/#{Swaggard.ref_name(@definition_id)}" }
34
+ }
35
+ }
36
+ }
37
37
  end
38
38
 
39
39
  def description=(description)
@@ -64,7 +64,11 @@ module Swaggard
64
64
  def to_doc
65
65
  result = @type.to_doc
66
66
  result['description'] = @description if @description
67
- result['enum'] = @options if @options.present?
67
+ if @options.present? && @type.is_array?
68
+ result['items']['enum'] = @options
69
+ elsif @options.present?
70
+ result['enum'] = @options
71
+ end
68
72
  result
69
73
  end
70
74
 
@@ -74,7 +78,6 @@ module Swaggard
74
78
  def parse(string)
75
79
  string.gsub!("\n", ' ')
76
80
  data_type, required, name, options_and_description = string.match(/\A\[(\S*)\](!)?\s*([\w\[\]]*)\s*(.*)\Z/).captures
77
- allow_multiple = name.gsub!('[]', '')
78
81
  options, description = options_and_description.match(/\A(\[.*\])?(.*)\Z/).captures
79
82
  options = options ? options.gsub(/\[?\]?\s?/, '').split(',') : []
80
83
 
@@ -6,10 +6,19 @@ module Swaggard
6
6
  class Form < Base
7
7
 
8
8
  def initialize(string)
9
- @in = 'formData'
10
9
  parse(string)
11
10
  end
12
11
 
12
+ def is_required?
13
+ @is_required
14
+ end
15
+
16
+ def form_property_doc
17
+ doc = { 'type' => @data_type }
18
+ doc['description'] = @description if @description.present?
19
+ doc
20
+ end
21
+
13
22
  private
14
23
 
15
24
  # Example: [Array] status Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
@@ -17,15 +26,12 @@ module Swaggard
17
26
  # Example: [Integer] media[media_type_id] ID of the desired media type.
18
27
  def parse(string)
19
28
  data_type, name, required, description = string.match(/\A\[(\w*)\]\s*([\w\[\]]*)(\(required\))?\s*(.*)\Z/).captures
20
- allow_multiple = name.gsub!('[]', '')
21
29
 
22
30
  @name = name
23
31
  @description = description
24
32
  @data_type = data_type.downcase
25
33
  @is_required = required.present?
26
- @allow_multiple = allow_multiple.present?
27
34
  end
28
-
29
35
  end
30
36
  end
31
37
  end
@@ -11,14 +11,11 @@ module Swaggard
11
11
 
12
12
  def to_doc
13
13
  doc = super
14
-
15
- doc.merge(
16
- {
17
- 'type' => 'array',
18
- 'items' => { 'type' => @data_type },
19
- 'enum' => @list_values
20
- }
21
- )
14
+ doc['schema'] = {
15
+ 'type' => 'array',
16
+ 'items' => { 'type' => @data_type, 'enum' => @list_values },
17
+ }
18
+ doc
22
19
  end
23
20
 
24
21
  private
@@ -11,26 +11,30 @@ module Swaggard
11
11
  end
12
12
 
13
13
  def to_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)
14
+ schema = @type.to_doc
15
+
16
+ if @options.present? && @type.is_array?
17
+ schema['items']['enum'] = @options
18
+ elsif @options.present?
19
+ schema['enum'] = @options
23
20
  end
21
+
22
+ {
23
+ 'name' => @name,
24
+ 'in' => @in,
25
+ 'required' => @is_required,
26
+ 'description' => description,
27
+ 'schema' => schema
28
+ }
24
29
  end
25
30
 
26
31
  private
27
32
 
28
33
  # Example: [Array] status Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
29
- # Example: [Array] status(required) Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
34
+ # Example: [Array] status! Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
30
35
  # Example: [Integer] media[media_type_id] ID of the desired media type.
31
36
  def parse(string)
32
37
  data_type, required, name, options_and_description = string.match(/\A\[(\S*)\](!)?\s*([\w\[\]]*)\s*(.*)\Z/).captures
33
- allow_multiple = name.gsub!('[]', '')
34
38
 
35
39
  options, description = options_and_description.match(/\A(\[.*\])?(.*)\Z/).captures
36
40
  options = options ? options.gsub(/\[?\]?\s?/, '').split(',') : []
@@ -39,7 +43,6 @@ module Swaggard
39
43
  @description = description
40
44
  @type = Parsers::Type.run(data_type)
41
45
  @is_required = required.present?
42
- @allow_multiple = allow_multiple.present?
43
46
  @options = options
44
47
  end
45
48
  end
@@ -50,13 +50,20 @@ module Swaggard
50
50
  def to_doc
51
51
  { 'description' => description }.tap do |doc|
52
52
  schema = if @response_root.present?
53
- { '$ref' => "#/definitions/#{definition.id}" }
53
+ { '$ref' => "#/components/schemas/#{Swaggard.ref_name(definition.id)}" }
54
54
  elsif @response_model.response_class.present?
55
55
  @response_model.to_doc
56
56
  end
57
57
 
58
- doc.merge!('schema' => schema) if schema
59
- doc.merge!('examples' => example_to_doc) if @example
58
+ if schema || @example
59
+ media_type_object = {}
60
+ media_type_object['schema'] = schema if schema
61
+ media_type_object['example'] = example_to_doc if @example
62
+ doc['content'] = Swaggard.configuration.api_formats.each_with_object({}) do |format, content|
63
+ content["application/#{format}"] = media_type_object
64
+ end
65
+ end
66
+
60
67
  if @headers.any?
61
68
  doc['headers'] = Hash[@headers.map { |header| [header.name, header.to_doc] }]
62
69
  end
@@ -64,10 +71,10 @@ module Swaggard
64
71
  end
65
72
 
66
73
  def example_to_doc
67
- Swaggard.configuration.api_formats.inject({}) do |examples, format|
68
- examples.merge!("application/#{format}" => { '$ref' => @example })
69
-
70
- examples
74
+ if File.exist?(@example)
75
+ JSON.parse(File.read(@example))
76
+ else
77
+ { '$ref' => @example }
71
78
  end
72
79
  end
73
80
 
@@ -100,7 +107,7 @@ module Swaggard
100
107
  if PRIMITIVE_TYPES.include?(@response_class)
101
108
  { 'type' => @response_class }
102
109
  else
103
- { '$ref' => "#/definitions/#@response_class" }
110
+ { '$ref' => "#/components/schemas/#{Swaggard.ref_name(@response_class)}" }
104
111
  end
105
112
  end
106
113
  end
@@ -10,7 +10,7 @@ module Swaggard
10
10
  def to_doc
11
11
  {
12
12
  'description' => @description,
13
- 'type' => @type,
13
+ 'schema' => { 'type' => @type },
14
14
  }
15
15
  end
16
16
 
@@ -21,7 +21,6 @@ module Swaggard
21
21
  # Example: [Integer] media[media_type_id] ID of the desired media type.
22
22
  def parse(string)
23
23
  data_type, name, options_and_description = string.match(/\A\[(\S*)\]\s*([\w\-\[\]]*)\s*(.*)\Z/).captures
24
- allow_multiple = name.gsub!('[]', '')
25
24
 
26
25
  options, description = options_and_description.match(/\A(\[.*\])?(.*)\Z/).captures
27
26
  options = options ? options.gsub(/\[?\]?\s?/, '').split(',') : []
@@ -29,7 +28,6 @@ module Swaggard
29
28
  @name = name
30
29
  @description = description
31
30
  @type = data_type
32
- @allow_multiple = allow_multiple.present?
33
31
  @options = options
34
32
  end
35
33
  end
@@ -24,6 +24,10 @@ module Swaggard
24
24
  @is_array = is_array
25
25
  end
26
26
 
27
+ def is_array?
28
+ @is_array
29
+ end
30
+
27
31
  def to_doc
28
32
  if @is_array
29
33
  { 'type' => 'array', 'items' => type_tag_and_name }
@@ -46,9 +50,9 @@ module Swaggard
46
50
  if basic_type?
47
51
  BASIC_TYPES[@name.downcase].dup
48
52
  elsif custom_type?
49
- Swaggard.configuration.custom_types[@name].dup
53
+ { '$ref' => "#/components/schemas/#{Swaggard.ref_name(@name)}" }
50
54
  else
51
- { '$ref' => "#/definitions/#{name}" }
55
+ { '$ref' => "#/components/schemas/#{Swaggard.ref_name(name)}" }
52
56
  end
53
57
  end
54
58
  end
@@ -1,3 +1,3 @@
1
1
  module Swaggard
2
- VERSION = '2.0.0'
2
+ VERSION = '4.0.1'
3
3
  end
data/lib/swaggard.rb CHANGED
@@ -41,12 +41,20 @@ module Swaggard
41
41
  ::YARD::Tags::Library.define_tag('Ignore inherited attributes', :ignore_inherited)
42
42
  end
43
43
 
44
+ # Sanitize a name for use as an OpenAPI component key and $ref fragment.
45
+ # Keys must match ^[a-zA-Z0-9\.\-_]+$ per the OAS 3.1 spec.
46
+ def ref_name(name)
47
+ name.to_s.gsub('::', '.').gsub('#', '_')
48
+ end
49
+
44
50
  def get_doc(host = nil)
45
51
  load!
46
52
 
47
53
  doc = @api.to_doc
48
54
 
49
- doc['host'] = host if doc['host'].blank? && host
55
+ if host && configuration.host.blank?
56
+ doc['servers'] = [{ 'url' => "#{configuration.schemes.first}://#{host}#{configuration.api_base_path}" }]
57
+ end
50
58
 
51
59
  doc
52
60
  end
@@ -1 +1,111 @@
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":{}}
1
+ {
2
+ "openapi": "3.1.0",
3
+ "info": {
4
+ "version": "0.1",
5
+ "title": "",
6
+ "description": "",
7
+ "contact": {
8
+ "name": ""
9
+ },
10
+ "license": {
11
+ "name": ""
12
+ }
13
+ },
14
+ "servers": [
15
+ {
16
+ "url": "https://localhost:3000/"
17
+ }
18
+ ],
19
+ "tags": [
20
+ {
21
+ "name": "admin/pets",
22
+ "description": "This document describes the API for interacting with Pet resources"
23
+ },
24
+ {
25
+ "name": "pets",
26
+ "description": "This document describes the API for interacting with Pet resources"
27
+ }
28
+ ],
29
+ "paths": {
30
+ "/admin/pets": {
31
+ "get": {
32
+ "tags": [
33
+ "admin/pets"
34
+ ],
35
+ "operationId": "index",
36
+ "summary": "return a list of Pets",
37
+ "description": "",
38
+ "parameters": [],
39
+ "responses": {
40
+ "default": {
41
+ "description": "successful operation"
42
+ }
43
+ }
44
+ }
45
+ },
46
+ "/pets": {
47
+ "get": {
48
+ "tags": [
49
+ "pets"
50
+ ],
51
+ "operationId": "index",
52
+ "summary": "return a list of Pets",
53
+ "description": "",
54
+ "parameters": [],
55
+ "responses": {
56
+ "default": {
57
+ "description": "successful operation"
58
+ }
59
+ }
60
+ }
61
+ },
62
+ "/pets/{id}": {
63
+ "get": {
64
+ "tags": [
65
+ "pets"
66
+ ],
67
+ "operationId": "show",
68
+ "summary": "return a Pet",
69
+ "description": "",
70
+ "parameters": [
71
+ {
72
+ "name": "id",
73
+ "in": "path",
74
+ "required": true,
75
+ "description": "Scope response to id",
76
+ "schema": {
77
+ "type": "string"
78
+ }
79
+ },
80
+ {
81
+ "name": "id",
82
+ "in": "query",
83
+ "required": false,
84
+ "description": "The ID for the Pet",
85
+ "schema": {
86
+ "type": "integer",
87
+ "format": "int32"
88
+ }
89
+ },
90
+ {
91
+ "name": "status",
92
+ "in": "query",
93
+ "required": false,
94
+ "description": "! [available, pending, sold] The status of the Pet",
95
+ "schema": {
96
+ "type": "array",
97
+ "items": {
98
+ "type": "string"
99
+ }
100
+ }
101
+ }
102
+ ],
103
+ "responses": {
104
+ "default": {
105
+ "description": "successful operation"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
@@ -9,6 +9,7 @@ class PetsController < ApplicationController
9
9
  # return a Pet
10
10
  #
11
11
  # @query_parameter [Integer] id The ID for the Pet
12
+ # @query_parameter [Array<String>] status! [available, pending, sold] The status of the Pet
12
13
  def show
13
14
  end
14
15
 
@@ -1,5 +1,4 @@
1
1
  require 'action_controller/railtie'
2
- require 'sprockets/railtie'
3
2
 
4
3
  # Load Swaggard
5
4
  require File.expand_path('../../../../../lib/swaggard', __FILE__)