@workos/oagen-emitters 0.12.0 → 0.12.1
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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-C408Wh-o.mjs → plugin-CmfzawTp.mjs} +825 -66
- package/dist/plugin-CmfzawTp.mjs.map +1 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/package.json +9 -9
- package/src/rust/fixtures.ts +87 -1
- package/src/rust/models.ts +17 -2
- package/src/rust/resources.ts +697 -62
- package/src/rust/tests.ts +540 -20
- package/test/rust/fixtures.test.ts +227 -0
- package/test/rust/models.test.ts +38 -0
- package/test/rust/resources.test.ts +505 -2
- package/test/rust/tests.test.ts +504 -0
- package/dist/plugin-C408Wh-o.mjs.map +0 -1
|
@@ -2450,17 +2450,9 @@ function dump$1(input, options) {
|
|
|
2450
2450
|
return "";
|
|
2451
2451
|
}
|
|
2452
2452
|
var dumper = { dump: dump$1 };
|
|
2453
|
-
function renamed(from, to) {
|
|
2454
|
-
return function() {
|
|
2455
|
-
throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
|
|
2456
|
-
};
|
|
2457
|
-
}
|
|
2458
2453
|
var load = loader.load;
|
|
2459
2454
|
loader.loadAll;
|
|
2460
2455
|
dumper.dump;
|
|
2461
|
-
renamed("safeLoad", "load");
|
|
2462
|
-
renamed("safeLoadAll", "loadAll");
|
|
2463
|
-
renamed("safeDump", "dump");
|
|
2464
2456
|
//#endregion
|
|
2465
2457
|
//#region src/shared/model-utils.ts
|
|
2466
2458
|
/**
|
|
@@ -6356,7 +6348,7 @@ function renderVoidTest(lines, op, plan, method, serviceProp, modelMap) {
|
|
|
6356
6348
|
lines.push(" });");
|
|
6357
6349
|
}
|
|
6358
6350
|
function renderErrorTest(lines, op, plan, method, serviceProp, modelMap) {
|
|
6359
|
-
const args = buildCallArgs(op, plan, modelMap);
|
|
6351
|
+
const args = buildCallArgs$1(op, plan, modelMap);
|
|
6360
6352
|
lines.push("");
|
|
6361
6353
|
lines.push(` testUnauthorized(() => workos.${serviceProp}.${method}(${args}));`);
|
|
6362
6354
|
const errorStatuses = new Set(op.errors.map((e) => e.statusCode));
|
|
@@ -6379,7 +6371,7 @@ function renderErrorTest(lines, op, plan, method, serviceProp, modelMap) {
|
|
|
6379
6371
|
* Build the argument string for a method call in tests.
|
|
6380
6372
|
* Shared by renderErrorTest and other test renderers.
|
|
6381
6373
|
*/
|
|
6382
|
-
function buildCallArgs(op, plan, modelMap) {
|
|
6374
|
+
function buildCallArgs$1(op, plan, modelMap) {
|
|
6383
6375
|
const pathArgs = buildTestPathArgs(op);
|
|
6384
6376
|
const isPaginated = plan.isPaginated;
|
|
6385
6377
|
const hasBody = plan.hasBody;
|
|
@@ -23562,7 +23554,12 @@ function resolveFieldNames(fields) {
|
|
|
23562
23554
|
}
|
|
23563
23555
|
function renderField(field, rustField, modelName, registry) {
|
|
23564
23556
|
const lines = [];
|
|
23565
|
-
|
|
23557
|
+
const hasDescription = !!field.description;
|
|
23558
|
+
if (hasDescription) for (const c of docComment$1(field.description)) lines.push(` ${c}`);
|
|
23559
|
+
if (field.default != null) {
|
|
23560
|
+
if (hasDescription) lines.push(" ///");
|
|
23561
|
+
lines.push(` /// Defaults to \`${formatDefault$1(field.default)}\`.`);
|
|
23562
|
+
}
|
|
23566
23563
|
const rename = rustField !== field.name ? field.name : null;
|
|
23567
23564
|
let baseType = mapTypeRef$1(field.type, {
|
|
23568
23565
|
hint: `${typeName(modelName)}${typeName(field.name)}`,
|
|
@@ -23587,6 +23584,15 @@ function renderModelsBarrel(modules) {
|
|
|
23587
23584
|
function docComment$1(text) {
|
|
23588
23585
|
return text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).map((l) => `/// ${l}`);
|
|
23589
23586
|
}
|
|
23587
|
+
/**
|
|
23588
|
+
* Render a spec-level default value for inclusion in a doc comment. Strings
|
|
23589
|
+
* render bare (e.g. `desc`) so they nest naturally inside the surrounding
|
|
23590
|
+
* backticks; numbers/booleans use JSON encoding.
|
|
23591
|
+
*/
|
|
23592
|
+
function formatDefault$1(value) {
|
|
23593
|
+
if (typeof value === "string") return value;
|
|
23594
|
+
return JSON.stringify(value);
|
|
23595
|
+
}
|
|
23590
23596
|
//#endregion
|
|
23591
23597
|
//#region src/rust/enums.ts
|
|
23592
23598
|
/**
|
|
@@ -23807,6 +23813,7 @@ function renderMountGroup(mountName, resolvedOps, ctx, registry, _lookup) {
|
|
|
23807
23813
|
lines.push(" pub(crate) client: &'a Client,");
|
|
23808
23814
|
lines.push("}");
|
|
23809
23815
|
lines.push("");
|
|
23816
|
+
const groupEmitter = new GroupEmitter();
|
|
23810
23817
|
const paramsStructs = [];
|
|
23811
23818
|
const methods = [];
|
|
23812
23819
|
const seenMethods = /* @__PURE__ */ new Set();
|
|
@@ -23819,7 +23826,7 @@ function renderMountGroup(mountName, resolvedOps, ctx, registry, _lookup) {
|
|
|
23819
23826
|
seenMethods.add(wrapperMethodName);
|
|
23820
23827
|
const paramsType = `${typeName(wrapper.name)}Params`;
|
|
23821
23828
|
const params = resolveWrapperParams(wrapper, ctx);
|
|
23822
|
-
paramsStructs.push(renderWrapperParamsStruct(paramsType, op, wrapper, params, registry));
|
|
23829
|
+
paramsStructs.push(renderWrapperParamsStruct(paramsType, op, wrapper, params, registry, ctx));
|
|
23823
23830
|
methods.push(renderWrapperMethod(op, wrapper, params, paramsType, wrapperMethodName));
|
|
23824
23831
|
}
|
|
23825
23832
|
continue;
|
|
@@ -23827,13 +23834,25 @@ function renderMountGroup(mountName, resolvedOps, ctx, registry, _lookup) {
|
|
|
23827
23834
|
const m = methodName(resolved.methodName);
|
|
23828
23835
|
if (seenMethods.has(m)) continue;
|
|
23829
23836
|
seenMethods.add(m);
|
|
23837
|
+
if (resolved.urlBuilder) {
|
|
23838
|
+
const paramsType = `${typeName(resolved.methodName)}Params`;
|
|
23839
|
+
const emptyParams = isEmptyParams(op, resolved);
|
|
23840
|
+
if (!emptyParams) paramsStructs.push(renderParamsStruct(paramsType, op, resolved, registry, ctx, groupEmitter));
|
|
23841
|
+
methods.push(renderUrlBuilderMethod(op, resolved, paramsType, m, emptyParams));
|
|
23842
|
+
continue;
|
|
23843
|
+
}
|
|
23830
23844
|
const paramsType = `${typeName(resolved.methodName)}Params`;
|
|
23831
23845
|
const emptyParams = isEmptyParams(op, resolved);
|
|
23832
|
-
if (!emptyParams) paramsStructs.push(renderParamsStruct(paramsType, op, resolved, registry));
|
|
23846
|
+
if (!emptyParams) paramsStructs.push(renderParamsStruct(paramsType, op, resolved, registry, ctx, groupEmitter));
|
|
23833
23847
|
methods.push(renderMethod(op, resolved, paramsType, m, emptyParams));
|
|
23834
23848
|
const autoPaging = renderAutoPagingMethod(op, resolved, paramsType, m, ctx);
|
|
23835
23849
|
if (autoPaging) methods.push(autoPaging);
|
|
23836
23850
|
}
|
|
23851
|
+
const groupBlock = groupEmitter.render();
|
|
23852
|
+
if (groupBlock.length > 0) {
|
|
23853
|
+
lines.push(groupBlock);
|
|
23854
|
+
lines.push("");
|
|
23855
|
+
}
|
|
23837
23856
|
for (const s of paramsStructs) {
|
|
23838
23857
|
lines.push(s);
|
|
23839
23858
|
lines.push("");
|
|
@@ -23846,14 +23865,158 @@ function renderMountGroup(mountName, resolvedOps, ctx, registry, _lookup) {
|
|
|
23846
23865
|
lines.push("}");
|
|
23847
23866
|
return lines.join("\n").replace(/\n+$/g, "\n");
|
|
23848
23867
|
}
|
|
23849
|
-
|
|
23868
|
+
var GroupEmitter = class {
|
|
23869
|
+
enums = [];
|
|
23870
|
+
bodies = [];
|
|
23871
|
+
seenEnums = /* @__PURE__ */ new Set();
|
|
23872
|
+
seenBodies = /* @__PURE__ */ new Set();
|
|
23873
|
+
/** Register a parameter-group enum, returning the PascalCase Rust name. */
|
|
23874
|
+
registerEnum(group) {
|
|
23875
|
+
const name = typeName(group.name);
|
|
23876
|
+
const variants = group.variants.map((v) => ({
|
|
23877
|
+
name: typeName(v.name),
|
|
23878
|
+
fields: v.parameters.map((p) => ({
|
|
23879
|
+
rustName: fieldName(p.name),
|
|
23880
|
+
wireName: p.name,
|
|
23881
|
+
rustType: rustTypeForGroupParam(p.type)
|
|
23882
|
+
}))
|
|
23883
|
+
}));
|
|
23884
|
+
if (!this.seenEnums.has(name)) {
|
|
23885
|
+
this.seenEnums.add(name);
|
|
23886
|
+
this.enums.push({
|
|
23887
|
+
name,
|
|
23888
|
+
variants
|
|
23889
|
+
});
|
|
23890
|
+
}
|
|
23891
|
+
return name;
|
|
23892
|
+
}
|
|
23893
|
+
/** Register a synthetic body struct, returning its PascalCase Rust name. */
|
|
23894
|
+
registerBody(spec) {
|
|
23895
|
+
if (!this.seenBodies.has(spec.name)) {
|
|
23896
|
+
this.seenBodies.add(spec.name);
|
|
23897
|
+
this.bodies.push(spec);
|
|
23898
|
+
}
|
|
23899
|
+
return spec.name;
|
|
23900
|
+
}
|
|
23901
|
+
render() {
|
|
23902
|
+
const blocks = [];
|
|
23903
|
+
for (const e of this.enums) {
|
|
23904
|
+
const lines = [];
|
|
23905
|
+
lines.push("#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]");
|
|
23906
|
+
lines.push("#[serde(untagged)]");
|
|
23907
|
+
lines.push(`pub enum ${e.name} {`);
|
|
23908
|
+
for (const v of e.variants) {
|
|
23909
|
+
if (v.fields.length === 0) {
|
|
23910
|
+
lines.push(` ${v.name},`);
|
|
23911
|
+
continue;
|
|
23912
|
+
}
|
|
23913
|
+
lines.push(` ${v.name} {`);
|
|
23914
|
+
for (const f of v.fields) {
|
|
23915
|
+
if (f.rustName !== f.wireName) lines.push(` #[serde(rename = ${JSON.stringify(f.wireName)})]`);
|
|
23916
|
+
lines.push(` ${f.rustName}: ${f.rustType},`);
|
|
23917
|
+
}
|
|
23918
|
+
lines.push(" },");
|
|
23919
|
+
}
|
|
23920
|
+
lines.push("}");
|
|
23921
|
+
blocks.push(lines.join("\n"));
|
|
23922
|
+
}
|
|
23923
|
+
for (const b of this.bodies) {
|
|
23924
|
+
const lines = [];
|
|
23925
|
+
const everythingOptional = b.flatFields.every((f) => !f.required) && b.flattenEnums.every((f) => !f.required);
|
|
23926
|
+
const baseDerives = "Debug, Clone, serde::Serialize, serde::Deserialize";
|
|
23927
|
+
const derives = everythingOptional ? `${baseDerives}, Default` : baseDerives;
|
|
23928
|
+
lines.push(`#[derive(${derives})]`);
|
|
23929
|
+
lines.push(`pub struct ${b.name} {`);
|
|
23930
|
+
for (const f of b.flatFields) {
|
|
23931
|
+
if (f.doc) for (const c of paramDocComment(f.doc)) lines.push(` ${c}`);
|
|
23932
|
+
if (f.required) {
|
|
23933
|
+
if (f.doc) lines.push(" ///");
|
|
23934
|
+
lines.push(" /// Required.");
|
|
23935
|
+
}
|
|
23936
|
+
if (!f.required) lines.push(" #[serde(skip_serializing_if = \"Option::is_none\")]");
|
|
23937
|
+
if (f.rustName !== f.wireName) lines.push(` #[serde(rename = ${JSON.stringify(f.wireName)})]`);
|
|
23938
|
+
lines.push(` pub ${f.rustName}: ${f.rustType},`);
|
|
23939
|
+
}
|
|
23940
|
+
for (const f of b.flattenEnums) {
|
|
23941
|
+
if (f.doc) for (const c of paramDocComment(f.doc)) lines.push(` ${c}`);
|
|
23942
|
+
lines.push(" #[serde(flatten)]");
|
|
23943
|
+
if (!f.required) lines.push(" #[serde(skip_serializing_if = \"Option::is_none\")]");
|
|
23944
|
+
lines.push(` pub ${f.rustName}: ${f.rustType},`);
|
|
23945
|
+
}
|
|
23946
|
+
lines.push("}");
|
|
23947
|
+
const required = [...b.flatFields.filter((f) => f.required), ...b.flattenEnums.filter((f) => f.required)];
|
|
23948
|
+
if (required.length > 0 || b.flatFields.length + b.flattenEnums.length > 0) {
|
|
23949
|
+
const ctorArgs = required.map((f) => `${f.rustName}: ${ctorParamType(f.rustType)}`).join(", ");
|
|
23950
|
+
const init = [];
|
|
23951
|
+
for (const f of b.flatFields) if (f.required) {
|
|
23952
|
+
const value = ctorParamConvert(f.rustType, f.rustName);
|
|
23953
|
+
init.push(value === f.rustName ? ` ${f.rustName},` : ` ${f.rustName}: ${value},`);
|
|
23954
|
+
} else init.push(` ${f.rustName}: Default::default(),`);
|
|
23955
|
+
for (const f of b.flattenEnums) if (f.required) {
|
|
23956
|
+
const value = ctorParamConvert(f.rustType, f.rustName);
|
|
23957
|
+
init.push(value === f.rustName ? ` ${f.rustName},` : ` ${f.rustName}: ${value},`);
|
|
23958
|
+
} else init.push(` ${f.rustName}: Default::default(),`);
|
|
23959
|
+
lines.push("");
|
|
23960
|
+
lines.push(`impl ${b.name} {`);
|
|
23961
|
+
lines.push(` /// Construct a new \`${b.name}\` with the required fields set.`);
|
|
23962
|
+
lines.push(` pub fn new(${ctorArgs}) -> Self {`);
|
|
23963
|
+
lines.push(" Self {");
|
|
23964
|
+
for (const l of init) lines.push(l);
|
|
23965
|
+
lines.push(" }");
|
|
23966
|
+
lines.push(" }");
|
|
23967
|
+
lines.push("}");
|
|
23968
|
+
}
|
|
23969
|
+
blocks.push(lines.join("\n"));
|
|
23970
|
+
}
|
|
23971
|
+
return blocks.join("\n\n");
|
|
23972
|
+
}
|
|
23973
|
+
};
|
|
23974
|
+
/**
|
|
23975
|
+
* Render the Rust type for a parameter-group variant field. Variants commit
|
|
23976
|
+
* the caller to supplying their full payload, so optional individual params
|
|
23977
|
+
* still flow as `String` (not `Option<String>`); the enum-level choice is the
|
|
23978
|
+
* one source of truth for "did the caller pick this variant or not."
|
|
23979
|
+
*/
|
|
23980
|
+
function rustTypeForGroupParam(type) {
|
|
23981
|
+
const rust = mapTypeRef$1(type);
|
|
23982
|
+
if (rust.startsWith("Option<")) return rust.slice(7, -1);
|
|
23983
|
+
return rust;
|
|
23984
|
+
}
|
|
23985
|
+
/** Classify each parameter group on an op as "query" or "body". */
|
|
23986
|
+
function classifyGroup(group, op) {
|
|
23987
|
+
const queryNames = new Set(op.queryParams.map((qp) => qp.name));
|
|
23988
|
+
return group.variants.every((v) => v.parameters.every((p) => queryNames.has(p.name))) ? "query" : "body";
|
|
23989
|
+
}
|
|
23990
|
+
function renderParamsStruct(name, op, resolved, registry, ctx, groupEmitter) {
|
|
23850
23991
|
const bodyRequired = isBodyRequired(op);
|
|
23851
23992
|
const hidden = new Set([...Object.keys(resolved.defaults ?? {}), ...resolved.inferFromClient ?? []]);
|
|
23993
|
+
const queryGroupParamNames = /* @__PURE__ */ new Set();
|
|
23994
|
+
const bodyGroupParamNames = /* @__PURE__ */ new Set();
|
|
23995
|
+
const queryGroupFields = [];
|
|
23996
|
+
const bodyGroupFields = [];
|
|
23997
|
+
for (const group of op.parameterGroups ?? []) {
|
|
23998
|
+
const enumName = groupEmitter.registerEnum(group);
|
|
23999
|
+
const rustType = group.optional ? `Option<${enumName}>` : enumName;
|
|
24000
|
+
const groupField = {
|
|
24001
|
+
name: fieldName(group.name),
|
|
24002
|
+
rustType,
|
|
24003
|
+
required: !group.optional,
|
|
24004
|
+
doc: void 0
|
|
24005
|
+
};
|
|
24006
|
+
if (classifyGroup(group, op) === "query") {
|
|
24007
|
+
for (const v of group.variants) for (const p of v.parameters) queryGroupParamNames.add(p.name);
|
|
24008
|
+
queryGroupFields.push(groupField);
|
|
24009
|
+
} else {
|
|
24010
|
+
for (const v of group.variants) for (const p of v.parameters) bodyGroupParamNames.add(p.name);
|
|
24011
|
+
bodyGroupFields.push(groupField);
|
|
24012
|
+
}
|
|
24013
|
+
}
|
|
23852
24014
|
const fields = [];
|
|
23853
24015
|
const fieldLines = [];
|
|
23854
24016
|
const seen = /* @__PURE__ */ new Set();
|
|
23855
|
-
const emitField = (p) => {
|
|
24017
|
+
const emitField = (p, opts = {}) => {
|
|
23856
24018
|
if (hidden.has(p.name)) return;
|
|
24019
|
+
if (queryGroupParamNames.has(p.name)) return;
|
|
23857
24020
|
const fname = fieldName(p.name);
|
|
23858
24021
|
if (seen.has(fname)) return;
|
|
23859
24022
|
seen.add(fname);
|
|
@@ -23863,26 +24026,47 @@ function renderParamsStruct(name, op, resolved, registry) {
|
|
|
23863
24026
|
});
|
|
23864
24027
|
if (!p.required && !rust.startsWith("Option<")) rust = makeOptional(rust);
|
|
23865
24028
|
rust = applySecretRedaction(rust, p.name);
|
|
24029
|
+
const defaultExpr = p.default != null ? rustDefaultExpr(p.default, p.type, rust.startsWith("Option<"), ctx) : null;
|
|
23866
24030
|
const desc = p.description?.trim();
|
|
23867
24031
|
if (desc) for (const c of paramDocComment(desc)) fieldLines.push(` ${c}`);
|
|
23868
|
-
if (p.
|
|
24032
|
+
if (p.default != null) {
|
|
23869
24033
|
if (desc) fieldLines.push(" ///");
|
|
24034
|
+
fieldLines.push(` /// Defaults to \`${formatDefault(p.default)}\`.`);
|
|
24035
|
+
}
|
|
24036
|
+
if (p.required && !rust.startsWith("Option<")) {
|
|
24037
|
+
if (desc || p.default != null) fieldLines.push(" ///");
|
|
23870
24038
|
fieldLines.push(" /// Required.");
|
|
23871
24039
|
}
|
|
23872
24040
|
if (rust.startsWith("Option<")) fieldLines.push(" #[serde(skip_serializing_if = \"Option::is_none\")]");
|
|
24041
|
+
if (opts.isQuery && p.explode === false && isVecType(rust)) fieldLines.push(rust.startsWith("Option<") ? " #[serde(serialize_with = \"crate::query::serialize_comma_separated_opt\")]" : " #[serde(serialize_with = \"crate::query::serialize_comma_separated\")]");
|
|
23873
24042
|
if (fname !== p.name) fieldLines.push(` #[serde(rename = ${JSON.stringify(p.name)})]`);
|
|
23874
24043
|
if (p.deprecated) fieldLines.push(" #[deprecated]");
|
|
23875
24044
|
fieldLines.push(` pub ${fname}: ${rust},`);
|
|
23876
24045
|
fields.push({
|
|
23877
24046
|
fname,
|
|
23878
24047
|
rust,
|
|
23879
|
-
required: !!p.required && !rust.startsWith("Option<")
|
|
24048
|
+
required: !!p.required && !rust.startsWith("Option<"),
|
|
24049
|
+
defaultExpr
|
|
23880
24050
|
});
|
|
23881
24051
|
};
|
|
23882
|
-
for (const p of op.queryParams) emitField(p);
|
|
24052
|
+
for (const p of op.queryParams) emitField(p, { isQuery: true });
|
|
23883
24053
|
for (const p of op.headerParams) emitField(p);
|
|
24054
|
+
for (const p of op.cookieParams ?? []) emitField(p);
|
|
24055
|
+
for (const g of queryGroupFields) {
|
|
24056
|
+
fieldLines.push(" #[serde(flatten)]");
|
|
24057
|
+
if (!g.required) fieldLines.push(" #[serde(skip_serializing_if = \"Option::is_none\")]");
|
|
24058
|
+
fieldLines.push(` pub ${g.name}: ${g.rustType},`);
|
|
24059
|
+
fields.push({
|
|
24060
|
+
fname: g.name,
|
|
24061
|
+
rust: g.rustType,
|
|
24062
|
+
required: g.required,
|
|
24063
|
+
defaultExpr: null
|
|
24064
|
+
});
|
|
24065
|
+
}
|
|
23884
24066
|
if (op.requestBody) {
|
|
23885
|
-
let bodyType
|
|
24067
|
+
let bodyType;
|
|
24068
|
+
if (bodyGroupFields.length > 0) bodyType = registerSyntheticBody(op, name, bodyGroupParamNames, bodyGroupFields, ctx, registry, groupEmitter);
|
|
24069
|
+
else bodyType = mapTypeRef$1(op.requestBody, {
|
|
23886
24070
|
hint: `${name}Body`,
|
|
23887
24071
|
registry
|
|
23888
24072
|
});
|
|
@@ -23895,21 +24079,36 @@ function renderParamsStruct(name, op, resolved, registry) {
|
|
|
23895
24079
|
fields.push({
|
|
23896
24080
|
fname: "body",
|
|
23897
24081
|
rust: bodyType,
|
|
23898
|
-
required: bodyRequired
|
|
24082
|
+
required: bodyRequired,
|
|
24083
|
+
defaultExpr: null
|
|
23899
24084
|
});
|
|
23900
24085
|
}
|
|
23901
24086
|
const requiredFields = fields.filter((f) => f.required);
|
|
23902
|
-
const
|
|
24087
|
+
const allOptional = fields.length === 0 || requiredFields.length === 0;
|
|
24088
|
+
const hasSpecDefault = fields.some((f) => f.defaultExpr !== null);
|
|
24089
|
+
const derives = allOptional && !hasSpecDefault ? "Debug, Clone, Default, Serialize" : "Debug, Clone, Serialize";
|
|
23903
24090
|
const out = [];
|
|
23904
24091
|
if (fieldLines.length === 0) out.push(`#[derive(${derives})]`, `pub struct ${name} {}`);
|
|
23905
24092
|
else out.push(`#[derive(${derives})]`, `pub struct ${name} {`, ...fieldLines, "}");
|
|
24093
|
+
if (allOptional && hasSpecDefault) {
|
|
24094
|
+
const defaultInitLines = fields.map((f) => ` ${f.fname}: ${f.defaultExpr ?? "Default::default()"},`);
|
|
24095
|
+
out.push("");
|
|
24096
|
+
out.push(`impl Default for ${name} {`);
|
|
24097
|
+
out.push(" #[allow(deprecated)]");
|
|
24098
|
+
out.push(" fn default() -> Self {");
|
|
24099
|
+
out.push(" Self {");
|
|
24100
|
+
out.push(...defaultInitLines);
|
|
24101
|
+
out.push(" }");
|
|
24102
|
+
out.push(" }");
|
|
24103
|
+
out.push("}");
|
|
24104
|
+
}
|
|
23906
24105
|
if (requiredFields.length > 0) {
|
|
23907
24106
|
const ctorArgs = requiredFields.map((f) => `${f.fname}: ${ctorParamType(f.rust)}`).join(", ");
|
|
23908
24107
|
const initLines = [];
|
|
23909
24108
|
for (const f of fields) if (f.required) {
|
|
23910
24109
|
const value = ctorParamConvert(f.rust, f.fname);
|
|
23911
24110
|
initLines.push(value === f.fname ? ` ${f.fname},` : ` ${f.fname}: ${value},`);
|
|
23912
|
-
} else initLines.push(` ${f.fname}: Default::default(),`);
|
|
24111
|
+
} else initLines.push(` ${f.fname}: ${f.defaultExpr ?? "Default::default()"},`);
|
|
23913
24112
|
out.push("");
|
|
23914
24113
|
out.push(`impl ${name} {`);
|
|
23915
24114
|
out.push(` /// Construct a new \`${name}\` with the required fields set.`);
|
|
@@ -23923,6 +24122,52 @@ function renderParamsStruct(name, op, resolved, registry) {
|
|
|
23923
24122
|
}
|
|
23924
24123
|
return out.join("\n");
|
|
23925
24124
|
}
|
|
24125
|
+
/** True when a Rust type expression is a `Vec<…>` (or `Option<Vec<…>>`). */
|
|
24126
|
+
function isVecType(rust) {
|
|
24127
|
+
return (rust.startsWith("Option<") ? rust.slice(7, -1) : rust).startsWith("Vec<");
|
|
24128
|
+
}
|
|
24129
|
+
/**
|
|
24130
|
+
* Build a synthetic body struct for an op that has body-side parameter
|
|
24131
|
+
* groups. The original body model can't be reused as-is because its grouped
|
|
24132
|
+
* fields are still flat optionals; the synthetic type swaps them for a
|
|
24133
|
+
* flattened enum so callers commit to one variant at construction time.
|
|
24134
|
+
*/
|
|
24135
|
+
function registerSyntheticBody(op, paramsName, bodyGroupParamNames, bodyGroupFields, ctx, registry, groupEmitter) {
|
|
24136
|
+
const bodyRef = op.requestBody;
|
|
24137
|
+
if (!bodyRef || bodyRef.kind !== "model") return mapTypeRef$1(bodyRef, {
|
|
24138
|
+
hint: `${paramsName}Body`,
|
|
24139
|
+
registry
|
|
24140
|
+
});
|
|
24141
|
+
const model = ctx.spec.models.find((m) => m.name === bodyRef.name);
|
|
24142
|
+
if (!model) return typeName(bodyRef.name);
|
|
24143
|
+
const name = `${paramsName}Body`;
|
|
24144
|
+
const flatFields = model.fields.filter((f) => !bodyGroupParamNames.has(f.name)).map((f) => {
|
|
24145
|
+
let rust = mapTypeRef$1(f.type, {
|
|
24146
|
+
hint: `${name}${typeName(f.name)}`,
|
|
24147
|
+
registry
|
|
24148
|
+
});
|
|
24149
|
+
if (!f.required && !rust.startsWith("Option<")) rust = makeOptional(rust);
|
|
24150
|
+
rust = applySecretRedaction(rust, f.name);
|
|
24151
|
+
return {
|
|
24152
|
+
rustName: fieldName(f.name),
|
|
24153
|
+
wireName: f.name,
|
|
24154
|
+
rustType: rust,
|
|
24155
|
+
required: !!f.required && !rust.startsWith("Option<"),
|
|
24156
|
+
doc: f.description
|
|
24157
|
+
};
|
|
24158
|
+
});
|
|
24159
|
+
const flattenEnums = bodyGroupFields.map((g) => ({
|
|
24160
|
+
rustName: g.name,
|
|
24161
|
+
rustType: g.rustType,
|
|
24162
|
+
required: g.required,
|
|
24163
|
+
doc: g.doc
|
|
24164
|
+
}));
|
|
24165
|
+
return groupEmitter.registerBody({
|
|
24166
|
+
name,
|
|
24167
|
+
flatFields,
|
|
24168
|
+
flattenEnums
|
|
24169
|
+
});
|
|
24170
|
+
}
|
|
23926
24171
|
/** Constructor parameter type — accept `impl Into<String>` for ergonomic strings. */
|
|
23927
24172
|
function ctorParamType(rust) {
|
|
23928
24173
|
if (rust === "String") return "impl Into<String>";
|
|
@@ -23934,6 +24179,16 @@ function ctorParamConvert(rust, name) {
|
|
|
23934
24179
|
if (rust === "crate::SecretString") return `${name}.into()`;
|
|
23935
24180
|
return name;
|
|
23936
24181
|
}
|
|
24182
|
+
/**
|
|
24183
|
+
* Detect a non-default per-operation security requirement (e.g. SSO's
|
|
24184
|
+
* `get_profile` requires an OAuth access token rather than the WorkOS API
|
|
24185
|
+
* key). Returns the snake_case parameter name to use for the override.
|
|
24186
|
+
*/
|
|
24187
|
+
function bearerOverrideToken(op) {
|
|
24188
|
+
const override = op.security?.find((s) => s.schemeName !== "bearerAuth");
|
|
24189
|
+
if (!override) return null;
|
|
24190
|
+
return fieldName(override.schemeName);
|
|
24191
|
+
}
|
|
23937
24192
|
function renderMethod(op, resolved, paramsType, method, emptyParams) {
|
|
23938
24193
|
planOperation(op);
|
|
23939
24194
|
const segments = parsePathTemplate(op.path);
|
|
@@ -23941,13 +24196,15 @@ function renderMethod(op, resolved, paramsType, method, emptyParams) {
|
|
|
23941
24196
|
const pathArgNames = op.pathParams.map((p) => methodName(p.name));
|
|
23942
24197
|
const returnType = renderResponseType(op);
|
|
23943
24198
|
const bodyRequired = isBodyRequired(op);
|
|
24199
|
+
const tokenParam = bearerOverrideToken(op);
|
|
23944
24200
|
const sig = [];
|
|
23945
24201
|
for (const line of methodDocLines(op)) sig.push(` ${line}`);
|
|
23946
24202
|
if (op.deprecated) sig.push(" #[deprecated]");
|
|
23947
24203
|
const argsConvenience = [
|
|
23948
24204
|
"&self",
|
|
23949
24205
|
...pathArgList,
|
|
23950
|
-
...emptyParams ? [] : [`params: ${paramsType}`]
|
|
24206
|
+
...emptyParams ? [] : [`params: ${paramsType}`],
|
|
24207
|
+
...tokenParam ? [`${tokenParam}: impl Into<String>`] : []
|
|
23951
24208
|
];
|
|
23952
24209
|
const convenienceSig = ` pub async fn ${method}(${argsConvenience.join(", ")}) -> Result<${returnType}, Error> {`;
|
|
23953
24210
|
if (convenienceSig.length <= 100) sig.push(convenienceSig);
|
|
@@ -23959,6 +24216,7 @@ function renderMethod(op, resolved, paramsType, method, emptyParams) {
|
|
|
23959
24216
|
const delegateArgs = [
|
|
23960
24217
|
...pathArgNames,
|
|
23961
24218
|
...emptyParams ? [] : ["params"],
|
|
24219
|
+
...tokenParam ? [tokenParam] : [],
|
|
23962
24220
|
"None"
|
|
23963
24221
|
].join(", ");
|
|
23964
24222
|
sig.push(` self.${method}_with_options(${delegateArgs}).await`);
|
|
@@ -23970,6 +24228,7 @@ function renderMethod(op, resolved, paramsType, method, emptyParams) {
|
|
|
23970
24228
|
"&self",
|
|
23971
24229
|
...pathArgList,
|
|
23972
24230
|
...emptyParams ? [] : [`params: ${paramsType}`],
|
|
24231
|
+
...tokenParam ? [`${tokenParam}: impl Into<String>`] : [],
|
|
23973
24232
|
"options: Option<&crate::RequestOptions>"
|
|
23974
24233
|
];
|
|
23975
24234
|
const optsSig = ` pub async fn ${method}_with_options(${argsOpts.join(", ")}) -> Result<${returnType}, Error> {`;
|
|
@@ -23988,6 +24247,14 @@ function renderMethod(op, resolved, paramsType, method, emptyParams) {
|
|
|
23988
24247
|
sig.push(` let path = format!(${JSON.stringify(pathFormat)});`);
|
|
23989
24248
|
} else sig.push(` let path = ${JSON.stringify(pathFormat)}.to_string();`);
|
|
23990
24249
|
sig.push(` let method = http::Method::${op.httpMethod.toUpperCase()};`);
|
|
24250
|
+
if (tokenParam) {
|
|
24251
|
+
sig.push(` let ${tokenParam}: String = ${tokenParam}.into();`);
|
|
24252
|
+
sig.push(` let auth = http::HeaderValue::from_str(&format!("Bearer {${tokenParam}}"))`);
|
|
24253
|
+
sig.push(` .map_err(|e| Error::Builder(format!("invalid bearer token: {e}")))?;`);
|
|
24254
|
+
sig.push(" let mut merged = options.cloned().unwrap_or_default();");
|
|
24255
|
+
sig.push(" merged.extra_headers.push((http::header::AUTHORIZATION, auth));");
|
|
24256
|
+
sig.push(" let options = Some(&merged);");
|
|
24257
|
+
}
|
|
23991
24258
|
const queryRef = emptyParams ? "&()" : "¶ms";
|
|
23992
24259
|
const emptyResp = isEmptyResponse(op);
|
|
23993
24260
|
const bodyMethod = emptyResp ? "request_with_body_opts_empty" : "request_with_body_opts";
|
|
@@ -24006,28 +24273,95 @@ function renderMethod(op, resolved, paramsType, method, emptyParams) {
|
|
|
24006
24273
|
return sig.join("\n");
|
|
24007
24274
|
}
|
|
24008
24275
|
/**
|
|
24009
|
-
*
|
|
24010
|
-
*
|
|
24011
|
-
*
|
|
24012
|
-
*
|
|
24276
|
+
* Render a URL-builder method. URL-builder ops (e.g. `GET /sso/authorize`,
|
|
24277
|
+
* `GET /sso/logout`) issue no HTTP request — they format a redirect URL the
|
|
24278
|
+
* application sends the user to. Generated methods return `Result<String,
|
|
24279
|
+
* Error>` because percent-encoding the query string can still fail.
|
|
24280
|
+
*/
|
|
24281
|
+
function renderUrlBuilderMethod(op, resolved, paramsType, method, emptyParams) {
|
|
24282
|
+
const segments = parsePathTemplate(op.path);
|
|
24283
|
+
const pathArgList = op.pathParams.map((p) => `${methodName(p.name)}: &str`);
|
|
24284
|
+
op.pathParams.map((p) => methodName(p.name));
|
|
24285
|
+
const sig = [];
|
|
24286
|
+
for (const line of methodDocLines(op)) sig.push(` ${line}`);
|
|
24287
|
+
if (op.deprecated) sig.push(" #[deprecated]");
|
|
24288
|
+
const args = [
|
|
24289
|
+
"&self",
|
|
24290
|
+
...pathArgList,
|
|
24291
|
+
...emptyParams ? [] : [`params: ${paramsType}`]
|
|
24292
|
+
];
|
|
24293
|
+
const headSig = ` pub fn ${method}(${args.join(", ")}) -> Result<String, Error> {`;
|
|
24294
|
+
if (headSig.length <= 100) sig.push(headSig);
|
|
24295
|
+
else {
|
|
24296
|
+
sig.push(` pub fn ${method}(`);
|
|
24297
|
+
for (const arg of args) sig.push(` ${arg},`);
|
|
24298
|
+
sig.push(" ) -> Result<String, Error> {");
|
|
24299
|
+
}
|
|
24300
|
+
const pathFormat = segments.map((s) => s.kind === "literal" ? s.value : `{${methodName(s.name)}}`).join("");
|
|
24301
|
+
if (segments.some((s) => s.kind === "param")) {
|
|
24302
|
+
for (const p of op.pathParams) {
|
|
24303
|
+
const n = methodName(p.name);
|
|
24304
|
+
sig.push(` let ${n} = crate::client::path_segment(${n});`);
|
|
24305
|
+
}
|
|
24306
|
+
sig.push(` let path = format!(${JSON.stringify(pathFormat)});`);
|
|
24307
|
+
} else sig.push(` let path = ${JSON.stringify(pathFormat)}.to_string();`);
|
|
24308
|
+
const defaults = resolved.defaults ?? {};
|
|
24309
|
+
const inferred = resolved.inferFromClient ?? [];
|
|
24310
|
+
if (Object.keys(defaults).length > 0 || inferred.length > 0) {
|
|
24311
|
+
sig.push(" let mut overlay = serde_json::Map::new();");
|
|
24312
|
+
for (const [k, v] of Object.entries(defaults)) sig.push(` overlay.insert(${JSON.stringify(k)}.to_string(), serde_json::json!(${JSON.stringify(v)}));`);
|
|
24313
|
+
for (const k of inferred) sig.push(` overlay.insert(${JSON.stringify(k)}.to_string(), serde_json::Value::String(${clientFieldExpression(k)}.to_string()));`);
|
|
24314
|
+
if (!emptyParams) {
|
|
24315
|
+
sig.push(" let params_value = serde_json::to_value(¶ms)");
|
|
24316
|
+
sig.push(" .map_err(|e| Error::Builder(format!(\"query encode failed: {e}\")))?;");
|
|
24317
|
+
sig.push(" if let serde_json::Value::Object(map) = params_value {");
|
|
24318
|
+
sig.push(" for (k, v) in map { overlay.insert(k, v); }");
|
|
24319
|
+
sig.push(" }");
|
|
24320
|
+
}
|
|
24321
|
+
sig.push(" let merged = serde_json::Value::Object(overlay);");
|
|
24322
|
+
sig.push(" let qs = crate::query::encode_query(&merged)?;");
|
|
24323
|
+
} else if (!emptyParams) sig.push(" let qs = crate::query::encode_query(¶ms)?;");
|
|
24324
|
+
else sig.push(" let qs = String::new();");
|
|
24325
|
+
sig.push(" let url = if qs.is_empty() {");
|
|
24326
|
+
sig.push(" format!(\"{}{}\", self.client.base_url(), path)");
|
|
24327
|
+
sig.push(" } else {");
|
|
24328
|
+
sig.push(" format!(\"{}{}?{}\", self.client.base_url(), path, qs)");
|
|
24329
|
+
sig.push(" };");
|
|
24330
|
+
sig.push(" Ok(url)");
|
|
24331
|
+
sig.push(" }");
|
|
24332
|
+
return sig.join("\n");
|
|
24333
|
+
}
|
|
24334
|
+
/**
|
|
24335
|
+
* Generate a `<method>_auto_paging` helper from `op.pagination`. Returns null
|
|
24336
|
+
* when the operation isn't paginated, when the strategy isn't `cursor`, or
|
|
24337
|
+
* when the response model lacks the expected `data` / pagination-cursor
|
|
24338
|
+
* fields (defensive — the IR shouldn't produce that combination today).
|
|
24013
24339
|
*/
|
|
24014
|
-
function renderAutoPagingMethod(op,
|
|
24015
|
-
if (!op.
|
|
24016
|
-
|
|
24017
|
-
|
|
24340
|
+
function renderAutoPagingMethod(op, resolved, paramsType, method, ctx) {
|
|
24341
|
+
if (!op.pagination) return null;
|
|
24342
|
+
if (op.pagination.strategy !== "cursor") return null;
|
|
24343
|
+
if (resolved.urlBuilder) return null;
|
|
24344
|
+
if (op.response.kind !== "model") return null;
|
|
24345
|
+
const responseModel = ctx.spec.models.find((m) => m.name === op.response.name);
|
|
24018
24346
|
if (!responseModel) return null;
|
|
24019
|
-
const
|
|
24020
|
-
const
|
|
24021
|
-
|
|
24022
|
-
if (dataField.type.kind !== "array") return null;
|
|
24347
|
+
const cursorParam = op.pagination.param;
|
|
24348
|
+
const dataPath = op.pagination.dataPath ?? "data";
|
|
24349
|
+
const dataField = responseModel.fields.find((f) => f.name === dataPath);
|
|
24350
|
+
if (!dataField || dataField.type.kind !== "array") return null;
|
|
24351
|
+
const listMetadataField = responseModel.fields.find((f) => f.name === "list_metadata");
|
|
24352
|
+
if (!listMetadataField || listMetadataField.type.kind !== "model") return null;
|
|
24353
|
+
const metadataModel = ctx.spec.models.find((m) => m.name === listMetadataField.type.name);
|
|
24354
|
+
if (!metadataModel) return null;
|
|
24355
|
+
if (!metadataModel.fields.some((f) => f.name === cursorParam)) return null;
|
|
24023
24356
|
const itemType = mapTypeRef$1(dataField.type.items);
|
|
24024
|
-
|
|
24357
|
+
const cursorField = fieldName(cursorParam);
|
|
24358
|
+
const dataAccessor = fieldName(dataPath);
|
|
24025
24359
|
const pathArgList = op.pathParams.map((p) => `${methodName(p.name)}: impl Into<String>`);
|
|
24026
24360
|
const pathArgNames = op.pathParams.map((p) => methodName(p.name));
|
|
24027
24361
|
const sig = [];
|
|
24028
24362
|
sig.push("");
|
|
24029
24363
|
sig.push(` /// Returns an async [\`futures_util::Stream\`] that yields every \`${itemType}\``);
|
|
24030
|
-
sig.push(` /// across all pages, advancing the \`
|
|
24364
|
+
sig.push(` /// across all pages, advancing the \`${cursorParam}\` cursor under the hood.`);
|
|
24031
24365
|
sig.push(" ///");
|
|
24032
24366
|
sig.push(" /// ```ignore");
|
|
24033
24367
|
sig.push(" /// use futures_util::TryStreamExt;");
|
|
@@ -24053,17 +24387,17 @@ function renderAutoPagingMethod(op, _resolved, paramsType, method, ctx) {
|
|
|
24053
24387
|
sig.push(" crate::pagination::auto_paginate_pages(move |after| {");
|
|
24054
24388
|
for (const n of pathArgNames) sig.push(` let ${n} = ${n}.clone();`);
|
|
24055
24389
|
sig.push(" let mut params = params.clone();");
|
|
24056
|
-
sig.push(
|
|
24390
|
+
sig.push(` params.${cursorField} = after;`);
|
|
24057
24391
|
sig.push(" async move {");
|
|
24058
24392
|
const callArgs = [...pathArgNames.map((n) => `&${n}`), "params"].join(", ");
|
|
24059
24393
|
sig.push(` let page = self.${method}(${callArgs}).await?;`);
|
|
24060
|
-
sig.push(
|
|
24394
|
+
sig.push(` Ok((page.${dataAccessor}, page.list_metadata.${cursorField}))`);
|
|
24061
24395
|
sig.push(" }");
|
|
24062
24396
|
sig.push(" })");
|
|
24063
24397
|
sig.push(" }");
|
|
24064
24398
|
return sig.join("\n");
|
|
24065
24399
|
}
|
|
24066
|
-
function renderWrapperParamsStruct(name, _op, _wrapper, params, registry) {
|
|
24400
|
+
function renderWrapperParamsStruct(name, _op, _wrapper, params, registry, ctx) {
|
|
24067
24401
|
const fields = [];
|
|
24068
24402
|
const fieldLines = [];
|
|
24069
24403
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -24081,32 +24415,52 @@ function renderWrapperParamsStruct(name, _op, _wrapper, params, registry) {
|
|
|
24081
24415
|
rust = applySecretRedaction(rust, rp.paramName);
|
|
24082
24416
|
const desc = rp.field?.description?.trim();
|
|
24083
24417
|
if (desc) for (const c of paramDocComment(desc)) fieldLines.push(` ${c}`);
|
|
24418
|
+
const fieldDefault = rp.field?.default;
|
|
24419
|
+
if (fieldDefault != null) {
|
|
24420
|
+
if (desc) fieldLines.push(" ///");
|
|
24421
|
+
fieldLines.push(` /// Defaults to \`${formatDefault(fieldDefault)}\`.`);
|
|
24422
|
+
}
|
|
24084
24423
|
const required = !rp.isOptional && !rust.startsWith("Option<");
|
|
24085
24424
|
if (required) {
|
|
24086
|
-
if (desc) fieldLines.push(" ///");
|
|
24425
|
+
if (desc || fieldDefault != null) fieldLines.push(" ///");
|
|
24087
24426
|
fieldLines.push(" /// Required.");
|
|
24088
24427
|
}
|
|
24089
24428
|
if (rust.startsWith("Option<")) fieldLines.push(" #[serde(skip_serializing_if = \"Option::is_none\")]");
|
|
24090
24429
|
if (fname !== rp.paramName) fieldLines.push(` #[serde(rename = ${JSON.stringify(rp.paramName)})]`);
|
|
24091
24430
|
fieldLines.push(` pub ${fname}: ${rust},`);
|
|
24431
|
+
const defaultExpr = fieldDefault != null && rp.field ? rustDefaultExpr(fieldDefault, rp.field.type, rust.startsWith("Option<"), ctx) : null;
|
|
24092
24432
|
fields.push({
|
|
24093
24433
|
fname,
|
|
24094
24434
|
rust,
|
|
24095
|
-
required
|
|
24435
|
+
required,
|
|
24436
|
+
defaultExpr
|
|
24096
24437
|
});
|
|
24097
24438
|
}
|
|
24098
24439
|
const requiredFields = fields.filter((f) => f.required);
|
|
24099
|
-
const
|
|
24440
|
+
const allOptional = fields.length === 0 || requiredFields.length === 0;
|
|
24441
|
+
const hasSpecDefault = fields.some((f) => f.defaultExpr !== null);
|
|
24442
|
+
const derives = allOptional && !hasSpecDefault ? "Debug, Clone, Default, Serialize" : "Debug, Clone, Serialize";
|
|
24100
24443
|
const out = [];
|
|
24101
24444
|
if (fieldLines.length === 0) out.push(`#[derive(${derives})]`, `pub struct ${name} {}`);
|
|
24102
24445
|
else out.push(`#[derive(${derives})]`, `pub struct ${name} {`, ...fieldLines, "}");
|
|
24446
|
+
if (allOptional && hasSpecDefault) {
|
|
24447
|
+
const defaultInitLines = fields.map((f) => ` ${f.fname}: ${f.defaultExpr ?? "Default::default()"},`);
|
|
24448
|
+
out.push("");
|
|
24449
|
+
out.push(`impl Default for ${name} {`);
|
|
24450
|
+
out.push(" fn default() -> Self {");
|
|
24451
|
+
out.push(" Self {");
|
|
24452
|
+
out.push(...defaultInitLines);
|
|
24453
|
+
out.push(" }");
|
|
24454
|
+
out.push(" }");
|
|
24455
|
+
out.push("}");
|
|
24456
|
+
}
|
|
24103
24457
|
if (requiredFields.length > 0) {
|
|
24104
24458
|
const ctorArgs = requiredFields.map((f) => `${f.fname}: ${ctorParamType(f.rust)}`).join(", ");
|
|
24105
24459
|
const initLines = [];
|
|
24106
24460
|
for (const f of fields) if (f.required) {
|
|
24107
24461
|
const value = ctorParamConvert(f.rust, f.fname);
|
|
24108
24462
|
initLines.push(value === f.fname ? ` ${f.fname},` : ` ${f.fname}: ${value},`);
|
|
24109
|
-
} else initLines.push(` ${f.fname}: Default::default(),`);
|
|
24463
|
+
} else initLines.push(` ${f.fname}: ${f.defaultExpr ?? "Default::default()"},`);
|
|
24110
24464
|
out.push("");
|
|
24111
24465
|
out.push(`impl ${name} {`);
|
|
24112
24466
|
out.push(` /// Construct a new \`${name}\` with the required fields set.`);
|
|
@@ -24207,6 +24561,47 @@ function clientFieldExpression(field) {
|
|
|
24207
24561
|
function paramDocComment(text) {
|
|
24208
24562
|
return text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).map((l) => `/// ${l}`);
|
|
24209
24563
|
}
|
|
24564
|
+
/**
|
|
24565
|
+
* Render a spec-level default value for inclusion in a doc comment. Strings
|
|
24566
|
+
* render bare (e.g. `desc`) so they nest naturally inside the surrounding
|
|
24567
|
+
* backticks; numbers/booleans use JSON encoding.
|
|
24568
|
+
*/
|
|
24569
|
+
function formatDefault(value) {
|
|
24570
|
+
if (typeof value === "string") return value;
|
|
24571
|
+
return JSON.stringify(value);
|
|
24572
|
+
}
|
|
24573
|
+
/**
|
|
24574
|
+
* Render a spec-level default value as a Rust expression suitable for an
|
|
24575
|
+
* `impl Default` body or a `new(…)` initialiser. When `isOptional` is true the
|
|
24576
|
+
* result is wrapped in `Some(…)` so it matches an `Option<T>` field.
|
|
24577
|
+
*
|
|
24578
|
+
* Returns `null` for types/values the emitter doesn't know how to materialise
|
|
24579
|
+
* — caller falls back to `Default::default()`.
|
|
24580
|
+
*/
|
|
24581
|
+
function rustDefaultExpr(value, ref, isOptional, ctx) {
|
|
24582
|
+
if (ref.kind === "nullable") return rustDefaultExpr(value, ref.inner, isOptional, ctx);
|
|
24583
|
+
let expr = null;
|
|
24584
|
+
switch (ref.kind) {
|
|
24585
|
+
case "primitive":
|
|
24586
|
+
if (ref.type === "integer" && typeof value === "number" && Number.isFinite(value)) expr = String(Math.trunc(value));
|
|
24587
|
+
else if (ref.type === "number" && typeof value === "number" && Number.isFinite(value)) expr = Number.isInteger(value) ? `${value}.0` : String(value);
|
|
24588
|
+
else if (ref.type === "boolean" && typeof value === "boolean") expr = String(value);
|
|
24589
|
+
else if (ref.type === "string" && typeof value === "string") expr = `${JSON.stringify(value)}.to_string()`;
|
|
24590
|
+
break;
|
|
24591
|
+
case "enum": {
|
|
24592
|
+
if (typeof value !== "string" && typeof value !== "number") break;
|
|
24593
|
+
const enumDef = ctx.spec.enums.find((e) => e.name === ref.name);
|
|
24594
|
+
if (!enumDef) break;
|
|
24595
|
+
const ev = enumDef.values.find((v) => v.value === value);
|
|
24596
|
+
if (!ev) break;
|
|
24597
|
+
expr = `${typeName(ref.name)}::${variantName(ev.value)}`;
|
|
24598
|
+
break;
|
|
24599
|
+
}
|
|
24600
|
+
default: break;
|
|
24601
|
+
}
|
|
24602
|
+
if (expr === null) return null;
|
|
24603
|
+
return isOptional ? `Some(${expr})` : expr;
|
|
24604
|
+
}
|
|
24210
24605
|
function methodDocLines(op) {
|
|
24211
24606
|
const lines = [];
|
|
24212
24607
|
if (op.description && op.description.trim().length > 0) for (const raw of op.description.split("\n")) {
|
|
@@ -24240,16 +24635,19 @@ function isBodyRequired(op) {
|
|
|
24240
24635
|
}
|
|
24241
24636
|
/**
|
|
24242
24637
|
* `true` when the resolved operation contributes nothing to a params struct:
|
|
24243
|
-
* no request body, and every exposed query/header parameter is
|
|
24244
|
-
* the client or supplied as a default. Such methods take no
|
|
24245
|
-
* the public API and skip the empty struct entirely.
|
|
24638
|
+
* no request body, and every exposed query/header/cookie parameter is
|
|
24639
|
+
* inferred from the client or supplied as a default. Such methods take no
|
|
24640
|
+
* `params:` arg in the public API and skip the empty struct entirely.
|
|
24246
24641
|
*/
|
|
24247
24642
|
function isEmptyParams(op, resolved) {
|
|
24248
24643
|
if (op.requestBody) return false;
|
|
24249
24644
|
const hidden = new Set([...Object.keys(resolved.defaults ?? {}), ...resolved.inferFromClient ?? []]);
|
|
24250
|
-
const
|
|
24645
|
+
const grouped = /* @__PURE__ */ new Set();
|
|
24646
|
+
for (const g of op.parameterGroups ?? []) for (const v of g.variants) for (const p of v.parameters) grouped.add(p.name);
|
|
24647
|
+
const visibleQuery = op.queryParams.filter((p) => !hidden.has(p.name) && !grouped.has(p.name));
|
|
24251
24648
|
const visibleHeader = op.headerParams.filter((p) => !hidden.has(p.name));
|
|
24252
|
-
|
|
24649
|
+
const visibleCookie = (op.cookieParams ?? []).filter((p) => !hidden.has(p.name));
|
|
24650
|
+
return visibleQuery.length === 0 && visibleHeader.length === 0 && visibleCookie.length === 0 && (op.parameterGroups?.length ?? 0) === 0;
|
|
24253
24651
|
}
|
|
24254
24652
|
function renderResourcesBarrel(exports) {
|
|
24255
24653
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -24356,7 +24754,8 @@ function generateModelFixture(model, modelMap, enumMap, visiting) {
|
|
|
24356
24754
|
visiting.add(model.name);
|
|
24357
24755
|
for (const field of model.fields) {
|
|
24358
24756
|
if (!field.required) continue;
|
|
24359
|
-
|
|
24757
|
+
const fromExample = exampleFromSpec(field.example, field.type, enumMap);
|
|
24758
|
+
result[field.name] = fromExample !== void 0 ? fromExample : exampleFor(field.type, modelMap, enumMap, visiting, field.name);
|
|
24360
24759
|
}
|
|
24361
24760
|
visiting.delete(model.name);
|
|
24362
24761
|
return result;
|
|
@@ -24398,13 +24797,80 @@ function exampleFor(type, modelMap, enumMap, visiting, fieldName) {
|
|
|
24398
24797
|
}
|
|
24399
24798
|
}
|
|
24400
24799
|
}
|
|
24800
|
+
/**
|
|
24801
|
+
* Resolve a spec-provided `example` against a TypeRef and return the value to
|
|
24802
|
+
* embed in the fixture, or `undefined` when the example cannot be used safely.
|
|
24803
|
+
*
|
|
24804
|
+
* "Safely" means the value would round-trip through serde to the generated
|
|
24805
|
+
* Rust type. We deliberately only accept primitives, enum string/number
|
|
24806
|
+
* values, and homogenous arrays of those; nested object examples (which the
|
|
24807
|
+
* spec sometimes supplies as illustrative metadata blobs) are skipped because
|
|
24808
|
+
* they rarely match the strict struct shape Rust expects.
|
|
24809
|
+
*/
|
|
24810
|
+
function exampleFromSpec(example, type, enumMap) {
|
|
24811
|
+
if (example === void 0) return void 0;
|
|
24812
|
+
if (example === null) return void 0;
|
|
24813
|
+
return matchExampleToType(example, type, enumMap);
|
|
24814
|
+
}
|
|
24815
|
+
function matchExampleToType(value, type, enumMap) {
|
|
24816
|
+
switch (type.kind) {
|
|
24817
|
+
case "primitive": return matchPrimitive(value, type.type);
|
|
24818
|
+
case "literal": return value === type.value ? value : void 0;
|
|
24819
|
+
case "enum": {
|
|
24820
|
+
const e = enumMap.get(type.name);
|
|
24821
|
+
if (!e) return void 0;
|
|
24822
|
+
return e.values.some((v) => v.value === value) ? value : void 0;
|
|
24823
|
+
}
|
|
24824
|
+
case "array": {
|
|
24825
|
+
if (!Array.isArray(value)) return void 0;
|
|
24826
|
+
const out = [];
|
|
24827
|
+
for (const item of value) {
|
|
24828
|
+
const matched = matchExampleToType(item, type.items, enumMap);
|
|
24829
|
+
if (matched === void 0) return void 0;
|
|
24830
|
+
out.push(matched);
|
|
24831
|
+
}
|
|
24832
|
+
if (out.length === 0) return void 0;
|
|
24833
|
+
return out;
|
|
24834
|
+
}
|
|
24835
|
+
case "nullable": return matchExampleToType(value, type.inner, enumMap);
|
|
24836
|
+
case "map":
|
|
24837
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
24838
|
+
return value;
|
|
24839
|
+
case "union":
|
|
24840
|
+
for (const variant of type.variants) {
|
|
24841
|
+
const matched = matchExampleToType(value, variant, enumMap);
|
|
24842
|
+
if (matched !== void 0) return matched;
|
|
24843
|
+
}
|
|
24844
|
+
return;
|
|
24845
|
+
case "model": return;
|
|
24846
|
+
}
|
|
24847
|
+
}
|
|
24848
|
+
function matchPrimitive(value, primitive) {
|
|
24849
|
+
switch (primitive) {
|
|
24850
|
+
case "string": return typeof value === "string" ? value : void 0;
|
|
24851
|
+
case "integer": return typeof value === "number" && Number.isInteger(value) ? value : void 0;
|
|
24852
|
+
case "number": return typeof value === "number" ? value : void 0;
|
|
24853
|
+
case "boolean": return typeof value === "boolean" ? value : void 0;
|
|
24854
|
+
case "unknown": return value;
|
|
24855
|
+
}
|
|
24856
|
+
}
|
|
24401
24857
|
//#endregion
|
|
24402
24858
|
//#region src/rust/tests.ts
|
|
24403
24859
|
/**
|
|
24404
24860
|
* Generate integration tests under `tests/`. Each mount group gets one
|
|
24405
|
-
* `tests/{mount}_test.rs` file.
|
|
24406
|
-
*
|
|
24407
|
-
*
|
|
24861
|
+
* `tests/{mount}_test.rs` file. Per operation the generator emits:
|
|
24862
|
+
*
|
|
24863
|
+
* - `_round_trip`: happy-path 200 mock + call.
|
|
24864
|
+
* - `_unauthorized`, `_not_found`, `_rate_limited`, `_server_error`: error
|
|
24865
|
+
* paths for every HTTP-calling op.
|
|
24866
|
+
* - `_bad_request`, `_unprocessable`: additional 4xx error paths for write
|
|
24867
|
+
* ops (POST/PUT/PATCH/DELETE).
|
|
24868
|
+
* - `_empty_page`: empty `data: []` response for paginated ops.
|
|
24869
|
+
* - `_encodes_query_params`: outbound query-string assertion for ops with
|
|
24870
|
+
* array query params (`explode: true` repeated keys, `explode: false`
|
|
24871
|
+
* comma-joined).
|
|
24872
|
+
*
|
|
24873
|
+
* URL-builder ops (no HTTP call) only get the round-trip test.
|
|
24408
24874
|
*/
|
|
24409
24875
|
function generateTests(spec, ctx) {
|
|
24410
24876
|
const files = [];
|
|
@@ -24456,6 +24922,7 @@ function renderMountTest(mountName, resolvedOps, ctx, modelMap, enumMap) {
|
|
|
24456
24922
|
lines.push("");
|
|
24457
24923
|
lines.push("use wiremock::matchers::{method, path as path_matcher};");
|
|
24458
24924
|
lines.push("use wiremock::{Mock, MockServer, ResponseTemplate};");
|
|
24925
|
+
lines.push(`use ${crate}::Error;`);
|
|
24459
24926
|
lines.push("");
|
|
24460
24927
|
const seen = /* @__PURE__ */ new Set();
|
|
24461
24928
|
for (const r of resolvedOps) {
|
|
@@ -24485,43 +24952,316 @@ function renderRegularTest(op, resolved, accessor, crate, modelMap, enumMap) {
|
|
|
24485
24952
|
const literalPath = op.path.replace(/\{[^}]+\}/g, "test_id");
|
|
24486
24953
|
const httpMethod = op.httpMethod.toUpperCase();
|
|
24487
24954
|
const responseExpr = responseBodyExpr(op.response, modelMap, enumMap);
|
|
24955
|
+
const isUrlBuilder = resolved.urlBuilder === true;
|
|
24956
|
+
const callArgs = buildCallArgs(op, resolved, crate, accessor, modelMap, enumMap).join(", ");
|
|
24957
|
+
const shape = {
|
|
24958
|
+
methodIdent: m,
|
|
24959
|
+
literalPath,
|
|
24960
|
+
httpMethod,
|
|
24961
|
+
callArgs,
|
|
24962
|
+
isUrlBuilder,
|
|
24963
|
+
isWrite: isWriteMethod(op.httpMethod),
|
|
24964
|
+
isPaginated: !!op.pagination,
|
|
24965
|
+
hasBodyGroup: !!op.requestBody && hasBodyParameterGroup(op)
|
|
24966
|
+
};
|
|
24967
|
+
const lines = [];
|
|
24968
|
+
lines.push("#[tokio::test]");
|
|
24969
|
+
lines.push(`async fn ${accessor}_${m}_round_trip() {`);
|
|
24970
|
+
if (isUrlBuilder) {
|
|
24971
|
+
lines.push(" let server = MockServer::start().await;");
|
|
24972
|
+
lines.push(" let client = common::test_client(&server).await;");
|
|
24973
|
+
lines.push(` let _ = client.${accessor}().${m}(${callArgs});`);
|
|
24974
|
+
} else {
|
|
24975
|
+
lines.push(" let server = MockServer::start().await;");
|
|
24976
|
+
lines.push(` Mock::given(method(${JSON.stringify(httpMethod)}))`);
|
|
24977
|
+
lines.push(` .and(path_matcher(${JSON.stringify(literalPath)}))`);
|
|
24978
|
+
lines.push(` .respond_with(ResponseTemplate::new(200).set_body_string(${responseExpr}))`);
|
|
24979
|
+
lines.push(" .expect(1)");
|
|
24980
|
+
lines.push(" .mount(&server)");
|
|
24981
|
+
lines.push(" .await;");
|
|
24982
|
+
lines.push(" let client = common::test_client(&server).await;");
|
|
24983
|
+
lines.push(` let _ = client.${accessor}().${m}(${callArgs}).await;`);
|
|
24984
|
+
}
|
|
24985
|
+
lines.push("}");
|
|
24986
|
+
if (isUrlBuilder) return lines;
|
|
24987
|
+
for (const errTest of standardErrorTests(shape, accessor)) {
|
|
24988
|
+
lines.push("");
|
|
24989
|
+
lines.push(...errTest);
|
|
24990
|
+
}
|
|
24991
|
+
if (shape.isWrite) for (const errTest of writeErrorTests(shape, accessor)) {
|
|
24992
|
+
lines.push("");
|
|
24993
|
+
lines.push(...errTest);
|
|
24994
|
+
}
|
|
24995
|
+
if (shape.isPaginated) {
|
|
24996
|
+
const emptyTest = emptyPageTest(op, shape, accessor);
|
|
24997
|
+
if (emptyTest) {
|
|
24998
|
+
lines.push("");
|
|
24999
|
+
lines.push(...emptyTest);
|
|
25000
|
+
}
|
|
25001
|
+
}
|
|
25002
|
+
const encodingTest = encodesQueryParamsTest(op, resolved, accessor, crate, modelMap, enumMap);
|
|
25003
|
+
if (encodingTest) {
|
|
25004
|
+
lines.push("");
|
|
25005
|
+
lines.push(...encodingTest);
|
|
25006
|
+
}
|
|
25007
|
+
return lines;
|
|
25008
|
+
}
|
|
25009
|
+
/**
|
|
25010
|
+
* Build the positional args for the resource method call: path-param literals,
|
|
25011
|
+
* then a single params struct (when the op has any), then a token-string for
|
|
25012
|
+
* non-bearer security overrides. This factors the constructor logic out of
|
|
25013
|
+
* the round-trip renderer so the error/encoding tests can reuse it verbatim
|
|
25014
|
+
* — every test category sends the same request, only the mocked response and
|
|
25015
|
+
* assertion differ.
|
|
25016
|
+
*/
|
|
25017
|
+
function buildCallArgs(op, resolved, crate, accessor, modelMap, enumMap) {
|
|
24488
25018
|
const hidden = new Set([...Object.keys(resolved.defaults ?? {}), ...resolved.inferFromClient ?? []]);
|
|
24489
|
-
const
|
|
25019
|
+
const groupedNames = /* @__PURE__ */ new Set();
|
|
25020
|
+
for (const g of op.parameterGroups ?? []) for (const v of g.variants) for (const p of v.parameters) groupedNames.add(p.name);
|
|
25021
|
+
const visibleQuery = op.queryParams.filter((p) => !hidden.has(p.name) && !groupedNames.has(p.name));
|
|
24490
25022
|
const visibleHeader = op.headerParams.filter((p) => !hidden.has(p.name));
|
|
24491
25023
|
const visibleParams = [...visibleQuery, ...visibleHeader];
|
|
24492
25024
|
const requiredParams = visibleParams.filter((p) => p.required);
|
|
24493
25025
|
const hasBody = op.requestBody !== void 0;
|
|
24494
25026
|
const bodyRequired = hasBody && op.requestBody.kind !== "nullable";
|
|
24495
|
-
const
|
|
25027
|
+
const queryNames = new Set(op.queryParams.map((p) => p.name));
|
|
25028
|
+
const requiredQueryGroups = (op.parameterGroups ?? []).filter((g) => !g.optional).filter((g) => g.variants.every((v) => v.parameters.every((p) => queryNames.has(p.name))));
|
|
25029
|
+
const emptyParams = !hasBody && visibleParams.length === 0 && (op.parameterGroups?.length ?? 0) === 0;
|
|
24496
25030
|
const callArgs = [];
|
|
24497
25031
|
for (const _ of op.pathParams) callArgs.push("\"test_id\"");
|
|
24498
25032
|
if (!emptyParams) {
|
|
24499
25033
|
const paramsType = `${crate}::${accessor}::${typeName(resolved.methodName)}Params`;
|
|
24500
|
-
if (requiredParams.length === 0 && !bodyRequired) callArgs.push(`${paramsType}::default()`);
|
|
25034
|
+
if (requiredParams.length === 0 && !bodyRequired && requiredQueryGroups.length === 0) callArgs.push(`${paramsType}::default()`);
|
|
24501
25035
|
else {
|
|
24502
25036
|
const ctorArgs = [];
|
|
24503
25037
|
for (const p of requiredParams) ctorArgs.push(stubExpr(p.type, p.name, modelMap, enumMap));
|
|
24504
|
-
|
|
24505
|
-
|
|
24506
|
-
|
|
25038
|
+
for (const g of requiredQueryGroups) ctorArgs.push(parameterGroupStubExpr(g, crate, accessor));
|
|
25039
|
+
if (hasBody && bodyRequired) if (hasBodyParameterGroup(op)) ctorArgs.push(syntheticBodyStubExpr(op, resolved, crate, accessor, modelMap));
|
|
25040
|
+
else ctorArgs.push(stubExpr(op.requestBody, "body", modelMap, enumMap));
|
|
24507
25041
|
callArgs.push(`${paramsType}::new(${ctorArgs.join(", ")})`);
|
|
24508
25042
|
}
|
|
24509
25043
|
}
|
|
25044
|
+
const tokenName = bearerOverrideTokenName(op);
|
|
25045
|
+
if (tokenName) callArgs.push(`"stub_${tokenName}".to_string()`);
|
|
25046
|
+
return callArgs;
|
|
25047
|
+
}
|
|
25048
|
+
/**
|
|
25049
|
+
* Build the four common-error test bodies (401/404/429/500). Each one mocks a
|
|
25050
|
+
* single response with the given status, calls the SDK method (which should
|
|
25051
|
+
* fail), and asserts on the unwrapped `Error::Api` payload.
|
|
25052
|
+
*/
|
|
25053
|
+
function standardErrorTests(shape, accessor) {
|
|
25054
|
+
const tests = [];
|
|
25055
|
+
tests.push(errorTestBody(shape, accessor, "unauthorized", 401, "{\"message\":\"Unauthorized\"}", void 0));
|
|
25056
|
+
tests.push(errorTestBody(shape, accessor, "not_found", 404, "{\"message\":\"Not found\"}", void 0));
|
|
25057
|
+
tests.push(errorTestBody(shape, accessor, "rate_limited", 429, "{\"message\":\"Slow down\"}", {
|
|
25058
|
+
retryAfterSeconds: 1,
|
|
25059
|
+
assertRetryAfter: true
|
|
25060
|
+
}));
|
|
25061
|
+
tests.push(errorTestBody(shape, accessor, "server_error", 500, "{\"message\":\"Internal error\"}", void 0));
|
|
25062
|
+
return tests;
|
|
25063
|
+
}
|
|
25064
|
+
/** Build the two write-op-only error tests (400/422). */
|
|
25065
|
+
function writeErrorTests(shape, accessor) {
|
|
25066
|
+
return [errorTestBody(shape, accessor, "bad_request", 400, "{\"code\":\"validation_error\",\"message\":\"Bad request\"}", { assertCode: "validation_error" }), errorTestBody(shape, accessor, "unprocessable", 422, "{\"message\":\"Unprocessable\"}", void 0)];
|
|
25067
|
+
}
|
|
25068
|
+
function errorTestBody(shape, accessor, category, status, body, opts) {
|
|
24510
25069
|
const lines = [];
|
|
24511
25070
|
lines.push("#[tokio::test]");
|
|
24512
|
-
lines.push(`async fn ${accessor}_${
|
|
25071
|
+
lines.push(`async fn ${accessor}_${shape.methodIdent}_${category}() {`);
|
|
24513
25072
|
lines.push(" let server = MockServer::start().await;");
|
|
24514
|
-
lines.push(`
|
|
24515
|
-
lines.push(` .
|
|
24516
|
-
lines.push(` .
|
|
25073
|
+
lines.push(` let template = ResponseTemplate::new(${status})`);
|
|
25074
|
+
if (opts?.retryAfterSeconds !== void 0) lines.push(` .insert_header("retry-after", ${JSON.stringify(String(opts.retryAfterSeconds))})`);
|
|
25075
|
+
lines.push(` .set_body_string(${JSON.stringify(body)});`);
|
|
25076
|
+
lines.push(` Mock::given(method(${JSON.stringify(shape.httpMethod)}))`);
|
|
25077
|
+
lines.push(` .and(path_matcher(${JSON.stringify(shape.literalPath)}))`);
|
|
25078
|
+
lines.push(" .respond_with(template)");
|
|
24517
25079
|
lines.push(" .expect(1)");
|
|
24518
25080
|
lines.push(" .mount(&server)");
|
|
24519
25081
|
lines.push(" .await;");
|
|
24520
25082
|
lines.push(" let client = common::test_client(&server).await;");
|
|
24521
|
-
lines.push(` let
|
|
25083
|
+
lines.push(` let err = client.${accessor}().${shape.methodIdent}(${shape.callArgs}).await.expect_err("expected error");`);
|
|
25084
|
+
lines.push(" let api = match &err {");
|
|
25085
|
+
lines.push(" Error::Api(api) => api.as_ref(),");
|
|
25086
|
+
lines.push(" other => panic!(\"expected Error::Api, got {other:?}\"),");
|
|
25087
|
+
lines.push(" };");
|
|
25088
|
+
lines.push(` assert_eq!(api.status, ${status});`);
|
|
25089
|
+
if (opts?.assertRetryAfter) lines.push(` assert_eq!(api.retry_after, Some(std::time::Duration::from_secs(${opts.retryAfterSeconds ?? 0})));`);
|
|
25090
|
+
if (opts?.assertCode) lines.push(` assert_eq!(api.code.as_deref(), Some(${JSON.stringify(opts.assertCode)}));`);
|
|
24522
25091
|
lines.push("}");
|
|
24523
25092
|
return lines;
|
|
24524
25093
|
}
|
|
25094
|
+
/**
|
|
25095
|
+
* Return an `_empty_page` test for a paginated list op. Two shapes are
|
|
25096
|
+
* supported:
|
|
25097
|
+
*
|
|
25098
|
+
* - Wrapper model: `{"data": [], "list_metadata": {...}}`, accessed via
|
|
25099
|
+
* `resp.data` on the returned struct.
|
|
25100
|
+
* - Bare array: `[]`, accessed via `resp.is_empty()` directly (the SDK
|
|
25101
|
+
* returns `Vec<T>` for paginated ops without a wrapper model).
|
|
25102
|
+
*
|
|
25103
|
+
* Returns null when the response shape isn't recognised (e.g. a primitive
|
|
25104
|
+
* or unknown shape we can't safely assert against).
|
|
25105
|
+
*/
|
|
25106
|
+
function emptyPageTest(op, shape, accessor) {
|
|
25107
|
+
const responseKind = op.response.kind;
|
|
25108
|
+
let body;
|
|
25109
|
+
let dataAccessor;
|
|
25110
|
+
if (responseKind === "array") {
|
|
25111
|
+
body = "[]";
|
|
25112
|
+
dataAccessor = "resp";
|
|
25113
|
+
} else if (responseKind === "model") {
|
|
25114
|
+
body = "{\"object\":\"list\",\"data\":[],\"list_metadata\":{\"before\":null,\"after\":null}}";
|
|
25115
|
+
dataAccessor = "resp.data";
|
|
25116
|
+
} else return null;
|
|
25117
|
+
const lines = [];
|
|
25118
|
+
lines.push("#[tokio::test]");
|
|
25119
|
+
lines.push(`async fn ${accessor}_${shape.methodIdent}_empty_page() {`);
|
|
25120
|
+
lines.push(" let server = MockServer::start().await;");
|
|
25121
|
+
lines.push(` Mock::given(method(${JSON.stringify(shape.httpMethod)}))`);
|
|
25122
|
+
lines.push(` .and(path_matcher(${JSON.stringify(shape.literalPath)}))`);
|
|
25123
|
+
lines.push(` .respond_with(ResponseTemplate::new(200).set_body_string(${JSON.stringify(body)}))`);
|
|
25124
|
+
lines.push(" .expect(1)");
|
|
25125
|
+
lines.push(" .mount(&server)");
|
|
25126
|
+
lines.push(" .await;");
|
|
25127
|
+
lines.push(" let client = common::test_client(&server).await;");
|
|
25128
|
+
lines.push(` let resp = client.${accessor}().${shape.methodIdent}(${shape.callArgs}).await.expect("expected success");`);
|
|
25129
|
+
lines.push(` assert!(${dataAccessor}.is_empty(), "expected empty data array");`);
|
|
25130
|
+
lines.push("}");
|
|
25131
|
+
return lines;
|
|
25132
|
+
}
|
|
25133
|
+
/**
|
|
25134
|
+
* Build an `_encodes_query_params` test when the op declares at least one
|
|
25135
|
+
* array query param. Constructs a params struct with a known Vec value on
|
|
25136
|
+
* each array field and asserts the actual outbound query string contains
|
|
25137
|
+
* either repeated keys (`events=foo&events=bar`, default for `explode: true`)
|
|
25138
|
+
* or a comma-joined value (`events=foo%2Cbar`, when `explode: false`).
|
|
25139
|
+
*
|
|
25140
|
+
* Returns null when no array query params apply — those ops have nothing
|
|
25141
|
+
* interesting to encode.
|
|
25142
|
+
*/
|
|
25143
|
+
function encodesQueryParamsTest(op, resolved, accessor, crate, modelMap, enumMap) {
|
|
25144
|
+
const hidden = new Set([...Object.keys(resolved.defaults ?? {}), ...resolved.inferFromClient ?? []]);
|
|
25145
|
+
const groupedNames = /* @__PURE__ */ new Set();
|
|
25146
|
+
for (const g of op.parameterGroups ?? []) for (const v of g.variants) for (const p of v.parameters) groupedNames.add(p.name);
|
|
25147
|
+
const arrayParams = op.queryParams.filter((p) => !hidden.has(p.name) && !groupedNames.has(p.name) && isStringArrayParam(p));
|
|
25148
|
+
if (arrayParams.length === 0) return null;
|
|
25149
|
+
const target = arrayParams[0];
|
|
25150
|
+
const expectedFragment = target.explode !== false ? `${target.name}=foo&${target.name}=bar` : `${target.name}=foo%2Cbar`;
|
|
25151
|
+
const callArgs = buildCallArgs(op, resolved, crate, accessor, modelMap, enumMap);
|
|
25152
|
+
const pathArgCount = op.pathParams.length;
|
|
25153
|
+
const tokenName = bearerOverrideTokenName(op);
|
|
25154
|
+
if (!(callArgs.length > pathArgCount && (tokenName ? callArgs.length > pathArgCount + 1 : true))) return null;
|
|
25155
|
+
const tokenArg = tokenName ? callArgs[callArgs.length - 1] : null;
|
|
25156
|
+
const paramsExpr = callArgs[pathArgCount];
|
|
25157
|
+
const lines = [];
|
|
25158
|
+
const respExpr = encodingResponseExpr(op, modelMap, enumMap);
|
|
25159
|
+
lines.push("#[tokio::test]");
|
|
25160
|
+
lines.push(`async fn ${accessor}_${methodName(resolved.methodName)}_encodes_query_params() {`);
|
|
25161
|
+
lines.push(" let server = MockServer::start().await;");
|
|
25162
|
+
lines.push(` Mock::given(method(${JSON.stringify("GET".replace("GET", op.httpMethod.toUpperCase()))}))`);
|
|
25163
|
+
lines.push(` .and(path_matcher(${JSON.stringify(op.path.replace(/\{[^}]+\}/g, "test_id"))}))`);
|
|
25164
|
+
lines.push(` .respond_with(ResponseTemplate::new(200).set_body_string(${respExpr}))`);
|
|
25165
|
+
lines.push(" .mount(&server)");
|
|
25166
|
+
lines.push(" .await;");
|
|
25167
|
+
lines.push(" let client = common::test_client(&server).await;");
|
|
25168
|
+
if (paramsExpr.endsWith("::default()")) {
|
|
25169
|
+
const ty = paramsExpr.slice(0, -11);
|
|
25170
|
+
lines.push(` let params = ${ty} {`);
|
|
25171
|
+
lines.push(` ${fieldIdent(target.name)}: Some(vec!["foo".to_string(), "bar".to_string()]),`);
|
|
25172
|
+
lines.push(" ..Default::default()");
|
|
25173
|
+
lines.push(" };");
|
|
25174
|
+
} else {
|
|
25175
|
+
lines.push(` let mut params = ${paramsExpr};`);
|
|
25176
|
+
lines.push(` params.${fieldIdent(target.name)} = Some(vec!["foo".to_string(), "bar".to_string()]);`);
|
|
25177
|
+
}
|
|
25178
|
+
const passArgs = [];
|
|
25179
|
+
for (let i = 0; i < pathArgCount; i++) passArgs.push(callArgs[i]);
|
|
25180
|
+
passArgs.push("params");
|
|
25181
|
+
if (tokenArg) passArgs.push(tokenArg);
|
|
25182
|
+
lines.push(` let _ = client.${accessor}().${methodName(resolved.methodName)}(${passArgs.join(", ")}).await;`);
|
|
25183
|
+
lines.push(" let received = server.received_requests().await.expect(\"recorded requests\");");
|
|
25184
|
+
lines.push(" let request = received.first().expect(\"at least one request\");");
|
|
25185
|
+
lines.push(" let query = request.url.query().unwrap_or(\"\");");
|
|
25186
|
+
lines.push(` assert!(query.contains(${JSON.stringify(expectedFragment)}), "expected query to contain {:?}, got {:?}", ${JSON.stringify(expectedFragment)}, query);`);
|
|
25187
|
+
lines.push("}");
|
|
25188
|
+
return lines;
|
|
25189
|
+
}
|
|
25190
|
+
/** Body expression for the encoding-test response (success, ignored). */
|
|
25191
|
+
function encodingResponseExpr(op, modelMap, enumMap) {
|
|
25192
|
+
if (op.pagination) {
|
|
25193
|
+
if (op.response.kind === "array") return JSON.stringify("[]");
|
|
25194
|
+
return JSON.stringify("{\"object\":\"list\",\"data\":[],\"list_metadata\":{\"before\":null,\"after\":null}}");
|
|
25195
|
+
}
|
|
25196
|
+
return responseBodyExpr(op.response, modelMap, enumMap);
|
|
25197
|
+
}
|
|
25198
|
+
/**
|
|
25199
|
+
* True if `param.type` is `Vec<String>` (or `Option<Vec<String>>`). Restricts
|
|
25200
|
+
* the encoding test to string arrays because non-string arrays would need
|
|
25201
|
+
* per-type constructors we can't synthesise reliably.
|
|
25202
|
+
*/
|
|
25203
|
+
function isStringArrayParam(p) {
|
|
25204
|
+
let t = p.type;
|
|
25205
|
+
while (t.kind === "nullable") t = t.inner;
|
|
25206
|
+
if (t.kind !== "array") return false;
|
|
25207
|
+
let inner = t.items;
|
|
25208
|
+
while (inner.kind === "nullable") inner = inner.inner;
|
|
25209
|
+
return inner.kind === "primitive" && inner.type === "string";
|
|
25210
|
+
}
|
|
25211
|
+
/** Snake-case field accessor matching the resources emitter's naming. */
|
|
25212
|
+
function fieldIdent(name) {
|
|
25213
|
+
return methodName(name);
|
|
25214
|
+
}
|
|
25215
|
+
/** True for HTTP methods that mutate state and should retry-defensively. */
|
|
25216
|
+
function isWriteMethod(method) {
|
|
25217
|
+
return method !== "get" && method !== "head";
|
|
25218
|
+
}
|
|
25219
|
+
/** True when the op has at least one body-side parameter group. */
|
|
25220
|
+
function hasBodyParameterGroup(op) {
|
|
25221
|
+
const queryNames = new Set(op.queryParams.map((p) => p.name));
|
|
25222
|
+
return (op.parameterGroups ?? []).some((g) => g.variants.every((v) => v.parameters.every((p) => !queryNames.has(p.name))));
|
|
25223
|
+
}
|
|
25224
|
+
/** Pick the snake_case token-arg name for an op with a non-bearer security override. */
|
|
25225
|
+
function bearerOverrideTokenName(op) {
|
|
25226
|
+
const override = op.security?.find((s) => s.schemeName !== "bearerAuth");
|
|
25227
|
+
if (!override) return null;
|
|
25228
|
+
return override.schemeName.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`).replace(/^_/, "");
|
|
25229
|
+
}
|
|
25230
|
+
/**
|
|
25231
|
+
* Construct a synthetic body type via its `new(...)` constructor. The
|
|
25232
|
+
* resources emitter passes required flat fields and required flatten enums
|
|
25233
|
+
* positionally; mirror that ordering here so the stub compiles against the
|
|
25234
|
+
* generated `impl <Type> { fn new(...) }`.
|
|
25235
|
+
*/
|
|
25236
|
+
function syntheticBodyStubExpr(op, resolved, crate, accessor, modelMap) {
|
|
25237
|
+
const bodyRef = op.requestBody;
|
|
25238
|
+
const fqn = `${crate}::${accessor}::${`${typeName(resolved.methodName)}ParamsBody`}`;
|
|
25239
|
+
const model = bodyRef.kind === "model" ? modelMap.get(bodyRef.name) ?? null : null;
|
|
25240
|
+
const queryNames = new Set(op.queryParams.map((p) => p.name));
|
|
25241
|
+
const bodyGroupNames = /* @__PURE__ */ new Set();
|
|
25242
|
+
for (const g of op.parameterGroups ?? []) if (g.variants.every((v) => v.parameters.every((p) => !queryNames.has(p.name)))) for (const v of g.variants) for (const p of v.parameters) bodyGroupNames.add(p.name);
|
|
25243
|
+
const args = [];
|
|
25244
|
+
if (model) for (const f of model.fields) {
|
|
25245
|
+
if (bodyGroupNames.has(f.name)) continue;
|
|
25246
|
+
if (!(!!f.required && f.type.kind !== "nullable")) continue;
|
|
25247
|
+
args.push(`${JSON.stringify(`stub_${f.name}`)}.to_string()`);
|
|
25248
|
+
}
|
|
25249
|
+
for (const g of op.parameterGroups ?? []) {
|
|
25250
|
+
if (!g.variants.every((v) => v.parameters.every((p) => !queryNames.has(p.name)))) continue;
|
|
25251
|
+
if (g.optional) continue;
|
|
25252
|
+
args.push(parameterGroupStubExpr(g, crate, accessor));
|
|
25253
|
+
}
|
|
25254
|
+
return `${fqn}::new(${args.join(", ")})`;
|
|
25255
|
+
}
|
|
25256
|
+
/** First-variant stub for a parameter-group enum, fully crate-qualified. */
|
|
25257
|
+
function parameterGroupStubExpr(group, crate, accessor) {
|
|
25258
|
+
const enumName = group.name.split("_").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
25259
|
+
const firstVariant = group.variants[0];
|
|
25260
|
+
const variantName = firstVariant.name.split("_").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
25261
|
+
const fqn = `${crate}::${accessor}::${enumName}`;
|
|
25262
|
+
if (firstVariant.parameters.length === 0) return `${fqn}::${variantName}`;
|
|
25263
|
+
return `${fqn}::${variantName} { ${firstVariant.parameters.map((p) => `${p.name}: ${JSON.stringify(`stub_${p.name}`)}.to_string()`).join(", ")} }`;
|
|
25264
|
+
}
|
|
24525
25265
|
/** Test for a wrapper-method operation. */
|
|
24526
25266
|
function renderWrapperTest(op, wrapper, ctx, accessor, crate, modelMap, enumMap) {
|
|
24527
25267
|
const m = methodName(wrapper.name);
|
|
@@ -24544,6 +25284,7 @@ function renderWrapperTest(op, wrapper, ctx, accessor, crate, modelMap, enumMap)
|
|
|
24544
25284
|
});
|
|
24545
25285
|
callArgs.push(`${paramsType}::new(${ctorArgs.join(", ")})`);
|
|
24546
25286
|
}
|
|
25287
|
+
const callArgsStr = callArgs.join(", ");
|
|
24547
25288
|
const lines = [];
|
|
24548
25289
|
lines.push("#[tokio::test]");
|
|
24549
25290
|
lines.push(`async fn ${accessor}_${m}_round_trip() {`);
|
|
@@ -24555,8 +25296,26 @@ function renderWrapperTest(op, wrapper, ctx, accessor, crate, modelMap, enumMap)
|
|
|
24555
25296
|
lines.push(" .mount(&server)");
|
|
24556
25297
|
lines.push(" .await;");
|
|
24557
25298
|
lines.push(" let client = common::test_client(&server).await;");
|
|
24558
|
-
lines.push(` let _ = client.${accessor}().${m}(${
|
|
25299
|
+
lines.push(` let _ = client.${accessor}().${m}(${callArgsStr}).await;`);
|
|
24559
25300
|
lines.push("}");
|
|
25301
|
+
const shape = {
|
|
25302
|
+
methodIdent: m,
|
|
25303
|
+
literalPath,
|
|
25304
|
+
httpMethod,
|
|
25305
|
+
callArgs: callArgsStr,
|
|
25306
|
+
isUrlBuilder: false,
|
|
25307
|
+
isWrite: isWriteMethod(op.httpMethod),
|
|
25308
|
+
isPaginated: !!op.pagination,
|
|
25309
|
+
hasBodyGroup: false
|
|
25310
|
+
};
|
|
25311
|
+
for (const errTest of standardErrorTests(shape, accessor)) {
|
|
25312
|
+
lines.push("");
|
|
25313
|
+
lines.push(...errTest);
|
|
25314
|
+
}
|
|
25315
|
+
if (shape.isWrite) for (const errTest of writeErrorTests(shape, accessor)) {
|
|
25316
|
+
lines.push("");
|
|
25317
|
+
lines.push(...errTest);
|
|
25318
|
+
}
|
|
24560
25319
|
return lines;
|
|
24561
25320
|
}
|
|
24562
25321
|
/** Rust string-expression for the mock response body. */
|
|
@@ -24727,4 +25486,4 @@ const workosEmittersPlugin = {
|
|
|
24727
25486
|
//#endregion
|
|
24728
25487
|
export { pythonEmitter as _, rustExtractor as a, pythonExtractor as c, rustEmitter as d, rubyEmitter as f, phpEmitter as g, goEmitter as h, kotlinExtractor as i, rubyExtractor as l, dotnetEmitter as m, elixirExtractor as n, goExtractor as o, kotlinEmitter as p, dotnetExtractor as r, phpExtractor as s, workosEmittersPlugin as t, nodeExtractor as u, nodeEmitter as v };
|
|
24729
25488
|
|
|
24730
|
-
//# sourceMappingURL=plugin-
|
|
25489
|
+
//# sourceMappingURL=plugin-CmfzawTp.mjs.map
|