swagger_yard 0.4.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.
@@ -0,0 +1,106 @@
1
+ module SwaggerYard
2
+ class Tag < Struct.new(:name, :description)
3
+ end
4
+
5
+ class Paths
6
+ attr_reader :path_items
7
+
8
+ def initialize(path_items)
9
+ @path_items = path_items
10
+ end
11
+
12
+ def paths
13
+ path_items.keys
14
+ end
15
+
16
+ def merge(other)
17
+ merged_items = {}
18
+ (paths + other.paths).uniq.each do |path|
19
+ merged_items[path] = (path_items[path] || PathItem.new) + (other.path_items[path] || PathItem.new)
20
+ end
21
+ Paths.new(merged_items)
22
+ end
23
+ end
24
+
25
+ class ApiGroup
26
+ attr_accessor :description, :resource
27
+ attr_reader :path_items, :authorizations, :class_name
28
+
29
+ def self.from_yard_object(yard_object)
30
+ new.add_yard_object(yard_object)
31
+ end
32
+
33
+ def initialize
34
+ @resource = nil
35
+ @path_items = {}
36
+ @authorizations = {}
37
+ end
38
+
39
+ def valid?
40
+ !@resource.nil?
41
+ end
42
+
43
+ def paths
44
+ Paths.new(path_items)
45
+ end
46
+
47
+ def tag
48
+ @tag ||= Tag.new(resource, description)
49
+ end
50
+
51
+ def add_yard_object(yard_object)
52
+ case yard_object.type
53
+ when :class # controller
54
+ add_info(yard_object)
55
+ if valid?
56
+ yard_object.children.each do |child_object|
57
+ add_yard_object(child_object)
58
+ end
59
+ end
60
+ when :method # actions
61
+ add_path_item(yard_object)
62
+ end
63
+ self
64
+ end
65
+
66
+ def add_info(yard_object)
67
+ @description = yard_object.docstring
68
+ @class_name = yard_object.path
69
+
70
+ if tag = yard_object.tags.detect {|t| t.tag_name == "resource"}
71
+ @resource = tag.text
72
+ end
73
+
74
+ # we only have api_key auth, the value for now is always empty array
75
+ @authorizations = Hash[yard_object.tags.
76
+ select {|t| t.tag_name == "authorize_with"}.
77
+ map(&:text).uniq.
78
+ map {|k| [k, []]}]
79
+ end
80
+
81
+ def add_path_item(yard_object)
82
+ path = path_from_yard_object(yard_object)
83
+
84
+ return if path.nil?
85
+
86
+ path_item = (path_items[path] ||= PathItem.new(self))
87
+ path_item.add_operation(yard_object)
88
+ path
89
+ end
90
+
91
+ def path_from_yard_object(yard_object)
92
+ if tag = yard_object.tags.detect {|t| t.tag_name == "path"}
93
+ tag.text
94
+ elsif fn = SwaggerYard.config.path_discovery_function
95
+ begin
96
+ method, path = fn[yard_object]
97
+ yard_object.add_tag YARD::Tags::Tag.new("path", path, [method]) if path
98
+ path
99
+ rescue => e
100
+ SwaggerYard.log.warn e.message
101
+ nil
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -1,34 +1,37 @@
1
1
  module SwaggerYard
2
2
  class Authorization
3
- attr_reader :pass_as, :key
4
- attr_writer :name
3
+ attr_reader :type, :name, :description
4
+ attr_writer :id, :key
5
5
 
6
6
  def self.from_yard_object(yard_object)
7
7
  new(yard_object.types.first, yard_object.name, yard_object.text)
8
8
  end
9
9
 
10
- def initialize(type, pass_as, key)
11
- @type, @pass_as, @key = type, pass_as, key
10
+ def initialize(type, name, description)
11
+ @type, @name, @description = type, name, description
12
+ @key = nil
12
13
  end
13
14
 
