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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export default tsPlugin.config(
name: "source/ez",
files: ["express-zod-api/src/*.ts"],
rules: {
complexity: ["error", 18],
complexity: ["error", 16],
"allowed/dependencies": ["error", { packageDir: ezDir }],
"no-restricted-syntax": [
"warn",
Expand Down
24 changes: 12 additions & 12 deletions express-zod-api/src/documentation-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,26 +296,26 @@ export const depictRequestParams = ({
const isQueryEnabled = inputSources.includes("query");
const areParamsEnabled = inputSources.includes("params");
const areHeadersEnabled = inputSources.includes("headers");
const isPathParam = (name: string) =>
areParamsEnabled && pathParams.includes(name);
const securityHeaders = R.chain(
R.filter((entry: Security) => entry.type === "header"),
security ?? [],
).map(({ name }) => name);
const isHeaderParam = (name: string) =>
areHeadersEnabled &&
(isHeader?.(name, method, path) ?? defaultIsHeader(name, securityHeaders));

const getLocation = (name: string) => {
if (areParamsEnabled && pathParams.includes(name)) return "path";
if (
areHeadersEnabled &&
(isHeader?.(name, method, path) ?? defaultIsHeader(name, securityHeaders))
)
return "header";
if (isQueryEnabled) return "query";
return undefined;
Comment thread
pullfrog[bot] marked this conversation as resolved.
Outdated
};

return Object.entries(flat.properties).reduce<ParameterObject[]>(
(acc, [name, jsonSchema]) => {
if (!isObject(jsonSchema)) return acc;
const location = isPathParam(name)
? "path"
: isHeaderParam(name)
? "header"
: isQueryEnabled
? "query"
: undefined;
const location = getLocation(name);
if (!location) return acc;
const depicted = asOAS(jsonSchema);
const result =
Expand Down
30 changes: 19 additions & 11 deletions express-zod-api/src/json-schema-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ export const processPropertyNames = (
if (!isOptional) requiredKeys.push(...keys);
};

/** @internal */
export const mergeExamples = (
target: FlattenObjectSchema,
entry: z.core.JSONSchema.BaseSchema,
isOptional: boolean,
) => {
if (!entry.examples?.length) return;
if (isOptional) {
target.examples = R.concat(target.examples || [], entry.examples);
} else {
target.examples = combinations(
target.examples?.filter(isObject) || [],
entry.examples.filter(isObject),
([a, b]) => R.mergeDeepRight(a, b),
);
}
};

export const flattenIO = (
jsonSchema: z.core.JSONSchema.BaseSchema,
mode: MergeMode = "coerce",
Expand All @@ -91,17 +109,7 @@ export const flattenIO = (
if (entry.description) flat.description ??= entry.description;
stack.push(...processAllOf(entry, mode, isOptional));
stack.push(...processVariants(entry));
if (entry.examples?.length) {
if (isOptional) {
flat.examples = R.concat(flat.examples || [], entry.examples);
} else {
flat.examples = combinations(
flat.examples?.filter(isObject) || [],
entry.examples.filter(isObject),
([a, b]) => R.mergeDeepRight(a, b),
);
}
}
mergeExamples(flat, entry, isOptional);
if (!isJsonObjectSchema(entry)) continue;
stack.push([isOptional, { examples: pullRequestExamples(entry) }]);
if (entry.properties) {
Expand Down
44 changes: 44 additions & 0 deletions express-zod-api/tests/json-schema-helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";
import {
flattenIO,
isJsonObjectSchema,
mergeExamples,
propsMerger,
canMerge,
nestOptional,
Expand Down Expand Up @@ -199,6 +200,49 @@ describe("JSON Schema helpers", () => {
});
});

describe("mergeExamples()", () => {
test("should do nothing when entry has no examples", () => {
const flat = { type: "object" as const, properties: {} };
mergeExamples(flat, { type: "string" }, false);
expect(flat).toEqual({ type: "object", properties: {} });
});

test("should concatenate examples when optional", () => {
const flat = {
type: "object" as const,
properties: {},
examples: [{ a: 1 }],
};
mergeExamples(flat, { examples: [{ b: 2 }, { c: 3 }] }, true);
expect(flat.examples).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]);
});

test.each([true, false])(
"should initialize examples when flat has none (isOptional=%s)",
(isOptional) => {
const flat: { type: "object"; properties: {}; examples?: unknown[] } = {
Comment thread
pullfrog[bot] marked this conversation as resolved.
Outdated
type: "object",
properties: {},
};
mergeExamples(flat, { examples: [{ a: 1 }] }, isOptional);
expect(flat.examples).toEqual([{ a: 1 }]);
},
);

test("should produce combinations when required", () => {
const flat = {
type: "object" as const,
properties: {},
examples: [{ a: 1 }],
};
mergeExamples(flat, { examples: [{ b: 2 }, { b: 3 }] }, false);
expect(flat.examples).toEqual([
{ a: 1, b: 2 },
{ a: 1, b: 3 },
]);
});
});

describe("pullRequestExamples()", () => {
test("should return empty array for empty properties", () => {
expect(pullRequestExamples({ type: "object", properties: {} })).toEqual(
Expand Down
Loading