Skip to content

Commit

Permalink
fix: minItems for relevant geometry types (#19)
Browse files Browse the repository at this point in the history
Among other uses, we use this module to generate random geometries (see
`@mapeo/mock-data`). Without `minItems` annotations, we would generate
invalid geometries. This fixes that.

This also fixes a related bug: I used `minLength`, not `minItems`, for
`LineString` and `MultiLineString`.
  • Loading branch information
EvanHahn authored Dec 18, 2024
1 parent 3353346 commit 37a2061
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 26 deletions.
19 changes: 12 additions & 7 deletions json/geometry.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"items": {
"$ref": "#/definitions/position"
},
"minLength": 2
"minItems": 2
}
},
"additionalProperties": false
Expand All @@ -85,8 +85,9 @@
"items": {
"$ref": "#/definitions/position"
},
"minLength": 2
}
"minItems": 2
},
"minItems": 1
}
},
"additionalProperties": false
Expand All @@ -104,7 +105,8 @@
"type": "array",
"items": {
"$ref": "#/definitions/linearRing"
}
},
"minItems": 1
}
},
"additionalProperties": false
Expand All @@ -122,7 +124,8 @@
"type": "array",
"items": {
"$ref": "#/definitions/position"
}
},
"minItems": 1
}
},
"additionalProperties": false
Expand All @@ -142,8 +145,10 @@
"type": "array",
"items": {
"$ref": "#/definitions/linearRing"
}
}
},
"minItems": 1
},
"minItems": 1
}
},
"additionalProperties": false
Expand Down
79 changes: 63 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,18 @@ function encodePoint({ coordinates }: Point): GeometryProto {
function decodeLineString({
coordinates: rawCoords,
}: GeometryProto): LineString {
const coordinates = readPositionArray(rawCoords, {
start: 0,
length: rawCoords.length / 2,
})
assertLengthIsAtLeast(
coordinates,
2,
'LineString must have at least 2 positions'
)
return {
type: 'LineString',
coordinates: readPositionArray(rawCoords, {
start: 0,
length: rawCoords.length / 2,
}),
coordinates,
}
}

Expand All @@ -104,21 +110,39 @@ function decodeMultiLineString({
coordinates: rawCoords,
lengths,
}: GeometryProto): MultiLineString {
let coordinates: Position[][]
let coordinates

if (lengths.length === 0) {
coordinates = [
readPositionArray(rawCoords, { start: 0, length: rawCoords.length / 2 }),
]
const line = readPositionArray(rawCoords, {
start: 0,
length: rawCoords.length / 2,
})
assertLengthIsAtLeast(
line,
2,
'MultiLineString line must have at least 2 positions'
)
coordinates = [line]
} else {
let start = 0
coordinates = lengths.map((length) => {
const line = readPositionArray(rawCoords, { start, length })
assertLengthIsAtLeast(
line,
2,
'MultiLineString line must have at least 2 positions'
)
start += length * 2
return line
})
}

assertLengthIsAtLeast(
coordinates,
1,
'MultiLineString must have at least 1 line'
)

return {
type: 'MultiLineString',
coordinates,
Expand All @@ -140,12 +164,14 @@ function encodeMultiLineString({
function decodeMultiPoint({
coordinates: rawCoords,
}: GeometryProto): MultiPoint {
const coordinates = readPositionArray(rawCoords, {
start: 0,
length: rawCoords.length / 2,
})
assertLengthIsAtLeast(coordinates, 1, 'MultiPoint must have at least 1 point')
return {
type: 'MultiPoint',
coordinates: readPositionArray(rawCoords, {
start: 0,
length: rawCoords.length / 2,
}),
coordinates,
}
}

Expand Down Expand Up @@ -174,6 +200,7 @@ function decodePolygon({
rings[i] = ring
start += length * 2
}
assertLengthIsAtLeast(rings, 1, 'Polygon must have at least 1 ring')
return {
type: 'Polygon',
coordinates: rings,
Expand All @@ -195,7 +222,7 @@ function decodeMultiPolygon({
lengths,
}: GeometryProto): MultiPolygon {
lengths = lengths.length === 0 ? [1, 1, rawCoords.length / 2] : lengths
const polygons = new Array<LinearRing[]>(lengths[0])
const polygons = new Array<BuildArrayMinLength<LinearRing, 1, []>>(lengths[0])
let start = 0
for (let i = 0, j = 1; i < lengths[0]; i++) {
const ringsLength = lengths[j + i]
Expand All @@ -211,8 +238,14 @@ function decodeMultiPolygon({
start += length * 2
}
j += ringsLength
assertLengthIsAtLeast(rings, 1, 'Polygon must have at least 1 ring')
polygons[i] = rings
}
assertLengthIsAtLeast(
polygons,
1,
'MultiPolygon must have at least 1 polygon'
)
return {
type: 'MultiPolygon',
coordinates: polygons,
Expand Down Expand Up @@ -260,9 +293,7 @@ const validateLatitude = rangeValidator(90)
const validateLongitude = rangeValidator(180)

function validateLinearRing(ring: Position[]): asserts ring is LinearRing {
if (ring.length < 4) {
throw new Error('Invalid number of coordinates in linear ring')
}
assertLengthIsAtLeast(ring, 4, 'Invalid number of coordinates in linear ring')
}

function readPositionArray(
Expand All @@ -287,6 +318,22 @@ function readPositionArray(
return positions
}

type BuildArrayMinLength<
T,
N extends number,
Current extends T[],
> = Current['length'] extends N
? [...Current, ...T[]]
: BuildArrayMinLength<T, N, [...Current, T]>

function assertLengthIsAtLeast<T, N extends number>(
arr: T[],
length: N,
message: string
): asserts arr is BuildArrayMinLength<T, N, []> {
if (arr.length < length) throw new Error(message)
}

class ExhaustivenessError extends Error {
constructor(
value: never,
Expand Down
5 changes: 5 additions & 0 deletions test/fixture/bad-proto/linestring-no-coordinates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "TYPE_LINE_STRING",
"coordinates": [],
"lengths": []
}
5 changes: 5 additions & 0 deletions test/fixture/bad-proto/multilinestring-no-coordinates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "TYPE_MULTI_LINE_STRING",
"coordinates": [],
"lengths": []
}
5 changes: 5 additions & 0 deletions test/fixture/bad-proto/multipoint-no-coordinates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "TYPE_MULTI_POINT",
"coordinates": [],
"lengths": []
}
5 changes: 5 additions & 0 deletions test/fixture/bad-proto/multipolygon-no-coordinates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "TYPE_MULTI_POLYGON",
"coordinates": [],
"lengths": []
}
File renamed without changes.
5 changes: 5 additions & 0 deletions test/fixture/bad-proto/polygon-no-coordinates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "TYPE_POLYGON",
"coordinates": [],
"lengths": []
}
2 changes: 1 addition & 1 deletion test/fixture/bad-proto/too-big-length-multilinestring.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"type": "MultiLineString",
"type": "TYPE_MULTI_LINE_STRING",
"lengths": [2, 1],
"coordinates": [12, 34]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "TYPE_MULTI_LINE_STRING",
"lengths": [],
"coordinates": [1, 2]
}
2 changes: 1 addition & 1 deletion test/fixture/bad-proto/too-short-line-multilinestring.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"type": "MultiLineString",
"type": "TYPE_MULTI_LINE_STRING",
"lengths": [2, 1],
"coordinates": [170.0, 45.0, 180.0, 45.0, -180.0, 45.0]
}
2 changes: 1 addition & 1 deletion test/fixture/bad-proto/too-short-linestring.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"type": "LineString",
"type": "TYPE_LINE_STRING",
"lengths": [],
"coordinates": [-170, 10]
}

0 comments on commit 37a2061

Please sign in to comment.