Skip to content

Commit

Permalink
No dependency runtime validators (#37)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
NandaScott authored Sep 9, 2024
1 parent 9d653a4 commit c16cdfb
Show file tree
Hide file tree
Showing 9 changed files with 3,971 additions and 656 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/jest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: Jest CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install modules
run: npm i
- name: Run tests
run: npm run test
6 changes: 6 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
modulePaths: ["<rootDir>"],
};
4,479 changes: 3,824 additions & 655 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"qa": "npm run check:types && npm run lint && npm run test",
"lint": "eslint",
"test": "",
"test": "jest",
"check:types": "tsc --noEmit"
},
"files": [
Expand All @@ -30,10 +30,13 @@
"typescript"
],
"devDependencies": {
"@types/jest": "^29.5.12",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"eslint": "^8.51.0",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"ts-jest": "^29.1.5",
"tslib": "^2.6.2",
"typescript": "^5.2.2"
}
Expand Down
35 changes: 35 additions & 0 deletions src/__tests__/objects/Catalog/Catalog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ScryfallCatalog } from "src/objects";
import CatalogValidator from "src/validators/objects/Catalog/CatalogValidator";

const cardNamesRequest: Promise<ScryfallCatalog> = fetch("https://api.scryfall.com/catalog/card-names").then((resp) =>
resp.json(),
);

describe("Catalog", () => {
test("has expected fields", async () => {
const cardNamesCatalog = await cardNamesRequest;
const goodValidator = new CatalogValidator(cardNamesCatalog);

expect(goodValidator.validKeys).toBe(true);
});

test("expected fields are expected type", async () => {
const cardNamesCatalog = await cardNamesRequest;
const validator = new CatalogValidator(cardNamesCatalog);
expect(validator.validKeyType).toBe(true);
});

test("has no unexpected fields", async () => {
const cardNamesCatalog = await cardNamesRequest;
const mockKeyUpdates = { ...cardNamesCatalog, notAKey: true };
const validator = new CatalogValidator(mockKeyUpdates);
expect(validator.validKeys).toBe(false);
});

test("total_values matches data length", async () => {
const cardNamesCatalog = await cardNamesRequest;
const validator = new CatalogValidator(cardNamesCatalog);
const dataLength = validator.validDataLength;
expect(dataLength).toBe(true);
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./objects";
export * from "./validators";
24 changes: 24 additions & 0 deletions src/validators/BaseValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Represents the most basic needs for a Validator class.
* @abstract
* @class BaseValidator
*/
export default abstract class BaseValidator<T extends Record<string, unknown> = Record<string, unknown>> {
/** The object passed to the Validator */
object: T;
/** A list of keys we expect to exist in a given object. */
abstract expectedKeys: string[];

/**
* @constructor
* @param {object} object The object to test against.
*/
constructor(object: T) {
this.object = object;
}

/** The keys of the object */
get keys(): Array<keyof typeof this.object> {
return Object.keys(this.object);
}
}
1 change: 1 addition & 0 deletions src/validators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as BaseValidator } from "./BaseValidator";
65 changes: 65 additions & 0 deletions src/validators/objects/Catalog/CatalogValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ScryfallCatalog } from "src/objects";
import { BaseValidator } from "src/validators";

/**
* CatalogValidator
*
* @extends BaseValidator
*/
export default class CatalogValidator extends BaseValidator {
expectedKeys: string[] = <Array<keyof ScryfallCatalog>>["object", "uri", "total_values", "data"];

/**
* @static
* @param {object} obj An object to check
* @returns {boolean} true if the object passes all expected checks
*/
static isCatalogObject(obj: Record<string, unknown>): obj is ScryfallCatalog {
const validator = new CatalogValidator(obj);

return validator.validKeys && validator.validKeyType && validator.validDataType;
}

/**
* true if the object matches all expected keys
* @type {boolean}
*/
get validKeys(): boolean {
return this.keys.every((val) => this.expectedKeys.includes(val));
}

/**
* true if the all keys are of the expected type
* @type {boolean}
* */
get validKeyType(): boolean {
const objectIsCatalog = this.object.object === "catalog";
const uriIsString = typeof this.object.uri === "string";
const totalValsIsNumber = typeof this.object.total_values === "number";
const dataIsStringArray = this.validDataType;
return objectIsCatalog && uriIsString && totalValsIsNumber && dataIsStringArray;
}

/**
* true if the 'data' field is the correct type
* @type {boolean}
*/
get validDataType(): boolean {
if (!Array.isArray(this.object.data)) throw new Error("data is not an array");

const isJSObject = typeof this.object.data === "object";
const isArray = Array.isArray(this.object.data);
const onlyHasStrings = this.object.data.every((val) => typeof val === "string");
return isJSObject && isArray && onlyHasStrings;
}

/**
* true if the 'data' field length matches the 'total_values' number
* @type {boolean}
*/
get validDataLength(): boolean {
if (!Array.isArray(this.object.data)) throw new Error("data is not an array");

return this.object.data.length === this.object.total_values;
}
}

0 comments on commit c16cdfb

Please sign in to comment.