From 9a967a577923e67e9284cc324a767fbbbdeb60cc Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 24 Feb 2023 04:04:20 +0100 Subject: [PATCH] Refactor JSON serialiable class pattern Change the JSON seriasiable data classes to use an explicit static fromJSON method and an optional toJSON method for turning them to and from JSON. This improves over the previous pattern of passing the JSON to the constructor as it frees up the constructor to be able to take whichever argument types suits the code instead of being beholden to the JSON serialised form. --- docs/devs/json-serialisable-classes.md | 119 ++++++++++++++++++ packages/controller/src/ControlConnection.js | 4 +- packages/controller/src/Controller.js | 2 +- packages/controller/src/InstanceInfo.js | 8 +- packages/controller/src/routes.js | 2 +- packages/ctl/src/commands.js | 8 +- packages/host/src/Instance.js | 2 +- packages/lib/data/ExportManifest.js | 12 +- packages/lib/data/ModInfo.js | 32 ++--- packages/lib/data/ModPack.js | 32 ++--- .../web_ui/src/components/ModPackViewPage.jsx | 4 +- packages/web_ui/src/components/ModsPage.jsx | 2 +- packages/web_ui/src/model/export_manifest.jsx | 2 +- packages/web_ui/src/model/mod_pack.jsx | 4 +- packages/web_ui/src/model/mods.jsx | 2 +- packages/web_ui/src/util/websocket.jsx | 4 +- test/integration/clusterio.js | 8 +- test/integration/index.js | 2 +- test/lib/data/ModInfo.js | 40 +++--- test/lib/data/ModPack.js | 30 ++--- 20 files changed, 222 insertions(+), 97 deletions(-) create mode 100644 docs/devs/json-serialisable-classes.md diff --git a/docs/devs/json-serialisable-classes.md b/docs/devs/json-serialisable-classes.md new file mode 100644 index 00000000..51101b43 --- /dev/null +++ b/docs/devs/json-serialisable-classes.md @@ -0,0 +1,119 @@ +# JSON Serialisable Classes + +To serialise and deserialise data the Clusterio codebase uses a JSON serialisable data class pattern. +Instances of these classes can be serialised to JSON by passing them directly to `JSON.serialise` and deserialised by passing the serialised JSON representation to its static `fromJSON` method. +The data class also has a static `jsonSchema` property describing the serialised JSON representation for validation purposes. +See the following example code for a hypothetical `Data` class with a single property `member` defaulting to a value of `"foo"`. + + +```js +// Data.js +"use strictc"; + +/** + * A Data class example + * @alias module:Data + */ +class Data { + /** @type {string} */ + member = "foo"; + + constructor(member) { + if (member !== undefined) { this.member = member; } + } + + static jsonSchema = { + type: "object", + properties: { + "member": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.member); + } + + // toJSON is called by JSON.stringify when serialising this class + toJSON() { + let json = {}; + // Omit property if it's set to the default value + if (this.member !== "foo") { json["member"] = this.member; } + return json; + } +} + +module.exports = Data; +``` + +Serialising an instance of `Data` for JSON is as simple as doing `serialised = JSON.stringify(data)` as the `toJSON` method is automatically called by `JSON.stringify` to create a suitable object. +Deserialising is done by calling the static `fromJSON` method with the serialised JSON data, like `data = Data.fromJson(JSON.parse(serialised))`. + +By structuring the classes like this they can be easily composed together. +For example a class holding a data property with an instance of the Data class from the previous example can be created as follows. + +```js +// Composed.js +"use strict"; +const Data = require("./Data"); + +/** + * Composed data class example + * @alias module:Data + */ +class Composed { + /** @type {module:Data|undefined} */ + data; + + constructor(data) { + if (data) { this.data = data; } + } + + static jsonSchema = { + type: "object", + properties: { + "data": Data.jsonSchema, + }, + }; + + static fromJSON(json) { + let data; + if (json["data"]) { data = Data.fromJSON(json["data"]); } + return new this(data); + } + + // toJSON is optional and not needed for this particular class +} + +module.exports = Composed; +``` + +It's easy to create a new instance of Composed by calling `new Composed(new Data("spam"))` and serialising to and from JSON is the same as for Data. +Meaning `serialised = JSON.stringify(composed)` and `composed = Composed.fromJSON(JSON.parse(serialised))` does as expected. + +## Interface description + +Data classes are ES6 style class declarations with the following methods and fields: + +### toJSON(): any (optional) + +Method returning an object or primitive suitable for passing to JSON.stringify in order to create a JSON representation of the class instance. +The returned object may contain objects that have their own toJSON methods which JSON.stringify will then call recursively. + +This method is optional if calling `JSON.stringify` on an instance without it would produce the desired serialised form, however this is rarely the case as often you'll have to provide it to convert the values fram Map/Set properites, omit default values or do similar actions. + +Note that the primary use case for serialising data classes is to send them over the network, this method should therefore try to reduce the size of the resulting JSON by for example omitting properties who's values are the default value. + +### static fromJSON(json: any): this + +Method constructing an instance of this class from one that was previously serialised by passing an instance of it to `JSON.stringify`. + +While not a requirement it is preferrable if the input is not mutated and that no references to any any structures in the input ends up in the returned object. +In other words calling fromJSON twice with the same input should produce two independent objects. + +Note that directly passing the output of `toJSON` to `fromJSON` is not valid, as the output of `toJSON` may require further expansion calling `toJSON` on objects returned by it. +It has to go through `JSON.stringify` or an equivalent recursive expansion of `toJSON`. + +### static jsonSchema + +A [JSON schema](http://json-schema.org/) describing the format of the JSON resulting from passing an instance of this class to `JSON.stringify`. +This should be lenient towards adding additional properties that would be ignored by `fromJSON` as it makes it easier to extend these classes in the future without breaking compatibility. diff --git a/packages/controller/src/ControlConnection.js b/packages/controller/src/ControlConnection.js index de792ba5..bc8a1bde 100644 --- a/packages/controller/src/ControlConnection.js +++ b/packages/controller/src/ControlConnection.js @@ -385,7 +385,7 @@ class ControlConnection extends BaseConnection { } async createModPackRequestHandler(message) { - let modPack = new libData.ModPack(message.data.mod_pack); + let modPack = libData.ModPack.fromJSON(message.data.mod_pack); if (this._controller.modPacks.has(modPack.id)) { throw new libErrors.RequestError(`Mod pack with ID ${modPack.id} already exist`); } @@ -394,7 +394,7 @@ class ControlConnection extends BaseConnection { } async updateModPackRequestHandler(message) { - let modPack = new libData.ModPack(message.data.mod_pack); + let modPack = libData.ModPack.fromJSON(message.data.mod_pack); if (!this._controller.modPacks.has(modPack.id)) { throw new libErrors.RequestError(`Mod pack with ID ${modPack.id} does not exist`); } diff --git a/packages/controller/src/Controller.js b/packages/controller/src/Controller.js index 7557995d..0e57db8b 100644 --- a/packages/controller/src/Controller.js +++ b/packages/controller/src/Controller.js @@ -413,7 +413,7 @@ class Controller { } return new Map(); } - return new Map(json.map(e => [e.id, new libData.ModPack(e)])); + return new Map(json.map(e => [e.id, libData.ModPack.fromJSON(e)])); } static async saveModPacks(filePath, modPacks) { diff --git a/packages/controller/src/InstanceInfo.js b/packages/controller/src/InstanceInfo.js index 090f00e5..ef0cd66b 100644 --- a/packages/controller/src/InstanceInfo.js +++ b/packages/controller/src/InstanceInfo.js @@ -26,11 +26,9 @@ class InstanceInfo { */ status; - // This is not actually json as a config can't be loaded synchronously but - // that is likely to change in the future. - constructor(json) { - this.config = json["config"]; - this.status = json["status"]; + constructor({ config, status }) { + this.config = config; + this.status = status; } toJSON() { diff --git a/packages/controller/src/routes.js b/packages/controller/src/routes.js index a1270df6..7392bebd 100644 --- a/packages/controller/src/routes.js +++ b/packages/controller/src/routes.js @@ -232,7 +232,7 @@ async function uploadExport(req, res) { await libFileOps.safeOutputFile(path.join("static", `${name}.${hash}${ext}`), await file.async("nodebuffer")); } - modPack.exportManifest = new libData.ExportManifest({ assets }); + modPack.exportManifest = new libData.ExportManifest(assets); modPack.fillDefaultSettings(settingPrototypes, logger); res.app.locals.controller.modPackUpdated(modPack); diff --git a/packages/ctl/src/commands.js b/packages/ctl/src/commands.js index b6d647bd..a89e702d 100644 --- a/packages/ctl/src/commands.js +++ b/packages/ctl/src/commands.js @@ -1002,7 +1002,7 @@ modPackCommands.add(new libCommand.Command({ }); }], handler: async function(args, control) { - const modPack = new libData.ModPack(); + const modPack = libData.ModPack.fromJSON({}); modPack.name = args.name; if (args.description) { modPack.description = args.description; } if (args.factorioVersion) { @@ -1038,7 +1038,7 @@ modPackCommands.add(new libCommand.Command({ const response = await libLink.messages.getModPack.send(control, { id: await libCommand.resolveModPack(control, args.modPack), }); - const modPack = new libData.ModPack(response.mod_pack); + const modPack = libData.ModPack.fromJSON(response.mod_pack); print(modPack.toModPackString()); }, })); @@ -1065,7 +1065,7 @@ modPackCommands.add(new libCommand.Command({ let response = await libLink.messages.getModPack.send(control, { id: await libCommand.resolveModPack(control, args.modPack), }); - let modPack = new libData.ModPack(response.mod_pack); + let modPack = libData.ModPack.fromJSON(response.mod_pack); if (args.name) { modPack.name = args.name; } if (args.description) { modPack.description = args.description; } @@ -1259,7 +1259,7 @@ modCommands.add(new libCommand.Command({ } if (result.body.mods && result.body.mods.length) { - const mod = new libData.ModInfo(result.body.mods[0]); + const mod = libData.ModInfo.fromJSON(result.body.mods[0]); logger.info(`Successfully uploaded ${mod.filename}`); } diff --git a/packages/host/src/Instance.js b/packages/host/src/Instance.js index d416e46c..3da1c9f9 100644 --- a/packages/host/src/Instance.js +++ b/packages/host/src/Instance.js @@ -604,7 +604,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); } else { response = await libLink.messages.getModPack.send(this, { id: modPackId }); } - this.activeModPack = new libData.ModPack(response.mod_pack); + this.activeModPack = libData.ModPack.fromJSON(response.mod_pack); // TODO validate factorioVersion diff --git a/packages/lib/data/ExportManifest.js b/packages/lib/data/ExportManifest.js index a07d663e..dcc226a2 100644 --- a/packages/lib/data/ExportManifest.js +++ b/packages/lib/data/ExportManifest.js @@ -22,6 +22,10 @@ class ExportManifest { */ assets = {}; + constructor(assets) { + if (assets) { this.assets = assets; } + } + static jsonSchema = { type: "object", additionalProperties: false, @@ -34,12 +38,8 @@ class ExportManifest { }, }; - constructor(json = {}) { - if (json.assets) { this.assets = json.assets; } - } - - toJSON() { - return { assets: this.assets }; + static fromJSON(json) { + return new this(json.assets); } } diff --git a/packages/lib/data/ModInfo.js b/packages/lib/data/ModInfo.js index e4953ae8..83cc175f 100644 --- a/packages/lib/data/ModInfo.js +++ b/packages/lib/data/ModInfo.js @@ -155,22 +155,26 @@ class ModInfo { static validate = libSchema.compile(this.jsonSchema); - constructor(json = {}) { + static fromJSON(json) { + const modInfo = new this(); + // info.json fields - if (json.name) { this.name = json.name; } - if (json.version) { this.version = json.version; } - if (json.title) { this.title = json.title; } - if (json.author) { this.author = json.author; } - if (json.contact) { this.contact = json.contact; } - if (json.homepage) { this.homepage = json.homepage; } - if (json.description) { this.description = json.description; } - if (json.factorio_version) { this.factorioVersion = json.factorio_version; } - if (json.dependencies) { this.dependencies = json.dependencies; } + if (json.name) { modInfo.name = json.name; } + if (json.version) { modInfo.version = json.version; } + if (json.title) { modInfo.title = json.title; } + if (json.author) { modInfo.author = json.author; } + if (json.contact) { modInfo.contact = json.contact; } + if (json.homepage) { modInfo.homepage = json.homepage; } + if (json.description) { modInfo.description = json.description; } + if (json.factorio_version) { modInfo.factorioVersion = json.factorio_version; } + if (json.dependencies) { modInfo.dependencies = json.dependencies; } // Additional data - if (json.size) { this.size = json.size; } - if (json.sha1) { this.sha1 = json.sha1; } - if (json.is_deleted) { this.isDeleted = json.is_deleted; } + if (json.size) { modInfo.size = json.size; } + if (json.sha1) { modInfo.sha1 = json.sha1; } + if (json.is_deleted) { modInfo.isDeleted = json.is_deleted; } + + return modInfo; } toJSON() { @@ -218,7 +222,7 @@ class ModInfo { throw new Error(`Mod's version (${modInfo.version}) is invalid`); } - return new this({ + return this.fromJSON({ ...modInfo, size: (await fs.stat(modPath)).size, diff --git a/packages/lib/data/ModPack.js b/packages/lib/data/ModPack.js index 8279cf03..7536f7b8 100644 --- a/packages/lib/data/ModPack.js +++ b/packages/lib/data/ModPack.js @@ -142,29 +142,33 @@ class ModPack { static validate = libSchema.compile(this.jsonSchema); - constructor(json = {}) { + static fromJSON(json) { + const modPack = new this(); + if (json.id) { - this.id = json.id; + modPack.id = json.id; } else { - this.id = Math.random() * 2**31 | 0; + modPack.id = Math.random() * 2**31 | 0; } - if (json.name) { this.name = json.name; } - if (json.description) { this.description = json.description; } - if (json.factorio_version) { this.factorioVersion = json.factorio_version; } - if (json.mods) { this.mods = new Map(json.mods.map(m => [m.name, m])); } + if (json.name) { modPack.name = json.name; } + if (json.description) { modPack.description = json.description; } + if (json.factorio_version) { modPack.factorioVersion = json.factorio_version; } + if (json.mods) { modPack.mods = new Map(json.mods.map(m => [m.name, m])); } if (json.settings) { - this.settings = { + modPack.settings = { "startup": new Map(Object.entries(json.settings["startup"])), "runtime-global": new Map(Object.entries(json.settings["runtime-global"])), "runtime-per-user": new Map(Object.entries(json.settings["runtime-per-user"])), }; } - if (json.export_manifest) { this.exportManifest = new ExportManifest(json.export_manifest); } - if (json.is_deleted) { this.isDeleted = json.is_deleted; } + if (json.export_manifest) { modPack.exportManifest = ExportManifest.fromJSON(json.export_manifest); } + if (json.is_deleted) { modPack.isDeleted = json.is_deleted; } - if (!this.mods.has("base")) { - this.mods.set("base", { name: "base", enabled: true, version: this.factorioVersion }); + if (!modPack.mods.has("base")) { + modPack.mods.set("base", { name: "base", enabled: true, version: modPack.factorioVersion }); } + + return modPack; } toJSON() { @@ -180,7 +184,7 @@ class ModPack { "runtime-per-user": Object.fromEntries(this.settings["runtime-per-user"]), }, }; - if (this.exportManifest) { json.export_manifest = this.exportManifest.toJSON(); } + if (this.exportManifest) { json.export_manifest = this.exportManifest; } if (this.isDeleted) { json.is_deleted = this.isDeleted; } return json; } @@ -217,7 +221,7 @@ class ModPack { throw new Error("Malformed mod pack string: Schema validation failed"); } - return new this(json); + return this.fromJSON(json); } toModSettingsDat() { diff --git a/packages/web_ui/src/components/ModPackViewPage.jsx b/packages/web_ui/src/components/ModPackViewPage.jsx index dbc0f20c..9b723d36 100644 --- a/packages/web_ui/src/components/ModPackViewPage.jsx +++ b/packages/web_ui/src/components/ModPackViewPage.jsx @@ -57,7 +57,7 @@ function SearchModsTable(props) { // In React < v18 this causes 3 renders and a partial state necessitating the use // of || 0 on modResultSelectedVersion.get() calls. Remove when updating React. setModResults(response.results.map( - ({ name, versions }) => ({ name, versions: versions.map(mod => new libData.ModInfo(mod)) }) + ({ name, versions }) => ({ name, versions: versions.map(mod => libData.ModInfo.fromJSON(mod)) }) )); setModResultSelectedVersion(new Map(response.results.map(({ name, versions }) => [name, 0]))); setModResultCount(response.result_count); @@ -630,7 +630,7 @@ export default function ModPackViewPage() { return; } - const modified = new libData.ModPack(modPack.toJSON()); + const modified = libData.ModPack.fromJSON(JSON.parse(JSON.stringify(modPack))); for (let change of changes) { if (change.type === "name") { modified.name = change.value; diff --git a/packages/web_ui/src/components/ModsPage.jsx b/packages/web_ui/src/components/ModsPage.jsx index 71a1cca6..d6050608 100644 --- a/packages/web_ui/src/components/ModsPage.jsx +++ b/packages/web_ui/src/components/ModsPage.jsx @@ -95,7 +95,7 @@ function CreateModPackButton() { } catch { return; // Validation failed } - const modPack = new libData.ModPack(); + const modPack = libData.ModPack.fromJSON({}); modPack.name = values.name; modPack.factorioVersion = values.factorioVersion; if (values.description) { modPack.description = values.description; } diff --git a/packages/web_ui/src/model/export_manifest.jsx b/packages/web_ui/src/model/export_manifest.jsx index 50ef1467..5e53ad48 100644 --- a/packages/web_ui/src/model/export_manifest.jsx +++ b/packages/web_ui/src/model/export_manifest.jsx @@ -12,7 +12,7 @@ export function useExportManifest() { useEffect(() => { async function load() { let result = await libLink.messages.getDefaultModPack.send(control); - let modPack = new libData.ModPack(result.mod_pack); + let modPack = libData.ModPack.fromJSON(result.mod_pack); if (modPack.exportManifest) { exportManifestCache = modPack.exportManifest; setExportManifest(exportManifestCache); diff --git a/packages/web_ui/src/model/mod_pack.jsx b/packages/web_ui/src/model/mod_pack.jsx index 4983cf54..438ff8ed 100644 --- a/packages/web_ui/src/model/mod_pack.jsx +++ b/packages/web_ui/src/model/mod_pack.jsx @@ -12,7 +12,7 @@ export function useModPack(id) { function updateModPack() { libLink.messages.getModPack.send(control, { id }).then(result => { - setModPack(new libData.ModPack(result.mod_pack)); + setModPack(libData.ModPack.fromJSON(result.mod_pack)); }).catch(err => { logger.error(`Failed to get mod pack: ${err}`); setModPack({ missing: true }); @@ -41,7 +41,7 @@ export function useModPackList() { function updateModPackList() { libLink.messages.listModPacks.send(control).then(result => { - setModPackList(result.list.map(pack => new libData.ModPack(pack))); + setModPackList(result.list.map(pack => libData.ModPack.fromJSON(pack))); }).catch(err => { logger.error(`Failed to list mod packs:\n${err}`); }); diff --git a/packages/web_ui/src/model/mods.jsx b/packages/web_ui/src/model/mods.jsx index 2ef9971c..6be1e75d 100644 --- a/packages/web_ui/src/model/mods.jsx +++ b/packages/web_ui/src/model/mods.jsx @@ -13,7 +13,7 @@ export function useModList() { function updateModList() { libLink.messages.listMods.send(control).then(result => { - setModList(result.list.map(mod => new libData.ModInfo(mod))); + setModList(result.list.map(mod => libData.ModInfo.fromJSON(mod))); }).catch(err => { logger.error(`Failed to list mods:\n${err}`); }); diff --git a/packages/web_ui/src/util/websocket.jsx b/packages/web_ui/src/util/websocket.jsx index 3d1df0cf..92177c02 100644 --- a/packages/web_ui/src/util/websocket.jsx +++ b/packages/web_ui/src/util/websocket.jsx @@ -312,7 +312,7 @@ export class Control extends libLink.Link { } async modPackUpdateEventHandler(message) { - let modPack = new libData.ModPack(message.data.mod_pack); + let modPack = libData.ModPack.fromJSON(message.data.mod_pack); let handlers = [].concat( this.modPackUpdateHandlers.get(null) || [], this.modPackUpdateHandlers.get(modPack.id) || [], @@ -370,7 +370,7 @@ export class Control extends libLink.Link { } async modUpdateEventHandler(message) { - let mod = new libData.ModInfo(message.data.mod); + let mod = libData.ModInfo.fromJSON(message.data.mod); let handlers = [].concat( this.modUpdateHandlers.get(null) || [], this.modUpdateHandlers.get(mod.name) || [] diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index bbcd662a..bb5436ca 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -308,7 +308,7 @@ describe("Integration of Clusterio", function() { await fs.remove(exportPath); await execCtl("instance export-data test"); let result = await libLink.messages.getDefaultModPack.send(getControl()); - let modPack = new libData.ModPack(result.mod_pack); + let modPack = libData.ModPack.fromJSON(result.mod_pack); let assets = modPack.exportManifest.assets; assert(Object.keys(assets).length > 1, "Export assets is empty"); for (let key of ["settings", "prototypes", "item-metadata", "item-spritesheet", "locale"]) { @@ -847,7 +847,7 @@ describe("Integration of Clusterio", function() { let response = await libLink.messages.listModPacks.send(getControl()); let modPack = response.list.find(entry => entry.name === "full-pack"); assert(modPack, "created mod pack not found"); - let reference = new libData.ModPack(); + let reference = libData.ModPack.fromJSON({}); reference.id = modPack.id; reference.name = "full-pack"; reference.description = "Description"; @@ -857,7 +857,7 @@ describe("Integration of Clusterio", function() { reference.settings["runtime-global"].set("MyInt", { value: 1235 }); reference.settings["runtime-global"].set("MyDouble", { value: 12.25 }); reference.settings["runtime-per-user"].set("MyString", { value: "a-string" }); - assert.deepEqual(new libData.ModPack(modPack), reference); + assert.deepEqual(libData.ModPack.fromJSON(modPack), reference); }); }); @@ -875,7 +875,7 @@ describe("Integration of Clusterio", function() { describe("mod-pack import/export", function() { it("should should roundtrip a mod-pack", async function() { - let reference = new libData.ModPack(); + let reference = libData.ModPack.fromJSON({}); reference.name = "imported-pack"; reference.description = "Description"; reference.factorioVersion = "0.17.59"; diff --git a/test/integration/index.js b/test/integration/index.js index 10c3686a..d66c6ef1 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -222,7 +222,7 @@ before(async function() { control = new TestControl(controlConnector); await controlConnector.connect(); - const testPack = new libData.ModPack(); + const testPack = libData.ModPack.fromJSON({}); testPack.id = 12; testPack.name = "subspace_storage-pack"; testPack.factorioVersion = "1.1.0"; diff --git a/test/lib/data/ModInfo.js b/test/lib/data/ModInfo.js index 9e435e81..8ff5c170 100644 --- a/test/lib/data/ModInfo.js +++ b/test/lib/data/ModInfo.js @@ -18,27 +18,27 @@ describe("lib/data/ModInfo", function() { if (!validate(json)) { throw validate.errors; } - assert.deepEqual(new ModInfo(json), pack); + assert.deepEqual(ModInfo.fromJSON(json), pack); } - check(new ModInfo()); - check(new ModInfo({ name: "MyMod" })); - check(new ModInfo({ version: "1.0.0" })); - check(new ModInfo({ title: "My Extravagent Mod" })); - check(new ModInfo({ author: "Me" })); - check(new ModInfo({ contact: "Use my email" })); - check(new ModInfo({ homepage: "It's over there" })); - check(new ModInfo({ description: "My Description" })); - check(new ModInfo({ factorio_version: "2.0" })); - check(new ModInfo({ dependencies: [] })); - check(new ModInfo({ dependencies: ["UltraMod", "SuperLib >= 1.00", "! bad-mod"] })); - check(new ModInfo({ filename: "MyMod_1.0.0.zip" })); - check(new ModInfo({ size: 1024 })); - check(new ModInfo({ sha1: "verified-as-MyMod" })); - check(new ModInfo({ is_deleted: true })); + check(ModInfo.fromJSON({})); + check(ModInfo.fromJSON({ name: "MyMod" })); + check(ModInfo.fromJSON({ version: "1.0.0" })); + check(ModInfo.fromJSON({ title: "My Extravagent Mod" })); + check(ModInfo.fromJSON({ author: "Me" })); + check(ModInfo.fromJSON({ contact: "Use my email" })); + check(ModInfo.fromJSON({ homepage: "It's over there" })); + check(ModInfo.fromJSON({ description: "My Description" })); + check(ModInfo.fromJSON({ factorio_version: "2.0" })); + check(ModInfo.fromJSON({ dependencies: [] })); + check(ModInfo.fromJSON({ dependencies: ["UltraMod", "SuperLib >= 1.00", "! bad-mod"] })); + check(ModInfo.fromJSON({ filename: "MyMod_1.0.0.zip" })); + check(ModInfo.fromJSON({ size: 1024 })); + check(ModInfo.fromJSON({ sha1: "verified-as-MyMod" })); + check(ModInfo.fromJSON({ is_deleted: true })); // All at once - check(new ModInfo({ + check(ModInfo.fromJSON({ name: "MyMod", version: "1.0.0", title: "My Extravagent Mod", @@ -58,14 +58,14 @@ describe("lib/data/ModInfo", function() { it("should sort integer mod versions lexicographically", function() { let unsortedVersions = ["1.0.0", "1.1.0", "0.1.0", "3.0.0", "1.2.0", "0.3.1", "0.3.3", "2.1.1", "0.0.1"]; let sortedVersions = ["0.0.1", "0.1.0", "0.3.1", "0.3.3", "1.0.0", "1.1.0", "1.2.0", "2.1.1", "3.0.0"]; - let mods = unsortedVersions.map(v => new ModInfo({ version: v })); + let mods = unsortedVersions.map(v => ModInfo.fromJSON({ version: v })); mods.sort((a, b) => a.integerVersion - b.integerVersion); assert.deepEqual(mods.map(mod => mod.version), sortedVersions); }); it("should sort integer factorio versions lexicographically", function() { let unsortedVersions = ["1.0", "1.1", "0.1", "3.0", "1.2", "0.3", "2.1"]; let sortedVersions = ["0.1", "0.3", "1.0", "1.1", "1.2", "2.1", "3.0"]; - let factorioMods = unsortedVersions.map(v => new ModInfo({ factorio_version: v })); + let factorioMods = unsortedVersions.map(v => ModInfo.fromJSON({ factorio_version: v })); factorioMods.sort((a, b) => a.integerFactorioVersion - b.integerFactorioVersion); assert.deepEqual(factorioMods.map(mod => mod.factorioVersion), sortedVersions); }); @@ -83,7 +83,7 @@ describe("lib/data/ModInfo", function() { const mod = await ModInfo.fromModFile(filePath); assert.deepEqual( mod, - new ModInfo({ + ModInfo.fromJSON({ name: "empty_mod", version: "1.0.0", title: "An Empty Mod", diff --git a/test/lib/data/ModPack.js b/test/lib/data/ModPack.js index 43cdc856..49c77a44 100644 --- a/test/lib/data/ModPack.js +++ b/test/lib/data/ModPack.js @@ -16,22 +16,22 @@ describe("lib/data/ModPack", function() { if (!validate(json)) { throw validate.errors; } - assert.deepEqual(new ModPack(json), pack); + assert.deepEqual(ModPack.fromJSON(json), pack); const packStringed = ModPack.fromModPackString(pack.toModPackString()); packStringed.id = pack.id; packStringed.exportManifest = pack.exportManifest; assert.deepEqual(packStringed, pack); } - check(new ModPack()); - check(new ModPack({ name: "MyPack" })); - check(new ModPack({ description: "My Description" })); - check(new ModPack({ factorio_version: "2.0" })); - check(new ModPack({ mods: [ + check(ModPack.fromJSON({})); + check(ModPack.fromJSON({ name: "MyPack" })); + check(ModPack.fromJSON({ description: "My Description" })); + check(ModPack.fromJSON({ factorio_version: "2.0" })); + check(ModPack.fromJSON({ mods: [ { name: "subspace_storage", enabled: true, version: "1.99.8" }, { name: "clusterio_lib", enabled: true, version: "0.1.2", sha1: "012345abcd" }, ]})); - check(new ModPack({ settings: { + check(ModPack.fromJSON({ settings: { "startup": { "bool-setting": { "value": true }, }, @@ -42,9 +42,9 @@ describe("lib/data/ModPack", function() { "string-setting": { "value": "a string" }, }, }})); - check(new ModPack({ export_manifest: { assets: { setting: "settings.json" }}})); - check(new ModPack({ deleted: true })); - check(new ModPack({ + check(ModPack.fromJSON({ export_manifest: { assets: { setting: "settings.json" }}})); + check(ModPack.fromJSON({ deleted: true })); + check(ModPack.fromJSON({ name: "Super pack", description: "Every option at once.", factorio_version: "2.0", @@ -71,7 +71,7 @@ describe("lib/data/ModPack", function() { it("should sort integer factorio versions lexicographically", function() { let unsortedVersions = ["1.0", "1.1.0", "0.1", "3.0.0", "1.2", "0.3.1", "0.3.3", "2.1.1", "0.0.1"]; let sortedVersions = ["0.0.1", "0.1", "0.3.1", "0.3.3", "1.0", "1.1.0", "1.2", "2.1.1", "3.0.0"]; - let factorioMods = unsortedVersions.map(v => new ModPack({ factorio_version: v })); + let factorioMods = unsortedVersions.map(v => ModPack.fromJSON({ factorio_version: v })); factorioMods.sort((a, b) => a.integerFactorioVersion - b.integerFactorioVersion); assert.deepEqual(factorioMods.map(mod => mod.factorioVersion), sortedVersions); }); @@ -105,7 +105,7 @@ describe("lib/data/ModPack", function() { }; const mockLogger = { warn: () => {} }; it("should fill in defaults for settings", function() { - const pack = new ModPack(); + const pack = ModPack.fromJSON({}); pack.fillDefaultSettings(prototypes, mockLogger); assert.deepEqual(pack.toJSON().settings, { "startup": { @@ -120,7 +120,7 @@ describe("lib/data/ModPack", function() { }); }); it("should not overwrite existing values", function() { - const pack = new ModPack({ settings: { + const pack = ModPack.fromJSON({ settings: { "startup": { "bool": { "value": false }, }, @@ -145,7 +145,7 @@ describe("lib/data/ModPack", function() { }); }); it("should ignore unknown setting_type and missing default_value", function() { - const pack = new ModPack(); + const pack = ModPack.fromJSON({}); pack.fillDefaultSettings({ "string-setting": { "foo": { @@ -196,7 +196,7 @@ describe("lib/data/ModPack", function() { describe("toModSettingsDat()", function() { it("should properly serialise settings", function() { - const pack = new ModPack({ settings: { + const pack = ModPack.fromJSON({ settings: { "startup": { "bool-setting": { "value": true }, },