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,161 @@
1
+ module SwaggerYard
2
+ class Operation
3
+ attr_accessor :description, :ruby_method
4
+ attr_writer :summary
5
+ attr_reader :path, :http_method, :error_messages, :response_type, :response_desc
6
+ attr_reader :parameters, :model_names
7
+
8
+ # TODO: extract to operation builder?
9
+ def self.from_yard_object(yard_object, api)
10
+ new(api).tap do |operation|
11
+ operation.ruby_method = yard_object.name(false)
12
+ operation.description = yard_object.docstring
13
+ yard_object.tags.each do |tag|
14
+ case tag.tag_name
15
+ when "path"
16
+ tag = SwaggerYard.requires_type(tag)
17
+ operation.add_path_params_and_method(tag) if tag
18
+ when "parameter"
19
+ operation.add_parameter(tag)
20
+ when "response_type"
21
+ tag = SwaggerYard.requires_type(tag)
22
+ operation.add_response_type(Type.from_type_list(tag.types), tag.text) if tag
23
+ when "error_message"
24
+ operation.add_error_message(tag)
25
+ when "summary"
26
+ operation.summary = tag.text
27
+ end
28
+ end
29
+
30
+ operation.sort_parameters
31
+ end
32
+ end
33
+
34
+ def initialize(api)
35
+ @api = api
36
+ @summary = nil
37
+ @description = ""
38
+ @parameters = []
39
+ @model_names = []
40
+ @error_messages = []
41
+ end
42
+
43
+ def summary
44
+ @summary || description.split("\n\n").first || ""
45
+ end
46
+
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
63
+
64
+ api_decl = @api.api_declaration
65
+
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?
74
+
75
+ authorizations = api_decl.authorizations
76
+ unless authorizations.empty?
77
+ h["security"] = authorizations.map {|k,v| { k => v} }
78
+ end
79
+
80
+ # Rails controller/action: if constantize/controller_path methods are
81
+ # unavailable or constant is not defined, catch exception and skip these
82
+ # attributes.
83
+ begin
84
+ h["x-controller"] = api_decl.class_name.constantize.controller_path.to_s
85
+ h["x-action"] = ruby_method.to_s
86
+ rescue NameError, NoMethodError
87
+ end
88
+ end
89
+ end
90
+
91
+ ##
92
+ # Example: [GET] /api/v2/ownerships
93
+ # Example: [PUT] /api/v1/accounts/{account_id}
94
+ def add_path_params_and_method(tag)
95
+ if @path && @http_method
96
+ SwaggerYard.log.warn 'multiple path tags not supported: ' \
97
+ "ignored [#{tag.types.first}] #{tag.text}"
98
+ return
99
+ end
100
+
101
+ @path = tag.text
102
+ @http_method = tag.types.first
103
+
104
+ parse_path_params(tag.text).each do |name|
105
+ add_or_update_parameter Parameter.from_path_param(name)
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Example: [Array] status Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
111
+ # Example: [Array] status(required) Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
112
+ # Example: [Array] status(required, body) Filter by status. (e.g. status[]=1&status[]=2&status[]=3)
113
+ # Example: [Integer] media[media_type_id] ID of the desired media type.
114
+ def add_parameter(tag)
115
+ param = Parameter.from_yard_tag(tag, self)
116
+ add_or_update_parameter param if param
117
+ end
118
+
119
+ def add_or_update_parameter(parameter)
120
+ if existing = @parameters.detect {|param| param.name == parameter.name }
121
+ existing.description = parameter.description unless parameter.from_path?
122
+ existing.param_type = parameter.param_type if parameter.from_path?
123
+ existing.required ||= parameter.required
124
+ existing.allow_multiple = parameter.allow_multiple
125
+ elsif parameter.param_type == 'body' && @parameters.detect {|param| param.param_type == 'body'}
126
+ SwaggerYard.log.warn 'multiple body parameters invalid: ' \
127
+ "ignored #{parameter.name} for #{@api.api_declaration.class_name}##{ruby_method}"
128
+ else
129
+ @parameters << parameter
130
+ end
131
+ end
132
+
133
+ ##
134
+ # Example:
135
+ # @response_type [Ownership] the requested ownership
136
+ def add_response_type(type, desc)
137
+ model_names << type.model_name
138
+ @response_type = type
139
+ @response_desc = desc
140
+ end
141
+
142
+ def add_error_message(tag)
143
+ tag = SwaggerYard.requires_name(tag)
144
+ 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?}
150
+ end
151
+
152
+ def sort_parameters
153
+ @parameters.sort_by! {|p| p.name}
154
+ end
155
+
156
+ private
157
+ def parse_path_params(path)
158
+ path.scan(/\{([^\}]+)\}/).flatten
159
+ end
160
+ end
161
+ end
@@ -2,7 +2,7 @@ module SwaggerYard
2
2
  class Parameter
