{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "api-snippet", "title": "API Snippet", "description": "Tabbed API call reference (curl, HTTPie, TypeScript SDK, fctl) rendered from an OpenAPI operationId. Defaults to the stack operations index; pass a different `operations` index and select tabs via the `tabs` prop for other APIs (e.g. membership/cloud).", "registryDependencies": [ "https://ds.formance.com/r/endpoint.json", "https://ds.formance.com/r/code-snippet.json", "https://ds.formance.com/r/tabs.json", "https://ds.formance.com/r/stack-operations.json", "https://ds.formance.com/r/copy-button.json" ], "files": [ { "path": "registry/default/ui-fragments/api-snippet.tsx", "content": "'use client';\n\nimport { useMemo, useState } from 'react';\n\nimport { cn } from '@/lib/utils';\nimport stackOperationsData from '@/registry/default/lib/stack-operations.json';\nimport {\n generateCurl,\n generateHttpie,\n generateSdk,\n resolvePathParams,\n type TStackOperation,\n type TStackOperationsIndex,\n} from '@/components/ui-fragments/_api-snippet/generators';\nimport { CopyButton } from '@/registry/default/ui-fragments/copy-button';\nimport { CodeSnippet } from '@/registry/default/ui/code/code-snippet';\nimport { Endpoint } from '@/registry/default/ui/endpoint';\nimport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@/registry/default/ui/tabs';\n\nconst stackOperations = (stackOperationsData as TStackOperationsIndex)\n .operations;\n\nexport type TApiSnippetTab = 'curl' | 'httpie' | 'sdk' | 'fctl';\n\nconst TAB_LABEL: Record = {\n curl: 'curl',\n httpie: 'HTTPie',\n sdk: 'TypeScript',\n fctl: 'fctl',\n};\n\nconst TAB_LANGUAGE: Record = {\n curl: 'bash',\n httpie: 'bash',\n sdk: 'typescript',\n fctl: 'bash',\n};\n\nexport type TApiSnippetProps = {\n /**\n * OpenAPI operationId, resolved against the `operations` index. Optional —\n * omit it to render an fctl-only snippet (`tabs={['fctl']}`) with no\n * generated request and no endpoint footer.\n */\n operation?: string;\n /**\n * Operations index the `operation` is resolved against. Defaults to the\n * bundled stack operations. Pass a different index (e.g. membership/cloud\n * operations) to generate snippets for another API.\n */\n operations?: Record;\n /**\n * HTTP method, used directly instead of resolving from `operation`. Takes\n * precedence over the resolved operation's method when both are provided.\n */\n method?: string;\n /**\n * API path, used directly instead of resolving from `operation`. May include\n * a query string (`?a=b`). Takes precedence over the resolved operation's path.\n */\n path?: string;\n /** Path parameter values, e.g. `{ ledger: 'testing' }`. */\n params?: Record;\n /** Request body. JSON-serialized for curl/sdk, flattened to key=value for HTTPie. */\n body?: Record;\n /**\n * Reference the request body from a file instead of inlining it\n * (`curl -d @file.json`, `http < file.json`). Takes precedence over `body`\n * for the curl/HTTPie tabs.\n */\n bodyFile?: string;\n /** Extra raw args appended to the generated curl/HTTPie command. */\n rawArgs?: string;\n /** Extra request headers. */\n headers?: Record;\n /** Override the base URL placeholder. */\n baseUrl?: string;\n /** fctl one-liner. Required for the `fctl` tab to render. */\n fctl?: string;\n /**\n * Which tabs to show, in order. Defaults to `['curl', 'httpie', 'sdk']`,\n * with `'fctl'` prepended when a `fctl` string is provided. A requested tab\n * is only rendered when it has content (`'fctl'` needs the `fctl` prop,\n * `'sdk'` needs the operation to declare an SDK module/method).\n */\n tabs?: TApiSnippetTab[];\n /** Initial tab (uncontrolled). Falls back to the first visible tab when not shown. */\n defaultTab?: TApiSnippetTab;\n /**\n * Controlled active tab. When provided, the component does not manage tab\n * state internally — pair with `onValueChange` to drive it from the host\n * (e.g. to persist the curl/HTTPie choice across snippets).\n */\n value?: TApiSnippetTab;\n /** Called with the selected tab whenever the user switches tabs. */\n onValueChange?: (tab: TApiSnippetTab) => void;\n /**\n * Cap the visible height of the code area. When true, long snippets scroll\n * inside the box instead of expanding the parent layout.\n */\n clipCodeHeight?: boolean;\n className?: string;\n};\n\nconst CLIPPED_CODE_HEIGHT = 240;\n\nexport function ApiSnippet({\n operation,\n operations = stackOperations,\n method: methodProp,\n path: pathProp,\n params,\n body,\n bodyFile,\n rawArgs,\n headers,\n baseUrl,\n fctl,\n tabs,\n defaultTab = 'curl',\n value,\n onValueChange,\n clipCodeHeight = false,\n className,\n}: TApiSnippetProps) {\n const codeStyle = clipCodeHeight\n ? { maxHeight: CLIPPED_CODE_HEIGHT }\n : undefined;\n const codeClassName = clipCodeHeight ? 'm-0 overflow-y-auto' : 'm-0';\n const op = operation\n ? (operations[operation] as TStackOperation | undefined)\n : undefined;\n const method = methodProp ?? op?.method ?? 'GET';\n const rawPath = pathProp ?? op?.path ?? '';\n const resolvedPath = resolvePathParams(rawPath, params);\n // Fail closed: a request needs a resolved path (or a known operation).\n // `method` alone is an incomplete override and shouldn't render a snippet.\n const hasRequest = !!op || !!resolvedPath;\n\n const curl = useMemo(\n () =>\n generateCurl(\n method,\n resolvedPath,\n body,\n headers,\n baseUrl,\n bodyFile,\n rawArgs\n ),\n [method, resolvedPath, body, headers, baseUrl, bodyFile, rawArgs]\n );\n const httpie = useMemo(\n () =>\n generateHttpie(\n method,\n resolvedPath,\n body,\n headers,\n baseUrl,\n bodyFile,\n rawArgs\n ),\n [method, resolvedPath, body, headers, baseUrl, bodyFile, rawArgs]\n );\n const sdk = useMemo(\n () => (op ? generateSdk(op, params, body) : ''),\n [op, params, body]\n );\n\n const hasContent: Record = {\n curl: hasRequest,\n httpie: hasRequest,\n sdk: !!op?.sdk,\n fctl: !!fctl,\n };\n const requestedTabs =\n tabs ??\n (fctl ? ['fctl', 'curl', 'httpie', 'sdk'] : ['curl', 'httpie', 'sdk']);\n const visibleTabs = requestedTabs.filter((t) => hasContent[t]);\n const isSingleTab = visibleTabs.length === 1;\n const initialTab = visibleTabs.includes(defaultTab)\n ? defaultTab\n : (visibleTabs[0] ?? 'curl');\n const [internalTab, setInternalTab] = useState(initialTab);\n const currentTab = value ?? internalTab;\n const activeTab = visibleTabs.includes(currentTab)\n ? currentTab\n : (visibleTabs[0] ?? 'curl');\n const handleTabChange = (next: TApiSnippetTab) => {\n if (value === undefined) setInternalTab(next);\n onValueChange?.(next);\n };\n\n if (visibleTabs.length === 0) {\n if (operation && !op) {\n return (\n \n Unknown operation: {operation}\n \n );\n }\n\n return null;\n }\n\n const codeByTab: Record = {\n curl,\n httpie,\n sdk,\n fctl: fctl ?? '',\n };\n\n return (\n \n handleTabChange(v as TApiSnippetTab)}\n className=\"gap-0\"\n >\n
\n \n {visibleTabs.map((t) => (\n \n {TAB_LABEL[t]}\n \n ))}\n \n \n
\n {visibleTabs.map((t) => (\n \n \n \n ))}\n \n {hasRequest && (\n
\n \n
\n )}\n \n );\n}\n", "type": "registry:component", "target": "components/ui-fragments/api-snippet.tsx" }, { "path": "registry/default/ui-fragments/_api-snippet/generators.ts", "content": "export type TStackOperation = {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n path: string;\n pathParams: string[];\n /**\n * SDK module/method for the TypeScript snippet. Absent for operations that\n * have no public TypeScript SDK (e.g. the membership/cloud API), in which\n * case the `sdk` tab is not rendered.\n */\n sdk?: { module: string; method: string };\n summary?: string;\n tags?: string[];\n};\n\nexport type TStackOperationsIndex = {\n _meta?: { specVersion?: string; specFile?: string };\n operations: Record;\n};\n\nexport function resolvePathParams(\n path: string,\n params?: Record\n): string {\n if (!params) return path;\n let resolved = path;\n for (const [k, v] of Object.entries(params))\n resolved = resolved.replace(`{${k}}`, v);\n\n return resolved;\n}\n\n/**\n * Wrap a value in shell single quotes, escaping any embedded single quotes via\n * the `'\\''` idiom. Keeps generated commands valid for filenames with spaces\n * and JSON payloads containing apostrophes (e.g. `{\"name\":\"Alice's ledger\"}`).\n */\nfunction shellSingleQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nexport function generateCurl(\n method: string,\n path: string,\n body?: unknown,\n headers?: Record,\n baseUrl = '$FORMANCE_API_URL',\n bodyFile?: string,\n rawArgs?: string\n): string {\n const parts = [`curl -X ${method} ${baseUrl}${path}`];\n if (headers) {\n for (const [k, v] of Object.entries(headers))\n parts.push(` -H \"${k}: ${v}\"`);\n }\n if (bodyFile) {\n parts.push(` -H \"Content-Type: application/json\"`);\n parts.push(` -d @${shellSingleQuote(bodyFile)}`);\n } else if (body) {\n const formatted = JSON.stringify(body, null, 2);\n const indented = formatted\n .split('\\n')\n .map((l, i) => (i === 0 ? l : ' ' + l))\n .join('\\n');\n parts.push(` -H \"Content-Type: application/json\"`);\n parts.push(` -d ${shellSingleQuote(indented)}`);\n }\n if (rawArgs) parts.push(` ${rawArgs}`);\n\n return parts.join(' \\\\\\n');\n}\n\nfunction jsonDepth(val: unknown): number {\n if (typeof val !== 'object' || val === null) return 0;\n if (Array.isArray(val)) return 1 + Math.max(0, ...val.map(jsonDepth));\n\n return (\n 1 +\n Math.max(0, ...Object.values(val as Record).map(jsonDepth))\n );\n}\n\nfunction flattenHttpieArgs(\n obj: Record,\n prefix: string,\n parts: string[],\n depth = 0\n) {\n for (const [k, v] of Object.entries(obj)) {\n const key = prefix ? `${prefix}[${k}]` : k;\n if (v === null || v === undefined) continue;\n if (typeof v === 'string') {\n const needsQuote = /[\\s[\\](){}$@|&;!<>'\"\\\\]/.test(v);\n parts.push(\n needsQuote ? ` ${key}=${shellSingleQuote(v)}` : ` ${key}=${v}`\n );\n } else if (typeof v === 'number' || typeof v === 'boolean') {\n parts.push(` ${key}:=${String(v)}`);\n } else if (Array.isArray(v)) {\n parts.push(` ${key}:=${shellSingleQuote(JSON.stringify(v))}`);\n } else if (typeof v === 'object') {\n if (depth >= 1 || Object.keys(v as object).length === 0) {\n parts.push(` ${key}:=${shellSingleQuote(JSON.stringify(v))}`);\n } else {\n flattenHttpieArgs(v as Record, key, parts, depth + 1);\n }\n }\n }\n}\n\nexport function generateHttpie(\n method: string,\n path: string,\n body?: unknown,\n headers?: Record,\n baseUrl = '$FORMANCE_API_URL',\n bodyFile?: string,\n rawArgs?: string\n): string {\n const verb = method === 'GET' ? '' : method + ' ';\n const [basePath, queryString] = path.split('?');\n const parts = [`http ${verb}${baseUrl}${basePath}`];\n if (queryString) {\n for (const param of queryString.split('&')) {\n const [k, v] = param.split('=');\n parts.push(` ${k}==${v}`);\n }\n }\n if (headers) {\n for (const [k, v] of Object.entries(headers)) parts.push(` ${k}:${v}`);\n }\n if (bodyFile) {\n parts.push(` < ${shellSingleQuote(bodyFile)}`);\n } else if (body && typeof body === 'object' && !Array.isArray(body)) {\n if (jsonDepth(body) > 2) {\n const indented = JSON.stringify(body, null, 2)\n .split('\\n')\n .map((l, i) => (i === 0 ? l : ' ' + l))\n .join('\\n');\n parts.push(` <<< ${shellSingleQuote(indented)}`);\n } else {\n flattenHttpieArgs(body as Record, '', parts);\n }\n } else if (body) {\n parts.push(` <<< ${shellSingleQuote(JSON.stringify(body))}`);\n }\n if (rawArgs) parts.push(` ${rawArgs}`);\n\n return parts.join(' \\\\\\n');\n}\n\nexport function generateSdk(\n op: TStackOperation,\n params?: Record,\n body?: Record\n): string {\n if (!op.sdk) return '';\n\n const args: Record = { ...(params ?? {}) };\n if (body && typeof body === 'object' && !Array.isArray(body))\n Object.assign(args, body);\n\n const argsBlock = Object.keys(args).length\n ? JSON.stringify(args, null, 2)\n .split('\\n')\n .map((l, i) => (i === 0 ? l : ' ' + l))\n .join('\\n')\n : '{}';\n\n return [\n `import { SDK } from '@formance/formance-sdk';`,\n ``,\n `const client = new SDK({ serverURL: process.env.FORMANCE_API_URL });`,\n ``,\n `const response = await client.${op.sdk.module}.${op.sdk.method}(${argsBlock});`,\n ].join('\\n');\n}\n", "type": "registry:component", "target": "components/ui-fragments/_api-snippet/generators.ts" } ], "type": "registry:component" }