14
- # the spec suggests most auth names are just the type of auth
15
- def name
16
- @name ||= [@pass_as, @key].join('_').downcase.gsub('-', '_')
15
+ def key
16
+ return @key if @key
17
+ return nil unless @description
18
+ return nil unless @type =~ /api_?key|bearer/i
19
+ @key, @description = @description.split(' ', 2)
20
+ @key
17
21
  end
18
22
 
19
- def type
20
- case @type
21
- when "api_key"
22
- "apiKey"
23
- when "basic_auth"
24
- "basicAuth"
25
- end
23
+ def id
24
+ @id ||= api_key_id || name
26
25
  end
27
26
 
28
- def to_h
29
- { "type" => type,
30
- "name" => @key,
31
- "in" => @pass_as }
27
+ private
28
+ def api_key_id
29
+ case type
30
+ when /api_?key/i
31
+ [name, key].compact.join('_').downcase.gsub('-', '_')
32
+ else
33
+ nil
34
+ end
32
35
  end
33
36
  end
34
37
  end
@@ -3,16 +3,19 @@ module SwaggerYard
3
3
  attr_accessor :api_version, :api_base_path
4
4
  attr_accessor :swagger_version
5
5
  attr_accessor :title, :description
6
- attr_accessor :enable, :reload
7
6
  attr_accessor :controller_path, :model_path
8
7
  attr_accessor :path_discovery_function
9
8
  attr_accessor :security_definitions
10
9
 
10
+ # openapi-compatible names
11
+ alias openapi_version swagger_version
12
+ alias openapi_version= swagger_version=
13
+ alias security_schemes security_definitions
14
+ alias security_schemes= security_definitions=
15
+
11
16
  def initialize
12
17
  @swagger_version = "2.0"
13
18
  @api_version = "0.1"
14
- @enable = false
15
- @reload = true
16
19
  @title = "Configure title with SwaggerYard.config.title"
17
20
  @description = "Configure description with SwaggerYard.config.description"
18
21
  @security_definitions = {}
@@ -25,13 +28,5 @@ module SwaggerYard
25
28
  end if mappings
26
29
  @external_schema
27
30
  end
28
-
29
- def swagger_spec_base_path=(ignored)
30
- warn "DEPRECATED: swagger_spec_base_path is no longer necessary."
31
- end
32
-
33
- def api_path=(ignored)
34
- warn "DEPRECATED: api_path is no longer necessary."
35
- end
36
31
  end
37
32
  end
@@ -0,0 +1,11 @@
1
+ module SwaggerYard
2
+ module Example
3
+ def example
4
+ @example
5
+ end
6
+
7
+ def example=(val)
8
+ @example = JSON.parse(val) rescue val
9
+ end
10
+ end
11
+ end
@@ -4,7 +4,8 @@ module SwaggerYard
4
4
  # complex model object as defined by swagger schema
5
5
  #
6
6
  class Model
7
- attr_reader :id, :discriminator, :inherits, :description
7
+ include Example
8
+ attr_reader :id, :discriminator, :inherits, :description, :properties
8
9
 
9
10
  def self.from_yard_object(yard_object)
10
11
  new.tap do |model|
@@ -31,6 +32,10 @@ module SwaggerYard
31
32
  @id = Model.mangle(yard_object.path)
32
33
  end
33
34
 
35
+ def property(key)
36
+ properties.detect {|prop| prop.name == key }
37
+ end
38
+
34
39
  def parse_tags(tags)
35
40
  tags.each do |tag|
36
41
  case tag.tag_name
@@ -48,43 +53,20 @@ module SwaggerYard
48
53
  end
49
54
  when "inherits"
50
55
  @inherits << tag.text
56
+ when "example"
57
+ if tag.name && !tag.name.empty?
58
+ if (prop = property(tag.name))
59
+ prop.example = tag.text
60
+ else
61
+ SwaggerYard.log.warn("no property '#{tag.name}' defined yet to which to attach example: #{value.inspect}")
62
+ end
63
+ else
64
+ self.example = tag.text
65
+ end
51
66
  end
52
67
  end
53
68
 
54
69
  self
55
70
  end
