jsonchema.inferFromObject syscall
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { inferFromObject } from "./jsonschema.ts";
|
||||
|
||||
test("inferFromObject maps primitive field types", () => {
|
||||
const schema = inferFromObject({
|
||||
name: "Foo",
|
||||
count: 3,
|
||||
ratio: 1.5,
|
||||
done: false,
|
||||
missing: null,
|
||||
});
|
||||
expect(schema.type).toBe("object");
|
||||
expect(schema["x-inferred"]).toBe(true);
|
||||
expect(schema.properties.name).toEqual({ type: "string" });
|
||||
expect(schema.properties.count).toEqual({ type: "integer" });
|
||||
expect(schema.properties.ratio).toEqual({ type: "number" });
|
||||
expect(schema.properties.done).toEqual({ type: "boolean" });
|
||||
expect(schema.properties.missing).toEqual({ type: "null" });
|
||||
});
|
||||
|
||||
test("inferFromObject recurses into arrays and nested objects", () => {
|
||||
const schema = inferFromObject({
|
||||
tags: ["a", "b"],
|
||||
meta: { owner: "z", weight: 2 },
|
||||
empty: [],
|
||||
});
|
||||
expect(schema.properties.tags).toEqual({
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
});
|
||||
expect(schema.properties.meta).toEqual({
|
||||
type: "object",
|
||||
properties: { owner: { type: "string" }, weight: { type: "integer" } },
|
||||
});
|
||||
// An empty array has no element to learn `items` from.
|
||||
expect(schema.properties.empty).toEqual({ type: "array" });
|
||||
});
|
||||
|
||||
test("inferFromObject handles a non-object top-level value", () => {
|
||||
const schema = inferFromObject("hello");
|
||||
expect(schema.type).toBe("string");
|
||||
expect(schema["x-inferred"]).toBe(true);
|
||||
});
|
||||
@@ -75,6 +75,53 @@ export function validateSchema(schema: any): undefined | string {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort: infer a JSON Schema (draft 2020-12) from the *shape* of a single
|
||||
* sample value. Types are guessed from one example, so the result is a hint,
|
||||
* not a contract — the top-level schema is marked `"x-inferred": true`.
|
||||
*
|
||||
* Useful when a tag/object type has no declared schema but an example object
|
||||
* exists and you want a plausible schema for it.
|
||||
*/
|
||||
export function inferFromObject(value: any): any {
|
||||
const schema = inferSchemaNode(value);
|
||||
schema["$schema"] = "https://json-schema.org/draft/2020-12/schema";
|
||||
schema["x-inferred"] = true;
|
||||
return schema;
|
||||
}
|
||||
|
||||
/** Infer a bare JSON Schema node for a value, recursing into arrays/objects. */
|
||||
function inferSchemaNode(value: any): any {
|
||||
if (value === null || value === undefined) {
|
||||
return { type: "null" };
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const node: any = { type: "array" };
|
||||
if (value.length > 0) {
|
||||
node.items = inferSchemaNode(value[0]);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
switch (typeof value) {
|
||||
case "boolean":
|
||||
return { type: "boolean" };
|
||||
case "number":
|
||||
return { type: Number.isInteger(value) ? "integer" : "number" };
|
||||
case "string":
|
||||
return { type: "string" };
|
||||
case "object": {
|
||||
const properties: Record<string, any> = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
properties[k] = inferSchemaNode(v);
|
||||
}
|
||||
return { type: "object", properties };
|
||||
}
|
||||
default:
|
||||
// functions, symbols, bigint, … — not representable in JSON Schema.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function jsonschemaSyscalls(): SysCallMapping {
|
||||
return {
|
||||
"jsonschema.validateObject": (
|
||||
@@ -87,5 +134,8 @@ export function jsonschemaSyscalls(): SysCallMapping {
|
||||
"jsonschema.validateSchema": (_ctx, schema: any): undefined | string => {
|
||||
return validateSchema(schema);
|
||||
},
|
||||
"jsonschema.inferFromObject": (_ctx, object: any): any => {
|
||||
return inferFromObject(object);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,18 @@ else
|
||||
end
|
||||
```
|
||||
|
||||
### jsonschema.inferFromObject(object)
|
||||
Infers a best-effort JSON schema from the *shape* of a single sample value. Types are guessed from one example, so the result is a hint rather than a contract — the returned schema is marked with `"x-inferred": true`. Useful when an object type has no declared schema but you have an example to learn from.
|
||||
|
||||
Example:
|
||||
```lua
|
||||
local sample = { name = "Widget", count = 3, tags = { "a", "b" } }
|
||||
local schema = jsonschema.inferFromObject(sample)
|
||||
-- schema.properties.name.type == "string"
|
||||
-- schema.properties.count.type == "integer"
|
||||
-- schema.properties.tags.type == "array" (items.type == "string")
|
||||
```
|
||||
|
||||
### jsonschema.validateSchema(schema)
|
||||
Validates a JSON schema itself to ensure it's well-formed.
|
||||
|
||||
|
||||
@@ -21,3 +21,12 @@ export function validateObject(
|
||||
export function validateSchema(schema: any): Promise<string | undefined> {
|
||||
return syscall("jsonschema.validateSchema", schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Infers a best-effort JSON schema from a sample value's shape.
|
||||
* @param object the sample value to infer a schema from
|
||||
* @returns an inferred JSON schema (draft 2020-12), marked `x-inferred`
|
||||
*/
|
||||
export function inferFromObject(object: any): Promise<any> {
|
||||
return syscall("jsonschema.inferFromObject", object);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user