diff --git a/CHANGELOG.md b/CHANGELOG.md index 262898252..3bf967097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.4.0] - 2018-04-27 + +### Added + +* `RegularExpression` scalar type _generator_ +* `UnsignedInt` alias for `NonNegativeInt` +* `UnsignedFloat` alias for `NonNegativeFloat` + ## [0.3.0] - 2018-04-06 ### Changed diff --git a/README.md b/README.md index fd162450b..5ecc6e091 100644 --- a/README.md +++ b/README.md @@ -6,53 +6,68 @@ > A library of custom GraphQL [scalar types](http://graphql.org/learn/schema/#scalar-types) for creating precise type-safe GraphQL schemas. ## Installation + ``` npm install --save @okgrow/graphql-scalars ``` +or + +``` +yarn add @okgrow/graphql-scalars +``` ## Usage + To use these scalars you'll need to add them in two places, your schema and your resolvers map. +NOTE: The new `RegularExpression` scalar will be used a little differently and is explained below. + In your schema: + ```graphql scalar DateTime scalar NonPositiveInt + scalar PositiveInt + scalar NonNegativeInt + scalar NegativeInt scalar NonPositiveFloat + scalar PositiveFloat + scalar NonNegativeFloat + scalar NegativeFloat scalar EmailAddress + scalar URL scalar PhoneNumber + scalar PostalCode ``` In your resolver map, first import them: + ```js import { DateTime, - NonPositiveInt, PositiveInt, NonNegativeInt, NegativeInt, - NonPositiveFloat, PositiveFloat, NonNegativeFloat, NegativeFloat, - EmailAddress, URL, - PhoneNumber, PostalCode, } from '@okgrow/graphql-scalars'; @@ -87,10 +102,14 @@ const myResolverMap = { Mutation: { // more stuff here }, -} +}; ``` +NOTE: `NonNegativeFloat` and `NonNegativeInt` are also available under the aliases `UnsignedFloat` +and `UnsignedInt`, respectively. + Alternatively, use the default import and ES6's spread operator syntax: + ```js import OKGGraphQLScalars from '@okgrow/graphql-scalars'; ``` @@ -108,11 +127,11 @@ const myResolverMap = { Mutation: { // more stuff here }, -} +}; ``` - That's it. Now you can use these scalar types in your schema definition like this: + ```graphql type Person { birthDate: DateTime @@ -130,70 +149,145 @@ type Person { phoneNumber: PhoneNumber homePostalCode: PostalCode } - ``` These scalars can be used just like the base, built-in ones. +### Using the RegulareExpression scalar + +First an explanation: To create a new scalar type to the GraphQL schema language, you must create an +instance of a new `GraphQLScalarType` object that implements three general functions/methods: +`serialize`, `parseValue` and `parseLiteral` which are used at different stages of processing your +GraphQL types during queries and mutations. So creating a new scalar looks like this: + +``` +const MyScalar = new GraphQLScalarType({ + 'MyScalar', + + description: 'A description of my scalar', + + serialize(value) { + // ... + return value; + }, + + parseValue(value) { + // ... + return value; + }, + + parseLiteral(ast) { + // ... + return ast.value; + } + }); +``` + +Given this, if we want to create a new type that is essentially the same except for one little +customizable aspect (e.g., a regular expression type that has all the same code except the regex is +different) then we need to dynamically _generate_ a new `GraphQLScalarType` object given some +parameters. That's the approach we take here. + +Therefore the `RegularExpression` scalar type is really a `GraphQLScalarType` object _generator_ +that takes two arguments: + +* a name +* the regex you want it to use + +So to create a new scalar for a given regex, you will do this: + +``` +const MyRegexType = new RegexType('MyRegexType', /^ABC$/); +``` + +Now `MyRegexType` is your new GraphQL scalar type that will enforce a value of, in this case, "ABC". + +Add your new scalar type to your esolver map: + +``` +export default { + MyRegexType, +}; +``` + +And to your schema: + +``` +scalar MyRegexType +``` + +That's it. Now you can use `MyRegexType` as a type in the rest of your schema. ## Why? + The primary purposes these scalars, really of _all_ types are to: -1. Communicate to users of your schema exactly what they can expect or to at least _reduce_ -ambiguity in cases where that's possible. For example if you have a `Person` type in your schema -and that type has as field like `ageInYears`, the value of that can only be null or a positive -integer (or float, depending on how you want your schema to work). It should never be zero or -negative. -1. Run-time type checking. GraphQL helps to tighten up the contract between client and server. It -does this with strong typing of the _interface_ (or _schema_). This helps us have greater -confidence about what we're receiving from the server and what the server is receiving from the -client. +1. Communicate to users of your schema exactly what they can expect or to at least _reduce_ + ambiguity in cases where that's possible. For example if you have a `Person` type in your schema + and that type has as field like `ageInYears`, the value of that can only be null or a positive + integer (or float, depending on how you want your schema to work). It should never be zero or + negative. +1. Run-time type checking. GraphQL helps to tighten up the contract between client and server. It + does this with strong typing of the _interface_ (or _schema_). This helps us have greater + confidence about what we're receiving from the server and what the server is receiving from the + client. This package adds to the base options available in GraphQL to support types that are reasonably common in defining schemas or interfaces to data. - ## The Types ### DateTime + Use real JavaScript Dates for GraphQL fields. Currently you can use a String or an Int (e.g., a timestamp in milliseconds) to represent a date/time. This scalar makes it easy to be explicit about the type and have a real JavaScript Date returned that the client can use _without_ doing the inevitable parsing or conversion themselves. ### NonNegativeInt + Integers that will have a value of 0 or more. Uses [`parseInt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt). ### NonPositiveInt + Integers that will have a value of 0 or less. Uses [`parseInt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt). ### PositiveInt + Integers that will have a value greater than 0. Uses [`parseInt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt). ### NegativeInt + Integers that will have a value less than 0. Uses [`parseInt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt). ### NonNegativeFloat + Floats that will have a value of 0 or more. Uses [`parseFloat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat). ### NonPositiveFloat + Floats that will have a value of 0 or less. Uses [`parseFloat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat). ### PositiveFloat + Floats that will have a value greater than 0. Uses [`parseFloat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat). ### NegativeFloat + Floats that will have a value less than 0. Uses [`parseFloat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat). ### EmailAddress + A field whose value conforms to the standard internet email address format as specified in [RFC822](https://www.w3.org/Protocols/rfc822/). ### URL + A field whose value conforms to the standard URL format as specified in [RFC3986](https://www.ietf.org/rfc/rfc3986.txt). ### PhoneNumber + A field whose value conforms to the standard E.164 format as specified in [E.164 specification](https://en.wikipedia.org/wiki/E.164). Basically this is `+17895551234`. The very powerful @@ -202,38 +296,43 @@ _that_ format, parse and display it in whatever display format you want. It can parse user input and _get_ the E.164 format to pass _into_ a schema. ### PostalCode -We're going to start with a limited set as suggested [here] (http://www.pixelenvision.com/1708/zip-postal-code-validation-regex-php-code-for-12-countries/) -and [here] (https://stackoverflow.com/questions/578406/what-is-the-ultimate-postal-code-and-zip-regex). + +We're going to start with a limited set as suggested [here](http://www.pixelenvision.com/1708/zip-postal-code-validation-regex-php-code-for-12-countries/) +and [here](https://stackoverflow.com/questions/578406/what-is-the-ultimate-postal-code-and-zip-regex). Which gives us the following countries: -- US - United States -- UK - United Kingdom -- DE - Germany -- CA - Canada -- FR - France -- IT - Italy -- AU - Australia -- NL - Netherlands -- ES - Spain -- DK - Denmark -- SE - Sweden -- BE - Belgium -- IN - India +* US - United States +* UK - United Kingdom +* DE - Germany +* CA - Canada +* FR - France +* IT - Italy +* AU - Australia +* NL - Netherlands +* ES - Spain +* DK - Denmark +* SE - Sweden +* BE - Belgium +* IN - India This is really a practical decision of weight (of the package) vs. completeness. -In the future we might expand this list and use the more comprehensive list found [here] (http://unicode.org/cldr/trac/browser/tags/release-26-0-1/common/supplemental/postalCodeData.xml). +In the future we might expand this list and use the more comprehensive list found [here](http://unicode.org/cldr/trac/browser/tags/release-26-0-1/common/supplemental/postalCodeData.xml). +### RegularExpression -## Future -We'd like to keep growing this package, within reason, to include the scalar types that are widely -required when defining GraphQL schemas. We welcome both suggestions and pull requests. One idea -we're considering is: +A `GraphQLScalarType` object generator that takes two arguments: -- BLOB, could be could be a base64-encoded object of some kind +* `name` - The name of your custom type +* `regex` - The regex to be used to check against any values for fields with this new type + +``` +const MyRegexType = new RegexType('MyRegexType', /^ABC$/); +``` ## What's this all about? + GraphQL is a wonderful new approach to application data and API layers that's gaining momentum. If you have not heard of it, start [here](http://graphql.org/learn/) and check out [Apollo](http://dev.apollodata.com/) also. @@ -249,12 +348,12 @@ what this package does. clean. Arguably not every project needs these additional scalar types. But _we_ have, and now _you_ can use them too if needed. - ## License -Released under the [MIT license](https://github.com/okgrow/analytics/blob/master/License.md). +Released under the [MIT license](https://github.com/okgrow/analytics/blob/master/License.md). ## Contributing + Issues and Pull Requests are always welcome. Please read our [contribution guidelines](https://okgrow.github.io/guides/docs/open-source-contributing.html). diff --git a/package-lock.json b/package-lock.json index 5c4f279c8..acf09e74f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@okgrow/graphql-scalars", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9f0e0c73f..afaa1ee0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@okgrow/graphql-scalars", - "version": "0.3.0", + "version": "0.4.0", "description": "A collection of scalar types not included in base GraphQL.", "repository": { "type": "git", diff --git a/src/RegularExpression.js b/src/RegularExpression.js new file mode 100644 index 000000000..edd16e4a8 --- /dev/null +++ b/src/RegularExpression.js @@ -0,0 +1,61 @@ +import { GraphQLScalarType } from 'graphql'; +import { GraphQLError } from 'graphql/error'; +import { Kind } from 'graphql/language'; + +function RegularExpression(name, regex) { + const REGEX = new RegExp(regex); + + return new GraphQLScalarType({ + name, + + description: `A field whose value matches the provided regular expression ${regex}.`, + + serialize(value) { + if (typeof value !== 'string') { + throw new TypeError(`Value is not string: ${value}`); + } + + if (!REGEX.test(value)) { + throw new TypeError( + `Value does not match the regular expression ${regex}: ${value}`, + ); + } + + return value; + }, + + parseValue(value) { + if (typeof value !== 'string') { + throw new TypeError(`Value is not string: ${value}`); + } + + if (!REGEX.test(value)) { + throw new TypeError( + `Value does not match the regular expression ${regex}: ${value}`, + ); + } + + return value; + }, + + parseLiteral(ast) { + if (ast.kind !== Kind.STRING) { + throw new GraphQLError( + `Can only validate strings as regular expressions but got a: ${ + ast.kind + }`, + ); + } + + if (!REGEX.test(ast.value)) { + throw new TypeError( + `Value does not match the regular expression ${regex}: ${ast.value}`, + ); + } + + return ast.value; + }, + }); +} + +export default RegularExpression; diff --git a/src/__tests__/NegativeFloat.test.js b/src/__tests__/NegativeFloat.test.js index 7b943874a..e8bf92427 100644 --- a/src/__tests__/NegativeFloat.test.js +++ b/src/__tests__/NegativeFloat.test.js @@ -47,10 +47,11 @@ describe('NegativeFloat', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. - // test('parseValue', () => { - // expect(() => NegativeFloat.parseValue(null)).toThrow(/Value is not a number/); - // }); + test('parseValue', () => { + expect(() => NegativeFloat.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); test('parseLiteral', () => { expect(() => @@ -68,7 +69,9 @@ describe('NegativeFloat', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NegativeFloat.parseValue(undefined)).toThrow(/Value is not a number/); + // expect(() => NegativeFloat.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -219,7 +222,9 @@ describe('NegativeFloat', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NegativeFloat.parseValue(Number.NaN)).toThrow(/Value is not a number/); + // expect(() => NegativeFloat.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { diff --git a/src/__tests__/NegativeInt.test.js b/src/__tests__/NegativeInt.test.js index 8f9c9bfb7..84f152b99 100644 --- a/src/__tests__/NegativeInt.test.js +++ b/src/__tests__/NegativeInt.test.js @@ -47,10 +47,11 @@ describe('NegativeInt', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. - // test('parseValue', () => { - // expect(() => NegativeInt.parseValue(null)).toThrow(/Value is not a number/); - // }); + test('parseValue', () => { + expect(() => NegativeInt.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); test('parseLiteral', () => { expect(() => @@ -68,7 +69,9 @@ describe('NegativeInt', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NegativeInt.parseValue(undefined)).toThrow(/Value is not a number/); + // expect(() => NegativeInt.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -85,7 +88,6 @@ describe('NegativeInt', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. test('parseValue', () => { expect(() => NegativeInt.parseValue(2 ** 53)).toThrow( /Value is not a safe integer/, @@ -238,7 +240,9 @@ describe('NegativeInt', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NegativeInt.parseValue(Number.NaN)).toThrow(/Value is not a number/); + // expect(() => NegativeInt.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { diff --git a/src/__tests__/NonNegativeFloat.test.js b/src/__tests__/NonNegativeFloat.test.js index 9cd656d24..3a5bd304c 100644 --- a/src/__tests__/NonNegativeFloat.test.js +++ b/src/__tests__/NonNegativeFloat.test.js @@ -2,7 +2,7 @@ import { Kind } from 'graphql/language'; -import { NonNegativeFloat } from '../'; +import { NonNegativeFloat, UnsignedFloat } from '../'; describe('NonNegativeFloat', () => { describe('valid', () => { @@ -86,10 +86,11 @@ describe('NonNegativeFloat', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. - // test('parseValue', () => { - // expect(() => NonNegativeFloat.parseValue(null)).toThrow(/Value is not a number/); - // }); + test('parseValue', () => { + expect(() => NonNegativeFloat.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); test('parseLiteral', () => { expect(() => @@ -107,7 +108,9 @@ describe('NonNegativeFloat', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NonNegativeFloat.parseValue(undefined)).toThrow(/Value is not a number/); + // expect(() => NonNegativeFloat.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -216,7 +219,9 @@ describe('NonNegativeFloat', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NonNegativeFloat.parseValue(Number.NaN)).toThrow(/Value is not a number/); + // expect(() => NonNegativeFloat.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -232,3 +237,237 @@ describe('NonNegativeFloat', () => { }); }); }); + +describe('UnsignedFloat', () => { + describe('valid', () => { + describe('greater than zero', () => { + describe('as float', () => { + test('serialize', () => { + expect(UnsignedFloat.serialize(123.45)).toBe(123.45); + }); + + test('parseValue', () => { + expect(UnsignedFloat.parseValue(123.45)).toBe(123.45); + }); + + test('parseLiteral', () => { + expect( + UnsignedFloat.parseLiteral({ value: 123.45, kind: Kind.FLOAT }), + ).toBe(123.45); + }); + }); + + describe('as string', () => { + test('serialize', () => { + expect(UnsignedFloat.serialize('123.45')).toBe(123.45); + }); + + test('parseValue', () => { + expect(UnsignedFloat.parseValue('123.45')).toBe(123.45); + }); + + test('parseLiteral', () => { + expect( + UnsignedFloat.parseLiteral({ + value: '123.45', + kind: Kind.FLOAT, + }), + ).toBe(123.45); + }); + }); + }); + + describe('zero', () => { + describe('as float', () => { + test('serialize', () => { + expect(UnsignedFloat.serialize(0.0)).toBe(0.0); + }); + + test('parseValue', () => { + expect(UnsignedFloat.parseValue(0.0)).toBe(0.0); + }); + + test('parseLiteral', () => { + expect( + UnsignedFloat.parseLiteral({ value: 0.0, kind: Kind.FLOAT }), + ).toBe(0.0); + }); + }); + + describe('as string', () => { + test('serialize', () => { + expect(UnsignedFloat.serialize('0.0')).toBe(0.0); + }); + + test('parseValue', () => { + expect(UnsignedFloat.parseValue('0.0')).toBe(0.0); + }); + + test('parseLiteral', () => { + expect( + UnsignedFloat.parseLiteral({ value: '0.0', kind: Kind.FLOAT }), + ).toBe(0.0); + }); + }); + }); + }); + + describe('invalid', () => { + describe('null', () => { + test('serialize', () => { + expect(() => UnsignedFloat.serialize(null)).toThrow( + /Value is not a number/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedFloat.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedFloat.parseLiteral({ value: null, kind: Kind.FLOAT }), + ).toThrow(/Value is not a number/); + }); + }); + + describe('undefined', () => { + test('serialize', () => { + expect(() => UnsignedFloat.serialize(undefined)).toThrow( + /Value is not a number/, + ); + }); + + // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. + // test('parseValue', () => { + // expect(() => UnsignedFloat.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); + // }); + + test('parseLiteral', () => { + expect(() => + UnsignedFloat.parseLiteral({ value: undefined, kind: Kind.FLOAT }), + ).toThrow(/Value is not a number/); + }); + }); + + describe('less than zero', () => { + describe('as float', () => { + test('serialize', () => { + expect(() => UnsignedFloat.serialize(-1.0)).toThrow( + /Value is not a non-negative number/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedFloat.parseValue(-1.0)).toThrow( + /Value is not a non-negative number/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedFloat.parseLiteral({ value: -1.0, kind: Kind.FLOAT }), + ).toThrow(/Value is not a non-negative number/); + }); + }); + + describe('as string', () => { + test('serialize', () => { + expect(() => UnsignedFloat.serialize('-1.0')).toThrow( + /Value is not a non-negative number/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedFloat.parseValue('-1.0')).toThrow( + /Value is not a non-negative number/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedFloat.parseLiteral({ value: '-1.0', kind: Kind.FLOAT }), + ).toThrow(/Value is not a non-negative number/); + }); + }); + }); + + describe('infinity', () => { + test('serialize', () => { + expect(() => UnsignedFloat.serialize(Number.POSITIVE_INFINITY)).toThrow( + /Value is not a finite number/, + ); + }); + + test('parseValue', () => { + expect(() => + UnsignedFloat.parseValue(Number.POSITIVE_INFINITY), + ).toThrow(/Value is not a finite number/); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedFloat.parseLiteral({ + value: Number.POSITIVE_INFINITY, + kind: Kind.FLOAT, + }), + ).toThrow(/Value is not a finite number/); + }); + }); + + describe('not a number', () => { + test('serialize', () => { + expect(() => UnsignedFloat.serialize('not a number')).toThrow( + /Value is not a number/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedFloat.parseValue('not a number')).toThrow( + /Value is not a number/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedFloat.parseLiteral({ + value: 'not a number', + kind: Kind.STRING, + }), + ).toThrow( + /Can only validate floating point numbers as non-negative floating point numbers but got a/, + ); + }); + }); + + describe('NaN', () => { + test('serialize', () => { + expect(() => UnsignedFloat.serialize(Number.NaN)).toThrow( + /Value is not a number/, + ); + }); + + // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. + // test('parseValue', () => { + // expect(() => UnsignedFloat.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); + // }); + + test('parseLiteral', () => { + expect(() => + UnsignedFloat.parseLiteral({ + value: Number.NaN, + kind: Kind.STRING, + }), + ).toThrow( + /Can only validate floating point numbers as non-negative floating point numbers but got a/, + ); + }); + }); + }); +}); diff --git a/src/__tests__/NonNegativeInt.test.js b/src/__tests__/NonNegativeInt.test.js index 55063d9d4..295699340 100644 --- a/src/__tests__/NonNegativeInt.test.js +++ b/src/__tests__/NonNegativeInt.test.js @@ -2,7 +2,7 @@ import { Kind } from 'graphql/language'; -import { NonNegativeInt } from '../'; +import { NonNegativeInt, UnsignedInt } from '../'; describe('NonNegativeInt', () => { describe('valid', () => { @@ -83,10 +83,11 @@ describe('NonNegativeInt', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. - // test('parseValue', () => { - // expect(() => NonNegativeInt.parseValue(null)).toThrow(/Value is not a number/); - // }); + test('parseValue', () => { + expect(() => NonNegativeInt.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); test('parseLiteral', () => { expect(() => @@ -104,7 +105,9 @@ describe('NonNegativeInt', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NonNegativeInt.parseValue(undefined)).toThrow(/Value is not a number/); + // expect(() => NonNegativeInt.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -121,7 +124,6 @@ describe('NonNegativeInt', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. test('parseValue', () => { expect(() => NonNegativeInt.parseValue(2 ** 53)).toThrow( /Value is not a safe integer/, @@ -234,7 +236,9 @@ describe('NonNegativeInt', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NonNegativeInt.parseValue(Number.NaN)).toThrow(/Value is not a number/); + // expect(() => NonNegativeInt.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -247,3 +251,251 @@ describe('NonNegativeInt', () => { }); }); }); + +describe('UnsignedInt', () => { + describe('valid', () => { + describe('greater than zero', () => { + describe('as int', () => { + test('serialize', () => { + expect(UnsignedInt.serialize(123)).toBe(123); + }); + + test('parseValue', () => { + expect(UnsignedInt.parseValue(123)).toBe(123); + }); + + test('parseLiteral', () => { + expect(UnsignedInt.parseLiteral({ value: 123, kind: Kind.INT })).toBe( + 123, + ); + }); + }); + + describe('as string', () => { + test('serialize', () => { + expect(UnsignedInt.serialize('123')).toBe(123); + }); + + test('parseValue', () => { + expect(UnsignedInt.parseValue('123')).toBe(123); + }); + + test('parseLiteral', () => { + expect( + UnsignedInt.parseLiteral({ value: '123', kind: Kind.INT }), + ).toBe(123); + }); + }); + }); + + describe('zero', () => { + describe('as int', () => { + test('serialize', () => { + expect(UnsignedInt.serialize(0)).toBe(0); + }); + + test('parseValue', () => { + expect(UnsignedInt.parseValue(0)).toBe(0); + }); + + test('parseLiteral', () => { + expect(UnsignedInt.parseLiteral({ value: 0, kind: Kind.INT })).toBe( + 0, + ); + }); + }); + + describe('as string', () => { + test('serialize', () => { + expect(UnsignedInt.serialize('0')).toBe(0); + }); + + test('parseValue', () => { + expect(UnsignedInt.parseValue('0')).toBe(0); + }); + + test('parseLiteral', () => { + expect(UnsignedInt.parseLiteral({ value: '0', kind: Kind.INT })).toBe( + 0, + ); + }); + }); + }); + }); + + describe('invalid', () => { + describe('null', () => { + test('serialize', () => { + expect(() => UnsignedInt.serialize(null)).toThrow( + /Value is not a number/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedInt.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedInt.parseLiteral({ value: null, kind: Kind.INT }), + ).toThrow(/Value is not a number/); + }); + }); + + describe('undefined', () => { + test('serialize', () => { + expect(() => UnsignedInt.serialize(undefined)).toThrow( + /Value is not a number/, + ); + }); + + // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. + // test('parseValue', () => { + // expect(() => UnsignedInt.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); + // }); + + test('parseLiteral', () => { + expect(() => + UnsignedInt.parseLiteral({ value: undefined, kind: Kind.INT }), + ).toThrow(/Value is not a number/); + }); + }); + + describe('unsafe integer', () => { + test('serialize', () => { + expect(() => UnsignedInt.serialize(2 ** 53)).toThrow( + /Value is not a safe integer/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedInt.parseValue(2 ** 53)).toThrow( + /Value is not a safe integer/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedInt.parseLiteral({ value: 2 ** 53, kind: Kind.INT }), + ).toThrow(/Value is not a safe integer/); + }); + }); + + describe('less than zero', () => { + describe('as int', () => { + test('serialize', () => { + expect(() => UnsignedInt.serialize(-1)).toThrow( + /Value is not a non-negative number/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedInt.parseValue(-1)).toThrow( + /Value is not a non-negative number/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedInt.parseLiteral({ value: -1, kind: Kind.INT }), + ).toThrow(/Value is not a non-negative number/); + }); + }); + + describe('as string', () => { + test('serialize', () => { + expect(() => UnsignedInt.serialize('-1')).toThrow( + /Value is not a non-negative number/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedInt.parseValue('-1')).toThrow( + /Value is not a non-negative number/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedInt.parseLiteral({ value: '-1', kind: Kind.INT }), + ).toThrow(/Value is not a non-negative number/); + }); + }); + }); + + describe('infinity', () => { + test('serialize', () => { + expect(() => UnsignedInt.serialize(Number.POSITIVE_INFINITY)).toThrow( + /Value is not a finite number/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedInt.parseValue(Number.POSITIVE_INFINITY)).toThrow( + /Value is not a finite number/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedInt.parseLiteral({ + value: Number.POSITIVE_INFINITY, + kind: Kind.INT, + }), + ).toThrow(/Value is not a finite number/); + }); + }); + + describe('not a number', () => { + test('serialize', () => { + expect(() => UnsignedInt.serialize('not a number')).toThrow( + /Value is not a number/, + ); + }); + + test('parseValue', () => { + expect(() => UnsignedInt.parseValue('not a number')).toThrow( + /Value is not a number/, + ); + }); + + test('parseLiteral', () => { + expect(() => + UnsignedInt.parseLiteral({ + value: 'not a number', + kind: Kind.STRING, + }), + ).toThrow( + /Can only validate integers as non-negative integers but got a/, + ); + }); + }); + + describe('NaN', () => { + test('serialize', () => { + expect(() => UnsignedInt.serialize(Number.NaN)).toThrow( + /Value is not a number/, + ); + }); + + // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. + // test('parseValue', () => { + // expect(() => UnsignedInt.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); + // }); + + test('parseLiteral', () => { + expect(() => + UnsignedInt.parseLiteral({ value: Number.NaN, kind: Kind.STRING }), + ).toThrow( + /Can only validate integers as non-negative integers but got a/, + ); + }); + }); + }); +}); diff --git a/src/__tests__/NonPositiveFloat.test.js b/src/__tests__/NonPositiveFloat.test.js index 6398aa5e8..d9af922e2 100644 --- a/src/__tests__/NonPositiveFloat.test.js +++ b/src/__tests__/NonPositiveFloat.test.js @@ -81,10 +81,11 @@ describe('NonPositiveFloat', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. - // test('parseValue', () => { - // expect(() => NonPositiveFloat.parseValue(null)).toThrow(/Value is not a number/); - // }); + test('parseValue', () => { + expect(() => NonPositiveFloat.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); test('parseLiteral', () => { expect(() => @@ -102,7 +103,9 @@ describe('NonPositiveFloat', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NonPositiveFloat.parseValue(undefined)).toThrow(/Value is not a number/); + // expect(() => NonPositiveFloat.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -211,7 +214,9 @@ describe('NonPositiveFloat', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NonPositiveFloat.parseValue(Number.NaN)).toThrow(/Value is not a number/); + // expect(() => NonPositiveFloat.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { diff --git a/src/__tests__/NonPositiveInt.test.js b/src/__tests__/NonPositiveInt.test.js index 6c9087281..cd14c0cb9 100644 --- a/src/__tests__/NonPositiveInt.test.js +++ b/src/__tests__/NonPositiveInt.test.js @@ -81,10 +81,11 @@ describe('NonPositiveInt', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. - // test('parseValue', () => { - // expect(() => NonPositiveInt.parseValue(null)).toThrow(/Value is not a number/); - // }); + test('parseValue', () => { + expect(() => NonPositiveInt.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); test('parseLiteral', () => { expect(() => @@ -102,7 +103,9 @@ describe('NonPositiveInt', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NonPositiveInt.parseValue(undefined)).toThrow(/Value is not a number/); + // expect(() => NonPositiveInt.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -119,7 +122,6 @@ describe('NonPositiveInt', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. test('parseValue', () => { expect(() => NonPositiveInt.parseValue(2 ** 53)).toThrow( /Value is not a safe integer/, @@ -232,7 +234,9 @@ describe('NonPositiveInt', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => NonPositiveInt.parseValue(Number.NaN)).toThrow(/Value is not a number/); + // expect(() => NonPositiveInt.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { diff --git a/src/__tests__/PositiveFloat.test.js b/src/__tests__/PositiveFloat.test.js index 7211dc673..8af2b47b9 100644 --- a/src/__tests__/PositiveFloat.test.js +++ b/src/__tests__/PositiveFloat.test.js @@ -47,10 +47,11 @@ describe('PositiveFloat', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. - // test('parseValue', () => { - // expect(() => PositiveFloat.parseValue(null)).toThrow(/Value is not a number/); - // }); + test('parseValue', () => { + expect(() => PositiveFloat.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); test('parseLiteral', () => { expect(() => @@ -68,7 +69,9 @@ describe('PositiveFloat', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => PositiveFloat.parseValue(undefined)).toThrow(/Value is not a number/); + // expect(() => PositiveFloat.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -219,7 +222,9 @@ describe('PositiveFloat', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => PositiveFloat.parseValue(Number.NaN)).toThrow(/Value is not a number/); + // expect(() => PositiveFloat.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { diff --git a/src/__tests__/PositiveInt.test.js b/src/__tests__/PositiveInt.test.js index 0e6f898b5..50d06bcd1 100644 --- a/src/__tests__/PositiveInt.test.js +++ b/src/__tests__/PositiveInt.test.js @@ -47,10 +47,11 @@ describe('PositiveInt', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. - // test('parseValue', () => { - // expect(() => PositiveInt.parseValue(null)).toThrow(/Value is not a number/); - // }); + test('parseValue', () => { + expect(() => PositiveInt.parseValue(null)).toThrow( + /Value is not a number/, + ); + }); test('parseLiteral', () => { expect(() => @@ -68,7 +69,9 @@ describe('PositiveInt', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => PositiveInt.parseValue(undefined)).toThrow(/Value is not a number/); + // expect(() => PositiveInt.parseValue(undefined)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { @@ -85,7 +88,6 @@ describe('PositiveInt', () => { ); }); - // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. test('parseValue', () => { expect(() => PositiveInt.parseValue(2 ** 53)).toThrow( /Value is not a safe integer/, @@ -238,7 +240,9 @@ describe('PositiveInt', () => { // FIXME: Does nothing. No throw. Call doesn't even seem to get to the parseValue() function. // test('parseValue', () => { - // expect(() => PositiveInt.parseValue(Number.NaN)).toThrow(/Value is not a number/); + // expect(() => PositiveInt.parseValue(Number.NaN)).toThrow( + // /Value is not a number/, + // ); // }); test('parseLiteral', () => { diff --git a/src/__tests__/RegularExpression.test.js b/src/__tests__/RegularExpression.test.js new file mode 100644 index 000000000..3963e7772 --- /dev/null +++ b/src/__tests__/RegularExpression.test.js @@ -0,0 +1,63 @@ +/* global describe, test, expect */ + +import { Kind } from 'graphql/language'; + +import { RegularExpression } from '../'; + +describe('RegularExpression', () => { + const Abc = RegularExpression('Abc', /^abc$/); + + describe('valid', () => { + test('serialize', () => { + expect(Abc.serialize('abc')).toBe('abc'); + }); + + test('parseValue', () => { + expect(Abc.parseValue('abc')).toBe('abc'); + }); + + test('parseLiteral', () => { + expect( + Abc.parseLiteral({ value: 'abc', kind: Kind.STRING }), + ).toBe('abc'); + }); + }); + + describe('invalid', () => { + describe('does not match', () => { + test('serialize', () => { + expect(() => Abc.serialize('this does not match')).toThrow( + /Value does not match the regular expression/, + ); + }); + + test('parseValue', () => { + expect(() => Abc.parseValue('this does not match')).toThrow( + /Value does not match the regular expression/, + ); + }); + + test('parseLiteral', () => { + expect(() => + Abc.parseLiteral({ value: 'this does not match', kind: Kind.STRING }), + ).toThrow(/Value does not match the regular expression/); + }); + }); + + describe('not a string', () => { + test('serialize', () => { + expect(() => Abc.serialize(123)).toThrow(/Value is not string/); + }); + + test('parseValue', () => { + expect(() => Abc.parseValue(123)).toThrow(/Value is not string/); + }); + + test('parseLiteral', () => { + expect(() => Abc.parseLiteral({ value: 123, kind: Kind.INT })).toThrow( + /Can only validate strings as regular expressions but got a/, + ); + }); + }); + }); +}); diff --git a/src/index.js b/src/index.js index 83009d4ed..2fbeb3b18 100644 --- a/src/index.js +++ b/src/index.js @@ -11,21 +11,25 @@ import EmailAddress from './EmailAddress'; import URL from './URL'; import PhoneNumber from './PhoneNumber'; import PostalCode from './PostalCode'; +import RegularExpression from './RegularExpression'; export { DateTime, NonPositiveInt, PositiveInt, NonNegativeInt, + NonNegativeInt as UnsignedInt, NegativeInt, NonPositiveFloat, PositiveFloat, NonNegativeFloat, + NonNegativeFloat as UnsignedFloat, NegativeFloat, EmailAddress, URL, PhoneNumber, PostalCode, + RegularExpression, }; export default { @@ -33,12 +37,15 @@ export default { DateTime, PositiveInt, NonNegativeInt, + UnsignedInt: NonNegativeInt, NegativeInt, PositiveFloat, NonNegativeFloat, + UnsignedFloat: NonNegativeFloat, NegativeFloat, EmailAddress, URL, PhoneNumber, PostalCode, + RegularExpression, };