forked from hyperledger-cacti/cacti
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core-api): add createIsJwsGeneralTypeGuard, createAjvTypeGuard<T>
1. createAjvTypeGuard<T>() is the lower level utility which can be used to construct the more convenient, higher level type predicates/type guards such as createIsJwsGeneralTypeGuard() which uses createAjvTypeGuard<JwsGeneral> under the hood. 2. This commit is also meant to be establishing a larger, more generic pattern of us being able to create type guards out of the Open API specs in a convenient way instead of having to write the validation code by hand. An example usage of the new createAjvTypeGuard<T>() utility is the createIsJwsGeneralTypeGuard() function itself. An example usage of the new createIsJwsGeneralTypeGuard() can be found in packages/cactus-plugin-consortium-manual/src/main/typescript/plugin-consortium-manual.ts The code documentation contains examples as well for maximum discoverabilty and I'll also include it here: ```typescript import { JWSGeneral } from "@hyperledger/cactus-core-api"; import { createIsJwsGeneralTypeGuard } from "@hyperledger/cactus-core-api"; export class PluginConsortiumManual { private readonly isJwsGeneral: (x: unknown) => x is JWSGeneral; constructor() { // Creating the type-guard function is relatively costly due to the Ajv schema // compilation that needs to happen as part of it so it is good practice to // cache the type-guard function as much as possible, for examle by adding it // as a class member on a long-lived object such as a plugin instance which is // expected to match the life-cycle of the API server NodeJS process itself. // The specific anti-pattern would be to create a new type-guard function // for each request received by a plugin as this would affect performance // negatively. this.isJwsGeneral = createIsJwsGeneralTypeGuard(); } public async getNodeJws(): Promise<JWSGeneral> { // rest of the implementation that produces a JWS ... const jws = await joseGeneralSign.sign(); if (!this.isJwsGeneral(jws)) { throw new TypeError("Jose GeneralSign.sign() gave non-JWSGeneral type"); } return jws; } } ``` Relevant discussion took place here: https://github.com/hyperledger/cacti/pull/3471#discussion_r1731894747 Signed-off-by: Peter Somogyvari <[email protected]>
- Loading branch information
Showing
8 changed files
with
238 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
packages/cactus-core-api/src/main/typescript/open-api/create-ajv-type-guard.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import type { ValidateFunction } from "ajv"; | ||
|
||
/** | ||
* Creates a TypeScript type guard based on an `ajv` validator. | ||
* | ||
* @template T The type of the data that the validator expects. This can be | ||
* one of the data model types that we generate from the OpenAPI specifications. | ||
* It could also be a schema that you defined in your code directly, but that is | ||
* not recommended since if you are going to define a schema then it's best to | ||
* do so within the Open API specification file(s) (`openapi.tpl.json` files). | ||
* | ||
* @param {ValidateFunction<T>} validator An `ajv` validator that validates data against a specific JSON schema. | ||
* You must make sure that this parameter was indeed constructed to validate the | ||
* specific `T` type that you are intending it for. See the example below for | ||
* further details on this. | ||
* @returns {(x: unknown) => x is T} A user-defined TypeScript type guard that | ||
* checks if an unknown value matches the schema defined in the validator and | ||
* also performs the ever-useful type-narrowing which helps writing less buggy | ||
* code and enhance the compiler's ability to catch issues during development. | ||
* | ||
* @example | ||
* | ||
* ### Define a validator for the `JWSGeneral` type from the openapi.json | ||
* | ||
* ```typescript | ||
* import Ajv from "ajv"; | ||
* | ||
* import * as OpenApiJson from "../../json/openapi.json"; | ||
* import { JWSGeneral } from "../generated/openapi/typescript-axios/api"; | ||
* import { createAjvTypeGuard } from "./create-ajv-type-guard"; | ||
* | ||
* export function createIsJwsGeneral(): (x: unknown) => x is JWSGeneral { | ||
* const ajv = new Ajv(); | ||
* const validator = ajv.compile<JWSGeneral>( | ||
* OpenApiJson.components.schemas.JWSGeneral, | ||
* ); | ||
* return createAjvTypeGuard<JWSGeneral>(validator); | ||
* } | ||
* ``` | ||
* | ||
* ### Then use it elsewhere in the code for validation & type-narrowing | ||
* | ||
* ```typescript | ||
* // make sure to cache the validator you created here because it's costly to | ||
* // re-create it (in terms of hardware resources such as CPU time) | ||
* const isJWSGeneral = createAjvTypeGuard<JWSGeneral>(validateJWSGeneral); | ||
* | ||
* const data: unknown = { payload: "some-payload" }; | ||
* | ||
* if (!isJWSGeneral(data)) { | ||
* throw new TypeError('Data is not a JWSGeneral object'); | ||
* } | ||
* // Now you can safely access properties of data as a JWSGeneral object | ||
* // **without** having to perform unsafe type casting such as `as JWSGeneral` | ||
* console.log(data.payload); | ||
* console.log(data.signatures); | ||
* ``` | ||
* | ||
*/ | ||
export function createAjvTypeGuard<T>( | ||
validator: ValidateFunction<T>, | ||
): (x: unknown) => x is T { | ||
return (x: unknown): x is T => { | ||
return validator(x); | ||
}; | ||
} |
61 changes: 61 additions & 0 deletions
61
packages/cactus-core-api/src/main/typescript/open-api/create-is-jws-general-type-guard.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import Ajv from "ajv-draft-04"; | ||
import addFormats from "ajv-formats"; | ||
|
||
import * as OpenApiJson from "../../json/openapi.json"; | ||
import { JWSGeneral } from "../generated/openapi/typescript-axios/api"; | ||
import { createAjvTypeGuard } from "./create-ajv-type-guard"; | ||
|
||
/** | ||
* | ||
* @example | ||
* | ||
* ```typescript | ||
* import { JWSGeneral } from "@hyperledger/cactus-core-api"; | ||
* import { createIsJwsGeneralTypeGuard } from "@hyperledger/cactus-core-api"; | ||
* | ||
* export class PluginConsortiumManual { | ||
* private readonly isJwsGeneral: (x: unknown) => x is JWSGeneral; | ||
* | ||
* constructor() { | ||
* // Creating the type-guard function is relatively costly due to the Ajv schema | ||
* // compilation that needs to happen as part of it so it is good practice to | ||
* // cache the type-guard function as much as possible, for example by adding it | ||
* // as a class member on a long-lived object such as a plugin instance which is | ||
* // expected to match the life-cycle of the API server NodeJS process itself. | ||
* // The specific anti-pattern would be to create a new type-guard function | ||
* // for each request received by a plugin as this would affect performance | ||
* // negatively. | ||
* this.isJwsGeneral = createIsJwsGeneralTypeGuard(); | ||
* } | ||
* | ||
* public async getNodeJws(): Promise<JWSGeneral> { | ||
* // rest of the implementation that produces a JWS ... | ||
* const jws = await joseGeneralSign.sign(); | ||
* | ||
* if (!this.isJwsGeneral(jws)) { | ||
* throw new TypeError("Jose GeneralSign.sign() gave non-JWSGeneral type"); | ||
* } | ||
* return jws; | ||
* } | ||
* } | ||
* | ||
* ``` | ||
* | ||
* @returns A user-defined Typescript type-guard (which is just another function) | ||
* that is primed to do type-narrowing and runtime type-checking as well. | ||
* | ||
* @see {createAjvTypeGuard()} | ||
* @see https://www.typescriptlang.org/docs/handbook/2/narrowing.html | ||
* @see https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates | ||
*/ | ||
export function createIsJwsGeneralTypeGuard(): (x: unknown) => x is JWSGeneral { | ||
const ajv = new Ajv({ allErrors: true, strict: false }); | ||
addFormats(ajv); | ||
ajv.addSchema(OpenApiJson, "core-api"); | ||
|
||
const validator = ajv.compile<JWSGeneral>({ | ||
$ref: "core-api#/components/schemas/JWSGeneral", | ||
}); | ||
|
||
return createAjvTypeGuard<JWSGeneral>(validator); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
packages/cactus-core-api/src/test/typescript/unit/open-api/create-ajv-type-guard.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import "jest-extended"; | ||
import Ajv from "ajv-draft-04"; | ||
import addFormats from "ajv-formats"; | ||
|
||
import * as OpenApiJson from "../../../../main/json/openapi.json"; | ||
import { JWSGeneral } from "../../../../main/typescript/generated/openapi/typescript-axios/api"; | ||
import { createAjvTypeGuard } from "../../../../main/typescript/open-api/create-ajv-type-guard"; | ||
|
||
describe("createAjvTypeGuard()", () => { | ||
it("creates Generic type-guards that work", () => { | ||
const ajv = new Ajv({ allErrors: true, strict: false }); | ||
addFormats(ajv); | ||
ajv.addSchema(OpenApiJson, "core-api"); | ||
|
||
const validator = ajv.compile<JWSGeneral>({ | ||
$ref: "core-api#/components/schemas/JWSGeneral", | ||
}); | ||
|
||
const isJwsGeneral = createAjvTypeGuard<JWSGeneral>(validator); | ||
|
||
const jwsGeneralGood1: JWSGeneral = { payload: "stuff", signatures: [] }; | ||
const jwsGeneralBad1 = { payload: "stuff", signatures: {} } as JWSGeneral; | ||
const jwsGeneralBad2 = { payload: "", signatures: {} } as JWSGeneral; | ||
|
||
expect(isJwsGeneral(jwsGeneralGood1)).toBeTrue(); | ||
expect(isJwsGeneral(jwsGeneralBad1)).toBeFalse(); | ||
expect(isJwsGeneral(jwsGeneralBad2)).toBeFalse(); | ||
|
||
// verify type-narrowing to be working | ||
const jwsGeneralGood2: unknown = { payload: "stuff", signatures: [] }; | ||
if (!isJwsGeneral(jwsGeneralGood2)) { | ||
throw new Error("isJwsGeneral test misclassified valid JWSGeneral."); | ||
} | ||
expect(jwsGeneralGood2.payload).toEqual("stuff"); | ||
}); | ||
}); |
25 changes: 25 additions & 0 deletions
25
...actus-core-api/src/test/typescript/unit/open-api/create-is-jws-general-type-guard.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import "jest-extended"; | ||
|
||
import { JWSGeneral } from "../../../../main/typescript/generated/openapi/typescript-axios/api"; | ||
import { createIsJwsGeneralTypeGuard } from "../../../../main/typescript/open-api/create-is-jws-general-type-guard"; | ||
|
||
describe("createIsJwsGeneralTypeGuard()", () => { | ||
it("creates JWSGeneral type-guards that work", () => { | ||
const isJwsGeneral = createIsJwsGeneralTypeGuard(); | ||
|
||
const jwsGeneralGood1: JWSGeneral = { payload: "stuff", signatures: [] }; | ||
const jwsGeneralBad1 = { payload: "stuff", signatures: {} } as JWSGeneral; | ||
const jwsGeneralBad2 = { payload: "", signatures: {} } as JWSGeneral; | ||
|
||
expect(isJwsGeneral(jwsGeneralGood1)).toBeTrue(); | ||
expect(isJwsGeneral(jwsGeneralBad1)).toBeFalse(); | ||
expect(isJwsGeneral(jwsGeneralBad2)).toBeFalse(); | ||
|
||
// verify type-narrowing to be working | ||
const jwsGeneralGood2: unknown = { payload: "stuff", signatures: [] }; | ||
if (!isJwsGeneral(jwsGeneralGood2)) { | ||
throw new Error("isJwsGeneral test misclassified valid JWSGeneral."); | ||
} | ||
expect(jwsGeneralGood2.payload).toEqual("stuff"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters