Skip to content

Commit

Permalink
working on definition validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikolas Howard committed Oct 12, 2023
1 parent 4c1f074 commit 4e175f1
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 8 deletions.
11 changes: 3 additions & 8 deletions src/BehaviourTreeDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* A type defining the an argument that can be passed to an agent function.
*/
export type AgentFunctionArgument = string | number | boolean | null | undefined;

/**
* An attribute for a node.
*/
Expand All @@ -14,7 +9,7 @@ export interface NodeAttributeDefinition {
/**
* An array of arguments to pass when invoking the agent function.
*/
args?: AgentFunctionArgument[];
args?: any[];
}

/**
Expand Down Expand Up @@ -87,7 +82,7 @@ export interface ActionNodeDefinition extends NodeDefinition {
*/
type: "action";
call: string;
args?: AgentFunctionArgument[];
args?: any[];
}

/**
Expand All @@ -99,7 +94,7 @@ export interface ConditionNodeDefinition extends NodeDefinition {
*/
type: "condition";
call: string;
args?: AgentFunctionArgument[];
args?: any[];
}

/**
Expand Down
157 changes: 157 additions & 0 deletions src/BehaviourTreeDefinitionValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { parseMDSLToJSON } from "./mdsl/MDSLDefinitionParser";

/**
* An object representing the result of validating a tree definition.
*/
export type DefinitionValidationResult = {
/**
* A flag defining whether validation succeeded.
*/
succeeded: boolean;
/**
* A string containing the error message if validation did not succeed.
*/
errorMessage?: string;
};

export function validateDefinition(definition: any): DefinitionValidationResult {
// The definition must be defined.
if (definition === null || typeof definition === "undefined") {
throw new Error(`definition is null or undefined`);
}

let rootNodeDefinitions: any[];

// We are expecting a definition in one of three different forms:
// - A string which we will assume is mdsl and we will parse this to JSON before validation.
// - An array which we will assume is an array of root node definitions with at least one being the primary root node (no 'id' property)
// - An object which we will assume is the primary root node and should not have an 'id' property.
if (typeof definition === "string") {
try {
// The definition is a string which we can assume is mdsl, so attempt to parse it to a JSON definition in the form of an array of root node definitions.
rootNodeDefinitions = parseMDSLToJSON(definition);
} catch (error) {
// We failed to parse the JSON from the mdsl, this is likely to be the result of it not being a valid mdsl string.
return {
succeeded: false,
errorMessage: `invalid mdsl: ${definition}`
};
}
} else if (typeof definition === "object") {
// The definition will either be an array (of root node definitions) or an object (the single primary root node definition).
// If our definition is an array, we should verify that each of the elements within it are objects (potential root node definitions).
if (Array.isArray(definition)) {
// Find any invalid node definitions in our definition array, not full validation just a check that each is a valid object.
const invalidDefinitionElements = definition.filter((element) => {
// Each element isn't valid unless it is an object that isn't also an array and isn't null.
return typeof element !== "object" || Array.isArray(element) || element === null;
});

// If we have any invalid node definitions then validation has failed.
if (invalidDefinitionElements.length) {
return {
succeeded: false,
errorMessage: "invalid elements in definition array, each must be an root node definition object"
};
}

// Our definition is already an array of root node definition objects.
rootNodeDefinitions = definition;
} else {
// Our definition is an object, but we want an array of root node definitions.
rootNodeDefinitions = [definition];
}
} else {
throw new Error(`unexpected definition type of '${typeof definition}'`);
}

// The definition could be an object (our single root node) or an array (multiple root nodes with one that has no id and the rest must have an id)
// Check that all elements in 'definition' ONE has no id AND there are no duplicate ids
// Get a list of all root nodes, we will need this to validate that all branch nodes refer to real root nodes.
// Check for circular dependencies in root node references via branches.

// Iterate over our array of root nodes and call validateNode for each, passing an initial depth of 0, wrapped in a try catch to handle validation failures.

// Our definition was valid!
return { succeeded: true };
}

function validateNode(definition: any, depth: number): void {
// Every node must be valid object and have a non-empty 'type' string property.
if (typeof definition !== "object" || typeof definition.type !== "string" || definition.type.length === 0) {
throw new Error(`node definition is not an object or 'type' property is not a non-empty string at depth '${depth}'`);
}

// How we validate this node will depend on its type.
switch (definition.type) {
case "root":
validateRootNode(definition, depth);
break;

// TODO Add cases for all other nodes.

default:
throw new Error(`unexpected node type of '${definition.type}' at depth '${depth}'`);
}
}

function validateNodeAttributes(definition: any, depth: number): void {
// Validate each of the attribute types for this node.
["while", "until", "entry", "exit", "step"].forEach((attributeName) => {
// Attempt to grab the definition for the current attribute from the node definition.
const attributeDefinition = definition[attributeName];

// All node attributes are optional, so there is nothing to do if the current attribute is not defined.
if (typeof attributeDefinition === "undefined") {
return;
}

// The attribute definition must be an object.
if (typeof attributeDefinition !== "object") {
throw new Error(`expected attribute '${attributeName}' to be an object for '${definition.type}' node at depth '${depth}'`);
}

// The 'call' property must be defined for any attribute definition.
if (typeof attributeDefinition.call !== "string" || attributeDefinition.call.length === 0) {
throw new Error(`expected 'call' property for attribute '${attributeName}' to be a non-empty string for '${definition.type}' node at depth '${depth}'`);
}

// If any node attribute arguments have been defined then they must have been defined in an array.
if (typeof attributeDefinition.args !== "undefined" && !Array.isArray(attributeDefinition.args)) {
throw new Error(`expected 'args' property for attribute '${attributeName}' to be an array for '${definition.type}' node at depth '${depth}'`);
}
});
}

/**
* Validate an object that we expect to be a root node definition.
* @param definition An object that we expect to be a root node definition.
* @param depth The depth of the node in the definition tree.
*/
function validateRootNode(definition: any, depth: number): void {
// Check that the node type is correct.
if (definition.type !== "root") {
throw new Error("expected node type of 'root' for root node");
}

// A root node cannot be the child of another node.
if (depth > 0) {
throw new Error("a root node cannot be the child of another node");
}

// Check that, if the root node 'id' property is defined, it is a non-empty string.
if (typeof definition.id !== "undefined" && (typeof definition.id !== "string" || definition.id.length === 0)) {
throw new Error("expected non-empty string for 'id' property if defined for root node");
}

// A root node is a decorator node, so must have a child node defined.
if (typeof definition.child === "undefined") {
throw new Error("expected property 'child' to be defined for root node");
}

// Validate the node attributes.
validateNodeAttributes(definition, depth);

// Validate the child node of this decorator node.
validateNode(definition.child, depth + 1);
}

0 comments on commit 4e175f1

Please sign in to comment.