diff --git a/src/utils/typescript/typescript-object.ts b/src/utils/typescript/typescript-object.ts new file mode 100644 index 0000000..10cdaee --- /dev/null +++ b/src/utils/typescript/typescript-object.ts @@ -0,0 +1,280 @@ +import { TypeScriptMember } from "./typescript-member"; +import { TypeScriptProperty, TypeScriptPropertyOptions } from "./typescript-property"; +import { getIndentation, getNewline, incrementIndent, TypeScriptFormattingOptions } from "./typescript-formatting-options"; +import { $i } from "@/utils/collections/iterable"; +import { decomposeType, TypeScriptTypeDefinition } from "./typescript-type-definition"; +import { TypeScriptUnionType } from "./typescript-union-type"; + +/** + * Represents a TypeScript object type definition. + */ +export class TypeScriptObject implements TypeScriptTypeDefinition, Iterable { + /** + * An internal data structure that stores the members of the TypeScriptObject instance. + */ + private readonly _members: Map; + + /** + * Constructs a new {@link TypeScriptObject} instance. + */ + private constructor() { + this._members = new Map(); + } + + /** + * Creates a new {@link TypeScriptObject} instance. + * + * @returns A new {@link TypeScriptObject} instance. + */ + static create(): TypeScriptObject { + return new TypeScriptObject(); + } + + /** + * @inheritdoc + */ + get isComposite(): false { + return false; + } + + /** + * @inheritdoc + */ + get isUnion(): false { + return false; + } + + /** + * @inheritdoc + */ + get isIntersection(): false { + return false; + } + + /** + * @inheritdoc + */ + get isAlias(): boolean { + return false; + } + + /** + * @inheritdoc + */ + composingTypes(): Iterable { + return [this]; + } + + /** + * Returns an iterable of all members in this object. + */ + members(): Iterable { + return this._members.values(); + } + + /** + * Returns an iterable of all properties in this object. + */ + properties(): Iterable { + return $i(this).filter((x): x is TypeScriptProperty => x instanceof TypeScriptProperty); + } + + /** + * Retrieves the specified member from this object. + * + * @param name - The name of the member to retrieve. + * + * @returns The specified member, or `undefined` if it does not exist. + */ + getMember(name: string): TypeScriptMember | undefined { + return this._members.get(name); + } + + /** + * Retrieves the specified nested member from this object. + * + * @param name - The name or path of the nested member to retrieve. + * + * @returns The specified nested member or undefined if it does not exist. + */ + getNestedMember(name: string | string[]): TypeScriptMember | undefined { + const path = typeof name === "string" ? name.split(".") : name; + if (!path || !path.length) { + return undefined; + } + + const member = this.getMember(path[0]); + if (path.length === 1) { + return member; + } + + if (!(member instanceof TypeScriptProperty) || !(member.type instanceof TypeScriptObject)) { + return undefined; + } + return member.type.getNestedMember(path.slice(1)); + } + + /** + * Determines whether this object contains a member with the specified name. + * + * @param name - The name of the member to search for. + * + * @returns `true` if the member exists; otherwise, `false`. + */ + hasMember(name: string): boolean { + return this.getMember(name) !== undefined; + } + + /** + * Determines whether this object contains a nested member with the specified name or path. + * + * @param name - The name or path of the nested member to search for. + * + * @returns `true` if the nested member exists; otherwise, `false`. + */ + hasNestedMember(name: string | string[]): boolean { + return this.getNestedMember(name) !== undefined; + } + + /** + * Adds the specified member to this object. + * + * @param member - The member to add. + * + * @returns The member that was added to this object. + */ + addMember(member: T): T { + this._members.set(member.name, member); + return member; + } + + /** + * Deletes the specified member from this object. + * + * @param member - The member to delete. + * + * @returns `true` if the member was deleted; otherwise, `false`. + */ + deleteMember(member: TypeScriptMember): boolean { + return this._members.delete(member.name); + } + + /** + * Retrieves the specified property from this object. + * + * @param name - The name of the property to retrieve. + * + * @returns The specified property, or `undefined` if it does not exist. + */ + getProperty(name: string): TypeScriptProperty | undefined { + const property = this.getMember(name); + return property instanceof TypeScriptProperty ? property : undefined; + } + + /** + * Retrieves the specified nested property from this object. + * + * @param name - The name or path of the nested property to retrieve. + * + * @returns The specified nested property, or `undefined` if it does not exist. + */ + getNestedProperty(name: string | string[]): TypeScriptProperty | undefined { + const property = this.getNestedMember(name); + return property instanceof TypeScriptProperty ? property : undefined; + } + + /** + * Determines whether this object contains a property with the specified name. + * + * @param name - The name of the property to search for. + * + * @returns `true` if the property exists; otherwise, `false`. + */ + hasProperty(name: string): boolean { + return this.getProperty(name) !== undefined; + } + + /** + * Determines whether this object contains a nested property with the specified name or path. + * + * @param name - The name or path of the nested property to search for. + * + * @returns `true` if the nested property exists; otherwise, `false`. + */ + hasNestedProperty(name: string | string[]): boolean { + return this.getNestedProperty(name) !== undefined; + } + + /** + * Adds a new property with the specified name and type to this object. + * + * @param name - The name of the new property. + * @param type - The type of the new property. + * @param isOptional - Indicates whether the property is optional or not. + * + * @returns The property that was added to this object. + */ + addProperty(name: string, type: TypeScriptTypeDefinition, options?: TypeScriptPropertyOptions): TypeScriptProperty { + return this.addMember(TypeScriptProperty.create(name, type, options)); + } + + /** + * Adds a new nested property with the specified name or path and type to this object. + * + * @param name - The name or path of the new nested property. + * @param type - The type of the new nested property. + * @param isOptional - Indicates whether the property is optional or not. + * + * @returns The nested property that was added to this object. + */ + addNestedProperty(name: string | string[], type: TypeScriptTypeDefinition, options?: TypeScriptPropertyOptions): TypeScriptProperty { + const path = typeof name === "string" ? name.split(".") : name; + const localPropertyName = path[0]; + if (path.length === 1) { + return this.addProperty(localPropertyName, type, options); + } + + if (!this.hasProperty(localPropertyName)) { + const nestedObject = TypeScriptObject.create(); + const nestedProperty = nestedObject.addNestedProperty(path.slice(1), type, options); + this.addProperty(localPropertyName, nestedObject, options); + return nestedProperty; + } + + let localProperty = this.getProperty(localPropertyName); + let nestedObject = $i(decomposeType(localProperty.type)).first((x): x is TypeScriptObject => x instanceof TypeScriptObject); + if (!nestedObject) { + nestedObject = TypeScriptObject.create(); + localProperty = localProperty.with({ type: TypeScriptUnionType.create([localProperty.type, nestedObject]) }); + this.addMember(localProperty); + } + + return nestedObject.addNestedProperty(path.slice(1), type, options); + } + + /** + * @inheritdoc + */ + format(options?: TypeScriptFormattingOptions): string { + const indent = getIndentation(options); + const newline = getNewline(options); + const doubleNewline = newline + newline; + const indentedOptions = incrementIndent(options); + + const formattedMembers = $i(this).map(x => x.format(indentedOptions)).join(doubleNewline); + const formattedObject = ( + `${indent}{` + + `${newline}${formattedMembers}${newline}` + + `${indent}}` + ); + + return formattedObject; + } + + /** + * Returns an iterator over all members in this object. + */ + [Symbol.iterator](): Iterator { + return this.members()[Symbol.iterator](); + } +}