Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
53 changes: 31 additions & 22 deletions apps/web/app/(main)/docs/validation/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import Link from "next/link";
import { Code } from "@/components/code";
import Link from 'next/link';
import { Code } from '@/components/code';

export const metadata = {
title: "Validation | json-render",
title: 'Validation | json-render',
};

export default function ValidationPage() {
return (
<article>
<h1 className="text-3xl font-bold mb-4">Validation</h1>
<h1 className="mb-4 text-3xl font-bold">Validation</h1>
<p className="text-muted-foreground mb-8">
Validate form inputs with built-in and custom functions.
</p>

<h2 className="text-xl font-semibold mt-12 mb-4">Built-in Validators</h2>
<p className="text-sm text-muted-foreground mb-4">
<h2 className="mt-12 mb-4 text-xl font-semibold">Built-in Validators</h2>
<p className="text-muted-foreground mb-4 text-sm">
json-render includes common validation functions:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4">
<ul className="text-muted-foreground mb-4 list-inside list-disc space-y-1 text-sm">
<li>
<code className="text-foreground">required</code> — Value must be
non-empty
Expand All @@ -43,9 +43,18 @@ export default function ValidationPage() {
<li>
<code className="text-foreground">max</code> — Maximum numeric value
</li>
<li>
<code className="text-foreground">numeric</code> — Numeric value
</li>
<li>
<code className="text-foreground">url</code> — Valid url format
</li>
<li>
<code className="text-foreground">matches</code> — Matches a value
</li>
</ul>

<h2 className="text-xl font-semibold mt-12 mb-4">
<h2 className="mt-12 mb-4 text-xl font-semibold">
Using Validation in JSON
</h2>
<Code lang="json">{`{
Expand All @@ -61,7 +70,7 @@ export default function ValidationPage() {
}
}`}</Code>

<h2 className="text-xl font-semibold mt-12 mb-4">
<h2 className="mt-12 mb-4 text-xl font-semibold">
Validation with Parameters
</h2>
<Code lang="json">{`{
Expand All @@ -71,8 +80,8 @@ export default function ValidationPage() {
"valuePath": "/form/password",
"checks": [
{ "fn": "required", "message": "Password is required" },
{
"fn": "minLength",
{
"fn": "minLength",
"args": { "length": 8 },
"message": "Password must be at least 8 characters"
},
Expand All @@ -85,10 +94,10 @@ export default function ValidationPage() {
}
}`}</Code>

<h2 className="text-xl font-semibold mt-12 mb-4">
<h2 className="mt-12 mb-4 text-xl font-semibold">
Custom Validation Functions
</h2>
<p className="text-sm text-muted-foreground mb-4">
<p className="text-muted-foreground mb-4 text-sm">
Define custom validators in your catalog:
</p>
<Code lang="typescript">{`const catalog = createCatalog({
Expand All @@ -103,7 +112,7 @@ export default function ValidationPage() {
},
});`}</Code>

<p className="text-sm text-muted-foreground mb-4">
<p className="text-muted-foreground mb-4 text-sm">
Then implement them in your ValidationProvider:
</p>
<Code lang="tsx">{`import { ValidationProvider } from '@json-render/react';
Expand All @@ -128,7 +137,7 @@ function App() {
);
}`}</Code>

<h2 className="text-xl font-semibold mt-12 mb-4">Using in Components</h2>
<h2 className="mt-12 mb-4 text-xl font-semibold">Using in Components</h2>
<Code lang="tsx">{`import { useFieldValidation } from '@json-render/react';

function TextField({ element }) {
Expand All @@ -152,12 +161,12 @@ function TextField({ element }) {
);
}`}</Code>

<h2 className="text-xl font-semibold mt-12 mb-4">Validation Timing</h2>
<p className="text-sm text-muted-foreground mb-4">
Control when validation runs with{" "}
<h2 className="mt-12 mb-4 text-xl font-semibold">Validation Timing</h2>
<p className="text-muted-foreground mb-4 text-sm">
Control when validation runs with{' '}
<code className="text-foreground">validateOn</code>:
</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
<ul className="text-muted-foreground list-inside list-disc space-y-1 text-sm">
<li>
<code className="text-foreground">change</code> — Validate on every
input change
Expand All @@ -172,9 +181,9 @@ function TextField({ element }) {
</li>
</ul>

<h2 className="text-xl font-semibold mt-12 mb-4">Next</h2>
<p className="text-sm text-muted-foreground">
Learn about{" "}
<h2 className="mt-12 mb-4 text-xl font-semibold">Next</h2>
<p className="text-muted-foreground text-sm">
Learn about{' '}
<Link href="/docs/ai-sdk" className="text-foreground hover:underline">
AI SDK integration
</Link>
Expand Down
100 changes: 62 additions & 38 deletions packages/core/src/validation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from "zod";
import type { DynamicValue, DataModel, LogicExpression } from "./types";
import { DynamicValueSchema, resolveDynamicValue } from "./types";
import { LogicExpressionSchema, evaluateLogicExpression } from "./visibility";
import { z } from 'zod';
import type { DynamicValue, DataModel, LogicExpression } from './types';
import { DynamicValueSchema, resolveDynamicValue } from './types';
import { LogicExpressionSchema, evaluateLogicExpression } from './visibility';

/**
* Validation check definition
Expand All @@ -22,7 +22,7 @@ export interface ValidationConfig {
/** Array of checks to run */
checks?: ValidationCheck[];
/** When to run validation */
validateOn?: "change" | "blur" | "submit";
validateOn?: 'change' | 'blur' | 'submit';
/** Condition for when validation is enabled */
enabled?: LogicExpression;
}
Expand All @@ -41,7 +41,7 @@ export const ValidationCheckSchema = z.object({
*/
export const ValidationConfigSchema = z.object({
checks: z.array(ValidationCheckSchema).optional(),
validateOn: z.enum(["change", "blur", "submit"]).optional(),
validateOn: z.enum(['change', 'blur', 'submit']).optional(),
enabled: LogicExpressionSchema.optional(),
});

Expand All @@ -50,7 +50,7 @@ export const ValidationConfigSchema = z.object({
*/
export type ValidationFunction = (
value: unknown,
args?: Record<string, unknown>,
args?: Record<string, unknown>
) => boolean;

/**
Expand All @@ -72,7 +72,7 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
*/
required: (value: unknown) => {
if (value === null || value === undefined) return false;
if (typeof value === "string") return value.trim().length > 0;
if (typeof value === 'string') return value.trim().length > 0;
if (Array.isArray(value)) return value.length > 0;
return true;
},
Expand All @@ -81,37 +81,49 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
* Check if value is a valid email address
*/
email: (value: unknown) => {
if (typeof value !== "string") return false;
if (typeof value !== 'string') return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
},

/**
* Check minimum string length
*/
minLength: (value: unknown, args?: Record<string, unknown>) => {
if (typeof value !== "string") return false;
if (typeof value !== 'string') return false;
const min = args?.min;
if (typeof min !== "number") return false;
if (min === undefined) {
console.warn('minLength validation requires a min argument');
return false;
}
if (typeof min !== 'number') return false;
return value.length >= min;
},

/**
* Check maximum string length
*/
maxLength: (value: unknown, args?: Record<string, unknown>) => {
if (typeof value !== "string") return false;
if (typeof value !== 'string') return false;
const max = args?.max;
if (typeof max !== "number") return false;
if (max === undefined) {
console.warn('maxLength validation requires a max argument');
return false;
}
if (typeof max !== 'number') return false;
return value.length <= max;
},

/**
* Check if string matches a regex pattern
*/
pattern: (value: unknown, args?: Record<string, unknown>) => {
if (typeof value !== "string") return false;
if (typeof value !== 'string') return false;
const pattern = args?.pattern;
if (typeof pattern !== "string") return false;
if (pattern === undefined) {
console.warn('pattern validation requires a pattern argument');
return false;
}
if (typeof pattern !== 'string') return false;
try {
return new RegExp(pattern).test(value);
} catch {
Expand All @@ -123,36 +135,44 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
* Check minimum numeric value
*/
min: (value: unknown, args?: Record<string, unknown>) => {
if (typeof value !== "number") return false;
if (typeof value !== 'number') return false;
const min = args?.min;
if (typeof min !== "number") return false;
if (min === undefined) {
console.warn('min validation requires a min argument');
return false;
}
if (typeof min !== 'number') return false;
return value >= min;
},

/**
* Check maximum numeric value
*/
max: (value: unknown, args?: Record<string, unknown>) => {
if (typeof value !== "number") return false;
if (typeof value !== 'number') return false;
const max = args?.max;
if (typeof max !== "number") return false;
if (max === undefined) {
console.warn('max validation requires a max argument');
return false;
}
if (typeof max !== 'number') return false;
return value <= max;
},

/**
* Check if value is a number
*/
numeric: (value: unknown) => {
if (typeof value === "number") return !isNaN(value);
if (typeof value === "string") return !isNaN(parseFloat(value));
if (typeof value === 'number') return !isNaN(value);
if (typeof value === 'string') return !isNaN(parseFloat(value));
return false;
},

/**
* Check if value is a valid URL
*/
url: (value: unknown) => {
if (typeof value !== "string") return false;
if (typeof value !== 'string') return false;
try {
new URL(value);
return true;
Expand All @@ -166,6 +186,10 @@ export const builtInValidationFunctions: Record<string, ValidationFunction> = {
*/
matches: (value: unknown, args?: Record<string, unknown>) => {
const other = args?.other;
if (other === undefined) {
console.warn('matches validation requires a value argument');
return false;
}
return value === other;
},
};
Expand Down Expand Up @@ -205,7 +229,7 @@ export interface ValidationContext {
*/
export function runValidationCheck(
check: ValidationCheck,
ctx: ValidationContext,
ctx: ValidationContext
): ValidationCheckResult {
const { value, dataModel, customFunctions } = ctx;

Expand Down Expand Up @@ -244,7 +268,7 @@ export function runValidationCheck(
*/
export function runValidation(
config: ValidationConfig,
ctx: ValidationContext & { authState?: { isSignedIn: boolean } },
ctx: ValidationContext & { authState?: { isSignedIn: boolean } }
): ValidationResult {
const checks: ValidationCheckResult[] = [];
const errors: string[] = [];
Expand Down Expand Up @@ -282,56 +306,56 @@ export function runValidation(
* Helper to create validation checks
*/
export const check = {
required: (message = "This field is required"): ValidationCheck => ({
fn: "required",
required: (message = 'This field is required'): ValidationCheck => ({
fn: 'required',
message,
}),

email: (message = "Invalid email address"): ValidationCheck => ({
fn: "email",
email: (message = 'Invalid email address'): ValidationCheck => ({
fn: 'email',
message,
}),

minLength: (min: number, message?: string): ValidationCheck => ({
fn: "minLength",
fn: 'minLength',
args: { min },
message: message ?? `Must be at least ${min} characters`,
}),

maxLength: (max: number, message?: string): ValidationCheck => ({
fn: "maxLength",
fn: 'maxLength',
args: { max },
message: message ?? `Must be at most ${max} characters`,
}),

pattern: (pattern: string, message = "Invalid format"): ValidationCheck => ({
fn: "pattern",
pattern: (pattern: string, message = 'Invalid format'): ValidationCheck => ({
fn: 'pattern',
args: { pattern },
message,
}),

min: (min: number, message?: string): ValidationCheck => ({
fn: "min",
fn: 'min',
args: { min },
message: message ?? `Must be at least ${min}`,
}),

max: (max: number, message?: string): ValidationCheck => ({
fn: "max",
fn: 'max',
args: { max },
message: message ?? `Must be at most ${max}`,
}),

url: (message = "Invalid URL"): ValidationCheck => ({
fn: "url",
url: (message = 'Invalid URL'): ValidationCheck => ({
fn: 'url',
message,
}),

matches: (
otherPath: string,
message = "Fields must match",
message = 'Fields must match'
): ValidationCheck => ({
fn: "matches",
fn: 'matches',
args: { other: { path: otherPath } },
message,
}),
Expand Down