Skip to content

Commit

Permalink
feature(zod): extracted the object/array parsing logic, used by `proj…
Browse files Browse the repository at this point in the history
…ect`
  • Loading branch information
scottrippey committed Jan 23, 2024
1 parent 98a15f1 commit 3b5d359
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 87 deletions.
10 changes: 6 additions & 4 deletions packages/groq-builder/src/commands/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
ProjectionFieldConfig,
ProjectionMap,
} from "./projection-types";
import { objectValidation } from "../validation/lite/object-shape";
import { arrayValidation } from "../validation/lite/array-shape";
import { isConditional } from "./conditional-types";
import {
simpleArrayParser,
simpleObjectParser,
} from "../validation/simple-validation";

declare module "../groq-builder" {
export interface GroqBuilder<TResult, TRootConfig> {
Expand Down Expand Up @@ -128,7 +130,7 @@ function createProjectionParser(
const objectShape = Object.fromEntries(
normalFields.map((f) => [f.key, f.parser])
);
const objectParser = objectValidation.object(objectShape);
const objectParser = simpleObjectParser(objectShape);

// Parse all conditional fields:
const conditionalFields = fields.filter((f) => isConditional(f.key));
Expand All @@ -148,7 +150,7 @@ function createProjectionParser(
};

// Finally, transparently handle arrays or objects:
const arrayParser = arrayValidation.array(combinedParser);
const arrayParser = simpleArrayParser(combinedParser);
return function projectionParser(
input: UnknownObject | Array<UnknownObject>
) {
Expand Down
39 changes: 7 additions & 32 deletions packages/groq-builder/src/validation/lite/array-shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
} from "../../types/public-types";
import { normalizeValidationFunction } from "../../commands/validate-utils";
import { Simplify } from "../../types/utils";
import { ValidationErrors } from "../validation-errors";
import { createOptionalParser, inspect, OptionalParser } from "./primitives";
import { createOptionalParser, OptionalParser } from "./primitives";
import { simpleArrayParser } from "../simple-validation";

export interface ArrayValidation {
array<TItem>(): OptionalParser<Array<TItem>, Array<TItem>>;
Expand All @@ -20,35 +20,10 @@ export interface ArrayValidation {

export const arrayValidation: ArrayValidation = {
array(itemParser?: Parser) {
if (!itemParser) {
return createOptionalParser((input) => {
if (!Array.isArray(input)) {
throw new TypeError(`Expected an array, received ${inspect(input)}`);
}
return input;
});
}

const normalizedItemParser = normalizeValidationFunction(itemParser)!;

return createOptionalParser((input) => {
if (!Array.isArray(input)) {
throw new TypeError(`Expected an array, received ${inspect(input)}`);
}

const validationErrors = new ValidationErrors();
const results = input.map((value, i) => {
try {
return normalizedItemParser(value);
} catch (err) {
validationErrors.add(`[${i}]`, value, err as Error);
return null;
}
});

if (validationErrors.length) throw validationErrors;

return results;
});
const normalizedItemParser = normalizeValidationFunction(
itemParser || null
);
const arrayParser = simpleArrayParser(normalizedItemParser);
return createOptionalParser(arrayParser);
},
};
43 changes: 3 additions & 40 deletions packages/groq-builder/src/validation/lite/object-shape.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { createOptionalParser, inspect, OptionalParser } from "./primitives";
import { createOptionalParser, OptionalParser } from "./primitives";
import {
InferParserInput,
InferParserOutput,
Parser,
ParserFunction,
} from "../../types/public-types";
import { Simplify } from "../../types/utils";
import { normalizeValidationFunction } from "../../commands/validate-utils";
import { ValidationErrors } from "../validation-errors";
import { simpleObjectParser } from "../simple-validation";

interface ObjectValidation {
object<TResult>(): OptionalParser<TResult, TResult>;
Expand Down Expand Up @@ -37,43 +36,7 @@ interface ObjectValidation {

export const objectValidation: ObjectValidation = {
object(map?: ObjectValidationMap) {
if (!map) {
return createOptionalParser((input) => {
if (input === null || typeof input !== "object") {
throw new TypeError(`Expected an object, received ${inspect(input)}`);
}
return input;
});
}

const keys = Object.keys(map) as Array<string>;
const normalized = keys.map(
(key) =>
[
key,
normalizeValidationFunction(map[key as keyof typeof map]),
] as const
);
return createOptionalParser((input) => {
if (input === null || typeof input !== "object") {
throw new TypeError(`Expected an object, received ${inspect(input)}`);
}

const validationErrors = new ValidationErrors();

const result: any = {};
for (const [key, parse] of normalized) {
const value = input[key as keyof typeof input];
try {
result[key] = parse ? parse(value) : value;
} catch (err) {
validationErrors.add(key, value, err as Error);
}
}

if (validationErrors.length) throw validationErrors;
return result;
});
return createOptionalParser(simpleObjectParser(map));
},

union(parserA, parserB) {
Expand Down
12 changes: 1 addition & 11 deletions packages/groq-builder/src/validation/lite/primitives.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ParserFunction } from "../../types/public-types";
import { inspect } from "../simple-validation";

export const primitiveValidation = {
string: memo(() => createTypeValidator("string")),
Expand Down Expand Up @@ -57,17 +58,6 @@ export function memo<T extends () => any>(fn: T): T {
return (() => result || (result = fn())) as T;
}

/**
* Pretty-prints the value
*/
export function inspect(value: unknown): string {
if (value) {
if (Array.isArray(value)) return "an array";
if (typeof value === "object") return "an object";
}
return JSON.stringify(value);
}

/**
* Extends the parsing function with an `.optional()` extension,
* which allows null/undefined values.
Expand Down
111 changes: 111 additions & 0 deletions packages/groq-builder/src/validation/simple-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
InferParserInput,
InferParserOutput,
ParserFunction,
} from "../types/public-types";
import { ValidationErrors } from "./validation-errors";
import { Simplify } from "../types/utils";
import { normalizeValidationFunction } from "../commands/validate-utils";
import { ObjectValidationMap } from "./lite/object-shape";

/**
* Pretty-prints the value
*/
export function inspect(value: unknown): string {
if (value) {
if (Array.isArray(value)) return "an array";
if (typeof value === "object") return "an object";
}
return JSON.stringify(value);
}

export function simpleArrayParser<TItemInput, TItemOutput>(
itemParser: null | ParserFunction<TItemInput, TItemOutput>
): ParserFunction<Array<TItemInput>, Array<TItemOutput>> {
if (!itemParser) {
return (input) => {
if (!Array.isArray(input)) {
throw new TypeError(`Expected an array, received ${inspect(input)}`);
}
return input as unknown as Array<TItemOutput>;
};
}

return (input) => {
if (!Array.isArray(input)) {
throw new TypeError(`Expected an array, received ${inspect(input)}`);
}

const validationErrors = new ValidationErrors();
const results = input.map((value, i) => {
try {
return itemParser(value);
} catch (err) {
validationErrors.add(`[${i}]`, value, err as Error);
return null as never;
}
});

if (validationErrors.length) throw validationErrors;

return results;
};
}

export function simpleObjectParser<TMap extends ObjectValidationMap>(
objectMapper?: TMap
): ParserFunction<
Simplify<{
[P in keyof TMap]: TMap[P] extends {}
? //
InferParserInput<TMap[P]>
: unknown;
}>,
Simplify<{
[P in keyof TMap]: TMap[P] extends {}
? //
InferParserOutput<TMap[P]>
: unknown;
}>
> {
if (!objectMapper) {
return (input: unknown) => {
if (input === null || typeof input !== "object") {
throw new TypeError(`Expected an object, received ${inspect(input)}`);
}
return input as any;
};
}

const keys = Object.keys(objectMapper) as Array<string>;
const entries = keys.map(
(key) =>
[
key,
normalizeValidationFunction(
objectMapper[key as keyof typeof objectMapper]
),
] as const
);

return (input) => {
if (input === null || typeof input !== "object") {
throw new TypeError(`Expected an object, received ${inspect(input)}`);
}

const validationErrors = new ValidationErrors();

const result: any = {};
for (const [key, parser] of entries) {
const value = input[key as keyof typeof input];
try {
result[key] = parser ? parser(value) : value;
} catch (err) {
validationErrors.add(key, value, err as Error);
}
}

if (validationErrors.length) throw validationErrors;
return result;
};
}

0 comments on commit 3b5d359

Please sign in to comment.