From 0f78b62a4926bfed8740d4cb9e3a7cdab18c8aea Mon Sep 17 00:00:00 2001 From: Carlo Corradini Date: Mon, 8 Mar 2021 15:16:02 +0100 Subject: [PATCH] Latitude & Longitude scalars (#748) * Added Latitude & Longitude scalars * Added Latitude & Longitude scalars fix --- README.md | 36 +++++++++- src/index.ts | 12 ++++ src/mocks.ts | 2 + src/scalars/Latitude.ts | 65 +++++++++++++++++ src/scalars/Longitude.ts | 65 +++++++++++++++++ src/scalars/index.ts | 2 + src/scalars/utilities.ts | 53 ++++++++++++++ src/typeDefs.ts | 4 ++ tests/Latitude.test.ts | 148 +++++++++++++++++++++++++++++++++++++++ tests/Longitude.test.ts | 148 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 src/scalars/Latitude.ts create mode 100644 src/scalars/Longitude.ts create mode 100644 tests/Latitude.test.ts create mode 100644 tests/Longitude.test.ts diff --git a/README.md b/README.md index 1e56bbc3e..8b436c114 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,10 @@ scalar ISBN scalar JWT +scalar Latitude + +scalar Longitude + scalar MAC scalar Port @@ -158,6 +162,8 @@ import { IPv6Resolver, ISBNResolver, JWTResolver, + LatitudeResolver, + LongitudeResolver, MACResolver, PortResolver, RGBResolver, @@ -225,6 +231,9 @@ const myResolverMap = { JWT: JWTResolver, + Latitude: LatitudeResolver, + Longitude: LongitudeResolver, + USCurrency: USCurrencyResolver, Currency: CurrencyResolver, JSON: JSONResolver, @@ -714,9 +723,34 @@ A field whose value is a [IPv6 address](https://en.wikipedia.org/wiki/IPv6). A field whose value is a [ISBN-10 or ISBN-13 number](https://en.wikipedia.org/wiki/International_Standard_Book_Number). ### JWT + A field whose value is a [JSON Web Token (JWT)](https://jwt.io/introduction). -The scalar checks only the format (*header.payload.signature*) using a regex and not the validity (signature) of the token. +The scalar checks only the format (_header.payload.signature_) using a regex and not the validity (signature) of the token. + +### Latitude + +A field whose value is a valid [decimal degrees latitude number](https://en.wikipedia.org/wiki/Latitude) (53.471). + +The **input** value can be either in _decimal_ (53.471) or _sexagesimal_ (53° 21' 16") format. + +The **output** value is always in _decimal_ format (53.471). + +The maximum decimal degrees **precision** is **8**. See [Decimal Degrees Precision](https://en.wikipedia.org/wiki/Decimal_degrees#Precision) for more information. + +_This scalar is inspired by [Geolib](https://github.com/manuelbieh/geolib)._ + +### Longitude + +A field whose value is a valid [decimal degrees longitude number](https://en.wikipedia.org/wiki/Longitude) (53.471). + +The **input** value can be either in _decimal_ (53.471) or _sexagesimal_ (53° 21' 16") format. + +The **output** value is always in _decimal_ format (53.471). + +The maximum decimal degrees **precision** is **8**. See [Decimal Degrees Precision](https://en.wikipedia.org/wiki/Decimal_degrees#Precision) for more information. + +_This scalar is inspired by [Geolib](https://github.com/manuelbieh/geolib)._ ### MAC diff --git a/src/index.ts b/src/index.ts index ff6ad6342..fdb127849 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,6 +39,8 @@ import { GraphQLIPv6, GraphQLISBN, GraphQLJWT, + GraphQLLatitude, + GraphQLLongitude, GraphQLMAC, GraphQLPort, GraphQLRGB, @@ -91,6 +93,8 @@ export { IPv6 as IPv6Definition, ISBN as ISBNDefinition, JWT as JWTDefinition, + Latitude as LatitudeDefinition, + Longitude as LongitudeDefinition, MAC as MACDefinition, Port as PortDefinition, RGB as RGBDefinition, @@ -146,6 +150,8 @@ export { GraphQLIPv6 as IPv6Resolver, GraphQLISBN as ISBNResolver, GraphQLJWT as JWTResolver, + GraphQLLatitude as LatitudeResolver, + GraphQLLongitude as LongitudeResolver, GraphQLMAC as MACResolver, GraphQLPort as PortResolver, GraphQLRGB as RGBResolver, @@ -199,6 +205,8 @@ export const resolvers = { IPv6: GraphQLIPv6, ISBN: GraphQLISBN, JWT: GraphQLJWT, + Latitude: GraphQLLatitude, + Longitude: GraphQLLongitude, MAC: GraphQLMAC, Port: GraphQLPort, RGB: GraphQLRGB, @@ -252,6 +260,8 @@ export { IPv6 as IPv6Mock, ISBN as ISBNMock, JWT as JWTMock, + Latitude as LatitudeMock, + Longitude as LongitudeMock, MAC as MACMock, Port as PortMock, RGB as RGBMock, @@ -309,6 +319,8 @@ export { GraphQLIPv6, GraphQLISBN, GraphQLJWT, + GraphQLLatitude, + GraphQLLongitude, GraphQLMAC, GraphQLPort, GraphQLRGB, diff --git a/src/mocks.ts b/src/mocks.ts index 322397b72..959a4239f 100644 --- a/src/mocks.ts +++ b/src/mocks.ts @@ -104,6 +104,8 @@ export const JWT = () => { // } return `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJwcm9qZWN0IjoiZ3JhcGhxbC1zY2FsYXJzIn0.nYdrSfE2nNRAgpiEU1uKgn2AYYKLo28Z0nhPXvsuIww`; }; +export const Latitude = () => 41.902782; +export const Longitude = () => 12.496366; export const USCurrency = () => 1000; export const Currency = () => 'USD'; export const JSON = () => ({}); diff --git a/src/scalars/Latitude.ts b/src/scalars/Latitude.ts new file mode 100644 index 000000000..d5d2d802d --- /dev/null +++ b/src/scalars/Latitude.ts @@ -0,0 +1,65 @@ +// Inspired by Geolib: https://github.com/manuelbieh/geolib +import { GraphQLError, GraphQLScalarType, Kind } from 'graphql'; +import { isDecimal, isSexagesimal, sexagesimalToDecimal } from './utilities'; + +// Minimum latitude +const MIN_LAT = -90.0; +// Maximum latitude +const MAX_LAT = 90.0; +// See https://en.wikipedia.org/wiki/Decimal_degrees#Precision +const MAX_PRECISION = 8; + +const validate = (value: any): number => { + // Check if value is a string or a number + if ( + (typeof value !== 'string' && typeof value !== 'number') || + value === null || + typeof value === 'undefined' || + Number.isNaN(value) + ) { + throw new TypeError(`Value is neither a number nor a string: ${value}`); + } + + if (isDecimal(value)) { + const decimalValue = + typeof value === 'string' ? Number.parseFloat(value) : value; + + if (decimalValue < MIN_LAT || decimalValue > MAX_LAT) { + throw new RangeError( + `Value must be between ${MIN_LAT} and ${MAX_LAT}: ${value}`, + ); + } + + return Number.parseFloat(decimalValue.toFixed(MAX_PRECISION)); + } + + if (isSexagesimal(value)) { + return validate(sexagesimalToDecimal(value)); + } + + throw new TypeError(`Value is not a valid latitude: ${value}`); +}; + +export const GraphQLLatitude = /*#__PURE__*/ new GraphQLScalarType({ + name: `Latitude`, + + description: `A field whose value is a valid decimal degrees latitude number (53.471): https://en.wikipedia.org/wiki/Latitude`, + + serialize(value) { + return validate(value); + }, + + parseValue(value) { + return validate(value); + }, + + parseLiteral(ast) { + if (ast.kind !== Kind.FLOAT && ast.kind !== Kind.STRING) { + throw new GraphQLError( + `Can only validate floats or strings as latitude but got a: ${ast.kind}`, + ); + } + + return validate(ast.value); + }, +}); diff --git a/src/scalars/Longitude.ts b/src/scalars/Longitude.ts new file mode 100644 index 000000000..4e9878f5b --- /dev/null +++ b/src/scalars/Longitude.ts @@ -0,0 +1,65 @@ +// Inspired by Geolib: https://github.com/manuelbieh/geolib +import { GraphQLError, GraphQLScalarType, Kind } from 'graphql'; +import { isDecimal, isSexagesimal, sexagesimalToDecimal } from './utilities'; + +// Minimum longitude +const MIN_LON = -180.0; +// Maximum longitude +const MAX_LON = 180.0; +// See https://en.wikipedia.org/wiki/Decimal_degrees#Precision +const MAX_PRECISION = 8; + +const validate = (value: any): number => { + // Check if value is a string or a number + if ( + (typeof value !== 'string' && typeof value !== 'number') || + value === null || + typeof value === 'undefined' || + Number.isNaN(value) + ) { + throw new TypeError(`Value is neither a number nor a string: ${value}`); + } + + if (isDecimal(value)) { + const decimalValue = + typeof value === 'string' ? Number.parseFloat(value) : value; + + if (decimalValue < MIN_LON || decimalValue > MAX_LON) { + throw new RangeError( + `Value must be between ${MIN_LON} and ${MAX_LON}: ${value}`, + ); + } + + return Number.parseFloat(decimalValue.toFixed(MAX_PRECISION)); + } + + if (isSexagesimal(value)) { + return validate(sexagesimalToDecimal(value)); + } + + throw new TypeError(`Value is not a valid longitude: ${value}`); +}; + +export const GraphQLLongitude = /*#__PURE__*/ new GraphQLScalarType({ + name: `Longitude`, + + description: `A field whose value is a valid decimal degrees longitude number (53.471): https://en.wikipedia.org/wiki/Longitude`, + + serialize(value) { + return validate(value); + }, + + parseValue(value) { + return validate(value); + }, + + parseLiteral(ast) { + if (ast.kind !== Kind.FLOAT && ast.kind !== Kind.STRING) { + throw new GraphQLError( + `Can only validate floats or strings as longitude but got a: ${ast.kind}`, + ); + } + + return validate(ast.value); + }, +}); diff --git a/src/scalars/index.ts b/src/scalars/index.ts index f12ffe61b..489fc7d8f 100644 --- a/src/scalars/index.ts +++ b/src/scalars/index.ts @@ -36,6 +36,8 @@ export { GraphQLIPv4 } from './IPv4'; export { GraphQLIPv6 } from './IPv6'; export { GraphQLISBN } from './ISBN'; export { GraphQLJWT } from './JWT'; +export { GraphQLLatitude } from './Latitude'; +export { GraphQLLongitude } from './Longitude'; export { GraphQLMAC } from './MAC'; export { GraphQLPort } from './Port'; export { GraphQLRGB } from './RGB'; diff --git a/src/scalars/utilities.ts b/src/scalars/utilities.ts index 93156c67b..862e63081 100644 --- a/src/scalars/utilities.ts +++ b/src/scalars/utilities.ts @@ -10,6 +10,9 @@ enum VALUE_TYPES { FLOAT, } +// More info about Sexagesimal: https://en.wikipedia.org/wiki/Sexagesimal +const SEXAGESIMAL_REGEX = /^([0-9]{1,3})°\s*([0-9]{1,3}(?:\.(?:[0-9]{1,}))?)['′]\s*(([0-9]{1,3}(\.([0-9]{1,}))?)["″]\s*)?([NEOSW]?)$/; + // TODO: Consider implementing coercion like this... // See: https://github.com/graphql/graphql-js/blob/master/src/type/scalars.js#L13 // See: https://github.com/graphql/graphql-js/blob/master/src/type/scalars.js#L60 @@ -117,3 +120,53 @@ export function processValue(value: any, scalarName: string) { return parsedValue; } + +/** + * Check if the value is in decimal format. + * + * @param value - Value to check + * @returns True if is decimal, false otherwise + */ +export function isDecimal(value: any): boolean { + const checkedValue = value.toString().trim(); + + if (Number.isNaN(Number.parseFloat(checkedValue))) { + return false; + } + + return Number.parseFloat(checkedValue) === Number(checkedValue); +} + +/** + * Check if the value is in sexagesimal format. + * + * @param value - Value to check + * @returns True if sexagesimal, false otherwise + */ +export function isSexagesimal(value: any): boolean { + if (typeof value !== 'string') return false; + + return SEXAGESIMAL_REGEX.test(value.toString().trim()); +} + +/** + * Converts a sexagesimal coordinate to decimal format. + * + * @param value - Value to convert + * @returns Decimal coordinate + * @throws {TypeError} if the value is not in sexagesimal format + */ +export function sexagesimalToDecimal(value: any) { + const data = SEXAGESIMAL_REGEX.exec(value); + + if (typeof data === 'undefined' || data === null) { + throw new TypeError(`Value is not in sexagesimal format: ${value}`); + } + + const min = Number(data[2]) / 60 || 0; + const sec = Number(data[4]) / 3600 || 0; + const decimal = Number.parseFloat(data[1]) + min + sec; + + // Southern and western coordinates must be negative decimals + return ['S', 'W'].includes(data[7]) ? -decimal : decimal; +} diff --git a/src/typeDefs.ts b/src/typeDefs.ts index fa9f08a56..c0e323c72 100644 --- a/src/typeDefs.ts +++ b/src/typeDefs.ts @@ -21,6 +21,8 @@ export const IPv4 = `scalar IPv4`; export const IPv6 = `scalar IPv6`; export const ISBN = `scalar ISBN`; export const JWT = `scalar JWT`; +export const Latitude = `scalar Latitude`; +export const Longitude = `scalar Longitude`; export const JSON = `scalar JSON`; export const JSONObject = `scalar JSONObject`; export const MAC = `scalar MAC`; @@ -90,6 +92,8 @@ export const typeDefs = [ IPv6, ISBN, JWT, + Latitude, + Longitude, MAC, Port, RGB, diff --git a/tests/Latitude.test.ts b/tests/Latitude.test.ts new file mode 100644 index 000000000..e696b85fb --- /dev/null +++ b/tests/Latitude.test.ts @@ -0,0 +1,148 @@ +import { Kind } from 'graphql'; +import { GraphQLLatitude } from '../src/scalars/Latitude'; + +const LATITUDES: { dms: string; dd: number; precision: number }[] = [ + { dms: `90° 0' 0.000" S`, dd: -90.0, precision: 0 }, + { dms: `90° 0' 0.000" N`, dd: 90.0, precision: 0 }, + { dms: `38° 36' 0.000" S`, dd: -38.6, precision: 1 }, + { dms: `66° 54' 0.000" S`, dd: -66.9, precision: 1 }, + { dms: `39° 51' 21.600" N`, dd: 39.86, precision: 2 }, + { dms: `52° 19' 48.000" N`, dd: 52.33, precision: 2 }, + { dms: `51° 30' 28.800" N`, dd: 51.508, precision: 3 }, + { dms: `64° 45' 18.000" N`, dd: 64.755, precision: 3 }, + { dms: `36° 16' 57.360" N`, dd: 36.2826, precision: 4 }, + { dms: `6° 10' 50.160" S`, dd: -6.1806, precision: 4 }, + { dms: `41°53'30.95"N`, dd: 41.89193, precision: 5 }, + { dms: `40°42'51.37"N`, dd: 40.71427, precision: 5 }, + { dms: `42° 49' 58.845" N`, dd: 42.833013, precision: 6 }, + { dms: `6° 41' 37.353" N`, dd: 6.693709, precision: 6 }, + { dms: `23° 6' 23.997" S`, dd: -23.1066658, precision: 7 }, + { dms: `23° 19' 19.453" S`, dd: -23.3220703, precision: 7 }, + { dms: `66° 0' 21.983" N`, dd: 66.00610639, precision: 8 }, + { dms: `76° 49' 14.845" N`, dd: 76.82079028, precision: 8 }, +]; + +const toPrecision = (latitude: number, precision: number): number => { + return Number.parseFloat(latitude.toFixed(precision)); +}; + +describe(`Latitude`, () => { + describe(`valid`, () => { + it(`serialize`, () => { + for (const latitude of LATITUDES) { + expect(GraphQLLatitude.serialize(latitude.dd)).toEqual(latitude.dd); + expect( + toPrecision( + GraphQLLatitude.serialize(latitude.dms), + latitude.precision, + ), + ).toEqual(latitude.dd); + } + }); + + it(`parseValue`, () => { + for (const latitude of LATITUDES) { + expect(GraphQLLatitude.serialize(latitude.dd)).toEqual(latitude.dd); + expect( + toPrecision( + GraphQLLatitude.serialize(latitude.dms), + latitude.precision, + ), + ).toEqual(latitude.dd); + } + }); + + it(`parseLiteral`, () => { + for (const latitude of LATITUDES) { + expect( + GraphQLLatitude.parseLiteral( + { + value: latitude.dd.toString(), + kind: Kind.FLOAT, + }, + {}, + ), + ).toEqual(latitude.dd); + expect( + toPrecision( + GraphQLLatitude.parseLiteral( + { + value: latitude.dms.toString(), + kind: Kind.STRING, + }, + {}, + ), + latitude.precision, + ), + ).toEqual(latitude.dd); + } + }); + }); + + describe('invalid', () => { + describe(`not a valid latitude`, () => { + it(`serialize`, () => { + expect(() => GraphQLLatitude.serialize(true)).toThrow( + /Value is neither a number nor a string/, + ); + expect(() => + GraphQLLatitude.serialize(`this is not a latitude`), + ).toThrow(/Value is not a valid latitude/); + expect(() => GraphQLLatitude.serialize(-90.00000001)).toThrow( + /Value must be between -90 and 90/, + ); + expect(() => GraphQLLatitude.serialize(90.00000001)).toThrow( + /Value must be between -90 and 90/, + ); + }); + + it(`parseValue`, () => { + expect(() => GraphQLLatitude.parseValue(true)).toThrow( + /Value is neither a number nor a string/, + ); + expect(() => + GraphQLLatitude.parseValue(`this is not a latitude`), + ).toThrow(/Value is not a valid latitude/); + expect(() => GraphQLLatitude.parseValue(-90.00000001)).toThrow( + /Value must be between -90 and 90/, + ); + expect(() => GraphQLLatitude.parseValue(90.00000001)).toThrow( + /Value must be between -90 and 90/, + ); + }); + + it(`parseLiteral`, () => { + expect(() => + GraphQLLatitude.parseLiteral( + { value: true, kind: Kind.BOOLEAN } as any, + {}, + ), + ).toThrow(/Can only validate floats or strings as latitude but got a/); + expect(() => + GraphQLLatitude.parseLiteral( + { value: `this is not a latitude`, kind: Kind.STRING }, + {}, + ), + ).toThrow(/Value is not a valid latitude/); + expect(() => + GraphQLLatitude.parseLiteral( + { + value: '-90.00000001', + kind: Kind.FLOAT, + }, + {}, + ), + ).toThrow(/Value must be between -90 and 90/); + expect(() => + GraphQLLatitude.parseLiteral( + { + value: '90.00000001', + kind: Kind.FLOAT, + }, + {}, + ), + ).toThrow(/Value must be between -90 and 90/); + }); + }); + }); +}); diff --git a/tests/Longitude.test.ts b/tests/Longitude.test.ts new file mode 100644 index 000000000..093a11be7 --- /dev/null +++ b/tests/Longitude.test.ts @@ -0,0 +1,148 @@ +import { Kind } from 'graphql'; +import { GraphQLLongitude } from '../src/scalars/Longitude'; + +const LONGITUDES: { dms: string; dd: number; precision: number }[] = [ + { dms: `180° 0' 0.000" W`, dd: -180.0, precision: 0 }, + { dms: `180° 0' 0.000" E`, dd: 180.0, precision: 0 }, + { dms: `176° 19' 26.576" E`, dd: 176.3, precision: 1 }, + { dms: `62° 12' 48.831" W`, dd: -62.2, precision: 1 }, + { dms: `4° 46' 6.456" W`, dd: -4.77, precision: 2 }, + { dms: `6° 28' 33.481" W`, dd: -6.48, precision: 2 }, + { dms: `0° 10' 6.902" W`, dd: -0.169, precision: 3 }, + { dms: `118° 45' 3.780" E`, dd: 118.751, precision: 3 }, + { dms: `139° 19' 8.803" E`, dd: 139.3191, precision: 4 }, + { dms: `141° 59' 27.377" E`, dd: 141.9909, precision: 4 }, + { dms: `12°30'40.79"E`, dd: 12.51133, precision: 5 }, + { dms: `74°0'21.49"W`, dd: -74.00597, precision: 5 }, + { dms: `99° 44' 56.030" W`, dd: -99.748897, precision: 6 }, + { dms: `21° 55' 56.083" E`, dd: 21.932245, precision: 6 }, + { dms: `129° 39' 38.704" E`, dd: 129.6607511, precision: 7 }, + { dms: `54° 33' 12.699" W`, dd: -54.5535275, precision: 7 }, + { dms: `148° 34' 9.124" W`, dd: -148.56920111, precision: 8 }, + { dms: `44° 44' 2.119" W`, dd: -44.73392194, precision: 8 }, +]; + +const toPrecision = (longitude: number, precision: number): number => { + return Number.parseFloat(longitude.toFixed(precision)); +}; + +describe(`Longitude`, () => { + describe(`valid`, () => { + it(`serialize`, () => { + for (const longitude of LONGITUDES) { + expect(GraphQLLongitude.serialize(longitude.dd)).toEqual(longitude.dd); + expect( + toPrecision( + GraphQLLongitude.serialize(longitude.dms), + longitude.precision, + ), + ).toEqual(longitude.dd); + } + }); + + it(`parseValue`, () => { + for (const longitude of LONGITUDES) { + expect(GraphQLLongitude.serialize(longitude.dd)).toEqual(longitude.dd); + expect( + toPrecision( + GraphQLLongitude.serialize(longitude.dms), + longitude.precision, + ), + ).toEqual(longitude.dd); + } + }); + + it(`parseLiteral`, () => { + for (const longitude of LONGITUDES) { + expect( + GraphQLLongitude.parseLiteral( + { + value: longitude.dd.toString(), + kind: Kind.FLOAT, + }, + {}, + ), + ).toEqual(longitude.dd); + expect( + toPrecision( + GraphQLLongitude.parseLiteral( + { + value: longitude.dms.toString(), + kind: Kind.STRING, + }, + {}, + ), + longitude.precision, + ), + ).toEqual(longitude.dd); + } + }); + }); + + describe('invalid', () => { + describe(`not a valid longitude`, () => { + it(`serialize`, () => { + expect(() => GraphQLLongitude.serialize(true)).toThrow( + /Value is neither a number nor a string/, + ); + expect(() => + GraphQLLongitude.serialize(`this is not a longitude`), + ).toThrow(/Value is not a valid longitude/); + expect(() => GraphQLLongitude.serialize(-180.00000001)).toThrow( + /Value must be between -180 and 180/, + ); + expect(() => GraphQLLongitude.serialize(180.00000001)).toThrow( + /Value must be between -180 and 180/, + ); + }); + + it(`parseValue`, () => { + expect(() => GraphQLLongitude.parseValue(true)).toThrow( + /Value is neither a number nor a string/, + ); + expect(() => + GraphQLLongitude.parseValue(`this is not a longitude`), + ).toThrow(/Value is not a valid longitude/); + expect(() => GraphQLLongitude.parseValue(-180.00000001)).toThrow( + /Value must be between -180 and 180/, + ); + expect(() => GraphQLLongitude.parseValue(180.00000001)).toThrow( + /Value must be between -180 and 180/, + ); + }); + + it(`parseLiteral`, () => { + expect(() => + GraphQLLongitude.parseLiteral( + { value: true, kind: Kind.BOOLEAN } as any, + {}, + ), + ).toThrow(/Can only validate floats or strings as longitude but got a/); + expect(() => + GraphQLLongitude.parseLiteral( + { value: `this is not a longitude`, kind: Kind.STRING }, + {}, + ), + ).toThrow(/Value is not a valid longitude/); + expect(() => + GraphQLLongitude.parseLiteral( + { + value: '-180.00000001', + kind: Kind.FLOAT, + }, + {}, + ), + ).toThrow(/Value must be between -180 and 180/); + expect(() => + GraphQLLongitude.parseLiteral( + { + value: '180.00000001', + kind: Kind.FLOAT, + }, + {}, + ), + ).toThrow(/Value must be between -180 and 180/); + }); + }); + }); +});