Skip to content

Commit

Permalink
Added types related to Library and Patent Classifications (#1817)
Browse files Browse the repository at this point in the history
* Added types related to common library classifications

* deleted lint report

* Added comments to LCCSubclass to make purpose of regex clearer

* Added types and tests related to the International Patent Classification Standard

* Changeset

---------

Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
  • Loading branch information
cmhhelgeson and ardatan committed Mar 24, 2023
1 parent bca280f commit 39e1890
Show file tree
Hide file tree
Showing 11 changed files with 518 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/curly-lizards-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-scalars': minor
---

Add new scalar types related to Library and Patent Classifications
17 changes: 17 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ import {
GraphQLAccountNumber,
GraphQLCuid,
GraphQLSemVer,
GraphQLDeweyDecimal,
GraphQLLCCSubclass,
GraphQLIPCPatent,
} from './scalars/index.js';
import { GraphQLDuration } from './scalars/iso-date/Duration.js';

Expand Down Expand Up @@ -125,6 +128,9 @@ export {
AccountNumber as AccountNumberDefinition,
Cuid as CuidDefinition,
SemVer as SemVerDefinition,
DeweyDecimal as DeweyDecimalDefinition,
LCCSubclass as LCCSubclassDefinition,
IPCPatent as IPCPatentDefinition,
} from './typeDefs.js';

export { typeDefs } from './typeDefs.js';
Expand Down Expand Up @@ -191,6 +197,8 @@ export {
GraphQLAccountNumber as AccountNumberResolver,
GraphQLCuid as CuidResolver,
GraphQLSemVer as SemVerResolver,
GraphQLDeweyDecimal as GraphQLDeweyDecimalResolver,
GraphQLIPCPatent as GraphQLIPCPatentResolver,
};

export const resolvers: Record<string, GraphQLScalarType> = {
Expand Down Expand Up @@ -255,6 +263,9 @@ export const resolvers: Record<string, GraphQLScalarType> = {
AccountNumber: GraphQLAccountNumber,
Cuid: GraphQLCuid,
SemVer: GraphQLSemVer,
DeweyDecimal: GraphQLDeweyDecimal,
LCCSubclass: GraphQLLCCSubclass,
IPCPatent: GraphQLIPCPatent,
};

export {
Expand Down Expand Up @@ -319,6 +330,9 @@ export {
AccountNumber as AccountNumberMock,
Cuid as CuidMock,
SemVer as SemVerMock,
DeweyDecimal as DeweyDecimalMock,
LCCSubclass as LCCSubclassMock,
IPCPatent as IPCPatentMock,
} from './mocks.js';

export { mocks };
Expand Down Expand Up @@ -387,4 +401,7 @@ export {
GraphQLAccountNumber,
GraphQLCuid,
GraphQLSemVer,
GraphQLDeweyDecimal,
GraphQLLCCSubclass,
GraphQLIPCPatent,
};
3 changes: 3 additions & 0 deletions src/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ export const RoutingNumber = () => '111000025';
export const AccountNumber = () => '000000012345';
export const Cuid = () => 'cjld2cyuq0000t3rmniod1foy';
export const SemVer = () => '1.0.0-alpha.1';
export const DeweyDecimal = () => '435.4357';
export const LCCSubclass = () => 'KBM';
export const IPCPatent = () => 'G06F 12/803';

export {
DateMock as Date,
Expand Down
3 changes: 3 additions & 0 deletions src/scalars/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ export { GraphQLRoutingNumber } from './RoutingNumber.js';
export { GraphQLAccountNumber } from './AccountNumber.js';
export { GraphQLCuid } from './Cuid.js';
export { GraphQLSemVer } from './SemVer.js';
export { GraphQLDeweyDecimal } from './library/DeweyDecimal.js';
export { GraphQLLCCSubclass } from './library/LCCSubclass.js';
export { GraphQLIPCPatent } from './patent/IPCPatent.js';
48 changes: 48 additions & 0 deletions src/scalars/library/DeweyDecimal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { GraphQLScalarTypeConfig, ASTNode, Kind, GraphQLScalarType } from 'graphql';
import { createGraphQLError } from '../../error.js';

const DEWEY_DECIMAL_REGEX = /^[0-9]{1,3}(?:\.[0-9]+)?$/;

const validate = (value: any, ast?: ASTNode) => {
if (typeof value !== 'string') {
throw createGraphQLError(`Value is not string: ${value}`, { nodes: ast });
}

if (!DEWEY_DECIMAL_REGEX.test(value)) {
throw createGraphQLError(`Value is not a valid Dewey Decimal Number: ${value}`, { nodes: ast });
}
return value;
};

const specifiedByURL = 'https://www.oclc.org/content/dam/oclc/dewey/resources/summaries/deweysummaries.pdf';

export const GraphQLDeweyDecimalConfig = {
name: 'DeweyDecimal',

description: `A field whose value conforms to the standard DeweyDecimal format as specified by the OCLC https://www.oclc.org/content/dam/oclc/dewey/resources/summaries/deweysummaries.pdf`,

serialize: validate,

parseValue: validate,

parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw createGraphQLError(`Can only validate strings as DeweyDecimal but got a: ${ast.kind}`, { nodes: ast });
}

return validate(ast.value, ast);
},

specifiedByURL,
specifiedByUrl: specifiedByURL,
extensions: {
codegenScalarType: 'string',
jsonSchema: {
title: 'DeweyDecimal',
type: 'string',
pattern: DEWEY_DECIMAL_REGEX.source,
},
},
} as GraphQLScalarTypeConfig<string, string>;

export const GraphQLDeweyDecimal: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType(GraphQLDeweyDecimalConfig);
52 changes: 52 additions & 0 deletions src/scalars/library/LCCSubclass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { GraphQLScalarType, GraphQLScalarTypeConfig, ASTNode, Kind } from 'graphql';
import { createGraphQLError } from '../../error.js';

//Regex for the various letter subclasses of the Library of Congress Classification
//As defined in the pdfs available by clicking on the letters at this link
//https://www.loc.gov/catdir/cpso/lcco/
const LCC_SUBCLASS_PREFIX =
/^((AC|AE|AG|AI|AM|AN|AP|AS|AY|AZ)|[B][CDFHJLMPQRSTVX]{0,1}|[C][BCDEJNRST]{0,1}|[D][AWBCDEFGH]{0,1}|[E]|[F]|[G][ABCEFNRTV]{0,1}|[H][ABCDEFGJMNQSTVX]{0,1}|[J][ACFJKLNQSVXZ]{0,1}|(K|KB[M,P,R,U]|KD[C,E,G,K,Z]{0,1}|KE[ABMNOPQSYZ]{0,1}|KF[ACDFGHIKLMNOPRSTUVWXZ]{0,1}|KG[ABCDEFGHJKLMNPQRSTUVWXYZ]{0,1}|KH[ACDFHKLMNPQSUW]{0,1}|KJ[ACEGHJKMNPRSTVW]{0,1}|KK[ABEFGHIJKLMNPQRSTVWXYZ]{0,1}|KL[ABDEFHMNPQRSTVW]{0,1}|KM[CEFGHJKLMNPQSTUVXY]{0,1}|KN[CEFGHKLMNPQRSTUVWXY]{0,1}|KP[ACEFGHJKLMPSTVW]{0,1}|KQ[CEGHJKMPTVWX]{0,1}|KR[BCEGKLMNPRSUVWXY]{0,1}|KS[ACEGHKLNPRSTUVWXYZ]{0,1}|KT[ACDEFGHJKLNQRTUVWXYZ]{0,1}|KU[ABCDEFGHNQ]{0,1}|KV[BCEHLMNPQRSUW]{0,1}|KW[ACEGHLPQRTWX]{0,1}|KZ[AD]{0,1})|[L][ABCDEFGHJT]{0,1}|[M][LT]{0,1}|[N][ABCDEKX]{0,1}|[P][ABCDEFGHJKLMNQRSTZ]{0,1}|[Q][ABCDEHKLMPR]{0,1}|[R][ABCDEFGJKLMSTVXZ]{0,1}|[S][BDFHK]{0,1}|[T][ACDEFGHJKLNPRSTX]{0,1}|[U][ABCDEFGH]{0,1}|[V][ABCDEFGKM]{0,1}|[Z][A]{0,1})$/;

const validate = (value: any, ast?: ASTNode) => {
if (typeof value !== 'string') {
throw createGraphQLError(`Value is not string: ${value}`, { nodes: ast });
}

if (!LCC_SUBCLASS_PREFIX.test(value)) {
throw createGraphQLError(`Value is not a valid LCC Subclass: ${value}`, { nodes: ast });
}
return value;
};

const specifiedByURL = 'https://www.loc.gov/catdir/cpso/lcco/';

export const GraphQLLCCSubclassConfig = {
name: 'LCCSubclass',

description: `A field whose value conforms to the Library of Congress Subclass Format ttps://www.loc.gov/catdir/cpso/lcco/`,

serialize: validate,

parseValue: validate,

parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw createGraphQLError(`Can only validate strings as LCC Subclasses but got a: ${ast.kind}`, { nodes: ast });
}

return validate(ast.value, ast);
},

specifiedByURL,
specifiedByUrl: specifiedByURL,
extensions: {
codegenScalarType: 'string',
jsonSchema: {
title: 'DeweyDecimal',
type: 'string',
pattern: LCC_SUBCLASS_PREFIX.source,
},
},
} as GraphQLScalarTypeConfig<string, string>;

