Module: RubstApi::OpenAPI

Defined in:
lib/rubst_api/openapi.rb

Class Method Summary collapse

Class Method Details

.collect_schema(type, schemas) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rubst_api/openapi.rb', line 92

def collect_schema(type, schemas)
  if type.is_a?(Class) && type <= Model
    schemas[type.name.split("::").last] ||= type.json_schema
    type.fields.each_value { |field| collect_schema(field.type, schemas) }
  elsif type.is_a?(Array)
    type.each { |item| collect_schema(item, schemas) }
  elsif type.is_a?(Hash) && type[:array]
    collect_schema(type[:array], schemas)
  end
  schemas["ValidationError"] ||= {
    type: "object", properties: {
      loc: { type: "array", items: { anyOf: [{ type: "string" }, { type: "integer" }] } },
      msg: { type: "string" }, type: { type: "string" }
    }, required: %w[loc msg type]
  }
  schemas["HTTPValidationError"] ||= {
    type: "object", properties: { detail: { type: "array", items: { "$ref": "#/components/schemas/ValidationError" } } }
  }
end

.generate(app) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/rubst_api/openapi.rb', line 7

def generate(app)
  schemas = {}
  security_schemes = {}
  paths = {}
  app.routes.select(&:include_in_schema).each do |route|
    operation = {
      tags: route.tags.empty? ? nil : route.tags,
      summary: route.summary || humanize(route.name || route.unique_id),
      description: route.description,
      operationId: route.unique_id,
      deprecated: route.deprecated ? true : nil,
      parameters: parameters_for(route, schemas),
      responses: responses_for(route, schemas)
    }.compact
    security = security_for(route, security_schemes)
    operation[:security] = security unless security.empty?
    body = request_body_for(route, schemas)
    operation[:requestBody] = body if body
    route.methods.each do |method|
      (paths[route.path] ||= {})[method.downcase.to_sym] = operation
    end
  end
  {
    openapi: app.openapi_version,
    info: { title: app.title, version: app.version, description: app.description }.compact,
    servers: app.servers.empty? ? nil : app.servers,
    paths:,
    components: {
      schemas: schemas.empty? ? nil : schemas,
      securitySchemes: security_schemes.empty? ? nil : security_schemes
    }.compact,
    tags: app.openapi_tags.empty? ? nil : app.openapi_tags
  }.compact
end

.humanize(value) ⇒ Object



116
# File 'lib/rubst_api/openapi.rb', line 116

def humanize(value) = value.to_s.tr("_", " ").split.map(&:capitalize).join(" ")

.parameters_for(route, schemas) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/rubst_api/openapi.rb', line 52

def parameters_for(route, schemas)
  route.params.filter_map do |name, param|
    next if param.is_a?(Dependency)
    next if param.location == :body || param.location == :form || param.location == :file || !param.include_in_schema
    collect_schema(param.type, schemas)
    {
      name: param.name_for(name), in: param.location.to_s, required: param.required?,
      description: param.description, deprecated: param.deprecated,
      schema: Schema.for(param.type, param.constraints)
    }.compact
  end
end

.request_body_for(route, schemas) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/rubst_api/openapi.rb', line 65

def request_body_for(route, schemas)
  body_params = route.params.select { |_, param| param.is_a?(Param) && %i[body form file].include?(param.location) }
  return if body_params.empty?
  body_params.each_value { |param| collect_schema(param.type, schemas) }
  media_type = body_params.values.any? { |param| param.location == :file } ? "multipart/form-data" :
               body_params.values.any? { |param| param.location == :form } ? "application/x-www-form-urlencoded" : "application/json"
  schema = if body_params.length == 1
             Schema.for(body_params.values.first.type)
           else
             { type: "object", properties: body_params.to_h { |name, param| [name, Schema.for(param.type)] },
               required: body_params.select { |_, param| param.required? }.keys }
           end
  { required: body_params.values.any?(&:required?), content: { media_type => { schema: } } }
end

.responses_for(route, schemas) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rubst_api/openapi.rb', line 80

def responses_for(route, schemas)
  schema = route.response_model ? Schema.for(route.response_model) : {}
  collect_schema(route.response_model, schemas) if route.response_model
  output = { route.status_code.to_s => { description: status_description(route.status_code),
                                         content: { "application/json" => { schema: } } } }
  output["422"] = { description: "Validation Error", content: { "application/json" => {
    schema: { "$ref": "#/components/schemas/HTTPValidationError" }
  } } } unless route.params.empty?
  route.responses.each { |code, definition| output[code.to_s] = definition }
  output
end

.security_for(route, schemes) ⇒ Object



42
43
44
45
46
47
48
49
50
# File 'lib/rubst_api/openapi.rb', line 42

def security_for(route, schemes)
  (route.dependencies + route.params.values.grep(Dependency)).filter_map do |dependency|
    callable = dependency.callable
    next unless callable.respond_to?(:openapi_scheme)
    name = callable.scheme_name
    schemes[name] ||= callable.openapi_scheme
    { name => dependency.is_a?(SecurityDependency) ? dependency.scopes : [] }
  end
end

.status_description(code) ⇒ Object



112
113
114
115
# File 'lib/rubst_api/openapi.rb', line 112

def status_description(code)
  { 200 => "Successful Response", 201 => "Successful Response", 202 => "Successful Response",
    204 => "Successful Response", 301 => "Redirect Response", 307 => "Redirect Response" }[code] || "Response"
end