docit 0.5.0 → 0.7.0

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: 85a7407aab5a5fea2aa0c679d469360f124884ebeac2e4479265cbcdd818210d
4
- data.tar.gz: c946eea9c5a664931630b00726f33b8e5bb296c1493e799c67cd905f330cba1d
3
+ metadata.gz: 7fbb7845b4244fcde9b80e6702c17bf6c1e64274b3dada27a7b5a021929819d8
4
+ data.tar.gz: 21c9ab47ab7c6d5423ed965a57297594093590e916b94addc3a446fd58799c1e
5
5
  SHA512:
6
- metadata.gz: 3dd0a62c6d0f138916839a7bcc445f98921abaf9e0ef477473e4dddba67f2033fd29ffb51a4803597d98f0807ee24c53d6b5a90a8d144d87c54669ee51155fef
7
- data.tar.gz: d953ceef06cd91034fa79503a07da386dcefc7cdddabcb3567d1638d6546a2c484bdebd82f263d732a94b89447a9e9f6b659d7008c322d2e238e002aca286a1b
6
+ metadata.gz: e809ab547345f748550f1545f52b87ed2b46f7d4aebda3cd96807d27eae4ff451f5d149d3bdc227a3b8aa6f9bfa8183d3ad234f8f7e216c0c5e5c9a648cf6566
7
+ data.tar.gz: bf6f544ac536d88a42a1256670c257cbc3d0a2cc8425057042a5930315f65a07af5ecb3b45aec75d87178ca2ca909ba4c6a51d05e74b2e35389b981fad858a55
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## [0.7.0] - 2026-06-21
2
+
3
+ ### Added
4
+ - `config.default_security :scheme`: apply a security requirement to every endpoint by default, for APIs that are authenticated by default. Emits a top-level OpenAPI `security` so you no longer repeat `security` on each doc block
5
+ - `security :none` in a doc block: opt an endpoint out of the default (emits an empty `security` array, e.g. for public endpoints)
6
+
7
+ ### Changed
8
+ - The System Map's JavaScript and CSS now live in real asset files (`lib/docit/ui/assets/system.js` and `system.css`) read at load time, instead of large Ruby heredocs — same rendered output, easier to maintain
9
+
10
+ ## [0.6.0] - 2026-06-20
11
+
12
+ ### Added
13
+ - Property options for richer schemas: `default`, `nullable`, `read_only`, and `write_only` on any `property` in a request body, response, or shared schema — emitted as OpenAPI `default`, `nullable`, `readOnly`, and `writeOnly`. Falsy values like `default: false` and `default: 0` are preserved
14
+ - Response headers: declare them inside a `response` block with `header "X-RateLimit-Remaining", type: :integer, description: "..."`, emitted as an OpenAPI `headers` object on that response
15
+ - System Map `uses_schema` edges: documented endpoints that reference a shared schema (`schema ref: :User`) are now linked to that schema node in the graph
16
+ - `TROUBLESHOOTING.md` covering common questions (missing routes, ignored options, AI setup, production exposure)
17
+ - README section on restricting access to the docs endpoints in production
18
+
19
+ ### Changed
20
+ - README documents the new property options and response headers
21
+
22
+ ### Removed
23
+ - The AI "Explain section" / system insights feature and its endpoint (`/api-docs/system/insights`). It was unauthenticated — exposing an outbound, billable AI call and rendering AI output without escaping — so it has been removed. The rest of the System Map (diagram, docs view, search, theming, PNG export) is unchanged, and `rails docit:autodoc` is unaffected
24
+
1
25
  ## [0.5.0] - 2026-05-31
2
26
 
3
27
  ### Added
