feat/custom-json-schema (#172)

* init

* update

* finished new repo query, removed old implementation

* remove debug folder
This commit is contained in:
dswbx
2025-05-22 08:52:25 +02:00
committed by GitHub
parent 6694c63990
commit 773df544dd
31 changed files with 614 additions and 424 deletions

View File

@@ -34,6 +34,8 @@ type ExpressionMap<Exps extends Expressions> = {
? E
: never;
};
type ExpressionKeys<Exps extends Expressions> = Exps[number]["key"];
type ExpressionCondition<Exps extends Expressions> = {
[K in keyof ExpressionMap<Exps>]: { [P in K]: ExpressionMap<Exps>[K] };
}[keyof ExpressionMap<Exps>];
@@ -195,5 +197,7 @@ export function makeValidator<Exps extends Expressions>(expressions: Exps) {
const fns = _build(query, expressions, options);
return _validate(fns);
},
expressions,
expressionKeys: expressions.map((e) => e.key) as ExpressionKeys<Exps>,
};
}

View File

@@ -0,0 +1,43 @@
import { mergeObject } from "core/utils";
export { jsc, type Options, type Hook } from "./validator";
import * as s from "jsonv-ts";
export { s };
export class InvalidSchemaError extends Error {
constructor(
public schema: s.TAnySchema,
public value: unknown,
public errors: s.ErrorDetail[] = [],
) {
super(
`Invalid schema given for ${JSON.stringify(value, null, 2)}\n\n` +
`Error: ${JSON.stringify(errors[0], null, 2)}`,
);
}
}
export type ParseOptions = {
withDefaults?: boolean;
coerse?: boolean;
};
export function parse<S extends s.TAnySchema>(
_schema: S,
v: unknown,
opts: ParseOptions = {},
): s.StaticCoerced<S> {
const schema = _schema as unknown as s.TSchema;
const value = opts.coerse !== false ? schema.coerce(v) : v;
const result = schema.validate(value, {
shortCircuit: true,
ignoreUnsupported: true,
});
if (!result.valid) throw new InvalidSchemaError(schema, v, result.errors);
if (opts.withDefaults) {
return mergeObject(schema.template({ withOptional: true }), value) as any;
}
return value as any;
}

View File

@@ -0,0 +1,63 @@
import type { Context, Env, Input, MiddlewareHandler, ValidationTargets } from "hono";
import { validator as honoValidator } from "hono/validator";
import type { Static, StaticCoerced, TAnySchema } from "jsonv-ts";
export type Options = {
coerce?: boolean;
includeSchema?: boolean;
};
type ValidationResult = {
valid: boolean;
errors: {
keywordLocation: string;
instanceLocation: string;
error: string;
data?: unknown;
}[];
};
export type Hook<T, E extends Env, P extends string> = (
result: { result: ValidationResult; data: T },
c: Context<E, P>,
) => Response | Promise<Response> | void;
export const validator = <
// @todo: somehow hono prevents the usage of TSchema
Schema extends TAnySchema,
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
Opts extends Options = Options,
Out = Opts extends { coerce: false } ? Static<Schema> : StaticCoerced<Schema>,
I extends Input = {
in: { [K in Target]: Static<Schema> };
out: { [K in Target]: Out };
},
>(
target: Target,
schema: Schema,
options?: Opts,
hook?: Hook<Out, E, P>,
): MiddlewareHandler<E, P, I> => {
// @ts-expect-error not typed well
return honoValidator(target, async (_value, c) => {
const value = options?.coerce !== false ? schema.coerce(_value) : _value;
// @ts-ignore
const result = schema.validate(value);
if (!result.valid) {
return c.json({ ...result, schema }, 400);
}
if (hook) {
const hookResult = hook({ result, data: value as Out }, c);
if (hookResult) {
return hookResult;
}
}
return value as Out;
});
};
export const jsc = validator;

View File

@@ -0,0 +1 @@
export { tbValidator } from "./tbValidator";

View File

@@ -0,0 +1,29 @@
import type { Env, Input, MiddlewareHandler, ValidationTargets } from "hono";
import { validator } from "hono/validator";
import type { Static, TSchema } from "simple-jsonschema-ts";
export const honoValidator = <
Target extends keyof ValidationTargets,
E extends Env,
P extends string,
const Schema extends TSchema = TSchema,
Out = Static<Schema>,
I extends Input = {
in: { [K in Target]: Static<Schema> };
out: { [K in Target]: Static<Schema> };
},
>(
target: Target,
schema: Schema,
): MiddlewareHandler<E, P, I> => {
// @ts-expect-error not typed well
return validator(target, async (value, c) => {
const coersed = schema.coerce(value);
const result = schema.validate(coersed);
if (!result.valid) {
return c.json({ ...result, schema }, 400);
}
return coersed as Out;
});
};

View File

@@ -406,3 +406,16 @@ export function objectToJsLiteral(value: object, indent: number = 0, _level: num
throw new TypeError(`Unsupported data type: ${t}`);
}
// lodash-es compatible `pick` with perfect type inference
export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce(
(acc, key) => {
if (key in obj) {
acc[key] = obj[key];
}
return acc;
},
{} as Pick<T, K>,
);
}