3
3
  attr_accessor :name, :type, :description, :param_type, :required, :allow_multiple
4
4
 
5
- def self.from_yard_tag(tag, operation)
5
+ def self.from_yard_tag(tag)
6
6
  tag = SwaggerYard.requires_name_and_type(tag)
7
7
  return nil unless tag
8
8
 
@@ -13,8 +13,6 @@ module SwaggerYard
13
13
 
14
14
  options = {}
15
15
 
16
- operation.model_names << type.name if type.ref?
17
-
18
16
  unless options_string.nil?
19
17
  options_string.split(',').map(&:strip).tap do |arr|
20
18
  options[:required] = !arr.delete('required').nil?
@@ -48,20 +46,5 @@ module SwaggerYard
48
46
  def from_path?
49
47
  @from_path
50
48
  end
51
-
52
- def to_h
53
- { "name" => name,
54
- "description" => description,
55
- "required" => required,
56
- "in" => param_type
57
- }.tap do |h|
58
- if h["in"] == "body"
59
- h["schema"] = @type.to_h
60
- else
61
- h.update(@type.to_h)
62
- end
63
- h["collectionFormat"] = 'multi' if !Array(allow_multiple).empty? && h["items"]
64
- end
65
- end
66
49
  end
67
50
  end
@@ -0,0 +1,21 @@
1
+ module SwaggerYard
2
+ class PathItem
3
+ attr_accessor :operations, :api_group
4
+
5
+ def initialize(api_group = nil)
6
+ @api_group = api_group
7
+ @operations = {}
8
+ end
9
+
10
+ def add_operation(yard_object)
11
+ operation = Operation.from_yard_object(yard_object, self)
12
+ @operations[operation.http_method.downcase] = operation
13
+ end
14
+
15
+ def +(other)
16
+ PathItem.new(api_group).tap do |pi|
17
+ pi.operations = operations.merge(other.operations)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -3,7 +3,8 @@ module SwaggerYard
3
3
  # Holds the name and type for a single model property
4
4
  #
5
5
  class Property
6
- attr_reader :name, :description
6
+ include Example
7
+ attr_reader :name, :description, :required, :type, :nullable
7
8
 
8
9
  def self.from_tag(tag)
9
10
  tag = SwaggerYard.requires_name_and_type(tag)
@@ -23,22 +24,8 @@ module SwaggerYard
23
24
  @type = Type.from_type_list(types)
24
25
  end
25
26
 
26
- def required?
27
+ def required?
27
28
  @required
28
29
  end
29
-
30
- def to_h
31
- @type.to_h.tap do |h|
32
- unless h['$ref']
33
- h["description"] = description if description && !description.strip.empty?
34
- if @nullable
35
- h["x-nullable"] = true
36
- if h["type"]
37
- h["type"] = [h["type"], "null"]
38
- end
39
- end
40
- end
41
- end
42
- end
43
30
  end
44
31
  end
@@ -1,12 +1,9 @@
1
1
  module SwaggerYard
2
- class ResourceListing
2
+ class Specification
3
3
  attr_accessor :authorizations
4
4
 
5
- def self.all
6
- new(SwaggerYard.config.controller_path, SwaggerYard.config.model_path)
7
- end
8
-
9
- def initialize(controller_path, model_path)
5
+ def initialize(controller_path = SwaggerYard.config.controller_path,
6
+ model_path = SwaggerYard.config.model_path)
10
7
  @model_paths = [*model_path].compact
11
8
  @controller_paths = [*controller_path].compact
12
9
 
@@ -14,44 +11,34 @@ module SwaggerYard
14
11
  @authorizations = []
15
12
  end
16
13
 
17
- def models
18
- @models ||= parse_models
19
- end
20
-
21
- def controllers
22
- @controllers ||= parse_controllers
23
- end
24
-
25
- def to_h
26
- { "paths" => path_objects,
27
- "definitions" => model_objects,
28
- "tags" => tag_objects,
29
- "securityDefinitions" => security_objects }
30
- end
31
-
32
14
  def path_objects
33
- controllers.map(&:apis_hash).reduce({}, :merge).tap do |paths|
15
+ api_groups.map(&:paths).reduce(Paths.new({}), :merge).tap do |paths|
34
16
  warn_duplicate_operations(paths)
