From c03d4c8d077f4ba571289874b027349b98381c33 Mon Sep 17 00:00:00 2001 From: BellCube Dev <33764825+BellCubeDev@users.noreply.github.com> Date: Sat, 23 Dec 2023 12:59:58 -0500 Subject: [PATCH] Enums For Everything & File Organization --- .eslintrc.json | 5 +- package.json | 2 +- src/DomUtils.ts | 12 ++ src/definitions/Enums.ts | 180 ++++++++++++++++++ src/definitions/Group.ts | 101 ---------- src/definitions/Metadata.ts | 8 +- src/definitions/index.ts | 13 +- src/definitions/{ => lib}/FlagInstance.ts | 2 +- .../{ => lib}/InvalidityReporting.ts | 0 src/definitions/{ => lib}/_core.ts | 9 +- src/definitions/lib/index.ts | 3 + src/definitions/{ => module}/Dependencies.ts | 83 ++++---- src/definitions/{ => module}/Fomod.ts | 74 +++---- src/definitions/module/Group.ts | 86 +++++++++ src/definitions/{ => module}/Install.ts | 75 ++++---- src/definitions/{ => module}/Option.ts | 120 ++++++------ src/definitions/{ => module}/Step.ts | 39 ++-- src/definitions/module/index.ts | 6 + src/index.ts | 3 +- src/parse.ts | 2 +- tests/build-from-scratch.test.ts | 4 +- tests/definitions/Dependencies.test.ts | 1 - tests/definitions/Fomod.test.ts | 12 +- tests/definitions/Install.test.ts | 12 +- tests/definitions/Option.test.ts | 21 +- tests/definitions/all.test.ts | 3 +- 26 files changed, 523 insertions(+), 353 deletions(-) create mode 100644 src/definitions/Enums.ts delete mode 100644 src/definitions/Group.ts rename src/definitions/{ => lib}/FlagInstance.ts (99%) rename src/definitions/{ => lib}/InvalidityReporting.ts (100%) rename src/definitions/{ => lib}/_core.ts (93%) create mode 100644 src/definitions/lib/index.ts rename src/definitions/{ => module}/Dependencies.ts (73%) rename src/definitions/{ => module}/Fomod.ts (69%) create mode 100644 src/definitions/module/Group.ts rename src/definitions/{ => module}/Install.ts (84%) rename src/definitions/{ => module}/Option.ts (77%) rename src/definitions/{ => module}/Step.ts (59%) create mode 100644 src/definitions/module/index.ts diff --git a/.eslintrc.json b/.eslintrc.json index 334bfa1..1f49eb0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -69,6 +69,7 @@ "prefer-template": "off", "func-style": "off", "@typescript-eslint/no-this-alias": "off", - "github/array-foreach": "off" + "github/array-foreach": "off", + "no-explicit-any": "off" } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 1cd6a13..4a94ac2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "fomod", "description": "A library for creating, parsing, editing, and validating XML-based Fomod installers, widely popularized in the Bethesda modding scene", - "version": "0.1.12", + "version": "0.1.13", "main": "dist/index.js", "repository": "https://github.com/BellCubeDev/fomod-js/", "bugs": { diff --git a/src/DomUtils.ts b/src/DomUtils.ts index 028f6b6..fac6821 100644 --- a/src/DomUtils.ts +++ b/src/DomUtils.ts @@ -1,3 +1,15 @@ +import { TagName } from "./definitions"; + +/** Searches for an existing tag by the provided name. If one does not yet exist, create one instead. + * + * @param fromElement The parent element + * @param tagName The tag name to search for, restricted to members of the TagName enum + * @returns The first element by the provided tagname or a new one if it did not yet exist. + */ +export function getOrCreateElementByTagNameSafe(fromElement: Element, tagName: TagName): Element { + return getOrCreateElementByTagName(fromElement, tagName); +} + /** Searches for an existing tag by the provided name. If one does not yet exist, create one instead. * * @param fromElement The parent element diff --git a/src/definitions/Enums.ts b/src/definitions/Enums.ts new file mode 100644 index 0000000..154bff3 --- /dev/null +++ b/src/definitions/Enums.ts @@ -0,0 +1,180 @@ +/** A valid tag for an XML-Based Fomod installer */ +export enum TagName { + Fomod = 'fomod', + Config = 'config', + ModuleName = 'moduleName', + ModuleImage = 'moduleImage', + ModuleDependencies = 'moduleDependencies', + Dependencies = 'dependencies', + // Dependencies = 'dependencies', + FileDependency = 'fileDependency', + FlagDependency = 'flagDependency', + GameDependency = 'gameDependency', + FOMMDependency = 'fommDependency', + FOSEDependency = 'foseDependency', + RequiredInstallFiles = 'requiredInstallFiles', + File = 'file', + Folder = 'folder', + InstallSteps = 'installSteps', + InstallStep = 'installStep', + Visible = 'visible', + // Dependencies = 'dependencies', + // Dependencies = 'dependencies', + // FileDependency = 'fileDependency', + // FlagDependency = 'flagDependency', + // GameDependency = 'gameDependency', + // FOMMDependency = 'fommDependency', + // FOSEDependency = 'foseDependency', + OptionalFileGroups = 'optionalFileGroups', + Group = 'group', + Plugins = 'plugins', + Plugin = 'plugin', + Description = 'description', + Image = 'image', + ConditionFlags = 'conditionFlags', + Flag = 'flag', + Files = 'files', + //File = 'file', + //Folder = 'folder', + TypeDescriptor = 'typeDescriptor', + Type = 'type', + DependencyType = 'dependencyType', + DefaultType = 'defaultType', + Patterns = 'patterns', + Pattern = 'pattern', + // Dependencies = 'dependencies', + // Dependencies = 'dependencies', + // FileDependency = 'fileDependency', + // FlagDependency = 'flagDependency', + // GameDependency = 'gameDependency', + // FOMMDependency = 'fommDependency', + // FOSEDependency = 'foseDependency', + //Type = 'type', + ConditionalFileInstalls = 'conditionalFileInstalls', + // Patterns = 'patterns', + // Pattern = 'pattern', + // Dependencies = 'dependencies', + // Dependencies = 'dependencies', + // FileDependency = 'fileDependency', + // FlagDependency = 'flagDependency', + // GameDependency = 'gameDependency', + // FOMMDependency = 'fommDependency', + // FOSEDependency = 'foseDependency', +} + +/** A valid attribute for an XML-Based Fomod installer */ +export enum AttributeName { + // moduleName + Color = 'colour', + Colour = 'colour', + + // moduleImage, image + Path = 'path', + + // moduleImage + Position = 'position', + Height = 'height', + ShowFade = 'showFade', + ShowImage = 'showImage', + + // moduleDependencies, dependencies, visible + Operator = 'operator', + + // file, folder + Source = 'source', + Destination = 'destination', + Priority = 'priority', + AlwaysInstall = 'alwaysInstall', + InstallIfUsable = 'installIfUsable', + + // installSteps, optionalFileGroups, plugins + Order = 'order', + + // installStep, group, plugin, flag, type, defaultType + Name = 'name', + + // group + Type = 'type', + + // fileDependency + File = 'file', + State = 'state', + + // flagDependency + Flag = 'flag', + Value = 'value', + + // gameDependency, fommDependency, foseDependency + Version = 'version', +} + + +/** Describes how the group should behave when allowing users to select its options */ +export enum GroupBehaviorType { + /** Users may select or deselect any otherwise-selectable option within the group without restriction. */ + SelectAny = 'SelectAny', + /** Users must select at least one otherwise-selectable option within the group. */ + SelectAtLeastOne = 'SelectAtLeastOne', + /** Users may select no option or a single, otherwise-selectable option within the group. */ + SelectAtMostOne = 'SelectAtMostOne', + /** Users must select exactly one otherwise-selectable option within the group; no more, no less. This is the default behavior. */ + SelectExactlyOne = 'SelectExactlyOne', + /** All options in the group are forcibly selected and cannot be deselected. */ + SelectAll = 'SelectAll' +} + +/** Describes how an option should behave in regard to user selection */ +export enum OptionType { + /** The option will not be selected and cannot be selected. */ + NotUsable = 'NotUsable', + /** Acts the same as `Optional`, except that mod managers may show a warning to the user when selecting this option. This is not universal, though, and the majority of mainstream mod managers at the moment forego this. */ + CouldBeUsable = 'CouldBeUsable', + /** The option will be selectable. This is the default behavior. */ + Optional = 'Optional', + /** The option will be selected by default but may be deselected. */ + Recommended = 'Recommended', + /** The option will be selected by default and cannot be deselected. */ + Required = 'Required', +} + +/** Describes how the group should behave when allowing users to select its options */ +export enum SortingOrder { + /** Items are ordered alphabetically starting with A and ending with Z. This is the default behavior. */ + Ascending = 'Ascending', + /** Items are ordered alphabetically starting with Z and ending with A. */ + Descending = 'Descending', + /** Items are ordered precisely as they appear in the XML (and, consequently, the Set within JS) */ + Explicit = 'Explicit', +} + +/** A state which this FileDependency expects the file to be in to be satisfied */ +export enum FileDependencyState { + /** The file must not exist on the user's system */ + Missing = 'Missing', + /** The file must be on the user's system but NOT loaded by the game */ + Inactive = 'Inactive', + /** The file must be on the user's system and loaded by the game */ + Active = 'Active', +} + +/** An operator which determines the behavior of the dependency group */ +export enum DependencyGroupOperator { + /** The dependency group is satisfied if *any* of its children are satisfied */ + Or = 'Or', + /** The dependency group is only satisfied if **all** of its children are satisfied */ + And = 'And', +} + +export enum ModuleNamePosition { + /** Positions the title on the left side of the form header. */ + Left = 'Left', + /** Positions the title on the right side of the form header. */ + Right = 'Right', + /** Positions the title on the right side of the image in the form header. */ + RightOfImage = 'RightOfImage', +} + +export enum BooleanString { + true = 'true', + false = 'false', +} diff --git a/src/definitions/Group.ts b/src/definitions/Group.ts deleted file mode 100644 index 961b42b..0000000 --- a/src/definitions/Group.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { getOrCreateElementByTagName } from "../DomUtils"; -import { InvalidityReason, InvalidityReport } from "./InvalidityReporting"; -import { Option } from "./Option"; -import { SortingOrder } from "./Step"; -import { ElementObjectMap, Verifiable, XmlRepresentation } from "./_core"; - -/** Describes how the group should behave when allowing users to select its options */ -export enum GroupBehaviorType { - /** Users may select or deselect any otherwise-selectable option within the group without restriction. */ - SelectAny = 'SelectAny', - /** Users must select at least one otherwise-selectable option within the group. */ - SelectAtLeastOne = 'SelectAtLeastOne', - /** Users may select no option or a single, otherwise-selectable option within the group. */ - SelectAtMostOne = 'SelectAtMostOne', - /** Users must select exactly one otherwise-selectable option within the group; no more, no less. This is the default behavior. */ - SelectExactlyOne = 'SelectExactlyOne', - /** All options in the group are forcibly selected and cannot be deselected. */ - SelectAll = 'SelectAll' -} - -export class Group extends XmlRepresentation { - static override readonly tagName = 'group'; - readonly tagName = 'group'; - - - constructor( - public name: string = '', - public behaviorType: TStrict extends true ? GroupBehaviorType : string = GroupBehaviorType.SelectExactlyOne, - public sortingOrder: TStrict extends true ? SortingOrder : string = SortingOrder.Ascending, - public options: Set> = new Set(), - ) { - super(); - } - - asElement(document: Document): Element { - const element = this.getElementForDocument(document); - - element.setAttribute('name', this.name); - element.setAttribute('type', this.behaviorType); - - const optionsContainer = getOrCreateElementByTagName(element, 'plugins'); - - optionsContainer.setAttribute('order', this.sortingOrder); - for (const option of this.options) optionsContainer.appendChild(option.asElement(document)); - - return element; - } - - isValid(): this is Group { - return ( - ( this.behaviorType === 'SelectAny' || this.behaviorType === 'SelectAtLeastOne' || this.behaviorType === 'SelectAtMostOne' || this.behaviorType === 'SelectExactlyOne' || this.behaviorType === 'SelectAll' ) && - ( this.sortingOrder === 'Ascending' || this.sortingOrder === 'Descending' || this.sortingOrder === 'Explicit' ) && - Array.from(this.options).every(option => option.isValid()) - ); - } - - reasonForInvalidity(...tree: Omit, 'isValid' | 'reasonForInvalidity'>[]): InvalidityReport | null { - tree.push(this); - - if (this.behaviorType !== 'SelectAny' && this.behaviorType !== 'SelectAtLeastOne' && this.behaviorType !== 'SelectAtMostOne' && this.behaviorType !== 'SelectExactlyOne' && this.behaviorType !== 'SelectAll') - return { reason: InvalidityReason.GroupUnknownBehaviorType, offendingValue: this.behaviorType, tree }; - - if (this.sortingOrder !== 'Ascending' && this.sortingOrder !== 'Descending' && this.sortingOrder !== 'Explicit') - return { reason: InvalidityReason.GroupUnknownOptionSortingOrder, offendingValue: this.sortingOrder, tree }; - - for (const option of this.options) { - const reason = option.reasonForInvalidity(...tree); - if (reason !== null) return reason; - } - - return null; - } - - static override parse(element: Element): Group { - const existing = ElementObjectMap.get(element); - if (existing && existing instanceof this) return existing; - - const name = element.getAttribute('name'); - const behaviorType = element.getAttribute('type'); - - const group = new Group(name ?? '', behaviorType ?? ''); - group.assignElement(element); - - const optionsContainer = element.querySelector('plugins'); - if (optionsContainer === null) return group; - - const sortingOrder = optionsContainer.getAttribute('order'); - if (sortingOrder !== null) group.sortingOrder = sortingOrder; - - for (const optionElement of optionsContainer.querySelectorAll('plugin')) { - const option = Option.parse(optionElement); - if (option !== null) group.options.add(option); - } - - return group; - } - - decommission(currentDocument?: Document) { - this.options.forEach(option => option.decommission(currentDocument)); - } -} diff --git a/src/definitions/Metadata.ts b/src/definitions/Metadata.ts index 8396354..7002040 100644 --- a/src/definitions/Metadata.ts +++ b/src/definitions/Metadata.ts @@ -1,5 +1,5 @@ -import { InvalidityReport } from "./InvalidityReporting"; -import { ElementObjectMap, Verifiable, XmlRepresentation } from "./_core"; +import { TagName } from "./Enums"; +import { ElementObjectMap, XmlRepresentation } from "./lib/_core"; export interface FomodInfoData { [key: string]: string|undefined; @@ -28,8 +28,8 @@ export const DefaultInfoSchema = 'https://fomod.bellcube.dev/schemas/info.xsd'; * Explicit types are given for known values, however there are no explicit specifications given for what is allowed or expected in the file. */ export class FomodInfo extends XmlRepresentation { - static override readonly tagName = 'fomod'; - readonly tagName = 'fomod'; + static override readonly tagName = TagName.Fomod; + readonly tagName = TagName.Fomod; constructor( public data: FomodInfoData = {} diff --git a/src/definitions/index.ts b/src/definitions/index.ts index 4caabdd..d64cb8b 100644 --- a/src/definitions/index.ts +++ b/src/definitions/index.ts @@ -1,10 +1,5 @@ -export * from './_core'; -export * from './Dependencies'; -export * from './FlagInstance'; -export * from './Fomod'; -export * from './Group'; -export * from './Install'; -export * from './InvalidityReporting'; +export * from './lib'; +export * from './module'; + +export * from './Enums'; export * from './Metadata'; -export * from './Option'; -export * from './Step'; diff --git a/src/definitions/FlagInstance.ts b/src/definitions/lib/FlagInstance.ts similarity index 99% rename from src/definitions/FlagInstance.ts rename to src/definitions/lib/FlagInstance.ts index 4d7b27a..871ba1c 100644 --- a/src/definitions/FlagInstance.ts +++ b/src/definitions/lib/FlagInstance.ts @@ -1,4 +1,4 @@ -import { Option } from './Option'; +import { Option } from '../module/Option'; interface FlagInstances { all: Set>; diff --git a/src/definitions/InvalidityReporting.ts b/src/definitions/lib/InvalidityReporting.ts similarity index 100% rename from src/definitions/InvalidityReporting.ts rename to src/definitions/lib/InvalidityReporting.ts diff --git a/src/definitions/_core.ts b/src/definitions/lib/_core.ts similarity index 93% rename from src/definitions/_core.ts rename to src/definitions/lib/_core.ts index c9039d9..c98acbc 100644 --- a/src/definitions/_core.ts +++ b/src/definitions/lib/_core.ts @@ -1,5 +1,6 @@ -import { ensureXmlDoctype } from "../DomUtils"; -import { InvalidityReason, InvalidityReport } from "./InvalidityReporting"; +import { ensureXmlDoctype } from "../../DomUtils"; +import { TagName } from "../Enums"; +import { InvalidityReport } from "./InvalidityReporting"; /** The foundation of a class that can be validated against a schema. * @@ -24,8 +25,8 @@ export const ElementObjectMap = new WeakMap> * */ export abstract class XmlRepresentation extends Verifiable { - static readonly tagName: string|string[]; - abstract readonly tagName: string; + static readonly tagName: TagName|TagName[]; + abstract readonly tagName: TagName; /** A [weak map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) of documents to this representation's associated element within that document. * diff --git a/src/definitions/lib/index.ts b/src/definitions/lib/index.ts new file mode 100644 index 0000000..96a2430 --- /dev/null +++ b/src/definitions/lib/index.ts @@ -0,0 +1,3 @@ +export * from './_core'; +export * from './FlagInstance'; +export * from './InvalidityReporting'; diff --git a/src/definitions/Dependencies.ts b/src/definitions/module/Dependencies.ts similarity index 73% rename from src/definitions/Dependencies.ts rename to src/definitions/module/Dependencies.ts index ffa8d9a..5ba4428 100644 --- a/src/definitions/Dependencies.ts +++ b/src/definitions/module/Dependencies.ts @@ -1,7 +1,8 @@ -import { FlagInstance } from "./FlagInstance"; -import { ElementObjectMap, Verifiable, XmlRepresentation } from "./_core"; +import { FlagInstance } from "../lib/FlagInstance"; +import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/_core"; import { Option } from './Option'; -import { InvalidityReason, InvalidityReport } from './InvalidityReporting'; +import { InvalidityReason, InvalidityReport } from '../lib/InvalidityReporting'; +import { AttributeName, BooleanString, DependencyGroupOperator, FileDependencyState, TagName } from '../Enums'; /** A parent class to all forms of dependency. * @@ -33,27 +34,29 @@ export abstract class Dependency extends XmlR } } +type DependencyTagName = TagName.Dependencies|TagName.ModuleDependencies|TagName.Visible; - -export class Dependencies extends Dependency { - static override readonly tagName = ['moduleDependencies', 'dependencies']; +export class Dependencies extends Dependency { + static override readonly tagName = [TagName.Dependencies, TagName.ModuleDependencies, TagName.Visible] as [TagName.Dependencies, TagName.ModuleDependencies, TagName.Visible]; constructor( public readonly tagName: TTagName, - public operator: TStrict extends true ? 'And' | 'Or' : string = 'And', + public operator: TStrict extends true ? DependencyGroupOperator : string = DependencyGroupOperator.And, public dependencies: Set> = new Set() ) { super(); } isValid(): this is Dependencies { - return (this.operator === 'And' || this.operator === 'Or') && Array.from(this.dependencies).every(d => d.isValid()); + return Object.values(DependencyGroupOperator).includes(this.operator as any) && + Array.from(this.dependencies).every(d => d.isValid()); } reasonForInvalidity(...tree: Omit, "isValid" | "reasonForInvalidity">[]): InvalidityReport | null { tree.push(this); - if (this.operator !== 'And' && this.operator !== 'Or') return {reason: InvalidityReason.DependenciesUnknownOperator, offendingValue: this.operator, tree}; + if (!Object.values(DependencyGroupOperator).includes(this.operator as any)) + return {reason: InvalidityReason.DependenciesUnknownOperator, offendingValue: this.operator, tree}; for (const dependency of this.dependencies) { const reason = dependency.reasonForInvalidity(...tree); @@ -66,7 +69,7 @@ export class Dependencies(element: Element): Dependencies { + static override parse(element: Element): Dependencies { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const tagName = element.tagName as TTagName; - const operator = element.getAttribute('operator') ?? 'And'; + const operator = element.getAttribute(AttributeName.Operator) ?? DependencyGroupOperator.And; const dependencies = Array.from(element.children).map(Dependency.parse).filter((d): d is Dependency => d !== null); const obj = new Dependencies(tagName, operator, new Set(dependencies)); @@ -97,21 +100,21 @@ export class Dependencies extends Dependency { - static override readonly tagName = 'fileDependency'; - readonly tagName = 'fileDependency'; + static override readonly tagName = TagName.FileDependency; + readonly tagName = TagName.FileDependency; - constructor(public filePath: string = '', public desiredState: TStrict extends true ? 'Active'|'Inactive'|'Missing' : string = 'Active') { + constructor(public filePath: string = '', public desiredState: TStrict extends true ? FileDependencyState : string = FileDependencyState.Active) { super(); } isValid(): this is FileDependency { - return this.desiredState === 'Active' || this.desiredState === 'Inactive' || this.desiredState === 'Missing'; + return Object.values(FileDependencyState).includes(this.desiredState as any); } reasonForInvalidity(...tree: Omit, 'isValid' | 'reasonForInvalidity'>[]): InvalidityReport | null { tree.push(this); - if (this.desiredState !== 'Active' && this.desiredState !== 'Inactive' && this.desiredState !== 'Missing') + if (!Object.values(FileDependencyState).includes(this.desiredState as any)) return {reason: InvalidityReason.DependencyFileInvalidState, offendingValue: this.desiredState, tree}; return null; @@ -120,8 +123,8 @@ export class FileDependency extends Dependency override asElement(document: Document): Element { const element = this.getElementForDocument(document); - element.setAttribute('file', this.filePath); - element.setAttribute('state', this.desiredState); + element.setAttribute(AttributeName.File, this.filePath); + element.setAttribute(AttributeName.State, this.desiredState); return element; } @@ -130,8 +133,8 @@ export class FileDependency extends Dependency const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; - const filePath = element.getAttribute('file') ?? ''; - const desiredState = element.getAttribute('state') ?? 'Active'; + const filePath = element.getAttribute(AttributeName.File) ?? ''; + const desiredState = element.getAttribute(AttributeName.State) ?? FileDependencyState.Active; const obj = new FileDependency(filePath, desiredState); obj.assignElement(element); @@ -146,8 +149,8 @@ export class FileDependency extends Dependency export class FlagDependency extends Dependency { - static override readonly tagName = 'flagDependency'; - readonly tagName = 'flagDependency'; + static override readonly tagName = TagName.FlagDependency; + readonly tagName = TagName.FlagDependency; protected readonly flagInstance: FlagInstance; @@ -180,12 +183,12 @@ export class FlagDependency extends Dependency { if (typeof this.flagKey === 'string') { if (typeof this.desiredValue !== 'string') throw new Error('Flag dependency `name` value is a string but `value` is a boolean! Expected string.', {cause: this}); - element.setAttribute('flag', this.flagKey); - element.setAttribute('value', this.desiredValue); + element.setAttribute(AttributeName.Flag, this.flagKey); + element.setAttribute(AttributeName.Value, this.desiredValue); } else { if (typeof this.flagInstance !== 'boolean') throw new Error('Flag dependency `name` value is an Option but `value` is a string! Expected boolean.', {cause: this}); - element.setAttribute('flag', this.flagKey.getFlagName(document)); - element.setAttribute('value', this.desiredValue ? 'true' : 'false'); + element.setAttribute(AttributeName.Flag, this.flagKey.getFlagName(document)); + element.setAttribute(AttributeName.Value, this.desiredValue ? BooleanString.true : BooleanString.false); } return element; @@ -195,8 +198,8 @@ export class FlagDependency extends Dependency { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; - const flagName = element.getAttribute('flag') ?? ''; // TODO: Parse Option - const desiredValue = element.getAttribute('value') ?? ''; + const flagName = element.getAttribute(AttributeName.Flag) ?? ''; // TODO: Parse Option Flags + const desiredValue = element.getAttribute(AttributeName.Value) ?? ''; const obj = new FlagDependency(flagName, desiredValue); obj.assignElement(element); @@ -229,7 +232,7 @@ export abstract class VersionDependency extends Dependency { override asElement(document: Document): Element { const element = this.getElementForDocument(document); - element.setAttribute('version', this.desiredVersion); + element.setAttribute(AttributeName.Version, this.desiredVersion); return element; } @@ -239,7 +242,7 @@ export abstract class VersionDependency extends Dependency { if (existing && (existing instanceof ScriptExtenderVersionDependency || existing instanceof GameVersionDependency || existing instanceof ModManagerVersionDependency)) return existing; - const desiredVersion = element.getAttribute('version') ?? ''; + const desiredVersion = element.getAttribute(AttributeName.Version) ?? ''; let obj: ReturnType = null; @@ -265,15 +268,15 @@ export abstract class VersionDependency extends Dependency { * Can be useful in a number of circumstances where the game executable version matters, such as determining what version of a script extender plugin to install. */ export class GameVersionDependency extends VersionDependency { - static override readonly tagName = 'gameDependency'; - readonly tagName = 'gameDependency'; + static override readonly tagName = TagName.GameDependency; + readonly tagName = TagName.GameDependency; constructor(desiredVersion?: string) { super(desiredVersion); } static override parse(element: Element): GameVersionDependency { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; - const desiredVersion = element.getAttribute('version') ?? ''; + const desiredVersion = element.getAttribute(AttributeName.Version) ?? ''; const obj = new GameVersionDependency(desiredVersion); obj.assignElement(element); @@ -288,15 +291,15 @@ export class GameVersionDependency extends VersionDependency { * Can be useful in a number of circumstances where the script extender version matters, such as determining if a mod is compatible with the given version. */ export class ScriptExtenderVersionDependency extends VersionDependency { - static override readonly tagName = 'foseDependency'; - readonly tagName = 'foseDependency'; + static override readonly tagName = TagName.FOSEDependency; + readonly tagName = TagName.FOSEDependency; constructor(desiredVersion?: string) { super(desiredVersion); } static override parse(element: Element): ScriptExtenderVersionDependency { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; - const desiredVersion = element.getAttribute('version') ?? ''; + const desiredVersion = element.getAttribute(AttributeName.Version) ?? ''; const obj = new ScriptExtenderVersionDependency(desiredVersion); obj.assignElement(element); @@ -312,15 +315,15 @@ export class ScriptExtenderVersionDependency extends VersionDependency { * @deprecated Should generally not be used as the value is inconsistent between mod managers. Included for completeness. */ export class ModManagerVersionDependency extends VersionDependency { - static override readonly tagName = 'fommDependency'; - readonly tagName = 'fommDependency'; + static override readonly tagName = TagName.FOMMDependency; + readonly tagName = TagName.FOMMDependency; constructor(desiredVersion?: string) { super(desiredVersion); } static override parse(element: Element): ModManagerVersionDependency { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; - const desiredVersion = element.getAttribute('version') ?? ''; + const desiredVersion = element.getAttribute(AttributeName.Version) ?? ''; const obj = new ModManagerVersionDependency(desiredVersion); obj.assignElement(element); diff --git a/src/definitions/Fomod.ts b/src/definitions/module/Fomod.ts similarity index 69% rename from src/definitions/Fomod.ts rename to src/definitions/module/Fomod.ts index 2d72803..9f4d7c7 100644 --- a/src/definitions/Fomod.ts +++ b/src/definitions/module/Fomod.ts @@ -1,18 +1,19 @@ -import { getOrCreateElementByTagName } from "../DomUtils"; +import { getOrCreateElementByTagNameSafe } from "../../DomUtils"; import { Dependencies } from "./Dependencies"; import { Install, InstallPattern } from "./Install"; -import { InvalidityReason, InvalidityReport } from "./InvalidityReporting"; -import { SortingOrder, Step } from "./Step"; -import { ElementObjectMap, Verifiable, XmlRepresentation } from "./_core"; +import { InvalidityReason, InvalidityReport } from "../lib/InvalidityReporting"; +import { Step } from "./Step"; +import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/_core"; +import { AttributeName, BooleanString, ModuleNamePosition, SortingOrder, TagName } from "../Enums"; export interface ModuleImageMetadata { showFade?: TStrict extends true ? `${boolean}` : string; showImage?: TStrict extends true ? `${boolean}` : string; - height?: TStrict extends true ? `${bigint}`|'' : string; + height?: TStrict extends true ? `${bigint}` | '' : string; } export interface ModuleNameMetadata { - position?: TStrict extends true ? 'Left'|'Right'|'RightOfImage' : string; + position?: TStrict extends true ? ModuleNamePosition : string; /** Must be an XML-valid hex string. XML-valid hex strings accept the standard `1234567890ABCDEF` range and the length must be even * @@ -32,8 +33,8 @@ function attrToObject(attributes: Iterable | ArrayLike): Record extends XmlRepresentation { - static override readonly tagName = 'config'; - readonly tagName = 'config'; + static override readonly tagName = TagName.Config; + readonly tagName = TagName.Config; @@ -44,12 +45,12 @@ export class Fomod extends XmlRepresentation { * Note that Info.xml also has its own name. */ public moduleName: string = '', - public moduleImage: string|null = null, + public moduleImage: string | null = null, /** Dependencies required for this FOMOD to be shown to the user. * * Mod managers will show the user an error message if attempting to install a FOMOD that does not meet the requirements specified here. */ - public moduleDependencies: Dependencies<'moduleDependencies', TStrict> = new Dependencies<'moduleDependencies', TStrict>('moduleDependencies'), + public moduleDependencies: Dependencies = new Dependencies(TagName.ModuleDependencies), /** Top-level file installs for the FOMOD * * Covers both the `requiredInstallFiles` and `conditionalFileInstalls` tags. @@ -79,9 +80,9 @@ export class Fomod extends XmlRepresentation { isValid(): this is Fomod { return ( - (!this.moduleNameMetadata.position || this.moduleNameMetadata.position === 'Left' || this.moduleNameMetadata.position === 'Right' || this.moduleNameMetadata.position === 'RightOfImage') && - (!this.moduleImageMetadata.showFade || this.moduleImageMetadata.showFade === 'true' || this.moduleImageMetadata.showFade === 'false') && - (!this.moduleImageMetadata.showImage || this.moduleImageMetadata.showImage === 'true' || this.moduleImageMetadata.showImage === 'false') && + (!this.moduleNameMetadata.position || Object.values(ModuleNamePosition).includes(this.moduleNameMetadata.position as any)) && + (!this.moduleImageMetadata.showFade || Object.values(BooleanString).includes(this.moduleImageMetadata.showFade as any)) && + (!this.moduleImageMetadata.showImage || Object.values(BooleanString).includes(this.moduleImageMetadata.showImage as any)) && !isNaN(parseInt(this.moduleNameMetadata.colour ?? '2', 16)) && !isNaN(parseInt(this.moduleImageMetadata.height ?? '1')) && (this.moduleDependencies?.isValid() ?? true) && @@ -93,7 +94,7 @@ export class Fomod extends XmlRepresentation { reasonForInvalidity(...tree: Omit, "isValid" | "reasonForInvalidity">[]): InvalidityReport | null { tree.push(this); - if (this.moduleNameMetadata.position && this.moduleNameMetadata.position !== 'Left' && this.moduleNameMetadata.position !== 'Right' && this.moduleNameMetadata.position !== 'RightOfImage') return { + if (this.moduleNameMetadata.position && !Object.values(ModuleNamePosition).includes(this.moduleNameMetadata.position as any)) return { tree, offendingValue: this.moduleNameMetadata.position, reason: InvalidityReason.FomodModuleNameMetadataInvalidPosition, @@ -105,13 +106,13 @@ export class Fomod extends XmlRepresentation { reason: InvalidityReason.FomodModuleNameMetadataColorHexNotHexNumber, }; - if (this.moduleImageMetadata.showFade && this.moduleImageMetadata.showFade !== 'true' && this.moduleImageMetadata.showFade !== 'false') return { + if (this.moduleImageMetadata.showFade && !Object.values(BooleanString).includes(this.moduleImageMetadata.showFade as any)) return { tree, offendingValue: this.moduleImageMetadata.showFade, reason: InvalidityReason.FomodModuleImageMetadataShowFadeNotBool, }; - if (this.moduleImageMetadata.showImage && this.moduleImageMetadata.showImage !== 'true' && this.moduleImageMetadata.showImage !== 'false') return { + if (this.moduleImageMetadata.showImage && !Object.values(BooleanString).includes(this.moduleImageMetadata.showImage as any)) return { tree, offendingValue: this.moduleImageMetadata.showImage, reason: InvalidityReason.FomodModuleImageMetadataShowImageNotBool, @@ -141,12 +142,12 @@ export class Fomod extends XmlRepresentation { element.setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:noNamespaceSchemaLocation', 'http://qconsulting.ca/fo3/ModConfig5.0.xsd'); - const moduleNameElement = getOrCreateElementByTagName(element, 'moduleName'); + const moduleNameElement = getOrCreateElementByTagNameSafe(element, TagName.ModuleName); moduleNameElement.textContent = this.moduleName; - for (const [key, value] of Object.entries(this.moduleNameMetadata) as [string, string|undefined][]) { + for (const [key, value] of Object.entries(this.moduleNameMetadata) as [string, string | undefined][]) { if (!value) continue; - if (key !== 'colour') moduleNameElement.setAttribute(key, value); + if (key !== AttributeName.Color) moduleNameElement.setAttribute(key, value); else { // `xs:hexBinary` values are required to have an even number of digits. This enforces that so long as the color value is a valid hex number. let colorString = value; @@ -155,19 +156,19 @@ export class Fomod extends XmlRepresentation { } } - const moduleImageElement = getOrCreateElementByTagName(element, 'moduleImage'); + const moduleImageElement = getOrCreateElementByTagNameSafe(element, TagName.ModuleImage); if (this.moduleImage === null) moduleImageElement.remove(); else { for (const [key, value] of Object.entries(this.moduleImageMetadata)) moduleImageElement.setAttribute(key, value); - moduleImageElement.setAttribute('path', this.moduleImage); + moduleImageElement.setAttribute(AttributeName.Path, this.moduleImage); } if (this.moduleDependencies.dependencies.size > 0) element.appendChild(this.moduleDependencies.asElement(document)); else this.moduleDependencies.getElementForDocument(document).remove(); - const requiredInstallContainer = getOrCreateElementByTagName(element, 'requiredInstallFiles'); - const conditionalInstallContainerRoot = getOrCreateElementByTagName(element, 'conditionalFileInstalls'); - const conditionalInstallContainer = getOrCreateElementByTagName(conditionalInstallContainerRoot, 'patterns'); + const requiredInstallContainer = getOrCreateElementByTagNameSafe(element, TagName.RequiredInstallFiles); + const conditionalInstallContainerRoot = getOrCreateElementByTagNameSafe(element, TagName.ConditionalFileInstalls); + const conditionalInstallContainer = getOrCreateElementByTagNameSafe(conditionalInstallContainerRoot, TagName.Patterns); for (const installOrPattern of this.installs) { if (installOrPattern instanceof Install) requiredInstallContainer.appendChild(installOrPattern.asElement(document)); @@ -177,13 +178,12 @@ export class Fomod extends XmlRepresentation { if (requiredInstallContainer.children.length === 0) requiredInstallContainer.remove(); else element.appendChild(requiredInstallContainer); - - const stepContainer = getOrCreateElementByTagName(element, 'installSteps'); + const stepContainer = getOrCreateElementByTagNameSafe(element, TagName.InstallSteps); for (const step of this.steps) stepContainer.appendChild(step.asElement(document)); if (stepContainer.children.length === 0) stepContainer.remove(); else { - stepContainer.setAttribute('order', this.sortingOrder); + stepContainer.setAttribute(AttributeName.Order, this.sortingOrder); element.appendChild(stepContainer); } @@ -197,33 +197,35 @@ export class Fomod extends XmlRepresentation { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; - const moduleName = element.querySelector('moduleName')?.textContent ?? ''; - const moduleImage = element.querySelector('moduleImage')?.getAttribute('path'); + const moduleNameElement = element.querySelector(`:scope > ${TagName.ModuleName}`); + const ModuleImageEllement = element.querySelector(`:scope > ${TagName.ModuleImage}`); + const moduleName = moduleNameElement?.textContent ?? ''; + const moduleImage = ModuleImageEllement?.getAttribute(AttributeName.Path) ?? null; const fomod = new Fomod(moduleName, moduleImage); fomod.assignElement(element); - fomod.moduleNameMetadata = attrToObject(element.querySelector('moduleName')?.attributes ?? []); - fomod.moduleImageMetadata = attrToObject(element.querySelector('moduleImage')?.attributes ?? []); + fomod.moduleNameMetadata = attrToObject(moduleNameElement?.attributes ?? []); + fomod.moduleImageMetadata = attrToObject(ModuleImageEllement?.attributes ?? []); fomod.moduleImageMetadata.path = ''; - const moduleDependencies = element.querySelector('moduleDependencies'); + const moduleDependencies = element.querySelector(`:scope > ${TagName.ModuleDependencies}`); if (moduleDependencies) fomod.moduleDependencies = Dependencies.parse(moduleDependencies); - for (const install of element.querySelectorAll(':scope > requiredInstallFiles > :is(file, folder)')) { + for (const install of element.querySelectorAll(`:scope > ${TagName.RequiredInstallFiles} > :is(${TagName.File}, ${TagName.Folder})`)) { const parsed = Install.parse(install); if (parsed) fomod.installs.add(parsed); } - fomod.sortingOrder = element.querySelector('installSteps')?.getAttribute('order') ?? SortingOrder.Ascending; + fomod.sortingOrder = element.querySelector(`:scope > ${TagName.InstallStep}`)?.getAttribute(AttributeName.Order) ?? SortingOrder.Ascending; - for (const install of element.querySelectorAll(':scope > conditionalFileInstalls > pattern')) { + for (const install of element.querySelectorAll(`:scope > ${TagName.ConditionalFileInstalls} > ${TagName.Pattern}`)) { const parsed = Install.parse(install); if (parsed) fomod.installs.add(parsed); } - for (const step of element.querySelectorAll(':scope > installSteps > installStep')) { + for (const step of element.querySelectorAll(`:scope > ${TagName.InstallSteps} > ${TagName.InstallStep}`)) { const parsed = Step.parse(step); if (parsed) fomod.steps.add(parsed); } diff --git a/src/definitions/module/Group.ts b/src/definitions/module/Group.ts new file mode 100644 index 0000000..664bdb0 --- /dev/null +++ b/src/definitions/module/Group.ts @@ -0,0 +1,86 @@ +import { getOrCreateElementByTagNameSafe } from "../../DomUtils"; +import { InvalidityReason, InvalidityReport } from "../lib/InvalidityReporting"; +import { Option } from "./Option"; +import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/_core"; +import { AttributeName, GroupBehaviorType, SortingOrder, TagName } from "../Enums"; + +export class Group extends XmlRepresentation { + static override readonly tagName = TagName.Group; + readonly tagName = TagName.Group; + + + constructor( + public name: string = '', + public behaviorType: TStrict extends true ? GroupBehaviorType : string = GroupBehaviorType.SelectExactlyOne, + public sortingOrder: TStrict extends true ? SortingOrder : string = SortingOrder.Ascending, + public options: Set> = new Set(), + ) { + super(); + } + + asElement(document: Document): Element { + const element = this.getElementForDocument(document); + + element.setAttribute(AttributeName.Name, this.name); + element.setAttribute(AttributeName.Type, this.behaviorType); + + const optionsContainer = getOrCreateElementByTagNameSafe(element, TagName.Plugins); + + optionsContainer.setAttribute(AttributeName.Order, this.sortingOrder); + for (const option of this.options) optionsContainer.appendChild(option.asElement(document)); + + return element; + } + + isValid(): this is Group { + return Object.values(GroupBehaviorType).includes(this.behaviorType as any) && + Object.values(SortingOrder).includes(this.sortingOrder as any) && + Array.from(this.options).every(option => option.isValid() + ); + } + + reasonForInvalidity(...tree: Omit, 'isValid' | 'reasonForInvalidity'>[]): InvalidityReport | null { + tree.push(this); + + if (!Object.values(GroupBehaviorType).includes(this.behaviorType as any)) + return { reason: InvalidityReason.GroupUnknownBehaviorType, offendingValue: this.behaviorType, tree }; + + if (!Object.values(SortingOrder).includes(this.sortingOrder as any)) + return { reason: InvalidityReason.GroupUnknownOptionSortingOrder, offendingValue: this.sortingOrder, tree }; + + for (const option of this.options) { + const reason = option.reasonForInvalidity(...tree); + if (reason !== null) return reason; + } + + return null; + } + + static override parse(element: Element): Group { + const existing = ElementObjectMap.get(element); + if (existing && existing instanceof this) return existing; + + const name = element.getAttribute(AttributeName.Name); + const behaviorType = element.getAttribute(AttributeName.Type); + + const group = new Group(name ?? '', behaviorType ?? ''); + group.assignElement(element); + + const optionsContainer = element.querySelector(`:scope > ${TagName.Plugins}`); + if (optionsContainer === null) return group; + + const sortingOrder = optionsContainer.getAttribute(AttributeName.Order); + if (sortingOrder !== null) group.sortingOrder = sortingOrder; + + for (const optionElement of optionsContainer.querySelectorAll(`:scope > ${TagName.Plugins}`)) { + const option = Option.parse(optionElement); + if (option !== null) group.options.add(option); + } + + return group; + } + + decommission(currentDocument?: Document) { + this.options.forEach(option => option.decommission(currentDocument)); + } +} diff --git a/src/definitions/Install.ts b/src/definitions/module/Install.ts similarity index 84% rename from src/definitions/Install.ts rename to src/definitions/module/Install.ts index c0c594d..35e5f87 100644 --- a/src/definitions/Install.ts +++ b/src/definitions/module/Install.ts @@ -1,7 +1,8 @@ -import { ensureXmlDoctype } from "../DomUtils"; +import { ensureXmlDoctype } from "../../DomUtils"; import { Dependencies } from "./Dependencies"; -import { InvalidityReason, InvalidityReport } from "./InvalidityReporting"; -import { ElementObjectMap, Verifiable, XmlRepresentation } from "./_core"; +import { InvalidityReason, InvalidityReport } from "../lib/InvalidityReporting"; +import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/_core"; +import { AttributeName, BooleanString, TagName } from "../Enums"; @@ -47,9 +48,11 @@ interface InstallInstances { */ export const InstallInstancesByDocument = new WeakMap(); +type InstallTagName = TagName.File|TagName.Folder; + export class Install extends XmlRepresentation { - static override readonly tagName = ['file', 'folder']; - tagName: 'file'|'folder' = 'file'; // Very interchangeable; + static override readonly tagName = [TagName.File, TagName.Folder] as [TagName.File, TagName.Folder]; + tagName: InstallTagName = TagName.File; // Very interchangeable; /** A list of documents this install is a part of */ @@ -60,8 +63,8 @@ export class Install extends XmlRepresentation catch { return false; } return ( - (this.alwaysInstall === 'true' || this.alwaysInstall === 'false') && - (this.installIfUsable === 'true' || this.installIfUsable === 'false') + Object.values(BooleanString).includes(this.alwaysInstall as any) && + Object.values(BooleanString).includes(this.installIfUsable as any) ); } @@ -73,10 +76,10 @@ export class Install extends XmlRepresentation return { reason: InvalidityReason.InstallPriorityNotInteger, offendingValue: this.priority, tree }; } - if (this.alwaysInstall !== 'true' && this.alwaysInstall !== 'false') + if (!Object.values(BooleanString).includes(this.alwaysInstall as any)) return { reason: InvalidityReason.InstallAlwaysInstallNotBoolean, offendingValue: this.priority, tree }; - if (this.installIfUsable !== 'true' && this.installIfUsable !== 'false') + if (!Object.values(BooleanString).includes(this.installIfUsable as any)) return { reason: InvalidityReason.InstallInstallIfUsableNotBoolean, offendingValue: this.priority, tree }; return null; @@ -107,13 +110,13 @@ export class Install extends XmlRepresentation * * @deprecated Has inconsistent behavior between mod managers. Instead, you might consider duplicating the `dependencies` object to specify when a file should be installed. Included for completeness. */ - public installIfUsable: TStrict extends true ? `${boolean}` : string = 'false', + public installIfUsable: TStrict extends true ? BooleanString : string = BooleanString.false, /** Whether to always install the file, even if the user has not selected it. * * @deprecated Has inconsistent behavior between mod managers. Instead, you might consider removing the `dependencies` object instead. Included for completeness. */ - public alwaysInstall: TStrict extends true ? `${boolean}` : string = 'false', + public alwaysInstall: TStrict extends true ? BooleanString : string = BooleanString.false, ) { super(); @@ -125,41 +128,41 @@ export class Install extends XmlRepresentation asElement(document: Document): Element { if (this.fileSource.endsWith('/') || this.fileSource.endsWith('\\')) { if (this.fileDestination && (!this.fileDestination.endsWith('/') && !this.fileDestination.endsWith('\\'))) throw new Error('Source is a folder but destination is not', {cause: this}); - this.tagName = 'folder'; + this.tagName = TagName.Folder; } else if (this.fileDestination && (this.fileDestination.endsWith('/') || this.fileDestination.endsWith('\\'))) throw new Error('Destination is a folder but source is not', {cause: this}); - else this.tagName = 'file'; + else this.tagName = TagName.File; const element = this.getElementForDocument(document); - element.setAttribute('source', this.fileSource); - if (this.fileDestination) element.setAttribute('destination', this.fileDestination); + element.setAttribute(AttributeName.Source, this.fileSource); + if (this.fileDestination) element.setAttribute(AttributeName.Destination, this.fileDestination); - if (this.priority !== '0') element.setAttribute('priority', this.priority); - else element.removeAttribute('priority'); + if (this.priority !== '0') element.setAttribute(AttributeName.Priority, this.priority); + else element.removeAttribute(AttributeName.Priority); - if (this.alwaysInstall !== 'false') element.setAttribute('alwaysInstall', this.alwaysInstall); - else element.removeAttribute('alwaysInstall'); + if (this.alwaysInstall !== BooleanString.false) element.setAttribute(AttributeName.AlwaysInstall, this.alwaysInstall); + else element.removeAttribute(AttributeName.AlwaysInstall); - if (this.installIfUsable !== 'false') element.setAttribute('installIfUsable', this.installIfUsable); - else element.removeAttribute('installIfUsable'); + if (this.installIfUsable !== BooleanString.false) element.setAttribute(AttributeName.InstallIfUsable, this.installIfUsable); + else element.removeAttribute(AttributeName.InstallIfUsable); return element; } static override parse(element: Element): Install { - let source = element.getAttribute('source') ?? ''; - let destination = element.getAttribute('destination') ?? null; + let source = element.getAttribute(AttributeName.Source) ?? ''; + let destination = element.getAttribute(AttributeName.Destination) ?? null; - if (element.tagName === 'folder') { + if (element.tagName === TagName.Folder) { if (!source.endsWith('/')) source += '/'; if (destination && !destination.endsWith('/')) destination += '/'; } - const install = new Install( source, destination, element.getAttribute('priority') ?? '0' ); + const install = new Install( source, destination, element.getAttribute(AttributeName.Priority) ?? '0' ); install.assignElement(element); - install.alwaysInstall = element.getAttribute('alwaysInstall') ?? 'false'; - install.installIfUsable = element.getAttribute('installIfUsable') ?? 'false'; + install.alwaysInstall = element.getAttribute(AttributeName.AlwaysInstall) ?? BooleanString.false; + install.installIfUsable = element.getAttribute(AttributeName.InstallIfUsable) ?? BooleanString.false; return install; } @@ -188,7 +191,7 @@ export class Install extends XmlRepresentation override assignElement(element: Element) { ensureXmlDoctype(element.ownerDocument); - if (element.tagName === 'file' || element.tagName === 'folder') this.tagName = element.tagName; + if (element.tagName === TagName.File || element.tagName === TagName.Folder) this.tagName = element.tagName; super.assignElement(element); } @@ -277,8 +280,8 @@ export class Install extends XmlRepresentation /** A helper class to represent the element. Contains a list of files to be installed by a dependency or option. */ export class InstallPatternFilesWrapper extends XmlRepresentation { - static override tagName = 'files'; - readonly tagName = 'files'; + static override tagName = TagName.Files; + readonly tagName = TagName.Files; constructor( public installs: Set> = new Set(), @@ -379,11 +382,11 @@ export class InstallPatternFilesWrapper extends XmlRepr /** A helper class to represent the element. Contains a list of files to install and a list of dependencies that must first be fulfilled. */ export class InstallPattern extends XmlRepresentation { - static override tagName = 'pattern'; - readonly tagName = 'pattern'; + static override tagName = TagName.Pattern; + readonly tagName = TagName.Pattern; constructor( - public dependencies: Dependencies<'dependencies', TStrict>|null = null, + public dependencies: Dependencies|null = null, public filesWrapper: InstallPatternFilesWrapper = new InstallPatternFilesWrapper(), ) { super(); @@ -410,10 +413,10 @@ export class InstallPattern extends XmlRepresentation(dependenciesElement) : undefined; + const dependenciesElement = element.querySelector(TagName.Dependencies); + const dependencies = dependenciesElement ? Dependencies.parse(dependenciesElement) : undefined; - const filesElement = element.querySelector('files'); + const filesElement = element.querySelector(TagName.Files); const filesWrapper = filesElement ? InstallPatternFilesWrapper.parse(filesElement) : undefined; const obj = new InstallPattern(dependencies, filesWrapper); diff --git a/src/definitions/Option.ts b/src/definitions/module/Option.ts similarity index 77% rename from src/definitions/Option.ts rename to src/definitions/module/Option.ts index 092d727..b8dfcb0 100644 --- a/src/definitions/Option.ts +++ b/src/definitions/module/Option.ts @@ -1,9 +1,10 @@ -import { ensureXmlDoctype, getOrCreateElementByTagName } from "../DomUtils"; -import { Dependencies, FlagDependency } from "./Dependencies"; -import { FlagInstance, FlagInstancesByDocument } from "./FlagInstance"; +import { ensureXmlDoctype, getOrCreateElementByTagNameSafe } from "../../DomUtils"; +import { Dependencies, FlagDependency } from './Dependencies'; +import { FlagInstance, FlagInstancesByDocument } from "../lib/FlagInstance"; import { Install, InstallPattern } from "./Install"; -import { InvalidityReason, InvalidityReport } from "./InvalidityReporting"; -import { ElementObjectMap, Verifiable, XmlRepresentation } from "./_core"; +import { InvalidityReason, InvalidityReport } from "../lib/InvalidityReporting"; +import { ElementObjectMap, Verifiable, XmlRepresentation } from "../lib/_core"; +import { AttributeName, GroupBehaviorType, OptionType, TagName } from "../Enums"; /*** * $$$$$$\ $$\ $$\ $$$$$$$\ $$\ @@ -21,8 +22,8 @@ import { ElementObjectMap, Verifiable, XmlRepresentation } from "./_core"; /** A single option (or "plugin") for a Fomod. These are typically presented as checkboxes or radio buttons. */ export class Option extends XmlRepresentation { - static override readonly tagName = 'plugin'; - readonly tagName = 'plugin'; + static override readonly tagName = TagName.Plugin; + readonly tagName = TagName.Plugin; constructor( public name: string = '', @@ -40,23 +41,23 @@ export class Option extends XmlRepresentation element.setAttribute('name', this.name); - const description = getOrCreateElementByTagName(element, 'description'); + const description = getOrCreateElementByTagNameSafe(element, TagName.Description); description.textContent = this.description; element.appendChild(description); - if (this.image === null) element.getElementsByTagName('image')[0]?.remove(); + if (this.image === null) element.getElementsByTagName(TagName.Image)[0]?.remove(); else { - const image = getOrCreateElementByTagName(element, 'image'); + const image = getOrCreateElementByTagNameSafe(element, TagName.Image); image.textContent = this.image; element.appendChild(image); } if (this.flagsToSet.size > 0) { - const flagsElement = getOrCreateElementByTagName(element, 'conditionFlags'); + const flagsElement = getOrCreateElementByTagNameSafe(element, TagName.ConditionFlags); for (const flag of this.flagsToSet) flagsElement.appendChild(flag.asElement(document)); element.appendChild(flagsElement); } else { // Create an empty `files` element - const filesElement = getOrCreateElementByTagName(element, 'files'); + const filesElement = getOrCreateElementByTagNameSafe(element, TagName.Files); filesElement.replaceChildren(); element.appendChild(filesElement); } @@ -110,33 +111,33 @@ export class Option extends XmlRepresentation const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; - const name = element.getAttribute('name'); + const name = element.getAttribute(AttributeName.Name); if (name === null) return null; - const description = element.getElementsByTagName('description')[0]?.textContent ?? ''; - const image = element.getElementsByTagName('image')[0]?.getAttribute('path') ?? null; + const description = element.getElementsByTagName(TagName.Description)[0]?.textContent ?? ''; + const image = element.getElementsByTagName(TagName.Image)[0]?.getAttribute(AttributeName.Path) ?? null; const flagsToSet = new Set(); - const conditionFlags = element.getElementsByTagName('conditionFlags')[0]; + const conditionFlags = element.getElementsByTagName(TagName.ConditionFlags)[0]; if (conditionFlags) { - for (const flagElement of conditionFlags.getElementsByTagName('flag')) { + for (const flagElement of conditionFlags.getElementsByTagName(TagName.Flag)) { const flag = FlagSetter.parse(flagElement); if (flag) flagsToSet.add(flag); } } - const typeDescriptorElement = element.getElementsByTagName('typeDescriptor')[0]; + const typeDescriptorElement = element.getElementsByTagName(TagName.TypeDescriptor)[0]; const typeDescriptor = typeDescriptorElement ? TypeDescriptor.parse(typeDescriptorElement) : undefined; const installsToSet = new InstallPattern(); - const filesElement = element.getElementsByTagName('files')[0]; + const filesElement = element.getElementsByTagName(TagName.File)[0]; if (filesElement) { - for (const flagElement of filesElement.getElementsByTagName('file')) { + for (const flagElement of filesElement.getElementsByTagName(TagName.File)) { const install = Install.parse(flagElement); if (install) installsToSet.filesWrapper.installs.add(install); } - for (const flagElement of filesElement.getElementsByTagName('folder')) { + for (const flagElement of filesElement.getElementsByTagName(TagName.Folder)) { const install = Install.parse(flagElement); if (install) installsToSet.filesWrapper.installs.add(install); } @@ -160,8 +161,8 @@ export class Option extends XmlRepresentation export class FlagSetter extends XmlRepresentation { - static override readonly tagName = 'flag'; - readonly tagName = 'flag'; + static override readonly tagName = TagName.Flag; + readonly tagName = TagName.Flag; get name() { return this.flagInstance.name; } @@ -177,7 +178,7 @@ export class FlagSetter extends XmlRepresentation { asElement(document: Document): Element { const element = this.getElementForDocument(document); - element.setAttribute('name', this.flagInstance.name); + element.setAttribute(AttributeName.Name, this.flagInstance.name); element.textContent = this.flagInstance.usedValue; return element; @@ -193,7 +194,7 @@ export class FlagSetter extends XmlRepresentation { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; - const flagName = element.getAttribute('name'); + const flagName = element.getAttribute(AttributeName.Name); if (flagName === null) return null; const obj = new FlagSetter(new FlagInstance(flagName, element.textContent ?? '', true)); @@ -232,20 +233,6 @@ export class FlagSetter extends XmlRepresentation { */ -/** Describes how an option should behave in regard to user selection */ -export enum OptionType { - /** The option will not be selected and cannot be selected. */ - NotUsable = 'NotUsable', - /** Acts the same as `Optional`, except that mod managers may show a warning to the user when selecting this option. This is not universal, though, and the majority of mainstream mod managers at the moment forego this. */ - CouldBeUsable = 'CouldBeUsable', - /** The option will be selectable. This is the default behavior. */ - Optional = 'Optional', - /** The option will be selected by default but may be deselected. */ - Recommended = 'Recommended', - /** The option will be selected by default and cannot be deselected. */ - Required = 'Required', -} - /** Describes the desired `OptionType` for an option * * Supports setting a default type and an optional list of conditions ('patterns') that can change the type. @@ -253,12 +240,12 @@ export enum OptionType { * @see `OptionType` */ export class TypeDescriptor extends XmlRepresentation { - static override readonly tagName = 'typeDescriptor'; - readonly tagName = 'typeDescriptor'; + static override readonly tagName = TagName.TypeDescriptor; + readonly tagName = TagName.TypeDescriptor; constructor( /** The type name descriptor */ - public defaultTypeNameDescriptor: TypeNameDescriptor<'type'|'defaultType', TStrict, false> = new TypeNameDescriptor('defaultType', OptionType.Optional, false), + public defaultTypeNameDescriptor: TypeNameDescriptor = new TypeNameDescriptor(TagName.DefaultType, OptionType.Optional, false), public patterns: TypeDescriptorPattern[] = [], ) { super(); @@ -268,14 +255,14 @@ export class TypeDescriptor extends XmlRepresentation extends XmlRepresentation defaultType, :scope > type'); + const defaultTypeNameDescriptorElement = element.querySelector(`:scope > ${TagName.DefaultType}, :scope > ${TagName.Type}`); if (defaultTypeNameDescriptorElement) { typeDescriptor.defaultTypeNameDescriptor = TypeNameDescriptor.parse(defaultTypeNameDescriptorElement); } - const patternsContainer = element.querySelector(':scope > patterns'); + const patternsContainer = element.querySelector(`:scope > ${TagName.DefaultType}`); if (patternsContainer) - for (const patternElement of patternsContainer.querySelectorAll(':scope > pattern')) + for (const patternElement of patternsContainer.querySelectorAll(`:scope > ${TagName.Patterns}`)) typeDescriptor.patterns.push(TypeDescriptorPattern.parse(patternElement)); return typeDescriptor; @@ -324,14 +311,13 @@ export class TypeDescriptor extends XmlRepresentation extends XmlRepresentation { - static override readonly tagName = 'pattern'; - readonly tagName = 'pattern'; + static override readonly tagName = TagName.Pattern; + readonly tagName = TagName.Pattern; constructor( - public typeNameDescriptor: TypeNameDescriptor<'type', TStrict, true> = new TypeNameDescriptor('type', OptionType.Optional, true), - public dependencies: Dependencies<'dependencies', TStrict> = new Dependencies('dependencies'), + public typeNameDescriptor: TypeNameDescriptor = new TypeNameDescriptor(TagName.Type, OptionType.Optional, true), + public dependencies: Dependencies = new Dependencies(TagName.Dependencies), ) { super(); } @@ -352,11 +338,11 @@ export class TypeDescriptorPattern extends XmlRepresent const typeDescriptorPattern = new TypeDescriptorPattern(); typeDescriptorPattern.assignElement(element); - const typeNameDescriptorElement = element.querySelector(':scope > type'); + const typeNameDescriptorElement = element.querySelector(`:scope > ${TagName.Type}`); if (typeNameDescriptorElement) typeDescriptorPattern.typeNameDescriptor = TypeNameDescriptor.parse(typeNameDescriptorElement, true); - const dependenciesElement = element.querySelector(':scope > dependencies'); - if (dependenciesElement) typeDescriptorPattern.dependencies = Dependencies.parse<'dependencies'>(dependenciesElement); + const dependenciesElement = element.querySelector(`:scope > ${TagName.Dependencies}`); + if (dependenciesElement) typeDescriptorPattern.dependencies = Dependencies.parse(dependenciesElement); return typeDescriptorPattern; } @@ -382,17 +368,19 @@ export class TypeDescriptorPattern extends XmlRepresent } } -export class TypeNameDescriptor extends XmlRepresentation { - static override readonly tagName = ['type', 'defaultType']; +type TypeDescriptorTagName = TagName.Type|TagName.DefaultType; + +export class TypeNameDescriptor extends XmlRepresentation { + static override readonly tagName = [TagName.Type, TagName.DefaultType] as [TagName.Type, TagName.DefaultType]; get tagName() { return this._tagName; } - set tagName(tagName: TTagNameIsReadOnly extends true ? TTagName : 'type'|'defaultType') { + set tagName(tagName: TTagNameIsReadOnly extends true ? TTagName : TypeDescriptorTagName) { if (!this.tagNameIsReadonly) this._tagName = tagName; else throw new Error(`Attempted to set read-only property 'tagName' on a TypeNameDescriptor instance`); } constructor( - private _tagName: TTagNameIsReadOnly extends true ? TTagName : 'type'|'defaultType', + private _tagName: TTagNameIsReadOnly extends true ? TTagName : TypeDescriptorTagName, public targetType: TStrict extends true ? OptionType : string = OptionType.Optional, public tagNameIsReadonly: TTagNameIsReadOnly ) { @@ -401,29 +389,29 @@ export class TypeNameDescriptor(element: Element, tagNameIsReadonly?: TTagNameIsReadOnly): TypeNameDescriptor { + static override parse(element: Element, tagNameIsReadonly?: TTagNameIsReadOnly): TypeNameDescriptor { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; const typeDescriptorName = new TypeNameDescriptor(element.tagName as TTagName, 'Optional', tagNameIsReadonly as TTagNameIsReadOnly); typeDescriptorName.assignElement(element); - typeDescriptorName.targetType = element.getAttribute('name') ?? 'Optional'; + typeDescriptorName.targetType = element.getAttribute(AttributeName.Name) ?? OptionType.Optional; return typeDescriptorName; } isValid(): this is TypeNameDescriptor { - return this.targetType === 'NotUsable' || this.targetType === 'CouldBeUsable' || this.targetType === 'Optional' || this.targetType === 'Recommended' || this.targetType === 'Required'; + return Object.values(OptionType).includes(this.targetType as any); } reasonForInvalidity(...tree: Omit, "isValid" | "reasonForInvalidity">[]): InvalidityReport | null { tree.push(this); - if (this.targetType !== 'NotUsable' && this.targetType !== 'CouldBeUsable' && this.targetType !== 'Optional' && this.targetType !== 'Recommended' && this.targetType !== 'Required') + if (!Object.values(OptionType).includes(this.targetType as any)) return {offendingValue: this.targetType, tree, reason: InvalidityReason.OptionTypeDescriptorUnknownOptionType}; return null; @@ -454,7 +442,7 @@ export class TypeNameDescriptor extends XmlRepresentation { - static override readonly tagName = 'installStep'; - readonly tagName = 'installStep'; + static override readonly tagName = TagName.InstallStep; + readonly tagName = TagName.InstallStep; constructor( @@ -29,11 +20,11 @@ export class Step extends XmlRepresentation { asElement(document: Document): Element { const element = this.getElementForDocument(document); - element.setAttribute('name', this.name); + element.setAttribute(AttributeName.Name, this.name); - const groupsContainer = getOrCreateElementByTagName(element, 'optionalFileGroups'); + const groupsContainer = getOrCreateElementByTagNameSafe(element, TagName.OptionalFileGroups); - groupsContainer.setAttribute('order', this.sortingOrder); + groupsContainer.setAttribute(AttributeName.Order, this.sortingOrder); for (const group of this.groups) groupsContainer.appendChild(group.asElement(document)); return element; @@ -41,7 +32,7 @@ export class Step extends XmlRepresentation { isValid(): this is Step { return ( - (this.sortingOrder === 'Ascending' || this.sortingOrder === 'Descending' || this.sortingOrder === 'Explicit') && + Object.values(SortingOrder).includes(this.sortingOrder as any) && Array.from(this.groups).every(group => group.isValid()) ); } @@ -49,7 +40,7 @@ export class Step extends XmlRepresentation { reasonForInvalidity(...tree: Omit, 'isValid' | 'reasonForInvalidity'>[]): InvalidityReport | null { tree.push(this); - if (this.sortingOrder !== 'Ascending' && this.sortingOrder !== 'Descending' && this.sortingOrder !== 'Explicit') + if (!Object.values(SortingOrder).includes(this.sortingOrder as any)) return { reason: InvalidityReason.StepUnknownGroupSortingOrder, offendingValue: this.sortingOrder, tree }; for (const group of this.groups) { @@ -64,18 +55,18 @@ export class Step extends XmlRepresentation { const existing = ElementObjectMap.get(element); if (existing && existing instanceof this) return existing; - const name = element.getAttribute('name'); + const name = element.getAttribute(AttributeName.Name); const step = new Step(name ?? ''); step.assignElement(element); - const groupsContainer = element.querySelector('optionalFileGroups'); + const groupsContainer = element.querySelector(TagName.OptionalFileGroups); if (groupsContainer === null) return step; - const sortingOrder = groupsContainer.getAttribute('order'); + const sortingOrder = groupsContainer.getAttribute(AttributeName.Order); if (sortingOrder !== null) step.sortingOrder = sortingOrder; - for (const groupElement of groupsContainer.querySelectorAll('group')) { + for (const groupElement of groupsContainer.querySelectorAll(TagName.Group)) { const group = Group.parse(groupElement); if (group !== null) step.groups.add(group); } diff --git a/src/definitions/module/index.ts b/src/definitions/module/index.ts new file mode 100644 index 0000000..0961cfa --- /dev/null +++ b/src/definitions/module/index.ts @@ -0,0 +1,6 @@ +export * from './Dependencies'; +export * from './Fomod'; +export * from './Group'; +export * from './Install'; +export * from './Option'; +export * from './Step'; diff --git a/src/index.ts b/src/index.ts index afb9ae5..bf1210f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +export * from './definitions'; + export * from './DomUtils'; export * from './parse'; -export * from './definitions'; diff --git a/src/parse.ts b/src/parse.ts index cb3d46c..3f0c67b 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,5 +1,5 @@ import { ensureXmlDoctype } from "./DomUtils"; -import { Dependency, Fomod, FomodInfo } from "./definitions"; +import { Fomod, FomodInfo } from "."; export function parseModuleDoc(document: Document): Fomod | null { ensureXmlDoctype(document); diff --git a/tests/build-from-scratch.test.ts b/tests/build-from-scratch.test.ts index f5eb51d..40c8f33 100644 --- a/tests/build-from-scratch.test.ts +++ b/tests/build-from-scratch.test.ts @@ -1,4 +1,4 @@ -import { Dependencies, Fomod, GameVersionDependency, Group, GroupBehaviorType, Install, InstallPattern, Option, ScriptExtenderVersionDependency, SortingOrder as SortingOrder, Step } from "../src"; +import { Dependencies, Fomod, GameVersionDependency, Group, GroupBehaviorType, Install, InstallPattern, Option, ScriptExtenderVersionDependency, SortingOrder as SortingOrder, Step, TagName } from "../src"; import { parseTag, testValidity } from "./testUtils"; const fomod = new Fomod('That Test Fomod', 'someImage.gif'); @@ -22,7 +22,7 @@ const conditionalDependenciesPattern = new InstallPattern(); conditionalDependenciesPattern.filesWrapper.installs.add(conditionedInstall); -conditionalDependenciesPattern.dependencies = new Dependencies('dependencies'); +conditionalDependenciesPattern.dependencies = new Dependencies(TagName.Dependencies); conditionalDependenciesPattern.dependencies.dependencies.add( new GameVersionDependency('1.6.640.0') ); conditionalDependenciesPattern.dependencies.dependencies.add( new ScriptExtenderVersionDependency('2.2.3') ); diff --git a/tests/definitions/Dependencies.test.ts b/tests/definitions/Dependencies.test.ts index e122aea..9bf3bc1 100644 --- a/tests/definitions/Dependencies.test.ts +++ b/tests/definitions/Dependencies.test.ts @@ -90,4 +90,3 @@ test('Mod Manager Version Dependencies', () => { versionTest(ModManagerVersionDependency, tag, version); }); - diff --git a/tests/definitions/Fomod.test.ts b/tests/definitions/Fomod.test.ts index 83988f9..55b85c7 100644 --- a/tests/definitions/Fomod.test.ts +++ b/tests/definitions/Fomod.test.ts @@ -1,7 +1,6 @@ -import { Fomod } from '../../src'; +import { Fomod, Install } from '../../src'; import { parseTag } from '../testUtils'; -import { Install } from '../../src/definitions/Install'; @@ -182,10 +181,6 @@ describe('Fomod/ModuleConfig', () => { }); describe('requiredInstallFiles', () => { - test('Fomod Has Required Install Files', () => { - expect(Array.from(cleanElement.children).findIndex(e => e.tagName === 'requiredInstallFiles')).toBeGreaterThan(-1); - }); - test('We Have Files Set In The Fomod', () => { expect(fomod.installs.size).toBe(3); }); @@ -208,5 +203,10 @@ describe('Fomod/ModuleConfig', () => { if ( !(f2 instanceof Install) ) return; expect(f2.fileSource).toBe('some-file-1'); }); + + test('Fomod Has Required Install Files', () => { + const childArr = Array.from(cleanElement.children); + expect(childArr.findIndex(e => e.tagName === 'requiredInstallFiles')).toBeGreaterThan(-1); + }); }); }); diff --git a/tests/definitions/Install.test.ts b/tests/definitions/Install.test.ts index cce843f..2284273 100644 --- a/tests/definitions/Install.test.ts +++ b/tests/definitions/Install.test.ts @@ -1,4 +1,4 @@ -import {parseTag} from '../testUtils'; +import {parseTag, testValidity} from '../testUtils'; import { Install } from '../../src'; @@ -10,7 +10,7 @@ describe('Typical File Install', () => { test('Priority Is Correct', () => expect(obj.priority).toBe('0')); test('Always Install Is Correct', () => expect(obj.alwaysInstall).toBe('false')); test('Install If Usable Is Correct', () => expect(obj.installIfUsable).toBe('false')); - test('Is Valid', () => expect(obj.isValid()).toBe(true)); + test('Is Valid', () => testValidity(obj)); }); describe('Typical Folder Install', () => { @@ -21,7 +21,7 @@ describe('Typical Folder Install', () => { test('Priority Is Correct', () => expect(obj.priority).toBe('0')); test('Always Install Is Correct', () => expect(obj.alwaysInstall).toBe('false')); test('Install If Usable Is Correct', () => expect(obj.installIfUsable).toBe('false')); - test('Is Valid', () => expect(obj.isValid()).toBe(true)); + test('Is Valid', () => testValidity(obj)); }); describe('File Install Without Explicit Destination', () => { @@ -37,7 +37,7 @@ describe('Folder With Always Install', () => { test('Source Is Correct', () => expect(obj.fileSource).toBe('apple/')); test('Destination Is Correct', () => expect(obj.fileDestination).toBe('banana/')); test('Always Install Is True', () => expect(obj.alwaysInstall).toBe('true')); - test('Is Valid', () => expect(obj.isValid()).toBe(true)); + test('Is Valid', () => testValidity(obj)); }); describe('Folder With Install If Usable', () => { @@ -46,7 +46,7 @@ describe('Folder With Install If Usable', () => { test('Source Is Correct', () => expect(obj.fileSource).toBe('apple/')); test('Destination Is Correct', () => expect(obj.fileDestination).toBe('banana/')); test('Install If Usable Is True', () => expect(obj.installIfUsable).toBe('true')); - test('Is Valid', () => expect(obj.isValid()).toBe(true)); + test('Is Valid', () => testValidity(obj)); }); describe('Folder With Priority', () => { @@ -55,5 +55,5 @@ describe('Folder With Priority', () => { test('Source Is Correct', () => expect(obj.fileSource).toBe('apple/')); test('Destination Is Correct', () => expect(obj.fileDestination).toBe('banana/')); test('Priority Is Correct', () => expect(obj.priority).toBe('100')); - test('Is Valid', () => expect(obj.isValid()).toBe(true)); + test('Is Valid', () => testValidity(obj)); }); diff --git a/tests/definitions/Option.test.ts b/tests/definitions/Option.test.ts index 45e1445..8445d20 100644 --- a/tests/definitions/Option.test.ts +++ b/tests/definitions/Option.test.ts @@ -1,5 +1,5 @@ -import { Option, OptionType, TypeDescriptor, TypeNameDescriptor } from "../../src"; -import { parseTag } from "../testUtils"; +import { Option, OptionType, TagName, TypeDescriptor, TypeNameDescriptor } from "../../src"; +import { parseTag, testValidity } from "../testUtils"; describe('Basic Option', () => { const obj = new Option('apple', 'banana', 'someImage.png'); @@ -8,17 +8,17 @@ describe('Basic Option', () => { test('Description Is Correct', () => expect(obj.description).toBe('banana')); test('Image Is Correct', () => expect(obj.image).toBe('someImage.png')); test('Type is Correct', () => expect(obj.typeDescriptor.defaultTypeNameDescriptor.targetType).toBe(OptionType.Optional)); - test('Is Valid', () => expect(obj.isValid()).toBe(true)); + test('Is Valid', () => testValidity(obj)); }); describe('Option With Type', () => { - const obj = new Option('apple', 'banana', 'someImage.png', new TypeDescriptor(new TypeNameDescriptor('defaultType', OptionType.Required, false))); + const obj = new Option('apple', 'banana', 'someImage.png', new TypeDescriptor(new TypeNameDescriptor(TagName.DefaultType, OptionType.Required, false))); test('Name Is Correct', () => expect(obj.name).toBe('apple')); test('Description Is Correct', () => expect(obj.description).toBe('banana')); test('Image Is Correct', () => expect(obj.image).toBe('someImage.png')); test('Type is Correct', () => expect(obj.typeDescriptor.defaultTypeNameDescriptor.targetType).toBe(OptionType.Required)); - test('Is Valid', () => expect(obj.isValid()).toBe(true)); + test('Is Valid', () => testValidity(obj)); }); describe('Parsing Option From Element #1', () => { @@ -34,10 +34,11 @@ describe('Parsing Option From Element #1', () => { const obj = Option.parse(doc); test('We Got An Option', () => expect(obj).toBeInstanceOf(Option)); - test('Name Is Correct', () => expect(obj?.name).toBe('lungs')); + if (!obj) return; + test('Name Is Correct', () => expect(obj.name).toBe('lungs')); test('Description Is Correct', () => expect(obj?.description).toBe('')); - test('Image Is Correct', () => expect(obj?.image).toBe('12345')); - test('Default Type is Correct', () => expect(obj?.typeDescriptor.defaultTypeNameDescriptor.targetType).toBe(OptionType.CouldBeUsable)); - test('Has No Installs', () => expect(obj?.installsToSet.filesWrapper.installs.size).toBe(0)); - test('Is Valid', () => expect(obj?.isValid()).toBe(true)); + test('Image Is Correct', () => expect(obj.image).toBe('12345')); + test('Default Type is Correct', () => expect(obj.typeDescriptor.defaultTypeNameDescriptor.targetType).toBe(OptionType.CouldBeUsable)); + test('Has No Installs', () => expect(obj.installsToSet.filesWrapper.installs.size).toBe(0)); + test('Is Valid', () => testValidity(obj)); }); diff --git a/tests/definitions/all.test.ts b/tests/definitions/all.test.ts index 2e17777..c2294f2 100644 --- a/tests/definitions/all.test.ts +++ b/tests/definitions/all.test.ts @@ -1,6 +1,5 @@ import * as index from '../../src/index'; import { parseTag } from '../testUtils'; -import { InstallPattern } from '../../src/definitions/Install'; const isPrototypeOf = Function.call.bind(Object.prototype.isPrototypeOf); @@ -61,7 +60,7 @@ describe('All XmlRepresentation classes with `parse()` should add that element t expect(asElement).toSatisfy(() => { if (asElement === element) return true; - if (result instanceof InstallPattern) { + if (result instanceof index.InstallPattern) { return asElement.tagName === index.InstallPatternFilesWrapper.tagName; }