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