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

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



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

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)


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

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

#custom_scalar_type_details(scalar) ⇒ Object



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

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



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

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



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

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



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

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



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

def export_parameter(field)
  validate_path_or_query_parameter(field)

  {
    name: field.name,
    deprecated: field.deprecated,
    description: field.desc,
    required: !field.null,
    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
# File 'lib/taro/export/open_api_v3.rb', line 26

def export_route(route, declaration)
  {
    route.verb.to_sym => {
      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



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

def export_scalar_field(field)
  base = { type: field.openapi_type }
  # 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



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

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

#extract_component_ref(type) ⇒ Object



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

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



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

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



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

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



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

def object_type_details(type)
  required = type.fields.values.reject(&:null).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



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

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



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

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



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

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



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

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

#responses(declaration) ⇒ Object



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

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



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

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

#type_details(type) ⇒ Object



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

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



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

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