56
-
57
- def inherits_references
58
- @inherits.map { |name| Type.new(name).to_h }
59
- end
60
-
61
- def to_h
62
- h = {}
63
-
64
- if !@properties.empty? || @inherits.empty?
65
- h["type"] = "object"
66
- h["properties"] = Hash[@properties.map {|p| [p.name, p.to_h]}]
67
- h["required"] = @properties.select(&:required?).map(&:name) if @properties.detect(&:required?)
68
- end
69
-
70
- h["discriminator"] = @discriminator if @discriminator
71
-
72
- # Polymorphism
73
- unless @inherits.empty?
74
- all_of = inherits_references
75
- all_of << h unless h.empty?
76
-
77
- if all_of.length == 1 && @description.empty?
78
- h.update(all_of.first)
79
- else
80
- h = { "allOf" => all_of }
81
- end
82
- end
83
-
84
- # Description
85
- h["description"] = @description unless @description.empty?
86
-
87
- h
88
- end
89
71
  end
90
72
  end
@@ -0,0 +1,120 @@
1
+ module SwaggerYard
2
+ class OpenAPI < Swagger
3
+ def to_h
4
+ metadata.merge(definitions)
5
+ end
6
+
7
+ def model_path
8
+ '#/components/schemas/'
9
+ end
10
+
11
+ def metadata
12
+ {
13
+ 'openapi' => '3.0.0',
14
+ 'info' => Info.new.to_h,
15
+ 'servers' => [{'url' => SwaggerYard.config.api_base_path}]
16
+ }
17
+ end
18
+
19
+ def definitions
20
+ {
21
+ "paths" => paths(specification.path_objects),
22
+ "tags" => tags(specification.tag_objects),
23
+ "components" => components
24
+ }
25
+ end
26
+
27
+ def components
28
+ {
29
+ "schemas" => models(specification.model_objects),
30
+ "securitySchemes" => security_defs(specification.security_objects)
31
+ }
32
+ end
33
+
34
+ def parameters(params)
35
+ params.select { |param| param.param_type != 'body' }.map do |param|
36
+ { "name" => param.name,
37
+ "description" => param.description,
38
+ "required" => param.required,
39
+ "in" => param.param_type
40
+ }.tap do |h|
41
+ schema = param.type.schema_with(model_path: model_path)
42
+ h["schema"] = schema
43
+ h["explode"] = true if !Array(param.allow_multiple).empty? && schema["items"]
44
+ end
45
+ end
46
+ end
47
+
48
+ def operation(op)
49
+ op_hash = super
50
+ if body_param = op.parameters.detect { |p| p.param_type == 'body' }
51
+ op_hash['requestBody'] = {
52
+ 'description' => body_param.description,
53
+ 'content' => {
54
+ 'application/json' => {
55
+ 'schema' => body_param.type.schema_with(model_path: model_path)
56
+ }
57
+ }
58
+ }
59
+ end
60
+ op_hash
61
+ end
62
+
63
+ def response(resp, op)
64
+ {}.tap do |h|
65
+ h['description'] = resp && resp.description || op.summary || ''
66
+ if resp && resp.type && (schema = resp.type.schema_with(model_path: model_path))
67
+ h['content'] = { 'application/json' => { 'schema' => schema } }
68
+ h['content']['application/json']['example'] = resp.example if resp.example
69
+ end
70
+ end
71
+ end
72
+
73
+ def security_defs(security_objects)
74
+ defs = super
75
+ Hash[defs.map do |name, d|
76
+ [name, map_security(d)]
77
+ end]
78
+ end
79
+
80
+ def security(obj)
81
+ case obj.type
82
+ when /api_?key/i
83
+ { 'type' => 'apiKey', 'name' => obj.key, 'in' => obj.name }
84
+ when /bearer/i
85
+ { 'type' => obj.type, 'name' => obj.name, 'format' => obj.key }
86
+ else
87
+ { 'type' => obj.type, 'name' => obj.name }
88
+ end.tap do |result|
89
+ result['description'] = obj.description if obj.description && !obj.description.empty?
90
+ end
91
+ end
92
+
93
+ def map_security(h)
94
+ h = Hash[h.map { |k, v| [k.to_s, v] }] # quick-n-dirty stringify keys
95
+ case type = h['type'].to_s
96
+ when 'apiKey', 'http'
97
+ h
98
+ when 'oauth2'
99
+ # convert from swagger2-style oauth2
100
+ if (authUrl = h.delete('authorizationUrl')) && (flow = h.delete('flow'))
101
+ { 'type' => 'oauth2', 'flows' => {
102
+ flow => { 'authorizationUrl' => authUrl } } }.tap do |result|
103
+ (h.keys - ['type']).each do |t|
104
+ result['flows'][flow][t] = h[t]
105
+ end
106
+ result['flows'][flow]['scopes'] = {} unless result['flows'][flow]['scopes']
107
+ end
108
+ else
109
+ h
110
+ end
111
+ else
112
+ { 'type' => 'http', 'scheme' => type }.tap do |result|
113
+ result['bearerFormat'] = h['format'] if h['format']
114
+ end
115
+ end.tap do |result|
116
+ result['description'] = h['description'] unless h['description'].nil? || h['description'].empty?
117
+ end
118
+ end
119
+ end
120
+ end
@@ -1,13 +1,19 @@
1
1
  module SwaggerYard
