@json-render/core
Advanced tools
+39
-13
@@ -40,4 +40,2 @@ import { z } from 'zod'; | ||
| interface UIElement<T extends string = string, P = Record<string, unknown>> { | ||
| /** Unique key for reconciliation */ | ||
| key: string; | ||
| /** Component type from the catalog */ | ||
@@ -49,4 +47,2 @@ type: T; | ||
| children?: string[]; | ||
| /** Parent element key (null for root) */ | ||
| parentKey?: string | null; | ||
| /** Visibility condition */ | ||
@@ -56,2 +52,13 @@ visible?: VisibilityCondition; | ||
| /** | ||
| * Element with key and parentKey for use with flatToTree. | ||
| * When elements are in an array (not a keyed map), key and parentKey | ||
| * are needed to establish identity and parent-child relationships. | ||
| */ | ||
| interface FlatElement<T extends string = string, P = Record<string, unknown>> extends UIElement<T, P> { | ||
| /** Unique key identifying this element */ | ||
| key: string; | ||
| /** Parent element key (null for root) */ | ||
| parentKey?: string | null; | ||
| } | ||
| /** | ||
| * Visibility condition types | ||
@@ -117,7 +124,7 @@ */ | ||
| /** | ||
| * JSON patch operation types | ||
| * JSON patch operation types (RFC 6902) | ||
| */ | ||
| type PatchOp = "add" | "remove" | "replace" | "set"; | ||
| type PatchOp = "add" | "remove" | "replace" | "move" | "copy" | "test"; | ||
| /** | ||
| * JSON patch operation | ||
| * JSON patch operation (RFC 6902) | ||
| */ | ||
@@ -127,3 +134,6 @@ interface JsonPatch { | ||
| path: string; | ||
| /** Required for add, replace, test */ | ||
| value?: unknown; | ||
| /** Required for move, copy (source location) */ | ||
| from?: string; | ||
| } | ||
@@ -135,7 +145,7 @@ /** | ||
| /** | ||
| * Get a value from an object by JSON Pointer path | ||
| * Get a value from an object by JSON Pointer path (RFC 6901) | ||
| */ | ||
| declare function getByPath(obj: unknown, path: string): unknown; | ||
| /** | ||
| * Set a value in an object by JSON Pointer path. | ||
| * Set a value in an object by JSON Pointer path (RFC 6901). | ||
| * Automatically creates arrays when the path segment is a numeric index. | ||
@@ -145,2 +155,14 @@ */ | ||
| /** | ||
| * Add a value per RFC 6902 "add" semantics. | ||
| * For objects: create-or-replace the member. | ||
| * For arrays: insert before the given index, or append if "-". | ||
| */ | ||
| declare function addByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Remove a value per RFC 6902 "remove" semantics. | ||
| * For objects: delete the property. | ||
| * For arrays: splice out the element at the given index. | ||
| */ | ||
| declare function removeByPath(obj: Record<string, unknown>, path: string): void; | ||
| /** | ||
| * Find a form value from params and/or data. | ||
@@ -175,4 +197,8 @@ * Useful in action handlers to locate form input values regardless of path format. | ||
| /** | ||
| * Apply a single SpecStream patch to an object. | ||
| * Apply a single RFC 6902 JSON Patch operation to an object. | ||
| * Mutates the object in place. | ||
| * | ||
| * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test. | ||
| * | ||
| * @throws {Error} If a "test" operation fails (value mismatch). | ||
| */ | ||
@@ -185,4 +211,4 @@ declare function applySpecStreamPatch<T extends Record<string, unknown>>(obj: T, patch: SpecStreamLine): T; | ||
| * @example | ||
| * const stream = `{"op":"set","path":"/name","value":"Alice"} | ||
| * {"op":"set","path":"/age","value":30}`; | ||
| * const stream = `{"op":"add","path":"/name","value":"Alice"} | ||
| * {"op":"add","path":"/age","value":30}`; | ||
| * const result = compileSpecStream(stream); | ||
@@ -886,2 +912,2 @@ * // { name: "Alice", age: 30 } | ||
| export { type Action, type ActionConfirm, ActionConfirmSchema, type ActionDefinition, type ActionExecutionContext, type ActionHandler, type ActionOnError, ActionOnErrorSchema, type ActionOnSuccess, ActionOnSuccessSchema, ActionSchema, type AuthState, type Catalog$1 as Catalog, type CatalogConfig, type ComponentDefinition, type ComponentSchema, type DataModel, type DynamicBoolean, DynamicBooleanSchema, type DynamicNumber, DynamicNumberSchema, type DynamicString, DynamicStringSchema, type DynamicValue, DynamicValueSchema, type InferActionParams, type InferCatalogActions, type InferCatalogComponentProps, type InferCatalogComponents, type InferCatalogInput, type InferComponentProps, type InferSpec, type JsonPatch, type Catalog as LegacyCatalog, type LogicExpression, LogicExpressionSchema, type PatchOp, type PromptContext, type PromptOptions, type PromptTemplate, type ResolvedAction, type Schema, type SchemaBuilder, type SchemaDefinition, type SchemaOptions, type SchemaType, type Spec, type SpecStreamCompiler, type SpecStreamLine, type SpecValidationResult, type SystemPromptOptions, type UIElement, type ValidationCheck, type ValidationCheckResult, ValidationCheckSchema, type ValidationConfig, ValidationConfigSchema, type ValidationContext, type ValidationFunction, type ValidationFunctionDefinition, type ValidationMode, type ValidationResult, type VisibilityCondition, VisibilityConditionSchema, type VisibilityContext, action, applySpecStreamPatch, builtInValidationFunctions, check, compileSpecStream, createCatalog, createSpecStreamCompiler, defineCatalog, defineSchema, evaluateLogicExpression, evaluateVisibility, executeAction, findFormValue, generateCatalogPrompt, generateSystemPrompt, getByPath, interpolateString, parseSpecStreamLine, resolveAction, resolveDynamicValue, runValidation, runValidationCheck, setByPath, visibility }; | ||
| export { type Action, type ActionConfirm, ActionConfirmSchema, type ActionDefinition, type ActionExecutionContext, type ActionHandler, type ActionOnError, ActionOnErrorSchema, type ActionOnSuccess, ActionOnSuccessSchema, ActionSchema, type AuthState, type Catalog$1 as Catalog, type CatalogConfig, type ComponentDefinition, type ComponentSchema, type DataModel, type DynamicBoolean, DynamicBooleanSchema, type DynamicNumber, DynamicNumberSchema, type DynamicString, DynamicStringSchema, type DynamicValue, DynamicValueSchema, type FlatElement, type InferActionParams, type InferCatalogActions, type InferCatalogComponentProps, type InferCatalogComponents, type InferCatalogInput, type InferComponentProps, type InferSpec, type JsonPatch, type Catalog as LegacyCatalog, type LogicExpression, LogicExpressionSchema, type PatchOp, type PromptContext, type PromptOptions, type PromptTemplate, type ResolvedAction, type Schema, type SchemaBuilder, type SchemaDefinition, type SchemaOptions, type SchemaType, type Spec, type SpecStreamCompiler, type SpecStreamLine, type SpecValidationResult, type SystemPromptOptions, type UIElement, type ValidationCheck, type ValidationCheckResult, ValidationCheckSchema, type ValidationConfig, ValidationConfigSchema, type ValidationContext, type ValidationFunction, type ValidationFunctionDefinition, type ValidationMode, type ValidationResult, type VisibilityCondition, VisibilityConditionSchema, type VisibilityContext, action, addByPath, applySpecStreamPatch, builtInValidationFunctions, check, compileSpecStream, createCatalog, createSpecStreamCompiler, defineCatalog, defineSchema, evaluateLogicExpression, evaluateVisibility, executeAction, findFormValue, generateCatalogPrompt, generateSystemPrompt, getByPath, interpolateString, parseSpecStreamLine, removeByPath, resolveAction, resolveDynamicValue, runValidation, runValidationCheck, setByPath, visibility }; |
+39
-13
@@ -40,4 +40,2 @@ import { z } from 'zod'; | ||
| interface UIElement<T extends string = string, P = Record<string, unknown>> { | ||
| /** Unique key for reconciliation */ | ||
| key: string; | ||
| /** Component type from the catalog */ | ||
@@ -49,4 +47,2 @@ type: T; | ||
| children?: string[]; | ||
| /** Parent element key (null for root) */ | ||
| parentKey?: string | null; | ||
| /** Visibility condition */ | ||
@@ -56,2 +52,13 @@ visible?: VisibilityCondition; | ||
| /** | ||
| * Element with key and parentKey for use with flatToTree. | ||
| * When elements are in an array (not a keyed map), key and parentKey | ||
| * are needed to establish identity and parent-child relationships. | ||
| */ | ||
| interface FlatElement<T extends string = string, P = Record<string, unknown>> extends UIElement<T, P> { | ||
| /** Unique key identifying this element */ | ||
| key: string; | ||
| /** Parent element key (null for root) */ | ||
| parentKey?: string | null; | ||
| } | ||
| /** | ||
| * Visibility condition types | ||
@@ -117,7 +124,7 @@ */ | ||
| /** | ||
| * JSON patch operation types | ||
| * JSON patch operation types (RFC 6902) | ||
| */ | ||
| type PatchOp = "add" | "remove" | "replace" | "set"; | ||
| type PatchOp = "add" | "remove" | "replace" | "move" | "copy" | "test"; | ||
| /** | ||
| * JSON patch operation | ||
| * JSON patch operation (RFC 6902) | ||
| */ | ||
@@ -127,3 +134,6 @@ interface JsonPatch { | ||
| path: string; | ||
| /** Required for add, replace, test */ | ||
| value?: unknown; | ||
| /** Required for move, copy (source location) */ | ||
| from?: string; | ||
| } | ||
@@ -135,7 +145,7 @@ /** | ||
| /** | ||
| * Get a value from an object by JSON Pointer path | ||
| * Get a value from an object by JSON Pointer path (RFC 6901) | ||
| */ | ||
| declare function getByPath(obj: unknown, path: string): unknown; | ||
| /** | ||
| * Set a value in an object by JSON Pointer path. | ||
| * Set a value in an object by JSON Pointer path (RFC 6901). | ||
| * Automatically creates arrays when the path segment is a numeric index. | ||
@@ -145,2 +155,14 @@ */ | ||
| /** | ||
| * Add a value per RFC 6902 "add" semantics. | ||
| * For objects: create-or-replace the member. | ||
| * For arrays: insert before the given index, or append if "-". | ||
| */ | ||
| declare function addByPath(obj: Record<string, unknown>, path: string, value: unknown): void; | ||
| /** | ||
| * Remove a value per RFC 6902 "remove" semantics. | ||
| * For objects: delete the property. | ||
| * For arrays: splice out the element at the given index. | ||
| */ | ||
| declare function removeByPath(obj: Record<string, unknown>, path: string): void; | ||
| /** | ||
| * Find a form value from params and/or data. | ||
@@ -175,4 +197,8 @@ * Useful in action handlers to locate form input values regardless of path format. | ||
| /** | ||
| * Apply a single SpecStream patch to an object. | ||
| * Apply a single RFC 6902 JSON Patch operation to an object. | ||
| * Mutates the object in place. | ||
| * | ||
| * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test. | ||
| * | ||
| * @throws {Error} If a "test" operation fails (value mismatch). | ||
| */ | ||
@@ -185,4 +211,4 @@ declare function applySpecStreamPatch<T extends Record<string, unknown>>(obj: T, patch: SpecStreamLine): T; | ||
| * @example | ||
| * const stream = `{"op":"set","path":"/name","value":"Alice"} | ||
| * {"op":"set","path":"/age","value":30}`; | ||
| * const stream = `{"op":"add","path":"/name","value":"Alice"} | ||
| * {"op":"add","path":"/age","value":30}`; | ||
| * const result = compileSpecStream(stream); | ||
@@ -886,2 +912,2 @@ * // { name: "Alice", age: 30 } | ||
| export { type Action, type ActionConfirm, ActionConfirmSchema, type ActionDefinition, type ActionExecutionContext, type ActionHandler, type ActionOnError, ActionOnErrorSchema, type ActionOnSuccess, ActionOnSuccessSchema, ActionSchema, type AuthState, type Catalog$1 as Catalog, type CatalogConfig, type ComponentDefinition, type ComponentSchema, type DataModel, type DynamicBoolean, DynamicBooleanSchema, type DynamicNumber, DynamicNumberSchema, type DynamicString, DynamicStringSchema, type DynamicValue, DynamicValueSchema, type InferActionParams, type InferCatalogActions, type InferCatalogComponentProps, type InferCatalogComponents, type InferCatalogInput, type InferComponentProps, type InferSpec, type JsonPatch, type Catalog as LegacyCatalog, type LogicExpression, LogicExpressionSchema, type PatchOp, type PromptContext, type PromptOptions, type PromptTemplate, type ResolvedAction, type Schema, type SchemaBuilder, type SchemaDefinition, type SchemaOptions, type SchemaType, type Spec, type SpecStreamCompiler, type SpecStreamLine, type SpecValidationResult, type SystemPromptOptions, type UIElement, type ValidationCheck, type ValidationCheckResult, ValidationCheckSchema, type ValidationConfig, ValidationConfigSchema, type ValidationContext, type ValidationFunction, type ValidationFunctionDefinition, type ValidationMode, type ValidationResult, type VisibilityCondition, VisibilityConditionSchema, type VisibilityContext, action, applySpecStreamPatch, builtInValidationFunctions, check, compileSpecStream, createCatalog, createSpecStreamCompiler, defineCatalog, defineSchema, evaluateLogicExpression, evaluateVisibility, executeAction, findFormValue, generateCatalogPrompt, generateSystemPrompt, getByPath, interpolateString, parseSpecStreamLine, resolveAction, resolveDynamicValue, runValidation, runValidationCheck, setByPath, visibility }; | ||
| export { type Action, type ActionConfirm, ActionConfirmSchema, type ActionDefinition, type ActionExecutionContext, type ActionHandler, type ActionOnError, ActionOnErrorSchema, type ActionOnSuccess, ActionOnSuccessSchema, ActionSchema, type AuthState, type Catalog$1 as Catalog, type CatalogConfig, type ComponentDefinition, type ComponentSchema, type DataModel, type DynamicBoolean, DynamicBooleanSchema, type DynamicNumber, DynamicNumberSchema, type DynamicString, DynamicStringSchema, type DynamicValue, DynamicValueSchema, type FlatElement, type InferActionParams, type InferCatalogActions, type InferCatalogComponentProps, type InferCatalogComponents, type InferCatalogInput, type InferComponentProps, type InferSpec, type JsonPatch, type Catalog as LegacyCatalog, type LogicExpression, LogicExpressionSchema, type PatchOp, type PromptContext, type PromptOptions, type PromptTemplate, type ResolvedAction, type Schema, type SchemaBuilder, type SchemaDefinition, type SchemaOptions, type SchemaType, type Spec, type SpecStreamCompiler, type SpecStreamLine, type SpecValidationResult, type SystemPromptOptions, type UIElement, type ValidationCheck, type ValidationCheckResult, ValidationCheckSchema, type ValidationConfig, ValidationConfigSchema, type ValidationContext, type ValidationFunction, type ValidationFunctionDefinition, type ValidationMode, type ValidationResult, type VisibilityCondition, VisibilityConditionSchema, type VisibilityContext, action, addByPath, applySpecStreamPatch, builtInValidationFunctions, check, compileSpecStream, createCatalog, createSpecStreamCompiler, defineCatalog, defineSchema, evaluateLogicExpression, evaluateVisibility, executeAction, findFormValue, generateCatalogPrompt, generateSystemPrompt, getByPath, interpolateString, parseSpecStreamLine, removeByPath, resolveAction, resolveDynamicValue, runValidation, runValidationCheck, setByPath, visibility }; |
+149
-29
@@ -36,2 +36,3 @@ "use strict"; | ||
| action: () => action, | ||
| addByPath: () => addByPath, | ||
| applySpecStreamPatch: () => applySpecStreamPatch, | ||
@@ -54,2 +55,3 @@ builtInValidationFunctions: () => builtInValidationFunctions, | ||
| parseSpecStreamLine: () => parseSpecStreamLine, | ||
| removeByPath: () => removeByPath, | ||
| resolveAction: () => resolveAction, | ||
@@ -94,2 +96,9 @@ resolveDynamicValue: () => resolveDynamicValue, | ||
| } | ||
| function unescapeJsonPointer(token) { | ||
| return token.replace(/~1/g, "/").replace(/~0/g, "~"); | ||
| } | ||
| function parseJsonPointer(path) { | ||
| const raw = path.startsWith("/") ? path.slice(1).split("/") : path.split("/"); | ||
| return raw.map(unescapeJsonPointer); | ||
| } | ||
| function getByPath(obj, path) { | ||
@@ -99,3 +108,3 @@ if (!path || path === "/") { | ||
| } | ||
| const segments = path.startsWith("/") ? path.slice(1).split("/") : path.split("/"); | ||
| const segments = parseJsonPointer(path); | ||
| let current = obj; | ||
@@ -106,3 +115,6 @@ for (const segment of segments) { | ||
| } | ||
| if (typeof current === "object") { | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| current = current[index]; | ||
| } else if (typeof current === "object") { | ||
| current = current[segment]; | ||
@@ -119,3 +131,3 @@ } else { | ||
| function setByPath(obj, path, value) { | ||
| const segments = path.startsWith("/") ? path.slice(1).split("/") : path.split("/"); | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
@@ -126,3 +138,3 @@ let current = obj; | ||
| const nextSegment = segments[i + 1]; | ||
| const nextIsNumeric = nextSegment !== void 0 && isNumericIndex(nextSegment); | ||
| const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-"); | ||
| if (Array.isArray(current)) { | ||
@@ -143,4 +155,8 @@ const index = parseInt(segment, 10); | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(lastSegment, 10); | ||
| current[index] = value; | ||
| if (lastSegment === "-") { | ||
| current.push(value); | ||
| } else { | ||
| const index = parseInt(lastSegment, 10); | ||
| current[index] = value; | ||
| } | ||
| } else { | ||
@@ -150,2 +166,81 @@ current[lastSegment] = value; | ||
| } | ||
| function addByPath(obj, path, value) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| const nextSegment = segments[i + 1]; | ||
| const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-"); | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| current[index] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| current[segment] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| if (lastSegment === "-") { | ||
| current.push(value); | ||
| } else { | ||
| const index = parseInt(lastSegment, 10); | ||
| current.splice(index, 0, value); | ||
| } | ||
| } else { | ||
| current[lastSegment] = value; | ||
| } | ||
| } | ||
| function removeByPath(obj, path) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| return; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| return; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(lastSegment, 10); | ||
| if (index >= 0 && index < current.length) { | ||
| current.splice(index, 1); | ||
| } | ||
| } else { | ||
| delete current[lastSegment]; | ||
| } | ||
| } | ||
| function deepEqual(a, b) { | ||
| if (a === b) return true; | ||
| if (a === null || b === null) return false; | ||
| if (typeof a !== typeof b) return false; | ||
| if (typeof a !== "object") return false; | ||
| if (Array.isArray(a)) { | ||
| if (!Array.isArray(b)) return false; | ||
| if (a.length !== b.length) return false; | ||
| return a.every((item, i) => deepEqual(item, b[i])); | ||
| } | ||
| const aObj = a; | ||
| const bObj = b; | ||
| const aKeys = Object.keys(aObj); | ||
| const bKeys = Object.keys(bObj); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| return aKeys.every((key) => deepEqual(aObj[key], bObj[key])); | ||
| } | ||
| function findFormValue(fieldName, params, data) { | ||
@@ -199,6 +294,34 @@ if (params?.[fieldName] !== void 0) { | ||
| function applySpecStreamPatch(obj, patch) { | ||
| if (patch.op === "set" || patch.op === "add" || patch.op === "replace") { | ||
| setByPath(obj, patch.path, patch.value); | ||
| } else if (patch.op === "remove") { | ||
| setByPath(obj, patch.path, void 0); | ||
| switch (patch.op) { | ||
| case "add": | ||
| addByPath(obj, patch.path, patch.value); | ||
| break; | ||
| case "replace": | ||
| setByPath(obj, patch.path, patch.value); | ||
| break; | ||
| case "remove": | ||
| removeByPath(obj, patch.path); | ||
| break; | ||
| case "move": { | ||
| if (!patch.from) break; | ||
| const moveValue = getByPath(obj, patch.from); | ||
| removeByPath(obj, patch.from); | ||
| addByPath(obj, patch.path, moveValue); | ||
| break; | ||
| } | ||
| case "copy": { | ||
| if (!patch.from) break; | ||
| const copyValue = getByPath(obj, patch.from); | ||
| addByPath(obj, patch.path, copyValue); | ||
| break; | ||
| } | ||
| case "test": { | ||
| const actual = getByPath(obj, patch.path); | ||
| if (!deepEqual(actual, patch.value)) { | ||
| throw new Error( | ||
| `Test operation failed: value at "${patch.path}" does not match` | ||
| ); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
@@ -946,6 +1069,6 @@ return obj; | ||
| lines.push(""); | ||
| lines.push(`{"op":"set","path":"/root","value":"card-1"} | ||
| {"op":"set","path":"/elements/card-1","value":{"key":"card-1","type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"],"parentKey":""}} | ||
| {"op":"set","path":"/elements/metric-1","value":{"key":"metric-1","type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[],"parentKey":"card-1"}} | ||
| {"op":"set","path":"/elements/chart-1","value":{"key":"chart-1","type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[],"parentKey":"card-1"}}`); | ||
| lines.push(`{"op":"add","path":"/root","value":"card-1"} | ||
| {"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"]}} | ||
| {"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[]}} | ||
| {"op":"add","path":"/elements/chart-1","value":{"type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[]}}`); | ||
| lines.push(""); | ||
@@ -977,8 +1100,7 @@ const components = catalog.data.components; | ||
| "Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences", | ||
| 'First line sets root: {"op":"set","path":"/root","value":"<root-key>"}', | ||
| 'Then add each element: {"op":"set","path":"/elements/<key>","value":{...}}', | ||
| 'First line sets root: {"op":"add","path":"/root","value":"<root-key>"}', | ||
| 'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}', | ||
| "ONLY use components listed above", | ||
| "Each element value needs: key, type, props, children (array of child keys), parentKey", | ||
| "Use unique keys (e.g., 'header', 'metric-1', 'chart-revenue')", | ||
| "Root element's parentKey is empty string, children reference their parent's key" | ||
| "Each element value needs: type, props, children (array of child keys)", | ||
| "Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')" | ||
| ]; | ||
@@ -1139,7 +1261,5 @@ const allRules = [...baseRules, ...customRules]; | ||
| return import_zod6.z.object({ | ||
| key: import_zod6.z.string(), | ||
| type: import_zod6.z.literal(componentName), | ||
| props: def.props, | ||
| children: import_zod6.z.array(import_zod6.z.string()).optional(), | ||
| parentKey: import_zod6.z.string().nullable().optional(), | ||
| visible: VisibilityConditionSchema.optional() | ||
@@ -1151,7 +1271,5 @@ }); | ||
| elementSchema = import_zod6.z.object({ | ||
| key: import_zod6.z.string(), | ||
| type: import_zod6.z.string(), | ||
| props: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.unknown()), | ||
| children: import_zod6.z.array(import_zod6.z.string()).optional(), | ||
| parentKey: import_zod6.z.string().nullable().optional(), | ||
| visible: VisibilityConditionSchema.optional() | ||
@@ -1377,6 +1495,6 @@ }); | ||
| } | ||
| lines.push("OUTPUT FORMAT (JSONL):"); | ||
| lines.push('{"op":"set","path":"/root","value":"element-key"}'); | ||
| lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):"); | ||
| lines.push('{"op":"add","path":"/root","value":"element-key"}'); | ||
| lines.push( | ||
| '{"op":"add","path":"/elements/key","value":{"key":"...","type":"...","props":{...},"children":[...]}}' | ||
| '{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}' | ||
| ); | ||
@@ -1387,8 +1505,8 @@ lines.push('{"op":"remove","path":"/elements/key"}'); | ||
| const baseRules = [ | ||
| "First line sets /root to root element key", | ||
| "Add elements with /elements/{key}", | ||
| 'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}', | ||
| 'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}', | ||
| "Remove elements with op:remove - also update the parent's children array to exclude the removed key", | ||
| "Children array contains string keys, not objects", | ||
| "Parent first, then children", | ||
| "Each element needs: key, type, props", | ||
| "Each element needs: type, props", | ||
| "ONLY use props listed above - never invent new props" | ||
@@ -1424,2 +1542,3 @@ ]; | ||
| action, | ||
| addByPath, | ||
| applySpecStreamPatch, | ||
@@ -1442,2 +1561,3 @@ builtInValidationFunctions, | ||
| parseSpecStreamLine, | ||
| removeByPath, | ||
| resolveAction, | ||
@@ -1444,0 +1564,0 @@ resolveDynamicValue, |
+147
-29
@@ -31,2 +31,9 @@ // src/types.ts | ||
| } | ||
| function unescapeJsonPointer(token) { | ||
| return token.replace(/~1/g, "/").replace(/~0/g, "~"); | ||
| } | ||
| function parseJsonPointer(path) { | ||
| const raw = path.startsWith("/") ? path.slice(1).split("/") : path.split("/"); | ||
| return raw.map(unescapeJsonPointer); | ||
| } | ||
| function getByPath(obj, path) { | ||
@@ -36,3 +43,3 @@ if (!path || path === "/") { | ||
| } | ||
| const segments = path.startsWith("/") ? path.slice(1).split("/") : path.split("/"); | ||
| const segments = parseJsonPointer(path); | ||
| let current = obj; | ||
@@ -43,3 +50,6 @@ for (const segment of segments) { | ||
| } | ||
| if (typeof current === "object") { | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| current = current[index]; | ||
| } else if (typeof current === "object") { | ||
| current = current[segment]; | ||
@@ -56,3 +66,3 @@ } else { | ||
| function setByPath(obj, path, value) { | ||
| const segments = path.startsWith("/") ? path.slice(1).split("/") : path.split("/"); | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
@@ -63,3 +73,3 @@ let current = obj; | ||
| const nextSegment = segments[i + 1]; | ||
| const nextIsNumeric = nextSegment !== void 0 && isNumericIndex(nextSegment); | ||
| const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-"); | ||
| if (Array.isArray(current)) { | ||
@@ -80,4 +90,8 @@ const index = parseInt(segment, 10); | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(lastSegment, 10); | ||
| current[index] = value; | ||
| if (lastSegment === "-") { | ||
| current.push(value); | ||
| } else { | ||
| const index = parseInt(lastSegment, 10); | ||
| current[index] = value; | ||
| } | ||
| } else { | ||
@@ -87,2 +101,81 @@ current[lastSegment] = value; | ||
| } | ||
| function addByPath(obj, path, value) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| const nextSegment = segments[i + 1]; | ||
| const nextIsNumeric = nextSegment !== void 0 && (isNumericIndex(nextSegment) || nextSegment === "-"); | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| current[index] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| current[segment] = nextIsNumeric ? [] : {}; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| if (lastSegment === "-") { | ||
| current.push(value); | ||
| } else { | ||
| const index = parseInt(lastSegment, 10); | ||
| current.splice(index, 0, value); | ||
| } | ||
| } else { | ||
| current[lastSegment] = value; | ||
| } | ||
| } | ||
| function removeByPath(obj, path) { | ||
| const segments = parseJsonPointer(path); | ||
| if (segments.length === 0) return; | ||
| let current = obj; | ||
| for (let i = 0; i < segments.length - 1; i++) { | ||
| const segment = segments[i]; | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(segment, 10); | ||
| if (current[index] === void 0 || typeof current[index] !== "object") { | ||
| return; | ||
| } | ||
| current = current[index]; | ||
| } else { | ||
| if (!(segment in current) || typeof current[segment] !== "object") { | ||
| return; | ||
| } | ||
| current = current[segment]; | ||
| } | ||
| } | ||
| const lastSegment = segments[segments.length - 1]; | ||
| if (Array.isArray(current)) { | ||
| const index = parseInt(lastSegment, 10); | ||
| if (index >= 0 && index < current.length) { | ||
| current.splice(index, 1); | ||
| } | ||
| } else { | ||
| delete current[lastSegment]; | ||
| } | ||
| } | ||
| function deepEqual(a, b) { | ||
| if (a === b) return true; | ||
| if (a === null || b === null) return false; | ||
| if (typeof a !== typeof b) return false; | ||
| if (typeof a !== "object") return false; | ||
| if (Array.isArray(a)) { | ||
| if (!Array.isArray(b)) return false; | ||
| if (a.length !== b.length) return false; | ||
| return a.every((item, i) => deepEqual(item, b[i])); | ||
| } | ||
| const aObj = a; | ||
| const bObj = b; | ||
| const aKeys = Object.keys(aObj); | ||
| const bKeys = Object.keys(bObj); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| return aKeys.every((key) => deepEqual(aObj[key], bObj[key])); | ||
| } | ||
| function findFormValue(fieldName, params, data) { | ||
@@ -136,6 +229,34 @@ if (params?.[fieldName] !== void 0) { | ||
| function applySpecStreamPatch(obj, patch) { | ||
| if (patch.op === "set" || patch.op === "add" || patch.op === "replace") { | ||
| setByPath(obj, patch.path, patch.value); | ||
| } else if (patch.op === "remove") { | ||
| setByPath(obj, patch.path, void 0); | ||
| switch (patch.op) { | ||
| case "add": | ||
| addByPath(obj, patch.path, patch.value); | ||
| break; | ||
| case "replace": | ||
| setByPath(obj, patch.path, patch.value); | ||
| break; | ||
| case "remove": | ||
| removeByPath(obj, patch.path); | ||
| break; | ||
| case "move": { | ||
| if (!patch.from) break; | ||
| const moveValue = getByPath(obj, patch.from); | ||
| removeByPath(obj, patch.from); | ||
| addByPath(obj, patch.path, moveValue); | ||
| break; | ||
| } | ||
| case "copy": { | ||
| if (!patch.from) break; | ||
| const copyValue = getByPath(obj, patch.from); | ||
| addByPath(obj, patch.path, copyValue); | ||
| break; | ||
| } | ||
| case "test": { | ||
| const actual = getByPath(obj, patch.path); | ||
| if (!deepEqual(actual, patch.value)) { | ||
| throw new Error( | ||
| `Test operation failed: value at "${patch.path}" does not match` | ||
| ); | ||
| } | ||
| break; | ||
| } | ||
| } | ||
@@ -883,6 +1004,6 @@ return obj; | ||
| lines.push(""); | ||
| lines.push(`{"op":"set","path":"/root","value":"card-1"} | ||
| {"op":"set","path":"/elements/card-1","value":{"key":"card-1","type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"],"parentKey":""}} | ||
| {"op":"set","path":"/elements/metric-1","value":{"key":"metric-1","type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[],"parentKey":"card-1"}} | ||
| {"op":"set","path":"/elements/chart-1","value":{"key":"chart-1","type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[],"parentKey":"card-1"}}`); | ||
| lines.push(`{"op":"add","path":"/root","value":"card-1"} | ||
| {"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Dashboard"},"children":["metric-1","chart-1"]}} | ||
| {"op":"add","path":"/elements/metric-1","value":{"type":"Metric","props":{"label":"Revenue","valuePath":"analytics.revenue","format":"currency"},"children":[]}} | ||
| {"op":"add","path":"/elements/chart-1","value":{"type":"Chart","props":{"type":"bar","dataPath":"analytics.salesByRegion"},"children":[]}}`); | ||
| lines.push(""); | ||
@@ -914,8 +1035,7 @@ const components = catalog.data.components; | ||
| "Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences", | ||
| 'First line sets root: {"op":"set","path":"/root","value":"<root-key>"}', | ||
| 'Then add each element: {"op":"set","path":"/elements/<key>","value":{...}}', | ||
| 'First line sets root: {"op":"add","path":"/root","value":"<root-key>"}', | ||
| 'Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}', | ||
| "ONLY use components listed above", | ||
| "Each element value needs: key, type, props, children (array of child keys), parentKey", | ||
| "Use unique keys (e.g., 'header', 'metric-1', 'chart-revenue')", | ||
| "Root element's parentKey is empty string, children reference their parent's key" | ||
| "Each element value needs: type, props, children (array of child keys)", | ||
| "Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')" | ||
| ]; | ||
@@ -1076,7 +1196,5 @@ const allRules = [...baseRules, ...customRules]; | ||
| return z6.object({ | ||
| key: z6.string(), | ||
| type: z6.literal(componentName), | ||
| props: def.props, | ||
| children: z6.array(z6.string()).optional(), | ||
| parentKey: z6.string().nullable().optional(), | ||
| visible: VisibilityConditionSchema.optional() | ||
@@ -1088,7 +1206,5 @@ }); | ||
| elementSchema = z6.object({ | ||
| key: z6.string(), | ||
| type: z6.string(), | ||
| props: z6.record(z6.string(), z6.unknown()), | ||
| children: z6.array(z6.string()).optional(), | ||
| parentKey: z6.string().nullable().optional(), | ||
| visible: VisibilityConditionSchema.optional() | ||
@@ -1314,6 +1430,6 @@ }); | ||
| } | ||
| lines.push("OUTPUT FORMAT (JSONL):"); | ||
| lines.push('{"op":"set","path":"/root","value":"element-key"}'); | ||
| lines.push("OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):"); | ||
| lines.push('{"op":"add","path":"/root","value":"element-key"}'); | ||
| lines.push( | ||
| '{"op":"add","path":"/elements/key","value":{"key":"...","type":"...","props":{...},"children":[...]}}' | ||
| '{"op":"add","path":"/elements/key","value":{"type":"...","props":{...},"children":[...]}}' | ||
| ); | ||
@@ -1324,8 +1440,8 @@ lines.push('{"op":"remove","path":"/elements/key"}'); | ||
| const baseRules = [ | ||
| "First line sets /root to root element key", | ||
| "Add elements with /elements/{key}", | ||
| 'First line sets /root to root element key: {"op":"add","path":"/root","value":"<key>"}', | ||
| 'Add elements with /elements/{key}: {"op":"add","path":"/elements/<key>","value":{...}}', | ||
| "Remove elements with op:remove - also update the parent's children array to exclude the removed key", | ||
| "Children array contains string keys, not objects", | ||
| "Parent first, then children", | ||
| "Each element needs: key, type, props", | ||
| "Each element needs: type, props", | ||
| "ONLY use props listed above - never invent new props" | ||
@@ -1360,2 +1476,3 @@ ]; | ||
| action, | ||
| addByPath, | ||
| applySpecStreamPatch, | ||
@@ -1378,2 +1495,3 @@ builtInValidationFunctions, | ||
| parseSpecStreamLine, | ||
| removeByPath, | ||
| resolveAction, | ||
@@ -1380,0 +1498,0 @@ resolveDynamicValue, |
+1
-1
| { | ||
| "name": "@json-render/core", | ||
| "version": "0.4.3", | ||
| "version": "0.4.4", | ||
| "license": "Apache-2.0", | ||
@@ -5,0 +5,0 @@ "description": "JSON becomes real things. Define your catalog, register your components, let AI generate.", |
+8
-6
@@ -119,10 +119,12 @@ # @json-render/core | ||
| SpecStream format (each line is a JSON patch): | ||
| SpecStream format uses [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations (each line is a patch): | ||
| ```jsonl | ||
| {"op":"set","path":"/root/type","value":"Card"} | ||
| {"op":"set","path":"/root/props","value":{"title":"Hello"}} | ||
| {"op":"set","path":"/root/children/0","value":{"type":"Button","props":{"label":"Click"}}} | ||
| {"op":"add","path":"/root/type","value":"Card"} | ||
| {"op":"add","path":"/root/props","value":{"title":"Hello"}} | ||
| {"op":"add","path":"/root/children/0","value":{"type":"Button","props":{"label":"Click"}}} | ||
| ``` | ||
| All six RFC 6902 operations are supported: `add`, `remove`, `replace`, `move`, `copy`, `test`. | ||
| ### Low-Level Utilities | ||
@@ -138,4 +140,4 @@ | ||
| // Parse a single line | ||
| const patch = parseSpecStreamLine('{"op":"set","path":"/root","value":{}}'); | ||
| // { op: "set", path: "/root", value: {} } | ||
| const patch = parseSpecStreamLine('{"op":"add","path":"/root","value":{}}'); | ||
| // { op: "add", path: "/root", value: {} } | ||
@@ -142,0 +144,0 @@ // Apply a patch to an object |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
405862
6.74%3936
7.19%201
1.01%