docit 0.2.0 → 0.3.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: cb189284c9d9b8ad90f9d39fccaea0265223c3be58009b95b51b0dfbc12a3d78
4
- data.tar.gz: 5bd6b3277e7686470f1c24bc2824b2cd9ec97ebc785d9e19fefe9fafe7a2be1d
3
+ metadata.gz: 19ca1ea84419aa3ec1c1605fec67c6355027d866de1b9e6b0d8fb27472b91602
4
+ data.tar.gz: a8fc557d32361a3729a9133791100bea3c7bf94e4c2f0ca55351eff4760efafe
5
5
  SHA512:
6
- metadata.gz: f921f704eb18446ecc475377d7fa8f0604c34349182139fa79ac26dd76260e04028b0b4443ca300e877298c96423ac5d8cc04b5a0887c6fa379a2752de8d6c21
7
- data.tar.gz: 1b6fbfc6aae77f31e74700243535e12be9c6caae790e87614903e7538e769746d774e4d31899fc3617fee01896f0f2a7e2a489fa0026d35e08cc5afd602e7c13
6
+ metadata.gz: 4614b2cc382920b578400ddbccb8afb0eb8a32830307e0fd75fc3a175678f218d7f658e82b61678422d520c6f6c841fa5643d36d563b6cacf0bd4233bbed655f
7
+ data.tar.gz: fe46a50539ba08a2783b8e1f4d95a2e313fc1a043f78dee017ad4dd47cba8166f5cb8341bf5cbf732b4e7f7a776e9966e12efcce4c1a1b3a3b2c08cf2887acf1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-04-16
4
+
5
+ ### Added
6
+ - Scalar API Reference as a second documentation UI alongside Swagger UI
7
+ - New routes: `/api-docs/scalar` and `/api-docs/swagger` for direct access to each UI
8
+ - `config.default_ui` option (`:scalar` or `:swagger`) to control which UI renders at the root `/api-docs` path
9
+ - Navigation bar across both UIs for one-click switching between Scalar and Swagger
10
+ - Modular renderer architecture (`Docit::UI::BaseRenderer`, `SwaggerRenderer`, `ScalarRenderer`) for easy extension
11
+
12
+ ### Changed
13
+ - Default documentation UI is now Scalar (previously Swagger UI)
14
+ - `config.description` now defaults to a welcome message instead of an empty string
15
+ - Install generator template now documents the `default_ui` option
16
+ - UI controller refactored from monolithic HTML generation to thin dispatcher with pluggable renderers
17
+
18
+ ## [0.2.1] - 2026-04-11
19
+
20
+ ### Fixed
21
+ - Engine autoloading: `Docit::Engine` is now properly required when Rails is present, fixing `uninitialized constant Docit::Engine` and `Could not find generator 'docit:install'` in consuming apps
22
+ - AI provider clients (OpenAI, Anthropic, Groq) now raise `Docit::Ai::RateLimitError` on 429 responses with parsed retry-after timing
23
+ - `AutodocRunner` now retries rate-limited requests up to 3 times with exponential backoff (capped at 5 minutes) instead of failing immediately
24
+
3
25
  ## [0.2.0] - 2026-04-11
4
26
 
5
27
  ### Added
@@ -30,7 +52,7 @@
30
52
 
31
53
  - Initial release
32
54
  - DSL: `swagger_doc` macro for inline controller documentation
33
- - DSL: `use_docs` + `Docit::DocFile` for separate doc files (drf-spectacular style)
55
+ - DSL: `use_docs` + `Docit::DocFile` for separate doc files
34
56
  - Builders: request body, response, and parameter builders with nested object/array support
35
57
  - Schema `$ref` components via `Docit.define_schema`
36
58
  - File upload support (`type: :file` → `string/binary`)
data/README.md CHANGED
@@ -5,7 +5,44 @@
5
5
 
6
6
  Decorator-style API documentation for Ruby on Rails. Write OpenAPI 3.0.3 docs with clean controller DSL macros, separate doc modules, or AI-assisted scaffolding for undocumented endpoints.
7
7
 
