Class: Taro::Export::OpenAPIv3

Inherits:
Base
  • Object
show all
Defined in:
lib/taro/export/open_api_v3.rb

Overview

rubocop:disable Metrics/ClassLength

Instance Attribute Summary collapse

Attributes inherited from Base

#result

Instance Method Summary collapse

Methods inherited from Base

call, #to_json, #to_yaml, #write_to_file

Constructor Details

#initializeOpenAPIv3

Returns a new instance of OpenAPIv3.



4
5
6
7
# File 'lib/taro/export/open_api_v3.rb', line 4

def initialize
  super
  @schemas = {}
end

Instance Attribute Details

#schemasObject (readonly)

Returns the value of attribute schemas.



2
3
4
# File 'lib/taro/export/open_api_v3.rb', line 2

def schemas
  @schemas
end

Instance Method Details

#assert_unique_openapi_name(type) ⇒ Object



241
242
243
244
245
246
247
248
249
250
# File 'lib/taro/export/open_api_v3.rb', line 241

def assert_unique_openapi_name(type)
  @name_to_type_map ||= {}
  if (prev = @name_to_type_map[type.openapi_name]) && !prev.equivalent?(type)
    raise Taro::InvariantError, <<~MSG
      Duplicate openapi_name "#{type.openapi_name}" for types #{prev} and #{type}
    MSG
  else
    @name_to_type_map[type.openapi_name] = type
  end
end

#call(declarations:, title:, version:) ⇒ Object



9
10
11
12
13
14
15
# File 'lib/taro/export/open_api_v3.rb', line 9

def call(declarations:, title:, version:)
  @result = { openapi: '3.1.0', info: { title:, version: } }
  paths = export_paths(declarations)
  @result[:paths] = paths.sort.to_h if paths.any?
  @result[:components] = { schemas: schemas.sort.to_h } if schemas.any?
  self
end

#custom_scalar_type?(type) ⇒ Boolean

Returns:

  • (Boolean)


139
140
141
# File 'lib/taro/export/open_api_v3.rb', line 139

def custom_scalar_type?(type)
  type < Taro::Types::ScalarType && (type.openapi_pattern || type.deprecated)
end

#custom_scalar_type_details(scalar) ⇒ Object



232
233
234
235
236
237
238
239
# File 'lib/taro/export/open_api_v3.rb', line 232

def custom_scalar_type_details(scalar)
  {
    type: scalar.openapi_type,
    deprecated: scalar.deprecated,
    description: scalar.desc,
    pattern: scalar.openapi_pattern,
  }.compact
end

#enum_type_details(enum) ⇒ Object



214
215
216
217
218
219
220
221
# File 'lib/taro/export/open_api_v3.rb', line 214

def enum_type_details(enum)
  {
    type: enum.item_type.openapi_type,
    deprecated: enum.deprecated,
    description: enum.desc,
    enum: enum.values,
  }.compact
end

#export_complex_field_ref(field) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/taro/export/open_api_v3.rb', line 160

def export_complex_field_ref(field)
  ref = extract_component_ref(field.type)
  return ref if (field).empty? && !field.null

  if field.null
    # RE nullable: https://stackoverflow.com/a/70658334
    { oneOf: [ref, { type: 'null' }] }
  else # i.e. with metadata such as description or deprecated
    # https://github.com/OAI/OpenAPI-Specification/issues/2033
    { allOf: [ref] }
  end.merge((field))
end

#export_field(field) ⇒ Object



143
144
145
146
147
148
149
# File 'lib/taro/export/open_api_v3.rb', line 143

def export_field(field)
  if field.type < Taro::Types::ScalarType
    export_scalar_field(field)
  else
    export_complex_field_ref(field)
  end
end

#export_parameter(field) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/taro/export/open_api_v3.rb', line 67

def export_parameter(field)
  validate_path_or_query_parameter(field)

  {
    name: field.name,
    deprecated: field.deprecated,
    description: field.desc,
    required: field.required,
    schema: export_field(field).except(:deprecated, :description),
  }.compact
end

#export_paths(declarations) ⇒ Object



17
18
19
20
21
22
23
24
# File 'lib/taro/export/open_api_v3.rb', line 17

def export_paths(declarations)
  declarations.sort.each_with_object({}) do |declaration, paths|
    declaration.routes.each do |route|
      paths[route.openapi_path] ||= {}
      paths[route.openapi_path].merge! export_route(route, declaration)
    end
  end
end

#export_route(route, declaration) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/taro/export/open_api_v3.rb', line 26

def export_route(route, declaration)
  {
    route.verb.to_sym => {
      deprecated: declaration.deprecated,
      description: declaration.desc,
      summary: declaration.summary,
      tags: declaration.tags,
      operationId: route.openapi_operation_id,
      parameters: route_parameters(declaration, route),
      requestBody: request_body(declaration, route),
      responses: responses(declaration),
    }.compact,
  }
end

#export_scalar_field(field) ⇒ Object



151
152
153
154
155
156
157
158
# File 'lib/taro/export/open_api_v3.rb', line 151

def export_scalar_field(field)
  base = { type: field.openapi_type, format: field.openapi_format }.compact
  # Using oneOf seems more correct than an array of types
  # as it puts props like format together with the main type.
  # https://github.com/OAI/OpenAPI-Specification/issues/3148
  base = { oneOf: [base, { type: 'null' }] } if field.null
  base.merge((field))
end

