feat: make node definitions type safe with zod
This commit is contained in:
@ -5,14 +5,14 @@ use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use syn::{parse_macro_input, LitStr};
|
||||
use types::NodeType;
|
||||
use types::NodeDefinition;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn define_node(input: TokenStream) -> TokenStream {
|
||||
pub fn node_definition(input: TokenStream) -> TokenStream {
|
||||
let input_string = parse_macro_input!(input as LitStr).value();
|
||||
|
||||
// Validate JSON format
|
||||
let json: NodeType = match serde_json::from_str(&input_string) {
|
||||
let json: NodeDefinition = match serde_json::from_str(&input_string) {
|
||||
Ok(json) => json,
|
||||
Err(e) => panic!("Invalid JSON input: {}", e),
|
||||
};
|
||||
@ -49,7 +49,7 @@ pub fn include_definition_file(input: TokenStream) -> TokenStream {
|
||||
});
|
||||
|
||||
// Optionally, validate that the content is valid JSON
|
||||
let _: NodeType = serde_json::from_str(&json_content)
|
||||
let _: NodeDefinition = serde_json::from_str(&json_content)
|
||||
.unwrap_or_else(|err| panic!("JSON file contains invalid JSON: {}", err));
|
||||
|
||||
// Generate the function that returns the JSON string
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { NodeInput } from "./inputs";
|
||||
import { z } from "zod";
|
||||
import { NodeInputSchema } from "./inputs";
|
||||
export type { NodeInput } from "./inputs";
|
||||
|
||||
export type Node = {
|
||||
@ -12,7 +13,7 @@ export type Node = {
|
||||
parents?: Node[],
|
||||
children?: Node[],
|
||||
inputNodes?: Record<string, Node>
|
||||
type?: NodeType;
|
||||
type?: NodeDefinition;
|
||||
downX?: number;
|
||||
downY?: number;
|
||||
x?: number;
|
||||
@ -28,15 +29,19 @@ export type Node = {
|
||||
position: [x: number, y: number]
|
||||
}
|
||||
|
||||
export type NodeType = {
|
||||
id: string;
|
||||
inputs?: Record<string, NodeInput>
|
||||
outputs?: string[];
|
||||
meta?: {
|
||||
title?: string;
|
||||
},
|
||||
execute?: (args: Int32Array) => Int32Array;
|
||||
}
|
||||
export const NodeDefinitionSchema = z.object({
|
||||
id: z.string(),
|
||||
inputs: z.record(NodeInputSchema).optional(),
|
||||
outputs: z.array(z.string()).optional(),
|
||||
meta: z.object({
|
||||
description: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
|
||||
execute(input: Int32Array): Int32Array;
|
||||
};
|
||||
|
||||
export type Socket = {
|
||||
node: Node;
|
||||
@ -63,12 +68,12 @@ export interface NodeRegistry {
|
||||
* @param id - The id of the node to get
|
||||
* @returns The node with the given id, or undefined if no such node exists
|
||||
*/
|
||||
getNode: (id: string) => NodeType | undefined;
|
||||
getNode: (id: string) => NodeDefinition | undefined;
|
||||
/**
|
||||
* Get all nodes
|
||||
* @returns An array of all nodes
|
||||
*/
|
||||
getAllNodes: () => NodeType[];
|
||||
getAllNodes: () => NodeDefinition[];
|
||||
}
|
||||
|
||||
export interface RuntimeExecutor {
|
||||
|
@ -1,65 +1,78 @@
|
||||
type NodeInputFloat = {
|
||||
type: "float";
|
||||
element?: "slider";
|
||||
value?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
import { z } from "zod";
|
||||
|
||||
type NodeInputInteger = {
|
||||
type: "integer";
|
||||
element?: "slider";
|
||||
value?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
type NodeInputBoolean = {
|
||||
type: "boolean";
|
||||
value?: boolean;
|
||||
}
|
||||
|
||||
type NodeInputSelect = {
|
||||
type: "select";
|
||||
options: string[];
|
||||
value?: number;
|
||||
}
|
||||
|
||||
type NodeInputSeed = {
|
||||
type: "seed"
|
||||
value?: number;
|
||||
}
|
||||
|
||||
type NodeInputVec3 = {
|
||||
type: "vec3";
|
||||
value?: number[];
|
||||
}
|
||||
|
||||
type NodeInputModel = {
|
||||
type: "model";
|
||||
}
|
||||
|
||||
type NodeInputPlant = {
|
||||
type: "plant"
|
||||
}
|
||||
|
||||
type InputTypes = (NodeInputSeed | NodeInputBoolean | NodeInputFloat | NodeInputInteger | NodeInputSelect | NodeInputSeed | NodeInputVec3 | NodeInputModel | NodeInputPlant);
|
||||
|
||||
type InputId = InputTypes["type"];
|
||||
|
||||
type DefaultOptions = {
|
||||
internal?: boolean;
|
||||
external?: boolean;
|
||||
setting?: string;
|
||||
label?: string | false;
|
||||
}
|
||||
|
||||
export type NodeInput = InputTypes & {
|
||||
type: InputId | InputId[];
|
||||
} & DefaultOptions;
|
||||
const DefaultOptionsSchema = z.object({
|
||||
internal: z.boolean().optional(),
|
||||
external: z.boolean().optional(),
|
||||
setting: z.string().optional(),
|
||||
label: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
accepts: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
|
||||
export type NodeInputType<T extends Record<string, NodeInput>> = {
|
||||
[K in keyof T]: T[K]["value"]
|
||||
};
|
||||
export const NodeInputFloatSchema = z.object({
|
||||
...DefaultOptionsSchema.shape,
|
||||
type: z.literal("float"),
|
||||
element: z.literal("slider").optional(),
|
||||
value: z.number().optional(),
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
step: z.number().optional(),
|
||||
});
|
||||
|
||||
export const NodeInputIntegerSchema = z.object({
|
||||
...DefaultOptionsSchema.shape,
|
||||
type: z.literal("integer"),
|
||||
element: z.literal("slider").optional(),
|
||||
value: z.number().optional(),
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
});
|
||||
|
||||
export const NodeInputBooleanSchema = z.object({
|
||||
...DefaultOptionsSchema.shape,
|
||||
type: z.literal("boolean"),
|
||||
vale: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const NodeInputSelectSchema = z.object({
|
||||
...DefaultOptionsSchema.shape,
|
||||
type: z.literal("select"),
|
||||
value: z.number().optional(),
|
||||
});
|
||||
|
||||
export const NodeInputSeedSchema = z.object({
|
||||
...DefaultOptionsSchema.shape,
|
||||
type: z.literal("seed"),
|
||||
value: z.number().optional(),
|
||||
});
|
||||
|
||||
export const NodeInputVec3Schema = z.object({
|
||||
...DefaultOptionsSchema.shape,
|
||||
type: z.literal("vec3"),
|
||||
value: z.array(z.number()).optional(),
|
||||
});
|
||||
|
||||
export const NodeInputModelSchema = z.object({
|
||||
...DefaultOptionsSchema.shape,
|
||||
type: z.literal("model"),
|
||||
});
|
||||
|
||||
export const NodeInputPlantSchema = z.object({
|
||||
...DefaultOptionsSchema.shape,
|
||||
type: z.literal("plant"),
|
||||
});
|
||||
|
||||
export const NodeInputSchema = z.union([
|
||||
NodeInputSeedSchema,
|
||||
NodeInputBooleanSchema,
|
||||
NodeInputFloatSchema,
|
||||
NodeInputIntegerSchema,
|
||||
NodeInputSelectSchema,
|
||||
NodeInputSeedSchema,
|
||||
NodeInputVec3Schema,
|
||||
NodeInputModelSchema,
|
||||
NodeInputPlantSchema
|
||||
]);
|
||||
|
||||
export type NodeInput = z.infer<typeof InputSchema>;
|
||||
|
@ -90,7 +90,7 @@ pub struct DefaultOptions {
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum NodeTypeOrArray {
|
||||
pub enum NodeDefinitionOrArray {
|
||||
Single(InputTypes),
|
||||
Multiple(Vec<String>),
|
||||
}
|
||||
@ -124,10 +124,10 @@ impl<'de> Deserialize<'de> for NodeInput {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Serialize)]
|
||||
pub struct NodeType {
|
||||
pub struct NodeDefinition {
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub inputs: Option<HashMap<String, NodeInput>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub outputs: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{decode_float, log};
|
||||
use crate::decode_float;
|
||||
|
||||
pub fn get_args(args: &[i32]) -> Vec<&[i32]> {
|
||||
let mut idx: usize = 0;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NodeType } from "@nodes/types";
|
||||
import { NodeDefinition } from "@nodes/types";
|
||||
|
||||
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
const cachedTextEncoder = new TextEncoder();
|
||||
@ -17,12 +17,9 @@ const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||
};
|
||||
});
|
||||
|
||||
export function createWasmWrapper() {
|
||||
function createWrapper() {
|
||||
let wasm: any;
|
||||
|
||||
|
||||
|
||||
|
||||
let cachedUint8Memory0: Uint8Array | null = null;
|
||||
let cachedInt32Memory0: Int32Array | null = null;
|
||||
let cachedUint32Memory0: Uint32Array | null = null;
|
||||
@ -120,7 +117,7 @@ export function createWasmWrapper() {
|
||||
deferred1_0 = r0;
|
||||
deferred1_1 = r1;
|
||||
const string = getStringFromWasm0(r0, r1);
|
||||
return JSON.parse(string) as Omit<NodeType, "id">;
|
||||
return JSON.parse(string) as NodeDefinition;
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
|
||||
@ -226,9 +223,12 @@ export function createWasmWrapper() {
|
||||
wasm = instance.exports;
|
||||
},
|
||||
|
||||
// Expose other methods that interact with the wasm instance
|
||||
execute,
|
||||
get_definition,
|
||||
|
||||
exports: {
|
||||
// Expose other methods that interact with the wasm instance
|
||||
execute,
|
||||
get_definition,
|
||||
},
|
||||
|
||||
__wbindgen_string_new,
|
||||
__wbindgen_object_drop_ref,
|
||||
@ -240,3 +240,11 @@ export function createWasmWrapper() {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function createWasmWrapper(wasmBuffer: ArrayBuffer) {
|
||||
const wrapper = createWrapper();
|
||||
const module = new WebAssembly.Module(wasmBuffer);
|
||||
const instance = new WebAssembly.Instance(module, { ["./index_bg.js"]: wrapper });
|
||||
wrapper.setInstance(instance);
|
||||
return wrapper.exports;
|
||||
}
|
||||
|
Reference in New Issue
Block a user