2
+ class Response
3
+ include Example
4
+ attr_accessor :status, :description, :type
5
+ end
6
+
2
7
  class Operation
3
8
  attr_accessor :description, :ruby_method
4
9
  attr_writer :summary
5
- attr_reader :path, :http_method, :error_messages, :response_type, :response_desc
6
- attr_reader :parameters, :model_names
10
+ attr_reader :path, :http_method
11
+ attr_reader :parameters
12
+ attr_reader :path_item, :responses
7
13
 
8
14
  # TODO: extract to operation builder?
9
- def self.from_yard_object(yard_object, api)
10
- new(api).tap do |operation|
15
+ def self.from_yard_object(yard_object, path_item)
16
+ new(path_item).tap do |operation|
11
17
  operation.ruby_method = yard_object.name(false)
12
18
  operation.description = yard_object.docstring
13
19
  yard_object.tags.each do |tag|
@@ -20,10 +26,16 @@ module SwaggerYard
20
26
  when "response_type"
21
27
  tag = SwaggerYard.requires_type(tag)
22
28
  operation.add_response_type(Type.from_type_list(tag.types), tag.text) if tag
23
- when "error_message"
24
- operation.add_error_message(tag)
29
+ when "error_message", "response"
30
+ operation.add_response(tag)
25
31
  when "summary"
26
32
  operation.summary = tag.text
33
+ when "example"
34
+ if tag.name && !tag.name.empty?
35
+ operation.response(tag.name).example = tag.text
36
+ else
37
+ operation.default_response.example = tag.text
38
+ end
27
39
  end
28
40
  end
29
41
 
@@ -31,57 +43,47 @@ module SwaggerYard
31
43
  end
32
44
  end
33
45
 
34
- def initialize(api)
35
- @api = api
46
+ def initialize(path_item)
47
+ @path_item = path_item
36
48
  @summary = nil
37
49
  @description = ""
38
50
  @parameters = []
39
- @model_names = []
40
- @error_messages = []
51
+ @default_response = nil
52
+ @responses = []
41
53
  end
42
54
 
43
55
  def summary
44
56
  @summary || description.split("\n\n").first || ""
45
57
  end
46
58
 
47
- def to_h
48
- params = parameters.map(&:to_h)
49
- responses = { "default" => { "description" => response_desc || summary } }
50
-
51
- if response_type
52
- responses["default"]["schema"] = response_type.to_h
53
- end
54
-
55
- unless error_messages.empty?
56
- error_messages.each do |err|
57
- responses[err["code"].to_s] = {}.tap do |h|
58
- h["description"] = err["message"]
59
- h["schema"] = Type.from_type_list(Array(err["responseModel"])).to_h if err["responseModel"]
60
- end
61
- end
62
- end
59
+ def operation_id
60
+ "#{api_group.resource}-#{ruby_method}"
61
+ end
63
62
 
