diff --git a/packages/groq-builder/src/commands/project.ts b/packages/groq-builder/src/commands/project.ts index 9606146..ed8a7c8 100644 --- a/packages/groq-builder/src/commands/project.ts +++ b/packages/groq-builder/src/commands/project.ts @@ -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 { @@ -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)); @@ -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 ) { diff --git a/packages/groq-builder/src/validation/lite/array-shape.ts b/packages/groq-builder/src/validation/lite/array-shape.ts index d41b3bb..4f26514 100644 --- a/packages/groq-builder/src/validation/lite/array-shape.ts +++ b/packages/groq-builder/src/validation/lite/array-shape.ts @@ -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(): OptionalParser, Array>; @@ -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); }, }; diff --git a/packages/groq-builder/src/validation/lite/object-shape.ts b/packages/groq-builder/src/validation/lite/object-shape.ts index aecc215..eec9bda 100644 --- a/packages/groq-builder/src/validation/lite/object-shape.ts +++ b/packages/groq-builder/src/validation/lite/object-shape.ts @@ -1,4 +1,4 @@ -import { createOptionalParser, inspect, OptionalParser } from "./primitives"; +import { createOptionalParser, OptionalParser } from "./primitives"; import { InferParserInput, InferParserOutput, @@ -6,8 +6,7 @@ import { 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(): OptionalParser; @@ -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; - 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) { diff --git a/packages/groq-builder/src/validation/lite/primitives.ts b/packages/groq-builder/src/validation/lite/primitives.ts index f4fa1bc..07f1324 100644 --- a/packages/groq-builder/src/validation/lite/primitives.ts +++ b/packages/groq-builder/src/validation/lite/primitives.ts @@ -1,4 +1,5 @@ import { ParserFunction } from "../../types/public-types"; +import { inspect } from "../simple-validation"; export const primitiveValidation = { string: memo(() => createTypeValidator("string")), @@ -57,17 +58,6 @@ export function memo 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. diff --git a/packages/groq-builder/src/validation/simple-validation.ts b/packages/groq-builder/src/validation/simple-validation.ts new file mode 100644 index 0000000..babd97b --- /dev/null +++ b/packages/groq-builder/src/validation/simple-validation.ts @@ -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( + itemParser: null | ParserFunction +): ParserFunction, Array> { + if (!itemParser) { + return (input) => { + if (!Array.isArray(input)) { + throw new TypeError(`Expected an array, received ${inspect(input)}`); + } + return input as unknown as Array; + }; + } + + 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( + objectMapper?: TMap +): ParserFunction< + Simplify<{ + [P in keyof TMap]: TMap[P] extends {} + ? // + InferParserInput + : unknown; + }>, + Simplify<{ + [P in keyof TMap]: TMap[P] extends {} + ? // + InferParserOutput + : 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; + 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; + }; +}