#export_type(type) ⇒ Object



131
132
133
134
135
136
137
# File 'lib/taro/export/open_api_v3.rb', line 131

def export_type(type)
  if type < Taro::Types::ScalarType && !custom_scalar_type?(type)
    { type: type.openapi_type, format: type.openapi_format }.compact
  else
    extract_component_ref(type)
  end
end

#extract_component_ref(type) ⇒ Object



182
183
184
185
186
# File 'lib/taro/export/open_api_v3.rb', line 182

def extract_component_ref(type)
  assert_unique_openapi_name(type)
  schemas[type.openapi_name.to_sym] ||= type_details(type)
  { '$ref': "#/components/schemas/#{type.openapi_name}" }
end

#field_metadata(field) ⇒ Object



173
174
175
176
177
178
179
180
# File 'lib/taro/export/open_api_v3.rb', line 173

def (field)
  meta = {}
  meta[:description] = field.desc if field.desc
  meta[:deprecated] = field.deprecated unless field.deprecated.nil?
  meta[:default] = field.default if field.default_specified?
  meta[:enum] = field.enum if field.enum
  meta
end

#list_type_details(list) ⇒ Object



223
224
225
226
227
228
229
230
# File 'lib/taro/export/open_api_v3.rb', line 223

def list_type_details(list)
  {
    type: 'array',
    deprecated: list.deprecated,
    description: list.desc,
    items: export_type(list.item_type),
  }.compact
end

#object_type_details(type) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
# File 'lib/taro/export/open_api_v3.rb', line 202

def object_type_details(type)
  required = type.fields.values.select(&:required).map(&:name)
  {
    type: type.openapi_type,
    deprecated: type.deprecated,
    description: type.desc,
    required: (required if required.any?),
    properties: type.fields.to_h { |name, f| [name, export_field(f)] },
    additionalProperties: (true if type.additional_properties?),
  }.compact
end

#path_parameters(declaration, route) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
# File 'lib/taro/export/open_api_v3.rb', line 45

def path_parameters(declaration, route)
  route.path_params.map do |param_name|
    param_field = declaration.params.fields[param_name] || raise(
      Taro::InvariantError,
      "Declaration missing for path param #{param_name} of route #{route}"
    )

    # path params are always required in rails
    export_parameter(param_field).merge(in: 'path', required: true)
  end
end

#query_parameters(declaration, route) ⇒ Object



57
58
59
60
61
62
63
64
65
# File 'lib/taro/export/open_api_v3.rb', line 57

def query_parameters(declaration, route)
  return [] if route.can_have_request_body?

  declaration.params.fields.filter_map do |name, param_field|
    next if route.path_params.include?(name)

    export_parameter(param_field).merge(in: 'query')
  end
end

#request_body(declaration, route) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/taro/export/open_api_v3.rb', line 87

def request_body(declaration, route)
  return unless route.can_have_request_body?

  params = declaration.params
  body_param_fields = params.fields.reject do |name, _field|
    route.path_params.include?(name)
  end
  return unless body_param_fields.any?

  body_input_type = Class.new(params)
  body_input_type.fields.replace(body_param_fields)
  body_input_type.openapi_name = "#{route.openapi_operation_id}_Input"

  # For polymorphic routes (more than one for the same declaration),
  # we can't use refs because their request body might differ:
  # Different params might be in the path vs. in the request body.
  use_refs = !declaration.polymorphic_route?
  schema = request_body_schema(body_input_type, use_refs:)
  { content: { 'application/json': { schema: } } }
end

#request_body_schema(type, use_refs:) ⇒ Object



108
109
110
111
112
113
114
# File 'lib/taro/export/open_api_v3.rb', line 108

def request_body_schema(type, use_refs:)
  if use_refs
    extract_component_ref(type)
  else
    type_details(type)
  end
end

#responses(declaration) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/taro/export/open_api_v3.rb', line 116

def responses(declaration)
  declaration.returns.sort.to_h do |code, type|
    # response description is required in openapi 3 – fall back to status code
    description = declaration.return_descriptions[code] || type.desc ||
                  Taro::StatusCode.coerce_to_message(code)
    [
      code.to_s,
      {
        description:,
        content: { 'application/json': { schema: export_type(type) } },
      }
    ]
  end
end

#route_parameters(declaration, route) ⇒ Object



41
42
43
# File 'lib/taro/export/open_api_v3.rb', line 41

def route_parameters(declaration, route)
  path_parameters(declaration, route) + query_parameters(declaration, route)
end

#type_details(type) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/taro/export/open_api_v3.rb', line 188

def type_details(type)
  if type.respond_to?(:fields) # InputType or ObjectType
    object_type_details(type)
  elsif type < Taro::Types::EnumType
    enum_type_details(type)
  elsif type < Taro::Types::ListType
    list_type_details(type)
  elsif custom_scalar_type?(type)
    custom_scalar_type_details(type)
  else
    raise Taro::InvariantError, "Unexpected type: #{type}"
  end
end

#validate_path_or_query_parameter(field) ⇒ Object



79
80
81
82
83
84
85
# File 'lib/taro/export/open_api_v3.rb', line 79

def validate_path_or_query_parameter(field)
  ok = %i[string integer]
  ok.include?(field.type.openapi_type) || raise(Taro::ArgumentError, <<~MSG)
    Unsupported #{field.openapi_type} as path/query param "#{field.name}",
    expected one of: #{ok.join(', ')}
  MSG
end