Skip to content

Commit 6a56915

Browse files
committed
feat: add PostalCode scalar
1 parent 6c85960 commit 6a56915

File tree

3 files changed

+387
-0
lines changed

3 files changed

+387
-0
lines changed

src/PostalCode.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { GraphQLScalarType } from 'graphql';
2+
import { GraphQLError } from 'graphql/error';
3+
import { Kind } from 'graphql/language';
4+
5+
// We're going to start with a limited set as suggested here:
6+
// http://www.pixelenvision.com/1708/zip-postal-code-validation-regex-php-code-for-12-countries/
7+
// and here:
8+
// https://stackoverflow.com/questions/578406/what-is-the-ultimate-postal-code-and-zip-regex
9+
//
10+
// Which gives us the following countries:
11+
//
12+
// US - United States
13+
// UK - United Kingdom
14+
// DE - Germany
15+
// CA - Canada
16+
// FR - France
17+
// IT - Italy
18+
// AU - Australia
19+
// NL - Netherlands
20+
// ES - Spain
21+
// DK - Denmark
22+
// SE - Sweden
23+
// BE - Belgium
24+
// IN - India
25+
//
26+
// This is really a practical decision of weight (of the package) vs. completeness.
27+
//
28+
// In the future we might expand this list and use the more comprehensive list found here:
29+
// http://unicode.org/cldr/trac/browser/tags/release-26-0-1/common/supplemental/postalCodeData.xml
30+
31+
// prettier-ignore
32+
const POSTAL_CODE_REGEXES = [
33+
/* US */ new RegExp(/^\d{5}([-]?\d{4})?$/),
34+
/* UK */ new RegExp(/^(GIR|[A-Z]\d[A-Z\d]??|[A-Z]{2}\d[A-Z\d]??)[ ]??(\d[A-Z]{2})$/),
35+
/* DE */ new RegExp(/\b((?:0[1-46-9]\d{3})|(?:[1-357-9]\d{4})|(?:[4][0-24-9]\d{3})|(?:[6][013-9]\d{3}))\b/),
36+
/* CA */ new RegExp(/^([ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVWXYZ]) {0,1}(\d[ABCEGHJKLMNPRSTVWXYZ]\d)$/),
37+
/* FR */ new RegExp(/^(F-)?((2[A|B])|[0-9]{2})[0-9]{3}$/),
38+
/* IT */ new RegExp(/^(V-|I-)?[0-9]{5}$/),
39+
/* AU */ new RegExp(/^(0[289][0-9]{2})|([1345689][0-9]{3})|(2[0-8][0-9]{2})|(290[0-9])|(291[0-4])|(7[0-4][0-9]{2})|(7[8-9][0-9]{2})$/),
40+
/* NL */ new RegExp(/^[1-9][0-9]{3}\s?([a-zA-Z]{2})?$/),
41+
/* ES */ new RegExp(/^([1-9]{2}|[0-9][1-9]|[1-9][0-9])[0-9]{3}$/),
42+
/* DK */ new RegExp(/^([D|d][K|k]( |-))?[1-9]{1}[0-9]{3}$/),
43+
/* SE */ new RegExp(/^(s-|S-){0,1}[0-9]{3}\s?[0-9]{2}$/),
44+
/* BE */ new RegExp(/^[1-9]{1}[0-9]{3}$/),
45+
/* IN */ new RegExp(/^\d{6}$/),
46+
];
47+
48+
function _testPostalCode(postalCode) {
49+
let result = false;
50+
51+
// eslint-disable-next-line no-plusplus
52+
for (let i = 0; i < POSTAL_CODE_REGEXES.length; i++) {
53+
const regex = POSTAL_CODE_REGEXES[i];
54+
55+
if (regex.test(postalCode)) {
56+
result = true;
57+
break;
58+
}
59+
}
60+
61+
return result;
62+
}
63+
64+
export default new GraphQLScalarType({
65+
name: 'PostalCode',
66+
67+
description:
68+
'A field whose value conforms to the standard postal code formats for United States, United Kingdom, Germany, Canada, France, Italy, Australia, Netherlands, Spain, Denmark, Sweden, Belgium or India.',
69+
70+
serialize(value) {
71+
if (typeof value !== 'string') {
72+
throw new TypeError(`Value is not string: ${value}`);
73+
}
74+
75+
if (!_testPostalCode(value)) {
76+
throw new TypeError(`Value is not a valid postal code: ${value}`);
77+
}
78+
79+
return value;
80+
},
81+
82+
parseValue(value) {
83+
if (typeof value !== 'string') {
84+
throw new TypeError(`Value is not string: ${value}`);
85+
}
86+
87+
if (!_testPostalCode(value)) {
88+
throw new TypeError(`Value is not a valid postal code: ${value}`);
89+
}
90+
91+
return value;
92+
},
93+
94+
parseLiteral(ast) {
95+
if (ast.kind !== Kind.STRING) {
96+
throw new GraphQLError(
97+
`Can only validate strings as phone numbers but got a: ${ast.kind}`,
98+
);
99+
}
100+
101+
if (!_testPostalCode(ast.value)) {
102+
throw new TypeError(`Value is not a valid postal code: ${ast.value}`);
103+
}
104+
105+
return ast.value;
106+
},
107+
});

