/**
* std/schema — ergonomic schema builders and typed-value helpers
*
* Import with: import "std/schema"
*
* @effects: []
* @errors: []
*/
pub import {
SchemaContract,
SchemaContractFailure,
ValidationIssue,
ValidationRule,
} from "std/schema/contracts"
fn __validation_schema_issue(raw: unknown) -> ValidationIssue {
if type_of(raw) != "dict" {
return {code: "schema.invalid", message: to_string(raw), metadata: {schema_issue: raw}}
}
let issue: ValidationIssue = {
code: to_string(raw?.code ?? "schema.invalid"),
message: to_string(raw?.message ?? raw),
metadata: {schema_issue: raw},
}
if raw?.path != nil {
issue = issue + {path: to_string(raw.path)}
}
return issue
}
fn __validation_schema_issues(found: list<unknown>) -> list<ValidationIssue> {
let issues: list<ValidationIssue> = []
for raw in found {
issues = issues + [__validation_schema_issue(raw)]
}
return issues
}
/**
* Build one named validation rule. Return an empty issue list on success.
*
* @effects: []
* @errors: [validation]
*/
pub fn validation_rule<T>(
name: string,
check: fn(T) -> list<ValidationIssue>,
) -> ValidationRule<T> {
const clean_name = trim(name)
if clean_name == "" {
throw "std/schema: validation rule name is required"
}
return {name: clean_name, check: check}
}
/**
* Build one stable issue returned by a validation rule.
*
* @effects: []
* @errors: [validation]
*/
pub fn validation_issue(
code: string,
message: string,
path: string? = nil,
metadata: dict? = nil,
) -> ValidationIssue {
const clean_code = trim(code)
const clean_message = trim(message)
if clean_code == "" || clean_message == "" {
throw "std/schema: validation issue code and message are required"
}
let issue: ValidationIssue = {code: clean_code, message: clean_message}
if path != nil {
issue = issue + {path: path}
}
if metadata != nil {
issue = issue + {metadata: metadata}
}
return issue
}
/**
* Bind a structural schema to ordered deterministic validation rules.
*
* @effects: []
* @errors: []
*/
pub fn schema_contract<T>(
schema: Schema<T>,
rules: list<ValidationRule<T>>,
apply_defaults: bool = false,
) -> SchemaContract<T> {
return {schema: schema, rules: rules, apply_defaults: apply_defaults}
}
/**
* Structurally validate a value, then collect every rule issue in order. The
* function never throws: a broken rule becomes a `rule_error` failure.
*
* @effects: []
* @errors: []
*/
pub fn schema_contract_check<T>(
value: unknown,
contract: SchemaContract<T>,
) -> Result<T, SchemaContractFailure> {
const report = schema_report(value, contract.schema, contract.apply_defaults)
if !report.ok {
const failure: SchemaContractFailure = {
kind: "schema_invalid",
detail: report.message,
issues: __validation_schema_issues(report.issues),
}
return Err(failure)
}
const validated: T = report.value
let issues: list<ValidationIssue> = []
for rule in contract.rules {
const checked = try {
rule.check(validated)
}
if !is_ok(checked) {
const message = to_string(unwrap_err(checked)?.message ?? unwrap_err(checked))
const failure: SchemaContractFailure = {
kind: "rule_error",
detail: "validation rule '" + rule.name + "' failed: " + message,
issues: [{code: "rule.error", message: message, rule: rule.name}],
}
return Err(failure)
}
for issue in unwrap(checked) {
issues = issues
+ [
if issue.rule == nil {
issue + {rule: rule.name}
} else {
issue
},
]
}
}
if len(issues) > 0 {
const failure: SchemaContractFailure = {
kind: "rule_failed",
detail: to_string(len(issues)) + " validation issue(s)",
issues: issues,
}
return Err(failure)
}
return Ok(validated)
}
pub fn schema_any() {
return {type: "any"}
}
pub fn schema_string() {
return {type: "string"}
}
pub fn schema_int() {
return {type: "int"}
}
pub fn schema_float() {
return {type: "float"}
}
pub fn schema_bool() {
return {type: "bool"}
}
pub fn schema_nil() {
return {type: "nil"}
}
pub fn schema_literal(value) {
return {const: value}
}
pub fn schema_enum(values) {
return {enum: values}
}
pub fn schema_list(item_schema = nil, options = nil) {
let out: dict = {type: "list"}
if item_schema != nil {
out = out + {items: item_schema}
}
if options != nil {
out = schema_extend(out, options)
}
return out
}
pub fn schema_array(item_schema = nil, options = nil) {
return schema_list(item_schema, options)
}
pub fn schema_dict(value_schema = nil, options = nil) {
let out: dict = {type: "dict"}
if value_schema != nil {
out = out + {additional_properties: value_schema}
}
if options != nil {
out = schema_extend(out, options)
}
return out
}
pub fn schema_field(schema, required = true) {
return schema + {required: required}
}
pub fn schema_object(fields, options = nil) {
let properties = {}
let required = []
for entry in fields {
const raw_schema = entry.value
const is_required = raw_schema.required == nil || raw_schema.required
const field_schema = schema_omit(raw_schema, ["required"])
properties = properties + {[entry.key]: field_schema}
if is_required {
required = required + [entry.key]
}
}
let out: dict = {type: "dict", properties: properties}
if len(required) > 0 {
out = out + {required: required}
}
if options != nil {
out = schema_extend(out, options)
}
return out
}
/**
* Return a dict/object schema that rejects unknown keys.
*
* This is the preferred builder for option bags, receipts, structured LLM
* outputs, and host-contract payloads where unexpected keys should fail closed
* instead of drifting through validation.
*
* @effects: []
* @errors: []
*/
pub fn schema_closed_object(fields, options = nil) {
if options == nil {
return schema_object(fields, {additional_properties: false})
}
return schema_object(fields, options + {additional_properties: false})
}
/**
* Alias for `schema_closed_object(...)` using strict-schema terminology.
*
* @effects: []
* @errors: []
*/
pub fn schema_strict_object(fields, options = nil) {
return schema_closed_object(fields, options)
}
pub fn schema_union(branches) {
return {union: branches}
}
pub fn schema_all_of(branches) {
return {all_of: branches}
}
pub fn schema_nullable(schema) {
return schema_extend(schema, {nullable: true})
}
pub fn schema_default(schema, value) {
return schema + {default: value, required: false}
}
pub fn schema_pick_keys(schema, keys) {
return schema_pick(schema, keys)
}
pub fn schema_omit_keys(schema, keys) {
return schema_omit(schema, keys)
}
pub fn schema_partial_deep(schema) {
return schema_partial(schema)
}
pub fn schema_json(schema) {
return schema_to_json_schema(schema)
}
pub fn schema_openapi(schema) {
return schema_to_openapi_schema(schema)
}
/**
* Return a reusable validator object for a schema.
*
* This is the preferred boundary shape when a harness author needs to validate
* several values against the same contract without repeating freeform
* `schema_*` calls or hand-written `type_of` guards.
*
* @effects: []
* @errors: []
*/
pub fn schema_validator(schema, options = nil) {
const default_apply_defaults = options?.apply_defaults ?? false
return {
schema: schema,
is: fn(value) { return schema_is(value, schema) },
check: fn(value) { return schema_check(value, schema) },
parse: fn(value) { return schema_parse(value, schema) },
report: fn(value, apply_defaults = default_apply_defaults) { return schema_report(
value,
schema,
apply_defaults,
) },
expect: fn(value, apply_defaults = default_apply_defaults) { return schema_expect(
value,
schema,
apply_defaults,
) },
errors: fn(value, apply_defaults = default_apply_defaults) { return schema_report(
value,
schema,
apply_defaults,
).errors },
issues: fn(value, apply_defaults = default_apply_defaults) { return schema_report(
value,
schema,
apply_defaults,
).issues },
json_schema: fn() { return schema_to_json_schema(schema) },
openapi_schema: fn() { return schema_to_openapi_schema(schema) },
}
}
pub fn from_json_schema(schema) {
return schema_from_json_schema(schema)
}
pub fn from_openapi_schema(schema) {
return schema_from_openapi_schema(schema)
}
/**
* Validate a value and return `Result.Ok(value)` or `Result.Err(...)`.
*
* @effects: []
* @errors: []
*/
pub fn get_typed_result(value, schema, apply_defaults = false) {
if apply_defaults {
return schema_parse(value, schema)
}
return schema_check(value, schema)
}
/**
* Validate a value and return `{ok, message, errors, issues, value?}`.
*
* @effects: []
* @errors: []
*/
pub fn get_typed_report(value, schema, apply_defaults = false) {
return schema_report(value, schema, apply_defaults)
}
/**
* Parse JSON text and validate the parsed value against a schema in one step.
* Returns `{ok, message, errors, issues, value?, stage}` where `stage` is
* `json_parse` or `schema`.
*
* @effects: []
* @errors: []
*/
pub fn parse_json_typed_report<T>(text: string, schema: Schema<T>, apply_defaults = false) -> dict {
const parsed = try {
json_parse(text ?? "")
}
if !is_ok(parsed) {
const message = unwrap_err(parsed).message
return {ok: false, message: message, errors: [message], issues: [], stage: "json_parse"}
}
return get_typed_report(unwrap(parsed), schema, apply_defaults) + {stage: "schema"}
}
/**
* Parse JSON text and return the typed/defaulted value, or `fallback` if either
* JSON parsing or schema validation fails.
*
* @effects: []
* @errors: []
*/
pub fn parse_json_typed<T>(
text: string,
schema: Schema<T>,
fallback: T? = nil,
apply_defaults = false,
) -> T? {
const report = parse_json_typed_report(text, schema, apply_defaults)
if report.ok {
return report.value
}
return fallback
}
/**
* Return structured validation issues for a value/schema pair.
*
* @effects: []
* @errors: []
*/
pub fn get_typed_issues(value, schema, apply_defaults = false) {
return get_typed_report(value, schema, apply_defaults).issues
}
/**
* Return rendered validation error messages for a value/schema pair.
*
* @effects: []
* @errors: []
*/
pub fn get_typed_errors(value, schema, apply_defaults = false) {
return get_typed_report(value, schema, apply_defaults).errors
}
/**
* Validate a value and return the normalized/defaulted value, throwing on failure.
*
* @effects: []
* @errors: ["schema validation failure"]
*/
pub fn get_typed_value(value, schema, apply_defaults = false) {
const report = get_typed_report(value, schema, apply_defaults)
if report.ok {
return report.value
}
throw_error(report.message)
}
/**
* Alias for `get_typed_value` when the caller wants assertion-style wording.
*
* @effects: []
* @errors: ["schema validation failure"]
*/
pub fn expect_typed_value(value, schema, apply_defaults = false) {
return get_typed_value(value, schema, apply_defaults)
}
pub fn is_type(value, schema) {
return schema_is(value, schema)
}