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.
- checksums.yaml +4 -4
- data/README.md +202 -103
- data/lib/swagger_yard.rb +8 -4
- data/lib/swagger_yard/api_group.rb +106 -0
- data/lib/swagger_yard/authorization.rb +21 -18
- data/lib/swagger_yard/configuration.rb +6 -11
- data/lib/swagger_yard/example.rb +11 -0
- data/lib/swagger_yard/model.rb +16 -34
- data/lib/swagger_yard/openapi.rb +120 -0
- data/lib/swagger_yard/operation.rb +67 -50
- data/lib/swagger_yard/operation.rb.~9b471577ebed4e4ba6ed266566355dbe5990787d~ +161 -0
- data/lib/swagger_yard/parameter.rb +1 -18
- data/lib/swagger_yard/path_item.rb +21 -0
- data/lib/swagger_yard/property.rb +3 -16
- data/lib/swagger_yard/{resource_listing.rb → specification.rb} +21 -34
- data/lib/swagger_yard/swagger.rb +172 -4
- data/lib/swagger_yard/type.rb +20 -12
- data/lib/swagger_yard/type.rb.~master~ +34 -0
- data/lib/swagger_yard/type_parser.rb +10 -4
- data/lib/swagger_yard/version.rb +1 -1
- metadata +9 -5
- data/lib/swagger_yard/api.rb +0 -39
- data/lib/swagger_yard/api_declaration.rb +0 -72
@@ -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
|
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
|
-
|
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
|
-
|
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
|
2
|
+
class Specification
|
3
3
|
attr_accessor :authorizations
|
4
4
|
|
5
|
-
def
|
6
|
-
|
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
|
-
|
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
|
-
|
22
|
+
api_groups.map(&:tag)
|
41
23
|
end
|
42
24
|
|
43
25
|
def model_objects
|
44
|
-
Hash[models.map {|m| [m.id, m
|
26
|
+
Hash[models.map {|m| [m.id, m]}]
|
45
27
|
end
|
46
28
|
|
47
29
|
def security_objects
|
48
|
-
|
49
|
-
|
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
|
-
|
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,
|
82
|
-
|
83
|
-
if operation_ids.include?(op
|
84
|
-
SwaggerYard.log.warn("duplicate operation #{op
|
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
|
74
|
+
operation_ids << op.operation_id
|
88
75
|
end
|
89
76
|
end
|
90
77
|
end
|
data/lib/swagger_yard/swagger.rb
CHANGED
@@ -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)
|
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
|