docit 0.1.0 → 0.2.1
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 +34 -1
- data/CONTRIBUTING.md +1 -1
- data/README.md +102 -9
- data/app/controllers/docit/ui_controller.rb +16 -4
- data/lib/docit/ai/anthropic_client.rb +63 -0
- data/lib/docit/ai/autodoc_runner.rb +184 -0
- data/lib/docit/ai/client.rb +32 -0
- data/lib/docit/ai/configuration.rb +66 -0
- data/lib/docit/ai/doc_writer.rb +120 -0
- data/lib/docit/ai/gap_detector.rb +54 -0
- data/lib/docit/ai/groq_client.rb +79 -0
- data/lib/docit/ai/openai_client.rb +61 -0
- data/lib/docit/ai/prompt_builder.rb +124 -0
- data/lib/docit/ai/scaffold_generator.rb +140 -0
- data/lib/docit/ai/tag_injector.rb +52 -0
- data/lib/docit/ai.rb +13 -0
- data/lib/docit/version.rb +1 -1
- data/lib/docit.rb +3 -0
- data/lib/generators/docit/ai_setup/ai_setup_generator.rb +106 -0
- data/lib/generators/docit/install/install_generator.rb +178 -3
- data/lib/tasks/docit.rake +19 -0
- metadata +25 -10
- data/.github/workflows/ci.yml +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fe9c345834352ce5e4219a291760ced965cdbad10084c28cbc2aabc2823a5b5
|
|
4
|
+
data.tar.gz: 86aa4599733b72c52fe66335c4581e4f69b408af1bbb00c0c2d5f42b1b7a51fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ad86a75a50a1a1177cf7cbde2bf49f12eb7cc36e1df9fd268ff2b6519c7b01efffc680aab2484a83f3dbdfb410740633a7cabbf8072d873403deafc6c14ccd3
|
|
7
|
+
data.tar.gz: 2fbf560890376ef66614e12f961496b77ab5bfb2e994ffaeb006cc2a5ddd9ef82128115cf7d1fba39a89a889b0aec8342d487525eedad276a9b8244a6ee574d3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.1] - 2026-04-11
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- 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
|
|
7
|
+
- AI provider clients (OpenAI, Anthropic, Groq) now raise `Docit::Ai::RateLimitError` on 429 responses with parsed retry-after timing
|
|
8
|
+
- `AutodocRunner` now retries rate-limited requests up to 3 times with exponential backoff (capped at 5 minutes) instead of failing immediately
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-04-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Unified install generator: `rails g docit:install` now offers AI auto-docs, manual scaffolding, or skip
|
|
14
|
+
- Manual scaffold mode: creates doc file placeholders with TODO markers, injects `use_docs` into controllers
|
|
15
|
+
- `AutodocRunner` class: reusable orchestrator for AI doc generation (used by both install generator and rake task)
|
|
16
|
+
- `ScaffoldGenerator` class: creates placeholder doc files for manual documentation
|
|
17
|
+
- Groq provider support (free tier) alongside OpenAI and Anthropic
|
|
18
|
+
- AI configuration generator: `rails g docit:ai_setup`
|
|
19
|
+
- Tasks: `rails docit:autodoc` with preview support via `DRY_RUN=1`
|
|
20
|
+
- Auto-injection of `use_docs` into controllers after doc generation
|
|
21
|
+
- Auto-injection of `config.tag` entries into initializer
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- AI setup flows now retry invalid menu input instead of exiting immediately
|
|
25
|
+
- AI setup flows now hide API key input in interactive terminals
|
|
26
|
+
- `rails docit:autodoc` now supports `DRY_RUN=1` for preview mode
|
|
27
|
+
- AI autodoc now warns before sending controller source to external providers
|
|
28
|
+
- Swagger UI assets are pinned to a specific `swagger-ui-dist` version
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- `.docit_ai.yml` is now written with restricted file permissions
|
|
32
|
+
- AI provider clients now return a clean Docit error when an upstream service responds with invalid JSON
|
|
33
|
+
- Swagger UI now escapes the generated spec URL before embedding it in JavaScript
|
|
34
|
+
- Manual scaffolds now use `200` for `PUT` and `PATCH` responses by default
|
|
35
|
+
|
|
3
36
|
## [0.1.0] - 2026-04-08
|
|
4
37
|
|
|
5
38
|
- Initial release
|
|
6
39
|
- DSL: `swagger_doc` macro for inline controller documentation
|
|
7
|
-
- DSL: `use_docs` + `Docit::DocFile` for separate doc files
|
|
40
|
+
- DSL: `use_docs` + `Docit::DocFile` for separate doc files
|
|
8
41
|
- Builders: request body, response, and parameter builders with nested object/array support
|
|
9
42
|
- Schema `$ref` components via `Docit.define_schema`
|
|
10
43
|
- File upload support (`type: :file` → `string/binary`)
|
data/CONTRIBUTING.md
CHANGED
|
@@ -33,7 +33,7 @@ Docit aims for high test coverage and an extensive test suite. Tests enable us t
|
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
# Fork the repo on GitHub, then:
|
|
36
|
-
git clone https://github.com/
|
|
36
|
+
git clone https://github.com/S13G/docit.git
|
|
37
37
|
cd docit
|
|
38
38
|
|
|
39
39
|
# Install dependencies
|
data/README.md
CHANGED
|
@@ -1,13 +1,47 @@
|
|
|
1
1
|
# Docit
|
|
2
2
|
|
|
3
|
-
[](https://rubygems.org/gems/docit)
|
|
4
|
-
[](https://github.com/S13G/docket/actions/workflows/ci.yml)
|
|
5
3
|
[](https://www.ruby-lang.org)
|
|
6
4
|
[](https://opensource.org/licenses/MIT)
|
|
7
5
|
|
|
8
|
-
Decorator-style API documentation for Ruby on Rails. Write OpenAPI 3.0 docs
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
|
|
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
|
+
- [How it works](#how-it-works)
|
|
36
|
+
- [Mounting at a different path](#mounting-at-a-different-path)
|
|
37
|
+
- [JSON spec only](#json-spec-only)
|
|
38
|
+
- [Development](#development)
|
|
39
|
+
- [Contributing](#contributing)
|
|
40
|
+
- [License](#license)
|
|
41
|
+
- Project docs
|
|
42
|
+
- [CHANGELOG](CHANGELOG.md)
|
|
43
|
+
- [CONTRIBUTING](CONTRIBUTING.md)
|
|
44
|
+
- [CODE OF CONDUCT](CODE_OF_CONDUCT.md)
|
|
11
45
|
|
|
12
46
|
## Installation
|
|
13
47
|
|
|
@@ -24,13 +58,19 @@ bundle install
|
|
|
24
58
|
rails generate docit:install
|
|
25
59
|
```
|
|
26
60
|
|
|
27
|
-
The generator does
|
|
61
|
+
The install generator does everything in one step:
|
|
28
62
|
|
|
29
63
|
1. Creates `config/initializers/docit.rb` with default settings
|
|
30
64
|
2. Mounts the Swagger UI engine at `/api-docs` in your routes
|
|
65
|
+
3. Asks how you'd like to set up your docs:
|
|
66
|
+
- **AI automatic docs** — configure an AI provider, then Docit scans your routes and generates complete documentation for every endpoint
|
|
67
|
+
- **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
|
|
68
|
+
- **Skip** — just install the base config and set up docs later
|
|
31
69
|
|
|
32
70
|
Visit `/api-docs` to see your interactive API documentation.
|
|
33
71
|
|
|
72
|
+
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.
|
|
73
|
+
|
|
34
74
|
## Configuration
|
|
35
75
|
|
|
36
76
|
Edit `config/initializers/docit.rb`:
|
|
@@ -81,7 +121,7 @@ end
|
|
|
81
121
|
|
|
82
122
|
### Style 2: Separate doc files (recommended for larger APIs)
|
|
83
123
|
|
|
84
|
-
Keep controllers clean by defining docs in dedicated files
|
|
124
|
+
Keep controllers clean by defining docs in dedicated files:
|
|
85
125
|
|
|
86
126
|
```ruby
|
|
87
127
|
# app/docs/api/v1/users_docs.rb
|
|
@@ -374,6 +414,59 @@ end
|
|
|
374
414
|
|
|
375
415
|
`type: :file` maps to `{ type: "string", format: "binary" }` in the OpenAPI spec.
|
|
376
416
|
|
|
417
|
+
## AI Automatic Documentation
|
|
418
|
+
|
|
419
|
+
Docit can generate complete API documentation using AI. This works with OpenAI, Anthropic, or Groq (free tier available).
|
|
420
|
+
|
|
421
|
+
### Quick start (included in install)
|
|
422
|
+
|
|
423
|
+
When you run `rails generate docit:install` and choose option 1 (AI automatic docs), everything is set up automatically — provider configuration, doc generation, controller wiring, and tag injection.
|
|
424
|
+
|
|
425
|
+
Before the first AI request, Docit warns that your controller source code will be sent to the selected provider and asks for confirmation in interactive terminals.
|
|
426
|
+
|
|
427
|
+
### Standalone commands
|
|
428
|
+
|
|
429
|
+
You can also set up AI docs separately:
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
# Configure your AI provider (one-time setup)
|
|
433
|
+
rails generate docit:ai_setup
|
|
434
|
+
|
|
435
|
+
# Generate docs for all undocumented endpoints
|
|
436
|
+
rails docit:autodoc
|
|
437
|
+
|
|
438
|
+
# Generate docs for a specific controller
|
|
439
|
+
rails docit:autodoc[Api::V1::UsersController]
|
|
440
|
+
|
|
441
|
+
# Preview what would be generated without writing files
|
|
442
|
+
DRY_RUN=1 rails docit:autodoc
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Supported providers
|
|
446
|
+
|
|
447
|
+
| Provider | Notes |
|
|
448
|
+
|------------|-------|
|
|
449
|
+
| OpenAI | Requires API key from platform.openai.com |
|
|
450
|
+
| Anthropic | Requires API key from console.anthropic.com |
|
|
451
|
+
| Groq | Free tier at console.groq.com |
|
|
452
|
+
|
|
453
|
+
All providers automatically retry on rate-limit (429) errors with exponential backoff, so free-tier usage works out of the box.
|
|
454
|
+
|
|
455
|
+
AI configuration is stored in `.docit_ai.yml`.
|
|
456
|
+
If your app does not have a `.gitignore`, add `.docit_ai.yml` manually.
|
|
457
|
+
|
|
458
|
+
### What the AI generates
|
|
459
|
+
|
|
460
|
+
For each undocumented endpoint, Docit:
|
|
461
|
+
|
|
462
|
+
1. Reads the controller source code
|
|
463
|
+
2. Inspects the route (HTTP method, path, parameters)
|
|
464
|
+
3. Writes the generated doc block to `app/docs/`
|
|
465
|
+
4. Injects `use_docs` into the controller
|
|
466
|
+
5. Adds tag descriptions to the initializer
|
|
467
|
+
|
|
468
|
+
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.
|
|
469
|
+
|
|
377
470
|
## How it works
|
|
378
471
|
|
|
379
472
|
1. `swagger_doc` registers an **Operation** for each controller action in a global **Registry**
|
|
@@ -401,7 +494,7 @@ GET /api-docs/spec
|
|
|
401
494
|
## Development
|
|
402
495
|
|
|
403
496
|
```bash
|
|
404
|
-
git clone https://github.com/S13G/
|
|
497
|
+
git clone https://github.com/S13G/docit.git
|
|
405
498
|
cd docit
|
|
406
499
|
bundle install
|
|
407
500
|
bundle exec rspec # run all tests
|
|
@@ -409,7 +502,7 @@ bundle exec rspec # run all tests
|
|
|
409
502
|
|
|
410
503
|
## Contributing
|
|
411
504
|
|
|
412
|
-
Bug reports and pull requests are welcome on [GitHub](https://github.com/S13G/
|
|
505
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/S13G/docit).
|
|
413
506
|
|
|
414
507
|
## License
|
|
415
508
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module Docit
|
|
4
6
|
class UiController < ActionController::Base
|
|
7
|
+
SWAGGER_UI_VERSION = "5.32.2"
|
|
8
|
+
|
|
5
9
|
def index
|
|
6
10
|
render html: swagger_ui_html.html_safe, layout: false
|
|
7
11
|
end
|
|
@@ -15,6 +19,7 @@ module Docit
|
|
|
15
19
|
|
|
16
20
|
def swagger_ui_html
|
|
17
21
|
spec_url = "#{request.base_url}#{Docit::Engine.routes.url_helpers.spec_path}"
|
|
22
|
+
spec_url_json = JSON.generate(spec_url)
|
|
18
23
|
escaped_title = ERB::Util.html_escape(Docit.configuration.title)
|
|
19
24
|
|
|
20
25
|
<<~HTML
|
|
@@ -24,7 +29,7 @@ module Docit
|
|
|
24
29
|
<meta charset="UTF-8">
|
|
25
30
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
26
31
|
<title>#{escaped_title}</title>
|
|
27
|
-
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist
|
|
32
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@#{SWAGGER_UI_VERSION}/swagger-ui.css" />
|
|
28
33
|
<style>
|
|
29
34
|
html { box-sizing: border-box; overflow-y: scroll; }
|
|
30
35
|
*, *:before, *:after { box-sizing: inherit; }
|
|
@@ -33,10 +38,10 @@ module Docit
|
|
|
33
38
|
</head>
|
|
34
39
|
<body>
|
|
35
40
|
<div id="swagger-ui"></div>
|
|
36
|
-
<script src="https://unpkg.com/swagger-ui-dist
|
|
41
|
+
<script src="https://unpkg.com/swagger-ui-dist@#{SWAGGER_UI_VERSION}/swagger-ui-bundle.js"></script>
|
|
37
42
|
<script>
|
|
38
43
|
SwaggerUIBundle({
|
|
39
|
-
url:
|
|
44
|
+
url: #{spec_url_json},
|
|
40
45
|
dom_id: '#swagger-ui',
|
|
41
46
|
presets: [
|
|
42
47
|
SwaggerUIBundle.presets.apis,
|
|
@@ -45,7 +50,14 @@ module Docit
|
|
|
45
50
|
layout: "BaseLayout",
|
|
46
51
|
deepLinking: true,
|
|
47
52
|
showExtensions: true,
|
|
48
|
-
showCommonExtensions: 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
|
+
}
|
|
49
61
|
})
|
|
50
62
|
</script>
|
|
51
63
|
</body>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Docit
|
|
8
|
+
module Ai
|
|
9
|
+
class AnthropicClient
|
|
10
|
+
API_URL = "https://api.anthropic.com/v1/messages"
|
|
11
|
+
API_VERSION = "2023-06-01"
|
|
12
|
+
|
|
13
|
+
def initialize(api_key:, model:)
|
|
14
|
+
@api_key = api_key
|
|
15
|
+
@model = model
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate(prompt)
|
|
19
|
+
uri = URI(API_URL)
|
|
20
|
+
request = Net::HTTP::Post.new(uri)
|
|
21
|
+
request["x-api-key"] = @api_key
|
|
22
|
+
request["anthropic-version"] = API_VERSION
|
|
23
|
+
request["Content-Type"] = "application/json"
|
|
24
|
+
request.body = {
|
|
25
|
+
model: @model,
|
|
26
|
+
max_tokens: 4096,
|
|
27
|
+
messages: [{ role: "user", content: prompt }]
|
|
28
|
+
}.to_json
|
|
29
|
+
|
|
30
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: 15, read_timeout: 60) do |http|
|
|
31
|
+
http.request(request)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
handle_response(response)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def handle_response(response)
|
|
40
|
+
body = parse_json(response, "Anthropic")
|
|
41
|
+
|
|
42
|
+
if response.is_a?(Net::HTTPSuccess) == false
|
|
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
|
+
|
|
50
|
+
raise Error, "Anthropic API error (#{response.code}): #{message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
body.dig("content", 0, "text") || raise(Error, "Empty response from Anthropic")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_json(response, provider_name)
|
|
57
|
+
JSON.parse(response.body)
|
|
58
|
+
rescue JSON::ParserError
|
|
59
|
+
raise Error, "#{provider_name} returned invalid JSON (HTTP #{response.code})"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docit
|
|
4
|
+
module Ai
|
|
5
|
+
class AutodocRunner
|
|
6
|
+
attr_reader :results
|
|
7
|
+
|
|
8
|
+
def initialize(controller_filter: nil, dry_run: false, input: $stdin, output: $stdout)
|
|
9
|
+
@controller_filter = controller_filter
|
|
10
|
+
@dry_run = dry_run
|
|
11
|
+
@input = input
|
|
12
|
+
@output = output
|
|
13
|
+
@results = { gaps: [], generated: 0, files: [], tags: [] }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
check_base_setup!
|
|
18
|
+
config = load_config
|
|
19
|
+
@output.puts "Using #{config.provider}"
|
|
20
|
+
@output.puts ""
|
|
21
|
+
|
|
22
|
+
gaps = detect_gaps
|
|
23
|
+
@results[:gaps] = gaps
|
|
24
|
+
|
|
25
|
+
if gaps.empty?
|
|
26
|
+
@output.puts "All endpoints are documented!"
|
|
27
|
+
return @results
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
print_gaps(gaps)
|
|
31
|
+
|
|
32
|
+
if @dry_run
|
|
33
|
+
@output.puts "[dry-run] No files written."
|
|
34
|
+
return @results
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
confirm_source_upload!(config)
|
|
38
|
+
generated = generate_docs(gaps, config)
|
|
39
|
+
write_docs(generated)
|
|
40
|
+
inject_tags(generated)
|
|
41
|
+
|
|
42
|
+
@output.puts "Review generated files and edit as needed."
|
|
43
|
+
@results
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def load_config
|
|
49
|
+
Docit::Ai::Configuration.load
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def check_base_setup!
|
|
53
|
+
if !(defined?(Rails) && Rails.respond_to?(:root) && Rails.root)
|
|
54
|
+
raise Docit::Error, "Docit requires a Rails application. Run this command from your app root."
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
initializer = Rails.root.join("config", "initializers", "docit.rb")
|
|
58
|
+
if File.exist?(initializer) == false
|
|
59
|
+
raise Docit::Error, "Docit is not installed. Run: rails generate docit:install"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
routes_file = Rails.root.join("config", "routes.rb")
|
|
63
|
+
if File.exist?(routes_file) && !File.read(routes_file).include?("Docit::Engine")
|
|
64
|
+
@output.puts "Warning: Docit engine is not mounted in config/routes.rb"
|
|
65
|
+
@output.puts " Run: rails generate docit:install (or add: mount Docit::Engine => \"/api-docs\")"
|
|
66
|
+
@output.puts ""
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def detect_gaps
|
|
71
|
+
detector = GapDetector.new(controller_filter: @controller_filter)
|
|
72
|
+
detector.detect
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def print_gaps(gaps)
|
|
76
|
+
@output.puts "Found #{gaps.length} undocumented endpoint#{"s" if gaps.length > 1}:"
|
|
77
|
+
gaps.each { |g| @output.puts " #{g[:method].upcase} #{g[:path]} (#{g[:controller]}##{g[:action]})" }
|
|
78
|
+
@output.puts ""
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def confirm_source_upload!(config)
|
|
82
|
+
@output.puts "Docit will send controller source code to #{config.provider.capitalize} to generate documentation."
|
|
83
|
+
@output.puts "Review the endpoints first if they contain secrets or proprietary logic."
|
|
84
|
+
|
|
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."
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def generate_docs(gaps, config)
|
|
104
|
+
client = Client.for(config)
|
|
105
|
+
generated = Hash.new { |h, k| h[k] = [] }
|
|
106
|
+
max_retries = 3
|
|
107
|
+
|
|
108
|
+
@output.puts "Generating documentation............."
|
|
109
|
+
|
|
110
|
+
gaps.each_with_index do |gap, index|
|
|
111
|
+
@output.print "[#{index + 1}/#{gaps.length}] Generating #{gap[:controller]}##{gap[:action]}..."
|
|
112
|
+
|
|
113
|
+
builder = PromptBuilder.new(gap: gap)
|
|
114
|
+
if builder.source_available? == false
|
|
115
|
+
@output.puts " skipped (controller source file not found)"
|
|
116
|
+
next
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
prompt = builder.build
|
|
120
|
+
retries = 0
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
doc_block = client.generate(prompt).strip
|
|
124
|
+
doc_block = strip_markdown_fences(doc_block)
|
|
125
|
+
|
|
126
|
+
generated[gap[:controller]] << doc_block
|
|
127
|
+
@results[:generated] += 1
|
|
128
|
+
@output.puts " done"
|
|
129
|
+
rescue Docit::Ai::RateLimitError => e
|
|
130
|
+
retries += 1
|
|
131
|
+
if retries <= max_retries
|
|
132
|
+
wait = e.retry_after || (2**retries * 10)
|
|
133
|
+
wait = [wait, 300].min # cap at 5 minutes
|
|
134
|
+
@output.puts " rate limited, waiting #{wait.round}s (attempt #{retries}/#{max_retries})..."
|
|
135
|
+
sleep(wait)
|
|
136
|
+
retry
|
|
137
|
+
else
|
|
138
|
+
@output.puts " failed (rate limit exceeded after #{max_retries} retries)"
|
|
139
|
+
end
|
|
140
|
+
rescue Docit::Ai::Error => e
|
|
141
|
+
@output.puts " failed (#{e.message})"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
generated
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def write_docs(generated)
|
|
149
|
+
generated.each do |controller, blocks|
|
|
150
|
+
next if blocks.empty?
|
|
151
|
+
|
|
152
|
+
writer = DocWriter.new(controller_name: controller)
|
|
153
|
+
writer.write(blocks)
|
|
154
|
+
@results[:files] << writer.doc_file_path
|
|
155
|
+
|
|
156
|
+
relative = writer.doc_file_path.sub("#{Rails.root}/", "")
|
|
157
|
+
@output.puts " Wrote: #{relative}"
|
|
158
|
+
|
|
159
|
+
if writer.inject_use_docs
|
|
160
|
+
controller_relative = File.join("app", "controllers", "#{controller.underscore}.rb")
|
|
161
|
+
@output.puts " Added use_docs to #{controller_relative}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
@output.puts ""
|
|
166
|
+
@output.puts "Generated docs for #{@results[:generated]} endpoint#{"s" if @results[:generated] > 1} in #{@results[:files].length} file#{"s" if @results[:files].length > 1}."
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def inject_tags(generated)
|
|
170
|
+
all_tags = generated.values.flatten.join("\n").scan(/tags\s+["']([^"']+)["']/).flatten
|
|
171
|
+
return unless all_tags.any?
|
|
172
|
+
|
|
173
|
+
injected = TagInjector.new(tags: all_tags).inject
|
|
174
|
+
injected.each { |tag| @output.puts " Added tag \"#{tag}\" to config/initializers/docit.rb" }
|
|
175
|
+
@results[:tags] = injected
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def strip_markdown_fences(text)
|
|
179
|
+
text = text.sub(/\A```\w*\n/, "")
|
|
180
|
+
text.sub(/\n```\z/, "")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docit
|
|
4
|
+
module Ai
|
|
5
|
+
class Error < Docit::Error; end
|
|
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
|
+
|
|
16
|
+
# Factory for AI provider clients.
|
|
17
|
+
module Client
|
|
18
|
+
def self.for(config)
|
|
19
|
+
case config.provider
|
|
20
|
+
when "openai"
|
|
21
|
+
OpenaiClient.new(api_key: config.api_key, model: config.model)
|
|
22
|
+
when "anthropic"
|
|
23
|
+
AnthropicClient.new(api_key: config.api_key, model: config.model)
|
|
24
|
+
when "groq"
|
|
25
|
+
GroqClient.new(api_key: config.api_key, model: config.model)
|
|
26
|
+
else
|
|
27
|
+
raise Error, "Unsupported provider: #{config.provider}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Docit
|
|
6
|
+
module Ai
|
|
7
|
+
class Configuration
|
|
8
|
+
CONFIG_FILE = ".docit_ai.yml"
|
|
9
|
+
|
|
10
|
+
PROVIDERS = %w[openai anthropic groq].freeze
|
|
11
|
+
|
|
12
|
+
DEFAULT_MODELS = {
|
|
13
|
+
"openai" => "gpt-4o-mini",
|
|
14
|
+
"anthropic" => "claude-haiku-4-5-20251001",
|
|
15
|
+
"groq" => "llama-3.3-70b-versatile"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :provider, :model, :api_key
|
|
19
|
+
|
|
20
|
+
def initialize(provider:, model:, api_key:)
|
|
21
|
+
@provider = provider.to_s
|
|
22
|
+
@model = model.to_s
|
|
23
|
+
@api_key = api_key.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def valid?
|
|
27
|
+
PROVIDERS.include?(provider) && !model.empty? && !api_key.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def config_path
|
|
32
|
+
Rails.root.join(CONFIG_FILE)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def configured?
|
|
36
|
+
File.exist?(config_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def load
|
|
40
|
+
raise Error, "AI not configured. Run: rails generate docit:ai_setup" if configured? == false
|
|
41
|
+
|
|
42
|
+
data = YAML.safe_load_file(config_path, permitted_classes: [Symbol])
|
|
43
|
+
new(
|
|
44
|
+
provider: data["provider"],
|
|
45
|
+
model: data["model"],
|
|
46
|
+
api_key: data["api_key"]
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def save(provider:, model:, api_key:)
|
|
51
|
+
config = new(provider: provider, model: model, api_key: api_key)
|
|
52
|
+
raise Error, "Invalid configuration" if config.valid? == false
|
|
53
|
+
|
|
54
|
+
File.write(config_path, {
|
|
55
|
+
"provider" => config.provider,
|
|
56
|
+
"model" => config.model,
|
|
57
|
+
"api_key" => config.api_key
|
|
58
|
+
}.to_yaml)
|
|
59
|
+
File.chmod(0o600, config_path)
|
|
60
|
+
|
|
61
|
+
config
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|