Skip to content

Commit

Permalink
feat(toBeFeatureCollection): add new matcher
Browse files Browse the repository at this point in the history
Verifies an object is a valid GeoJSON FeatureCollection. Also updates namespaces for new
core/matcher type as well as adds appropriate setup scripts and tests.

Resolves: #25
  • Loading branch information
M-Scott-Lassiter committed Jun 2, 2022
1 parent bcfb91e commit 21fe044
Show file tree
Hide file tree
Showing 14 changed files with 990 additions and 15 deletions.
1 change: 1 addition & 0 deletions .cz-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const coordinateMatchers = [
{ name: 'isValidCoordinate' },
{ name: 'toBeAnyGeometry' },
{ name: 'toBeFeature' },
{ name: 'toBeFeatureCollection' },
{ name: 'toBeGeometryCollection' },
{ name: 'toBeLineStringGeometry' },
{ name: 'toBeMultiLineStringGeometry' },
Expand Down
17 changes: 2 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,24 +125,18 @@ Functional matchers assess more generic attributes and qualities and accept mult

## Coordiantes

_1.0.0_

- [x] isValid2DCoordinate
- [x] isValid3DCoordinate
- [x] isValidCoordinate

## Bounding Boxes

_1.0.0_

- [x] isValid2DBoundingBox
- [x] isValid3DBoundingBox
- [x] isValidBoundingBox

## Geometries

_1.0.0_

- [x] toBePointGeometry
- [x] toBeMultiPointGeometry
- [x] toBeLineStringGeometry
Expand All @@ -164,8 +158,6 @@ _Future_

## Features

_1.0.0_

- [x] toBeFeature

---
Expand All @@ -179,9 +171,7 @@ _Future_

## Feature Collections

_1.0.0_

- [ ] toBeFeatureCollection
- [x] toBeFeatureCollection

---

Expand All @@ -197,14 +187,11 @@ _Future_

## Functional

_1.0.0_

- [ ] toBeValidGeoJSON

---

_Future_

- [ ] toBeValidGeoJSON
- [ ] toHave2DBoundingBox
- [ ] toHave3DBoundingBox
- [ ] toHaveBoundingBox
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"./setup/all": "./src/setup/all.js",
"./setup/boundingboxes": "./src/setup/boundingBoxes.js",
"./setup/coordinates": "./src/setup/coordinates.js",
"./setup/featureCollections": "./src/setup/featureCollections.js",
"./setup/features": "./src/setup/features.js",
"./setup/geometries": "./src/setup/geometries.js"
},
"repository": {
Expand All @@ -45,6 +47,7 @@
"test": "jest --coverage --verbose",
"test:coordinates": "jest tests/coordinates --coverage --verbose",
"test:boundingboxes": "jest tests/boundingBoxes --coverage --verbose",
"test:featurecollections": "jest tests/featureCollections --coverage --verbose",
"test:features": "jest tests/features --coverage --verbose",
"test:geometries": "jest tests/geometries --coverage --verbose",
"lint": "eslint . --ext .js --fix",
Expand Down
4 changes: 4 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ exports.coordinates = {
validCoordinate: require('./core/coordinates/validCoordinate')
}

exports.featureCollections = {
featureCollection: require('./core/featureCollections/featureCollection')
}

exports.features = {
feature: require('./core/features/feature')
}
Expand Down
120 changes: 120 additions & 0 deletions src/core/featureCollections/featureCollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const { validBoundingBox } = require('../boundingBoxes/validBoundingBox')
const { feature } = require('../features/feature')

/**
* Verifies an object is a valid GeoJSON FeatureCollection. This object requires a "type" member that must
* equal 'FeatureCollection', and a "features" member that contains either a valid GeoJSON Feature
* or an empty array.
*
* Foreign members are allowed with the exceptions thrown below.
* If present, bounding boxes must be valid.
*
* @memberof Core.FeatureCollections
* @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/25
* @param {object} featureCollectionObject a GeoJSON LineString Geometry object
* @returns {boolean} True if a valid GeoJSON FeatureCollection. If invalid, it will throw an error.
* @throws {Error} Argument not an object
* @throws {Error} Must have a type property with value 'FeatureCollection'
* @throws {Error} Forbidden from having a property 'coordinates', 'geometries', 'geometry', or 'properties'
* @throws {Error} Bounding box must be valid (if present)
* @example
* const testFeatureCollection = {
* "type": "FeatureCollection",
* "features": [{
* "type": "Feature",
* "geometry": {
* "type": "Point",
* "coordinates": [102.0, 0.5]
* }
* },
* ...
* ]
* }
* const multiPoint = {
* type: "MultiPoint",
* coordinates: [
* [101.0, 0.0],
* [102.0, 1.0]
* ]
* }
*
* const goodExample1 = featureCollection(testFeatureCollection)) // true
*
* const badExample1 = featureCollection(multiPoint)) // throws error
* const badExample2 = featureCollection(testFeatureCollection.features)) // throws error
*/
function featureCollection(featureCollectionObject) {
if (typeof featureCollectionObject !== 'object') {
throw new Error(`Argument must be a FeatureCollection object.`)
}

if (featureCollectionObject.type !== 'FeatureCollection') {
throw new Error(`Must have a type property with value 'FeatureCollection'.`)
}

if ('coordinates' in featureCollectionObject) {
throw new Error(
`GeoJSON FeatureCollection objects are forbidden from having a property 'coordinates'.`
)
}

if ('geometries' in featureCollectionObject) {
throw new Error(
`GeoJSON FeatureCollection objects are forbidden from having a property 'geometries'.`
)
}

if ('geometry' in featureCollectionObject) {
throw new Error(
`GeoJSON FeatureCollection objects are forbidden from having a property 'geometry'.`
)
}

if ('properties' in featureCollectionObject) {
throw new Error(
`GeoJSON FeatureCollection objects are forbidden from having a property 'properties'.`
)
}

if (!('features' in featureCollectionObject)) {
throw new Error(`GeoJSON FeatureCollection objects must have a property 'features'.`)
}

if (!Array.isArray(featureCollectionObject.features)) {
throw new Error(
`GeoJSON FeatureCollection features must be either an array of valid Feature objects or an empty array.`
)
}

// if (typeof featureCollectionObject.geometry !== 'object' || Array.isArray(featureCollectionObject.geometry)) {
// throw new Error(`GeoJSON Feature 'geometry' must be a valid GeoJSON geometry object.`)
// }

if ('bbox' in featureCollectionObject) {
validBoundingBox(featureCollectionObject.bbox)
}

// if ('id' in featureCollectionObject) {
// if (
// !(typeof featureCollectionObject.id === 'number' || typeof featureCollectionObject.id === 'string') ||
// Number.isNaN(featureCollectionObject.id)
// ) {
// throw new Error(`If present, ID must be either a number or string.`)
// }
// }

// // Guard clause; features are allowed to have null geometry. However, if the matcher explicitly calls
// // for a particular geometry type, null isn't an option. We have to check for that.
// if (featureCollectionObject.geometry === null && geometryType === undefined) {
// return true
// }

// // At this point, we have guaranteed there is a features array here. Validate each element with the core functions.
featureCollectionObject.features.forEach((featureObject) => {
feature(featureObject)
})

return true
}

exports.featureCollection = featureCollection
5 changes: 5 additions & 0 deletions src/matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ exports.coordinates = {
isValidCoordinate: require('./matchers/coordinates/isValidCoordinate').isValidCoordinate
}

exports.featureCollections = {
toBeFeatureCollection: require('./matchers/featureCollections/toBeFeatureCollection')
.toBeFeatureCollection
}

exports.features = {
toBeFeature: require('./matchers/features/toBeFeature').toBeFeature
}
Expand Down
79 changes: 79 additions & 0 deletions src/matchers/featureCollections/toBeFeatureCollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const { featureCollection } = require('../../core/featureCollections/featureCollection')

// eslint-disable-next-line jsdoc/require-returns
/**
* Verifies an object is a valid GeoJSON FeatureCollection. This object requires a "type" member that must
* equal 'FeatureCollection', and a "features" member that contains either a valid GeoJSON Feature
* or an empty array.
*
* Foreign members are allowed with the exception of 'coordinates', 'geometries', 'geometry', or 'properties'.
* If present, bounding boxes must be valid.
*
* @memberof Matchers.FeatureCollections
* @see https://github.com/M-Scott-Lassiter/jest-geojson/issues/25
* @param {object} featureCollectionObject any GeoJSON Feature object
* @example
* const testFeatureCollection = {
* "type": "FeatureCollection",
* "features": [{
* "type": "Feature",
* "geometry": {
* "type": "Point",
* "coordinates": [102.0, 0.5]
* }
* },
* ...
* ]
* }
* test('Object is valid GeoJSON Feature', () => {
* expect(testFeatureCollection).toBeFeatureCollection()
* })
* @example
* const multiPoint = {
* type: "MultiPoint",
* coordinates: [
* [101.0, 0.0],
* [102.0, 1.0]
* ]
* }
*
* test('Object is NOT valid GeoJSON Geometry Object', () => {
* expect(multiPoint).not.toBeFeatureCollection()
* expect(testFeatureCollection.features).not.toBeFeatureCollection()
* })
*/
function toBeFeatureCollection(featureCollectionObject) {
const { printReceived, matcherHint } = this.utils
const passMessage =
// eslint-disable-next-line prefer-template
matcherHint('.not.toBeFeatureCollection', 'FeatureCollectionObject', '') +
'\n\n' +
`Expected input to not be a valid GeoJSON FeatureCollection object.\n\n` +
`Received: ${printReceived(featureCollectionObject)}`

/**
* 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('.toBeFeatureCollection', 'FeatureObject', 'GeometryType') +
'\n\n' +
`${errorMessage}\n\n` +
`Received: ${printReceived(featureCollectionObject)}`
)
}

try {
featureCollection(featureCollectionObject)
} catch (err) {
return { pass: false, message: () => failMessage(err.message) }
}
return { pass: true, message: () => passMessage }
}

exports.toBeFeatureCollection = toBeFeatureCollection
1 change: 1 addition & 0 deletions src/setup/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const jestExpect = global.expect
if (jestExpect !== undefined) {
expect.extend(matchers.coordinates)
expect.extend(matchers.boundingBoxes)
expect.extend(matchers.featureCollections)
expect.extend(matchers.features)
expect.extend(matchers.geometries)
} else {
Expand Down
10 changes: 10 additions & 0 deletions src/setup/featureCollections.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const matchers = require('../matchers')
const { throwJestRuntimeError } = require('./all')

const jestExpect = global.expect

if (jestExpect !== undefined) {
expect.extend(matchers.featureCollections)
} else {
throwJestRuntimeError()
}
14 changes: 14 additions & 0 deletions src/typedefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
* @namespace Matchers.Coordinates
*/

/**
* A set of matchers related to validating FeatureCollection objects.
*
* @see https://datatracker.ietf.org/doc/html/rfc7946#section-3.3
* @namespace Matchers.FeatureCollections
*/

/**
* A set of matchers related to validating feature objects.
*
Expand Down Expand Up @@ -76,6 +83,13 @@
* @namespace Core.BoundingBoxes
*/

/**
* FeatureCollection object validation functions used within Core.
*
* @private
* @namespace Core.FeatureCollections
*/

/**
* Feature object validation functions used within Core.
*
Expand Down
6 changes: 6 additions & 0 deletions tests/core.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ describe('Coordinate Functions Exported', () => {
})
})

describe('FeatureCollection Functions Exported', () => {
test('featureCollection', () => {
expect('featureCollection' in core.featureCollections).toBeTruthy()
})
})

describe('Feature Functions Exported', () => {
test('feature', () => {
expect('feature' in core.features).toBeTruthy()
Expand Down
Loading

0 comments on commit 21fe044

Please sign in to comment.