Skip to content

Commit c16cdfb

Browse files
authored
No dependency runtime validators (#37)
* Added jest for automated testing * Created BaseValidator and CatalogValidator classes * Updated exports * Created basic jest test for Catalog objects * Added github action to run jest tests
1 parent 9d653a4 commit c16cdfb

File tree

9 files changed

+3971
-656
lines changed

9 files changed

+3971
-656
lines changed

.github/workflows/jest.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: Jest CI
2+
on: push
3+
jobs:
4+
build:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v2
8+
- name: Install modules
9+
run: npm i
10+
- name: Run tests
11+
run: npm run test

jest.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: "ts-jest",
4+
testEnvironment: "node",
5+
modulePaths: ["<rootDir>"],
6+
};

package-lock.json

Lines changed: 3824 additions & 655 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"qa": "npm run check:types && npm run lint && npm run test",
88
"lint": "eslint",
9-
"test": "",
9+
"test": "jest",
1010
"check:types": "tsc --noEmit"
1111
},
1212
"files": [
@@ -30,10 +30,13 @@
3030
"typescript"
3131
],
3232
"devDependencies": {
33+
"@types/jest": "^29.5.12",
3334
"@typescript-eslint/eslint-plugin": "^6.7.4",
3435
"@typescript-eslint/parser": "^6.7.4",
3536
"eslint": "^8.51.0",
37+
"jest": "^29.7.0",
3638
"prettier": "^3.0.3",
39+
"ts-jest": "^29.1.5",
3740
"tslib": "^2.6.2",
3841
"typescript": "^5.2.2"
3942
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ScryfallCatalog } from "src/objects";
2+
import CatalogValidator from "src/validators/objects/Catalog/CatalogValidator";
3+
4+
const cardNamesRequest: Promise<ScryfallCatalog> = fetch("https://api.scryfall.com/catalog/card-names").then((resp) =>
5+
resp.json(),
6+
);
7+
8+
describe("Catalog", () => {
9+
test("has expected fields", async () => {
10+
const cardNamesCatalog = await cardNamesRequest;
11+
const goodValidator = new CatalogValidator(cardNamesCatalog);
12+
13+
expect(goodValidator.validKeys).toBe(true);
14+
});
15+
16+
test("expected fields are expected type", async () => {
17+
const cardNamesCatalog = await cardNamesRequest;
18+
const validator = new CatalogValidator(cardNamesCatalog);
19+
expect(validator.validKeyType).toBe(true);
20+
});
21+
22+
test("has no unexpected fields", async () => {
23+
const cardNamesCatalog = await cardNamesRequest;
24+
const mockKeyUpdates = { ...cardNamesCatalog, notAKey: true };
25+
const validator = new CatalogValidator(mockKeyUpdates);
26+
expect(validator.validKeys).toBe(false);
27+
});
28+
29+
test("total_values matches data length", async () => {
30+
const cardNamesCatalog = await cardNamesRequest;
31+
const validator = new CatalogValidator(cardNamesCatalog);
32+
const dataLength = validator.validDataLength;
33+
expect(dataLength).toBe(true);
34+
});
35+
});

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./objects";
2+
export * from "./validators";

src/validators/BaseValidator.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Represents the most basic needs for a Validator class.
3+
* @abstract
4+
* @class BaseValidator
5+
*/
6+
export default abstract class BaseValidator<T extends Record<string, unknown> = Record<string, unknown>> {
7+
/** The object passed to the Validator */
8+
object: T;
9+
/** A list of keys we expect to exist in a given object. */
10+
abstract expectedKeys: string[];
11+
12+
/**
13+
* @constructor
14+
* @param {object} object The object to test against.
15+
*/
16+
constructor(object: T) {
17+
this.object = object;
18+
}
19+
20+
/** The keys of the object */
21+
get keys(): Array<keyof typeof this.object> {
22+
return Object.keys(this.object);
23+
}
24+
}

src/validators/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as BaseValidator } from "./BaseValidator";
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { ScryfallCatalog } from "src/objects";
2+
import { BaseValidator } from "src/validators";
3+
4+
/**
5+
* CatalogValidator
6+
*
7+
* @extends BaseValidator
8+
*/
9+
export default class CatalogValidator extends BaseValidator {
10+
expectedKeys: string[] = <Array<keyof ScryfallCatalog>>["object", "uri", "total_values", "data"];
11+
12+
/**
13+
* @static
14+
* @param {object} obj An object to check
15+
* @returns {boolean} true if the object passes all expected checks
16+
*/
17+
static isCatalogObject(obj: Record<string, unknown>): obj is ScryfallCatalog {
18+
const validator = new CatalogValidator(obj);
19+
20+
return validator.validKeys && validator.validKeyType && validator.validDataType;
21+
}
22+
23+
/**
24+
* true if the object matches all expected keys
25+
* @type {boolean}
26+
*/
27+
get validKeys(): boolean {
28+
return this.keys.every((val) => this.expectedKeys.includes(val));
29+
}
30+
31+
/**
32+
* true if the all keys are of the expected type
33+
* @type {boolean}
34+
* */
35+
get validKeyType(): boolean {
36+
const objectIsCatalog = this.object.object === "catalog";
37+
const uriIsString = typeof this.object.uri === "string";
38+
const totalValsIsNumber = typeof this.object.total_values === "number";
39+
const dataIsStringArray = this.validDataType;
40+
return objectIsCatalog && uriIsString && totalValsIsNumber && dataIsStringArray;
41+
}
42+
43+
/**
44+
* true if the 'data' field is the correct type
45+
* @type {boolean}
46+
*/
47+
get validDataType(): boolean {
48+
if (!Array.isArray(this.object.data)) throw new Error("data is not an array");
49+
50+
const isJSObject = typeof this.object.data === "object";
51+
const isArray = Array.isArray(this.object.data);
52+
const onlyHasStrings = this.object.data.every((val) => typeof val === "string");
53+
return isJSObject && isArray && onlyHasStrings;
54+
}
55+
56+
/**
57+
* true if the 'data' field length matches the 'total_values' number
58+
* @type {boolean}
59+
*/
60+
get validDataLength(): boolean {
61+
if (!Array.isArray(this.object.data)) throw new Error("data is not an array");
62+
63+
return this.object.data.length === this.object.total_values;
64+
}
65+
}

0 commit comments

Comments
 (0)