Skip to content

Commit fc12d09

Browse files
committed
feat: generate TWILIO_MESSAGE from Twilio OpenAPI spec
Replace the hand-written TWILIO_MESSAGE zod schema with one generated from Twilio's official OpenAPI specification (twilio/twilio-oai, spec/json/twilio_api_v2010.json), mirroring the woocommerce-action and shopware-action generation pipeline. - scripts/filterTwilioSpec.mjs reduces the large v2010 spec to the api.v2010.account.message component, re-exposed as `TwilioMessage` - scripts/patchGeneratedSchemas.mjs applies the zod v3 -> v4 z.record fix - package.json: add openapi-zod-client and a generate:twilio-schemas script - twilioMessage.ts now uses schemas.TwilioMessage from the generated file NOTE: run `npm install && npm run generate:twilio-schemas` to produce and commit src/generated/twilio-schemas.ts; the action does not build until that generated file exists. Refs #33 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hrvb3LRhmxcrASCuSXc7Jn
1 parent 6e51775 commit fc12d09

4 files changed

Lines changed: 80 additions & 35 deletions

File tree

actions/twilio-action/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"scripts": {
88
"dev": "tsx watch src/index.ts",
99
"typecheck": "tsc --noEmit",
10+
"generate:twilio-schemas": "curl -fsSL --create-dirs -o schemas/twilio-api-v2010-openapi.json https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json && node scripts/filterTwilioSpec.mjs && mkdir -p src/generated && openapi-zod-client schemas/twilio-api-v2010-openapi.filtered.json -o src/generated/twilio-schemas.ts -t node_modules/openapi-zod-client/src/templates/schemas-only.hbs --export-schemas && node scripts/patchGeneratedSchemas.mjs",
1011
"build": "vite build",
1112
"test": "vitest run",
1213
"start": "node dist/index.js"
@@ -17,6 +18,7 @@
1718
},
1819
"devDependencies": {
1920
"@types/node": "^22.0.0",
21+
"openapi-zod-client": "^1.18.3",
2022
"tsx": "^4.0.0",
2123
"typescript": "^5.9.3",
2224
"vite": "^8.0.3",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Twilio's api v2010 spec (twilio_api_v2010.json) defines the entire 2010 API
2+
// and is large and heavily cross-referenced. Only the Message resource schema
3+
// is needed for the TWILIO_MESSAGE data type, so this script reduces the
4+
// downloaded spec before code generation, mirroring filterShopwareSpec.mjs in
5+
// shopware-action:
6+
// - keeps `api.v2010.account.message`, re-exposed under the stable name
7+
// `TwilioMessage` so the generated schema has a predictable identifier
8+
// regardless of how openapi-zod-client would sanitize the dotted name
9+
// - replaces $refs to any other component with a permissive untyped object,
10+
// cutting the rest of the graph
11+
// Runs as part of "generate:twilio-schemas" right after the download.
12+
import {readFileSync, writeFileSync} from "node:fs";
13+
14+
const SOURCE_NAME = "api.v2010.account.message";
15+
const TARGET_NAME = "TwilioMessage";
16+
17+
const input = new URL("../schemas/twilio-api-v2010-openapi.json", import.meta.url);
18+
const output = new URL("../schemas/twilio-api-v2010-openapi.filtered.json", import.meta.url);
19+
20+
const spec = JSON.parse(readFileSync(input, "utf8"));
21+
const allSchemas = spec.components?.schemas ?? {};
22+
23+
if (!(SOURCE_NAME in allSchemas)) {
24+
throw new Error(`Schema "${SOURCE_NAME}" not found in ${input}`);
25+
}
26+
27+
// Rewrite $refs: self-references point at the renamed schema, every other
28+
// component reference is stubbed out as an untyped object.
29+
const rewriteRefs = (node) => {
30+
if (Array.isArray(node)) {
31+
return node.map(rewriteRefs);
32+
}
33+
if (node !== null && typeof node === "object") {
34+
const ref = node.$ref;
35+
if (typeof ref === "string") {
36+
const match = ref.match(/^#\/components\/schemas\/(.+)$/);
37+
if (match) {
38+
if (match[1] === SOURCE_NAME) {
39+
return {$ref: `#/components/schemas/${TARGET_NAME}`};
40+
}
41+
return {type: "object", additionalProperties: true};
42+
}
43+
}
44+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, rewriteRefs(value)]));
45+
}
46+
return node;
47+
};
48+
49+
const filtered = {
50+
openapi: spec.openapi,
51+
info: spec.info,
52+
paths: {},
53+
components: {
54+
schemas: {
55+
[TARGET_NAME]: rewriteRefs(allSchemas[SOURCE_NAME]),
56+
},
57+
},
58+
};
59+
60+
writeFileSync(output, JSON.stringify(filtered, null, 2));
61+
console.log(`Kept "${SOURCE_NAME}" as "${TARGET_NAME}" (of ${Object.keys(allSchemas).length} schemas)`);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// openapi-zod-client emits zod v3 code, but this project uses zod v4,
2+
// where z.record() requires an explicit key schema. Rewrites the
3+
// generated single-argument form z.record(Value) to
4+
// z.record(z.string(), Value). Runs as part of
5+
// "generate:twilio-schemas" right after code generation.
6+
import {readFileSync, writeFileSync} from "node:fs";
7+
8+
const file = new URL("../src/generated/twilio-schemas.ts", import.meta.url);
9+
const source = readFileSync(file, "utf8");
10+
writeFileSync(file, source.replaceAll("z.record(", "z.record(z.string(), "));

