Data validation
한국어.
Validate data with the same specification on the client and server. Fields use
validate for rules. Validate the specification's declaration shape separately
with the schema procedure. The
schema contract defines composition and accepted fields.
Install dependencies and build the JavaScript packages first. JavaScript and PHP
examples run from the repository root; Go and Rust use the module setup described
below. Each language example supplies an empty required email and expects valid to be
false.
TypeScript and JavaScript
The package root exports validate(spec, data, options?). Options accept files,
loader and basepath for composition. A supplied loader takes precedence over
the in-memory file set.
import { validate } from '@polyspec/crudui-validator';
const spec = {
type: 'group',
properties: { email: { type: 'email', validate: { required: true, email: true } } },
};
const result = validate(spec, { email: '' });
if (result.valid || result.errors[0]?.rule !== 'required') {
throw new Error('Expected required validation failure');
}
PHP
Polyspec\Crudui\Validator::validate accepts the specification, data,
optional virtual files, an optional loader and a base path. Its result exposes
valid, errors and toArray().
<?php
require 'vendor/autoload.php';
use Polyspec\Crudui\Validator;
$spec = [
'type' => 'group',
'properties' => [
'email' => ['type' => 'email', 'validate' => ['required' => true, 'email' => true]],
],
];
$result = Validator::validate($spec, ['email' => '']);
if ($result->valid || $result->errors[0]['rule'] !== 'required') {
throw new RuntimeException('Expected required validation failure');
}
Go
The validator/validate package provides Validate for an ordered specification value
and ValidateJSON for JSON bytes. ValidateJSON accepts specification bytes,
data bytes, virtual-file bytes and a base path. Run the example in the
packages/validator/go module. Its result exposes Valid and Errors.
package main
import (
"fmt"
"github.com/polyspec/crudui/packages/validator/go/validator/validate"
)
func main() {
spec := []byte(`{"type":"group","properties":{"email":{"type":"email","validate":{"required":true,"email":true}}}}`)
result, err := validate.ValidateJSON(spec, []byte(`{"email":""}`), nil, "")
if err != nil {
panic(err)
}
if result.Valid || len(result.Errors) == 0 || result.Errors[0].Rule != "required" {
panic(fmt.Errorf("expected required validation failure"))
}
}
Rust
The crate root exports validate and ValidateOptions. Options accept virtual
files, a loader and a base path. Run the example with the polyspec-crudui-validator
crate and serde_json dependencies.
use polyspec_crudui_validator::{validate, ValidateOptions};
use serde_json::json;
fn main() {
let spec = json!({
"type": "group",
"properties": {"email": {"type": "email", "validate": {"required": true, "email": true}}}
});
let result = validate(&spec, &json!({"email": ""}), &ValidateOptions::default())
.expect("specification should load");
assert!(!result.valid);
assert_eq!(result.errors[0].rule, "required");
}
Browser validation
Validate a server-rendered form in the browser with the specification the server validates it with, so a user sees errors before the request is sent. The browser validation contract defines the data, the error markup and the timing. The server still validates every submission it receives.
- Render the complete form on the server with
renderForm(form, { action, ... }), from a template compiled with akeyPrefix. - Send the specification with the page, for example as JSON in a
<script type="application/json">element. The binding callsvalidate(spec, data)without files or a loader, so send the specification with its composition resolved. - Bind the parsed
formelement with the same key prefix and the page's texts:
import { bindForm } from '@polyspec/crudui-form-binding';
const texts = { 'email.required': 'Enter your email address.', invalid: 'Check the marked fields.' };
const spec = JSON.parse(document.querySelector('#member-spec').textContent);
const binding = bindForm(document.querySelector('#member-form'), spec, {
keyPrefix: 'form',
message: (error) => texts[`${error.field}.${error.rule}`] ?? error.message,
formErrors: (result) => (result.valid ? [] : [texts.invalid]),
});
The binding validates a changed field when it loses focus and on every later change, and the whole
form on submit; an invalid form is not sent and focus moves to its first invalid control. Send a
form from script with form.requestSubmit(): form.submit() fires no submit event, so the
binding does not validate it. Call binding.dispose() before the page removes the form.
The binding also shows and hides the fields and groups that declare design.show as the data
changes (display), without changing a value;
hiddenPaths(spec, data) of @polyspec/crudui-validator returns the hidden paths for other scripts.
Run the binding checks, including the check in Chromium, Firefox and WebKit, from the repository root:
make crudui-test-form-binding
Results and failures
A validation result contains valid, a boolean, errors, a flat error list, and hidden, the
data paths of the fields that design.show hides (visibility).
Each error identifies path, field, rule and message, with value when available. A
required-input failure is a validation result. A server that stores the submitted data can leave
the values at the hidden paths, and everything below them, unstored.
Two failures produce no validation result. A missing composition reference, a
forbidden schema key, a rule name that is not registered (UNKNOWN_RULE) or a rule
parameter outside its definition
(parameter errors) is a load failure
(ComposeLoadError). Submitted data with
the wrong shape is an input failure (FormInputError, code INVALID_FORM_INPUT):
| Data | Message |
|---|---|
| Root data that is not an object, checked before composition | Form data must be an object |
| Present group data or a repeated group row that is not an object | Group data must be an object: {path} |
| Present repeated data that is not a keyed object | Repeated data must be a keyed object: {path} |
| An object member in root, group or repeated group row data without a declared field | Unknown form data field: {path} |
| An array or object as the value of a single-choice field or of one of its rows | Choice data must be a single value: {path} |
Missing group or repeated data is not a failure. JavaScript and PHP throw the
failure; Go returns it as an error; Rust returns Err(ValidateError). Every
implementation reports the same code, message and location: a load failure's
location is its composition trace joined with ., and an input failure's location
is empty. Do not convert a failure into a validation result.
The packages provide these library functions; no package installs a validation command. The cross-check console compares the four implementations by running each one in its own process through small programs that call the same functions; its validator processes document that request and response contract.
A field that design.show hides for the submitted data is not validated, and its value stays in
the data; see visibility. Which
values to store is decided from the same data. A condition in validate.required makes a visible field
optional; see the expression contract.
Data validation does not implement HTTP decoding, persistence or transport order.
Follow the ordered JSON procedure and
form checks for those operations.
Complete symbol references are generated with make docs-api; see the
documentation procedure.