diff --git a/runtime/include/hemlock_runtime.h b/runtime/include/hemlock_runtime.h index b2242040..97498be2 100644 --- a/runtime/include/hemlock_runtime.h +++ b/runtime/include/hemlock_runtime.h @@ -111,6 +111,9 @@ HmlValue hml_convert_to_type(HmlValue val, HmlValueType target_type); // Type conversion that allows string parsing (for type constructors like i32("42")) HmlValue hml_parse_string_to_type(HmlValue val, HmlValueType target_type); +// Schema extraction - returns JSON Schema-compatible object from type registry +HmlValue hml_schema(HmlValue type_name); + // Assertions void hml_assert(HmlValue condition, HmlValue message); __attribute__((noreturn)) void hml_panic(HmlValue message); diff --git a/runtime/src/builtins_types.c b/runtime/src/builtins_types.c index 50fa6dbc..b9750da1 100644 --- a/runtime/src/builtins_types.c +++ b/runtime/src/builtins_types.c @@ -225,5 +225,135 @@ HmlValue hml_validate_enum_value(HmlValue val, const char *enum_name) { exit(1); } +// ========== SCHEMA EXTRACTION ========== + +// Map HML_VAL_* type kind to JSON Schema type string +static const char* hml_type_kind_to_json_schema_type(int type_kind) { + switch (type_kind) { + case HML_VAL_I8: + case HML_VAL_I16: + case HML_VAL_I32: + case HML_VAL_I64: + case HML_VAL_U8: + case HML_VAL_U16: + case HML_VAL_U32: + case HML_VAL_U64: + return "integer"; + case HML_VAL_F32: + case HML_VAL_F64: + return "number"; + case HML_VAL_BOOL: + return "boolean"; + case HML_VAL_STRING: + return "string"; + case HML_VAL_ARRAY: + return "array"; + case HML_VAL_OBJECT: + return "object"; + case HML_VAL_NULL: + return "null"; + default: + return "any"; + } +} + +// Map HML_VAL_* type kind to Hemlock type name string +static const char* hml_type_kind_to_hemlock_type(int type_kind) { + switch (type_kind) { + case HML_VAL_I8: return "i8"; + case HML_VAL_I16: return "i16"; + case HML_VAL_I32: return "i32"; + case HML_VAL_I64: return "i64"; + case HML_VAL_U8: return "u8"; + case HML_VAL_U16: return "u16"; + case HML_VAL_U32: return "u32"; + case HML_VAL_U64: return "u64"; + case HML_VAL_F32: return "f32"; + case HML_VAL_F64: return "f64"; + case HML_VAL_BOOL: return "bool"; + case HML_VAL_STRING: return "string"; + case HML_VAL_RUNE: return "rune"; + case HML_VAL_PTR: return "ptr"; + case HML_VAL_BUFFER: return "buffer"; + case HML_VAL_ARRAY: return "array"; + case HML_VAL_OBJECT: return "object"; + case HML_VAL_NULL: return "null"; + default: return "any"; + } +} + +// hml_schema(type_name) -> object +// Extracts a JSON Schema-compatible object from the runtime type registry. +HmlValue hml_schema(HmlValue type_name_val) { + if (type_name_val.type != HML_VAL_STRING) { + hml_runtime_error("schema() argument must be a string"); + } + + const char *type_name = type_name_val.as.as_string->data; + HmlTypeDef *type = hml_lookup_type(type_name); + + if (!type) { + fprintf(stderr, "Runtime error: schema() unknown type '%s'\n", type_name); + exit(1); + } + + // Build schema object: { type: "object", name: "TypeName", properties: {...}, required: [...] } + HmlValue schema = hml_val_object(); + + // schema.type = "object" + hml_object_set_field(schema, "type", hml_val_string("object")); + + // schema.name = type_name + hml_object_set_field(schema, "name", hml_val_string(type_name)); + + // Build properties object + HmlValue properties = hml_val_object(); + + // Build required array + HmlValue required = hml_val_array(); + + for (int i = 0; i < type->num_fields; i++) { + HmlTypeField *field = &type->fields[i]; + + // Build property descriptor + HmlValue prop = hml_val_object(); + + // JSON Schema type + const char *json_type = (field->type_kind >= 0) + ? hml_type_kind_to_json_schema_type(field->type_kind) + : "any"; + hml_object_set_field(prop, "type", hml_val_string(json_type)); + + // Hemlock type + const char *hemlock_type = (field->type_kind >= 0) + ? hml_type_kind_to_hemlock_type(field->type_kind) + : "any"; + hml_object_set_field(prop, "hemlock_type", hml_val_string(hemlock_type)); + + // Required flag + hml_object_set_field(prop, "required", hml_val_bool(!field->is_optional)); + + // Add property to properties + hml_object_set_field(properties, field->name, prop); + hml_release(&prop); + + // Add to required array if not optional + if (!field->is_optional) { + HmlValue req_name = hml_val_string(field->name); + hml_array_push(required, req_name); + hml_release(&req_name); + } + } + + // Set properties and required on schema + hml_object_set_field(schema, "properties", properties); + hml_release(&properties); + + hml_object_set_field(schema, "required", required); + hml_release(&required); + + return schema; +} + // FFI (Foreign Function Interface) operations moved to builtins_ffi.c diff --git a/src/backends/compiler/codegen_call.c b/src/backends/compiler/codegen_call.c index 5c6e9824..93703ed4 100644 --- a/src/backends/compiler/codegen_call.c +++ b/src/backends/compiler/codegen_call.c @@ -175,6 +175,15 @@ char* codegen_expr_call(CodegenContext *ctx, Expr *expr, char *result) { return result; } + // Handle schema builtin - extract JSON Schema from type registry + if (strcmp(fn_name, "schema") == 0 && expr->as.call.num_args == 1) { + char *arg = codegen_expr(ctx, expr->as.call.args[0]); + codegen_writeln(ctx, "HmlValue %s = hml_schema(%s);", result, arg); + codegen_writeln(ctx, "hml_release(&%s);", arg); + free(arg); + return result; + } + // Handle get_stack_limit builtin if (strcmp(fn_name, "get_stack_limit") == 0 && expr->as.call.num_args == 0) { codegen_writeln(ctx, "HmlValue %s = hml_get_stack_limit();", result); diff --git a/src/backends/compiler/type_check.c b/src/backends/compiler/type_check.c index 64cadf3c..e580e623 100644 --- a/src/backends/compiler/type_check.c +++ b/src/backends/compiler/type_check.c @@ -1232,6 +1232,7 @@ CheckedType* type_check_infer_expr(TypeCheckContext *ctx, Expr *expr) { if (strcmp(name, "open") == 0) return checked_type_primitive(CHECKED_FILE); if (strcmp(name, "channel") == 0) return checked_type_primitive(CHECKED_CHANNEL); if (strcmp(name, "spawn") == 0) return checked_type_primitive(CHECKED_TASK); + if (strcmp(name, "schema") == 0) return checked_type_primitive(CHECKED_OBJECT); if (strcmp(name, "read_line") == 0) { CheckedType *t = checked_type_primitive(CHECKED_STRING); t->nullable = 1; diff --git a/src/backends/interpreter/builtins/internal.h b/src/backends/interpreter/builtins/internal.h index a2fbb891..6df86e23 100644 --- a/src/backends/interpreter/builtins/internal.h +++ b/src/backends/interpreter/builtins/internal.h @@ -316,6 +316,9 @@ Value builtin_atomic_exchange_i64(Value *args, int num_args, ExecutionContext *c // Memory fence Value builtin_atomic_fence(Value *args, int num_args, ExecutionContext *ctx); +// Schema builtin (schema.c) +Value builtin_schema(Value *args, int num_args, ExecutionContext *ctx); + // Regex builtins (regex.c) Value builtin_regex_compile(Value *args, int num_args, ExecutionContext *ctx); Value builtin_regex_test(Value *args, int num_args, ExecutionContext *ctx); diff --git a/src/backends/interpreter/builtins/registration.c b/src/backends/interpreter/builtins/registration.c index 9455deba..412b2eea 100644 --- a/src/backends/interpreter/builtins/registration.c +++ b/src/backends/interpreter/builtins/registration.c @@ -295,6 +295,8 @@ static BuiltinInfo builtins[] = { {"atomic_exchange_i64", builtin_atomic_exchange_i64}, // Memory fence {"atomic_fence", builtin_atomic_fence}, + // Schema extraction for JSON validation + {"schema", builtin_schema}, {NULL, NULL} // Sentinel }; diff --git a/src/backends/interpreter/builtins/schema.c b/src/backends/interpreter/builtins/schema.c new file mode 100644 index 00000000..81fa8bbf --- /dev/null +++ b/src/backends/interpreter/builtins/schema.c @@ -0,0 +1,197 @@ +/* + * Hemlock Built-in: schema() + * + * Extracts a JSON Schema-compatible object from a registered define type. + * This enables validation of JSON data against Hemlock's structural types. + * + * Usage: + * define User { name: string, age: i32, email?: string } + * let s = schema("User"); + * // Returns: { + * // type: "object", + * // name: "User", + * // properties: { + * // name: { type: "string", required: true }, + * // age: { type: "integer", required: true }, + * // email: { type: "string", required: false } + * // }, + * // required: ["name", "age"] + * // } + */ + +#include "internal.h" + +// Map a Hemlock TypeKind to a JSON Schema type string +static const char* type_kind_to_json_schema_type(TypeKind kind) { + switch (kind) { + case TYPE_I8: + case TYPE_I16: + case TYPE_I32: + case TYPE_I64: + case TYPE_U8: + case TYPE_U16: + case TYPE_U32: + case TYPE_U64: + return "integer"; + case TYPE_F32: + case TYPE_F64: + return "number"; + case TYPE_BOOL: + return "boolean"; + case TYPE_STRING: + return "string"; + case TYPE_ARRAY: + return "array"; + case TYPE_GENERIC_OBJECT: + case TYPE_CUSTOM_OBJECT: + return "object"; + case TYPE_NULL: + return "null"; + default: + return "any"; + } +} + +// Map a Hemlock TypeKind to its Hemlock type name string +static const char* type_kind_to_hemlock_type(TypeKind kind) { + switch (kind) { + case TYPE_I8: return "i8"; + case TYPE_I16: return "i16"; + case TYPE_I32: return "i32"; + case TYPE_I64: return "i64"; + case TYPE_U8: return "u8"; + case TYPE_U16: return "u16"; + case TYPE_U32: return "u32"; + case TYPE_U64: return "u64"; + case TYPE_F32: return "f32"; + case TYPE_F64: return "f64"; + case TYPE_BOOL: return "bool"; + case TYPE_STRING: return "string"; + case TYPE_RUNE: return "rune"; + case TYPE_PTR: return "ptr"; + case TYPE_BUFFER: return "buffer"; + case TYPE_ARRAY: return "array"; + case TYPE_NULL: return "null"; + case TYPE_GENERIC_OBJECT: return "object"; + default: return "any"; + } +} + +// schema(type_name: string) -> object +// Extracts a JSON Schema-compatible object from the type registry. +Value builtin_schema(Value *args, int num_args, ExecutionContext *ctx) { + if (num_args != 1) { + fprintf(stderr, "Runtime error: schema() expects 1 argument (type name)\n"); + exit(1); + } + + if (args[0].type != VAL_STRING) { + fprintf(stderr, "Runtime error: schema() argument must be a string\n"); + exit(1); + } + + const char *type_name = args[0].as.as_string->data; + ObjectType *obj_type = lookup_object_type(type_name); + + if (!obj_type) { + fprintf(stderr, "Runtime error: schema() unknown type '%s'\n", type_name); + exit(1); + } + + // Build the schema object + // Top-level: { type: "object", name: "TypeName", properties: {...}, required: [...] } + int num_top_fields = 4; // type, name, properties, required + Object *schema = object_new(NULL, num_top_fields); + + // schema.type = "object" + Value type_val = val_string("object"); + schema->fields[schema->num_fields].name = strdup("type"); + schema->fields[schema->num_fields].value = type_val; + schema->num_fields++; + + // schema.name = type_name + Value name_val = val_string(type_name); + schema->fields[schema->num_fields].name = strdup("name"); + schema->fields[schema->num_fields].value = name_val; + schema->num_fields++; + + // Build properties object + int num_fields = obj_type->num_fields; + Object *properties = object_new(NULL, num_fields > 0 ? num_fields : 1); + + // Build required array + Array *required = array_new(); + + for (int i = 0; i < num_fields; i++) { + const char *field_name = obj_type->field_names[i]; + Type *field_type = obj_type->field_types[i]; + int is_optional = obj_type->field_optional[i]; + + // Build property descriptor: { type: "string", hemlock_type: "string", required: true/false } + int prop_field_count = 3; + if (field_type && field_type->kind == TYPE_CUSTOM_OBJECT && field_type->type_name) { + prop_field_count = 4; // Add ref field for custom object types + } + Object *prop = object_new(NULL, prop_field_count); + + // JSON Schema type + const char *json_type = "any"; + const char *hemlock_type = "any"; + if (field_type) { + json_type = type_kind_to_json_schema_type(field_type->kind); + if (field_type->kind == TYPE_CUSTOM_OBJECT && field_type->type_name) { + hemlock_type = field_type->type_name; + } else { + hemlock_type = type_kind_to_hemlock_type(field_type->kind); + } + } + + Value prop_type_val = val_string(json_type); + prop->fields[prop->num_fields].name = strdup("type"); + prop->fields[prop->num_fields].value = prop_type_val; + prop->num_fields++; + + Value prop_hml_type_val = val_string(hemlock_type); + prop->fields[prop->num_fields].name = strdup("hemlock_type"); + prop->fields[prop->num_fields].value = prop_hml_type_val; + prop->num_fields++; + + Value prop_required_val = val_bool(!is_optional); + prop->fields[prop->num_fields].name = strdup("required"); + prop->fields[prop->num_fields].value = prop_required_val; + prop->num_fields++; + + // For custom object types, add a $ref field + if (field_type && field_type->kind == TYPE_CUSTOM_OBJECT && field_type->type_name) { + Value ref_val = val_string(field_type->type_name); + prop->fields[prop->num_fields].name = strdup("$ref"); + prop->fields[prop->num_fields].value = ref_val; + prop->num_fields++; + } + + // Add property to properties object + properties->fields[properties->num_fields].name = strdup(field_name); + properties->fields[properties->num_fields].value = val_object(prop); + properties->num_fields++; + + // Add to required array if not optional + if (!is_optional) { + Value req_name = val_string(field_name); + array_push(required, req_name); + value_release(req_name); // array_push retains + } + } + + // schema.properties = properties + schema->fields[schema->num_fields].name = strdup("properties"); + schema->fields[schema->num_fields].value = val_object(properties); + schema->num_fields++; + + // schema.required = required + // Note: the field takes ownership of the ref_count=1 reference from array_new() + schema->fields[schema->num_fields].name = strdup("required"); + schema->fields[schema->num_fields].value = val_array(required); + schema->num_fields++; + + return val_object(schema); +} diff --git a/stdlib/docs/json.md b/stdlib/docs/json.md index ec5cb58a..b3bf46ea 100644 --- a/stdlib/docs/json.md +++ b/stdlib/docs/json.md @@ -645,22 +645,153 @@ try { } ``` +## JSON Schema Validation + +Hemlock's `define` types can be used for JSON schema validation. The built-in `schema()` function extracts a JSON Schema-compatible object from any registered `define` type, and the `@stdlib/json` module provides validation functions. + +### `to_json_schema(type_name: string)` + +Generate a JSON Schema-compatible object from a registered `define` type. + +```hemlock +import { to_json_schema } from "@stdlib/json"; + +define User { + name: string, + age: i32, + email?: string +} + +let user_schema = to_json_schema("User"); +// Returns: +// { +// type: "object", +// name: "User", +// properties: { +// name: { type: "string", hemlock_type: "string", required: true }, +// age: { type: "integer", hemlock_type: "i32", required: true }, +// email: { type: "string", hemlock_type: "string", required: false } +// }, +// required: ["name", "age"] +// } +``` + +**Parameters:** +- `type_name` (string) - Name of a registered `define` type + +**Returns:** Schema object with `type`, `name`, `properties`, and `required` fields + +**Throws:** Runtime error if type name is not found + +--- + +### `validate_schema(data, schema_obj)` + +Validate data against a JSON schema object. Returns a result object instead of throwing. + +```hemlock +import { validate_schema, to_json_schema } from "@stdlib/json"; + +define User { name: string, age: i32 } +let schema = to_json_schema("User"); + +// Valid data +let result = validate_schema({ name: "Alice", age: 30 }, schema); +print(result.valid); // true +print(result.errors); // [] + +// Missing required field +let result2 = validate_schema({ name: "Bob" }, schema); +print(result2.valid); // false +print(result2.errors); // ["Missing required field 'age'"] + +// Wrong field type +let result3 = validate_schema({ name: 123, age: 30 }, schema); +print(result3.valid); // false +print(result3.errors); // ["Field 'name' expected type 'string', got 'i32'"] +``` + +**Parameters:** +- `data` - Value to validate +- `schema_obj` (object) - Schema object (from `to_json_schema()`) + +**Returns:** `{ valid: bool, errors: array }` + +--- + +### `validate_type(data, type_name: string)` + +Convenience function that combines `to_json_schema()` and `validate_schema()`. + +```hemlock +import { validate_type } from "@stdlib/json"; + +define Product { id: i32, title: string, price: f64 } + +let data = { id: 1, title: "Widget", price: 9.99 }; +let result = validate_type(data, "Product"); +print(result.valid); // true +``` + +**Parameters:** +- `data` - Value to validate +- `type_name` (string) - Name of a registered `define` type + +**Returns:** `{ valid: bool, errors: array }` + +--- + +### Built-in: `schema(type_name: string)` + +Low-level built-in function that extracts schema metadata from the type registry. Typically used via `to_json_schema()` from `@stdlib/json`. + +```hemlock +// Direct usage (no import needed) +define Point { x: f64, y: f64 } +let s = schema("Point"); +print(s.properties.x.type); // "number" +``` + +### Schema Property Descriptors + +Each property in the schema's `properties` object contains: + +| Field | Type | Description | +|-------|------|-------------| +| `type` | string | JSON Schema type: `"string"`, `"integer"`, `"number"`, `"boolean"`, `"array"`, `"object"`, `"null"`, `"any"` | +| `hemlock_type` | string | Hemlock type name: `"i32"`, `"f64"`, `"string"`, etc. | +| `required` | bool | `true` if field is required, `false` if optional | +| `$ref` | string | (Only for custom object types) Name of the referenced `define` type | + +### Validation Rules + +- **Missing required fields** produce errors +- **Missing optional fields** are accepted +- **Extra fields** are accepted (duck typing) +- **Type checking** validates against the JSON Schema type category: + - `"integer"` accepts: i8, i16, i32, i64, u8, u16, u32, u64 + - `"number"` accepts: all integer types plus f32, f64 + - `"string"` accepts: string + - `"boolean"` accepts: bool + - `"array"` accepts: array + - `"object"` accepts: object + - `"any"` accepts: all types + ## Current Limitations 1. **No object iteration builtin** - `merge()`, `patch()`, and object `equals()` not yet implemented 2. **No line numbers in parse errors** - Validation doesn't report exact error location -3. **No JSON Schema** - Schema validation not yet supported -4. **No streaming** - Large files must fit in memory +3. **No streaming** - Large files must fit in memory ## Future Enhancements Planned additions: - Object iteration support (enables merge/patch/equals) -- JSON Schema validation - JSON Patch (RFC 6902) - JSON Pointer (RFC 6901) - Streaming parser for large files - JSON5 support (comments, trailing commas) +- Nested schema validation (recursive `$ref` resolution) ## See Also diff --git a/stdlib/json.hml b/stdlib/json.hml index f5c1a594..d8b1d02b 100644 --- a/stdlib/json.hml +++ b/stdlib/json.hml @@ -629,6 +629,165 @@ fn equals(a, b): bool { return false; } +// ============================================================================ +// JSON Schema Validation +// ============================================================================ + +// Generate a JSON Schema from a registered define type. +// Uses the built-in schema() function to extract type metadata. +// +// Usage: +// define User { name: string, age: i32, email?: string } +// let user_schema = to_json_schema("User"); +fn to_json_schema(type_name: string) { + return schema(type_name); +} + +// Validate data against a JSON schema object (as returned by to_json_schema()). +// Returns: { valid: bool, errors: array } +// +// Usage: +// define User { name: string, age: i32 } +// let s = to_json_schema("User"); +// let result = validate_schema(data, s); +// if (!result.valid) { print(result.errors); } +fn validate_schema(data, schema_obj) { + let errors = []; + + // Schema must be an object + if (typeof(schema_obj) != "object") { + errors.push("Schema must be an object"); + return { valid: false, errors }; + } + + let schema_type = schema_obj["type"] ?? "any"; + + // Top-level type check + if (schema_type == "object") { + if (typeof(data) != "object") { + errors.push("Expected object, got " + typeof(data)); + return { valid: false, errors }; + } + + let properties = schema_obj["properties"]; + let required = schema_obj["required"]; + + if (properties != null) { + // Check required fields + if (required != null) { + for (let i = 0; i < required.length; i++) { + let field_name = required[i]; + if (!data.has(field_name)) { + errors.push("Missing required field '" + field_name + "'"); + } + } + } + + // Check field types + let prop_keys = properties.keys(); + for (let i = 0; i < prop_keys.length; i++) { + let field_name = prop_keys[i]; + let field_schema = properties[field_name]; + let field_type = field_schema["type"] ?? "any"; + let hemlock_type = field_schema["hemlock_type"] ?? "any"; + let field_required = field_schema["required"] ?? true; + + if (data.has(field_name)) { + let value = data[field_name]; + let value_type = typeof(value); + + if (!check_type_match(value, value_type, field_type, hemlock_type)) { + errors.push("Field '" + field_name + "' expected type '" + hemlock_type + "', got '" + value_type + "'"); + } + } + // Missing optional fields are OK (already checked required above) + } + } + } else if (schema_type == "array") { + if (typeof(data) != "array") { + errors.push("Expected array, got " + typeof(data)); + } + } else if (schema_type == "string") { + if (typeof(data) != "string") { + errors.push("Expected string, got " + typeof(data)); + } + } else if (schema_type == "integer") { + if (!is_number(data)) { + errors.push("Expected integer, got " + typeof(data)); + } + } else if (schema_type == "number") { + if (!is_number(data)) { + errors.push("Expected number, got " + typeof(data)); + } + } else if (schema_type == "boolean") { + if (typeof(data) != "bool") { + errors.push("Expected boolean, got " + typeof(data)); + } + } else if (schema_type == "null") { + if (typeof(data) != "null") { + errors.push("Expected null, got " + typeof(data)); + } + } + // "any" accepts everything + + return { valid: errors.length == 0, errors }; +} + +// Internal: Check if a value matches the expected type. +fn check_type_match(value, value_type: string, json_type: string, hemlock_type: string): bool { + // "any" matches everything + if (json_type == "any" || hemlock_type == "any") { + return true; + } + + if (json_type == "string") { + return value_type == "string"; + } + + if (json_type == "boolean") { + return value_type == "bool"; + } + + if (json_type == "null") { + return value_type == "null"; + } + + if (json_type == "array") { + return value_type == "array"; + } + + if (json_type == "object") { + return value_type == "object"; + } + + if (json_type == "integer") { + // Accept any integer type + return value_type == "i8" || value_type == "i16" || value_type == "i32" || value_type == "i64" || + value_type == "u8" || value_type == "u16" || value_type == "u32" || value_type == "u64"; + } + + if (json_type == "number") { + // Accept any numeric type + return value_type == "f32" || value_type == "f64" || + value_type == "i8" || value_type == "i16" || value_type == "i32" || value_type == "i64" || + value_type == "u8" || value_type == "u16" || value_type == "u32" || value_type == "u64"; + } + + return false; +} + +// Validate parsed JSON data against a define type by name. +// Convenience function combining to_json_schema() and validate_schema(). +// +// Usage: +// define User { name: string, age: i32 } +// let data = parse('{"name": "Alice", "age": 30}'); +// let result = validate_type(data, "User"); +fn validate_type(data, type_name: string) { + let s = to_json_schema(type_name); + return validate_schema(data, s); +} + // ============================================================================ // Export Note // ============================================================================ diff --git a/tests/parity/builtins/schema.expected b/tests/parity/builtins/schema.expected new file mode 100644 index 00000000..926b0682 --- /dev/null +++ b/tests/parity/builtins/schema.expected @@ -0,0 +1,19 @@ +object +Person +2 +name +age +string +string +true +integer +i32 +true +false +Point +2 +number +f64 +1 +host +done diff --git a/tests/parity/builtins/schema.hml b/tests/parity/builtins/schema.hml new file mode 100644 index 00000000..65fdc631 --- /dev/null +++ b/tests/parity/builtins/schema.hml @@ -0,0 +1,61 @@ +// Test schema() built-in for JSON Schema extraction from define types + +define Person { + name: string, + age: i32, + active?: true +} + +define Point { + x: f64, + y: f64 +} + +define Config { + host: string, + port?: 8080, + debug?: false +} + +// Test basic schema extraction +let s = schema("Person"); +print(s["type"]); +print(s["name"]); + +// Test required fields +let req = s["required"]; +print(req.length); +print(req[0]); +print(req[1]); + +// Test properties +let props = s["properties"]; +let name_prop = props["name"]; +print(name_prop["type"]); +print(name_prop["hemlock_type"]); +print(name_prop["required"]); + +let age_prop = props["age"]; +print(age_prop["type"]); +print(age_prop["hemlock_type"]); +print(age_prop["required"]); + +let active_prop = props["active"]; +print(active_prop["required"]); + +// Test Point schema +let ps = schema("Point"); +print(ps["name"]); +let preq = ps["required"]; +print(preq.length); +let px = ps["properties"]["x"]; +print(px["type"]); +print(px["hemlock_type"]); + +// Test Config schema with multiple optional fields +let cs = schema("Config"); +let creq = cs["required"]; +print(creq.length); +print(creq[0]); + +print("done"); diff --git a/tests/stdlib_json/schema_validation_test.hml b/tests/stdlib_json/schema_validation_test.hml new file mode 100644 index 00000000..611f05a7 --- /dev/null +++ b/tests/stdlib_json/schema_validation_test.hml @@ -0,0 +1,73 @@ +// Test JSON Schema validation using define types +import { validate_schema, validate_type, to_json_schema } from "@stdlib/json"; + +// Define types for validation +define User { + name: string, + age: i32, + email?: string +} + +define Product { + id: i32, + title: string, + price: f64, + in_stock?: true +} + +// Test 1: Generate schema from define type +let user_schema = to_json_schema("User"); +print("Schema type: " + user_schema["type"]); +print("Schema name: " + user_schema["name"]); +print("Required fields: " + user_schema["required"].length); + +// Test 2: Valid data passes validation +let valid_data = { name: "Alice", age: 30 }; +let result = validate_schema(valid_data, user_schema); +print("Valid user: " + result.valid); +print("Errors: " + result.errors.length); + +// Test 3: Valid data with optional field +let valid_full = { name: "Bob", age: 25, email: "bob@example.com" }; +let result2 = validate_schema(valid_full, user_schema); +print("Valid full user: " + result2.valid); + +// Test 4: Missing required field +let missing_age = { name: "Charlie" }; +let result3 = validate_schema(missing_age, user_schema); +print("Missing age valid: " + result3.valid); +print("Missing age errors: " + result3.errors.length); +print("Error: " + result3.errors[0]); + +// Test 5: Wrong field type +let wrong_type = { name: 123, age: 30 }; +let result4 = validate_schema(wrong_type, user_schema); +print("Wrong type valid: " + result4.valid); +print("Wrong type error: " + result4.errors[0]); + +// Test 6: Non-object data against object schema +let result5 = validate_schema("just a string", user_schema); +print("Not object valid: " + result5.valid); + +// Test 7: validate_type convenience function +let data7 = { name: "Diana", age: 28 }; +let result7 = validate_type(data7, "User"); +print("validate_type valid: " + result7.valid); + +// Test 8: validate_type with invalid data +let data8 = { title: "Widget" }; +let result8 = validate_type(data8, "User"); +print("validate_type invalid: " + result8.valid); + +// Test 9: Product schema validation +let product = { id: 1, title: "Widget", price: 9.99 }; +let result9 = validate_type(product, "Product"); +print("Valid product: " + result9.valid); + +// Test 10: Multiple errors +let bad_product = { title: 42 }; +let result10 = validate_type(bad_product, "Product"); +print("Bad product valid: " + result10.valid); +print("Bad product errors: " + result10.errors.length); + +print("done");