tina4ruby 3.13.41 → 3.13.42
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.
- checksums.yaml +4 -4
- data/lib/tina4/swagger.rb +234 -19
- data/lib/tina4/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fdfe0b3f854e7ecea3419e6c10efe9e527be73952c8d570fb46106e0476255ec
|
|
4
|
+
data.tar.gz: 1c63029ec5a6e61fb91c7add6377735f6a8b740d0a416826333d400f493a623a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53d20a66c7f796f36befa7777c8e62cf494ff659ef0e9d8aa7f5b7cb078c2abef4c3fc4fe99bc61df797681212d3a91bdd73bf67e6be16a4cd4da15cf91c3f02
|
|
7
|
+
data.tar.gz: 93a184a4b081ad26a81162ac08d61b4ab6051dd1ea90097dfce1dbd177610d2154dc741eb77a0ca31a3d68eb3813dfea1f99801a92550e568450cd3e9a9a2869
|
data/lib/tina4/swagger.rb
CHANGED
|
@@ -3,23 +3,68 @@ require "json"
|
|
|
3
3
|
|
|
4
4
|
module Tina4
|
|
5
5
|
module Swagger
|
|
6
|
+
# Process-wide registries for security schemes and reusable component
|
|
7
|
+
# schemas declared programmatically (add_security_scheme / add_schema).
|
|
8
|
+
# Kept module-level so app bootstrap can register before any generate()
|
|
9
|
+
# call; reset_registry clears them (tests).
|
|
10
|
+
@registered_schemes = {}
|
|
11
|
+
@registered_schemas = {}
|
|
12
|
+
|
|
6
13
|
class << self
|
|
14
|
+
# ── Programmatic registries ────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
# Register a named OpenAPI security scheme (e.g. an oauth2 scheme with
|
|
17
|
+
# scopes, or a custom apiKey). Call at app bootstrap, before generate().
|
|
18
|
+
#
|
|
19
|
+
# Tina4::Swagger.add_security_scheme("oauth2", {
|
|
20
|
+
# "type" => "oauth2",
|
|
21
|
+
# "flows" => { "clientCredentials" => {
|
|
22
|
+
# "tokenUrl" => "https://api.example.com/oauth/token",
|
|
23
|
+
# "scopes" => { "read:users" => "Read users", "write:users" => "Write users" }
|
|
24
|
+
# } }
|
|
25
|
+
# })
|
|
26
|
+
def add_security_scheme(name, definition)
|
|
27
|
+
@registered_schemes[name.to_s] = definition
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Register a reusable component schema, referenceable via
|
|
31
|
+
# swagger_meta[:request_schema] / [:response_schemas] or a raw $ref.
|
|
32
|
+
def add_schema(name, schema)
|
|
33
|
+
@registered_schemas[name.to_s] = schema
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Clear the security-scheme and schema registries (test helper).
|
|
37
|
+
def reset_registry
|
|
38
|
+
@registered_schemes = {}
|
|
39
|
+
@registered_schemas = {}
|
|
40
|
+
end
|
|
41
|
+
|
|
7
42
|
def generate(routes = [])
|
|
8
43
|
spec = base_spec
|
|
9
44
|
route_list = routes.empty? ? Tina4::Router.routes : routes
|
|
10
45
|
# Accumulators shared across routes: ORM models referenced
|
|
11
|
-
# (-> components.schemas),
|
|
12
|
-
#
|
|
13
|
-
|
|
46
|
+
# (-> components.schemas), registered custom-schema names referenced
|
|
47
|
+
# by routes, tags used (-> top-level tags[]), seen operationIds
|
|
48
|
+
# (de-dup — OpenAPI requires them unique).
|
|
49
|
+
ctx = { models: {}, ref_schemas: [], used_tags: [], seen_ids: [],
|
|
50
|
+
schemes: spec["components"]["securitySchemes"] }
|
|
14
51
|
route_list.each do |route|
|
|
52
|
+
next unless included?(route.path)
|
|
53
|
+
|
|
15
54
|
add_route_to_spec(spec, route, ctx)
|
|
16
55
|
end
|
|
17
56
|
|
|
18
|
-
unless ctx[:models].empty?
|
|
19
|
-
spec["components"]["schemas"]
|
|
57
|
+
unless ctx[:models].empty? && ctx[:ref_schemas].empty?
|
|
58
|
+
spec["components"]["schemas"] ||= {}
|
|
20
59
|
ctx[:models].each do |name, klass|
|
|
21
60
|
spec["components"]["schemas"][name] = model_schema(klass)
|
|
22
61
|
end
|
|
62
|
+
ctx[:ref_schemas].each do |name|
|
|
63
|
+
next unless @registered_schemas.key?(name)
|
|
64
|
+
next if spec["components"]["schemas"].key?(name)
|
|
65
|
+
|
|
66
|
+
spec["components"]["schemas"][name] = @registered_schemas[name]
|
|
67
|
+
end
|
|
23
68
|
end
|
|
24
69
|
spec["tags"] = ctx[:used_tags].map { |t| { "name" => t } } unless ctx[:used_tags].empty?
|
|
25
70
|
|
|
@@ -63,22 +108,88 @@ module Tina4
|
|
|
63
108
|
end
|
|
64
109
|
|
|
65
110
|
{
|
|
66
|
-
"openapi" => "
|
|
111
|
+
"openapi" => resolve_openapi_version(ENV["TINA4_SWAGGER_OPENAPI"]),
|
|
67
112
|
"info" => info,
|
|
68
113
|
"servers" => servers,
|
|
69
114
|
"paths" => {},
|
|
70
115
|
"components" => {
|
|
71
|
-
"securitySchemes" =>
|
|
72
|
-
"bearerAuth" => {
|
|
73
|
-
"type" => "http",
|
|
74
|
-
"scheme" => "bearer",
|
|
75
|
-
"bearerFormat" => "JWT"
|
|
76
|
-
}
|
|
77
|
-
}
|
|
116
|
+
"securitySchemes" => security_schemes
|
|
78
117
|
}
|
|
79
118
|
}
|
|
80
119
|
end
|
|
81
120
|
|
|
121
|
+
# OpenAPI version — default 3.0.3 for broad tool compatibility; opt in to
|
|
122
|
+
# 3.1.0 via TINA4_SWAGGER_OPENAPI=3.1 (the schemas this generator emits
|
|
123
|
+
# are valid in both dialects).
|
|
124
|
+
def resolve_openapi_version(val)
|
|
125
|
+
v = val.to_s.strip
|
|
126
|
+
return "3.0.3" if v.empty?
|
|
127
|
+
return "3.1.0" if %w[3.1 3.1.0].include?(v)
|
|
128
|
+
return "3.0.3" if %w[3.0 3.0.3].include?(v)
|
|
129
|
+
|
|
130
|
+
v # honour an explicit full version verbatim
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Resolve components.securitySchemes from defaults + env + registry.
|
|
134
|
+
def security_schemes
|
|
135
|
+
schemes = {
|
|
136
|
+
"bearerAuth" => {
|
|
137
|
+
"type" => "http",
|
|
138
|
+
"scheme" => "bearer",
|
|
139
|
+
# Default bearer format (the built-in bearerAuth scheme). JWT unless an
|
|
140
|
+
# API uses opaque tokens / API keys as the bearer (e.g. sk_live_...).
|
|
141
|
+
"bearerFormat" => ENV["TINA4_SWAGGER_BEARER_FORMAT"] || "JWT"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Optional apiKey scheme — emitted as "apiKeyAuth" when a header/query
|
|
146
|
+
# name is configured (e.g. X-Api-Key).
|
|
147
|
+
api_key_name = ENV["TINA4_SWAGGER_API_KEY_NAME"]
|
|
148
|
+
if api_key_name && !api_key_name.empty?
|
|
149
|
+
api_key_in = ENV["TINA4_SWAGGER_API_KEY_IN"] || "header"
|
|
150
|
+
api_key_in = "header" unless %w[header query cookie].include?(api_key_in)
|
|
151
|
+
schemes["apiKeyAuth"] = {
|
|
152
|
+
"type" => "apiKey",
|
|
153
|
+
"name" => api_key_name,
|
|
154
|
+
"in" => api_key_in
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Registered schemes win (let an app override bearerAuth or add oauth2).
|
|
159
|
+
@registered_schemes.each { |name, defn| schemes[name] = defn }
|
|
160
|
+
schemes
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Which scheme secured routes use by default when no per-route security
|
|
164
|
+
# is declared.
|
|
165
|
+
def default_scheme
|
|
166
|
+
scheme = ENV["TINA4_SWAGGER_DEFAULT_SCHEME"]
|
|
167
|
+
scheme && !scheme.empty? ? scheme : "bearerAuth"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Path filtering. Framework internals (/swagger, /__dev) are ALWAYS
|
|
171
|
+
# excluded; then TINA4_SWAGGER_INCLUDE (allow-list) / _EXCLUDE apply.
|
|
172
|
+
def included?(raw_path)
|
|
173
|
+
["/swagger", "/__dev"].each do |internal|
|
|
174
|
+
return false if raw_path == internal || raw_path.start_with?("#{internal}/")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
includes = csv(ENV["TINA4_SWAGGER_INCLUDE"])
|
|
178
|
+
unless includes.empty?
|
|
179
|
+
matched = includes.any? { |p| raw_path == p || raw_path.start_with?(p) }
|
|
180
|
+
return false unless matched
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
excludes = csv(ENV["TINA4_SWAGGER_EXCLUDE"])
|
|
184
|
+
return false if excludes.any? { |p| raw_path == p || raw_path.start_with?(p) }
|
|
185
|
+
|
|
186
|
+
true
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def csv(val)
|
|
190
|
+
val.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
191
|
+
end
|
|
192
|
+
|
|
82
193
|
# servers[] — TINA4_SWAGGER_SERVERS (comma-separated) for a multi-server
|
|
83
194
|
# list, else SWAGGER_DEV_URL, else the relative "/" default.
|
|
84
195
|
def servers
|
|
@@ -124,31 +235,135 @@ module Tina4
|
|
|
124
235
|
"description" => meta[:description] || "",
|
|
125
236
|
"tags" => tags,
|
|
126
237
|
"parameters" => build_parameters(route),
|
|
127
|
-
"responses" => meta
|
|
238
|
+
"responses" => build_responses(meta, ref, ctx)
|
|
128
239
|
}
|
|
129
240
|
|
|
130
241
|
operation["deprecated"] = true if meta[:deprecated]
|
|
131
242
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
end
|
|
243
|
+
security = resolve_security(meta, route, ctx[:schemes])
|
|
244
|
+
operation["security"] = security unless security.nil?
|
|
135
245
|
|
|
136
246
|
if %w[post put patch].include?(method)
|
|
137
|
-
operation["requestBody"] = build_request_body(method, meta, ref)
|
|
247
|
+
operation["requestBody"] = build_request_body(method, meta, ref, ctx)
|
|
138
248
|
end
|
|
139
249
|
|
|
140
250
|
spec["paths"][path][method] = operation
|
|
141
251
|
end
|
|
142
252
|
|
|
143
|
-
|
|
253
|
+
# Auth — explicit per-route security wins (an explicit "public"/[] = no
|
|
254
|
+
# security); otherwise a secured route gets the default scheme. Returns
|
|
255
|
+
# nil when the route declares nothing and is not secured (omit the key).
|
|
256
|
+
def resolve_security(meta, route, schemes)
|
|
257
|
+
if meta.key?(:security)
|
|
258
|
+
reqs = normalize_security(meta[:security], meta[:scopes])
|
|
259
|
+
return reqs.empty? ? [] : sanitize_security(reqs, schemes)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
return sanitize_security([{ default_scheme => [] }], schemes) if route.auth_handler
|
|
263
|
+
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Normalize swagger_meta[:security] to an OpenAPI security-requirement list.
|
|
268
|
+
#
|
|
269
|
+
# security: "oauth2", scopes: ["read"] -> [{"oauth2" => ["read"]}]
|
|
270
|
+
# security: { "bearerAuth" => [] } -> [{"bearerAuth" => []}] (AND within one map)
|
|
271
|
+
# security: [{"oauth2"=>["read"]}, {...}] -> verbatim (OR across maps)
|
|
272
|
+
# security: "public" / [] -> [] (explicitly no auth)
|
|
273
|
+
def normalize_security(value, scopes)
|
|
274
|
+
scope_list = Array(scopes).map(&:to_s)
|
|
275
|
+
if value.nil? || value == [] || (value.is_a?(String) && %w[public none].include?(value))
|
|
276
|
+
return []
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
case value
|
|
280
|
+
when String
|
|
281
|
+
[{ value => scope_list }]
|
|
282
|
+
when Hash
|
|
283
|
+
[value.each_with_object({}) { |(k, v), h| h[k.to_s] = Array(v).map(&:to_s) }]
|
|
284
|
+
when Array
|
|
285
|
+
value.map { |req| req.each_with_object({}) { |(k, v), h| h[k.to_s] = Array(v).map(&:to_s) } }
|
|
286
|
+
else
|
|
287
|
+
[]
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Keep a security-requirement list spec-valid: scopes are allowed only on
|
|
292
|
+
# oauth2/openIdConnect schemes; everything else gets an empty array
|
|
293
|
+
# (OpenAPI requires that for http/apiKey).
|
|
294
|
+
def sanitize_security(reqs, schemes)
|
|
295
|
+
scope_ok = %w[oauth2 openIdConnect]
|
|
296
|
+
reqs.map do |req|
|
|
297
|
+
req.each_with_object({}) do |(name, scopes), clean|
|
|
298
|
+
stype = (schemes[name] || {})["type"]
|
|
299
|
+
clean[name] = scope_ok.include?(stype) ? Array(scopes) : []
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def build_request_body(_method, meta, ref, ctx)
|
|
144
305
|
return meta[:request_body] if meta[:request_body]
|
|
145
306
|
|
|
307
|
+
# Registered custom request schema ($ref) wins over the ORM-model ref.
|
|
308
|
+
if (req_schema = meta[:request_schema])
|
|
309
|
+
name, content_type = request_schema_parts(req_schema)
|
|
310
|
+
ctx[:ref_schemas] << name unless ctx[:ref_schemas].include?(name)
|
|
311
|
+
content = { "schema" => { "$ref" => "#/components/schemas/#{name}" } }
|
|
312
|
+
content["example"] = meta[:example] if meta[:example]
|
|
313
|
+
return { "content" => { content_type => content } }
|
|
314
|
+
end
|
|
315
|
+
|
|
146
316
|
schema = ref ? { "$ref" => ref } : { "type" => "object" }
|
|
147
317
|
content = { "schema" => schema }
|
|
148
318
|
content["example"] = meta[:example] if meta[:example]
|
|
149
319
|
{ "content" => { "application/json" => content } }
|
|
150
320
|
end
|
|
151
321
|
|
|
322
|
+
# swagger_meta[:request_schema] accepts "Name" or { name:, content_type: }.
|
|
323
|
+
def request_schema_parts(req_schema)
|
|
324
|
+
if req_schema.is_a?(Hash)
|
|
325
|
+
[(req_schema[:name] || req_schema["name"]).to_s,
|
|
326
|
+
(req_schema[:content_type] || req_schema["content_type"] || "application/json").to_s]
|
|
327
|
+
else
|
|
328
|
+
[req_schema.to_s, "application/json"]
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def build_responses(meta, ref, ctx)
|
|
333
|
+
return meta[:responses] if meta[:responses]
|
|
334
|
+
|
|
335
|
+
responses = model_or_default_responses(ref, meta[:model_list])
|
|
336
|
+
|
|
337
|
+
# Registered response schemas ($ref) — explicit and authoritative.
|
|
338
|
+
if (resp_schemas = meta[:response_schemas])
|
|
339
|
+
resp_schemas.each do |status, spec|
|
|
340
|
+
name, is_list = response_schema_parts(spec)
|
|
341
|
+
ctx[:ref_schemas] << name unless ctx[:ref_schemas].include?(name)
|
|
342
|
+
sref = "#/components/schemas/#{name}"
|
|
343
|
+
schema = is_list ? { "type" => "array", "items" => { "$ref" => sref } } : { "$ref" => sref }
|
|
344
|
+
responses[status.to_s] = {
|
|
345
|
+
"description" => status.to_s.start_with?("2") ? "Successful response" : "Response",
|
|
346
|
+
"content" => { "application/json" => { "schema" => schema } }
|
|
347
|
+
}
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
responses
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# A response-schema entry is "Name", { name:, list: } or [name, is_list].
|
|
355
|
+
def response_schema_parts(spec)
|
|
356
|
+
case spec
|
|
357
|
+
when Hash
|
|
358
|
+
[(spec[:name] || spec["name"]).to_s,
|
|
359
|
+
!!(spec[:list] || spec["list"] || spec[:is_list] || spec["is_list"])]
|
|
360
|
+
when Array
|
|
361
|
+
[spec[0].to_s, !!spec[1]]
|
|
362
|
+
else
|
|
363
|
+
[spec.to_s, false]
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
152
367
|
def model_or_default_responses(ref, model_list)
|
|
153
368
|
return default_responses unless ref
|
|
154
369
|
|
data/lib/tina4/version.rb
CHANGED