/**
* std/schema — ergonomic schema builders and typed-value helpers
*
* Import with: import "std/schema"
*
* @effects: []
* @errors: []
*/
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 = {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 = {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 = {type: "dict", properties: properties}
if len(required) > 0 {
out = out + {required: required}
}
if options != nil {
out = schema_extend(out, options)
}
return out
}
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 = to_string(unwrap_err(parsed))
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)
}