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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74de2870c01871240bb98d6ceeb66cd5bc5bf05c6e1c4e82d19a198d585d83e6
4
- data.tar.gz: 9d548edf025f53d6547216f3a421d9215eb893cfda89131c342910ebc2c820bb
3
+ metadata.gz: fdfe0b3f854e7ecea3419e6c10efe9e527be73952c8d570fb46106e0476255ec
4
+ data.tar.gz: 1c63029ec5a6e61fb91c7add6377735f6a8b740d0a416826333d400f493a623a
5
5
  SHA512:
6
- metadata.gz: 3d2db7a1fc7dfcb95848dbc80c4f76664800bd6692a460667f5dfcbb59cb59fcba101737773c4eda4b4731275f735b4ef494164388869ded0849d29bb6de3804
7
- data.tar.gz: 49e9c4742313a45ebb610cc5840c1b277915db9b1a5eb3a98dcb5b680a93d336b5b1d846698a9ec9121d2d722c905b8e0261b2532d050d0d7805b27a6c2c5228
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), tags used (-> top-level tags[]), seen
12
- # operationIds (de-dup OpenAPI requires them unique).
13
- ctx = { models: {}, used_tags: [], seen_ids: [] }
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" => "3.0.3",
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[:responses] || model_or_default_responses(ref, meta[:model_list])
238
+ "responses" => build_responses(meta, ref, ctx)
128
239
  }
129
240
 
130
241
  operation["deprecated"] = true if meta[:deprecated]
131
242
 
132
- if route.auth_handler
133
- operation["security"] = [{ "bearerAuth" => [] }]
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
- def build_request_body(_method, meta, ref)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.41"
4
+ VERSION = "3.13.42"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.41
4
+ version: 3.13.42
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team