Skip to content

Commit

Permalink
Latitude & Longitude scalars (#748)
Browse files Browse the repository at this point in the history
* Added Latitude & Longitude scalars

* Added Latitude & Longitude scalars fix
  • Loading branch information
carlocorradini authored Mar 8, 2021
1 parent 897feb9 commit 0f78b62
Show file tree
Hide file tree
Showing 10 changed files with 534 additions and 1 deletion.
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ scalar ISBN

scalar JWT

scalar Latitude

scalar Longitude

scalar MAC

scalar Port
Expand Down Expand Up @@ -158,6 +162,8 @@ import {
IPv6Resolver,
ISBNResolver,
JWTResolver,
LatitudeResolver,
LongitudeResolver,
MACResolver,
PortResolver,
RGBResolver,
Expand Down Expand Up @@ -225,6 +231,9 @@ const myResolverMap = {

JWT: JWTResolver,

Latitude: LatitudeResolver,
Longitude: LongitudeResolver,

USCurrency: USCurrencyResolver,
Currency: CurrencyResolver,
JSON: JSONResolver,
Expand Down Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
GraphQLIPv6,
GraphQLISBN,
GraphQLJWT,
GraphQLLatitude,
GraphQLLongitude,
GraphQLMAC,
GraphQLPort,
GraphQLRGB,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -199,6 +205,8 @@ export const resolvers = {
IPv6: GraphQLIPv6,
ISBN: GraphQLISBN,
JWT: GraphQLJWT,
Latitude: GraphQLLatitude,
Longitude: GraphQLLongitude,
MAC: GraphQLMAC,
Port: GraphQLPort,
RGB: GraphQLRGB,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -309,6 +319,8 @@ export {
GraphQLIPv6,
GraphQLISBN,
GraphQLJWT,
GraphQLLatitude,
GraphQLLongitude,
GraphQLMAC,
GraphQLPort,
GraphQLRGB,
Expand Down
2 changes: 2 additions & 0 deletions src/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => ({});
Expand Down
65 changes: 65 additions & 0 deletions src/scalars/Latitude.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
65 changes: 65 additions & 0 deletions src/scalars/Longitude.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
2 changes: 2 additions & 0 deletions src/scalars/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
53 changes: 53 additions & 0 deletions src/scalars/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
4 changes: 4 additions & 0 deletions src/typeDefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -90,6 +92,8 @@ export const typeDefs = [
IPv6,
ISBN,
JWT,
Latitude,
Longitude,
MAC,
Port,
RGB,
Expand Down
Loading

0 comments on commit 0f78b62

Please sign in to comment.