Skip to content

Commit

Permalink
test: improve tilejson
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelKreil committed Feb 25, 2024
1 parent 312b815 commit 7e774f0
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 66 deletions.
144 changes: 101 additions & 43 deletions src/types/tilejson.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,110 @@
import { isTileJSONSpecification } from './tilejson';

describe('isTileJSONSpecification', () => {
it('should return true for a valid TileJSONSpecificationRaster object', () => {
const rasterSpec = {
type: 'raster',
format: 'png',
tiles: ['http://example.com/{z}/{x}/{y}.png'],
};
expect(isTileJSONSpecification(rasterSpec)).toBeTruthy();
});

it('should throw an error for an invalid TileJSONSpecification object', () => {
const invalidSpec = {
type: 'vector', // Missing required 'vector_layers' property for 'vector' type
format: 'pbf',
tiles: ['http://example.com/{z}/{x}/{y}.pbf'],
};
expect(() => isTileJSONSpecification(invalidSpec)).toThrow();
});

// Test for valid TileJSONSpecificationVector object
it('should return true for a valid TileJSONSpecificationVector object', () => {
const vectorSpec = {
type: 'vector',
format: 'pbf',
tiles: ['http://example.com/{z}/{x}/{y}.pbf'],
vector_layers: [{ id: 'layer1', fields: { property1: 'Number' }, description: 'A test layer' }],
};
expect(isTileJSONSpecification(vectorSpec)).toBeTruthy();
});

// Test for missing 'tiles' property
const validVectorSpec = {
tilejson: '3.0.0',
type: 'vector',
format: 'pbf',
tiles: ['http://example.com/{z}/{x}/{y}.pbf'],
vector_layers: [{ id: 'layer1', fields: { property1: 'Number' }, description: 'A test layer' }],
};
const validRasterSpec = {
tilejson: '3.0.0',
type: 'raster',
format: 'png',
tiles: ['http://example.com/{z}/{x}/{y}.png'],
};

it('should return true for a valid raster source', () => {
expect(isTileJSONSpecification({ ...validRasterSpec })).toBeTruthy();
});

it('should return true for a valid vector source', () => {
expect(isTileJSONSpecification({ ...validVectorSpec })).toBeTruthy();
});

it('should throw an error if not object', () => {
expect(() => isTileJSONSpecification(null)).toThrow('spec must be an object');
expect(() => isTileJSONSpecification(1)).toThrow('spec must be an object');
});

it('should throw an error if type is unknown', () => {
expect(() => isTileJSONSpecification({ ...validRasterSpec, type: 'cake' })).toThrow('spec.type must be "raster" or "vector"');
});

it('should throw an error if raster source has incompatible format', () => {
expect(() => isTileJSONSpecification({ ...validRasterSpec, format: 'pbf' })).toThrow('spec.format must be "avif", "jpg", "png", or "webp" for raster sources');
});

it('should throw an error if vector source has incompatible format', () => {
expect(() => isTileJSONSpecification({ ...validVectorSpec, format: 'png' })).toThrow('spec.format must be "pbf" for vector sources');
});

it('should throw an error if the tiles property is missing', () => {
const missingTilesSpec = {
type: 'raster',
format: 'png',
};
expect(() => isTileJSONSpecification(missingTilesSpec)).toThrow('spec.tiles must be an array of strings');
expect(() => isTileJSONSpecification({ ...validRasterSpec, tiles: undefined })).toThrow('spec.tiles must be an array of strings');
});

it('should throw an error for missing vector_layers in vector source', () => {
expect(() => isTileJSONSpecification({ ...validVectorSpec, vector_layers: undefined })).toThrow('spec.vector_layers is invalid: Expected an array of layers');
});

// Test for invalid 'bounds' property
it('should throw an error if the bounds property is invalid', () => {
const invalidBoundsSpec = {
type: 'vector',
format: 'pbf',
tiles: ['http://example.com/{z}/{x}/{y}.pbf'],
bounds: [180, -90, 190, 90], // Invalid longitude
};
expect(() => isTileJSONSpecification(invalidBoundsSpec)).toThrow();
[
{ bounds: [-181, -90, 180, 90], errorMessage: 'spec.bounds[0] must be between -180 and 180' },
{ bounds: [-180, -91, 180, 90], errorMessage: 'spec.bounds[1] must be between -90 and 90' },
{ bounds: [-180, -90, 181, 90], errorMessage: 'spec.bounds[2] must be between -180 and 180' },
{ bounds: [-180, -90, 180, 91], errorMessage: 'spec.bounds[3] must be between -90 and 90' },
{ bounds: [180, -90, -180, 90], errorMessage: 'spec.bounds[0] must be smaller than spec.bounds[2]' },
{ bounds: [-180, 90, 180, -90], errorMessage: 'spec.bounds[1] must be smaller than spec.bounds[3]' },
].forEach(({ bounds, errorMessage }) => {
expect(() => isTileJSONSpecification({ ...validVectorSpec, bounds })).toThrow(errorMessage);
});
});

it('should throw an error if the center property is invalid', () => {
[
{ center: [-181, 0], errorMessage: 'spec.center[0] must be between -180 and 180' },
{ center: [181, 0], errorMessage: 'spec.center[0] must be between -180 and 180' },
{ center: [0, -91], errorMessage: 'spec.center[1] must be between -90 and 90' },
{ center: [0, 91], errorMessage: 'spec.center[1] must be between -90 and 90' },
].forEach(({ center, errorMessage }) => {
expect(() => isTileJSONSpecification({ ...validVectorSpec, center })).toThrow(errorMessage);
});
});

describe('check every property', () => {
[
['tilejson', '"3.0.0"', '3.0.0', '2.0.0'],
['tiles', 'an array of strings', ['url'], 'url', [], [1], 1],
['attribution', 'a string if present', 'valid', 1],
['bounds', 'an array of four numbers if present', [1, 2, 3, 4], ['1', '2', '3', '4'], [1, 2, 3], [], 'invalid'],
['center', 'an array of two numbers if present', [1, 2], ['1', '2'], [1, 2, 3], [], 'invalid'],
['data', 'an array of strings if present', ['url'], 'url', [1], 1],
['description', 'a string if present', 'valid', 1],
['fillzoom', 'a positive integer if present', 5, 'invalid', -1],
['format', 'a string', 'pbf', 1],
['grids', 'an array of strings if present', ['1', '2', '3', '4'], [1, 2, 3, 4], 'invalid'],
['legend', 'a string if present', 'valid', 1],
['maxzoom', 'a positive integer if present', 5, 'invalid', -1],
['minzoom', 'a positive integer if present', 5, 'invalid', -1],
['name', 'a string if present', 'valid', 1],
['scheme', '"tms" or "xyz" if present', 'xyz', 'invalid', 1],
['template', 'a string if present', 'valid', 1],
].forEach(test => {
const key = test[0] as string;
const errorMessage = test[1] as string;
const values = test.slice(2) as unknown[];
it(key, () => {
for (let i = 0; i < values.length; i++) {
const value = values[i];
if (i === 0) {
expect(isTileJSONSpecification({ ...validVectorSpec, [key]: value })).toBe(true);
} else {
expect(() => isTileJSONSpecification({ ...validVectorSpec, [key]: value }))
.toThrow(`spec.${key} must be ${errorMessage}`);
}
}
});
});
});
});
77 changes: 55 additions & 22 deletions src/types/tilejson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import { isVectorLayers } from './vector_layer';