export const GraphQLLCCSubclass: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType(GraphQLLCCSubclassConfig);
57 changes: 57 additions & 0 deletions src/scalars/patent/IPCPatent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { GraphQLScalarTypeConfig, ASTNode, Kind, GraphQLScalarType } from 'graphql';
import { createGraphQLError } from '../../error.js';

/* 1. [A-H] represents the Section Level of the Classification
2. \d{2} represents the class level
3. [A-Z] represents the subclass level
4. \/ separates the subclass from the subgroup
5. \d{2,4} represents the subgroup level
(Only four levels of subgroup, as far as I know)
*/
const IPC_PATENT_REGEX = /^[A-H]\d{2}[A-Z] \d{1,2}\/\d{2,4}$/;

const validate = (value: any, ast?: ASTNode) => {
if (typeof value !== 'string') {
throw createGraphQLError(`Value is not string: ${value}`, { nodes: ast });
}

if (!IPC_PATENT_REGEX.test(value)) {
throw createGraphQLError(`Value is not a valid IPC Class Symbol: ${value}`, { nodes: ast });
}
return value;
};

const specifiedByURL = 'https://www.wipo.int/classifications/ipc/en/';

export const GraphQLIPCPatentConfig = {
name: 'IPCPatentClassification',

description: `A field whose value is an IPC Class Symbol within the International Patent Classification System: https://www.wipo.int/classifications/ipc/en/`,

serialize: validate,

parseValue: validate,

parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw createGraphQLError(`Can only validate strings as an IPC Class Symbol but got a: ${ast.kind}`, {
nodes: ast,
});
}