35
17
  end
36
18
  end
37
19
 
38
20
  # Resources
39
21
  def tag_objects
40
- controllers.sort {|a,b| a.resource.upcase <=> b.resource.upcase}.map(&:to_tag)
22
+ api_groups.map(&:tag)
41
23
  end
42
24
 
43
25
  def model_objects
44
- Hash[models.map {|m| [m.id, m.to_h]}]
26
+ Hash[models.map {|m| [m.id, m]}]
45
27
  end
46
28
 
47
29
  def security_objects
48
- controllers # triggers controller parsing in case it did not happen before
49
- SwaggerYard.config.security_definitions.merge(
50
- Hash[authorizations.map {|auth| [auth.name, auth.to_h]}]
51
- )
30
+ api_groups # triggers controller parsing in case it did not happen before
31
+ Hash[authorizations.map {|auth| [auth.id, auth]}]
52
32
  end
53
33
 
54
34
  private
35
+ def models
36
+ @models ||= parse_models
37
+ end
38
+
39
+ def api_groups
40
+ @api_groups ||= parse_controllers
41
+ end
55
42
 
56
43
  def parse_models
57
44
  @model_paths.map do |model_path|
@@ -70,7 +57,7 @@ module SwaggerYard
70
57
  obj.tags.select {|t| t.tag_name == "authorization"}.each do |t|
71
58
  @authorizations << Authorization.from_yard_object(t)
72
59
  end
73
- ApiDeclaration.from_yard_object(obj)
60
+ ApiGroup.from_yard_object(obj)
74
61
  end
75
62
  end
76
63
  end.flatten.select(&:valid?)
@@ -78,13 +65,13 @@ module SwaggerYard
78
65
 
79
66
  def warn_duplicate_operations(paths)
80
67
  operation_ids = []
81
- paths.each do |path,ops|
82
- ops.each do |method,op|
83
- if operation_ids.include?(op['operationId'])
84
- SwaggerYard.log.warn("duplicate operation #{op['operationId']}")
68
+ paths.path_items.each do |path,pi|
69
+ pi.operations.each do |_, op|
70
+ if operation_ids.include?(op.operation_id)
71
+ SwaggerYard.log.warn("duplicate operation #{op.operation_id}")
85
72
  next
86
73
  end
87
- operation_ids << op['operationId']
74
+ operation_ids << op.operation_id
88
75
  end
89
76
  end
90
77
  end
@@ -10,15 +10,45 @@ module SwaggerYard
10
10
  end
11
11
 
12
12
  class Swagger
13
+ class << self; alias object_new new; end
14
+
15
+ def self.new(*args)
16
+ return OpenAPI.object_new(*args) if SwaggerYard.config.swagger_version.start_with?("3.0")
17
+ super
18
+ end
19
+
20
+ attr_reader :specification
21
+
22
+ def initialize(spec = Specification.new)
23
+ @specification = spec
24
+ end
25
+
13
26
  def to_h
27
+ metadata.merge(definitions).merge(model_definitions)
28
+ end
29
+
30
+ private
31
+ def model_path
32
+ Type::MODEL_PATH
33
+ end
34
+
35
+ def definitions
36
+ { "paths" => paths(specification.path_objects),
37
+ "tags" => tags(specification.tag_objects),
38
+ "securityDefinitions" => security_defs(specification.security_objects) }
39
+ end
40
+
41
+ def model_definitions
42
+ { "definitions" => models(specification.model_objects) }
43
+ end
44
+
45
+ def metadata
14
46
  {
15
47
  "swagger" => "2.0",
16
48
  "info" => Info.new.to_h
17
- }.merge(uri_info).merge(ResourceListing.all.to_h)
49
+ }.merge(uri_info)
18
50
  end
19
51
 
20
- private
21
-
22
52
  def uri_info
23
53
  uri = URI(SwaggerYard.config.api_base_path)
24
54
  host = uri.host
@@ -26,8 +56,146 @@ module SwaggerYard
26
56
 
27
57
  {
28
58
  'host' => host,
29
- 'basePath' => uri.request_uri
59
+ 'basePath' => uri.request_uri,
60
+ 'schemes' => [uri.scheme]
30
61
  }
31
62
  end