8
- Inspired by [drf-spectacular](https://github.com/tfranzel/drf-spectacular) for Django REST Framework.
8
+ ## Table Of Contents
9
+
10
+ - Getting started
11
+ - [Installation](#installation)
12
+ - [Configuration](#configuration)
13
+ - [Usage](#usage)
14
+ - Documentation styles
15
+ - [Style 1: Inline (simple APIs)](#style-1-inline-simple-apis)
16
+ - [Style 2: Separate doc files (recommended for larger APIs)](#style-2-separate-doc-files-recommended-for-larger-apis)
17
+ - Endpoint DSL reference
18
+ - [Endpoint documentation DSL](#endpoint-documentation-dsl)
19
+ - [Request bodies](#request-bodies)
20
+ - [Path parameters](#path-parameters)
21
+ - [Enums](#enums)
22
+ - [Security](#security)
23
+ - [Deprecated endpoints](#deprecated-endpoints)
24
+ - [Nested objects and arrays](#nested-objects-and-arrays)
25
+ - [Response examples](#response-examples)
26
+ - [Shared schemas (`$ref`)](#shared-schemas-ref)
27
+ - [File uploads](#file-uploads)
28
+ - AI documentation
29
+ - [AI Automatic Documentation](#ai-automatic-documentation)
30
+ - [Quick start (included in install)](#quick-start-included-in-install)
31
+ - [Standalone commands](#standalone-commands)
32
+ - [Supported providers](#supported-providers)
33
+ - [What the AI generates](#what-the-ai-generates)
34
+ - Runtime and development
35
+ - [Documentation UIs](#documentation-uis)
36
+ - [How it works](#how-it-works)
37
+ - [Mounting at a different path](#mounting-at-a-different-path)
38
+ - [JSON spec only](#json-spec-only)
39
+ - [Development](#development)
40
+ - [Contributing](#contributing)
41
+ - [License](#license)
42
+ - Project docs
43
+ - [CHANGELOG](CHANGELOG.md)
44
+ - [CONTRIBUTING](CONTRIBUTING.md)
45
+ - [CODE OF CONDUCT](CODE_OF_CONDUCT.md)
9
46
 
10
47
  ## Installation
11
48
 
@@ -25,13 +62,13 @@ rails generate docit:install
25
62
  The install generator does everything in one step:
26
63
 
27
64
  1. Creates `config/initializers/docit.rb` with default settings
28
- 2. Mounts the Swagger UI engine at `/api-docs` in your routes
65
+ 2. Mounts the documentation engine at `/api-docs` in your routes
29
66
  3. Asks how you'd like to set up your docs:
30
67
  - **AI automatic docs** — configure an AI provider, then Docit scans your routes and generates complete documentation for every endpoint
31
68
  - **Manual docs** — Docit scans your routes and creates scaffolded doc files with TODO placeholders, injects `use_docs` into controllers, and lets you fill in the details
32
69
  - **Skip** — just install the base config and set up docs later
33
70
 
34
- Visit `/api-docs` to see your interactive API documentation.
71
+ Visit `/api-docs` to see your interactive API documentation (Scalar by default, Swagger UI also available at `/api-docs/swagger`).
35
72
 
36
73
  If you choose AI setup, Docit stores your provider config in `.docit_ai.yml` with restricted file permissions and adds that file to `.gitignore` when possible.
37
74
 
@@ -45,17 +82,20 @@ Docit.configure do |config|
45
82
  config.version = "1.0.0"
46
83
  config.description = "Backend API documentation"
47
84
 
85
+ # Documentation UI: :scalar (default) or :swagger
86
+ config.default_ui = :scalar
87
+
48
88
  # Authentication: pick one (or multiple):
49
89
  config.auth :bearer # Bearer token (JWT by default)
50
90
  config.auth :basic # HTTP Basic
51
91
  config.auth :api_key, name: "X-API-Key", # API key in header
52
92
  location: "header"
53
93
 
54
- # Tag descriptions (shown in Swagger UI sidebar):
94
+ # Tag descriptions (shown in the documentation sidebar):
55
95
  config.tag "Users", description: "User account management"
56
96
  config.tag "Auth", description: "Authentication endpoints"
57
97
 
58
- # Server URLs (shown in Swagger UI server dropdown):
98
+ # Server URLs (shown in the server dropdown):
59
99
  config.server "https://api.example.com", description: "Production"
60
100
  config.server "https://staging.example.com", description: "Staging"
61
101
  config.server "http://localhost:3000", description: "Development"
@@ -85,7 +125,7 @@ end
85
125
 
86
126
  ### Style 2: Separate doc files (recommended for larger APIs)
87
127
 
88
- Keep controllers clean by defining docs in dedicated files, just like drf-spectacular:
128
+ Keep controllers clean by defining docs in dedicated files:
89
129
 
90
130
  ```ruby
91
131
  # app/docs/api/v1/users_docs.rb
@@ -414,6 +454,8 @@ DRY_RUN=1 rails docit:autodoc
414
454
  | Anthropic | Requires API key from console.anthropic.com |
415
455
  | Groq | Free tier at console.groq.com |
416
456
 
457
+ All providers automatically retry on rate-limit (429) errors with exponential backoff, so free-tier usage works out of the box.
458
+
417
459
  AI configuration is stored in `.docit_ai.yml`.
418
460
  If your app does not have a `.gitignore`, add `.docit_ai.yml` manually.
419
461
 
@@ -429,11 +471,24 @@ For each undocumented endpoint, Docit:
429
471
 
430
472
  Do not use AI autodoc on controllers that contain secrets, proprietary business rules, or internal comments you do not want sent to an external provider.
431
473
 
474
+ ## Documentation UIs
475
+
476
+ Docit ships with two documentation UIs, both reading from the same OpenAPI spec:
477
+
478
+ | Path | UI | Notes |
479
+ |------|------|-------|
480
+ | `/api-docs` | Default (Scalar) | Configurable via `config.default_ui` |
481
+ | `/api-docs/scalar` | Scalar API Reference | Modern UI with built-in API client, dark mode, code samples |
482
+ | `/api-docs/swagger` | Swagger UI | Classic OpenAPI explorer |
483
+ | `/api-docs/spec` | Raw JSON | OpenAPI 3.0.3 spec |
484
+
485
+ Both UIs include a navigation bar to switch between them. Set `config.default_ui = :swagger` to make Swagger the default at `/api-docs`.
486
+
432
487
  ## How it works
433
488
 
434
489
  1. `swagger_doc` registers an **Operation** for each controller action in a global **Registry**
435
490
  2. When someone visits `/api-docs/spec`, Docit's **SchemaGenerator** combines all registered operations with your Rails routes (via **RouteInspector**) to produce an OpenAPI 3.0.3 JSON document
436
- 3. The **Engine** serves Swagger UI at `/api-docs`, pointing it at the generated spec
491
+ 3. The **Engine** serves the configured documentation UI at `/api-docs`, pointing it at the generated spec
437
492
 
438
493
  The DSL is included in all controllers automatically via a Rails Engine initializer — no manual `include` needed if you're using `ActionController::API` or `ActionController::Base`.
439
494
 
@@ -4,10 +4,16 @@ require "json"
4
4
 
5
5
  module Docit
6
6
  class UiController < ActionController::Base
7
- SWAGGER_UI_VERSION = "5.32.2"
8
-
9
7
  def index
10
- render html: swagger_ui_html.html_safe, layout: false
8
+ render_ui(Docit.configuration.default_ui)
9
+ end
10
+
11
+ def swagger
12
+ render_ui(:swagger)
13
+ end
14
+
15
+ def scalar
16
+ render_ui(:scalar)
11
17
  end
12
18
 
13
19
  def spec
@@ -17,52 +23,23 @@ module Docit
17
23
 
18
24
  private
19
25
 
20
- def swagger_ui_html
21
- spec_url = "#{request.base_url}#{Docit::Engine.routes.url_helpers.spec_path}"
22
- spec_url_json = JSON.generate(spec_url)
23
- escaped_title = ERB::Util.html_escape(Docit.configuration.title)
26
+ RENDERERS = {
27
+ swagger: UI::SwaggerRenderer,
28
+ scalar: UI::ScalarRenderer
29
+ }.freeze
30
+
31
+ def render_ui(ui_name)
32
+ renderer = RENDERERS.fetch(ui_name).new(spec_url: spec_url, nav_paths: nav_paths)
33
+ render html: renderer.render.html_safe, layout: false
34
+ end
35
+
36
+ def spec_url
37
+ "#{request.base_url}#{Docit::Engine.routes.url_helpers.spec_path}"
38
+ end
24
39
 
25
- <<~HTML
26
- <!DOCTYPE html>
27
- <html lang="en">
28
- <head>
29
- <meta charset="UTF-8">
30
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
31
- <title>#{escaped_title}</title>
32
- <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@#{SWAGGER_UI_VERSION}/swagger-ui.css" />
33
- <style>
34
- html { box-sizing: border-box; overflow-y: scroll; }
35
- *, *:before, *:after { box-sizing: inherit; }
36
- body { margin: 0; background: #fafafa; }
37
- </style>
38
- </head>
39
- <body>
40
- <div id="swagger-ui"></div>
41
- <script src="https://unpkg.com/swagger-ui-dist@#{SWAGGER_UI_VERSION}/swagger-ui-bundle.js"></script>
42
- <script>
43
- SwaggerUIBundle({
44
- url: #{spec_url_json},
45
- dom_id: '#swagger-ui',
46
- presets: [
47
- SwaggerUIBundle.presets.apis,
48
- SwaggerUIBundle.SwaggerUIStandalonePreset
49
- ],
50
- layout: "BaseLayout",
51
- deepLinking: true,
52
- showExtensions: true,
53
- showCommonExtensions: true,
54
- requestInterceptor: function(req) {
55
- var url = new URL(req.url);
56
- url.protocol = window.location.protocol;
57
- url.host = window.location.host;
58
- req.url = url.toString();
59
- return req;
60
- }
61
- })
62
- </script>
63
- </body>
64
- </html>
65
- HTML
40
+ def nav_paths
41
+ helpers = Docit::Engine.routes.url_helpers
42
+ { swagger: helpers.swagger_path, scalar: helpers.scalar_path }
66
43
  end
67
44
  end
68
45
  end
data/config/routes.rb CHANGED
@@ -2,5 +2,7 @@
2
2
 
3
3
  Docit::Engine.routes.draw do
4
4
  root to: "ui#index"
5
+ get "swagger", to: "ui#swagger"
6
+ get "scalar", to: "ui#scalar"
5
7
  get "spec", to: "ui#spec", defaults: { format: :json }
6
8
  end
@@ -41,6 +41,12 @@ module Docit
41
41
 
42
42
  if response.is_a?(Net::HTTPSuccess) == false
43
43
  message = body.dig("error", "message") || "Unknown API error"
44
+
45
+ if response.code == "429"
46
+ retry_after = response["Retry-After"]&.to_f
47
+ raise RateLimitError.new("Anthropic rate limit exceeded", retry_after: retry_after)
48
+ end
49
+
44
50
  raise Error, "Anthropic API error (#{response.code}): #{message}"
45
51
  end
46
52
 
@@ -3,6 +3,8 @@
3
3
  module Docit
4
4
  module Ai
5
5
  class AutodocRunner
6
+ MAX_INVALID_OUTPUT_RETRIES = 2
7
+
6
8
  attr_reader :results
7
9
 
8
10
  def initialize(controller_filter: nil, dry_run: false, input: $stdin, output: $stdout)
@@ -50,7 +52,7 @@ module Docit
50
52
  end
51
53
 
52
54
  def check_base_setup!
53
- if !(defined?(Rails) && Rails.respond_to?(:root) && Rails.root)
55
+ if defined?(Rails) == false || Rails.respond_to?(:root) == false || Rails.root.nil?
54
56
  raise Docit::Error, "Docit requires a Rails application. Run this command from your app root."
55
57
  end
56
58
 
@@ -82,20 +84,20 @@ module Docit
82
84
  @output.puts "Docit will send controller source code to #{config.provider.capitalize} to generate documentation."
83
85
  @output.puts "Review the endpoints first if they contain secrets or proprietary logic."
84
86
 
85
- return if !(@input.respond_to?(:tty?) && @input.tty?)
86
-
87
- loop do
88
- @output.print "Continue? (y/n): "
89
- choice = @input.gets.to_s.strip.downcase
90
-
91
- case choice
92
- when "y", "yes"
93
- @output.puts ""
94
- return
95
- when "n", "no"
96
- raise Docit::Error, "Autodoc cancelled."
97
- else
98
- @output.puts "Please enter y or n."
87
+ if @input.respond_to?(:tty?) && @input.tty?
88
+ loop do
89
+ @output.print "Continue? (y/n): "
90
+ choice = @input.gets.to_s.strip.downcase
91
+
92
+ case choice
93
+ when "y", "yes"
94
+ @output.puts ""
95
+ return
96
+ when "n", "no"
97
+ raise Docit::Error, "Autodoc cancelled."
98
+ else
99
+ @output.puts "Please enter y or n."
100
+ end
99
101
  end
100
102
  end
101
103
  end
@@ -103,6 +105,7 @@ module Docit
103
105
  def generate_docs(gaps, config)
104
106
  client = Client.for(config)
105
107
  generated = Hash.new { |h, k| h[k] = [] }
108
+ max_retries = 3
106
109
 
107
110
  @output.puts "Generating documentation............."
108
111
 
@@ -114,16 +117,45 @@ module Docit
114
117
  @output.puts " skipped (controller source file not found)"
115
118
  next
116
119
  end
117
-
118
- prompt = builder.build
119
- doc_block = client.generate(prompt).strip
120
- doc_block = strip_markdown_fences(doc_block)
121
-
122
- generated[gap[:controller]] << doc_block
123
- @results[:generated] += 1
124
- @output.puts " done"
125
- rescue Docit::Ai::Error => e
126
- @output.puts " failed (#{e.message})"
120
+ retries = 0
121
+ invalid_output_retries = 0
122
+ validation_error = nil
123
+
124
+ begin
125
+ prompt = builder.build(validation_error: validation_error)
126
+ doc_block = client.generate(prompt).strip
127
+ doc_block = strip_markdown_fences(doc_block)
128
+ DocBlockValidator.new(
129
+ controller: gap[:controller],
130
+ action: gap[:action],
131
+ doc_block: doc_block
132
+ ).validate!
133
+
134
+ generated[gap[:controller]] << doc_block
135
+ @results[:generated] += 1
136
+ @output.puts " done"
137
+ rescue Docit::Ai::InvalidDocBlockError => e
138
+ invalid_output_retries += 1
139
+ if invalid_output_retries <= MAX_INVALID_OUTPUT_RETRIES
140
+ validation_error = e.message
141
+ retry
142
+ end
143
+
144
+ @output.puts " failed (invalid doc DSL: #{e.message})"
145
+ rescue Docit::Ai::RateLimitError => e
146
+ retries += 1
147
+ if retries <= max_retries
148
+ wait = e.retry_after || (2**retries * 10)
149
+ wait = [wait, 300].min # cap at 5 minutes
150
+ @output.puts " rate limited, waiting #{wait.round}s (attempt #{retries}/#{max_retries})..."
151
+ sleep(wait)
152
+ retry
153
+ else
154
+ @output.puts " failed (rate limit exceeded after #{max_retries} retries)"
155
+ end
156
+ rescue Docit::Ai::Error => e
157
+ @output.puts " failed (#{e.message})"
158
+ end
127
159
  end
128
160
 
129
161
  generated
@@ -152,11 +184,11 @@ module Docit
152
184
 
153
185
  def inject_tags(generated)
154
186
  all_tags = generated.values.flatten.join("\n").scan(/tags\s+["']([^"']+)["']/).flatten
155
- return unless all_tags.any?
156
-
157
- injected = TagInjector.new(tags: all_tags).inject
158
- injected.each { |tag| @output.puts " Added tag \"#{tag}\" to config/initializers/docit.rb" }
159
- @results[:tags] = injected
187
+ if all_tags.any?
188
+ injected = TagInjector.new(tags: all_tags).inject
189
+ injected.each { |tag| @output.puts " Added tag \"#{tag}\" to config/initializers/docit.rb" }
190
+ @results[:tags] = injected
191
+ end
160
192
  end
161
193
 
162
194
  def strip_markdown_fences(text)
@@ -4,6 +4,15 @@ module Docit
4
4
  module Ai
5
5
  class Error < Docit::Error; end
6
6
 
7
+ class RateLimitError < Error
8
+ attr_reader :retry_after
9
+
10
+ def initialize(message, retry_after: nil)
11
+ @retry_after = retry_after
12
+ super(message)
13
+ end
14
+ end
15
+
7
16
  # Factory for AI provider clients.
8
17
  module Client
9
18
  def self.for(config)
@@ -6,6 +6,11 @@ module Docit
6
6
  module Ai
7
7
  class Configuration
8
8
  CONFIG_FILE = ".docit_ai.yml"
9
+ GENERATED_FILE_COMMENT = <<~TEXT
10
+ # If you want to change the model and start Docit setup again,
11
+ # rerun: rails generate docit:install
12
+
13
+ TEXT
9
14
 
10
15
  PROVIDERS = %w[openai anthropic groq].freeze
11
16
 
@@ -51,11 +56,13 @@ module Docit
51
56
  config = new(provider: provider, model: model, api_key: api_key)
52
57
  raise Error, "Invalid configuration" if config.valid? == false
53
58
 
54
- File.write(config_path, {
59
+ yaml = {
55
60
  "provider" => config.provider,
56
61
  "model" => config.model,
57
62
  "api_key" => config.api_key
58
- }.to_yaml)
63
+ }.to_yaml
64
+
65
+ File.write(config_path, GENERATED_FILE_COMMENT + yaml)
59
66
  File.chmod(0o600, config_path)
60
67
 
61
68
  config
@@ -39,6 +39,12 @@ module Docit
39
39
 
40
40
  if response.is_a?(Net::HTTPSuccess) == false
41
41
  message = body.dig("error", "message") || "Unknown API error"
42
+
43
+ if response.code == "429"
44
+ retry_after = parse_retry_after(response, message)
45
+ raise RateLimitError.new("Groq rate limit exceeded", retry_after: retry_after)
46
+ end
47
+
42
48
  raise Error, "Groq API error (#{response.code}): #{message}"
43
49
  end
44
50
 
@@ -50,6 +56,24 @@ module Docit
50
56
  rescue JSON::ParserError
51
57
  raise Error, "#{provider_name} returned invalid JSON (HTTP #{response.code})"
52
58
  end
59
+
60
+ def parse_retry_after(response, message)
61
+ # Check Retry-After header first (seconds)
62
+ if (header = response["Retry-After"])
63
+ return header.to_f if header.to_f > 0
64
+ end
65
+
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
70
+
71
+ if message =~ /([\d.]+)s/
72
+ return Regexp.last_match(1).to_f
73
+ end
74
+
75
+ nil
76
+ end
53
77
  end
54
78
  end
55
79
  end
@@ -39,6 +39,12 @@ module Docit
39
39
 
40
40
  if response.is_a?(Net::HTTPSuccess) == false
41
41
  message = body.dig("error", "message") || "Unknown API error"
42
+
43
+ if response.code == "429"
44
+ retry_after = response["Retry-After"]&.to_f
45
+ raise RateLimitError.new("OpenAI rate limit exceeded", retry_after: retry_after)
46
+ end
47
+
42
48
  raise Error, "OpenAI API error (#{response.code}): #{message}"
43
49
  end
44
50
 
@@ -66,7 +66,7 @@ module Docit
66
66
  @gap = gap
67
67
  end
68
68
 
69
- def build
69
+ def build(validation_error: nil)
70
70
  <<~PROMPT
71
71
  You are generating Docit DSL documentation for a Ruby on Rails API endpoint.
72
72
 
@@ -89,12 +89,18 @@ module Docit
89
89
  Rules:
90
90
  - Output ONLY the `doc :#{@gap[:action]} do ... end` block
91
91
  - No module wrapper, no explanation, no markdown fences
92
+ - Use exactly `doc :#{@gap[:action]} do ... end` for the requested action
93
+ - Use ONLY the DSL methods listed above
92
94
  - Infer parameters from the path (e.g., {id} → path parameter)
93
95
  - Infer request body from params usage in the controller
94
96
  - Infer response structure from render calls
97
+ - Inside `request_body` and `response` blocks, define nested data only with `property ..., type: :object` or `property ..., type: :array`
98
+ - Never call standalone helpers such as `object`, `array`, `string`, `integer`, `number`, or `boolean`
99
+ - Return valid Ruby that can be `instance_eval`'d as-is
95
100
  - Use realistic examples
96
101
  - Include appropriate error responses
97
102
  - Use the controller name to determine appropriate tags
103
+ #{validation_feedback(validation_error)}
98
104
  PROMPT
99
105
  end
100
106
 
@@ -114,10 +120,22 @@ module Docit
114
120
  end
115
121
 
116
122
  def controller_file_path
117
- return nil if defined?(Rails) == false || Rails.respond_to?(:root) == false || Rails.root.nil?
123
+ return nil unless defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
118
124
 
119
125
  relative = @gap[:controller].underscore
120
- Rails.root.join("app", "controllers", "#{relative}.rb").to_s
126
+ ::Rails.root.join("app", "controllers", "#{relative}.rb").to_s
127
+ end
128
+
129
+ def validation_feedback(validation_error)
130
+ return "" if validation_error.nil? || validation_error.empty?
131
+
132
+ <<~FEEDBACK
133
+
134
+ Previous attempt failed Docit validation:
135
+ - #{validation_error}
136
+
137
+ Regenerate the block and fix that error.
138
+ FEEDBACK
121
139
  end
122
140
  end
123
141
  end
data/lib/docit/ai.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "ai/openai_client"
6
6
  require_relative "ai/anthropic_client"
7
7
  require_relative "ai/groq_client"
8
8
  require_relative "ai/gap_detector"
9
+ require_relative "ai/doc_block_validator"
9
10
  require_relative "ai/prompt_builder"
10
11
  require_relative "ai/doc_writer"
11
12
  require_relative "ai/tag_injector"
@@ -3,18 +3,31 @@
3
3
  module Docit
4
4
  # Holds global API documentation settings: metadata, authentication, tags, and servers.
5
5
  class Configuration
6
+ SUPPORTED_UIS = %i[scalar swagger].freeze
7
+
6
8
  attr_accessor :title, :version, :description, :base_url
9
+ attr_reader :default_ui
7
10
 
8
11
  def initialize
9
12
  @title = "API Documentation"
10
13
  @version = "1.0.0"
11
- @description = ""
14
+ @description = "Welcome to the API documentation. Browse the endpoints below to get started."
12
15
  @base_url = "/"
16
+ @default_ui = :scalar
13
17
  @security_schemes = {}
14
18
  @tags = []
15
19
  @servers = []
16
20
  end
17
21
 
22
+ def default_ui=(value)
23
+ ui = value.to_sym
24
+ unless SUPPORTED_UIS.include?(ui)
25
+ raise ArgumentError, "Unsupported UI: #{value}. Must be one of: #{SUPPORTED_UIS.join(", ")}"
26
+ end
27
+
28
+ @default_ui = ui
29
+ end
30
+
18
31
  def auth(type, **options)
19
32
  case type.to_s.downcase
20
33
  when "basic"
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docit
4
+ module UI
5
+ class BaseRenderer
6
+ attr_reader :spec_url, :title, :nav_paths
7
+
8
+ def initialize(spec_url:, nav_paths: {})
9
+ @spec_url = spec_url
10
+ @nav_paths = nav_paths
11
+ @title = ERB::Util.html_escape(Docit.configuration.title)
12
+ end
13
+
14
+ def render
15
+ raise NotImplementedError, "#{self.class}#render must be implemented"
16
+ end
17
+
18
+ private
19
+
20
+ def nav_bar(active:)
21
+ swagger_active = active == :swagger
22
+ scalar_active = active == :scalar
23
+
24
+ <<~HTML
25
+ <nav style="
26
+ display: flex; align-items: center; gap: 8px;
27
+ padding: 6px 16px;
28
+ background: #1a1a2e; color: #fff;
29
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
30
+ font-size: 13px; position: sticky; top: 0; z-index: 9999;
31
+ ">
32
+ <span style="font-weight: 600; margin-right: auto;">#{title}</span>
33
+ #{nav_link("Swagger", nav_paths[:swagger], active: swagger_active)}
34
+ #{nav_link("Scalar", nav_paths[:scalar], active: scalar_active)}
35
+ </nav>
36
+ HTML
37
+ end
38
+
39
+ def nav_link(label, path, active:)
40
+ escaped_path = ERB::Util.html_escape(path)
41
+ style = if active
42
+ "color: #fff; text-decoration: none; padding: 4px 12px; border-radius: 4px; background: rgba(255,255,255,0.15); font-weight: 500;"
43
+ else
44
+ "color: rgba(255,255,255,0.7); text-decoration: none; padding: 4px 12px; border-radius: 4px;"
45
+ end
46
+
47
+ %(<a href="#{escaped_path}" style="#{style}">#{label}</a>)
48
+ end
49
+
50
+ def spec_url_json
51
+ JSON.generate(spec_url)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docit
4
+ module UI
5
+ class ScalarRenderer < BaseRenderer
6
+ def render
7
+ <<~HTML
8
+ <!DOCTYPE html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="UTF-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
+ <title>#{title}</title>
14
+ <style>
15
+ body { margin: 0; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ #{nav_bar(active: :scalar)}
20
+ <script id="api-reference"></script>
21
+ <script>
22
+ document.getElementById('api-reference').dataset.configuration = JSON.stringify({
23
+ spec: { url: #{spec_url_json} },
24
+ theme: "elysiajs",
25
+ showSidebar: true,
26
+ hideDownloadButton: false,
27
+ hideModels: false,
28
+ searchHotKey: "k"
29
+ })
30
+ </script>
31
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
32
+ </body>
33
+ </html>
34
+ HTML
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docit
4
+ module UI
5
+ class SwaggerRenderer < BaseRenderer
6
+ VERSION = "5.32.2"
7
+
8
+ def render
9
+ <<~HTML
10
+ <!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ <title>#{title}</title>
16
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@#{VERSION}/swagger-ui.css" />
17
+ <style>
18
+ html { box-sizing: border-box; overflow-y: scroll; }
19
+ *, *:before, *:after { box-sizing: inherit; }
20
+ body { margin: 0; background: #fafafa; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ #{nav_bar(active: :swagger)}
25
+ <div id="swagger-ui"></div>
26
+ <script src="https://unpkg.com/swagger-ui-dist@#{VERSION}/swagger-ui-bundle.js"></script>
27
+ <script>
28
+ SwaggerUIBundle({
29
+ url: #{spec_url_json},
30
+ dom_id: '#swagger-ui',
31
+ presets: [
32
+ SwaggerUIBundle.presets.apis,
33
+ SwaggerUIBundle.SwaggerUIStandalonePreset
34
+ ],
35
+ layout: "BaseLayout",
36
+ deepLinking: true,
37
+ showExtensions: true,
38
+ showCommonExtensions: true,
39
+ requestInterceptor: function(req) {
40
+ var url = new URL(req.url);
41
+ url.protocol = window.location.protocol;
42
+ url.host = window.location.host;
43
+ req.url = url.toString();
44
+ return req;
45
+ }
46
+ })
47
+ </script>
48
+ </body>
49
+ </html>
50
+ HTML
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/docit/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docit
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/docit.rb CHANGED
@@ -12,6 +12,9 @@ require_relative "docit/doc_file"
12
12
  require_relative "docit/route_inspector"
13
13
  require_relative "docit/schema_generator"
14
14
  require_relative "docit/dsl"
15
+ require_relative "docit/ui/base_renderer"
16
+ require_relative "docit/ui/swagger_renderer"
17
+ require_relative "docit/ui/scalar_renderer"
15
18
 
16
19
  # Docit is a decorator-style API documentation gem for Ruby on Rails.
17
20
  # It generates OpenAPI 3.0.3 specs from clean DSL macros on your controllers.
@@ -47,4 +50,5 @@ module Docit
47
50
  end
48
51
  end
49
52
 
53
+ require_relative "docit/engine" if defined?(Rails::Engine)
50
54
  require_relative "docit/ai"
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Docit.configure do |config|
4
- # The title shown in Swagger UI
4
+ # The title shown in the API documentation UI
5
5
  config.title = "<%= Rails.application.class.module_parent_name rescue 'My API' %>"
6
6
 
7
7
  # API version
8
8
  config.version = "1.0.0"
9
9
 
10
- # Description shown in Swagger UI
10
+ # Description shown on the introduction page
11
11
  config.description = "API documentation powered by Docit"
12
12
 
13
+ # Documentation UI: :scalar (default) or :swagger
14
+ # config.default_ui = :scalar
15
+
13
16
  # Authentication scheme (options: :bearer, :basic, :api_key)
14
17
  # config.auth :bearer
15
18
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - S13G
@@ -68,6 +68,9 @@ files:
68
68
  - lib/docit/route_inspector.rb
69
69
  - lib/docit/schema_definition.rb
70
70
  - lib/docit/schema_generator.rb
71
+ - lib/docit/ui/base_renderer.rb
72
+ - lib/docit/ui/scalar_renderer.rb
73
+ - lib/docit/ui/swagger_renderer.rb
71
74
  - lib/docit/version.rb
72
75
  - lib/generators/docit/ai_setup/ai_setup_generator.rb
73
76
  - lib/generators/docit/install/install_generator.rb