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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e48afeb14f8b73781bf089a31b30616c00d4aa053f079668fbecbc7215ea13e2
4
- data.tar.gz: dd1ea68be0fb59dd014af55bb457fe110e2da2227fe56a0216d75c5051046215
3
+ metadata.gz: 3fe9c345834352ce5e4219a291760ced965cdbad10084c28cbc2aabc2823a5b5
4
+ data.tar.gz: 86aa4599733b72c52fe66335c4581e4f69b408af1bbb00c0c2d5f42b1b7a51fe
5
5
  SHA512:
6
- metadata.gz: ba5dadbd366f719b4420c515ad252380e4e5fd66137bb183e6b8d109426b0cf828eed12918658ea3a9af6e55d73f2c8e806e7a92c9d600d3a58e1fb643319576
7
- data.tar.gz: 17690c65bec465a9b3479d80375a558a72bec94fd5677f8389e028d07d7b915e146bfa16b380ba0fb89aef33b848980033bcd38eb51ba04c6ef899c1c7c6663f
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 (drf-spectacular style)
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/YOURGITHUBNAME/docit.git
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
- [![Gem Version](https://badge.fury.io/rb/docit.svg)](https://rubygems.org/gems/docit)
4
- [![CI](https://github.com/S13G/docket/actions/workflows/ci.yml/badge.svg)](https://github.com/S13G/docket/actions/workflows/ci.yml)
5
3
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg)](https://www.ruby-lang.org)
6
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
5
 
8
- Decorator-style API documentation for Ruby on Rails. Write OpenAPI 3.0 docs as clean DSL macros directly on your controller actions: no separate doc files, no RSpec integration required. Just annotate and go.
9
-
10
- Inspired by [drf-spectacular](https://github.com/tfranzel/drf-spectacular) for Django REST Framework.
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 two things:
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, just like drf-spectacular:
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/docket.git
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/docket).
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@5/swagger-ui.css" />
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@5/swagger-ui-bundle.js"></script>
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: "#{spec_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