Skip to main content

schemaValidatorII

/**
* @typedef {string | number} PathSegment
*
* @typedef {{
* path: PathSegment[],
* message: string,
* }} ValidationError
*
* @typedef {{
* success: true,
* data: unknown,
* } | {
* success: false,
* errors: ValidationError[],
* }} ParseResult
*
* @typedef {{
* safeParse(value: unknown): ParseResult,
* }} Schema
*
* @typedef {Schema & {
* min(length: number): StringSchema,
* max(length: number): StringSchema,
* }} StringSchema
*
* @typedef {Schema & {
* min(value: number): NumberSchema,
* max(value: number): NumberSchema,
* }} NumberSchema
*
* @typedef {{
* string(): StringSchema,
* number(): NumberSchema,
* boolean(): Schema,
* object(shape: Record<string, Schema>): Schema,
* }} SchemaFactory
*/

/** @type {SchemaFactory} */
/**
* Mini schema validator with immutable, chainable rules.
*
* Data structure:
* - Schema object with safeParse()
* - Primitive schemas keep an ordered rules array
*
* Core idea:
* - Type check first
* - Then run rules in the order they were chained
* - .min() / .max() return a NEW schema instead of mutating old one
*/

function createStringSchema(rules = []) {
return {
min(length) {
return createStringSchema([
...rules,
{
check: (value) => value.length >= length,
message: `Expected at least ${length} characters`,
},
]);
},

max(length) {
return createStringSchema([
...rules,
{
check: (value) => value.length <= length,
message: `Expected at most ${length} characters`,
},
]);
},

safeParse(value) {
if (typeof value !== 'string') {
return {
success: false,
errors: [{ path: [], message: 'Expected string' }],
};
}

const errors = [];

for (const rule of rules) {
if (!rule.check(value)) {
errors.push({
path: [],
message: rule.message,
});
}
}

if (errors.length > 0) {
return {
success: false,
errors,
};
}

return {
success: true,
data: value,
};
},
};
}

function createNumberSchema(rules = []) {
return {
min(minValue) {
return createNumberSchema([
...rules,
{
check: (value) => value >= minValue,
message: `Expected number >= ${minValue}`,
},
]);
},

max(maxValue) {
return createNumberSchema([
...rules,
{
check: (value) => value <= maxValue,
message: `Expected number <= ${maxValue}`,
},
]);
},

safeParse(value) {
if (typeof value !== 'number') {
return {
success: false,
errors: [{ path: [], message: 'Expected number' }],
};
}

const errors= [];
for (const rule of rules) {
if (!rule.check(value)) {
errors.push({
path: [],
message: rule.message,
});
}
}

if (errors.length > 0) {
return {
success: false,
errors
};
}
return {
success: true,
data: value,
};
},
};
}

function createBooleanSchema() {
return {
safeParse(value) {
if (typeof value !== 'boolean') {
return {
success: false,
errors: [{ path: [], message: 'Expected boolean' }],
};
}

return { success: true, data: value };
},
};
}

const v = {
string() {
return createStringSchema();
},

number() {
return createNumberSchema();
},

boolean() {
return createBooleanSchema();
},

object(shape) {
return {
safeParse(value) {
if (
value === null ||
typeof value !== 'object' ||
Array.isArray(value)
) {
return {
success: false,
errors: [{ path: [], message: 'Expected object' }],
};
}

const errors = [];

for (const key of Object.keys(shape)) {
if (value[key] === undefined) {
errors.push({
path: [key],
message: 'Required',
});
continue;
}

const result = shape[key].safeParse(value[key]);

if (!result.success) {
errors.push({
path: [key],
message: result.errors[0].message,
});
}
}

if (errors.length > 0) {
return { success: false, errors };
}

return {
success: true,
data: { ...value },
};
},
};
},
};

export default v;


/**
const Name = v.string().min(2).max(5);

Name.safeParse('Ada');
// { success: true, data: 'Ada' }

Name.safeParse('');
// {
// success: false,
// errors: [{ path: [], message: 'Expected at least 2 characters' }],
// }

*/