Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 76 additions & 28 deletions packages/lib/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type PropValidator<T, InstanceType> = {
errorMsg: (value: unknown) => string;
default: T | unknown;
apply?: (container: InstanceType, props: Record<string, unknown>, key: string) => void;
readOnly?: boolean;
}

// A more generic schema type that works with both functions
Expand Down Expand Up @@ -219,37 +220,36 @@ export function validatePropsWithDefaults<T extends object, InstanceType>(
* @param props The props to apply
*/
export function applyProps<T extends Record<string, unknown>, InstanceType>(
instance: InstanceType,
schema: Schema<T, InstanceType>,
instance: InstanceType,
schema: Schema<T, InstanceType>,
props: T
) {
Object.entries(props as Record<keyof T, unknown>).forEach(([key, value]) => {
if (key in schema) {
const propDef = schema[key as keyof T] as PropValidator<T[keyof T], InstanceType>;
if (propDef) {
if (propDef.apply) {
// Use type assertion to satisfy the type checker
propDef.apply(instance, props, key as string);
} else {
try {
(instance as Record<string, unknown>)[key] = value;
} catch (error) {
console.error(`Error applying prop ${key}: ${error}`);
}
}
}
}
});
}
) {
Object.entries(props).forEach(([key, value]) => {
if (!(key in schema)) return;
const propDef = schema[key] as PropValidator<T[keyof T], InstanceType> | undefined;
if (!propDef || propDef.readOnly) return;

if (propDef.apply) {
propDef.apply(instance, props, key as string);
} else {
try {
(instance as Record<string, unknown>)[key] = value;
} catch (error) {
console.error(`Error applying prop ${key}:`, error);
}
}
});
}


/**
* Property information including whether it's defined with a setter.
*/
export type PropertyInfo = {
value: unknown;
isDefinedWithSetter: boolean;
};
readOnly?: boolean; // Mark properties that should not be assigned
};

/**
* Get the pseudo public props of an instance with setter information. This is useful for creating a component definition from an instance.
Expand Down Expand Up @@ -283,7 +283,7 @@ export function getPseudoPublicProps(container: Record<string, unknown>): Record
const hasGetter = typeof descriptor.get === 'function';
const hasSetter = typeof descriptor.set === 'function';

if (hasSetter && !hasGetter) return;
if (hasSetter && !hasGetter) return; // Only setter-only props are skipped

// If it's a getter/setter property, try to get the value
if (descriptor.get) {
Expand Down Expand Up @@ -369,8 +369,18 @@ export function createComponentDefinition<T, InstanceType>(

// Basic type detection
entries.forEach(([key, propertyInfo]) => {
if(exclude.includes(String(key))) return;
const { value, isDefinedWithSetter } = propertyInfo;
if (exclude.includes(String(key))) return;

// Mark getter-only properties as read-only
const descriptor = Object.getOwnPropertyDescriptor(
(instance as any).constructor.prototype,
key as string | symbol
);
if (descriptor && descriptor.get && !descriptor.set) {
propertyInfo.readOnly = true;
}

const { value, isDefinedWithSetter } = propertyInfo;

// Colors
if (value instanceof Color) {
Expand All @@ -380,6 +390,9 @@ export function createComponentDefinition<T, InstanceType>(
errorMsg: (val: unknown) => `Invalid value for prop "${String(key)}": "${val}". ` +
`Expected a hex like "#FF0000", CSS color name like "red", or an array "[1, 0, 0]").`,
apply: (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
if(typeof props[key] === 'string') {
const colorString = getColorFromName(props[key] as string) || props[key] as string;
(instance[key as keyof InstanceType] as Color) = new Color().fromString(colorString);
Expand All @@ -397,8 +410,14 @@ export function createComponentDefinition<T, InstanceType>(
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` +
`Expected an array of 2 numbers.`,
apply: isDefinedWithSetter ? (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
(instance[key as keyof InstanceType] as Vec2) = new Vec2().fromArray(props[key] as number[]);
} : (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
(instance[key as keyof InstanceType] as Vec2).set(...props[key] as [number, number]);
}
};
Expand All @@ -411,8 +430,14 @@ export function createComponentDefinition<T, InstanceType>(
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` +
`Expected an array of 3 numbers.`,
apply: isDefinedWithSetter ? (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
(instance[key as keyof InstanceType] as Vec3) = new Vec3().fromArray(props[key] as number[]);
} : (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
(instance[key as keyof InstanceType] as Vec3).set(...props[key] as [number, number, number]);
}
};
Expand All @@ -424,8 +449,14 @@ export function createComponentDefinition<T, InstanceType>(
default: [value.x, value.y, value.z, value.w],
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". Expected an array of 4 numbers.`,
apply: isDefinedWithSetter ? (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
(instance[key as keyof InstanceType] as Vec4) = new Vec4().fromArray(props[key] as number[]);
} : (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
(instance[key as keyof InstanceType] as Vec4).set(...props[key] as [number, number, number, number]);
}
};
Expand All @@ -439,8 +470,14 @@ export function createComponentDefinition<T, InstanceType>(
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` +
`Expected an array of 4 numbers.`,
apply: isDefinedWithSetter ? (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
(instance[key as keyof InstanceType] as Quat) = new Quat().fromArray(props[key] as number[]);
} : (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
(instance[key as keyof InstanceType] as Quat).set(...props[key] as [number, number, number, number]);
}
};
Expand All @@ -452,30 +489,34 @@ export function createComponentDefinition<T, InstanceType>(
default: Array.from((value.data)),
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". ` +
`Expected an array of 16 numbers.`,
...(propertyInfo.readOnly && { readOnly: true }),
};
}
// Numbers
else if (typeof value === 'number') {
schema[key] = {
validate: (val) => typeof val === 'number',
default: value,
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a number.`
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a number.`,
...(propertyInfo.readOnly && { readOnly: true }),
};
}
// Strings
else if (typeof value === 'string') {
schema[key] = {
validate: (val) => typeof val === 'string',
default: value as string,
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a string.`
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a string.`,
...(propertyInfo.readOnly && { readOnly: true }),
};
}
// Booleans
else if (typeof value === 'boolean') {
schema[key] = {
validate: (val) => typeof val === 'boolean',
default: value as boolean,
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a boolean.`
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${val}". Expected a boolean.`,
...(propertyInfo.readOnly && { readOnly: true }),
};
}

Expand All @@ -486,6 +527,9 @@ export function createComponentDefinition<T, InstanceType>(
default: value,
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". Expected an array.`,
apply: (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
// For arrays, use a different approach to avoid spread operator issues
const values = props[key] as unknown[];

Expand All @@ -505,6 +549,7 @@ export function createComponentDefinition<T, InstanceType>(
validate: (val) => val instanceof Material,
default: value,
errorMsg: (val) => `Invalid value for prop "${String(key)}": "${JSON.stringify(val)}". Expected a Material.`,
...(propertyInfo.readOnly && { readOnly: true }),
};
}

Expand All @@ -515,6 +560,9 @@ export function createComponentDefinition<T, InstanceType>(
default: value,
errorMsg: () => '',
apply: (instance, props, key) => {
if (propertyInfo.readOnly) {
return;
}
(instance[key as keyof InstanceType] as unknown) = props[key];
}
};
Expand Down