return validate(ast.value, ast);
},

specifiedByURL,
specifiedByUrl: specifiedByURL,
extensions: {
codegenScalarType: 'string',
jsonSchema: {
title: 'DeweyDecimal',
type: 'string',
pattern: IPC_PATENT_REGEX.source,
},
},
} as GraphQLScalarTypeConfig<string, string>;

export const GraphQLIPCPatent: GraphQLScalarType = /*#__PURE__*/ new GraphQLScalarType(GraphQLIPCPatentConfig);
8 changes: 8 additions & 0 deletions src/typeDefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export const DID = 'scalar DID';
export const CountryCode = 'scalar CountryCode';
export const Locale = 'scalar Locale';

export const DeweyDecimal = 'scalar DeweyDecimal';
export const LCCSubclass = 'scalar LCCSubclass';

export const IPCPatent = 'scalar IPCPatent';

export const typeDefs = [
Date,
Time,
Expand Down Expand Up @@ -124,4 +129,7 @@ export const typeDefs = [
AccountNumber,
Cuid,
SemVer,
DeweyDecimal,
LCCSubclass,
IPCPatent,
];
98 changes: 98 additions & 0 deletions tests/DeweyDecimal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* global describe, test, expect */
import { Kind } from 'graphql/language';
import { GraphQLDeweyDecimal } from '../src/scalars/library/DeweyDecimal.js';

describe('DID', () => {
describe('valid - Dewey Decimal', () => {
test('serialize', () => {
expect(GraphQLDeweyDecimal.serialize('1')).toBe('1');
expect(GraphQLDeweyDecimal.serialize('1.2345')).toBe('1.2345');
expect(GraphQLDeweyDecimal.serialize('01')).toBe('01');
expect(GraphQLDeweyDecimal.serialize('01.2345')).toBe('01.2345');
expect(GraphQLDeweyDecimal.serialize('001')).toBe('001');
expect(GraphQLDeweyDecimal.serialize('001.2345')).toBe('001.2345');
expect(GraphQLDeweyDecimal.serialize('10')).toBe('10');
expect(GraphQLDeweyDecimal.serialize('10.2345')).toBe('10.2345');
expect(GraphQLDeweyDecimal.serialize('010')).toBe('010');
expect(GraphQLDeweyDecimal.serialize('010.2345')).toBe('010.2345');
expect(GraphQLDeweyDecimal.serialize('100')).toBe('100');
expect(GraphQLDeweyDecimal.serialize('100.2345')).toBe('100.2345');
});

test('parseValue', () => {
expect(GraphQLDeweyDecimal.parseValue('1')).toBe('1');
expect(GraphQLDeweyDecimal.parseValue('1.2345')).toBe('1.2345');
expect(GraphQLDeweyDecimal.parseValue('01')).toBe('01');
expect(GraphQLDeweyDecimal.parseValue('01.2345')).toBe('01.2345');
expect(GraphQLDeweyDecimal.parseValue('001')).toBe('001');
expect(GraphQLDeweyDecimal.parseValue('001.2345')).toBe('001.2345');
expect(GraphQLDeweyDecimal.parseValue('10')).toBe('10');
expect(GraphQLDeweyDecimal.parseValue('10.2345')).toBe('10.2345');
expect(GraphQLDeweyDecimal.parseValue('010')).toBe('010');
expect(GraphQLDeweyDecimal.parseValue('010.2345')).toBe('010.2345');
expect(GraphQLDeweyDecimal.parseValue('100')).toBe('100');
expect(GraphQLDeweyDecimal.parseValue('100.2345')).toBe('100.2345');
});

test('parseLiteral', () => {
expect(GraphQLDeweyDecimal.parseLiteral({ value: '1', kind: Kind.STRING }, {})).toBe('1');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '1.2345', kind: Kind.STRING }, {})).toBe('1.2345');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '01', kind: Kind.STRING }, {})).toBe('01');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '01.2345', kind: Kind.STRING }, {})).toBe('01.2345');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '001', kind: Kind.STRING }, {})).toBe('001');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '001.2345', kind: Kind.STRING }, {})).toBe('001.2345');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '10', kind: Kind.STRING }, {})).toBe('10');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '10.2345', kind: Kind.STRING }, {})).toBe('10.2345');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '010', kind: Kind.STRING }, {})).toBe('010');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '010.2345', kind: Kind.STRING }, {})).toBe('010.2345');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '100', kind: Kind.STRING }, {})).toBe('100');
expect(GraphQLDeweyDecimal.parseLiteral({ value: '100.2345', kind: Kind.STRING }, {})).toBe('100.2345');
});
});

