From 54416a5749b81120a618b8b472e1862e720f4108 Mon Sep 17 00:00:00 2001 From: M-Scott-Lassiter Date: Fri, 27 May 2022 08:32:31 -0700 Subject: [PATCH] feat(toBeLineStringGeometry): add new matcher Verifies an object is a valid GeoJSON LineString Geometry. Resolves: #11 --- .cz-config.js | 1 + CONTRIBUTING.md | 9 +- README.md | 2 +- src/core.js | 5 +- src/core/geometries/lineStringGeometry.js | 83 +++++ src/core/geometries/multiPointGeometry.js | 3 - src/matchers.js | 6 +- .../geometries/toBeLineStringGeometry.js | 68 ++++ tests/core.test.js | 8 +- .../toBeLineStringGeometry.test.js.snap | 17 + .../geometries/toBeLineStringGeometry.test.js | 345 ++++++++++++++++++ tests/matchers.test.js | 4 + 12 files changed, 536 insertions(+), 15 deletions(-) create mode 100644 src/core/geometries/lineStringGeometry.js create mode 100644 src/matchers/geometries/toBeLineStringGeometry.js create mode 100644 tests/geometries/__snapshots__/toBeLineStringGeometry.test.js.snap create mode 100644 tests/geometries/toBeLineStringGeometry.test.js diff --git a/.cz-config.js b/.cz-config.js index 82b150a..a8f9cf6 100644 --- a/.cz-config.js +++ b/.cz-config.js @@ -7,6 +7,7 @@ const coordinateMatchers = [ { name: 'isValid3DCoordinate' }, { name: 'isValidBoundingBox' }, { name: 'isValidCoordinate' }, + { name: 'toBeLineStringGeometry' }, { name: 'toBeMultiPointGeometry' }, { name: 'toBePointGeometry' } ] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1979d54..6975f28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,16 +88,15 @@ To submit a pull request, - [ ] Create a core function under `src/core/` - [ ] Document the function using JSDoc. Refer to the issue. - [ ] Register the core function in `src/core.js` + - [ ] Add a verification test to `tests/core.test.js` - Create Matcher Function - [ ] Create a matcher function under `src/matchers/` - [ ] Document the matcher using JSDoc. Refer to the issue. - [ ] Register the matcher in `src/matchers.js` + - [ ] Add a verification test to `matchers.test.js` - [ ] Add the matcher to the `.cz-config.js` list (alphabetical order under the `coordinateMatchers` variable) -- Add Testing - - [ ] Create a test for the matcher under `tests/` - - [ ] Add a test to `tests/core.test.js` - - [ ] Add a test to `matchers.test.js` - - [ ] Verify all tests pass and have 100% coverage +- [ ] Create a test for the matcher under `tests/` +- [ ] Verify all tests pass and have 100% coverage - [ ] Add the matcher to the README.md list (alphabetical order within category) - [ ] Run the `build` script locally - [ ] Push to Github then open pull request diff --git a/README.md b/README.md index dfba05b..895d8de 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ _1.0.0_ - [x] toBePointGeometry - [x] toBeMultiPointGeometry -- [ ] toBeLineStringGeometry +- [x] toBeLineStringGeometry - [ ] toBeMultiLineStringGeometry - [ ] toBePolygonGeometry - [ ] toBeMultiPolygonGeometry diff --git a/src/core.js b/src/core.js index 4adb6b6..22ecb9c 100644 --- a/src/core.js +++ b/src/core.js @@ -13,6 +13,7 @@ exports.coordinates = { } exports.geometries = { - pointGeometry: require('./core/geometries/pointGeometry'), - multiPointGeometry: require('./core/geometries/multiPointGeometry') + lineStringGeometry: require('./core/geometries/lineStringGeometry'), + multiPointGeometry: require('./core/geometries/multiPointGeometry'), + pointGeometry: require('./core/geometries/pointGeometry') } diff --git a/src/core/geometries/lineStringGeometry.js b/src/core/geometries/lineStringGeometry.js new file mode 100644 index 0000000..f980561 --- /dev/null +++ b/src/core/geometries/lineStringGeometry.js @@ -0,0 +1,83 @@ +const { validCoordinate } = require('../coordinates/validCoordinate') + +/** + * Verifies an object is a valid GeoJSON LineString Geometry. This geometry requires a + * 'type' property that must equal "LineString", and a 'coordinates' property that contains + * an array of valid WGS-84 GeoJSON coordinate(s). The coordinates may be an empty array, + * but may not be an array of empty arrays. + * + * Foreign members are allowed with the exceptions thrown below. + * + * @memberof Core.Geometries + * @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/11 + * @param {object} geometryObject a GeoJSON Multi Point Geometry object + * @returns {boolean} True if a valid GeoJSON Multi Point Geometry. If invalid, it will throw an error. + * @throws {Error} Argument not an object + * @throws {Error} Must have a type property with value 'LineString' + * @throws {Error} forbidden from having a property 'geometry', 'properties', or 'features' + * @example + * const linestring = { + "type": "LineString", + "coordinates": [ + [ + [180.0, 40.0], [180.0, 50.0], [170.0, 50.0], + [170.0, 40.0], [180.0, 40.0] + ] + ] + } + const point = { + type: "Point", + coordinates: [100.0, 0.0] + } + + console.log(lineStringGeometry(linestring)) // true + console.log(lineStringGeometry(point)) // throws error + */ +function lineStringGeometry(geometryObject) { + if ( + typeof geometryObject !== 'object' || + Array.isArray(geometryObject) || + geometryObject === null + ) { + throw new Error('Argument must be a GeoJSON LineString Geometry object.') + } + + if (!('coordinates' in geometryObject)) { + throw new Error(`GeoJSON LineString Geometry must contain a 'coordinates' property.`) + } + + if (geometryObject.type !== 'LineString') { + throw new Error(`Must have a type property with value 'LineString'`) + } + + if ('geometry' in geometryObject) { + throw new Error( + `GeoJSON LineString Geometry objects are forbidden from having a property 'geometry'.` + ) + } + + if ('properties' in geometryObject) { + throw new Error( + `GeoJSON LineString Geometry objects are forbidden from having a property 'properties'.` + ) + } + + if ('features' in geometryObject) { + throw new Error( + `GeoJSON LineString Geometry objects are forbidden from having a property 'features'.` + ) + } + + // // Geometry objects are allowed to have empty arrays as coordinates, however validCoordinate may not. + // If coordinates is an empty array, we're done. Otherwise, check for coordinate validity. + if (!Array.isArray(geometryObject.coordinates) && geometryObject.coordinates.length !== 1) { + throw new Error('Coordinates property must be an array of valid GeoJSON coordinates') + } + for (let i = 0; i < geometryObject.coordinates.length; i++) { + validCoordinate(geometryObject.coordinates[i]) + } + + return true +} + +exports.lineStringGeometry = lineStringGeometry diff --git a/src/core/geometries/multiPointGeometry.js b/src/core/geometries/multiPointGeometry.js index 224e0fe..fc41f62 100644 --- a/src/core/geometries/multiPointGeometry.js +++ b/src/core/geometries/multiPointGeometry.js @@ -66,9 +66,6 @@ function multiPointGeometry(geometryObject) { // Geometry objects are allowed to have empty arrays as coordinates, however validCoordinate may not. // If coordinates is an empty array, we're done. Otherwise, check for coordinate validity. - // if (Array.isArray(geometryObject.coordinates) && geometryObject.coordinates.length === 0) { - // return true - // } if (!Array.isArray(geometryObject.coordinates) && geometryObject.coordinates.length !== 1) { throw new Error('Coordinates property must be an array of valid GeoJSON coordinates') } diff --git a/src/matchers.js b/src/matchers.js index d87f267..9c738e2 100644 --- a/src/matchers.js +++ b/src/matchers.js @@ -22,9 +22,11 @@ exports.coordinates = { // Geometries exports.geometries = { - toBePointGeometry: require('./matchers/geometries/toBePointGeometry').toBePointGeometry, + toBeLineStringGeometry: require('./matchers/geometries/toBeLineStringGeometry') + .toBeLineStringGeometry, toBeMultiPointGeometry: require('./matchers/geometries/toBeMultiPointGeometry') - .toBeMultiPointGeometry + .toBeMultiPointGeometry, + toBePointGeometry: require('./matchers/geometries/toBePointGeometry').toBePointGeometry } // Properties diff --git a/src/matchers/geometries/toBeLineStringGeometry.js b/src/matchers/geometries/toBeLineStringGeometry.js new file mode 100644 index 0000000..9181b9b --- /dev/null +++ b/src/matchers/geometries/toBeLineStringGeometry.js @@ -0,0 +1,68 @@ +const { lineStringGeometry } = require('../../core/geometries/lineStringGeometry') + +// eslint-disable-next-line jsdoc/require-returns +/** + * Verifies an object is a valid GeoJSON LineString Geometry. This geometry requires a + * 'type' property that must equal "LineString", and a 'coordinates' property that contains + * an array of valid WGS-84 GeoJSON coordinate(s). The coordinates may be an empty array, + * but may not be an array of empty arrays. + * + * Foreign members are allowed with the exception of 'geometry', 'properties', or 'features'. + * + * @memberof Matchers.Geometries + * @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/11 + * @param {object} geometryObject a GeoJSON MultiPoint Geometry object + * @example + const linestring = { + "type": "LineString", + "coordinates": [ + [ + [180.0, 40.0], [180.0, 50.0], [170.0, 50.0], + [170.0, 40.0], [180.0, 40.0] + ] + ] + } + const point = { + type: "Point", + coordinates: [100.0, 0.0] + } + + expect(linestring).toBeLineStringGeometry() + + expect(point).not.toBeLineStringGeometry() + */ +function toBeLineStringGeometry(geometryObject) { + const { printReceived, matcherHint } = this.utils + const passMessage = + // eslint-disable-next-line prefer-template + matcherHint('.not.toBeLineStringGeometry', 'GeometryObject', '') + + '\n\n' + + `Expected input to not be a valid GeoJSON LineString geometry.\n\n` + + `Received: ${printReceived(geometryObject)}` + + /** + * Combines a custom error message with built in Jest tools to provide a more descriptive error + * meessage to the end user. + * + * @param {string} errorMessage Error message text to return to the user + * @returns {string} Concatenated Jest test result string + */ + function failMessage(errorMessage) { + return ( + // eslint-disable-next-line prefer-template, no-unused-expressions + matcherHint('.toBeLineStringGeometry', 'GeometryObject', '') + + '\n\n' + + `${errorMessage}\n\n` + + `Received: ${printReceived(geometryObject)}` + ) + } + + try { + lineStringGeometry(geometryObject) + } catch (err) { + return { pass: false, message: () => failMessage(err.message) } + } + return { pass: true, message: () => passMessage } +} + +exports.toBeLineStringGeometry = toBeLineStringGeometry diff --git a/tests/core.test.js b/tests/core.test.js index 3a8f510..388953d 100644 --- a/tests/core.test.js +++ b/tests/core.test.js @@ -31,11 +31,15 @@ describe('Coordinate Functions Exported', () => { }) describe('Geometry Functions Exported', () => { - test('valid2DBoundingBox', () => { + test('lineStringGeometry', () => { + expect('lineStringGeometry' in core.geometries).toBeTruthy() + }) + + test('multiPointGeometry', () => { expect('multiPointGeometry' in core.geometries).toBeTruthy() }) - test('valid3DBoundingBox', () => { + test('pointGeometry', () => { expect('pointGeometry' in core.geometries).toBeTruthy() }) }) diff --git a/tests/geometries/__snapshots__/toBeLineStringGeometry.test.js.snap b/tests/geometries/__snapshots__/toBeLineStringGeometry.test.js.snap new file mode 100644 index 0000000..703d0e4 --- /dev/null +++ b/tests/geometries/__snapshots__/toBeLineStringGeometry.test.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Error Snapshot Testing. Throws error: expect({type: 'LineString', coordinates: [[0, 0]]}).not.toBeLineStringGeometry 1`] = ` +"expect(GeometryObject).not.toBeLineStringGeometry() + +Expected input to not be a valid GeoJSON LineString geometry. + +Received: {\\"coordinates\\": [[0, 0]], \\"type\\": \\"LineString\\"}" +`; + +exports[`Error Snapshot Testing. Throws error: expect(false).toBeLineStringGeometry() 1`] = ` +"expect(GeometryObject).toBeLineStringGeometry() + +Argument must be a GeoJSON LineString Geometry object. + +Received: false" +`; diff --git a/tests/geometries/toBeLineStringGeometry.test.js b/tests/geometries/toBeLineStringGeometry.test.js new file mode 100644 index 0000000..9937389 --- /dev/null +++ b/tests/geometries/toBeLineStringGeometry.test.js @@ -0,0 +1,345 @@ +const invalidInputValues = [ + undefined, + null, + true, + false, + 200, + -200, + Infinity, + -Infinity, + NaN, + [ + { + coordinates: [ + [0, 0], + [1, 1] + ] + } + ], + '', + 'Random Geometry', + '[0, 0]', + '[[0, 0], [0, 0]]', + JSON.stringify({ + type: 'Point', + coordinates: [ + [25, 90], + [2, 2] + ] + }) +] +const coordinatesInRange = [ + [ + [ + [0, 1], + [0, 2] + ] + ], + [ + [ + [1, 0], + [2, 0], + [3, 0] + ] + ], + [ + [ + [2, 20, 0], + [4, 10, 0] + ] + ], + [ + [ + [3, 0.0, 0], + [6, -10, 0], + [9, -20, 0] + ] + ], + [ + [ + [100.0, 0.0], + [90, 0.0, 0] + ] + ], + [ + [ + [100.0, 0.0, 0], + [110, 5], + [100.0, 11.33, 259] + ] + ], + [ + [ + [180.0, 40.0], + [180.0, 50.0], + [170.0, 50.0], + [170.0, 40.0], + [180.0, 40.0] + ] + ], + [ + [ + [175, 0], + [-175, 0] + ] + ], + [ + [ + [-175, 0], + [175, 0] + ] + ], + [ + [ + [0, 0], + [0, 0], + [0, 0] + ] + ] +] +const coordinatesOutOfRange = [ + [ + [ + [0, 0], + [181, 91] + ] + ], + [ + [ + [0, 0], + [181, -91] + ] + ], + [ + [ + [0, 0], + [-181, 91, 0] + ] + ], + [ + [ + [0, 0], + [-181, -91, 200] + ] + ], + [[[0, 0, 0, 0]]] +] +const emptyArrays = [[[[]]], [[[], []]], [[[], [], []]]] +const incorrectTypeValues = [ + ...invalidInputValues, + 'Point', + 'MultiPoint', + 'MultiLineString', + 'Polygon', + 'MultiPolygon', + 'GeometryCollection', + 'LINESTRING', + 'linestring' +] + +describe('Valid Use Cases', () => { + describe('Basic Formatting, Values in Range:', () => { + test.each([...coordinatesInRange])('Good in range coordinates: %p', (coordinateArray) => { + const testMultiPoint = { + type: 'LineString', + coordinates: coordinateArray + } + expect(testMultiPoint).toBeLineStringGeometry() + }) + + test('Empty coordinate', () => { + const testMultiPoint = { + type: 'LineString', + coordinates: [] + } + expect(testMultiPoint).toBeLineStringGeometry() + }) + }) + + describe('Foreign Properties Allowed:', () => { + const testLineString1 = { + type: 'LineString', + id: null, + coordinates: [ + [25, 90], + [-180, 0] + ] + } + const testLineString2 = { + type: 'LineString', + geometries: testLineString1, + coordinates: [ + [-100.0, -15.0, 2000], + [0, 0] + ] + } + const testLineString3 = { + type: 'LineString', + someRandomProp: true, + geometries: testLineString2, + coordinates: [[180, 10.2, -125]] + } + + test.each(['Test 1', 1])('ID: %p', (input) => { + const testLineString = { + type: 'LineString', + id: input, + coordinates: [ + [25, 90], + [-180, 0] + ] + } + expect(testLineString).toBeLineStringGeometry() + }) + + test.each([testLineString1, testLineString2, testLineString3])( + 'Non-alphanumeric ID', + (testLineString) => { + expect(testLineString).toBeLineStringGeometry() + } + ) + }) +}) + +describe('Inalid Use Cases', () => { + describe('Expect to fail with bad inputs:', () => { + test.each([...invalidInputValues])( + 'expect(%p).not.toBeLineStringGeometry()', + (badInput) => { + expect(badInput).not.toBeLineStringGeometry() + } + ) + }) + + describe('Expect to fail with out of range or bad coordinate:', () => { + test.each([...coordinatesOutOfRange, ...invalidInputValues])( + 'coordinates: %p', + (coordinate) => { + const testLineString = { + type: 'LineString', + coordinates: coordinate + } + expect(testLineString).not.toBeLineStringGeometry() + } + ) + }) + + describe('Expect to fail with one bad coordinate:', () => { + // Add the extra empty array here. "coordinates" can be an empty array, but individual elements within cannot. + test.each([...invalidInputValues, []])('coordinates: [[0, 0], %p]', (coordinate) => { + const testLineString = { + type: 'LineString', + coordinates: [[0, 0], coordinate] + } + expect(testLineString).not.toBeLineStringGeometry() + }) + }) + + describe('Expect to fail with coordinates array of empty arrays:', () => { + test.each([...emptyArrays])('coordinates: %p', (coordinate) => { + const testLineString = { + type: 'LineString', + coordinates: coordinate + } + expect(testLineString).not.toBeLineStringGeometry() + }) + }) + + describe('Expect to fail with bad type value:', () => { + test.each([...incorrectTypeValues, ...invalidInputValues])('type: %p', (input) => { + const testLineString = { + type: input, + coordinates: [ + [0, 0], + [1, 1, 0] + ] + } + expect(testLineString).not.toBeLineStringGeometry() + }) + }) + + describe('Expect to fail when contains prohibited properties:', () => { + test(`Contains: 'geometry'`, () => { + const testLineString = { + type: 'LineString', + coordinates: [ + [0, 0], + [1, 1, 0] + ], + geometry: { + type: 'Point', + coordinates: [102.0, 0.5] + } + } + + expect(testLineString).not.toBeLineStringGeometry() + }) + + test(`Contains: 'properties'`, () => { + const testLineString = { + type: 'LineString', + coordinates: [ + [0, 0], + [1, 1, 0] + ], + properties: { + prop1: true + } + } + + expect(testLineString).not.toBeLineStringGeometry() + }) + + test(`Contains: 'features'`, () => { + const testLineString = { + type: 'LineString', + coordinates: [ + [0, 0], + [1, 1, 0] + ], + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [102.0, 0.5] + }, + properties: { + prop0: 'value0' + } + } + ] + } + expect(testLineString).not.toBeLineStringGeometry() + }) + }) + + describe('Expect to fail when missing required properties:', () => { + test(`Missing: 'type'`, () => { + const testLineString = { + coordinates: [ + [0, 0], + [1, 1, 0] + ] + } + expect(testLineString).not.toBeLineStringGeometry() + }) + test(`Missing: 'coordinates'`, () => { + const testLineString = { + type: 'LineString' + } + expect(testLineString).not.toBeLineStringGeometry() + }) + }) +}) + +describe('Error Snapshot Testing. Throws error:', () => { + test(`expect({type: 'LineString', coordinates: [[0, 0]]}).not.toBeLineStringGeometry`, () => { + expect(() => + expect({ type: 'LineString', coordinates: [[0, 0]] }).not.toBeLineStringGeometry() + ).toThrowErrorMatchingSnapshot() + }) + test('expect(false).toBeLineStringGeometry()', () => { + expect(() => expect(false).toBeLineStringGeometry()).toThrowErrorMatchingSnapshot() + }) +}) diff --git a/tests/matchers.test.js b/tests/matchers.test.js index c894fd0..d206294 100644 --- a/tests/matchers.test.js +++ b/tests/matchers.test.js @@ -31,6 +31,10 @@ describe('Coordinate Matchers Exported', () => { }) describe('Geometry Matchers Exported', () => { + test('toBeLineStringGeometry', () => { + expect('toBeLineStringGeometry' in matchers.geometries).toBeTruthy() + }) + test('toBeMultiPointGeometry', () => { expect('toBeMultiPointGeometry' in matchers.geometries).toBeTruthy() })