rails-api-docs 0.1.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.
Potentially problematic release.
This version of rails-api-docs might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/CHANGELOG.md +142 -0
- data/LICENSE.txt +21 -0
- data/README.md +552 -0
- data/app/controllers/rails_api_docs/docs_controller.rb +20 -0
- data/config/routes.rb +5 -0
- data/lib/generators/rails-api-docs/init/init_generator.rb +167 -0
- data/lib/generators/rails-api-docs/update/update_generator.rb +28 -0
- data/lib/rails-api-docs/config/appender.rb +98 -0
- data/lib/rails-api-docs/config/builder.rb +175 -0
- data/lib/rails-api-docs/config/loader.rb +30 -0
- data/lib/rails-api-docs/configuration.rb +64 -0
- data/lib/rails-api-docs/engine.rb +27 -0
- data/lib/rails-api-docs/inspectors/body_inferrer.rb +88 -0
- data/lib/rails-api-docs/inspectors/controller_inspector.rb +78 -0
- data/lib/rails-api-docs/inspectors/json_route_detector.rb +189 -0
- data/lib/rails-api-docs/inspectors/route_inspector.rb +126 -0
- data/lib/rails-api-docs/inspectors/schema_inspector.rb +36 -0
- data/lib/rails-api-docs/sample_value.rb +28 -0
- data/lib/rails-api-docs/tasks/rails-api-docs.rake +25 -0
- data/lib/rails-api-docs/templates/api_docs.html.erb +695 -0
- data/lib/rails-api-docs/version.rb +5 -0
- data/lib/rails-api-docs.rb +21 -0
- metadata +155 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module RailsApiDocs
|
|
6
|
+
module Inspectors
|
|
7
|
+
# Walks a controller file via Prism and returns the list of attribute
|
|
8
|
+
# names declared in any `params.require(:X).permit(...)` (or
|
|
9
|
+
# `params.permit(...)`) call inside the file.
|
|
10
|
+
#
|
|
11
|
+
# This is a deliberately coarse pass — we don't try to map a permit
|
|
12
|
+
# call back to a specific action. In typical Rails controllers strong
|
|
13
|
+
# params live in a single shared `*_params` method used by both create
|
|
14
|
+
# and update, so the flat list of permitted attributes is enough for
|
|
15
|
+
# documentation purposes.
|
|
16
|
+
#
|
|
17
|
+
# Limitations (Phase 5):
|
|
18
|
+
# - Nested permits (`permit(items: [:name])`) — only top-level scalar
|
|
19
|
+
# keys are extracted; the nested array is ignored.
|
|
20
|
+
# - Method-call args inside permit (e.g. `permit(*PERMITTED)`) — not
|
|
21
|
+
# resolved; only literal symbols/strings are picked up.
|
|
22
|
+
class ControllerInspector
|
|
23
|
+
def initialize(controller:, root: nil)
|
|
24
|
+
@controller = controller
|
|
25
|
+
@root = root || (defined?(Rails) && Rails.root ? Rails.root.to_s : ".")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call
|
|
29
|
+
return [] unless File.exist?(file_path)
|
|
30
|
+
|
|
31
|
+
result = Prism.parse_file(file_path)
|
|
32
|
+
visitor = PermitVisitor.new
|
|
33
|
+
visitor.visit(result.value)
|
|
34
|
+
|
|
35
|
+
visitor.permitted.uniq.map(&:to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def file_path
|
|
39
|
+
File.join(@root, "app/controllers", "#{@controller}_controller.rb")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class PermitVisitor < ::Prism::Visitor
|
|
43
|
+
attr_reader :permitted
|
|
44
|
+
|
|
45
|
+
def initialize
|
|
46
|
+
@permitted = []
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def visit_call_node(node)
|
|
51
|
+
collect_permitted(node) if node.name == :permit
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def collect_permitted(node)
|
|
58
|
+
args = node.arguments&.arguments || []
|
|
59
|
+
args.each do |arg|
|
|
60
|
+
case arg
|
|
61
|
+
when ::Prism::SymbolNode
|
|
62
|
+
@permitted << arg.value
|
|
63
|
+
when ::Prism::StringNode
|
|
64
|
+
@permitted << arg.unescaped
|
|
65
|
+
when ::Prism::KeywordHashNode, ::Prism::HashNode
|
|
66
|
+
# Nested permits — pick up the top-level keys only.
|
|
67
|
+
arg.elements.each do |element|
|
|
68
|
+
next unless element.respond_to?(:key)
|
|
69
|
+
key = element.key
|
|
70
|
+
@permitted << key.value if key.is_a?(::Prism::SymbolNode)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require "set"
|
|
5
|
+
require "active_support/core_ext/string/inflections"
|
|
6
|
+
|
|
7
|
+
module RailsApiDocs
|
|
8
|
+
module Inspectors
|
|
9
|
+
# Decides whether an endpoint should be considered "JSON-returning" for
|
|
10
|
+
# the `--api-only` filter on the install generator.
|
|
11
|
+
#
|
|
12
|
+
# Decision logic for `call(controller:, action:)`:
|
|
13
|
+
# 1. If the controller's inheritance chain reaches `ActionController::API`
|
|
14
|
+
# (directly, or transitively via ApplicationController / a custom
|
|
15
|
+
# base controller) → true for every action.
|
|
16
|
+
# 2. Else if the action body contains a `render json: …` call (kwarg
|
|
17
|
+
# form or hash-rocket form, including inside `respond_to` blocks)
|
|
18
|
+
# → true.
|
|
19
|
+
# 3. Else → false.
|
|
20
|
+
#
|
|
21
|
+
# `:unknown` classifications (controller file missing, unresolvable
|
|
22
|
+
# parent constant) are strict — they DO NOT count as :api. Per-action
|
|
23
|
+
# render-json detection still runs if the file exists but inheritance
|
|
24
|
+
# is unresolvable.
|
|
25
|
+
#
|
|
26
|
+
# Each controller file is parsed at most once per detector instance —
|
|
27
|
+
# the same Prism AST feeds both visitors, and the result hash is cached
|
|
28
|
+
# so the inheritance chain ApplicationController → ActionController::API
|
|
29
|
+
# is walked once even when 50 controllers inherit from it.
|
|
30
|
+
#
|
|
31
|
+
# Known limitations:
|
|
32
|
+
# - `render json:` inside a helper method called from the action is
|
|
33
|
+
# not detected (we don't follow method calls).
|
|
34
|
+
# - `render template: "x", formats: :json` doesn't have a `json:`
|
|
35
|
+
# key in the call, so it's missed.
|
|
36
|
+
# - Includes (`include SomeRenderingModule`) are not traversed.
|
|
37
|
+
class JsonRouteDetector
|
|
38
|
+
def initialize(root: nil)
|
|
39
|
+
@root = root || (defined?(Rails) && Rails.root ? Rails.root.to_s : ".")
|
|
40
|
+
@cache = {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def call(controller:, action:)
|
|
44
|
+
profile = profile_for(controller)
|
|
45
|
+
return true if profile[:type] == :api
|
|
46
|
+
profile[:json_actions].include?(action.to_s)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Exposed for testing / debugging.
|
|
50
|
+
def profile_for(controller)
|
|
51
|
+
@cache[controller] ||= analyze(controller)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def analyze(controller)
|
|
57
|
+
file = controller_path(controller)
|
|
58
|
+
return blank_profile unless File.exist?(file)
|
|
59
|
+
|
|
60
|
+
ast = ::Prism.parse_file(file).value
|
|
61
|
+
|
|
62
|
+
sc_visitor = SuperclassVisitor.new
|
|
63
|
+
sc_visitor.visit(ast)
|
|
64
|
+
|
|
65
|
+
ja_visitor = JsonActionVisitor.new
|
|
66
|
+
ja_visitor.visit(ast)
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
type: classify(sc_visitor.superclass),
|
|
70
|
+
json_actions: ja_visitor.actions
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def classify(parent_name)
|
|
75
|
+
return :unknown unless parent_name
|
|
76
|
+
|
|
77
|
+
case parent_name
|
|
78
|
+
when "ActionController::API" then :api
|
|
79
|
+
when "ActionController::Base" then :html
|
|
80
|
+
else
|
|
81
|
+
parent_controller = parent_name.underscore.sub(/_controller\z/, "")
|
|
82
|
+
# Guard against pathological cycles (e.g. file that references
|
|
83
|
+
# itself somehow). The cache hit on second visit makes the loop
|
|
84
|
+
# short-circuit because we'd recurse into ourselves and read
|
|
85
|
+
# back the same in-progress entry — but to be safe, mark visited.
|
|
86
|
+
return :unknown if @cache.key?(parent_controller)
|
|
87
|
+
|
|
88
|
+
@cache[parent_controller] = blank_profile # placeholder to break cycles
|
|
89
|
+
@cache[parent_controller] = analyze(parent_controller)
|
|
90
|
+
@cache[parent_controller][:type]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def blank_profile
|
|
95
|
+
{ type: :unknown, json_actions: [] }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def controller_path(controller)
|
|
99
|
+
File.join(@root, "app/controllers", "#{controller}_controller.rb")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# ============================================================
|
|
103
|
+
# Visitors
|
|
104
|
+
# ============================================================
|
|
105
|
+
|
|
106
|
+
# Captures the superclass name from the FIRST class definition in
|
|
107
|
+
# the file. Handles both simple constants (`ApplicationController`)
|
|
108
|
+
# and qualified ones (`ActionController::API`, `Api::V1::BaseController`).
|
|
109
|
+
class SuperclassVisitor < ::Prism::Visitor
|
|
110
|
+
attr_reader :superclass
|
|
111
|
+
|
|
112
|
+
def visit_class_node(node)
|
|
113
|
+
@superclass ||= constant_name(node.superclass)
|
|
114
|
+
super
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def constant_name(node)
|
|
120
|
+
case node
|
|
121
|
+
when nil
|
|
122
|
+
nil
|
|
123
|
+
when ::Prism::ConstantReadNode
|
|
124
|
+
node.name.to_s
|
|
125
|
+
when ::Prism::ConstantPathNode
|
|
126
|
+
parts = []
|
|
127
|
+
walk = node
|
|
128
|
+
while walk.is_a?(::Prism::ConstantPathNode)
|
|
129
|
+
parts.unshift(walk.name.to_s)
|
|
130
|
+
walk = walk.parent
|
|
131
|
+
end
|
|
132
|
+
parts.unshift(walk.name.to_s) if walk.is_a?(::Prism::ConstantReadNode)
|
|
133
|
+
parts.join("::")
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Walks every method definition and records the names of methods that
|
|
139
|
+
# contain a `render` call with a `json:` keyword. Nested calls inside
|
|
140
|
+
# `respond_to do |format| format.json { render json: … } end` are
|
|
141
|
+
# caught because `super` recurses into child nodes.
|
|
142
|
+
class JsonActionVisitor < ::Prism::Visitor
|
|
143
|
+
def initialize
|
|
144
|
+
@actions = Set.new
|
|
145
|
+
@current_method = nil
|
|
146
|
+
super
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def actions
|
|
150
|
+
@actions.to_a
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def visit_def_node(node)
|
|
154
|
+
prev = @current_method
|
|
155
|
+
@current_method = node.name.to_s
|
|
156
|
+
super
|
|
157
|
+
@current_method = prev
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def visit_call_node(node)
|
|
161
|
+
if @current_method && node.name == :render && renders_json?(node)
|
|
162
|
+
@actions.add(@current_method)
|
|
163
|
+
end
|
|
164
|
+
super
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def renders_json?(call_node)
|
|
170
|
+
args = call_node.arguments&.arguments || []
|
|
171
|
+
args.any? { |arg| has_json_key?(arg) }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def has_json_key?(node)
|
|
175
|
+
case node
|
|
176
|
+
when ::Prism::KeywordHashNode, ::Prism::HashNode
|
|
177
|
+
node.elements.any? do |elem|
|
|
178
|
+
next false unless elem.respond_to?(:key)
|
|
179
|
+
key = elem.key
|
|
180
|
+
key.is_a?(::Prism::SymbolNode) && key.value.to_s == "json"
|
|
181
|
+
end
|
|
182
|
+
else
|
|
183
|
+
false
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsApiDocs
|
|
4
|
+
module Inspectors
|
|
5
|
+
# Walks a Rails route set and produces a normalized array of hashes
|
|
6
|
+
# describing each user-facing route:
|
|
7
|
+
#
|
|
8
|
+
# { verb:, path:, controller:, action:, name:, path_params: }
|
|
9
|
+
#
|
|
10
|
+
# Internal Rails routes (mailers, conductor, active_storage, etc.) are
|
|
11
|
+
# skipped. Routes without a resolvable controller/action are skipped.
|
|
12
|
+
class RouteInspector
|
|
13
|
+
INTERNAL_PREFIXES = %w[
|
|
14
|
+
/rails/info
|
|
15
|
+
/rails/conductor
|
|
16
|
+
/rails/mailers
|
|
17
|
+
/rails/active_storage
|
|
18
|
+
/rails/action_mailbox
|
|
19
|
+
/action_cable
|
|
20
|
+
/assets
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def initialize(route_set: nil, config: nil)
|
|
24
|
+
@route_set = route_set || (defined?(Rails) && Rails.application&.routes)
|
|
25
|
+
@config = config || (defined?(RailsApiDocs) ? RailsApiDocs.configuration : nil)
|
|
26
|
+
raise ArgumentError, "no route set available" unless @route_set
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call
|
|
30
|
+
@route_set.routes.flat_map { |route| extract(route) }.compact
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def extract(route)
|
|
36
|
+
controller = route.defaults[:controller]
|
|
37
|
+
action = route.defaults[:action]
|
|
38
|
+
return [] unless controller && action
|
|
39
|
+
return [] if ignored_controller?(controller.to_s)
|
|
40
|
+
return [] if only_controllers_active? && !only_controllers_match?(controller.to_s)
|
|
41
|
+
return [] if ignored_action?(action.to_s)
|
|
42
|
+
|
|
43
|
+
path = normalize_path(route.path.spec.to_s)
|
|
44
|
+
return [] if internal?(path) || user_ignored?(path)
|
|
45
|
+
|
|
46
|
+
verbs = verbs_for(route)
|
|
47
|
+
return [] if verbs.empty?
|
|
48
|
+
|
|
49
|
+
verbs.map do |verb|
|
|
50
|
+
{
|
|
51
|
+
verb: verb,
|
|
52
|
+
path: path,
|
|
53
|
+
controller: controller.to_s,
|
|
54
|
+
action: action.to_s,
|
|
55
|
+
name: route.name,
|
|
56
|
+
path_params: extract_path_params(path)
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ignored_controller?(controller)
|
|
62
|
+
Array(@config&.ignored_controllers).any? { |pattern| controller_matches?(pattern, controller) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def only_controllers_active?
|
|
66
|
+
Array(@config&.only_controllers).any?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def only_controllers_match?(controller)
|
|
70
|
+
Array(@config&.only_controllers).any? { |pattern| controller_matches?(pattern, controller) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Shared rule for matching a controller path against a user-supplied
|
|
74
|
+
# pattern. Used by both the blacklist (ignored_controllers) and the
|
|
75
|
+
# whitelist (only_controllers) so the two are symmetric.
|
|
76
|
+
#
|
|
77
|
+
# Rules:
|
|
78
|
+
# - Regexp pattern → standard regex match.
|
|
79
|
+
# - String containing "/" → exact path match. So "api/v1/users"
|
|
80
|
+
# matches only that exact controller, not the bare "users".
|
|
81
|
+
# - String without "/" → boundary-aware suffix match. So "users"
|
|
82
|
+
# matches both `users` and `api/v1/users`, but NOT `super_users`
|
|
83
|
+
# (the boundary requires a "/" before the pattern, or equality).
|
|
84
|
+
def controller_matches?(pattern, controller)
|
|
85
|
+
return pattern.match?(controller) if pattern.is_a?(Regexp)
|
|
86
|
+
|
|
87
|
+
s = pattern.to_s
|
|
88
|
+
if s.include?("/")
|
|
89
|
+
controller == s
|
|
90
|
+
else
|
|
91
|
+
controller == s || controller.end_with?("/#{s}")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def ignored_action?(action)
|
|
96
|
+
Array(@config&.ignored_actions).include?(action)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def user_ignored?(path)
|
|
100
|
+
Array(@config&.ignored_path_prefixes).any? { |prefix| path.start_with?(prefix) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def normalize_path(raw)
|
|
104
|
+
raw.sub(/\(\.:format\)\z/, "").sub(/\/\z/, "").then { |p| p.empty? ? "/" : p }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def internal?(path)
|
|
108
|
+
INTERNAL_PREFIXES.any? { |prefix| path.start_with?(prefix) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# `route.verb` is a String in modern Rails ("GET", "POST", ...).
|
|
112
|
+
# When a route accepts multiple verbs (via `match via: [:get, :post]`)
|
|
113
|
+
# it can return a regex-source string like "GET|POST" — we split it.
|
|
114
|
+
def verbs_for(route)
|
|
115
|
+
raw = route.verb.to_s
|
|
116
|
+
return [] if raw.empty?
|
|
117
|
+
|
|
118
|
+
raw.split("|").map { |v| v.upcase.strip }.reject(&:empty?)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def extract_path_params(path)
|
|
122
|
+
path.scan(/:(\w+)/).flatten
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsApiDocs
|
|
4
|
+
module Inspectors
|
|
5
|
+
# Looks up the ActiveRecord model corresponding to a controller and
|
|
6
|
+
# returns its columns metadata:
|
|
7
|
+
#
|
|
8
|
+
# { "name" => { type: :string, null: false, default: nil }, ... }
|
|
9
|
+
#
|
|
10
|
+
# If the model can't be resolved or ActiveRecord isn't loaded, returns
|
|
11
|
+
# an empty hash — schema inference is always best-effort.
|
|
12
|
+
class SchemaInspector
|
|
13
|
+
DEFAULT_RESOLVER = lambda do |controller|
|
|
14
|
+
last = controller.split("/").last.to_s
|
|
15
|
+
model_name = last.singularize.camelize
|
|
16
|
+
Object.const_get(model_name) if Object.const_defined?(model_name)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(controller:, model_resolver: nil)
|
|
20
|
+
@controller = controller
|
|
21
|
+
@model_resolver = model_resolver || DEFAULT_RESOLVER
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call
|
|
25
|
+
model = @model_resolver.call(@controller)
|
|
26
|
+
return {} unless model && model.respond_to?(:columns_hash)
|
|
27
|
+
|
|
28
|
+
model.columns_hash.each_with_object({}) do |(name, col), acc|
|
|
29
|
+
acc[name.to_s] = { type: col.type, null: col.null, default: col.default }
|
|
30
|
+
end
|
|
31
|
+
rescue StandardError
|
|
32
|
+
{}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsApiDocs
|
|
4
|
+
# Single source of truth for type-derived placeholder values. Used by:
|
|
5
|
+
# - BodyInferrer → seeds `example:` for inferred body fields
|
|
6
|
+
# - Builder → seeds `example:` for inferred path params
|
|
7
|
+
# - CurlRenderer → fills body fields in `--data` when no example is set
|
|
8
|
+
# - Renderer → synthesizes a default response example from body schema
|
|
9
|
+
#
|
|
10
|
+
# The mapping is intentionally narrow — for niche types it returns nil
|
|
11
|
+
# rather than guessing, which JSON-encodes to `null` and signals to the
|
|
12
|
+
# user "we don't know, fill this in".
|
|
13
|
+
module SampleValue
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def for(type)
|
|
17
|
+
case type.to_s
|
|
18
|
+
when "string", "text" then "example"
|
|
19
|
+
when "integer", "bigint" then 1
|
|
20
|
+
when "float", "decimal" then 1.0
|
|
21
|
+
when "boolean" then true
|
|
22
|
+
when "date" then "2026-01-01"
|
|
23
|
+
when "datetime", "time" then "2026-01-01T00:00:00Z"
|
|
24
|
+
else nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :"rails-api-docs" do
|
|
4
|
+
desc "Generate the API docs HTML at public/api-docs.html from config/rails-api-docs.yml.\n" \
|
|
5
|
+
"Override paths with CONFIG= and OUTPUT= env vars if needed."
|
|
6
|
+
task build: :environment do
|
|
7
|
+
config_relative = ENV["CONFIG"] || RailsApiDocs.configuration.config_path
|
|
8
|
+
output_relative = ENV["OUTPUT"] || RailsApiDocs.configuration.output_path
|
|
9
|
+
|
|
10
|
+
config_path = Rails.root.join(config_relative).to_s
|
|
11
|
+
output_path = Rails.root.join(output_relative).to_s
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
written = RailsApiDocs::Doc::FileBuilder.new(
|
|
15
|
+
config_path: config_path,
|
|
16
|
+
output_path: output_path
|
|
17
|
+
).call
|
|
18
|
+
|
|
19
|
+
puts "[rails-api-docs] wrote #{written} (#{File.size(written)} bytes)"
|
|
20
|
+
rescue RailsApiDocs::Doc::FileBuilder::MissingConfigError => e
|
|
21
|
+
warn "[rails-api-docs] #{e.message}"
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|