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,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