Class: RailsApiDocs::Doc::Renderer

Inherits:
Object
  • Object
show all
Defined in:
lib/rails-api-docs/doc/renderer.rb

Overview

Turns the parsed YAML config into the final self-contained HTML document. Used by both the rake task (writes to public/api-docs.html) and the dev controller mounted at /rails/api-docs.

The output is a single HTML string with CSS and JS inlined — no external assets, no asset pipeline required.

Constant Summary collapse

TEMPLATE_PATH =
File.expand_path("../templates/api_docs.html.erb", __dir__)
DEFAULT_GENERAL =
{
  "title"           => "API Documentation",
  "base_url"        => "https://api.example.com",
  "primary_color"   => "#CC0000",
  "secondary_color" => "#2E2E2E",
  "accent_color"    => "#D30001",
  "font_family"     => "system-ui, -apple-system, sans-serif",
  "show_curl"       => true,
  "show_examples"   => true
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Renderer

Returns a new instance of Renderer.



28
29
30
# File 'lib/rails-api-docs/doc/renderer.rb', line 28

def initialize(config)
  @config = config || {}
end

Instance Method Details

#auth_label(auth) ⇒ Object



145
146
147
148
149
150
151
152
# File 'lib/rails-api-docs/doc/renderer.rb', line 145

def auth_label(auth)
  case auth.to_s.downcase
  when "bearer" then "Bearer auth"
  when "basic"  then "Basic auth"
  when "none"   then "No auth"
  else               auth.to_s
  end
end

#body_present?(endpoint) ⇒ Boolean

Returns:

  • (Boolean)


117
118
119
# File 'lib/rails-api-docs/doc/renderer.rb', line 117

def body_present?(endpoint)
  endpoint["body"].is_a?(Array) && !endpoint["body"].empty?
end

#callObject



32
33
34
# File 'lib/rails-api-docs/doc/renderer.rb', line 32

def call
  ERB.new(File.read(TEMPLATE_PATH), trim_mode: "-").result(binding)
end

#curl_for(endpoint) ⇒ Object



86
87
88
# File 'lib/rails-api-docs/doc/renderer.rb', line 86

def curl_for(endpoint)
  CurlRenderer.new(endpoint, base_url: general["base_url"]).call
end

#empty?Boolean

Returns:

  • (Boolean)


53
54
55
# File 'lib/rails-api-docs/doc/renderer.rb', line 53

def empty?
  visible_sections.empty?
end

#endpoint_id(section_key, endpoint) ⇒ Object



61
62
63
64
65
# File 'lib/rails-api-docs/doc/renderer.rb', line 61

def endpoint_id(section_key, endpoint)
  method = endpoint["method"].to_s.downcase
  path   = endpoint["path"].to_s.gsub(/[^a-z0-9]+/i, "-").gsub(/^-+|-+$/, "")
  "#{section_slug(section_key)}--#{method}--#{path}"
end

#first_endpoint_idObject



79
80
81
82
83
84
# File 'lib/rails-api-docs/doc/renderer.rb', line 79

def first_endpoint_id
  visible_sections.each do |key, section|
    return endpoint_id(key, section["endpoints"].first)
  end
  nil
end

#generalObject

template helpers — public so ERB binding can reach them =====


38
39
40
# File 'lib/rails-api-docs/doc/renderer.rb', line 38

def general
  @general ||= DEFAULT_GENERAL.merge(@config["general_configurations"] || {})
end

#h(value) ⇒ Object



57
58
59
# File 'lib/rails-api-docs/doc/renderer.rb', line 57

def h(value)
  ERB::Util.html_escape(value.to_s)
end

#headers_present?(endpoint) ⇒ Boolean

Returns:

  • (Boolean)


121
122
123
# File 'lib/rails-api-docs/doc/renderer.rb', line 121

def headers_present?(endpoint)
  endpoint["headers"].is_a?(Array) && !endpoint["headers"].empty?
end

#params_present?(endpoint) ⇒ Boolean

Returns:

  • (Boolean)


125
126
127
# File 'lib/rails-api-docs/doc/renderer.rb', line 125

def params_present?(endpoint)
  endpoint["params"].is_a?(Array) && !endpoint["params"].empty?
end

#render_field(field) ⇒ Object

Renders a single field row with all supported attributes. Used uniformly by body, params, headers, and response schema — one source of truth for badge rendering.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/rails-api-docs/doc/renderer.rb', line 157

def render_field(field)
  parts = []
  parts << %(<div class="field">)
  parts << %(<div class="field-row">)
  parts << %(<span class="field-name">#{h(field["name"])}</span>)
  parts << %(<span class="field-type">#{h(field["type"])}</span>) if field["type"]
  parts << %(<span class="field-badge format">#{h(field["format"])}</span>)         if field["format"]
  parts << %(<span class="field-badge in-path">#{h(field["in"])}</span>)             if field["in"]
  parts << %(<span class="field-badge readonly">read-only</span>)                    if field["read_only"]
  parts << %(<span class="field-badge writeonly">write-only</span>)                  if field["write_only"]
  parts << %(<span class="field-badge nullable">nullable</span>)                     if field["nullable"]
  parts << %(<span class="field-badge required">Required</span>)                     if field["required"]

  min = field["min"] || field["min_length"]
  max = field["max"] || field["max_length"]
  parts << %(<span class="field-meta">min: #{h(min)}</span>)                         if min
  parts << %(<span class="field-meta">max: #{h(max)}</span>)                         if max
  parts << %(<span class="field-meta">default: #{h(field["default"])}</span>)        unless field["default"].nil?
  parts << %(<span class="field-meta mono">pattern: #{h(field["pattern"])}</span>)   if field["pattern"]
  parts << %(</div>)

  if field["enum"].is_a?(Array) && !field["enum"].empty?
    values = field["enum"].map { |v| %(<code>#{h(v)}</code>) }.join(" · ")
    parts << %(<div class="field-enum">one of: #{values}</div>)
  end

  parts << %(<div class="field-desc">#{h(field["description"])}</div>) if field["description"] && !field["description"].to_s.empty?
  parts << %(<div class="field-example">Example: <code>#{h(field["example"])}</code></div>) unless field["example"].nil?

  parts << %(</div>)
  parts.join("\n")
end

#render_fields(fields) ⇒ Object

Helper: render an array of fields as joined HTML. Use from the template like ‘<%= render_fields(endpoint) -%>`.



192
193
194
# File 'lib/rails-api-docs/doc/renderer.rb', line 192

def render_fields(fields)
  Array(fields).map { |f| render_field(f) }.join("\n")
end

#responses_for(endpoint) ⇒ Object

Returns an array of { status:, description:, example:, headers:, schema: } always in YAML insertion order. If the user defined ‘responses:` in the YAML we honor it verbatim; otherwise we synthesize a single “200” entry from `response_example` or the inferred body sample.



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/rails-api-docs/doc/renderer.rb', line 94

def responses_for(endpoint)
  if endpoint["responses"].is_a?(Hash) && !endpoint["responses"].empty?
    endpoint["responses"].map do |status, resp|
      resp ||= {}
      {
        "status"      => status.to_s,
        "description" => resp["description"].to_s,
        "example"     => resp["example"].to_s,
        "headers"     => Array(resp["headers"]),
        "schema"      => Array(resp["schema"])
      }
    end
  else
    [{
      "status"      => "200",
      "description" => "",
      "example"     => default_response_example(endpoint),
      "headers"     => [],
      "schema"      => []
    }]
  end
end

#responses_have_details?(endpoint) ⇒ Boolean

Returns:

  • (Boolean)


129
130
131
132
133
# File 'lib/rails-api-docs/doc/renderer.rb', line 129

def responses_have_details?(endpoint)
  responses_for(endpoint).any? do |r|
    !r["headers"].empty? || !r["schema"].empty? || !r["description"].empty?
  end
end

#section_slug(key) ⇒ Object



67
68
69
# File 'lib/rails-api-docs/doc/renderer.rb', line 67

def section_slug(key)
  key.to_s.gsub(/[^a-z0-9]+/i, "-")
end

Returns the ‘data-endpoint-tags=’‘` attribute (with leading space) for a sidebar <li>, or an empty string when the endpoint has no tags. JSON encoding is intentional — robust against tag values that contain spaces or special characters.



139
140
141
142
143
# File 'lib/rails-api-docs/doc/renderer.rb', line 139

def sidebar_tags_attr(endpoint)
  tags = Array(endpoint["tags"])
  return "" if tags.empty?
  %( data-endpoint-tags='#{h(tags.to_json)}')
end

#status_class(code) ⇒ Object



196
197
198
199
200
201
202
203
204
# File 'lib/rails-api-docs/doc/renderer.rb', line 196

def status_class(code)
  case code.to_s[0]
  when "2" then "status-2xx"
  when "3" then "status-3xx"
  when "4" then "status-4xx"
  when "5" then "status-5xx"
  else          ""
  end
end

#verb_class(method) ⇒ Object



71
72
73
# File 'lib/rails-api-docs/doc/renderer.rb', line 71

def verb_class(method)
  "verb-#{method.to_s.downcase}"
end

#verb_label(method) ⇒ Object



75
76
77
# File 'lib/rails-api-docs/doc/renderer.rb', line 75

def verb_label(method)
  method.to_s.upcase == "DELETE" ? "DEL" : method.to_s.upcase
end

#visible_sectionsObject



42
43
44
45
46
47
48
49
50
51
# File 'lib/rails-api-docs/doc/renderer.rb', line 42

def visible_sections
  @visible_sections ||= (@config["sections"] || {}).each_with_object({}) do |(key, section), acc|
    next if section["show"] == false

    endpoints = (section["endpoints"] || []).reject { |e| e["show"] == false }
    next if endpoints.empty?

    acc[key] = section.merge("endpoints" => endpoints)
  end
end