swagger_yard 0.4.4 → 1.0.0

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