63
+
64
+ def paths(paths)
65
+ Hash[paths.path_items.map {|path,pi| [path, operations(pi.operations)] }]
66
+ end
67
+
68
+ def operations(ops)
69
+ expanded_ops = ops.map do |meth, op|
70
+
71
+ [meth, operation(op)]
72
+ end
73
+ Hash[expanded_ops]
74
+ end
75
+
76
+ def operation(op)
77
+ op_hash = {
78
+ "tags" => op.tags,
79
+ "operationId" => op.operation_id,
80
+ "parameters" => parameters(op.parameters),
81
+ "responses" => responses(op.responses_by_status, op),
82
+ }
83
+
84
+ op_hash["description"] = op.description unless op.description.empty?
85
+ op_hash["summary"] = op.summary unless op.summary.empty?
86
+
87
+ authorizations = op.api_group.authorizations
88
+ unless authorizations.empty?
89
+ op_hash["security"] = authorizations.map {|k,v| { k => v} }
90
+ end
91
+
92
+ op_hash.update(op.extended_attributes)
93
+ end
94
+
95
+ def parameters(params)
96
+ params.map do |param|
97
+ { "name" => param.name,
98
+ "description" => param.description,
99
+ "required" => param.required,
100
+ "in" => param.param_type
101
+ }.tap do |h|
102
+ schema = param.type.schema_with(model_path: model_path)
103
+ if h["in"] == "body"
104
+ h["schema"] = schema
105
+ else
106
+ h.update(schema)
107
+ end
108
+ h["collectionFormat"] = 'multi' if !Array(param.allow_multiple).empty? && h["items"]
109
+ end
110
+ end
111
+ end
112
+
113
+ def responses(responses_by_status, op)
114
+ Hash[responses_by_status.map { |status, resp| [status, response(resp, op)] }]
115
+ end
116
+
117
+ def response(resp, op)
118
+ {}.tap do |h|
119
+ h['description'] = resp && resp.description || op.summary || ''
120
+ h['schema'] = resp.type.schema_with(model_path: model_path) if resp && resp.type
121
+ if resp && resp.example
122
+ h['examples'] = {
123
+ 'application/json' => resp.example
124
+ }
125
+ end
126
+ end
127
+ end
128
+
129
+ def models(model_objects)
130
+ Hash[model_objects.map { |name, mod| [name, model(mod)] }]
131
+ end
132
+
133
+ def model(mod)
134
+ h = {}
135
+
136
+ if !mod.properties.empty? || mod.inherits.empty?
137
+ h["type"] = "object"
138
+ h["properties"] = Hash[mod.properties.map {|p| [p.name, property(p)]}]
139
+ h["required"] = mod.properties.select(&:required?).map(&:name) if mod.properties.detect(&:required?)
140
+ end
141
+
142
+ h["discriminator"] = mod.discriminator if mod.discriminator
143
+
144
+ # Polymorphism
145
+ unless mod.inherits.empty?
146
+ all_of = mod.inherits.map { |name| Type.new(name).schema_with(model_path: model_path) }
147
+ all_of << h unless h.empty?
148
+
149
+ if all_of.length == 1 && mod.description.empty?
150
+ h.update(all_of.first)
151
+ else
152
+ h = { "allOf" => all_of }
153
+ end
154
+ end
155
+
156
+ # Description
157
+ h["description"] = mod.description unless mod.description.empty?
158
+
159
+ h["example"] = mod.example if mod.example
160
+
161
+ h
162
+ end
163
+
164
+ def property(prop)
165
+ prop.type.schema_with(model_path: model_path).tap do |h|
166
+ unless h['$ref']
167
+ h["description"] = prop.description if prop.description && !prop.description.strip.empty?
168
+ if prop.nullable
169
+ h["x-nullable"] = true
170
+ if h["type"]
171
+ h["type"] = [h["type"], "null"]
172
+ end
173
+ end
174
+ h["example"] = prop.example if prop.example
175
+ end
176
+ end
177
+ end
178
+
179
+ def tags(tag_objects)
180
+ tag_objects.sort_by {|t| t.name.upcase }.map do |t|
181
+ { 'name' => t.name, 'description' => t.description }
182
+ end
183
+ end
184
+
185
+ def security_defs(security_objects)
186
+ config_defs = SwaggerYard.config.security_definitions
187
+ config_defs.merge(Hash[security_objects.map { |name, obj| [name, security(obj)] }])
188
+ end
189
+
190
+ def security(obj)
191
+ case obj.type
192
+ when /api_?key/i
193
+ { 'type' => 'apiKey', 'name' => obj.key, 'in' => obj.name }
194
+ else
195
+ { 'type' => 'basic' }
196
+ end.tap do |result|
197
+ result['description'] = obj.description if obj.description && !obj.description.empty?
198
+ end
199
+ end
32
200
  end
33
201
  end