Skip to content

feat(page): DocsUI::RequestExample + JsonResponse — one request definition, every client tab#31

Merged
mhenrixon merged 1 commit into
mainfrom
issue-15-request-example-json-response
Jul 3, 2026
Merged

feat(page): DocsUI::RequestExample + JsonResponse — one request definition, every client tab#31
mhenrixon merged 1 commit into
mainfrom
issue-15-request-example-json-response

Conversation

@mhenrixon

Copy link
Copy Markdown
Owner

Closes #15.

Problem

An endpoint example was authored 5–7 times — curl, CLI, JavaScript, Ruby, Python heredocs (one builder method per client per endpoint), plus a hand-pretty-printed JSON response with deep_stringify copy-pasted 3×. A field rename meant editing up to 7 places. The gem's Example was just a tab container; there was no way to define a request once and derive the language tabs.

What this ships

One structured request declaration renders every configured client tab; one Ruby hash renders a pretty JSON response block.

DocsUI::Section("Create a payment link",
  description: DocsUI::Endpoint.new(:post, "/v1/payment_links")) do

  render DocsUI::RequestExample.new(
    method: :post, path: "/v1/payment_links",
    body: { amount: 4900, currency: "usd", description: "Pro plan" }
  )
  render DocsUI::JsonResponse.new(
    { id: "plink_1a2b3c", object: "payment_link", amount: 4900, currency: "usd" }
  )
end

Value objects (lib/docs_kit/)

  • DocsKit::ApiRequest (Data.define) — method/path/url/query/headers/body, plus derived #http_method, #body?, #pretty_body_json (deep-stringified), #url_with_query. This is what templates receive, so each stays one short heredoc.
  • DocsKit::ApiClient (Data.define) — label/lexer/filename (String or proc)/template ((request) -> String). Ships four generic-HTTP defaults (curl, javascript, ruby, python) in ApiClient::DEFAULTS, backed by DocsKit::ApiTemplates.

Config (all defaulted — backwards compatible)

Knob Default Purpose
c.api_clients the four defaults Ordered { token => ApiClient } merged over the defaults — reuse a token to swap in an SDK-flavored snippet, new token to append a tab (e.g. cli). Order stable.
c.api_base_url "https://api.example.com" Prefixed onto each request path.
c.api_auth_header nil Example Authorization: line merged into every snippet; nil ⇒ no auth line.

Components (app/components/docs_ui/)

  • DocsUI::JsonResponse.new(body, filename: "response.json") — Hash → deep-stringified + JSON.pretty_generate; String → passthrough; renders DocsUI::Code(lexer: :json).
  • DocsUI::RequestExample.new(method:, path:, body:, query:, headers:, clients:) — composes DocsUI::Example, one tab per selected client, so the global sticky language preference keeps working. clients: filters/orders the tabs.
  • DocsUI::Example#code gains an optional label: (additive — existing behavior unchanged) so a client carries its own tab name.

The gem ships generic HTTP templates only — it can't know a site's SDK. app-4's SDK flavors and its cli tab become c.api_clients overrides.

Test coverage

  • api_request_spec — derived helpers; deep-stringify across Hash/Array/Symbol; query encoding; String passthrough.
  • api_client_spec — value object + filename_for (String/proc); the four default templates asserting body-present vs body-less GET, and the auth-header line.
  • configuration_specapi_clients defaults/order/merge/override; api_base_url/api_auth_header defaults + overrides.
  • json_response_spec — Hash pretty-print with string keys, String passthrough, HTML-escape safety via the Rouge path.
  • request_example_spec — a POST renders 4 tabs; curl carries api_base_url+method+-d body; auth header injected; clients: [:ruby, :curl] filters and orders; a custom client from config renders its own label; a body-less GET omits payload lines in every default template; Example's progressive-enhancement markup (no server-side hidden panels).

Also smoke-tested the exact dogfood composition (RequestExample + JsonResponse in an endpoint Section) renders clean end-to-end.

Verification

  • bundle exec rake (rspec + rubocop) — 232 examples, 0 failures; no offenses
  • DocsKit::Configuration still 100% covered; suite 93% line coverage (floor 80%)
  • Backwards compatible — DocsUI::Example behavior preserved (additive optional label:), all new config knobs defaulted
  • Dogfooded — an "API-docs kit" section on the components page; README "API docs" section

Out of scope (per the issue)

OpenAPI import (the request struct is the stepping stone a bridge would target later); response generation from live serializers; WebSocket/streaming shapes.

…y client tab

## Summary
Declare an API request once and render one code tab per configured client;
render a Ruby hash as a pretty JSON response. Kills the per-client heredoc
duplication (curl/CLI/JS/Ruby/Python authored 5× per endpoint) and the
copy-pasted deep_stringify.

- `DocsKit::ApiRequest` (Data) — method/path/url/query/headers/body + derived
  #http_method, #body?, #pretty_body_json (deep-stringified), #url_with_query.
- `DocsKit::ApiClient` (Data) — label/lexer/filename/template; ships four
  generic-HTTP defaults (curl, javascript, ruby, python) via ApiTemplates.
- Config: `c.api_clients` (ordered, merged OVER defaults — reuse a token to
  replace, new token to append), `c.api_base_url` ("https://api.example.com"),
  `c.api_auth_header` (nil ⇒ no auth line). All defaulted → backwards compatible.
- `DocsUI::JsonResponse.new(body, filename:)` — Hash → pretty JSON with string
  keys; String → passthrough; renders through DocsUI::Code(lexer: :json).
- `DocsUI::RequestExample.new(method:, path:, body:, query:, headers:, clients:)`
  — composes DocsUI::Example so the sticky global language preference keeps
  working; `clients:` filters/orders the tabs.
- `DocsUI::Example#code` gains an optional `label:` (additive, backwards
  compatible) so a client carries its own tab name.
- Dogfooded: an "API-docs kit" section on the components page (Endpoint +
  FieldTable + RequestExample + JsonResponse). README "API docs" section.

## Test Coverage
- api_request_spec: derived helpers, deep-stringify (Hash/Array/Symbol), query.
- api_client_spec: value object + filename_for (String/proc) + the four default
  templates (body-present vs body-less GET, auth header line).
- configuration_spec: api_clients defaults/order/merge/override, base_url/auth.
- json_response_spec: Hash pretty-print, String passthrough, HTML-escape safety.
- request_example_spec: 4 tabs, curl base_url+method+body, auth injection,
  clients: filter/order, custom client tab, body-less GET omits payload lines,
  progressive-enhancement markup (no server-side hidden panels).

## Verification
- [x] bundle exec rake (rspec + rubocop) — 232 examples, 0 failures; no offenses
- [x] Configuration still 100% covered; suite 93% line coverage (floor 80%)
- [x] DocsUI::Example behavior preserved; all new config knobs defaulted

Closes #15
@mhenrixon mhenrixon self-assigned this Jul 3, 2026
@mhenrixon mhenrixon added the enhancement New feature or request label Jul 3, 2026
@mhenrixon mhenrixon merged commit 911935a into main Jul 3, 2026
3 checks passed
@mhenrixon mhenrixon deleted the issue-15-request-example-json-response branch July 4, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(api-docs): DocsUI::RequestExample + JsonResponse — one request definition, every client tab

1 participant