@emseepea/create-soap-backed-server 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/initializer-dist/LICENSE +21 -0
- package/initializer-dist/create.mjs +44 -0
- package/initializer-dist/template/README.md +68 -0
- package/initializer-dist/template/contracts/pea-service.wsdl +30 -0
- package/initializer-dist/template/contracts/pea-service.xsd +29 -0
- package/initializer-dist/template/contracts/soap-envelope.xsd +20 -0
- package/initializer-dist/template/eval/meaning.test.mjs +25 -0
- package/initializer-dist/template/package.json +35 -0
- package/initializer-dist/template/scripts/check-generated.mjs +15 -0
- package/initializer-dist/template/scripts/generate-types.mjs +42 -0
- package/initializer-dist/template/src/app.ts +26 -0
- package/initializer-dist/template/src/capabilities/context.ts +5 -0
- package/initializer-dist/template/src/capabilities/tool.get-pea-variety.ts +41 -0
- package/initializer-dist/template/src/generated/pea-service.schema-model.json +205 -0
- package/initializer-dist/template/src/generated/pea-service.ts +34 -0
- package/initializer-dist/template/src/server.ts +21 -0
- package/initializer-dist/template/src/soap-schema.ts +28 -0
- package/initializer-dist/template/src/validating-http-client.ts +125 -0
- package/initializer-dist/template/test/server.test.mjs +250 -0
- package/initializer-dist/template/test-support/soap-fixture.mjs +160 -0
- package/initializer-dist/template/test-types/generated-soap-types.ts +45 -0
- package/initializer-dist/template/tsconfig.json +13 -0
- package/initializer-dist/template/tsconfig.type-tests.json +8 -0
- package/package.json +59 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
|
|
3
|
+
export async function startSoapFixture(test) {
|
|
4
|
+
let requests = 0;
|
|
5
|
+
let lastRequest;
|
|
6
|
+
const server = createServer(async (request, response) => {
|
|
7
|
+
requests += 1;
|
|
8
|
+
const body = await readBody(request);
|
|
9
|
+
lastRequest = Object.freeze({
|
|
10
|
+
body,
|
|
11
|
+
method: request.method,
|
|
12
|
+
path: request.url,
|
|
13
|
+
soapAction: request.headers.soapaction,
|
|
14
|
+
});
|
|
15
|
+
const name = body.match(/<(?:\w+:)?name>([^<]+)<\/(?:\w+:)?name>/)?.[1] ?? "";
|
|
16
|
+
if (name === "Redirect") {
|
|
17
|
+
response.writeHead(302, { Location: "http://example.com/soap" });
|
|
18
|
+
response.end();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (name === "Trickle") {
|
|
22
|
+
response.writeHead(200, { "Content-Type": "text/xml; charset=utf-8" });
|
|
23
|
+
const body = successXml("Trickle", "snap", "60").body;
|
|
24
|
+
let offset = 0;
|
|
25
|
+
const chunkSize = Math.ceil(body.length / 10);
|
|
26
|
+
const interval = setInterval(() => {
|
|
27
|
+
if (offset < body.length) {
|
|
28
|
+
response.write(body.slice(offset, offset + chunkSize));
|
|
29
|
+
offset += chunkSize;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
clearInterval(interval);
|
|
33
|
+
response.end();
|
|
34
|
+
}
|
|
35
|
+
}, 200);
|
|
36
|
+
response.once("close", () => clearInterval(interval));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (name === "Timeout") await new Promise((resolve) => setTimeout(resolve, 2_500));
|
|
40
|
+
const fixture = responseFor(name);
|
|
41
|
+
response.writeHead(fixture.status, { "Content-Type": "text/xml; charset=utf-8" });
|
|
42
|
+
response.end(fixture.body);
|
|
43
|
+
});
|
|
44
|
+
await new Promise((resolve, reject) => {
|
|
45
|
+
server.once("error", reject);
|
|
46
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
47
|
+
});
|
|
48
|
+
test.after(() => new Promise((resolve) => {
|
|
49
|
+
server.closeAllConnections();
|
|
50
|
+
server.close(resolve);
|
|
51
|
+
}));
|
|
52
|
+
const address = server.address();
|
|
53
|
+
if (!address || typeof address === "string") throw new Error("SOAP fixture address unavailable");
|
|
54
|
+
return {
|
|
55
|
+
get lastRequest() { return lastRequest; },
|
|
56
|
+
get requests() { return requests; },
|
|
57
|
+
url: new URL(`http://127.0.0.1:${address.port}/soap`),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function responseFor(name) {
|
|
62
|
+
if (name === "Invalid XML") return { status: 200, body: "<soap:Envelope>" };
|
|
63
|
+
if (name === "Invalid XSD") return successXml("Invalid XSD", "snap", "soon");
|
|
64
|
+
if (name === "Zero Days") return successXml("Zero Days", "snap", "0");
|
|
65
|
+
if (name === "Negative Days") return successXml("Negative Days", "snap", "-1");
|
|
66
|
+
if (name === "Fractional Days") return successXml("Fractional Days", "snap", "1.5");
|
|
67
|
+
if (name === "Valid Error Status") {
|
|
68
|
+
return { ...successXml("Valid Error Status", "snap", "60"), status: 500 };
|
|
69
|
+
}
|
|
70
|
+
if (name === "Empty Type") return successXml("Empty Type", "", "60");
|
|
71
|
+
if (name === "Long Type") return successXml("Long Type", "x".repeat(41), "60");
|
|
72
|
+
if (name === "Too Many Traits") {
|
|
73
|
+
return successXml("Too Many Traits", "snap", "60", undefined, ["1", "2", "3", "4", "5", "6"]);
|
|
74
|
+
}
|
|
75
|
+
if (name === "Missing Required Field") {
|
|
76
|
+
return {
|
|
77
|
+
status: 200,
|
|
78
|
+
body: envelope(
|
|
79
|
+
"<pea:GetPeaResponse><pea:name>Missing Required Field</pea:name>" +
|
|
80
|
+
"<pea:peaType>snap</pea:peaType></pea:GetPeaResponse>",
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (name === "Wrong Namespace") {
|
|
85
|
+
return {
|
|
86
|
+
status: 200,
|
|
87
|
+
body: envelope(
|
|
88
|
+
"<wrong:GetPeaResponse xmlns:wrong=\"urn:wrong\"><wrong:name>Wrong Namespace</wrong:name>" +
|
|
89
|
+
"<wrong:peaType>snap</wrong:peaType><wrong:daysToMaturity>60</wrong:daysToMaturity>" +
|
|
90
|
+
"</wrong:GetPeaResponse>",
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (name === "DTD") return { status: 200, body: `<!DOCTYPE Envelope>${successXml("DTD", "snap", "60").body}` };
|
|
95
|
+
if (name === "Entity") {
|
|
96
|
+
return { status: 200, body: `<!DOCTYPE Envelope [<!ENTITY p "snap">]>${successXml("Entity", "&p;", "60").body}` };
|
|
97
|
+
}
|
|
98
|
+
if (name === "Too Deep") {
|
|
99
|
+
return { status: 200, body: `<root>${"<node>".repeat(40)}value${"</node>".repeat(40)}</root>` };
|
|
100
|
+
}
|
|
101
|
+
if (name === "Too Many Nodes") {
|
|
102
|
+
return { status: 200, body: `<root>${"<node/>".repeat(300)}</root>` };
|
|
103
|
+
}
|
|
104
|
+
if (name === "Too Many Attributes") {
|
|
105
|
+
return {
|
|
106
|
+
status: 200,
|
|
107
|
+
body: envelope(`<pea:GetPeaResponse ${Array.from(
|
|
108
|
+
{ length: 33 },
|
|
109
|
+
(_, index) => `a${index}="x"`,
|
|
110
|
+
).join(" ")}/>`),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (name === "Text Too Long") return successXml("Text Too Long", "snap", "60", "x".repeat(8_193));
|
|
114
|
+
if (name === "Oversized") return successXml("Oversized", "snap", "60", "x".repeat(70_000));
|
|
115
|
+
if (name === "SOAP Fault") {
|
|
116
|
+
return {
|
|
117
|
+
status: 500,
|
|
118
|
+
body: envelope("<soap:Fault><faultcode>soap:Server</faultcode><faultstring>Private fixture detail</faultstring></soap:Fault>"),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (name === "Sugar Ann") {
|
|
122
|
+
return successXml("Sugar Ann", "snap", "56", "Compact plants with edible pods.", ["bush", "early"]);
|
|
123
|
+
}
|
|
124
|
+
if (name === "Boundary Zero Traits") return successXml(name, "x", "1");
|
|
125
|
+
if (name === "Boundary Five Traits") {
|
|
126
|
+
return successXml(name, "x".repeat(40), "1", undefined, ["1", "2", "3", "4", "5"]);
|
|
127
|
+
}
|
|
128
|
+
return successXml(name || "Green Arrow", "shelling", "68", undefined, ["climbing"]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function successXml(name, peaType, days, note, traits = []) {
|
|
132
|
+
return {
|
|
133
|
+
status: 200,
|
|
134
|
+
body: envelope(
|
|
135
|
+
`<pea:GetPeaResponse>` +
|
|
136
|
+
`<pea:name>${escapeXml(name)}</pea:name>` +
|
|
137
|
+
`<pea:peaType>${escapeXml(peaType)}</pea:peaType>` +
|
|
138
|
+
`<pea:daysToMaturity>${escapeXml(days)}</pea:daysToMaturity>` +
|
|
139
|
+
`${note === undefined ? "" : `<pea:note>${escapeXml(note)}</pea:note>`}` +
|
|
140
|
+
traits.map((trait) => `<pea:trait>${escapeXml(trait)}</pea:trait>`).join("") +
|
|
141
|
+
`</pea:GetPeaResponse>`,
|
|
142
|
+
),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function envelope(content) {
|
|
147
|
+
return `<?xml version="1.0" encoding="UTF-8"?>` +
|
|
148
|
+
`<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" ` +
|
|
149
|
+
`xmlns:pea="urn:emseepea:pea-service"><soap:Body>${content}</soap:Body></soap:Envelope>`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function escapeXml(value) {
|
|
153
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function readBody(request) {
|
|
157
|
+
const chunks = [];
|
|
158
|
+
for await (const chunk of request) chunks.push(chunk);
|
|
159
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
160
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { GetPeaRequest, GetPeaResponse } from "../src/generated/pea-service.js";
|
|
2
|
+
|
|
3
|
+
const validRequest: GetPeaRequest = { name: "Sugar Ann" };
|
|
4
|
+
|
|
5
|
+
// @ts-expect-error The XSD requires the request name.
|
|
6
|
+
const missingRequestName: GetPeaRequest = {};
|
|
7
|
+
|
|
8
|
+
const validWithoutOptionalFields: GetPeaResponse = {
|
|
9
|
+
name: "Sugar Ann",
|
|
10
|
+
peaType: "snap",
|
|
11
|
+
daysToMaturity: 56,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const validWithArray: GetPeaResponse = {
|
|
15
|
+
...validWithoutOptionalFields,
|
|
16
|
+
trait: ["bush", "early"],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// @ts-expect-error The XSD requires daysToMaturity.
|
|
20
|
+
const missingRequiredField: GetPeaResponse = {
|
|
21
|
+
name: "Sugar Ann",
|
|
22
|
+
peaType: "snap",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const wrongNumericType: GetPeaResponse = {
|
|
26
|
+
...validWithoutOptionalFields,
|
|
27
|
+
// @ts-expect-error The XSD declares daysToMaturity as numeric.
|
|
28
|
+
daysToMaturity: "56",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const wrongArrayShape: GetPeaResponse = {
|
|
32
|
+
...validWithoutOptionalFields,
|
|
33
|
+
// @ts-expect-error The XSD permits repeated trait elements, represented as an array.
|
|
34
|
+
trait: "early",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
void [
|
|
38
|
+
validRequest,
|
|
39
|
+
missingRequestName,
|
|
40
|
+
validWithoutOptionalFields,
|
|
41
|
+
validWithArray,
|
|
42
|
+
missingRequiredField,
|
|
43
|
+
wrongNumericType,
|
|
44
|
+
wrongArrayShape,
|
|
45
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "NodeNext",
|
|
4
|
+
"moduleResolution": "NodeNext",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"target": "ES2023",
|
|
9
|
+
"types": ["node"],
|
|
10
|
+
"verbatimModuleSyntax": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts"]
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@emseepea/create-soap-backed-server",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Create an Em See Pea server that validates a SOAP service from its XSD contract.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"starterDependencies": [
|
|
8
|
+
"@emseepea/server",
|
|
9
|
+
"@types/sax",
|
|
10
|
+
"soap",
|
|
11
|
+
"xml-xsd-engine",
|
|
12
|
+
"zod"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "npm run build:example && npm run build:initializer",
|
|
16
|
+
"build:example": "tsc -p tsconfig.json && tsc -p tsconfig.type-tests.json",
|
|
17
|
+
"build:initializer": "node ../../scripts/build-initializer.mjs",
|
|
18
|
+
"generate": "node scripts/generate-types.mjs",
|
|
19
|
+
"generate:check": "node scripts/check-generated.mjs",
|
|
20
|
+
"start": "node dist/server.js",
|
|
21
|
+
"test": "npm run build && npm run test:built",
|
|
22
|
+
"test:built": "node --test test/*.test.mjs",
|
|
23
|
+
"test:llm": "npm run build && npm run test:llm:built",
|
|
24
|
+
"test:llm:built": "emseepea-test eval",
|
|
25
|
+
"lint": "oxlint src test test-types eval scripts test-support",
|
|
26
|
+
"prepack": "npm run build:initializer"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@emseepea/server": "0.3.3",
|
|
30
|
+
"soap": "1.11.0",
|
|
31
|
+
"xml-xsd-engine": "1.7.3",
|
|
32
|
+
"zod": "4.4.3",
|
|
33
|
+
"@emseepea/testing": "0.5.3",
|
|
34
|
+
"@types/node": "24.13.3",
|
|
35
|
+
"@types/sax": "1.2.7",
|
|
36
|
+
"typescript": "6.0.3",
|
|
37
|
+
"oxlint": "1.80.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.13.0"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/emseepea/emseepea.git",
|
|
45
|
+
"directory": "examples/soap-backed-server"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://emseepea.github.io/emseepea/examples/",
|
|
48
|
+
"bugs": "https://github.com/emseepea/emseepea/issues",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public",
|
|
51
|
+
"provenance": true
|
|
52
|
+
},
|
|
53
|
+
"bin": {
|
|
54
|
+
"create-soap-backed-server": "./initializer-dist/create.mjs"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"initializer-dist"
|
|
58
|
+
]
|
|
59
|
+
}
|