Skip to content

Commit

Permalink
feat(config): Make GameMetadata more robust
Browse files Browse the repository at this point in the history
  • Loading branch information
jcowman2 committed Dec 5, 2018
1 parent 7c36375 commit 1817b91
Show file tree
Hide file tree
Showing 6 changed files with 368 additions and 60 deletions.
14 changes: 13 additions & 1 deletion src/config/game-metadata.ts
Expand Up @@ -17,7 +17,7 @@ export interface GameMetadata {
readonly name: string;

/** The game's author. */
readonly author?: string;
readonly author: string;

/** A brief description of the game. */
readonly headline?: string;
Expand All @@ -37,3 +37,15 @@ export interface GameMetadata {
/** The version of the Regal Game Library used by the game. */
readonly regalVersion?: string;
}

/** The names of every metadata property. */
export const METADATA_KEYS = [
"name",
"author",
"headline",
"description",
"homepage",
"repository",
"options",
"regalVersion"
];
1 change: 1 addition & 0 deletions src/config/impl/index.ts
Expand Up @@ -8,3 +8,4 @@

export { buildInstanceOptions } from "./instance-options-impl";
export { validateOptions, ensureOverridesAllowed } from "./validate-options";
export { copyMetadata, validateMetadata } from "./metadata-funcs";
88 changes: 88 additions & 0 deletions src/config/impl/metadata-funcs.ts
@@ -0,0 +1,88 @@
/*
* Contains functions for mutating game metadata.
*
* Copyright (c) 2018 Joseph R Cowman
* Licensed under MIT License (see https://github.com/regal/regal)
*/

import { version as regalVersion } from "../../../package.json";
import { RegalError } from "../../error";
import { GameMetadata, METADATA_KEYS } from "../game-metadata";
import { GameOptions, OPTION_KEYS } from "../game-options";
import { checkPropertyType, validateOptions } from "./validate-options";

/** Safe copies an allowOverrides option. */
const copyAllowOverrides = (opt: string[] | boolean) =>
Array.isArray(opt) ? opt.map(str => str) : opt;

/** Safe copies a partial GameOptions object. */
const copyOptions = (opts: Partial<GameOptions>): Partial<GameOptions> => {
const copies = {} as any; // Writeable GameOptions

if (opts.allowOverrides !== undefined) {
copies.allowOverrides = copyAllowOverrides(opts.allowOverrides);
}

for (const opt of OPTION_KEYS.filter(key => key !== "allowOverrides")) {
if (opts[opt] !== undefined) {
copies[opt] = opts[opt];
}
}

return copies;
};

/** Safe copies a metadata object. */
export const copyMetadata = (md: GameMetadata): GameMetadata => ({
author: md.author,
description: md.description,
headline: md.headline,
homepage: md.homepage,
name: md.name,
options: copyOptions(md.options),
regalVersion,
repository: md.repository
});

const optionalStringProps: Array<keyof GameMetadata> = [
"headline",
"description",
"homepage",
"repository"
];

/**
* Throws an error if any of the given metadata properties are invalid.
* This should be called before auto-generated properties, like regalVersion, are created.
* @param md The metadata object.
*/
export const validateMetadata = (md: GameMetadata): void => {
Object.keys(md).forEach(key => {
if (!METADATA_KEYS.includes(key)) {
throw new RegalError(`Invalid metadata property <${key}>.`);
}
});

const checkMdPropType = (
key: keyof GameMetadata,
expectedType: string,
allowUndefined: boolean
) =>
checkPropertyType(
md,
key,
expectedType,
allowUndefined,
"metadata property"
);

checkMdPropType("name", "string", false);
checkMdPropType("author", "string", false);

for (const prop of optionalStringProps) {
checkMdPropType(prop, "string", true);
}

checkMdPropType("options", "object", false);
validateOptions(md.options);
};
56 changes: 36 additions & 20 deletions src/config/impl/validate-options.ts
Expand Up @@ -8,6 +8,36 @@
import { RegalError } from "../../error";
import { GameOptions, OPTION_KEYS } from "../game-options";

/**
* Throws an error if the property of the object is not the given type.
*
* @param target The object with the property.
* @param key The name of the property.
* @param expectedType What the property's type should be.
* @param allowUndefined Whether an error should be thrown if the property is undefined.
* @param generalKeyName The general key name to use in error messages (i.e. "option", "key", or "prop").
*/
export const checkPropertyType = <T>(
target: T,
key: keyof T,
expectedType: string,
allowUndefined: boolean,
generalKeyName: string
) => {
const value = target[key];
const actualType = typeof value;

if (value !== undefined) {
if (actualType !== expectedType) {
throw new RegalError(
`The ${generalKeyName} <${key}> is of type <${actualType}>, must be of type <${expectedType}>.`
);
}
} else if (!allowUndefined) {
throw new RegalError(`The ${generalKeyName} <${key}> must be defined.`);
}
};

/**
* Throws an error if any of the given options are invalid.
* @param options Any game options.
Expand All @@ -20,24 +50,10 @@ export const validateOptions = (options: Partial<GameOptions>): void => {
}
});

// Helper function that ensures the given property has the correct type if it's defined.
const checkTypeIfDefined = (
key: keyof GameOptions,
expectedType: string
): void => {
const value = options[key];
const actualType = typeof value;

if (options[key] !== undefined) {
if (actualType !== expectedType) {
throw new RegalError(
`The option <${key}> is of type <${actualType}>, must be of type <${expectedType}>.`
);
}
}
};
const checkOptionType = (key: keyof GameOptions, expectedType: string) =>
checkPropertyType(options, key, expectedType, true, "option");

checkTypeIfDefined("debug", "boolean");
checkOptionType("debug", "boolean");

// Validate allowOverrides
if (options.allowOverrides !== undefined) {
Expand All @@ -64,9 +80,9 @@ export const validateOptions = (options: Partial<GameOptions>): void => {
}
}

checkTypeIfDefined("showMinor", "boolean");
checkTypeIfDefined("trackAgentChanges", "boolean");
checkTypeIfDefined("seed", "string");
checkOptionType("showMinor", "boolean");
checkOptionType("trackAgentChanges", "boolean");
checkOptionType("seed", "string");
};

/**
Expand Down
16 changes: 4 additions & 12 deletions src/config/metadata-manager.ts
Expand Up @@ -5,9 +5,9 @@
* Licensed under MIT License (see https://github.com/regal/regal)
*/

import { version as regalVersion } from "../../package.json";
import { RegalError } from "../error";
import { GameMetadata } from "./game-metadata";
import { copyMetadata, validateMetadata } from "./impl";

/**
* Static manager for every game instance's metadata.
Expand All @@ -26,7 +26,7 @@ export class MetadataManager {
);
}

return MetadataManager._metadata;
return copyMetadata(this._metadata);
}

/**
Expand All @@ -40,16 +40,8 @@ export class MetadataManager {
);
}

MetadataManager._metadata = {
author: metadata.author,
description: metadata.description,
headline: metadata.headline,
homepage: metadata.homepage,
name: metadata.name,
options: metadata.options,
regalVersion,
repository: metadata.repository
};
validateMetadata(metadata);
MetadataManager._metadata = copyMetadata(metadata);
}

/**
Expand Down

0 comments on commit 1817b91

Please sign in to comment.