64
- api_decl = @api.api_declaration
63
+ def api_group
64
+ path_item.api_group
65
+ end
65
66
 
66
- {
67
- "tags" => [api_decl.resource].compact,
68
- "operationId" => "#{api_decl.resource}-#{ruby_method}",
69
- "parameters" => params,
70
- "responses" => responses,
71
- }.tap do |h|
72
- h["description"] = description unless description.empty?
73
- h["summary"] = summary unless summary.empty?
67
+ def tags
68
+ [api_group.resource].compact
69
+ end
74
70
 
75
- authorizations = api_decl.authorizations
76
- unless authorizations.empty?
77
- h["security"] = authorizations.map {|k,v| { k => v} }
71
+ def responses_by_status
72
+ {}.tap do |hash|
73
+ hash['default'] = default_response if @default_response || @responses.empty?
74
+ responses.each do |response|
75
+ hash[response.status] = response
78
76
  end
77
+ end
78
+ end
79
79
 
80
+ def extended_attributes
81
+ {}.tap do |h|
80
82
  # Rails controller/action: if constantize/controller_path methods are
81
83
  # unavailable or constant is not defined, catch exception and skip these
82
84
  # attributes.
83
85
  begin
84
- h["x-controller"] = api_decl.class_name.constantize.controller_path.to_s
86
+ h["x-controller"] = api_group.class_name.constantize.controller_path.to_s
85
87
  h["x-action"] = ruby_method.to_s
86
88
  rescue NameError, NoMethodError
87
89
  end
@@ -112,7 +114,7 @@ module SwaggerYard
112
114
  # Example: [Array] status(required, body) Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
113
115
  # Example: [Integer] media[media_type_id] ID of the desired media type.
114
116
  def add_parameter(tag)
115
- param = Parameter.from_yard_tag(tag, self)
117
+ param = Parameter.from_yard_tag(tag)
116
118
  add_or_update_parameter param if param
117
119
  end
118
120
 
@@ -124,29 +126,44 @@ module SwaggerYard
124
126
  existing.allow_multiple = parameter.allow_multiple
125
127
  elsif parameter.param_type == 'body' && @parameters.detect {|param| param.param_type == 'body'}
126
128
  SwaggerYard.log.warn 'multiple body parameters invalid: ' \
127
- "ignored #{parameter.name} for #{@api.api_declaration.class_name}##{ruby_method}"
129
+ "ignored #{parameter.name} for #{@path_item.api_group.class_name}##{ruby_method}"
128
130
  else
129
131
  @parameters << parameter
130
132
  end
131
133
  end
132
134
 
135
+ def default_response
136
+ @default_response ||= Response.new.tap do |r|
137
+ r.status = 'default'
138
+ end
139
+ end
140
+
133
141
  ##
134
142
  # Example:
135
143
  # @response_type [Ownership] the requested ownership
136
144
  def add_response_type(type, desc)
137
- model_names << type.model_name
138
- @response_type = type
139
- @response_desc = desc
145
+ default_response.type = type
146
+ default_response.description = desc
147
+ end
148
+
149
+ def response(name)
150
+ status = Integer(name)
151
+ resp = responses.detect { |r| r.status == status }
152
+ unless resp
153
+ resp = Response.new
154
+ resp.status = status
155
+ responses << resp
156
+ end
157
+ resp
140
158
  end
141
159
 
142
- def add_error_message(tag)
160
+ def add_response(tag)
143
161
  tag = SwaggerYard.requires_name(tag)
144
162
  return unless tag
145
- @error_messages << {
146
- "code" => Integer(tag.name),
147
- "message" => tag.text,
148
- "responseModel" => Array(tag.types).first
149
- }.reject {|_,v| v.nil?}
163
+ r = response(tag.name)
164
+ r.description = tag.text if tag.text
165
+ r.type = Type.from_type_list(Array(tag.types)) if tag.types
166
+ r
150
167
  end
151
168
 
152
169
  def sort_parameters