src/__tests__/PostalCode.test.js

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
/* global describe, test, expect */
2+
3+
import { Kind } from 'graphql/language';
4+
5+
import { PostalCode } from '../';
6+
7+
describe('PostalCode', () => {
8+
describe('valid', () => {
9+
describe('United States', () => {
10+
describe('basic', () => {
11+
test('serialize', () => {
12+
expect(PostalCode.serialize('60031')).toBe('60031');
13+
});
14+
15+
test('parseValue', () => {
16+
expect(PostalCode.parseValue('60031')).toBe('60031');
17+
});
18+
19+
test('parseLiteral', () => {
20+
expect(
21+
PostalCode.parseLiteral({ value: '60031', kind: Kind.STRING }),
22+
).toBe('60031');
23+
});
24+
});
25+
describe('plus 4', () => {
26+
test('serialize', () => {
27+
expect(PostalCode.serialize('60031-1234')).toBe('60031-1234');
28+
});
29+
30+
test('parseValue', () => {
31+
expect(PostalCode.parseValue('60031-1234')).toBe('60031-1234');
32+
});
33+
34+
test('parseLiteral', () => {
35+
expect(
36+
PostalCode.parseLiteral({ value: '60031-1234', kind: Kind.STRING }),
37+
).toBe('60031-1234');
38+
});
39+
});
40+
});
41+
42+
// TODO: the rest of the countries
43+
describe('United Kingdom', () => {
44+
// Hi Paul, John, George and Ringo
45+
test('serialize', () => {
46+
expect(PostalCode.serialize('NW8 9AY')).toBe('NW8 9AY');
47+
});
48+
49+
test('parseValue', () => {
50+
expect(PostalCode.parseValue('NW8 9AY')).toBe('NW8 9AY');
51+
});
52+
53+
test('parseLiteral', () => {
54+
expect(
55+
PostalCode.parseLiteral({ value: 'NW8 9AY', kind: Kind.STRING }),
56+
).toBe('NW8 9AY');
57+
});
58+
});
59+
60+
describe('Germany', () => {
61+
test('serialize', () => {
62+
expect(PostalCode.serialize('10318')).toBe('10318');
63+
});
64+
65+
test('parseValue', () => {
66+
expect(PostalCode.parseValue('10318')).toBe('10318');
67+
});
68+
69+
test('parseLiteral', () => {
70+
expect(
71+
PostalCode.parseLiteral({ value: '10318', kind: Kind.STRING }),
72+
).toBe('10318');
73+
});
74+
});
75+
76+
describe('Canada', () => {
77+
test('serialize', () => {
78+
expect(PostalCode.serialize('M5T 1G2')).toBe('M5T 1G2');
79+
});
80+
81+
test('parseValue', () => {
82+
expect(PostalCode.parseValue('M5T 1G2')).toBe('M5T 1G2');
83+
});
84+
85+
test('parseLiteral', () => {
86+
expect(
87+
PostalCode.parseLiteral({ value: 'M5T 1G2', kind: Kind.STRING }),
88+
).toBe('M5T 1G2');
89+
});
90+
});
91+
92+
describe('France', () => {
93+
// Hi Xavier!
94+
test('serialize', () => {
95+
expect(PostalCode.serialize('34000')).toBe('34000');
96+
});
97+
98+
test('parseValue', () => {
99+
expect(PostalCode.parseValue('34000')).toBe('34000');
100+
});
101+
102+
test('parseLiteral', () => {
103+
expect(
104+
PostalCode.parseLiteral({ value: '34000', kind: Kind.STRING }),
105+
).toBe('34000');
106+
});
107+
});
108+
109+
describe('Italy', () => {
110+
// Ahhh...Venice!
111+
test('serialize', () => {
112+
expect(PostalCode.serialize('30123')).toBe('30123');
113+
});
114+
115+
test('parseValue', () => {
116+
expect(PostalCode.parseValue('30123')).toBe('30123');
117+
});
118+
119+
test('parseLiteral', () => {
120+
expect(
121+
PostalCode.parseLiteral({ value: '30123', kind: Kind.STRING }),
122+
).toBe('30123');
123+
});
124+
});
125+
126+
describe('Australia', () => {
127+
// Charles says, "Hi Mom."
128+
test('serialize', () => {
129+
expect(PostalCode.serialize('4217')).toBe('4217');
130+
});
131+
132+
test('parseValue', () => {
133+
expect(PostalCode.parseValue('4217')).toBe('4217');
134+
});
135+
136+
test('parseLiteral', () => {
137+
expect(
138+
PostalCode.parseLiteral({ value: '4217', kind: Kind.STRING }),
139+
).toBe('4217');
140+
});
141+
});
142+
143+
describe('Netherlands', () => {
144+
test('serialize', () => {
145+
expect(PostalCode.serialize('1011 AC')).toBe('1011 AC');
146+
});
147+
148+
test('parseValue', () => {
149+
expect(PostalCode.parseValue('1011 AC')).toBe('1011 AC');
150+
});
151+
152+
test('parseLiteral', () => {
153+
expect(
154+
PostalCode.parseLiteral({ value: '1011 AC', kind: Kind.STRING }),
155+
).toBe('1011 AC');
156+
});
157+
});
158+
159+
describe('Spain', () => {
160+
test('serialize', () => {
161+
expect(PostalCode.serialize('28009')).toBe('28009');
162+
});
163+
164+
test('parseValue', () => {
165+
expect(PostalCode.parseValue('28009')).toBe('28009');
166+
});
167+
168+
test('parseLiteral', () => {
169+
expect(
170+
PostalCode.parseLiteral({ value: '28009', kind: Kind.STRING }),
171+
).toBe('28009');
172+
});
173+
});
174+
175+
describe('Denmark', () => {
176+
test('serialize', () => {
177+
expect(PostalCode.serialize('2100')).toBe('2100');
178+
});
179+
180+
test('parseValue', () => {
181+
expect(PostalCode.parseValue('2100')).toBe('2100');
182+
});
183+
184+
test('parseLiteral', () => {
185+
expect(
186+
PostalCode.parseLiteral({ value: '2100', kind: Kind.STRING }),
187+
).toBe('2100');
188+
});
189+
});
190+
191+
describe('Sweden', () => {
192+
test('serialize', () => {
193+
expect(PostalCode.serialize('114 55')).toBe('114 55');
194+
});
195+
196+
test('parseValue', () => {
197+
expect(PostalCode.parseValue('114 55')).toBe('114 55');
198+
});
199+
200+
test('parseLiteral', () => {
201+
expect(
202+
PostalCode.parseLiteral({ value: '114 55', kind: Kind.STRING }),
203+
).toBe('114 55');
204+
});
205+
});
206+
207+
describe('Belgium', () => {
208+
test('serialize', () => {
209+
expect(PostalCode.serialize('1043')).toBe('1043');
210+
});
211+
212+
test('parseValue', () => {
213+
expect(PostalCode.parseValue('1043')).toBe('1043');
214+
});
215+
216+
test('parseLiteral', () => {
217+
expect(
218+
PostalCode.parseLiteral({ value: '1043', kind: Kind.STRING }),
219+
).toBe('1043');
220+
});
221+
});
222+
223+
describe('India', () => {
224+
test('serialize', () => {
225+
expect(PostalCode.serialize('110003')).toBe('110003');
226+
});
227+
228+
test('parseValue', () => {
229+
expect(PostalCode.parseValue('110003')).toBe('110003');
230+
});
231+
232+
test('parseLiteral', () => {
233+
expect(
234+
PostalCode.parseLiteral({ value: '110003', kind: Kind.STRING }),
235+
).toBe('110003');
236+
});
237+
});
238+
});
239+
240+
describe('invalid', () => {
241+
describe('not a phone number', () => {
242+
test('serialize', () => {
243+
expect(() => PostalCode.serialize('this is not a phone number')).toThrow(
244+
/^Value is not a valid postal code/,
245+
);
246+
});
247+
248+
test('parseValue', () => {
249+
expect(() => PostalCode.parseValue('this is not a phone number')).toThrow(
250+
/^Value is not a valid postal code/,
251+
);
252+
});
253+
254+
test('parseLiteral', () => {
255+
expect(() =>
256+
PostalCode.parseLiteral({ value: 'this is not a phone number', kind: Kind.STRING }),
257+
).toThrow(/^Value is not a valid postal code/);
258+
});
259+
});
260+
261+
describe('not a string', () => {
262+
test('serialize', () => {
263+
expect(() => PostalCode.serialize(123)).toThrow(/Value is not string/);
264+
});
265+
266+
test('parseValue', () => {
267+
expect(() => PostalCode.parseValue(123)).toThrow(/Value is not string/);
268+
});
269+
270+
test('parseLiteral', () => {
271+
expect(() => PostalCode.parseLiteral({ value: 123, kind: Kind.INT })).toThrow(
272+
/Can only validate strings as phone numbers but got a/,
273+
);
274+
});
275+
});
276+
});
277+
});

0 commit comments

Comments
 (0)