From 9357a49d5e8832b4d55f4b6ea23846d9a3cd9242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Fri, 14 Aug 2026 21:28:36 +0200 Subject: [PATCH 1/3] feat(api): improve codesamples Use samples provided by individual SDKs in favor of locally trying to built valid samples. --- package-lock.json | 2 +- package.json | 2 +- src/components/ApiDocs/ExampleRequest.astro | 10 +- src/content.config.ts | 3 +- src/lib/codesamples/dotnet.ts | 208 ----------------- src/lib/codesamples/go.ts | 151 ------------- src/lib/codesamples/index.test.ts | 173 +++----------- src/lib/codesamples/index.ts | 72 +++++- src/lib/codesamples/java.ts | 237 -------------------- src/lib/codesamples/node.ts | 124 ---------- src/lib/codesamples/python.ts | 156 ------------- src/lib/codesamples/rust.ts | 210 ----------------- src/lib/codesamples/util.ts | 25 +-- 13 files changed, 108 insertions(+), 1265 deletions(-) delete mode 100644 src/lib/codesamples/dotnet.ts delete mode 100644 src/lib/codesamples/go.ts delete mode 100644 src/lib/codesamples/java.ts delete mode 100644 src/lib/codesamples/node.ts delete mode 100644 src/lib/codesamples/python.ts delete mode 100644 src/lib/codesamples/rust.ts diff --git a/package-lock.json b/package-lock.json index 622d5069..9d077457 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "starlight-links-validator": "^0.25.3", "starlight-llms-txt": "0.11.0", "tsx": "4.23.12", - "typescript": "^6.0.3", + "typescript": "6.0.3", "typescript-eslint": "8.64.0", "vitest": "4.1.10", "wrangler": "4.127.1" diff --git a/package.json b/package.json index e95e2ae1..2b9c861b 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "starlight-links-validator": "^0.25.3", "starlight-llms-txt": "0.11.0", "tsx": "4.23.12", - "typescript": "^6.0.3", + "typescript": "6.0.3", "typescript-eslint": "8.64.0", "vitest": "4.1.10", "wrangler": "4.127.1" diff --git a/src/components/ApiDocs/ExampleRequest.astro b/src/components/ApiDocs/ExampleRequest.astro index 5c1d010e..fa72c11d 100644 --- a/src/components/ApiDocs/ExampleRequest.astro +++ b/src/components/ApiDocs/ExampleRequest.astro @@ -1,14 +1,15 @@ --- import { MultiCode } from "@components/Code"; import { + cli, curl, dotnet, go, java, - node, php, python, rust, + typescript, } from "@lib/codesamples"; import type { OperationObject } from "src/types/openapi"; import OperationEndpoint from "./OperationEndpoint.astro"; @@ -28,9 +29,14 @@ const { operation } = Astro.props; code: curl(operation), lang: "bash", }, + { + label: "CLI", + code: cli(operation), + lang: "bash", + }, { label: "JavaScript", - code: node(operation), + code: typescript(operation), lang: "ts", }, { diff --git a/src/content.config.ts b/src/content.config.ts index c626a5b6..377fa0f2 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -1,7 +1,8 @@ import { docsLoader } from "@astrojs/starlight/loaders"; import { docsSchema } from "@astrojs/starlight/schema"; import { glob } from "astro/loaders"; -import { defineCollection, z } from "astro:content"; +import { z } from "astro/zod"; +import { defineCollection } from "astro:content"; import { openapiDescriptionsLoader } from "./loaders/openapiDescriptions"; const help = defineCollection({ diff --git a/src/lib/codesamples/dotnet.ts b/src/lib/codesamples/dotnet.ts deleted file mode 100644 index 06f27cf6..00000000 --- a/src/lib/codesamples/dotnet.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { resolveSchema } from "@lib/openapi"; -import { Case } from "change-case-all"; -import type { OpenAPIV3_1 } from "openapi-types"; -import type { OperationObject } from "src/types/openapi"; -import { getParameterExample, getRequestBodyExample } from "./util"; - -const INDENT = " "; - -const indentMultiline = (text: string, indent: string): string => { - return text.replace(/\n/g, `\n${indent}`); -}; - -const formatPrimitive = (value: string | number | boolean | null): string => { - if (typeof value === "string") { - return JSON.stringify(value); - } - - if (typeof value === "number" || typeof value === "boolean") { - return `${value}`; - } - - if (value === null) { - return "null"; - } - - return "null"; -}; - -const getSchemaTypeName = ( - schema: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject, - fallback: string, -): string => { - if ("$ref" in schema) { - return Case.pascal(schema.$ref.split("/").pop()!); - } - - if ("title" in schema && schema.title) { - return Case.pascal(schema.title); - } - - return Case.pascal(fallback); -}; - -const generateArrayInitializer = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - values: any[], - schema: OpenAPIV3_1.SchemaObject, - indentLevel: number, - propertyName: string, -): string => { - if (values.length === 0) { - return "Array.Empty()"; - } - - const firstValue = values[0]; - - if (typeof firstValue === "object" && firstValue !== null) { - const itemsSchema = - "items" in schema && schema.items ? resolveSchema(schema.items) : {}; - const typeName = - "items" in schema && schema.items - ? getSchemaTypeName(schema.items, `${Case.pascal(propertyName)}Item`) - : `${Case.pascal(propertyName)}Item`; - - const collectionIndent = INDENT.repeat(indentLevel); - const itemIndent = INDENT.repeat(indentLevel + 1); - - const items = values - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map((item: any) => { - const objectInit = generateCSharpObjectInitializer( - item, - itemsSchema as OpenAPIV3_1.SchemaObject, - typeName, - indentLevel + 1, - ); - - return `${itemIndent}${indentMultiline(objectInit, itemIndent)},`; - }) - .join("\n"); - - return `new[]\n${collectionIndent}{\n${items}\n${collectionIndent}}`; - } - - const primitiveValues = values.map((value) => formatPrimitive(value)); - return `new[] { ${primitiveValues.join(", ")} }`; -}; - -const generateCSharpObjectInitializer = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - example: any, - schema: OpenAPIV3_1.SchemaObject, - typeName: string, - indentLevel = 0, -): string => { - if (!example || typeof example !== "object") { - return ""; - } - - const indent = INDENT.repeat(indentLevel); - const fieldIndent = INDENT.repeat(indentLevel + 1); - const properties = schema.properties || {}; - - const fields = Object.entries(example) - .map(([key, value]) => { - const propertyName = Case.pascal(key); - const propSchema = properties[key]; - - if (!propSchema) { - return `${fieldIndent}${propertyName} = ${formatPrimitive(value as never)},`; - } - - const resolved = resolveSchema(propSchema); - - if ( - value !== null && - typeof value === "object" && - !Array.isArray(value) - ) { - const nestedTypeName = getSchemaTypeName(propSchema, propertyName); - const nestedInit = generateCSharpObjectInitializer( - value, - resolved, - nestedTypeName, - indentLevel + 1, - ); - - return `${fieldIndent}${propertyName} = ${nestedInit},`; - } - - if (Array.isArray(value)) { - const arrayInit = generateArrayInitializer( - value, - resolved, - indentLevel + 1, - propertyName, - ); - - return `${fieldIndent}${propertyName} = ${arrayInit},`; - } - - return `${fieldIndent}${propertyName} = ${formatPrimitive(value as never)},`; - }) - .join("\n"); - - return `new ${typeName}\n${indent}{\n${fields}\n${indent}}`; -}; - -const requestBody = (operation: OperationObject): string => { - const requestBodySchema = - operation.requestBody?.content?.["application/json"]?.schema; - if (!requestBodySchema) { - return ""; - } - - const schema = resolveSchema(requestBodySchema); - - if (!schema || !schema.properties) { - return ""; - } - - const example = getRequestBodyExample(operation); - - if (!example || typeof example !== "object") { - return ""; - } - - const typeName = getSchemaTypeName( - requestBodySchema, - `${Case.pascal(operation.operationId!)}Body`, - ); - - return generateCSharpObjectInitializer(example, schema, typeName); -}; - -const formatArgument = (argument: string): string => { - return `${INDENT}${indentMultiline(argument, INDENT)}`; -}; - -export const dotnet = (operation: OperationObject): string => { - const resource = Case.pascal(operation.tag); - const methodName = Case.pascal( - operation["x-codegen"]?.method_name || operation.operationId!, - ); - - const requiredParams = operation.parameters?.filter((p) => p.required) || []; - const paramArgs = requiredParams.map((param) => { - return formatPrimitive(getParameterExample(param) as never); - }); - - const bodyArg = requestBody(operation); - const args = [...paramArgs]; - - if (bodyArg) { - args.push(bodyArg); - } - - const formattedArgs = - args.length > 0 - ? `\n${args.map((arg) => formatArgument(arg)).join(",\n")}\n` - : ""; - - return `using SumUp; - -var client = new SumUpClient(); - -var result = await client.${resource}.${methodName}Async(${formattedArgs});`; -}; diff --git a/src/lib/codesamples/go.ts b/src/lib/codesamples/go.ts deleted file mode 100644 index 04e83896..00000000 --- a/src/lib/codesamples/go.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { resolveSchema } from "@lib/openapi"; -import { Case } from "change-case-all"; -import type { OpenAPIV3_1 } from "openapi-types"; -import type { OperationObject } from "src/types/openapi"; -import { getRequestBodyExample, getParameterExample } from "./util"; - -/** - * Generates Go struct initialization code from an example object. - */ -const generateGoStructInit = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - example: any, - schema: OpenAPIV3_1.SchemaObject, - indent = "", -): string => { - if (example === null || example === undefined) { - return ""; - } - - const properties = schema.properties || {}; - - const fields = Object.entries(example) - .map(([key, value]) => { - const fieldName = Case.pascal(key); - const propSchema = properties[key]; - - if (!propSchema) { - // Fallback for unknown properties - return `${indent} ${fieldName}: ${JSON.stringify(value)},`; - } - - const resolved = resolveSchema(propSchema); - - // Handle nested objects - if ( - typeof value === "object" && - value !== null && - !Array.isArray(value) - ) { - // Get the struct name from $ref or use schema title/key - let structName = ""; - if ("$ref" in propSchema) { - structName = Case.pascal(propSchema.$ref.split("/").pop()!); - } else if (resolved.title) { - structName = Case.pascal(resolved.title); - } else { - structName = Case.pascal(key); - } - - const nestedInit = generateGoStructInit(value, resolved, indent + " "); - return `${indent} ${fieldName}: sumup.${structName}${nestedInit},`; - } - - // Handle arrays - if (Array.isArray(value)) { - if (value.length === 0) { - return `${indent} ${fieldName}: []any{},`; - } - - const firstItem = value[0]; - if (typeof firstItem === "object" && firstItem !== null) { - // Array of objects - let itemStructName = ""; - if ( - "items" in resolved && - resolved.items && - "$ref" in resolved.items - ) { - itemStructName = Case.pascal(resolved.items.$ref.split("/").pop()!); - } else if ( - "items" in resolved && - resolved.items && - "title" in resolved.items && - resolved.items.title - ) { - itemStructName = Case.pascal(resolved.items.title); - } else { - itemStructName = Case.pascal(key); - } - - const itemsInit = value - .map((item) => { - const itemSchema = - "items" in resolved && resolved.items - ? resolveSchema(resolved.items) - : {}; - return `${indent} sumup.${itemStructName}${generateGoStructInit(item, itemSchema as OpenAPIV3_1.SchemaObject, indent + " ")}`; - }) - .join(",\n"); - - return `${indent} ${fieldName}: []sumup.${itemStructName}{\n${itemsInit},\n${indent} },`; - } - - // Array of primitives - return `${indent} ${fieldName}: ${JSON.stringify(value)},`; - } - - // Primitives - return `${indent} ${fieldName}: ${JSON.stringify(value)},`; - }) - .join("\n"); - - return `{\n${fields}\n${indent}}`; -}; - -const requestBody = ( - operation: OperationObject, - methodName: string, -): string => { - const example = getRequestBodyExample(operation); - - if (!example || typeof example !== "object") { - return ""; - } - - const requestBodySchema = - operation.requestBody?.content?.["application/json"]?.schema; - if (!requestBodySchema) { - return ""; - } - - const schema = resolveSchema(requestBodySchema); - if (!schema || !schema.properties) { - return ""; - } - - const bodyStructName = `${Case.pascal(operation.tag)}${Case.pascal(methodName)}Params`; - - return `sumup.${bodyStructName}${generateGoStructInit(example, schema)}`; -}; - -export const go = (operation: OperationObject): string => { - const resource = Case.pascal(operation.tag); - const methodName = - operation["x-codegen"]?.method_name || operation.operationId || ""; - const method = Case.pascal(methodName); - - const body = requestBody(operation, methodName); - - // Extract required parameters and use their examples from referenced schemas if available - const requiredParams = operation.parameters?.filter((p) => p.required) || []; - const paramsString = requiredParams - .map((param) => JSON.stringify(getParameterExample(param))) - .join(", "); - - const go = `client := sumup.NewClient() - -result, err := client.${resource}.${method}(context.Background()${paramsString ? `, ${paramsString}` : ""}${body ? `, ${body}` : ""})`; - - return go; -}; diff --git a/src/lib/codesamples/index.test.ts b/src/lib/codesamples/index.test.ts index ee099f01..44153088 100644 --- a/src/lib/codesamples/index.test.ts +++ b/src/lib/codesamples/index.test.ts @@ -1,15 +1,23 @@ import { describe, expect, it } from "vitest"; import type { OpenAPIV3_1 } from "openapi-types"; +import cliSamples from "../../codesamples/cli.json"; +import dotnetSamples from "../../codesamples/dotnet.json"; +import goSamples from "../../codesamples/go.json"; +import javaSamples from "../../codesamples/java.json"; +import pythonSamples from "../../codesamples/python.json"; +import rustSamples from "../../codesamples/rust.json"; +import typescriptSamples from "../../codesamples/typescript.json"; import type { OperationObject } from "../../types/openapi"; import { + cli, curl, dotnet, go, java, - node as nodeSample, php, python, rust, + typescript, } from "./index"; const checkoutBodySchema: OpenAPIV3_1.SchemaObject = { @@ -73,154 +81,33 @@ const checkoutOperation: OperationObject = { }, }; -const updateCustomerBodySchema: OpenAPIV3_1.SchemaObject = { - type: "object", - required: ["email"], - properties: { - email: { - type: "string", - example: "ada@example.com", - }, - }, -}; - -const updateCustomerOperation: OperationObject = { - operationId: "UpdateCustomer", - tag: "Customers", - slug: "customers-update", - tagSlug: "customers", - summary: "Update customer", - description: "Updates a customer profile.", - method: "PUT", - path: "/v0.1/customers/{customer_id}", - "x-codegen": { - method_name: "update", - }, - parameters: [ - { - name: "customer_id", - in: "path", - required: true, - schema: { - type: "string", - example: "cust_123", - }, - }, - { - name: "fields", - in: "query", - required: false, - schema: { - type: "string", - example: "personal_details", - }, - }, - ], - requestBody: { - content: { - "application/json": { - schema: updateCustomerBodySchema, - example: { - email: "ada@example.com", - }, - }, - }, - }, - responses: { - "200": { - description: "Updated", - }, - }, -}; - -describe("code sample generators", () => { - it("node sample matches the TypeScript SDK usage", () => { - expect(nodeSample(checkoutOperation)).toBe( - `import SumUp from '@sumup/sdk'; - -const client = new SumUp(); - -const result = await client.checkouts.create("MC123", { - amount: 1000, - currency: "EUR", - description: "Online order #42", -});`, +describe("code samples", () => { + it.each([ + ["Python", python, pythonSamples], + [".NET", dotnet, dotnetSamples], + ["Go", go, goSamples], + ["Java", java, javaSamples], + ["Rust", rust, rustSamples], + ["TypeScript", typescript, typescriptSamples], + ["CLI", cli, cliSamples], + ])("uses the vendored %s sample", (_, codeSample, vendoredSamples) => { + const expected = vendoredSamples.samples.find( + ({ operationId }) => operationId === checkoutOperation.operationId, ); - }); - - it("python sample uses the Sumup SDK client", () => { - expect(python(checkoutOperation)).toBe( - `from sumup import Sumup - -client = Sumup() -result = client.checkouts.create("MC123", CreateCheckoutBody( - amount=1000, - currency="EUR", - description="Online order #42", -))`, - ); + expect(codeSample(checkoutOperation)).toBe(expected?.sample); }); - it("dotnet sample uses the SumUp .NET client", () => { - expect(dotnet(checkoutOperation)).toBe( - `using SumUp; - -var client = new SumUpClient(); - -var result = await client.Checkouts.CreateAsync( - "MC123", - new CreateCheckoutBody - { - Amount = 1000, - Currency = "EUR", - Description = "Online order #42", - } -);`, - ); - }); - - it("go sample renders struct initialisers", () => { - expect(go(checkoutOperation)).toBe( - `client := sumup.NewClient() - -result, err := client.Checkouts.Create(context.Background(), "MC123", sumup.CheckoutsCreateParams{ - Amount: 1000, - Currency: "EUR", - Description: "Online order #42", -})`, - ); - }); - - it("java sample uses the SumUp client", () => { - expect(java(checkoutOperation)).toBe( - `import com.sumup.sdk.SumUpClient; - -SumUpClient client = SumUpClient.builder().build(); - -var result = client.checkouts().createCheckout( - "MC123", - CreateCheckoutBody.builder() - .amount(1000f) - .currency("EUR") - .description("Online order #42") - .build() -);`, - ); - }); - - it("rust sample includes params and body examples", () => { - expect(rust(updateCustomerOperation)).toBe( - `use sumup::Client; - -let client = Client::default(); + it("fails clearly when a vendored sample is missing", () => { + const operation = { + ...checkoutOperation, + operationId: "UnknownOperation", + }; -let result = client.customers().update("cust_123", sumup::UpdateCustomerParams{ - fields: Some("personal_details".to_string()), -}, sumup::UpdateCustomerBody{ - email: "ada@example.com".to_string(), -}).await;`, + expect(() => go(operation)).toThrow( + "Missing go code sample for UnknownOperation", ); + expect(cli(operation)).toBeUndefined(); }); it("php sample uses the SumUp PHP SDK", () => { diff --git a/src/lib/codesamples/index.ts b/src/lib/codesamples/index.ts index 3470ef17..37469d89 100644 --- a/src/lib/codesamples/index.ts +++ b/src/lib/codesamples/index.ts @@ -1,10 +1,68 @@ +import cliSamples from "../../codesamples/cli.json"; +import dotnetSamples from "../../codesamples/dotnet.json"; +import goSamples from "../../codesamples/go.json"; +import javaSamples from "../../codesamples/java.json"; +import pythonSamples from "../../codesamples/python.json"; +import rustSamples from "../../codesamples/rust.json"; +import typescriptSamples from "../../codesamples/typescript.json"; +import type { OperationObject } from "../../types/openapi"; import { curl } from "./curl"; -import { dotnet } from "./dotnet"; -import { go } from "./go"; -import { java } from "./java"; -import { node } from "./node"; import { php } from "./php"; -import { python } from "./python"; -import { rust } from "./rust"; -export { curl, dotnet, go, java, node, php, python, rust }; +type CodeSamples = { + language: string; + samples: { + operationId: string; + sample: string; + }[]; +}; + +const samplesByOperationId = (codeSamples: CodeSamples) => { + const samples = new Map(); + + // The API page shows one sample per language, so use the first example when + // an operation has multiple vendored variants. + for (const sample of codeSamples.samples) { + if (!samples.has(sample.operationId)) { + samples.set(sample.operationId, sample.sample); + } + } + + return samples; +}; + +const findVendoredSample = (codeSamples: CodeSamples) => { + const samples = samplesByOperationId(codeSamples); + + return (operation: OperationObject): string | undefined => { + return operation.operationId + ? samples.get(operation.operationId) + : undefined; + }; +}; + +const fromVendoredSamples = (codeSamples: CodeSamples) => { + const findSample = findVendoredSample(codeSamples); + + return (operation: OperationObject): string => { + const sample = findSample(operation); + + if (!sample) { + throw new Error( + `Missing ${codeSamples.language} code sample for ${operation.operationId ?? "operation without an ID"}`, + ); + } + + return sample; + }; +}; + +const cli = fromVendoredSamples(cliSamples); +const dotnet = fromVendoredSamples(dotnetSamples); +const go = fromVendoredSamples(goSamples); +const java = fromVendoredSamples(javaSamples); +const python = fromVendoredSamples(pythonSamples); +const rust = fromVendoredSamples(rustSamples); +const typescript = fromVendoredSamples(typescriptSamples); + +export { cli, curl, dotnet, go, java, php, python, rust, typescript }; diff --git a/src/lib/codesamples/java.ts b/src/lib/codesamples/java.ts deleted file mode 100644 index 437f8a7a..00000000 --- a/src/lib/codesamples/java.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { resolveSchema } from "@lib/openapi"; -import { Case } from "change-case-all"; -import type { OpenAPIV3_1 } from "openapi-types"; -import type { OperationObject } from "src/types/openapi"; -import { getParameterExample, getRequestBodyExample } from "./util"; - -const INDENT = " "; - -const indentMultiline = (text: string, indent: string): string => { - return text.replace(/\n/g, `\n${indent}`); -}; - -const getSchemaTypeName = ( - schema: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject, - fallback: string, -): string => { - if ("$ref" in schema) { - return Case.pascal(schema.$ref.split("/").pop()!); - } - - if ("title" in schema && schema.title) { - return Case.pascal(schema.title); - } - - return Case.pascal(fallback); -}; - -const formatPrimitive = ( - value: string | number | boolean | null, - schema?: OpenAPIV3_1.SchemaObject, -): string => { - if (value === null) { - return "null"; - } - - if (typeof value === "string") { - if (schema?.format === "date-time") { - return `java.time.OffsetDateTime.parse(${JSON.stringify(value)})`; - } - - if (schema?.format === "date") { - return `java.time.LocalDate.parse(${JSON.stringify(value)})`; - } - - return JSON.stringify(value); - } - - if (typeof value === "number") { - if (schema?.type === "integer") { - if (schema.format === "int64") { - return `${value}L`; - } - - return `${value}`; - } - - if (schema?.type === "number") { - return Number.isInteger(value) ? `${value}f` : `${value}f`; - } - - return `${value}`; - } - - if (typeof value === "boolean") { - return value ? "true" : "false"; - } - - return "null"; -}; - -const isEnumSchema = (schema: OpenAPIV3_1.SchemaObject): boolean => { - return Array.isArray(schema.enum) && schema.enum.length > 0; -}; - -const formatEnum = (value: string, typeName: string): string => { - return `${typeName}.fromValue(${JSON.stringify(value)})`; -}; - -const formatArray = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - values: any[], - schema: OpenAPIV3_1.SchemaObject, - indentLevel: number, - propertyName: string, -): string => { - if (values.length === 0) { - return "java.util.List.of()"; - } - - const itemSchema = - "items" in schema && schema.items ? resolveSchema(schema.items) : {}; - const itemTypeName = - "items" in schema && schema.items - ? getSchemaTypeName(schema.items, `${Case.pascal(propertyName)}Item`) - : `${Case.pascal(propertyName)}Item`; - - const arrayIndent = INDENT.repeat(indentLevel); - const itemIndent = INDENT.repeat(indentLevel + 1); - - const items = values - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map((item: any) => { - if (item !== null && typeof item === "object") { - const objectInit = generateJavaBuilder( - item, - itemSchema as OpenAPIV3_1.SchemaObject, - itemTypeName, - indentLevel + 1, - ); - - return `${itemIndent}${indentMultiline(objectInit, itemIndent)}`; - } - - return `${itemIndent}${formatPrimitive(item as never, itemSchema as OpenAPIV3_1.SchemaObject)}`; - }) - .join(",\n"); - - return `java.util.List.of(\n${items}\n${arrayIndent})`; -}; - -const generateJavaBuilder = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - example: any, - schema: OpenAPIV3_1.SchemaObject, - typeName: string, - indentLevel = 0, -): string => { - if (!example || typeof example !== "object") { - return ""; - } - - const fieldIndent = INDENT.repeat(indentLevel + 1); - const properties = schema.properties || {}; - - const fields = Object.entries(example) - .map(([key, value]) => { - const methodName = Case.camel(key); - const propSchema = properties[key]; - - if (!propSchema) { - return `${fieldIndent}.${methodName}(${formatPrimitive(value as never)})`; - } - - const resolved = resolveSchema(propSchema); - - if (resolved && isEnumSchema(resolved) && typeof value === "string") { - const enumTypeName = getSchemaTypeName(propSchema, key); - return `${fieldIndent}.${methodName}(${formatEnum(value, enumTypeName)})`; - } - - if ( - value !== null && - typeof value === "object" && - !Array.isArray(value) - ) { - const nestedTypeName = getSchemaTypeName(propSchema, key); - const nestedInit = generateJavaBuilder( - value, - resolved, - nestedTypeName, - indentLevel + 1, - ); - - return `${fieldIndent}.${methodName}(${indentMultiline(nestedInit, fieldIndent)})`; - } - - if (Array.isArray(value)) { - const arrayInit = formatArray(value, resolved, indentLevel + 1, key); - - return `${fieldIndent}.${methodName}(${indentMultiline(arrayInit, fieldIndent)})`; - } - - return `${fieldIndent}.${methodName}(${formatPrimitive(value as never, resolved)})`; - }) - .join("\n"); - - return `${typeName}.builder()\n${fields}\n${fieldIndent}.build()`; -}; - -const requestBody = (operation: OperationObject): string => { - const requestBodySchema = - operation.requestBody?.content?.["application/json"]?.schema; - if (!requestBodySchema) { - return ""; - } - - const schema = resolveSchema(requestBodySchema); - if (!schema || !schema.properties) { - return ""; - } - - const example = getRequestBodyExample(operation); - - if (!example || typeof example !== "object") { - return ""; - } - - const typeName = getSchemaTypeName( - requestBodySchema, - `${Case.pascal(operation.operationId!)}Body`, - ); - - return generateJavaBuilder(example, schema, typeName); -}; - -const formatArgument = (argument: string): string => { - return `${INDENT}${indentMultiline(argument, INDENT)}`; -}; - -export const java = (operation: OperationObject): string => { - const resource = Case.camel(operation.tag); - const methodName = Case.camel(operation.operationId!); - - const requiredParams = operation.parameters?.filter((p) => p.required) || []; - const paramArgs = requiredParams.map((param) => { - const schema = param.schema ? resolveSchema(param.schema) : undefined; - return formatPrimitive(getParameterExample(param), schema); - }); - - const bodyArg = requestBody(operation); - const args = [...paramArgs]; - - if (bodyArg) { - args.push(bodyArg); - } - - const formattedArgs = - args.length > 0 - ? `\n${args.map((arg) => formatArgument(arg)).join(",\n")}\n` - : ""; - - return `import com.sumup.sdk.SumUpClient; - -SumUpClient client = SumUpClient.builder().build(); - -var result = client.${resource}().${methodName}(${formattedArgs});`; -}; diff --git a/src/lib/codesamples/node.ts b/src/lib/codesamples/node.ts deleted file mode 100644 index 65bdbbea..00000000 --- a/src/lib/codesamples/node.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { resolveSchema } from "@lib/openapi"; -import { Case } from "change-case-all"; -import type { OpenAPIV3_1 } from "openapi-types"; -import type { OperationObject } from "src/types/openapi"; -import { getRequestBodyExample, getParameterExample } from "./util"; - -/** - * Generates TypeScript object literal code from an example object. - */ -const generateTypeScriptObjectLiteral = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - example: any, - schema: OpenAPIV3_1.SchemaObject, - indent = "", -): string => { - if (example === null || example === undefined) { - return ""; - } - - const properties = schema.properties || {}; - - const fields = Object.entries(example) - .map(([key, value]) => { - const propSchema = properties[key]; - - if (!propSchema) { - return `${indent} ${key}: ${JSON.stringify(value)},`; - } - - const resolved = resolveSchema(propSchema); - - // Handle nested objects - if ( - typeof value === "object" && - value !== null && - !Array.isArray(value) - ) { - const nestedLiteral = generateTypeScriptObjectLiteral( - value, - resolved, - indent + " ", - ); - return `${indent} ${key}: ${nestedLiteral},`; - } - - // Handle arrays - if (Array.isArray(value)) { - if (value.length === 0) { - return `${indent} ${key}: [],`; - } - - const firstItem = value[0]; - if (typeof firstItem === "object" && firstItem !== null) { - const itemSchema = - "items" in resolved && resolved.items - ? resolveSchema(resolved.items) - : {}; - const itemsLiteral = value - .map((item) => { - return `${indent} ${generateTypeScriptObjectLiteral(item, itemSchema as OpenAPIV3_1.SchemaObject, indent + " ")}`; - }) - .join(",\n"); - - return `${indent} ${key}: [\n${itemsLiteral},\n${indent} ],`; - } - - return `${indent} ${key}: ${JSON.stringify(value)},`; - } - - // Primitives - return `${indent} ${key}: ${JSON.stringify(value)},`; - }) - .join("\n"); - - return `{\n${fields}\n${indent}}`; -}; - -const requestBody = (operation: OperationObject): string => { - const requestBodySchema = - operation.requestBody?.content?.["application/json"]?.schema; - if (!requestBodySchema) { - return ""; - } - - const schema = resolveSchema(requestBodySchema); - if (!schema || !schema.properties) { - return ""; - } - - const example = getRequestBodyExample(operation); - - if (!example || typeof example !== "object") { - return ""; - } - - return generateTypeScriptObjectLiteral(example, schema); -}; - -export const node = (operation: OperationObject): string => { - const resource = Case.camel(operation.tag); - const method = Case.camel( - operation["x-codegen"]?.method_name || operation.operationId!, - ); - - const body = requestBody(operation); - - // Extract required parameters and use their examples from referenced schemas if available - const requiredParams = operation.parameters?.filter((p) => p.required) || []; - const paramsString = requiredParams - .map((param) => JSON.stringify(getParameterExample(param))) - .join(", "); - - const paramsSection = paramsString - ? `${paramsString}${body ? ", " : ""}` - : ""; - - const njs = `import SumUp from '@sumup/sdk'; - -const client = new SumUp(); - -const result = await client.${resource}.${method}(${paramsSection}${body});`; - - return njs; -}; diff --git a/src/lib/codesamples/python.ts b/src/lib/codesamples/python.ts deleted file mode 100644 index 684f556e..00000000 --- a/src/lib/codesamples/python.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { resolveSchema } from "@lib/openapi"; -import { Case } from "change-case-all"; -import type { OpenAPIV3_1 } from "openapi-types"; -import type { OperationObject } from "src/types/openapi"; -import { getRequestBodyExample, getParameterExample } from "./util"; - -/** - * Generates Python class instantiation code from an example object. - * Similar to Go struct initialization, uses Pydantic classes with keyword arguments. - */ -const generatePythonClassInit = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - example: any, - schema: OpenAPIV3_1.SchemaObject, - indent = "", -): string => { - if (example === null || example === undefined) { - return ""; - } - - const properties = schema.properties || {}; - - const fields = Object.entries(example) - .map(([key, value]) => { - const propSchema = properties[key]; - - if (!propSchema) { - return `${indent} ${key}=${JSON.stringify(value)},`; - } - - const resolved = resolveSchema(propSchema); - - // Handle nested objects - if ( - typeof value === "object" && - value !== null && - !Array.isArray(value) - ) { - // Get the class name from $ref or use schema title/key - let className = ""; - if ("$ref" in propSchema) { - className = Case.pascal(propSchema.$ref.split("/").pop()!); - } else if (resolved.title) { - className = Case.pascal(resolved.title); - } else { - className = Case.pascal(key); - } - - const nestedInit = generatePythonClassInit( - value, - resolved, - indent + " ", - ); - return `${indent} ${key}=${className}${nestedInit},`; - } - - // Handle arrays - if (Array.isArray(value)) { - if (value.length === 0) { - return `${indent} ${key}=[],`; - } - - const firstItem = value[0]; - if (typeof firstItem === "object" && firstItem !== null) { - // Array of objects - let itemClassName = ""; - if ( - "items" in resolved && - resolved.items && - "$ref" in resolved.items - ) { - itemClassName = Case.pascal(resolved.items.$ref.split("/").pop()!); - } else if ( - "items" in resolved && - resolved.items && - "title" in resolved.items && - resolved.items.title - ) { - itemClassName = Case.pascal(resolved.items.title); - } else { - itemClassName = Case.pascal(key); - } - - const itemSchema = - "items" in resolved && resolved.items - ? resolveSchema(resolved.items) - : {}; - const itemsInit = value - .map((item) => { - return `${indent} ${itemClassName}${generatePythonClassInit(item, itemSchema as OpenAPIV3_1.SchemaObject, indent + " ")}`; - }) - .join(",\n"); - - return `${indent} ${key}=[\n${itemsInit},\n${indent} ],`; - } - - return `${indent} ${key}=${JSON.stringify(value)},`; - } - - // Primitives - return `${indent} ${key}=${JSON.stringify(value)},`; - }) - .join("\n"); - - return `(\n${fields}\n${indent})`; -}; - -const requestBody = (operation: OperationObject): string => { - const requestBodySchema = - operation.requestBody?.content?.["application/json"]?.schema; - if (!requestBodySchema) { - return ""; - } - - const schema = resolveSchema(requestBodySchema); - if (!schema || !schema.properties) { - return ""; - } - - const bodyClassName = `${Case.pascal(operation.operationId!)}Body`; - - const example = getRequestBodyExample(operation); - - if (!example || typeof example !== "object") { - return ""; - } - - return `${bodyClassName}${generatePythonClassInit(example, schema)}`; -}; - -export const python = (operation: OperationObject): string => { - const resource = Case.snake(operation.tag); - const method = Case.snake( - operation["x-codegen"]?.method_name || operation.operationId!, - ); - - const body = requestBody(operation); - - // Extract required parameters and use their examples from referenced schemas if available - const requiredParams = operation.parameters?.filter((p) => p.required) || []; - const paramsString = requiredParams - .map((param) => JSON.stringify(getParameterExample(param))) - .join(", "); - - const paramsSection = paramsString - ? `${paramsString}${body ? ", " : ""}` - : ""; - - const njs = `from sumup import Sumup - -client = Sumup() - -result = client.${resource}.${method}(${paramsSection}${body})`; - - return njs; -}; diff --git a/src/lib/codesamples/rust.ts b/src/lib/codesamples/rust.ts deleted file mode 100644 index e7f13e65..00000000 --- a/src/lib/codesamples/rust.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { resolveSchema } from "@lib/openapi"; -import { Case } from "change-case-all"; -import type { OpenAPIV3_1 } from "openapi-types"; -import type { OperationObject } from "src/types/openapi"; -import { getRequestBodyExample, getParameterExample } from "./util"; - -/** - * Generates Rust struct initialization code from an example object. - * Uses Rust struct literal syntax with named fields. - */ -const generateRustStructInit = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - example: any, - schema: OpenAPIV3_1.SchemaObject, - indent = "", -): string => { - if (example === null || example === undefined) { - return ""; - } - - const properties = schema.properties || {}; - - const fields = Object.entries(example) - .map(([key, value]) => { - const fieldName = Case.snake(key); - const propSchema = properties[key]; - - if (!propSchema) { - return `${indent} ${fieldName}: ${JSON.stringify(value)},`; - } - - const resolved = resolveSchema(propSchema); - - // Handle nested objects - if ( - typeof value === "object" && - value !== null && - !Array.isArray(value) - ) { - // Get the struct name from $ref or use schema title/key - let structName = ""; - if ("$ref" in propSchema) { - structName = Case.pascal(propSchema.$ref.split("/").pop()!); - } else if (resolved.title) { - structName = Case.pascal(resolved.title); - } else { - structName = Case.pascal(key); - } - - const nestedInit = generateRustStructInit( - value, - resolved, - indent + " ", - ); - return `${indent} ${fieldName}: sumup::${structName} ${nestedInit},`; - } - - // Handle arrays - if (Array.isArray(value)) { - if (value.length === 0) { - return `${indent} ${fieldName}: vec![],`; - } - - const firstItem = value[0]; - if (typeof firstItem === "object" && firstItem !== null) { - // Array of objects - let itemStructName = ""; - if ( - "items" in resolved && - resolved.items && - "$ref" in resolved.items - ) { - itemStructName = Case.pascal(resolved.items.$ref.split("/").pop()!); - } else if ( - "items" in resolved && - resolved.items && - "title" in resolved.items && - resolved.items.title - ) { - itemStructName = Case.pascal(resolved.items.title); - } else { - itemStructName = Case.pascal(key); - } - - const itemSchema = - "items" in resolved && resolved.items - ? resolveSchema(resolved.items) - : {}; - const itemsInit = value - .map((item) => { - return `${indent} ${itemStructName} ${generateRustStructInit(item, itemSchema as OpenAPIV3_1.SchemaObject, indent + " ")}`; - }) - .join(",\n"); - - return `${indent} ${fieldName}: vec![\n${itemsInit},\n${indent} ],`; - } - - // Array of primitives - const primitiveValues = value.map((v) => JSON.stringify(v)).join(", "); - return `${indent} ${fieldName}: vec![${primitiveValues}],`; - } - - // Primitives - handle strings with proper Rust syntax - if (typeof value === "string") { - if (resolved.format === "password") { - return `${indent} ${fieldName}: crate::secret::Secret::from("${value}"),`; - } - return `${indent} ${fieldName}: "${value}".to_string(),`; - } - - return `${indent} ${fieldName}: ${JSON.stringify(value)},`; - }) - .join("\n"); - - if (!fields) return "{}"; - - return `{\n${fields}\n${indent}}`; -}; - -const requestBody = (operation: OperationObject): string => { - const requestBodySchema = - operation.requestBody?.content?.["application/json"]?.schema; - if (!requestBodySchema) { - return ""; - } - - const schema = resolveSchema(requestBodySchema); - if (!schema || !schema.properties) { - return ""; - } - - const bodyStructName = `${Case.pascal(operation.operationId!)}Body`; - - const example = getRequestBodyExample(operation); - - if (!example || typeof example !== "object") { - return ""; - } - - return `sumup::${bodyStructName}${generateRustStructInit(example, schema)}`; -}; - -export const rust = (operation: OperationObject): string => { - const resource = Case.snake(operation.tag); - const method = Case.snake( - operation["x-codegen"]?.method_name || operation.operationId!, - ); - - const body = requestBody(operation); - - // Separate path and query parameters - const pathParams = - operation.parameters?.filter((p) => p.in === "path" && p.required) || []; - const queryParams = - operation.parameters?.filter((p) => p.in === "query") || []; - - // Build the parameter list - const paramsList: string[] = []; - - // Add path parameters first - for (const param of pathParams) { - const example = getParameterExample(param); - const paramSchema = param.schema ? resolveSchema(param.schema) : {}; - if (typeof example === "string") { - if (paramSchema.format === "password") { - paramsList.push(`crate::secret::Secret::from("${example}")`); - } else { - paramsList.push(`"${example}"`); - } - } else { - paramsList.push(`&${JSON.stringify(example)}`); - } - } - - // Add query parameters as a struct if any exist - if (queryParams.length > 0) { - const paramsStructName = `${Case.pascal(operation.operationId!)}Params`; - const queryFields = queryParams - .map((param) => { - const fieldName = Case.snake(param.name); - const example = getParameterExample(param); - if (typeof example === "string") { - return ` ${fieldName}: Some("${example}".to_string()),`; - } else if (typeof example === "number") { - return ` ${fieldName}: Some(${example}),`; - } else if (typeof example === "boolean") { - return ` ${fieldName}: Some(${example}),`; - } - return ` ${fieldName}: Some(${JSON.stringify(example)}),`; - }) - .join("\n"); - - paramsList.push(`sumup::${paramsStructName}{\n${queryFields}\n}`); - } - - // Add body last - if (body) { - paramsList.push(`${body}`); - } - - const paramsSection = paramsList.length > 0 ? paramsList.join(", ") : ""; - - const rs = `use sumup::Client; - -let client = Client::default(); - -let result = client.${resource}().${method}(${paramsSection}).await;`; - - return rs; -}; diff --git a/src/lib/codesamples/util.ts b/src/lib/codesamples/util.ts index 03041c60..3b1acd0e 100644 --- a/src/lib/codesamples/util.ts +++ b/src/lib/codesamples/util.ts @@ -1,4 +1,4 @@ -import { isRequestBody, resolveSchema, schemaToExample } from "@lib/openapi"; +import { resolveSchema, schemaToExample } from "@lib/openapi"; import type { OpenAPIV3_1 } from "openapi-types"; import type { OperationObject } from "src/types/openapi"; @@ -125,29 +125,6 @@ export const getRequestBodyExample = (operation: OperationObject): any => { return filterRequiredFields(example, schema); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const bodyExample = (operation: OperationObject): any => { - if (!isRequestBody(operation.requestBody)) { - return undefined; - } - - if (!("application/json" in operation.requestBody.content)) { - return undefined; - } - - if (operation.requestBody.content["application/json"].example) { - return operation.requestBody.content["application/json"].example; - } - - if (!operation.requestBody.content["application/json"].schema) { - return undefined; - } - - return schemaToExample( - operation.requestBody.content["application/json"].schema!, - ); -}; - /** * Gets the example value for a parameter. * If the parameter schema is a reference, resolves it and uses its example. From abc0da6dfbc81e481488150cfd580039e537231e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Fri, 21 Aug 2026 00:24:55 +0200 Subject: [PATCH 2/3] feat: CLI docs --- src/components/ApiDocs/SDKList.tsx | 12 +- src/components/ApiDocs/TopSections.astro | 20 ++- .../docs/online-payments/checkouts/index.mdx | 1 + .../docs/online-payments/guides/index.mdx | 2 +- src/content/docs/tools/cli.mdx | 118 ++++++++++++++++++ src/content/docs/tools/sdks/index.mdx | 4 +- src/content/help/support-libraries.mdx | 5 + src/lib/codesamples/index.test.ts | 4 +- src/pages/api/[...path].astro | 2 +- src/pages/index.astro | 4 +- 10 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 src/content/docs/tools/cli.mdx diff --git a/src/components/ApiDocs/SDKList.tsx b/src/components/ApiDocs/SDKList.tsx index 9803c921..85219dbb 100644 --- a/src/components/ApiDocs/SDKList.tsx +++ b/src/components/ApiDocs/SDKList.tsx @@ -1,4 +1,5 @@ import { ListItemGroup } from "@sumup-oss/circuit-ui"; +import bashIcon from "@assets/languages/bash.svg"; import dotnetIcon from "@assets/languages/dotnet.svg"; import goIcon from "@assets/languages/go.svg"; import javaIcon from "@assets/languages/java.svg"; @@ -20,8 +21,17 @@ export default () => { return ( ( + + ), + label: "CLI", + href: "/tools/cli/", + variant: "navigation", + }, { key: "javascript", leadingComponent: () => ( diff --git a/src/components/ApiDocs/TopSections.astro b/src/components/ApiDocs/TopSections.astro index dabe474d..a102d172 100644 --- a/src/components/ApiDocs/TopSections.astro +++ b/src/components/ApiDocs/TopSections.astro @@ -74,11 +74,14 @@ const sectionAttrs = (id: ApiTopSection) =>
- SDKs + + SDKs and CLI +

- The SumUp SDKs reduce the amount of work required to use our REST - APIs. SumUp maintains SDKs for PHP, JavaScript, Python, Java, Go, - Rust, and .NET. + The SumUp SDKs and command-line interface reduce the amount of work + required to use our REST APIs. Use the CLI directly from your + terminal, or choose an SDK for PHP, JavaScript, Python, Java, Go, + Rust, or .NET.

@@ -89,7 +92,12 @@ const sectionAttrs = (id: ApiTopSection) => options={[ { label: "cURL", - code: "# Select a client library to see installation instructions.", + code: "# Select the CLI or an SDK to see installation instructions.", + lang: "bash", + }, + { + label: "CLI", + code: "brew install sumup/cli/sumup", lang: "bash", }, { @@ -146,7 +154,7 @@ uv add sumup`, ]} isLanguageSelect > -
Install SDK
+
Install SDK or CLI
diff --git a/src/content/docs/online-payments/checkouts/index.mdx b/src/content/docs/online-payments/checkouts/index.mdx index 43fd684b..65d8580e 100644 --- a/src/content/docs/online-payments/checkouts/index.mdx +++ b/src/content/docs/online-payments/checkouts/index.mdx @@ -13,4 +13,5 @@ SumUp provides the following checkout integrations for online payments: - [Hosted Checkout](/online-payments/checkouts/hosted-checkout/) - SumUp-hosted payment page with minimal integration effort - [Swift Checkout SDK](/online-payments/checkouts/swift-checkout/) - accelerated wallet checkout for Apple Pay and Google Pay - [Server-side SDKs](/tools/sdks/) - JavaScript, Go, Python, Java, PHP, .NET, and Rust clients for the SumUp API +- [SumUp CLI](/tools/cli/) - command-line access to the SumUp API for development and automation - [React Native SDK](/online-payments/sdks/react-native/) - payment sheet for mobile apps diff --git a/src/content/docs/online-payments/guides/index.mdx b/src/content/docs/online-payments/guides/index.mdx index 2337a01a..368eaa9f 100644 --- a/src/content/docs/online-payments/guides/index.mdx +++ b/src/content/docs/online-payments/guides/index.mdx @@ -36,4 +36,4 @@ With your sandbox merchant account, begin making API calls with real data. Sandb When finished experimenting with the sandbox merchant account, switch back to a regular account for business purposes. -SumUp provides official SDKs for JavaScript, Go, Python, Java, PHP, .NET, and Rust — visit the [SDKs overview page](/tools/sdks/) to choose the client that fits your stack. +SumUp provides official SDKs for JavaScript, Go, Python, Java, PHP, .NET, and Rust, as well as the [SumUp CLI](/tools/cli/) for working from a terminal. Visit the [SDKs overview page](/tools/sdks/) to choose the client that fits your stack. diff --git a/src/content/docs/tools/cli.mdx b/src/content/docs/tools/cli.mdx new file mode 100644 index 00000000..f2810ed2 --- /dev/null +++ b/src/content/docs/tools/cli.mdx @@ -0,0 +1,118 @@ +--- +title: CLI +description: Install and use the SumUp command-line interface to interact with SumUp APIs from your terminal. +sidebar: + order: 101 +links: + - title: Source code + href: https://github.com/sumup/sumup-cli + - title: Releases + href: https://github.com/sumup/sumup-cli/releases +--- + +The SumUp CLI, `sumup`, lets you manage your SumUp account and call SumUp APIs from a terminal. Use it to explore API operations, run development workflows, or write scripts without setting up an SDK project. + +The [API reference](/api/) includes a **CLI** sample for every operation supported by the tool. + +## Install the CLI + +Install the CLI with [Homebrew](https://brew.sh/): + +```bash +brew install sumup/cli/sumup +``` + +Alternatively, install it with Go: + +```bash +go install github.com/sumup/sumup-cli/cmd/sumup +``` + +Confirm that the installation succeeded: + +```bash +sumup version +``` + +## Configure Authentication + +Create a secret API key in the [developer dashboard](https://me.sumup.com/developers), then expose it to the CLI through the `SUMUP_API_KEY` environment variable: + +```bash +export SUMUP_API_KEY=sup_sk_your_api_key +``` + +You can also pass an API key with the global `--api-key` option. Avoid saving secret API keys in shell history or source control. + +## Set Your Merchant Context + +Commands that operate on a merchant accept the `--merchant-code` option. To avoid repeating it, select a merchant interactively and save it as your current context: + +```bash +sumup context set +``` + +View or clear the saved context with: + +```bash +sumup context get +sumup context unset +``` + +An explicit `--merchant-code` option overrides the saved context. + +## Run Commands + +Create an online checkout: + +```bash +sumup checkouts create \ + --reference order-123 \ + --amount 19.99 \ + --currency EUR \ + --merchant-code M123 \ + --description "Ticket purchase" +``` + +List the readers paired with a merchant: + +```bash +sumup readers list --merchant-code M123 +``` + +Add the global `--json` option when a script needs machine-readable output: + +```bash +sumup --json readers list --merchant-code M123 +``` + +Use the built-in help to discover available resources, operations, and options: + +```bash +sumup --help +sumup checkouts --help +sumup checkouts create --help +``` + +## Enable Shell Completion + +Generate and load a completion script for your shell: + +```bash +# bash +source <(sumup completion bash) + +# zsh +source <(sumup completion zsh) + +# fish +sumup completion fish > ~/.config/fish/completions/sumup.fish +``` + +Release archives also include pre-generated completion scripts and a man page. + +## Next Steps + +- Browse the [API reference](/api/) and select the **CLI** tab for operation-specific commands. +- Review [authorization options](/tools/authorization/) before using the CLI with production accounts. +- Use an [official server SDK](/tools/sdks/) when you need typed clients and application-level integration. diff --git a/src/content/docs/tools/sdks/index.mdx b/src/content/docs/tools/sdks/index.mdx index e520ae3b..59617b86 100644 --- a/src/content/docs/tools/sdks/index.mdx +++ b/src/content/docs/tools/sdks/index.mdx @@ -1,6 +1,6 @@ --- title: SDKs -description: Build on SumUp's APIs with officially supported server-side SDKs. +description: Build on SumUp's APIs with the SumUp CLI or an officially supported server-side SDK. sidebar: label: Overview order: 100 @@ -8,6 +8,8 @@ sidebar: SumUp maintains open-source server SDKs that wrap the public API and handle authentication, pagination, and resource helpers. You can use the same SDK for online payments and for card-present payments through the Cloud API. Each guide includes examples for creating an online checkout and starting a checkout on a paired Solo reader. +Prefer working from a terminal? The [SumUp CLI](/tools/cli/) provides commands for SumUp API operations without requiring you to write an application. + ## Choose Your SDK - **[JavaScript](/tools/sdks/javascript/)** – published on npm as `@sumup/sdk` with first-class TypeScript typings and ESM support across JavaScript runtimes. diff --git a/src/content/help/support-libraries.mdx b/src/content/help/support-libraries.mdx index 33075c8f..b0e91b1e 100644 --- a/src/content/help/support-libraries.mdx +++ b/src/content/help/support-libraries.mdx @@ -9,6 +9,11 @@ SumUp maintains multiple SDKs and APIs to simplify your integration work across - [JavaScript / TypeScript, Go, Python, Java, PHP, .NET, and Rust SDK guides](/tools/sdks/) cover server-to-SumUp API calls for online payments and remote Solo reader management through the Cloud API. - Use these when you’re building checkout pages, recurring billing, or card-present integrations from a backend or platform that can make API requests. +## Command Line Interface + +- The [SumUp CLI](/tools/cli/) lets you call SumUp APIs from a terminal without writing an application. +- Use it for exploring operations, development workflows, and scripts that consume JSON output. + ## Card-Present Integrations - [Android SDK](https://github.com/sumup/sumup-android-sdk) and [iOS SDK](https://github.com/sumup/sumup-ios-sdk) let you embed our reader experience inside native apps. diff --git a/src/lib/codesamples/index.test.ts b/src/lib/codesamples/index.test.ts index 44153088..be55cd55 100644 --- a/src/lib/codesamples/index.test.ts +++ b/src/lib/codesamples/index.test.ts @@ -107,7 +107,9 @@ describe("code samples", () => { expect(() => go(operation)).toThrow( "Missing go code sample for UnknownOperation", ); - expect(cli(operation)).toBeUndefined(); + expect(() => cli(operation)).toThrow( + "Missing bash code sample for UnknownOperation", + ); }); it("php sample uses the SumUp PHP SDK", () => { diff --git a/src/pages/api/[...path].astro b/src/pages/api/[...path].astro index 9ac423d5..a4da0988 100644 --- a/src/pages/api/[...path].astro +++ b/src/pages/api/[...path].astro @@ -45,7 +45,7 @@ const sidebar: StarlightUserConfig["sidebar"] = [ link: "/api", }, { - label: "SDKs", + label: "SDKs and CLI", link: "/api/sdks", attrs: { "data-scroll-to": "sdks", diff --git a/src/pages/index.astro b/src/pages/index.astro index b774a17e..29c7ae3a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -117,8 +117,8 @@ const props = { variant="navigation" leadingComponent={Apps} href="/tools/sdks/" - label="Call APIs with a Server SDK" - details="Use an official client across online and in-person integrations." + label="Call APIs with an SDK or CLI" + details="Use an official client library or work directly from your terminal." /> Date: Fri, 21 Aug 2026 00:29:58 +0200 Subject: [PATCH 3/3] feat: add changelog entry --- src/components/ApiDocs/SDKList.tsx | 2 +- src/components/ApiDocs/TopSections.astro | 9 +++++++-- src/pages/api/[...path].astro | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/ApiDocs/SDKList.tsx b/src/components/ApiDocs/SDKList.tsx index 85219dbb..bcdfdf42 100644 --- a/src/components/ApiDocs/SDKList.tsx +++ b/src/components/ApiDocs/SDKList.tsx @@ -21,7 +21,7 @@ export default () => { return (
- SDKs and CLI + SDKs

The SumUp SDKs and command-line interface reduce the amount of work @@ -154,7 +154,7 @@ uv add sumup`, ]} isLanguageSelect > -

Install SDK or CLI
+
Install
@@ -205,6 +205,11 @@ uv add sumup`, -H "Authorization: Bearer sup_sk_MvxmLOl0..."`, lang: "bash", }, + { + label: "CLI", + code: "export SUMUP_API_KEY='sup_sk_MvxmLOl0...'", + lang: "bash", + }, { label: "JavaScript", code: `import { SumUp } from '@sumup/sdk'; diff --git a/src/pages/api/[...path].astro b/src/pages/api/[...path].astro index a4da0988..9ac423d5 100644 --- a/src/pages/api/[...path].astro +++ b/src/pages/api/[...path].astro @@ -45,7 +45,7 @@ const sidebar: StarlightUserConfig["sidebar"] = [ link: "/api", }, { - label: "SDKs and CLI", + label: "SDKs", link: "/api/sdks", attrs: { "data-scroll-to": "sdks",