data/README.md CHANGED
@@ -27,6 +27,8 @@ Decorator-style API documentation for Ruby on Rails. Write OpenAPI 3.0.3 docs wi
27
27
  - [Request bodies](#request-bodies)
28
28
  - [Path parameters](#path-parameters)
29
29
  - [Enums](#enums)
30
+ - [Property options](#property-options)
31
+ - [Response headers](#response-headers)
30
32
  - [Security](#security)
31
33
  - [Deprecated endpoints](#deprecated-endpoints)
32
34
  - [Nested objects and arrays](#nested-objects-and-arrays)
@@ -44,12 +46,14 @@ Decorator-style API documentation for Ruby on Rails. Write OpenAPI 3.0.3 docs wi
44
46
  - [System Map](#system-map)
45
47
  - [How it works](#how-it-works)
46
48
  - [Mounting at a different path](#mounting-at-a-different-path)
49
+ - [Restricting access in production](#restricting-access-in-production)
47
50
  - [JSON spec only](#json-spec-only)
48
51
  - [Development](#development)
49
52
  - [Contributing](#contributing)
50
53
  - [License](#license)
51
54
  - Project docs
52
55
  - [CHANGELOG](CHANGELOG.md)
56
+ - [TROUBLESHOOTING](TROUBLESHOOTING.md)
53
57
  - [CONTRIBUTING](CONTRIBUTING.md)
54
58
  - [CODE OF CONDUCT](CODE_OF_CONDUCT.md)
55
59
 
@@ -302,6 +306,37 @@ doc_for :index do
302
306
  end
303
307
  ```
304
308
 
309
+ ### Property options
310
+
311
+ Any `property` (in a request body, response, or shared schema) accepts these OpenAPI 3.0.3 options:
312
+
313
+ ```ruby
314
+ request_body required: true do
315
+ property :id, type: :integer, read_only: true # readOnly: present in responses, not accepted as input
316
+ property :password, type: :string, write_only: true # writeOnly: accepted as input, never returned
317
+ property :role, type: :string, default: "member" # default value
318
+ property :nickname, type: :string, nullable: true # may be null
319
+ property :active, type: :boolean, default: false # falsy defaults are preserved
320
+ end
321
+ ```
322
+
323
+ These map to `readOnly`, `writeOnly`, `default`, and `nullable` in the generated spec.
324
+
325
+ ### Response headers
326
+
327
+ Document the headers a response returns with `header` inside a `response` block:
328
+
329
+ ```ruby
330
+ response 200, "Orders list" do
331
+ header "X-Total-Count", type: :integer, description: "Total orders available"
332
+ header "X-RateLimit-Remaining", type: :integer, description: "Requests left in the window"
333
+
334
+ property :orders, type: :array do
335
+ property :id, type: :integer
336
+ end
337
+ end
338
+ ```
339
+
305
340
  ### Security
306
341
 
307
342
  Mark endpoints as requiring authentication:
@@ -317,6 +352,28 @@ doc_for :destroy do
317
352
  end
318
353
  ```
319
354
 
355
+ #### Default (global) security
356
+
357
+ If most of your API is authenticated, declare the requirement once instead of on every endpoint. In your initializer:
358
+
359
+ ```ruby
360
+ config.auth :api_key, name: "session", location: "query"
361
+ config.default_security :api_key # applies to every endpoint
362
+ ```
363
+
364
+ Then mark only the **public** endpoints as exceptions with `security :none`:
365
+
366
+ ```ruby
367
+ doc_for :index do
368
+ summary "List banks"
369
+ tags "Banks"
370
+ security :none # public — opts out of the default
371
+ response 200, "OK"
372
+ end
373
+ ```
374
+
375
+ An explicit `security :scheme` on an endpoint still overrides the default.
376
+
320
377
  ### Deprecated endpoints
321
378
 
322
379
  ```ruby
@@ -533,6 +590,34 @@ In `config/routes.rb`:
533
590
  mount Docit::Engine => "/docs" # now at /docs instead of /api-docs
534
591
  ```
535
592
 
593
+ ## Restricting access in production
594
+
595
+ The Docit endpoints (`/api-docs`, `/api-docs/spec`, `/api-docs/system`, `/api-docs/system.json`) are **unauthenticated by default** and expose your full API surface, the OpenAPI spec, and — for the System Map — your app's structure and source file paths. That is convenient in development, but in production you almost always want to gate them.
596
+
597
+ Mount the engine inside an authenticated constraint, or behind whatever auth your app already uses:
598
+
599
+ ```ruby
600
+ # config/routes.rb
601
+
602
+ # Option A — only mount outside production
603
+ mount Docit::Engine => "/api-docs" unless Rails.env.production?
604
+
605
+ # Option B — gate behind an authenticated constraint (e.g. Devise admins)
606
+ authenticate :user, ->(u) { u.admin? } do
607
+ mount Docit::Engine => "/api-docs"
608
+ end
609
+
610
+ # Option C — HTTP Basic auth via a Rack constraint
611
+ admins_only = ->(request) do
612
+ ActiveSupport::SecurityUtils.secure_compare(
613
+ request.authorization.to_s, ENV.fetch("DOCS_AUTH", "")
614
+ )
615
+ end
616
+ constraints(admins_only) { mount Docit::Engine => "/api-docs" }
617
+ ```
618
+
619
+ If you disable the System Map specifically, set `config.system_graph_enabled = false` — `/api-docs/system.json` then returns `404`.
620
+
536
621
  ## JSON spec only
537
622
 
538
623
  If you just want the raw OpenAPI JSON (e.g., for code generation):
@@ -0,0 +1,57 @@
1
+ # Troubleshooting
2
+
3
+ Common questions and fixes when working with Docit.
4
+
5
+ ## My route isn't showing up in the spec
6
+
7
+ Docit only documents an endpoint if **both** are true:
8
+
9
+ 1. The route exists in `config/routes.rb` (visible in `rails routes`).
10
+ 2. A doc is registered for that controller + action — either inline with `doc_for :action do ... end` or via `use_docs SomeDocs`.
11
+
12
+ A route with no matching `doc_for`/`use_docs` is simply absent from `/api-docs/spec`. It will still appear on the **System Map** (`/api-docs/system`) marked `undocumented`, which is the quickest way to see your coverage gaps.
13
+
14
+ Checklist:
15
+
16
+ - Is the action name spelled exactly the same in the route and in `doc_for`?
17
+ - Is the controller actually loaded? Docit eager-loads controllers when generating the spec, but a controller in a non-standard path may be missed.
18
+ - If using `use_docs`, does the doc module define a `doc :action` block for that action?
19
+
20
+ ## A doc_for option seems to be ignored
21
+
22
+ Docit's DSL methods are explicit. If you mistype one (e.g. `summmary` instead of `summary`), the call currently does nothing rather than raising — so the option silently disappears.
23
+
24
+ Double-check the spelling of DSL methods against the [DSL reference in the README](README.md). The supported response/property options include `summary`, `description`, `tags`, `response`, `request_body`, `parameter`, `property` (with `type`, `format`, `example`, `enum`, `default`, `nullable`, `read_only`, `write_only`), `header`, `schema ref:`, `security`, `deprecated`, and `operation_id`.
25
+
26
+ ## A System Map section says "undocumented" or shows low coverage
27
+
28
+ That section's endpoints have no registered docs. Add `doc_for`/`use_docs` for those actions, or accept the gap — coverage is informational, not an error.
29
+
30
+ ## Schemas aren't linked on the System Map
31
+
32
+ `uses_schema` edges only appear when a documented endpoint references a shared schema with `schema ref: :Name` (in its request body or a response), and that schema is defined with `Docit.define_schema :Name`. A plain inline `property` block does not create a schema node.
33
+
34
+ ## I documented an action twice and my changes are ignored
35
+
36
+ Docit's registry keeps the **first** doc registered for a given controller + action and ignores later ones. So if an action is already documented (e.g. via `use_docs SomeDocs`) and you also add an inline `doc_for :same_action do ... end`, the inline block is silently dropped — the `use_docs` version wins.
37
+
38
+ Document each action in exactly one place. If you need to override a shared doc module for one action, change it in the module rather than re-declaring it on the controller.
39
+
40
+ ## AI autodoc / scaffolding isn't working
41
+
42
+ - Run `rails generate docit:ai_setup` once to configure a provider (OpenAI, Anthropic, or Groq). The key is written to `.docit_ai.yml`, which the generator adds to `.gitignore` — **never commit it.**
43
+ - `rails docit:autodoc` sends controller **source code** to your configured provider. In an interactive terminal it asks for confirmation first; in a non-interactive context (CI, piping) it proceeds without prompting, so only run it where sending source is acceptable.
44
+ - Preview without writing files: `DRY_RUN=1 rails docit:autodoc`.
45
+ - Rate-limit / 429 errors are retried with backoff automatically; a persistent failure usually means an invalid key or an unsupported model name.
46
+
47
+ ## The System Map or docs pages are exposed in production
48
+
49
+ The Docit endpoints are **unauthenticated by default**. They expose your API surface, the OpenAPI spec, and your app structure. In production, gate the engine — see [Restricting access in production](README.md#restricting-access-in-production) in the README. To disable the System Map specifically, set `config.system_graph_enabled = false`.
50
+
51
+ ## Spec generation is slow on a large app
52
+
53
+ Generating the spec eager-loads controllers and walks every route. For very large apps this is a one-time cost per request; put the docs behind a cache or a non-production-only mount if it matters. The System Map graph is rebuilt per request — if that becomes a bottleneck, mount it behind auth and access it sparingly.
54
+
55
+ ## Still stuck?
56
+
57
+ Open an issue at <https://github.com/S13G/docit/issues> with your Docit version, Rails version, and a minimal `doc_for` / route example.
@@ -21,26 +21,12 @@ module Docit
21
21
  end
22
22
 
23
23
  def system_spec
24
- unless Docit.configuration.system_graph_enabled
25
- return render(json: { error: "System graph disabled" }, status: :not_found)
26
- end
24
+ return render(json: { error: "System graph disabled" }, status: :not_found) unless Docit.configuration.system_graph_enabled
27
25
 
28
26
  RouteInspector.eager_load_controllers!
29
27
  render json: SystemGraph::Generator.generate
30
28
  end
31
29
 
32
- def system_insights
33
- graph = system_graph
34
- insight = Ai::SystemInsightGenerator.new(
35
- graph: graph,
36
- selected_node_ids: selected_node_ids,
37
- mode: insight_mode
38
- ).generate
39
- render json: { insight: insight }
40
- rescue Docit::Error => e
41
- render json: { error: e.message }, status: :unprocessable_entity
42
- end
43
-
44
30
  def spec
45
31
  RouteInspector.eager_load_controllers!
46
32
  render json: SchemaGenerator.generate
@@ -58,27 +44,11 @@ module Docit
58
44
  renderer = RENDERERS.fetch(ui_name).new(
59
45
  spec_url: spec_url,
60
46
  system_url: system_url,
61
- system_insights_url: system_insights_url,
62
47
  nav_paths: nav_paths
63
48
  )
64
49
  render html: renderer.render.html_safe, layout: false
65
50
  end
66
51
 
67
- def system_graph
68
- RouteInspector.eager_load_controllers!
69
- SystemGraph::Generator.generate
70
- end
71
-
72
- def selected_node_ids
73
- params.fetch(:node_ids, "").to_s.split(",").reject(&:empty?)
74
- end
75
-
76
- # Validate at the boundary: only the known modes are honored, anything
77
- # else falls back to the safe per-node default.
78
- def insight_mode
79
- params[:mode].to_s == "section" ? :section : :nodes
80
- end
81
-
82
52
  def spec_url
83
53
  "#{request.base_url}#{Docit::Engine.routes.url_helpers.spec_path}"
84
54
  end
@@ -87,10 +57,6 @@ module Docit
87
57
  "#{request.base_url}#{Docit::Engine.routes.url_helpers.system_spec_path}"
88
58
  end
89
59
 
90
- def system_insights_url
91
- "#{request.base_url}#{Docit::Engine.routes.url_helpers.system_insights_path}"
92
- end
93
-
94
60
  def nav_paths
95
61
  helpers = Docit::Engine.routes.url_helpers
96
62
  { swagger: helpers.swagger_path, scalar: helpers.scalar_path, system: helpers.system_path }
data/config/routes.rb CHANGED
@@ -5,7 +5,6 @@ Docit::Engine.routes.draw do
5
5
  get "swagger", to: "ui#swagger"
6
6
  get "scalar", to: "ui#scalar"
7
7
  get "system.json", to: "ui#system_spec", defaults: { format: :json }, as: :system_spec
8
- get "system/insights", to: "ui#system_insights", defaults: { format: :json }, as: :system_insights
9
8
  get "system", to: "ui#system", as: :system
10
9
  get "spec", to: "ui#spec", defaults: { format: :json }
11
10
  end
Binary file
Binary file
@@ -57,9 +57,7 @@ module Docit
57
57
  end
58
58
 
59
59
  initializer = Rails.root.join("config", "initializers", "docit.rb")
60
- if File.exist?(initializer) == false
61
- raise Docit::Error, "Docit is not installed. Run: rails generate docit:install"
62
- end
60
+ raise Docit::Error, "Docit is not installed. Run: rails generate docit:install" if File.exist?(initializer) == false
63
61
 
64
62
  routes_file = Rails.root.join("config", "routes.rb")
65
63
  return unless File.exist?(routes_file) && !File.read(routes_file).include?("Docit::Engine")
@@ -179,8 +177,15 @@ module Docit
179
177
  end
180
178
  end
181
179
 
180
+ endpoints = pluralize(@results[:generated], "endpoint")
181
+ files = pluralize(@results[:files].length, "file")
182
182
  @output.puts ""
183
- @output.puts "Generated docs for #{@results[:generated]} endpoint#{"s" if @results[:generated] > 1} in #{@results[:files].length} file#{"s" if @results[:files].length > 1}."
183
+ @output.puts "Generated docs for #{endpoints} in #{files}."
184
+ end
185
+
186
+ # "1 endpoint" / "2 endpoints" — keeps the summary lines short and readable.
187
+ def pluralize(count, noun)
188
+ "#{count} #{noun}#{"s" unless count == 1}"
184
189
  end
185
190
 
186
191
  def inject_tags(generated)
@@ -20,7 +20,7 @@ module Docit
20
20
  end
21
21
 
22
22
  def doc_module_name
23
- @controller_name.delete_suffix("Controller").gsub("::", "::") + "Docs"
23
+ "#{@controller_name.delete_suffix("Controller").gsub("::", "::")}Docs"
24
24
  end
25
25
 
26
26
  def file_exists?
@@ -44,7 +44,7 @@ module Docit
44
44
  match = content.match(class_pattern)
45
45
  return false if match.nil?
46
46
 
47
- indent = match[1][/^\s*/] + " "
47
+ indent = "#{match[1][/^\s*/]} "
48
48
  use_docs_line = "#{indent}use_docs #{doc_module_name}\n"
49
49
  content = content.sub(class_pattern, "\\1\n#{use_docs_line}")
50
50
 
@@ -100,7 +100,7 @@ module Docit
100
100
  lines << "#{" " * (@module_parts.length - 1 - i)}end"
101
101
  end
102
102
 
103
- lines.join("\n") + "\n"
103
+ "#{lines.join("\n")}\n"
104
104
  end
105
105
 
106
106
  def indent_block(block, depth)
@@ -59,18 +59,14 @@ module Docit
59
59
 
60
60
  def parse_retry_after(response, message)
61
61
  # Check Retry-After header first (seconds)
62
- if (header = response["Retry-After"])
63
- return header.to_f if header.to_f > 0
62
+ if (header = response["Retry-After"]) && header.to_f.positive?
63
+ return header.to_f
64
64
  end
65
65
 
66
66
  # Parse "try again in XmY.Zs" from error message
67
- if message =~ /(\d+)m([\d.]+)s/
68
- return (Regexp.last_match(1).to_i * 60) + Regexp.last_match(2).to_f
69
- end
67
+ return (Regexp.last_match(1).to_i * 60) + Regexp.last_match(2).to_f if message =~ /(\d+)m([\d.]+)s/
70
68
 
71
- if message =~ /([\d.]+)s/
72
- return Regexp.last_match(1).to_f
73
- end
69
+ return Regexp.last_match(1).to_f if message =~ /([\d.]+)s/
74
70
 
75
71
  nil
76
72
  end
@@ -50,8 +50,10 @@ module Docit
50
50
 
51
51
  inject_tags(grouped)
52
52
 
53
+ endpoints = "endpoint".pluralize(gaps.length)
54
+ files = "file".pluralize(@files_written.length)
53
55
  @output.puts ""
54
- @output.puts "Scaffolded #{gaps.length} endpoint#{"s" if gaps.length > 1} in #{@files_written.length} file#{"s" if @files_written.length > 1}."
56
+ @output.puts "Scaffolded #{gaps.length} #{endpoints} in #{@files_written.length} #{files}."
55
57
  @output.puts "Fill in the TODO placeholders in your doc files."
56
58
  @files_written
57
59
  end
@@ -24,7 +24,7 @@ module Docit
24
24
  insertion_point = find_insertion_point(content)
25
25
  return [] if insertion_point.nil?
26
26
 
27
- content = content.insert(insertion_point, "\n#{lines.join("\n")}")
27
+ content.insert(insertion_point, "\n#{lines.join("\n")}")
28
28
  File.write(initializer_path, content)
29
29
 
30
30
  new_tags
data/lib/docit/ai.rb CHANGED
@@ -12,4 +12,3 @@ require_relative "ai/doc_writer"
12
12
  require_relative "ai/tag_injector"
13
13
  require_relative "ai/autodoc_runner"
14
14
  require_relative "ai/scaffold_generator"
15
- require_relative "ai/system_insight_generator"
@@ -3,15 +3,16 @@
3
3
  module Docit
4
4
  module Builders
5
5
  # Builds the schema for a single HTTP response, including properties,
6
- # examples, and schema references.
6
+ # examples, headers, and schema references.
7
7
  class ResponseBuilder
8
- attr_reader :status, :description, :properties, :examples, :schema_ref
8
+ attr_reader :status, :description, :properties, :examples, :headers, :schema_ref
9
9
 
10
10
  def initialize(status:, description:)
11
11
  @status = status
12
12
  @description = description
13
13
  @properties = []
14
14
  @examples = []
15
+ @headers = []
15
16
  @schema_ref = nil
16
17
  end
17
18
 
@@ -19,6 +20,15 @@ module Docit
19
20
  @schema_ref = ref.to_sym
20
21
  end
21
22
 
23
+ # Declares a response header, e.g. a rate-limit or pagination header:
24
+ # header "X-RateLimit-Remaining", type: :integer, description: "..."
25
+ def header(name, type: :string, description: nil, example: nil)
26
+ entry = { name: name.to_s, type: type.to_s }
27
+ entry[:description] = description if description
28
+ entry[:example] = example unless example.nil?
29
+ @headers << entry
30
+ end
31
+
22
32
  def property(name, type:, format: nil, example: nil, enum: nil, description: nil, items: nil, **opts, &block)
23
33
  prop = { name: name, type: type }
24
34
  prop[:format] = format if format
@@ -18,6 +18,7 @@ module Docit
18
18
  @system_graph_excluded_paths = []
19
19
  @default_ui = :scalar
20
20
  @security_schemes = {}
21
+ @default_security = nil
21
22
  @tags = []
22
23
  @servers = []
23
24
  @license = nil
@@ -27,9 +28,7 @@ module Docit
27
28
 
28
29
  def default_ui=(value)
29
30
  ui = value.to_sym
30
- unless SUPPORTED_UIS.include?(ui)
31
- raise ArgumentError, "Unsupported UI: #{value}. Must be one of: #{SUPPORTED_UIS.join(", ")}"
32
- end
31
+ raise ArgumentError, "Unsupported UI: #{value}. Must be one of: #{SUPPORTED_UIS.join(", ")}" unless SUPPORTED_UIS.include?(ui)
33
32
 
34
33
  @default_ui = ui
35
34
  end
@@ -62,6 +61,17 @@ module Docit
62
61
  @security_schemes.dup
63
62
  end
64
63
 
64
+ # Apply one or more security schemes to every endpoint by default. Individual
65
+ # endpoints opt out with `security :none` in their doc block.
66
+ def default_security(*schemes)
67
+ @default_security = schemes.flatten.map(&:to_sym) unless schemes.empty?
68
+ @default_security
69
+ end
70
+
71
+ def default_security?
72
+ !@default_security.nil? && !@default_security.empty?
73
+ end
74
+
65
75
  def tag(name, description: nil)
66
76
  entry = { name: name.to_s }
67
77
  entry[:description] = description if description
@@ -19,6 +19,10 @@ module Docit
19
19
  }
20
20
  }
21
21
 
22
+ # Top-level security applies to every operation unless an operation
23
+ # overrides it (a documented scheme, or `security :none` to opt out).
24
+ spec[:security] = config.default_security.map { |s| { s.to_s => [] } } if config.default_security?
25
+
22
26
  tag_defs = config.tags
23
27
  spec[:tags] = tag_defs if tag_defs.any?
24
28
 
@@ -78,11 +82,19 @@ module Docit
78
82
 
79
83
  result[:requestBody] = build_request_body(operation._request_body) if operation._request_body
80
84
  result[:deprecated] = true if operation._deprecated
81
- result[:security] = operation._security.map { |s| { s.to_s => [] } } if operation._security.any?
85
+ security = build_operation_security(operation)
86
+ result[:security] = security unless security.nil?
82
87
 
83
88
  result
84
89
  end
85
90
 
91
+ def build_operation_security(operation)
92
+ return nil if operation._security.empty?
93
+ return [] if operation._security.map(&:to_sym) == [:none]
94
+
95
+ operation._security.reject { |s| s.to_sym == :none }.map { |s| { s.to_s => [] } }
96
+ end
97
+
86
98
  def build_responses(response_builders)
87
99
  return { "200" => { description: "Success" } } if response_builders.empty?
88
100
 
@@ -104,10 +116,21 @@ module Docit
104
116
  entry[:content] = content
105
117
  end
106
118
 
119
+ entry[:headers] = build_response_headers(builder.headers) if builder.headers.any?
120
+
107
121
  hash[builder.status.to_s] = entry
108
122
  end
109
123
  end
110
124
 
125
+ def build_response_headers(headers)
126
+ headers.each_with_object({}) do |header, hash|
127
+ schema = { schema: { type: header[:type] } }
128
+ schema[:description] = header[:description] if header[:description]
129
+ schema[:schema][:example] = header[:example] if header.key?(:example)
130
+ hash[header[:name]] = schema
131
+ end
132
+ end
133
+
111
134
  def build_request_body(request_body_builder)
112
135
  if request_body_builder.schema_ref
113
136
  schema = { "$ref" => "#/components/schemas/#{request_body_builder.schema_ref}" }
@@ -138,43 +161,50 @@ module Docit
138
161
 
139
162
  def build_property_schema(prop)
140
163
  type = prop[:type].to_s
164
+ schema = base_property_schema(type, prop)
165
+
166
+ # These OpenAPI fields apply to any property shape, so set them once here
167
+ # rather than in each branch above. nil-checks let `false`/`0` through as
168
+ # explicit values (e.g. default: false), but skip unset options.
169
+ schema[:description] = prop[:description] if prop[:description]
170
+ schema[:example] = prop[:example] if prop[:example]
171
+ schema[:default] = prop[:default] unless prop[:default].nil?
172
+ schema[:nullable] = prop[:nullable] unless prop[:nullable].nil?
173
+ schema[:readOnly] = prop[:read_only] unless prop[:read_only].nil?
174
+ schema[:writeOnly] = prop[:write_only] unless prop[:write_only].nil?
175
+ schema
176
+ end
141
177
 
142
- if type == "file"
143
- schema = { type: "string", format: "binary" }
144
- schema[:description] = prop[:description] if prop[:description]
145
- schema
146
- elsif type == "array"
147
- schema = { type: "array" }
148
- if prop[:children]
149
- schema[:items] = {
150
- type: "object",
151
- properties: build_properties(prop[:children])
152
- }
178
+ def base_property_schema(type, prop)
179
+ case type
180
+ when "file"
181
+ { type: "string", format: "binary" }
182
+ when "array"
183
+ { type: "array", items: array_items_schema(prop) }
184
+ else
185
+ # An object with declared children becomes a nested schema; everything
186
+ # else (including a childless "object") is a scalar with type/format/enum.
187
+ if type == "object" && prop[:children]
188
+ { type: "object", properties: build_properties(prop[:children]) }
153
189
  else
154
- items_type = prop[:items]&.to_s || "string"
155
- schema[:items] = { type: items_type }
190
+ scalar_property_schema(type, prop)
156
191
  end
157
- schema[:description] = prop[:description] if prop[:description]
158
- schema[:example] = prop[:example] if prop[:example]
159
- schema
160
- elsif type == "object" && prop[:children]
161
- schema = {
162
- type: "object",
163
- properties: build_properties(prop[:children])
164
- }
165
- schema[:description] = prop[:description] if prop[:description]
166
- schema[:example] = prop[:example] if prop[:example]
167
- schema
168
- else
169
- schema = { type: type }
170
- schema[:format] = prop[:format].to_s if prop[:format]
171
- schema[:enum] = prop[:enum] if prop[:enum]
172
- schema[:example] = prop[:example] if prop[:example]
173
- schema[:description] = prop[:description] if prop[:description]
174
- schema
175
192
  end
176
193
  end
177
194
 
195
+ def array_items_schema(prop)
196
+ return { type: "object", properties: build_properties(prop[:children]) } if prop[:children]
197
+
198
+ { type: prop[:items]&.to_s || "string" }
199
+ end
200
+
201
+ def scalar_property_schema(type, prop)
202
+ schema = { type: type }
203
+ schema[:format] = prop[:format].to_s if prop[:format]
204
+ schema[:enum] = prop[:enum] if prop[:enum]
205
+ schema
206
+ end
207
+
178
208
  def build_component_schemas
179
209
  Docit.schemas.each_with_object({}) do |(name, definition), hash|
180
210
  schema = {