describe('invalid', () => {
describe('not a Dewey Decimal', () => {
expect(() => GraphQLDeweyDecimal.serialize('invaliddid')).toThrow(/Value is not a valid Dewey Decimal Number/);
});

test(`parseValue invaliddidexample`, () => {
expect(() => GraphQLDeweyDecimal.parseValue('invaliddidexample')).toThrow(
/Value is not a valid Dewey Decimal Number/
);
});

test(`parseLiteral invaliddidexample`, () => {
expect(() => GraphQLDeweyDecimal.parseLiteral({ value: 'invaliddidexample', kind: Kind.STRING }, {})).toThrow(
/Value is not a valid Dewey Decimal Number/
);
});
});

describe('not a string', () => {
test('serialize', () => {
expect(() => GraphQLDeweyDecimal.serialize(123)).toThrow();
});

test('parseValue', () => {
expect(() => GraphQLDeweyDecimal.parseValue(123)).toThrow();
});

test('parseLiteral', () => {
expect(() => GraphQLDeweyDecimal.parseLiteral({ value: '123', kind: Kind.INT }, {})).toThrow();
});
});

describe('not a empty string', () => {
test('serialize', () => {
expect(() => GraphQLDeweyDecimal.serialize('')).toThrow();
});

test('parseValue', () => {
expect(() => GraphQLDeweyDecimal.parseValue('')).toThrow();
});

test('parseLiteral', () => {
expect(() => GraphQLDeweyDecimal.parseLiteral({ value: '', kind: Kind.STRING }, {})).toThrow();
});
});
});

0 comments on commit 39e1890

Please sign in to comment.