diff --git a/package-lock.json b/package-lock.json index a96e866789..9924a0e0e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6153,7 +6153,7 @@ }, "which-module": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, diff --git a/src/model/node/NodeVersion.ts b/src/model/node/NodeVersion.ts new file mode 100644 index 0000000000..5d49577711 --- /dev/null +++ b/src/model/node/NodeVersion.ts @@ -0,0 +1,133 @@ +/* + * Copyright 2021 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class NodeVersion { + /** + * Create a NodeVersion from a given raw version number. + * @param {number} rawNodeVersion - Node version in number format. + * ex: 655367 + * @returns {NodeVersion} + */ + public static createFromRawNodeVersion(rawNodeVersion: number): NodeVersion { + if (!NodeVersion.isValidRawNodeVersion(rawNodeVersion)) { + throw new Error(`Invalid node version number '${rawNodeVersion}'`); + } + + return new NodeVersion(rawNodeVersion); + } + + /** + * Create a NodeVersion from a given formatted version string. + * @param {string} formattedNodeVersion - Node version in string format. + * ex: 0.10.0.7 + * @returns {NodeVersion} + */ + public static createFromFormattedNodeVersion(formattedNodeVersion: string): NodeVersion { + if (!NodeVersion.isValidFormattedNodeVersion(formattedNodeVersion)) { + throw new Error(`Invalid node version string '${formattedNodeVersion}'`); + } + + const placeholderHex = '00'; + const hexVersionNumber = formattedNodeVersion + .split('.') + .map((value) => (placeholderHex + parseInt(value).toString(16)).slice(-2)) + .join(''); + + const rawVersionNumber = parseInt(hexVersionNumber, 16); + return new NodeVersion(rawVersionNumber); + } + + /** + * Determines the validity of a raw node version number. + * @param {string} rawNodeVersion The raw node version number. Expected format 655367 + * @returns {boolean} true if the raw node version number is valid, false otherwise. + */ + public static isValidRawNodeVersion = (rawNodeVersion: number): boolean => { + const maxRawNodeVersion = 4294967295; + const minRawNodeVersion = 0; + + return Number.isInteger(rawNodeVersion) && rawNodeVersion >= minRawNodeVersion && rawNodeVersion <= maxRawNodeVersion; + }; + + /** + * Determines the validity of a formatted node version string. + * @param {string} formattedNodeVersion The formatted node version string. Expected format: 0.10.0.7 + * @returns {boolean} true if the formatted node version string is valid, false otherwise. + */ + public static isValidFormattedNodeVersion = (formattedNodeVersion: string): boolean => { + const maxFormattedNodeVersionChunkValue = 255; + const minFormattedNodeVersionChunkValue = 0; + + const versionChuncks = formattedNodeVersion.split('.').map((value) => parseInt(value)); + + if (versionChuncks.length !== 4) { + return false; + } + + const isVersionChuncksValid = !versionChuncks.find( + (value) => isNaN(value) || value < minFormattedNodeVersionChunkValue || value > maxFormattedNodeVersionChunkValue, + ); + + return isVersionChuncksValid; + }; + + /** + * @internal + * @param nodeVersion + */ + private constructor( + /** + * The raw node version value. + */ + private readonly nodeVersion: number, + ) {} + + /** + * Get node version in formatted format ex: 0.10.0.7 + * @returns {string} + */ + public formatted(): string { + const placeholderHex = '00000000'; + const hexNodeVersion = (placeholderHex + this.nodeVersion.toString(16)).slice(-8); + const formattedNodeVersion = hexNodeVersion + .match(/.{1,2}/g)! + .map((hex) => parseInt(hex, 16)) + .join('.'); + + return formattedNodeVersion; + } + + /** + * Get node version in the raw numeric format ex: 655367. + * @returns {number} + */ + public raw(): number { + return this.nodeVersion; + } + + /** + * Compares node versions for equality + * @param nodeVersion - Node version to compare + * @returns {boolean} + */ + public equals(nodeVersion: any): boolean { + if (nodeVersion instanceof NodeVersion) { + return this.nodeVersion === nodeVersion.raw(); + } + + return false; + } +} diff --git a/src/model/node/index.ts b/src/model/node/index.ts index f4f07cae7e..d6078a8c0c 100644 --- a/src/model/node/index.ts +++ b/src/model/node/index.ts @@ -3,5 +3,6 @@ export * from './NodeHealth'; export * from './NodeInfo'; export * from './NodeTime'; +export * from './NodeVersion'; export * from './RoleType'; export * from './ServerInfo'; diff --git a/test/model/node/NodeVersion.spec.ts b/test/model/node/NodeVersion.spec.ts new file mode 100644 index 0000000000..be255898c8 --- /dev/null +++ b/test/model/node/NodeVersion.spec.ts @@ -0,0 +1,149 @@ +/* + * Copyright 2021 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { NodeVersion } from '../../../src/model/node/NodeVersion'; + +describe('NodeVersion', () => { + const validRawVersion1 = 4294967295; + const validFormattedVersion1 = '255.255.255.255'; + const validRawVersion2 = 0; + const validFormattedVersion2 = '0.0.0.0'; + const validRawVersion3 = 655367; + const validFormattedVersion3 = '0.10.0.7'; + + const invalidFormattedVersion1 = '0.0.0.0.0'; + const invalidFormattedVersion2 = '-1.0.0.0'; + const invalidFormattedVersion3 = '0.0.0.256'; + const invalidFormattedVersion4 = 'some text'; + const invalidFormattedVersion5 = ''; + + const invalidRawVersion1 = -1231; + const invalidRawVersion2 = 42949672955; + const invalidRawVersion3 = 23.34; + const invalidRawVersion4 = Infinity; + + it('createComplete a NodeVersion by given raw version', () => { + const nodeVersion = NodeVersion.createFromRawNodeVersion(validRawVersion1); + expect(nodeVersion.raw()).to.be.equal(validRawVersion1); + }); + + it('createComplete a NodeVersion by given raw version', () => { + const nodeVersion = NodeVersion.createFromRawNodeVersion(validRawVersion2); + expect(nodeVersion.raw()).to.be.equal(validRawVersion2); + }); + + it('createComplete a NodeVersion by given raw version', () => { + const nodeVersion = NodeVersion.createFromRawNodeVersion(validRawVersion3); + expect(nodeVersion.raw()).to.be.equal(validRawVersion3); + }); + + it('createComplete a NodeVersion by given formatted version', () => { + const nodeVersion = NodeVersion.createFromFormattedNodeVersion(validFormattedVersion1); + expect(nodeVersion.raw()).to.be.equal(validRawVersion1); + }); + + it('createComplete a NodeVersion by given formatted version', () => { + const nodeVersion = NodeVersion.createFromFormattedNodeVersion(validFormattedVersion2); + expect(nodeVersion.raw()).to.be.equal(validRawVersion2); + }); + + it('createComplete a NodeVersion by given formatted version', () => { + const nodeVersion = NodeVersion.createFromFormattedNodeVersion(validFormattedVersion3); + expect(nodeVersion.raw()).to.be.equal(validRawVersion3); + }); + + it('print formatted node version', () => { + const nodeVersion = NodeVersion.createFromRawNodeVersion(validRawVersion1); + expect(nodeVersion.formatted()).to.be.equal(validFormattedVersion1); + }); + + it('print formatted node version', () => { + const nodeVersion = NodeVersion.createFromRawNodeVersion(validRawVersion2); + expect(nodeVersion.formatted()).to.be.equal(validFormattedVersion2); + }); + + it('print formatted node version', () => { + const nodeVersion = NodeVersion.createFromRawNodeVersion(validRawVersion3); + expect(nodeVersion.formatted()).to.be.equal(validFormattedVersion3); + }); + + it('should throw Error when negative raw version provided', () => { + expect(() => { + NodeVersion.createFromRawNodeVersion(invalidRawVersion1); + }).to.throw(`Invalid node version number '${invalidRawVersion1}'`); + }); + + it('should throw Error when too large raw version provided', () => { + expect(() => { + NodeVersion.createFromRawNodeVersion(invalidRawVersion2); + }).to.throw(`Invalid node version number '${invalidRawVersion2}'`); + }); + + it('should throw Error when float number raw version provided', () => { + expect(() => { + NodeVersion.createFromRawNodeVersion(invalidRawVersion3); + }).to.throw(`Invalid node version number '${invalidRawVersion3}'`); + }); + + it('should throw Error when Infinity number raw version provided', () => { + expect(() => { + NodeVersion.createFromRawNodeVersion(invalidRawVersion4); + }).to.throw(`Invalid node version number '${invalidRawVersion4}'`); + }); + + it('should throw Error when invalid formatted version provided', () => { + expect(() => { + NodeVersion.createFromFormattedNodeVersion(invalidFormattedVersion1); + }).to.throw(`Invalid node version string '${invalidFormattedVersion1}'`); + }); + + it('should throw Error when invalid formatted version with one negative provided', () => { + expect(() => { + NodeVersion.createFromFormattedNodeVersion(invalidFormattedVersion2); + }).to.throw(`Invalid node version string '${invalidFormattedVersion2}'`); + }); + + it('should throw Error when invalid formatted version with one large provided', () => { + expect(() => { + NodeVersion.createFromFormattedNodeVersion(invalidFormattedVersion3); + }).to.throw(`Invalid node version string '${invalidFormattedVersion3}'`); + }); + + it('should throw Error when invalid text string provided', () => { + expect(() => { + NodeVersion.createFromFormattedNodeVersion(invalidFormattedVersion4); + }).to.throw(`Invalid node version string '${invalidFormattedVersion4}'`); + }); + + it('should throw Error when empty string provided', () => { + expect(() => { + NodeVersion.createFromFormattedNodeVersion(invalidFormattedVersion5); + }).to.throw(`Invalid node version string '${invalidFormattedVersion5}'`); + }); + + it('should equal versions', () => { + const version = NodeVersion.createFromRawNodeVersion(validRawVersion1); + const compareVersion = NodeVersion.createFromRawNodeVersion(validRawVersion1); + expect(version.equals(compareVersion)).to.be.equal(true); + }); + + it('should not equal versions', () => { + const version = NodeVersion.createFromRawNodeVersion(validRawVersion1); + const compareVersion = NodeVersion.createFromRawNodeVersion(validRawVersion2); + expect(version.equals(compareVersion)).to.be.equal(false); + }); +});