/** Basic structure for TileJSON specification, applicable to both raster and vector types. */
export interface TileJSONSpecificationBasic {
tilejson?: '3.0.0';
attribution?: string;
tilejson: '3.0.0';
tiles: string[];
scheme?: 'tms' | 'xyz';
attribution?: string;
bounds?: [number, number, number, number];
center?: [number, number];
data?: string[];
description?: string;
fillzoom?: number;
grids?: string[];
legend?: string;
minzoom?: number;
maxzoom?: number;
minzoom?: number;
name?: string;
scheme?: 'tms' | 'xyz';
template?: string;
}

Expand Down Expand Up @@ -48,42 +49,72 @@ export function isTileJSONSpecification(spec: unknown): spec is TileJSONSpecific
const obj = spec as Record<string, unknown>;

// Common property validation
if (obj.tilejson !== undefined && obj.tilejson !== '3.0.0') {
throw Error('spec.tilejson must be "3.0.0" if present');
if (obj.tilejson !== '3.0.0') {
throw Error('spec.tilejson must be "3.0.0"');
}

if (obj.attribution !== undefined && typeof obj.attribution !== 'string') {
throw Error('spec.attribution must be a string if present');
}
if (obj.scheme !== undefined && obj.scheme !== 'xyz' && obj.scheme !== 'tms') {
throw Error('spec.scheme must be "tms" or "xyz" if present');

if (obj.bounds !== undefined) {
if (!Array.isArray(obj.bounds) || obj.bounds.length !== 4 || obj.bounds.some(num => typeof num !== 'number')) {
throw Error('spec.bounds must be an array of four numbers if present');
}
const a = obj.bounds as [number, number, number, number];
if (a[0] < -180 || a[0] > 180) throw Error('spec.bounds[0] must be between -180 and 180');
if (a[1] < -90 || a[1] > 90) throw Error('spec.bounds[1] must be between -90 and 90');
if (a[2] < -180 || a[2] > 180) throw Error('spec.bounds[2] must be between -180 and 180');
if (a[3] < -90 || a[3] > 90) throw Error('spec.bounds[3] must be between -90 and 90');
if (a[0] > a[2]) throw Error('spec.bounds[0] must be smaller than spec.bounds[2]');
if (a[1] > a[3]) throw Error('spec.bounds[1] must be smaller than spec.bounds[3]');
}
if (obj.bounds !== undefined && (!Array.isArray(obj.bounds) || obj.bounds.length !== 4 || obj.bounds.some(num => typeof num !== 'number'))) {
throw Error('spec.bounds must be an array of four numbers if present');

if (obj.center !== undefined) {
if (!Array.isArray(obj.center) || obj.center.length !== 2 || obj.center.some(num => typeof num !== 'number')) {
throw Error('spec.center must be an array of two numbers if present');
}
const a = obj.center as [number, number];
if (a[0] < -180 || a[0] > 180) throw Error('spec.center[0] must be between -180 and 180');
if (a[1] < -90 || a[1] > 90) throw Error('spec.center[1] must be between -90 and 90');
}
if (obj.center !== undefined && (!Array.isArray(obj.center) || obj.center.length !== 2 || obj.center.some(num => typeof num !== 'number'))) {
throw Error('spec.center must be an array of two numbers if present');

if (obj.data !== undefined && (!Array.isArray(obj.data) || obj.data.some(url => typeof url !== 'string'))) {
throw Error('spec.data must be an array of strings if present');
}

if (obj.description !== undefined && typeof obj.description !== 'string') {
throw Error('spec.description must be a string if present');
}
if (obj.fillzoom !== undefined && typeof obj.fillzoom !== 'number') {
throw Error('spec.fillzoom must be a number if present');

if (obj.fillzoom !== undefined && (typeof obj.fillzoom !== 'number' || (obj.fillzoom < 0))) {
throw Error('spec.fillzoom must be a positive integer if present');
}

if (obj.grids !== undefined && (!Array.isArray(obj.grids) || obj.grids.some(url => typeof url !== 'string'))) {
throw Error('spec.grids must be an array of strings if present');
}

if (obj.legend !== undefined && typeof obj.legend !== 'string') {
throw Error('spec.legend must be a string if present');
}
if (obj.minzoom !== undefined && typeof obj.minzoom !== 'number') {
throw Error('spec.minzoom must be a number if present');

if (obj.minzoom !== undefined && (typeof obj.minzoom !== 'number' || (obj.minzoom < 0))) {
throw Error('spec.minzoom must be a positive integer if present');
}
if (obj.maxzoom !== undefined && typeof obj.maxzoom !== 'number') {
throw Error('spec.maxzoom must be a number if present');

if (obj.maxzoom !== undefined && (typeof obj.maxzoom !== 'number' || (obj.maxzoom < 0))) {
throw Error('spec.maxzoom must be a positive integer if present');
}

if (obj.name !== undefined && typeof obj.name !== 'string') {
throw Error('spec.name must be a string if present');
}

if (obj.scheme !== undefined && obj.scheme !== 'xyz' && obj.scheme !== 'tms') {
throw Error('spec.scheme must be "tms" or "xyz" if present');
}

if (obj.template !== undefined && typeof obj.template !== 'string') {
throw Error('spec.template must be a string if present');
}
Expand All @@ -98,14 +129,16 @@ export function isTileJSONSpecification(spec: unknown): spec is TileJSONSpecific

if (obj.type === 'raster') {
if (!['avif', 'jpg', 'png', 'webp'].includes(obj.format)) {
throw Error('spec.format must be "avif", "jpg", "png", or "webp"');
throw Error('spec.format must be "avif", "jpg", "png", or "webp" for raster sources');
}
} else if (obj.type === 'vector') {
if (obj.format !== 'pbf') {
throw Error('spec.format must be "pbf"');
throw Error('spec.format must be "pbf" for vector sources');
}
if (!isVectorLayers(obj.vector_layers)) {
throw Error('spec.vector_layers must be an array of VectorLayer');
try {
if (!isVectorLayers(obj.vector_layers)) throw Error('spec.vector_layers is invalid');
} catch (error) {
throw Error('spec.vector_layers is invalid: ' + String((error instanceof Error) ? error.message : error));
}
} else {
throw Error('spec.type must be "raster" or "vector"');
Expand Down
2 changes: 1 addition & 1 deletion src/types/vector_layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function isVectorLayers(layers: unknown): layers is VectorLayer[] {
} catch (error) {
// Assuming `isVectorLayer` throws an error with a meaningful message, you can rethrow it
// Alternatively, customize the error message or handle the error as needed
throw new Error(`Layer[${index}] at invalid: ${String(error)}`);
throw new Error(`Layer[${index}] at invalid: ${String((error instanceof Error) ? error.message : error)}`);
}
});

Expand Down

0 comments on commit 7e774f0

Please sign in to comment.