///|
/// JSON Schema subset validation for tool input.
/// Compatible with OpenAI-style object schemas.
///|
pub fn validate_tool_input(input : Json, schema : Json) -> Array[String] {
let root_type = match schema_string_field(schema, "type") {
Some(t) => t
None => "object"
}
if root_type != "object" {
return ["Schema must be object type, got " + "\"" + root_type + "\""]
}
validate_tool_input_internal(input, schema, "")
}
///|
pub fn validate_params(input : Json, schema : Json) -> Array[String] {
validate_tool_input(input, schema)
}
///|
fn validate_tool_input_internal(
value : Json,
schema : Json,
path : String,
) -> Array[String] {
let t = match schema_string_field(schema, "type") {
Some(type_) => type_
None => "object"
}
let label = if path.is_empty() { "parameter" } else { path }
match t {
"string" =>
match value {
String(s) => {
let errors : Array[String] = []
validate_enum(value, schema, label, errors)
validate_string_range(s, schema, label, errors)
errors
}
_ => [label + " should be string"]
}
"integer" =>
match value {
Number(n, ..) => {
if n != n.floor() {
return [label + " should be integer"]
}
let errors : Array[String] = []
validate_enum(value, schema, label, errors)
validate_number_range(n, schema, label, errors)
errors
}
_ => [label + " should be integer"]
}
"number" =>
match value {
Number(n, ..) => {
let errors : Array[String] = []
validate_enum(value, schema, label, errors)
validate_number_range(n, schema, label, errors)
errors
}
_ => [label + " should be number"]
}
"boolean" =>
if is_json_boolean(value) {
let errors : Array[String] = []
validate_enum(value, schema, label, errors)
errors
} else {
[label + " should be boolean"]
}
"object" =>
match value {
Object(map) => {
let errors : Array[String] = []
validate_enum(value, schema, label, errors)
validate_required_fields(map, schema, path, errors)
validate_object_fields(map, schema, path, errors)
errors
}
_ => [label + " should be object"]
}
"array" =>
match value {
Array(items) => {
let errors : Array[String] = []
validate_enum(value, schema, label, errors)
validate_array_items(items, schema, path, errors)
errors
}
_ => [label + " should be array"]
}
_ => []
}
}
///|
fn validate_enum(
value : Json,
schema : Json,
label : String,
errors : Array[String],
) -> Unit {
match schema_json_field(schema, "enum") {
Some(Array(items)) =>
if !json_array_contains(items, value) {
errors.push(label + " must be one of " + Json::array(items).stringify())
}
_ => ()
}
}
///|
fn validate_number_range(
n : Double,
schema : Json,
label : String,
errors : Array[String],
) -> Unit {
match schema_number_field(schema, "minimum") {
Some(minimum) =>
if n < minimum {
errors.push(label + " must be >= " + format_number(minimum))
}
None => ()
}
match schema_number_field(schema, "maximum") {
Some(maximum) =>
if n > maximum {
errors.push(label + " must be <= " + format_number(maximum))
}
None => ()
}
}
///|
fn validate_string_range(
s : String,
schema : Json,
label : String,
errors : Array[String],
) -> Unit {
match schema_int_field(schema, "minLength") {
Some(min_len) =>
if s.length() < min_len {
errors.push(
label + " must be at least " + min_len.to_string() + " chars",
)
}
None => ()
}
match schema_int_field(schema, "maxLength") {
Some(max_len) =>
if s.length() > max_len {
errors.push(
label + " must be at most " + max_len.to_string() + " chars",
)
}
None => ()
}
}
///|
fn validate_required_fields(
map : Map[String, Json],
schema : Json,
path : String,
errors : Array[String],
) -> Unit {
match schema_json_field(schema, "required") {
Some(Array(required_list)) =>
for required in required_list {
match required {
String(field_name) =>
if !map.contains(field_name) {
errors.push("missing required " + dot_path(path, field_name))
}
_ => ()
}
}
_ => ()
}
}
///|
fn validate_object_fields(
map : Map[String, Json],
schema : Json,
path : String,
errors : Array[String],
) -> Unit {
let props = match schema_object_field(schema, "properties") {
Some(p) => p
None => Map([])
}
for entry in map.to_array() {
let key = entry.0
let val = entry.1
match props.get(key) {
Some(prop_schema) =>
errors.append(
validate_tool_input_internal(val, prop_schema, dot_path(path, key)),
)
None => ()
}
}
}
///|
fn validate_array_items(
items : Array[Json],
schema : Json,
path : String,
errors : Array[String],
) -> Unit {
match schema_json_field(schema, "items") {
Some(item_schema) =>
for i, item in items {
errors.append(
validate_tool_input_internal(item, item_schema, index_path(path, i)),
)
}
None => ()
}
}
///|
fn schema_json_field(obj : Json, key : String) -> Json? {
match obj {
Object(map) => map.get(key)
_ => None
}
}
///|
fn schema_object_field(obj : Json, key : String) -> Map[String, Json]? {
match schema_json_field(obj, key) {
Some(Object(map)) => Some(map)
_ => None
}
}
///|
fn schema_string_field(obj : Json, key : String) -> String? {
match schema_json_field(obj, key) {
Some(String(s)) => Some(s)
_ => None
}
}
///|
fn schema_number_field(obj : Json, key : String) -> Double? {
match schema_json_field(obj, key) {
Some(Number(n, ..)) => Some(n)
_ => None
}
}
///|
fn schema_int_field(obj : Json, key : String) -> Int? {
match schema_number_field(obj, key) {
Some(n) => Some(n.to_int())
None => None
}
}
///|
fn is_json_boolean(j : Json) -> Bool {
match j {
True => true
False => true
_ => false
}
}
///|
fn json_array_contains(items : Array[Json], value : Json) -> Bool {
for item in items {
if item == value {
return true
}
}
false
}
///|
fn format_number(n : Double) -> String {
if n == n.floor() {
n.to_int().to_string()
} else {
n.to_string()
}
}
///|
fn dot_path(path : String, key : String) -> String {
if path.is_empty() {
key
} else {
path + "." + key
}
}
///|
fn index_path(path : String, index : Int) -> String {
if path.is_empty() {
"[" + index.to_string() + "]"
} else {
path + "[" + index.to_string() + "]"
}
}