harn-stdlib 0.10.2

Embedded Harn standard library source catalog
Documentation
/**
 * 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)
}