actions/twilio-action/src/data_types/twilioMessage.ts

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,15 @@
11
import { DisplayMessage, Identifier, Name, Schema } from "@code0-tech/hercules";
22
import { z } from "zod";
3+
import { schemas } from "../generated/twilio-schemas.js";
34

45
/**
5-
* Represents a Twilio Message resource as returned by the Messages API.
6-
* See https://www.twilio.com/docs/messaging/api/message-resource
6+
* TWILIO_MESSAGE is generated from Twilio's official OpenAPI spec
7+
* (twilio/twilio-oai, spec/json/twilio_api_v2010.json). The spec is filtered
8+
* down to the `api.v2010.account.message` component and re-exposed as
9+
* `TwilioMessage` (see scripts/filterTwilioSpec.mjs), then converted to a zod
10+
* schema by openapi-zod-client. Regenerate with `npm run generate:twilio-schemas`.
711
*/
8-
export const TwilioMessageSchema = z.object({
9-
sid: z.string().describe("The unique string that identifies the message resource."),
10-
account_sid: z.string().describe("The SID of the Account that created the message resource."),
11-
messaging_service_sid: z
12-
.string()
13-
.nullish()
14-
.describe("The SID of the Messaging Service used, if the message was sent through one."),
15-
from: z
16-
.string()
17-
.nullish()
18-
.describe("The sender's phone number, short code, or channel address (e.g. whatsapp:+1...)."),
19-
to: z.string().describe("The recipient's phone number, short code, or channel address."),
20-
body: z.string().nullish().describe("The text body of the message. Up to 1600 characters long."),
21-
status: z
22-
.string()
23-
.describe(
24-
"The status of the message. Common values: accepted, scheduled, queued, sending, sent, receiving, received, delivered, undelivered, failed, read, canceled."
25-
),
26-
direction: z
27-
.string()
28-
.describe("The direction of the message: inbound, outbound-api, outbound-call, or outbound-reply."),
29-
num_segments: z.string().nullish().describe("The number of segments that make up the complete message."),
30-
num_media: z.string().nullish().describe("The number of media files associated with the message."),
31-
price: z.string().nullish().describe("The amount billed for the message, in the currency specified by price_unit."),
32-
price_unit: z.string().nullish().describe("The currency in which price is measured, in ISO 4127 format (e.g. USD)."),
33-
error_code: z.number().nullish().describe("The error code returned if the message status is failed or undelivered."),
34-
error_message: z.string().nullish().describe("A human-readable description of the error_code, if any."),
35-
date_created: z.string().nullish().describe("The date and time the message was created, in RFC 2822 format."),
36-
date_updated: z.string().nullish().describe("The date and time the message was last updated, in RFC 2822 format."),
37-
date_sent: z.string().nullish().describe("The date and time the message was sent, in RFC 2822 format."),
38-
api_version: z.string().nullish().describe("The version of the Twilio API used to process the message."),
39-
uri: z.string().nullish().describe("The URI of the message resource, relative to https://api.twilio.com."),
40-
});
12+
export const TwilioMessageSchema = schemas.TwilioMessage;
4113
export type TwilioMessage = z.infer<typeof TwilioMessageSchema>;
4214

4315
@Identifier("TWILIO_MESSAGE")

0 commit comments

Comments
 (0)