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 +4 -4
- data/CHANGELOG.md +23 -1
- data/README.md +62 -7
- data/app/controllers/docit/ui_controller.rb +25 -48
- data/config/routes.rb +2 -0
- data/lib/docit/ai/anthropic_client.rb +6 -0
- data/lib/docit/ai/autodoc_runner.rb +62 -30
- data/lib/docit/ai/client.rb +9 -0
- data/lib/docit/ai/configuration.rb +9 -2
- data/lib/docit/ai/groq_client.rb +24 -0
- data/lib/docit/ai/openai_client.rb +6 -0
- data/lib/docit/ai/prompt_builder.rb +21 -3
- data/lib/docit/ai.rb +1 -0
- data/lib/docit/configuration.rb +14 -1
- data/lib/docit/ui/base_renderer.rb +55 -0
- data/lib/docit/ui/scalar_renderer.rb +38 -0
- data/lib/docit/ui/swagger_renderer.rb +54 -0
- data/lib/docit/version.rb +1 -1
- data/lib/docit.rb +4 -0
- data/lib/generators/docit/install/templates/initializer.rb +5 -2
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19ca1ea84419aa3ec1c1605fec67c6355027d866de1b9e6b0d8fb27472b91602
|
|
4
|
+
data.tar.gz: a8fc557d32361a3729a9133791100bea3c7bf94e4c2f0ca55351eff4760efafe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
@@ -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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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)
|
data/lib/docit/ai/client.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/docit/ai/groq_client.rb
CHANGED
|
@@ -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
|
|
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"
|
data/lib/docit/configuration.rb
CHANGED
|
@@ -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
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
|
|
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
|
|
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.
|
|
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
|