From 07365d310d66936d5d192b7cd55b34e1108c426b Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 11 Jun 2020 14:25:33 -0400 Subject: [PATCH] some tests for the schema --- README.md | 8 +- package.json | 1 + src/index.ts | 7 +- src/isVersionUpdate.ts | 18 ++++ src/tokenlist.schema.json | 66 +++++++++------ test/isVersionUpdate.test.ts | 84 +++++++++++++++++++ test/{ => testschemas}/example.tokenlist.json | 6 +- test/tokenlist.schema.test.ts | 48 +++++++++++ test/validateTokenList.test.ts | 7 -- tsconfig.json | 1 + 10 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 src/isVersionUpdate.ts create mode 100644 test/isVersionUpdate.test.ts rename test/{ => testschemas}/example.tokenlist.json (87%) create mode 100644 test/tokenlist.schema.test.ts delete mode 100644 test/validateTokenList.test.ts diff --git a/README.md b/README.md index ad6cbcc5..5f74b19b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # @uniswap/token-lists -This package includes a JSON schema for token lists, and TypeScript for -validating token lists against the schema. +This package includes a JSON schema for token lists, and TypeScript utilities for working with token lists. + +## Validating token lists + +This package does not include token list validation. You can easily do this by including a library such as +[ajv](https://ajv.js.org/) to perform the validation against the JSON schema. ## Local Development diff --git a/package.json b/package.json index 1e4f3d53..fabc474a 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ }, "module": "dist/token-lists.esm.js", "devDependencies": { + "ajv": "^6.12.2", "husky": "^4.2.5", "tsdx": "^0.13.2", "tslib": "^2.0.0", diff --git a/src/index.ts b/src/index.ts index 2da86d86..203f9736 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ -import { TokenList } from 'types'; +import schema from './tokenlist.schema.json'; export * from './types'; +export * from './isVersionUpdate'; -export function validateTokenList(list: unknown): list is TokenList { - return typeof list === 'object'; -} +export { schema }; diff --git a/src/isVersionUpdate.ts b/src/isVersionUpdate.ts new file mode 100644 index 00000000..18642a3b --- /dev/null +++ b/src/isVersionUpdate.ts @@ -0,0 +1,18 @@ +import { Version } from './types'; + +/** + * Returns true if versionB is an update over versionA + */ +export function isVersionUpdate(versionA: Version, versionB: Version): boolean { + if (versionB.major > versionA.major) { + return true; + } + if (versionB.major < versionA.major) { + return false; + } + if (versionB.minor > versionA.minor) { + return true; + } + if (versionB.minor < versionA.minor) return false; + return versionB.patch > versionA.patch; +} diff --git a/src/tokenlist.schema.json b/src/tokenlist.schema.json index 32a99f62..1f4a54d0 100644 --- a/src/tokenlist.schema.json +++ b/src/tokenlist.schema.json @@ -1,27 +1,27 @@ { - "$schema": "https://json-schema.org/draft-07/schema#", - "$id": "https://uniswap.org/token-list.schema.json", - "title": "Uniswap Token List", - "description": "A list of tokens compatible with the Uniswap Interface", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://uniswap.org/token-lists.schema.json", + "title": "Uniswap Token Lists", + "description": "Schema for lists of tokens compatible with the Uniswap Interface", "$ref": "#/definitions/TokenList", "definitions": { "Version": { "type": "object", - "description": "A version number for a piece of data, useful for change detection", + "description": "A version number for a piece of data, used in list change detection", "properties": { "major": { "type": "integer", - "description": "The major version of the list. Recommend incrementing major version when tokens are removed.", - "minimum": 1 + "description": "The major version of the list. Must be incremented when tokens are removed from the list or token addresses are changed.", + "minimum": 0 }, "minor": { "type": "integer", - "description": "The minor version of the list. Recommend incrementing minor version when tokens are added.", + "description": "The minor version of the list. Must be incremented when tokens are added to the list.", "minimum": 0 }, "patch": { "type": "integer", - "description": "The patch version of the list. Recommend incrementing for changes to tokens.", + "description": "The patch version of the list. Must be incremented for any changes to the list.", "minimum": 0 } }, @@ -31,9 +31,16 @@ "patch" ] }, + "TagIdentifier": { + "type": "string", + "description": "The identifier of a tag", + "minLength": 1, + "maxLength": 10, + "pattern": "^[\\w]+$" + }, "TagDefinition": { "type": "object", - "description": "Tag definition", + "description": "Definition of a tag that can be associated with a token", "properties": { "name": { "type": "string", @@ -44,7 +51,7 @@ }, "description": { "type": "string", - "description": "The user-friendly description of the tag", + "description": "A user-friendly description of the tag", "pattern": "^[\\s\\w\\.]+$", "minLength": 1, "maxLength": 200 @@ -57,40 +64,49 @@ }, "TokenInfo": { "type": "object", - "description": "Information about a single token", + "description": "Information about a single token on the token list", "properties": { "chainId": { "type": "integer", - "description": "The chain ID of the chain where this token lives", + "description": "The chain ID of the Ethereum network where this token is deployed", "minimum": 1 }, "address": { "type": "string", - "description": "The address of the token", + "description": "The address of the token on the given chain ID", "pattern": "^0x[a-fA-F0-9]{40}$" }, "decimals": { "type": "integer", - "description": "How many decimals the token has. Defaults to on-chain data", + "description": "The number of decimals for the token balance; if not set, defaults to on-chain data", "minimum": 0, "maximum": 78 }, "name": { "type": "string", - "description": "The name of the token. Defaults to on-chain data", - "minLength": 1 + "description": "The name of the token; if not set, defaults to on-chain data", + "minLength": 1, + "pattern": "^[\\s\\w]+$" }, "symbol": { "type": "string", - "description": "The symbol for the token. Defaults to on-chain data", + "description": "The symbol for the token; must be alphanumeric; if not set, defaults to on-chain data", "pattern": "^[a-zA-Z0-9]+$", "minLength": 1, "maxLength": 20 }, "logoUrl": { "type": "string", - "description": "A URL to the token description", + "description": "A URL to the token logo asset; if not set, interface will attempt to find a logo based on the address", "pattern": "^(https|ipfs|ipns)://.+$" + }, + "tags": { + "type": "array", + "description": "An array of tag identifiers associated with the token", + "items": { + "$ref": "#/definitions/TagIdentifier" + }, + "maxLength": 10 } }, "required": [ @@ -105,11 +121,13 @@ "type": "string", "description": "The name of the token list", "minLength": 1, - "maxLength": 20 + "maxLength": 20, + "pattern": "^[\\w\\s]+$" }, "timestamp": { "type": "integer", - "description": "The epoch seconds timestamp of the list version" + "description": "The epoch seconds timestamp of the list version; must be after June 11, 2020", + "minimum": 1591833600 }, "version": { "$ref": "#/definitions/Version" @@ -125,7 +143,7 @@ }, "keywords": { "type": "array", - "description": "Keywords associated with the contents of the list", + "description": "Keywords associated with the contents of the list; may be used in list discoverability", "items": { "type": "string", "minLength": 1, @@ -136,9 +154,9 @@ }, "tags": { "type": "object", - "description": "A mapping of tag identifiers to their name and description.", + "description": "A mapping of tag identifiers to their name and description; tag identifier keys are slugs", "propertyNames": { - "pattern": "^\\d+$" + "$ref": "#/definitions/TagIdentifier" }, "additionalProperties": { "$ref": "#/definitions/TagDefinition" diff --git a/test/isVersionUpdate.test.ts b/test/isVersionUpdate.test.ts new file mode 100644 index 00000000..d4faf7d2 --- /dev/null +++ b/test/isVersionUpdate.test.ts @@ -0,0 +1,84 @@ +import { isVersionUpdate } from '../src'; + +describe('#isVersionUpdate', () => { + it('major version increase', () => { + expect( + isVersionUpdate( + { major: 1, minor: 0, patch: 0 }, + { major: 2, minor: 0, patch: 0 } + ) + ).toEqual(true); + expect( + isVersionUpdate( + { major: 1, minor: 0, patch: 0 }, + { major: 1, minor: 0, patch: 0 } + ) + ).toEqual(false); + expect( + isVersionUpdate( + { major: 1, minor: 0, patch: 0 }, + { major: 0, minor: 0, patch: 0 } + ) + ).toEqual(false); + expect( + isVersionUpdate( + { major: 1, minor: 1, patch: 0 }, + { major: 2, minor: 0, patch: 0 } + ) + ).toEqual(true); + }); + + it('minor version increase', () => { + expect( + isVersionUpdate( + { major: 1, minor: 0, patch: 0 }, + { major: 1, minor: 1, patch: 0 } + ) + ).toEqual(true); + expect( + isVersionUpdate( + { major: 1, minor: 0, patch: 0 }, + { major: 1, minor: 0, patch: 0 } + ) + ).toEqual(false); + expect( + isVersionUpdate( + { major: 1, minor: 1, patch: 0 }, + { major: 1, minor: 0, patch: 0 } + ) + ).toEqual(false); + expect( + isVersionUpdate( + { major: 1, minor: 1, patch: 1 }, + { major: 1, minor: 2, patch: 0 } + ) + ).toEqual(true); + }); + + it('patch version', () => { + expect( + isVersionUpdate( + { major: 1, minor: 0, patch: 0 }, + { major: 1, minor: 1, patch: 0 } + ) + ).toEqual(true); + expect( + isVersionUpdate( + { major: 1, minor: 0, patch: 0 }, + { major: 1, minor: 0, patch: 0 } + ) + ).toEqual(false); + expect( + isVersionUpdate( + { major: 1, minor: 1, patch: 0 }, + { major: 1, minor: 0, patch: 0 } + ) + ).toEqual(false); + expect( + isVersionUpdate( + { major: 1, minor: 1, patch: 1 }, + { major: 1, minor: 2, patch: 0 } + ) + ).toEqual(true); + }); +}); diff --git a/test/example.tokenlist.json b/test/testschemas/example.tokenlist.json similarity index 87% rename from test/example.tokenlist.json rename to test/testschemas/example.tokenlist.json index eb993c53..bd130a81 100644 --- a/test/example.tokenlist.json +++ b/test/testschemas/example.tokenlist.json @@ -7,7 +7,7 @@ "description": "blah blah" } }, - "timestamp": 100, + "timestamp": 1591897594, "tokens": [ { "name": "blah", @@ -19,8 +19,8 @@ } ], "version": { - "major": 1, + "major": 0, "minor": 0, - "patch": 0 + "patch": 1 } } \ No newline at end of file diff --git a/test/tokenlist.schema.test.ts b/test/tokenlist.schema.test.ts new file mode 100644 index 00000000..2fd5a51d --- /dev/null +++ b/test/tokenlist.schema.test.ts @@ -0,0 +1,48 @@ +import Ajv from 'ajv'; +import { schema } from '../src'; +import example from './testschemas/example.tokenlist.json'; +const ajv = new Ajv({ allErrors: true }); + +const validator = ajv.compile(schema); +describe('schema', () => { + it('is valid', () => { + expect(ajv.validateSchema(schema)).toEqual(true); + }); + it('requires name, timestamp, version, tokens', () => { + expect(validator({})).toEqual(false); + expect(validator.errors).toEqual([ + { + keyword: 'required', + dataPath: '', + schemaPath: '#/required', + params: { missingProperty: 'name' }, + message: "should have required property 'name'", + }, + { + keyword: 'required', + dataPath: '', + schemaPath: '#/required', + params: { missingProperty: 'timestamp' }, + message: "should have required property 'timestamp'", + }, + { + keyword: 'required', + dataPath: '', + schemaPath: '#/required', + params: { missingProperty: 'version' }, + message: "should have required property 'version'", + }, + { + keyword: 'required', + dataPath: '', + schemaPath: '#/required', + params: { missingProperty: 'tokens' }, + message: "should have required property 'tokens'", + }, + ]); + }); + it('works for example schema', () => { + expect(validator(example)).toEqual(true); + expect(validator.errors).toBeNull(); + }); +}); diff --git a/test/validateTokenList.test.ts b/test/validateTokenList.test.ts deleted file mode 100644 index 9a4cf4bb..00000000 --- a/test/validateTokenList.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { validateTokenList } from '../src'; - -describe('#validateTokenList', () => { - it('works', () => { - expect(validateTokenList(undefined)).toEqual(false); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 1e79b510..eaf3600a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, "moduleResolution": "node", "baseUrl": "./", "paths": {