Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions packages/sdk-generator/__tests__/backends/elixir.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -998,3 +998,92 @@ describe("elixir nullable composite descriptors", () => {
);
});
});

describe("elixir emission of binary-format application/json responses", () => {
// Mirrors the two production ops whose 200 response is a raw blob served
// with a JSON content type: trajectories contents and private service
// definitions. Both must come out as raw ops, never decode: :string.
const binaryRawFixture = {
openapi: "3.0.0",
info: { title: "Binary Raw API", version: "1.0.0" },
paths: {
"/api/v1/trajectories/{trajectory}/contents": {
get: {
operationId: "get_api_v1_trajectories_contents",
parameters: [
{ name: "trajectory", in: "path", required: true, schema: { type: "string" } },
],
responses: {
"200": {
description: "Raw trajectory JSON blob",
content: {
"application/json": { schema: { type: "string", format: "binary" } },
},
},
},
},
},
"/api/v1/private_service_definitions/{app_id}/{private_service_id}": {
get: {
operationId: "get_api_v1_private_service_definitions",
parameters: [
{ name: "app_id", in: "path", required: true, schema: { type: "string" } },
{
name: "private_service_id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
"200": {
description: "Raw service definition",
content: {
"application/json": { schema: { type: "string", format: "binary" } },
},
},
},
},
},
"/api/v1/configs/{config}/summary": {
get: {
operationId: "get_api_v1_configs_summary",
parameters: [
{ name: "config", in: "path", required: true, schema: { type: "string" } },
],
responses: {
"200": {
description: "Plain JSON string",
content: { "application/json": { schema: { type: "string" } } },
},
},
},
},
},
};

it("emits raw ops for binary-format json responses and typed ops for plain strings", () => {
const spec = parseOpenApiSpec(binaryRawFixture, {
apiBase: "/api",
defaultVersion: "v1",
});
const files = generateElixir(spec, { outDir: "sdk" });

const trajectories = Object.entries(files).find(([path]) =>
path.includes("trajectories")
)![1];
expect(trajectories).toContain("raw: true");
expect(trajectories).toContain("{:ok, Req.Response.t()}");
expect(trajectories).not.toContain("decode: :string");

const definitions = Object.entries(files).find(([path]) =>
path.includes("private_service_definitions")
)![1];
expect(definitions).toContain("raw: true");
expect(definitions).not.toContain("decode: :string");

const configs = Object.entries(files).find(([path]) => path.includes("configs"))![1];
expect(configs).toContain("decode: :string");
expect(configs).not.toContain("raw: true");
});
});
59 changes: 59 additions & 0 deletions packages/sdk-generator/__tests__/frontend/parse-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,52 @@ const rawFixture = {
},
},
},
"/api/v1/trajectories/{trajectory}/contents": {
get: {
operationId: "get_api_v1_trajectories_contents",
parameters: [
{
name: "trajectory",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
"200": {
description: "Raw trajectory JSON blob served as application/json",
content: {
"application/json": {
schema: { type: "string", format: "binary" },
},
},
},
},
},
},
"/api/v1/configs/{config}/summary": {
get: {
operationId: "get_api_v1_configs_summary",
parameters: [
{
name: "config",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
"200": {
description: "Plain JSON string",
content: {
"application/json": {
schema: { type: "string" },
},
},
},
},
},
},
},
};

Expand Down Expand Up @@ -320,6 +366,19 @@ describe("parseOpenApiSpec detects raw responses", () => {
const content = configs.operations.find((o) => o.name === "content")!;
expect(content.rawResponse).toBe(true);
});

it("marks binary-format application/json responses as raw", () => {
const trajectories = ast.resources.find((r) => r.name === "trajectories")!;
const contents = trajectories.operations.find((o) => o.name === "contents")!;
expect(contents.rawResponse).toBe(true);
});

it("keeps plain application/json string responses typed", () => {
const configs = ast.resources.find((r) => r.name === "configs")!;
const summary = configs.operations.find((o) => o.name === "summary")!;
expect(summary.rawResponse).toBeFalsy();
expect(summary.returnType).toEqual({ kind: "primitive", type: "string" });
});
});

describe("parseOpenApiSpec preserves no-content response descriptions", () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/sdk-generator/src/frontend/operation-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,12 @@ function extractSuccessResponse(
return { returnType: { kind: "void" }, returnDescription };
}

// A binary-format schema is a raw blob even when served with a JSON content
// type, and must not be typed: HTTP clients decode the JSON body before the
// SDK's own decode runs. Only a top-level format is inspected; no spec shape
// wraps binary in a $ref or allOf today.
const jsonContent = successResponse.content["application/json"];
if (jsonContent?.schema) {
if (jsonContent?.schema && jsonContent.schema.format !== "binary") {
return {
returnType: jsonSchemaToTypeRef(jsonContent.schema),
returnDescription,
Expand Down
Loading