diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index e4a26a3..e1babea 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -27,7 +27,7 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - - run: npm install + - run: npm ci - run: npm run codegen - run: npm run build - run: npm test diff --git a/README.md b/README.md index 4560c60..ae86c17 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,15 @@ # `typed-ocpp` -A library for type-aware parsing, serialization and validation of OCPP 1.6-J -and OCPP 2.0.1 messages, built against the official [JSON Schema][i2] documents -published by the [Open Charge Alliance][i1]. +A library for fast, type-aware validation of OCPP 1.6-J and OCPP 2.0.1 +messages, built against the official [JSON Schema][i2] documents published +by the [Open Charge Alliance][i1]. [i1]: https://openchargealliance.org [i2]: https://json-schema.org ## Usage -### `setAjv()` - -This library requires an instance of `Ajv` with support for string formats, -as provided by the `ajv-formats` plugin. See [https://npm.im/ajv][a1] and -[https://npm.im/ajv-formats][a2]. - -```typescript -import Ajv from 'ajv'; -import formats from 'ajv-formats'; -import { setAjv } from 'typed-ocpp'; - -setAjv(formats(new Ajv())); -``` - -[a1]: https://npm.im/ajv -[a2]: https://npm.im/ajv-formats - ### `OCPP16` and `OCPP20` namespaces This library exports all functions and typings related to OCPP 1.6 and @@ -40,125 +23,140 @@ Both namespaces export identical APIs while typings and schemas differ according to the differences in the respective OCPP versions. All of the examples below apply to both namespaces. -### `parse()` +### `validate()` -The `parse()` function return a fully-typed and validated "view" of the -original array. No additional transformation is applied beside the eventual -`JSON.parse()` if a string is provided. +The `validate()` function is a [user-defined, validating type guard][v1] which +returns `true`if the provided value is a spec-compliant OCPP message and +`false` otherwise. ```typescript import { OCPP16 } from 'typed-ocpp'; - -const raw = '[2,"test","BootNotification",{"chargePointModel":"model","chargePointVendor":"vendor"}]'; -const parsed = OCPP.parse(raw); - -Array.isArray(parsed); // true -``` - -```typescript -import { OCPP20 } from 'typed-ocpp'; - -const raw = '[2,"test","BootNotification",{ "chargingStation": { "model": "test", "vendorName": "test" }, "reason": "PowerUp"}]'; -const parsed = OCPP.parse(raw); - -Array.isArray(parsed); // true +const value = [2,"test","BootNotification",{"chargePointModel":"model","chargePointVendor":"vendor"}]; +if (OCPP16.validate(value)) { + // valid +} ``` -Values returned by `parse()` have one of the following types: - -```typescript -OCPP16.Call // union of all types for Call messages -OCPP16.CallError // type for Call Error messages -OCPP16.UncheckedCallResult // generic type for Call Result messages -``` +If `validate()` returns `true`, the TS compiler will infer the provided value +to be of one of the following types: ```typescript -OCPP20.Call // union of all types for Call messages -OCPP20.CallError // type for Call Error messages -OCPP20.UncheckedCallResult // generic type for Call Result messages +OCPP16.Call // Union of all types of Call messages +OCPP16.CallError // Type for Call Error messages +OCPP16.UncheckedCallResult // Type for "unchecked" Call Result messages ``` -As returned values are fully-typed, the TS compiler can use known types to -infer others: +The TS compiler will then be able to use known types to infer others: ```typescript -if (OCPP16.isCall(parsed)) { - parsed[2]; // TS gives type "OCPP.Action" - if (parsed[2] === OCPP16.Action.BootNotification) { - // TS infers the shape of the call payload based on the action - parsed[3].chargePointModel; // TS gives type "string" - parsed[3].randomProp; // TS compilation error +const value = [2,"test","BootNotification",{"chargePointModel":"model","chargePointVendor":"vendor"}]; +if (OCPP16.validate(value)) { + if (OCPP16.isCall(value)) { + value[2]; // TS gives type "OCPP.Action" + if (value[2] === OCPP16.Action.BootNotification) { + // TS infers the shape of the call payload based on the action + value[3].chargePointModel; // TS gives type "string" + value[3].randomProp; // TS compilation error + } } } ``` -### `isCall()`, `isCallResult()` and `isCallError()` - -The `isCall()`, `isCallResult()` and `isCallError()` functions are utility type -guards that facilitates identifying the type of a parsed message: +If validation fails and `validate()` returns `false`, validation errors will +be stored in the `validate.errors` array: ```typescript -const parsed = OCPP16.parse(message); - -if (OCPP16.isCall(parsed)) { - // TS infers that parsed is of type OCPP16.Call +const value = 'foobar'; +if (!OCPP16.validate(value)) { + // prints: [ 'Invalid OCPP message: invalid message type or not an array' ] + console.log(OCPP16.validate.errors); } +``` -if (OCPP16.isCallResult(message)) { - // TS infers that parsed is of type OCPP16.UncheckedCallResult -} +[v1]: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates -if (OCPP16.isCallError(message)) { -// TS infers that parsed is of type OCPP16.CallError -} -``` +### `validateCall()`, `validateCallError()` and `validateCallResult()` -### `checkCallResult()` +The `validateCall()`, `validateCallError()` and `validateCallResult()` +functions are user-defined, validating type guards specific to each type of +message defined by the OCPP specs: **_call_**, **_call error_** and +**_call result_**. -Call Result messages are returned by `parse()` as the `UncheckedCallResult` -type and must be checked against the originating `Call` messages for further -validation and type-awareness. +These functions behave in the same way as the `validate()` function but only +return `true` when provided with values of the corresponding message type. +Validation errors can be retrieved via the `.errors` property, just as with +`validate()`. -If `checkCallResult()` does not throw the returned values are guaranteed to be -valid `CallResult` objects _matching the provided `Call` objects_: +### `isCall()`, `isCallResult()` and `isCallError()` + +The `isCall()`, `isCallResult()` and `isCallError()` functions are +user-defined, _non-validating_ type guards that facilitate identifying the type +of a valid message: ```typescript -const call = '[2,"test","BootNotification",{"chargePointModel":"model","chargePointVendor":"vendor"}]'; -const result = '[3, "test", { status: "Accepted", currentTime: "1970-01-01T00:00:00.000Z", interval: 10 }]'; - -const parsedCall = OCPP16.parse(call); -const parsedResult = OCPP16.parse(result); - -if (OCPP16.isCall(parsedCall)) { // Narrows parsedCall to OCPP16.Call - if (OCPP16.isCallResult(parsedResult)) { // Narrows parsedResult to OCPP16.UncheckedCallResult - if (parsedCall[2] === OCPP16.Action.BootNotification) { // Narrows parsedCall to OCPP16.BootNotificationCall - const checkedResult = OCPP16.checkCallResult( // checkedResult inferred as having type - parsedResult, parsedCall // OCPP16.BootNotificationCallResult - ); - checkedResult[2].status; // Inferred as "Accepted" | "Pending" | "Rejected" - } +if (OCPP16.validate(message)) { + if (OCPP16.isCall(message)) { + // TS infers that parsed is of type OCPP16.Call + } + if (OCPP16.isCallResult(message)) { + // TS infers that parsed is of type OCPP16.UncheckedCallResult + } + if (OCPP16.isCallError(message)) { + // TS infers that parsed is of type OCPP16.CallError } } ``` -The `checkCallResult()` function returns values of the generic type -`CheckedCallResult` which resolves to the type of Call Result -message that corresponds to the type of Call messages provided as the `C` type -argument. `CheckedCallResult` may also be used on its own to -model Call Result messages matching a specific Call message. See below. +### `checkCallResult()` -### `stringify()` +Post-validation, **_call result_** messages are inferred by the TS compiler to +be of the `UncheckedCallResult` type, which is a generic type that does not +constrain the **_call result_**'s payload to any specific shape as doing so +requires matching against the originating **_call_** message. -Returns the JSON serialization of the provided OCPP object. +Complete validation of a **_call result_** message against its originating +**_call_** message can be done through the `checkCallResult()` user-defined, +validating type guard. -```typescript -const serialized = OCPP16.stringify(parsed); -``` +If `checkCallResult()` returns `true`, the provided **_call result_** message +is guaranteed to match the provided **_call_** message in terms of: + +- Matching the expected type, incl. the payload (for example: being of type + `OCPP16.BootNotificationCallResult` given an originating + `OCPP16.BootNotificationCall` essage). +- Sharing the same _call identifier_ with the originating **_call_** message. ```typescript -const serialized = OCPP20.stringify(parsed); +const call = [ + 2, + "test", + "BootNotification", + {"chargePointModel":"model","chargePointVendor":"vendor"}, +] satisfies OCPP16.Call; + +const result = [ + 3, + "test", + { status: "Accepted", currentTime: "1970-01-01T00:00:00.000Z", interval: 10 }, +] satisfies OCPP16.UncheckedCallResult; + +// Narrows the type of `call` to `OCPP16.BootNotificationCall` +if (call[2] === OCPP16.Action.BootNotification) { + // Validates `result` against `call`, narrowing the type of + // `result` to `OCPP16.BootNotificationCallResult` + if (OCPP16.checkCallResult(result, call)) { + // Inferred as "Accepted" | "Pending" | "Rejected" + result[2].status; + } else { + // The `result` message does not match the originating `call` message + console.log(OCPP16.checkCallResult.errors); + } +} ``` +Just like `validate()`, when `checkCallResult()` returns `false` it stores +validation errors in the `checkCallResult.errors` array. + ### Types #### Primary types @@ -166,52 +164,53 @@ const serialized = OCPP20.stringify(parsed); Within both the `OCPP16` and `OCPP20` namespaces, `typed-ocpp` provides a set of typings and schemas that covers most aspects of OCPP messages. -We've already mentioned the primary types returned by `parse()` and -`checkCallResult()`: - ```typescript -OCPP16.Call // union type of all Call message types -OCPP16.CallResult // union type of all Call Result message types -OCPP16.CallError // type of Call Error messages -OCPP16.UncheckedCallResult // type of unchecked Call Result messages -OCPP16.CheckedCallResult< // generic type of that resolves to the specific - C extends OCPP16.Call> // type of Call Result message matching the - // provided type of Call message "C" -``` +// Union of all types of Call messages +OCPP16.Call -```typescript -OCPP20.Call // union type of all Call message types -OCPP20.CallResult // union type of all Call Result message types -OCPP20.CallError // type of Call Error messages -OCPP20.UncheckedCallResult // type of unchecked Call Result messages -OCPP20.CheckedCallResult< // generic type of that resolves to the specific - C extends OCPP20.Call> // type of Call Result message matching the - // provided type of Call message "C" -``` +// Union of all Call Result message types +OCPP16.CallResult -#### Message-specific types +// Type for Call Error messages +OCPP16.CallError -Specific types for _Call_ and _Call Result_ messages use the `Call` and -`CallResult` suffixes: `AuthorizeCall`, `AuthorizeCallResult`, -`MeterValuesCall`, `MeterValuesCallResult` and so on: +// Type for "unchecked" Call Result messages +OCPP16.UncheckedCallResult -```typescript -OCPP16.MeterValuesCall -OCPP16.MeterValuesCallResult +// Generic type of Call Result message that resolves to the specific type of +// Call Result message matching the provided type of Call message "C" +OCPP16.CheckedCallResult + +// Message-specific types +OCPP16.AuthorizationCall +OCPP16.AuthorizationCallResult +OCPP16.BootNotificationCall +OCPP16.BootNotificationCallResult /* ... */ -``` +``` + +#### Types for specific messages + +Specific types for **_Call_** and **_Call Result_** messages making up the +`OCPP16.Call` and `OCPP16.CallResult` unions use the `Call` and `CallResult` +suffixes: ```typescript -OCPP20.MeterValuesCall -OCPP20.MeterValuesCallResult -/* ... */ +OCPP16.MeterValuesCall +OCPP16.MeterValuesCallResult +/* ... and so on ...*/ ``` #### The `CheckedCallResult` type +When returning `true`, the `checkCallResult()` function leads the TS compiler +to infer the generic type `CheckedCallResult`, which resolves +to the specific type of **_call result_** message that corresponds to the type +of **_call_** message provided as the `C` type argument. + The generic `CheckedCallResult` type can also be used on its -own to model a Call Result message after a known or inferred type of Call -message: +own to model a **_call result_** message after a known or inferred type of +**_call_** message: ```typescript const result: OCPP16.CheckedCallResult = [ @@ -230,8 +229,6 @@ const result: OCPP16.CheckedCallResult = [ #### Utility enums -The following enumerations are used within these primary types: - ```typescript OCPP16.MessageType // enum of message types (CALL = 2, CALLRESULT = 3, CALLERROR = 4) OCPP16.Action // enum of actions in Call messages ("Authorize", "BootNotification", ...) diff --git a/package-lock.json b/package-lock.json index b1dd18b..db99b01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,21 @@ { "name": "typed-ocpp", - "version": "0.4.3", + "version": "1.0.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "typed-ocpp", - "version": "0.4.3", + "version": "1.0.0-beta.0", "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1" + }, "devDependencies": { - "@types/node": "^20.14.2", - "ajv": "^8.16.0", - "ajv-formats": "^3.0.1", - "json-schema-to-typescript": "^14.0.5", - "typescript": "^5.4.5" + "@types/node": "^22.5.4", + "json-schema-to-typescript": "^15.0.2", + "typescript": "^5.5.4" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -73,30 +75,29 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/ajv": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", - "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", - "dev": true, + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -107,7 +108,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, "dependencies": { "ajv": "^8.0.0" }, @@ -144,12 +144,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -171,22 +165,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/cli-color": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", - "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.64", - "es6-iterator": "^2.0.3", - "memoizee": "^0.4.15", - "timers-ext": "^0.1.7" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -219,28 +197,6 @@ "node": ">= 8" } }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", - "dev": true, - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -253,120 +209,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", - "dev": true, - "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dev": true, - "dependencies": { - "type": "^2.7.2" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" }, "node_modules/foreground-child": { "version": "3.1.1", @@ -384,18 +235,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dev": true, - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/glob": { "version": "10.3.15", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", @@ -448,12 +287,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -491,23 +324,19 @@ } }, "node_modules/json-schema-to-typescript": { - "version": "14.0.5", - "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-14.0.5.tgz", - "integrity": "sha512-JmHsbgY0KKo8Pw0HRXpGzAlZYxlu+M5kFhSzhNkUSrVJ4sCXPdAGIdSpzva5ev2/Kybz10S6AfnNdF4o3Pzt3A==", + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.2.tgz", + "integrity": "sha512-+cRBw+bBJ3k783mZroDIgz1pLNPB4hvj6nnbHTWwEVl0dkW8qdZ+M9jWhBb+Y0FAdHvNsXACga3lewGO8lktrw==", "dev": true, "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.5.5", "@types/json-schema": "^7.0.15", - "@types/lodash": "^4.17.0", - "cli-color": "^2.0.4", + "@types/lodash": "^4.17.7", "glob": "^10.3.12", "is-glob": "^4.0.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimist": "^1.2.8", - "mkdirp": "^3.0.1", - "mz": "^2.7.0", - "node-fetch": "^3.3.2", "prettier": "^3.2.5" }, "bin": { @@ -520,8 +349,7 @@ "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/lodash": { "version": "4.17.21", @@ -538,31 +366,6 @@ "node": "14 || >=16.14" } }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", - "dev": true, - "dependencies": { - "es5-ext": "~0.10.2" - } - }, - "node_modules/memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" - } - }, "node_modules/minimatch": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", @@ -596,84 +399,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dev": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -714,20 +439,10 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -861,47 +576,10 @@ "node": ">=8" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/timers-ext": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", - "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", - "dev": true, - "dependencies": { - "es5-ext": "~0.10.46", - "next-tick": "1" - } - }, - "node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", - "dev": true - }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -912,29 +590,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index e8493ab..5226d52 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,17 @@ { "name": "typed-ocpp", - "version": "0.4.3", + "version": "1.0.0-beta.0", "description": "A library for type-aware parsing, serialization and validation of OCPP 1.6 and OCPP 2.0.1 messages", "main": "dist/index.js", "type": "module", "devDependencies": { - "@types/node": "^20.14.2", - "ajv": "^8.16.0", - "ajv-formats": "^3.0.1", - "json-schema-to-typescript": "^14.0.5", - "typescript": "^5.4.5" + "@types/node": "^22.5.4", + "json-schema-to-typescript": "^15.0.2", + "typescript": "^5.5.4" + }, + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1" }, "scripts": { "test": "node --test --test-reporter=spec ./dist", @@ -47,12 +49,12 @@ "EV", "EVSE", "Central Station", + "Charging Station", "ajv", "validation", - "serialization", - "parsing", "typescript", "typed", + "types", "JSON", "JSON Schema", "Open Charge Alliance", diff --git a/src/common/ajv.test.ts b/src/common/ajv.test.ts deleted file mode 100644 index a2392ab..0000000 --- a/src/common/ajv.test.ts +++ /dev/null @@ -1,7 +0,0 @@ - -import Ajv from 'ajv'; -import formats from 'ajv-formats'; - -import { setAjv } from './ajv.js'; - -setAjv(formats(new Ajv())); diff --git a/src/common/ajv.ts b/src/common/ajv.ts index 7f5f59c..4e690b5 100644 --- a/src/common/ajv.ts +++ b/src/common/ajv.ts @@ -1,34 +1,25 @@ -let __ajv: any = null; +import type { JSONSchemaType } from 'ajv'; -const isAjv = (ajv: any): boolean => { - return !!ajv - && typeof ajv === 'object' - && typeof ajv.validate === 'function' - ; -}; +import Ajv from 'ajv'; +import addFormats from 'ajv-formats'; +import { EMPTY_ARR } from './utils.js'; -export const setAjv = (ajv: any) => { - if (!isAjv(ajv)) { - throw new Error(` - The argument provided to \`setAjv()\` does not look like an instance of - Ajv. See https://npm.im/typed-ocpp. - `); - } - __ajv = ajv; -}; +const ajv = addFormats(new Ajv()); -export const getAjv = (): any => { - if (!isAjv(__ajv)) { - throw new Error(` - The typed-ocpp library requires an instance of Ajv (https://npm.im/ajv) - extended with the ajv-formats plugin (https://npm.im/ajv-formats) to be - provided using \`setAjv()\`. See https://npm.im/typed-ocpp. - `); - } - return __ajv; -}; +export interface AjvValidateFn { + (value: any, schema: JSONSchemaType, prefix: string): value is T; + errors: string[]; +} -export const ajvErrorsToString = (ajv: any): string => { - return ajv.errors?.map((e: any) => `${e.instancePath} ${e.message}`).join(', ') || ''; -}; +export const validate: AjvValidateFn = Object.assign( + (value: any, schema: JSONSchemaType, prefix: string): value is T => { + if (ajv.validate(schema, value)) { + validate.errors = EMPTY_ARR; + return true; + } + validate.errors = ajv.errors!.map((e: any) => `${prefix}: ${e.instancePath} ${e.message}`); + return false; + }, + { errors: EMPTY_ARR }, +) ; diff --git a/src/common/ensure.ts b/src/common/ensure.ts deleted file mode 100644 index 7f4f4c2..0000000 --- a/src/common/ensure.ts +++ /dev/null @@ -1,31 +0,0 @@ - -const ensure = (item: any, predicate: boolean, message: string): T => { - if (!predicate) { - throw new Error(message); - } - return item as T; -}; - -export const array = (item: any, message: string): T => { - return ensure(item, Array.isArray(item), message); -}; - -export const length = (item: T, length: number, message: string): T => { - return ensure(item, item.length === length, message); -}; - -export const object = (item: any, message: string): T => { - return ensure(item, typeof item === 'object' && item !== null, message); -}; - -export const string = (item: any, message: string): T => { - return ensure(item, typeof item === 'string', message); -}; - -export const keyOf = (key: any, dict: T, message: string): K => { - return ensure(key, key in dict, message); -}; - -export const equal = (item: any, compare: T, message: string): T => { - return ensure(item, item === compare, message); -}; diff --git a/src/common/utils.test.ts b/src/common/utils.test.ts new file mode 100644 index 0000000..e90cad8 --- /dev/null +++ b/src/common/utils.test.ts @@ -0,0 +1,4 @@ + +export const assertNil = (val: any): val is (null | undefined) => { + return typeof val === 'undefined' || val === null; +}; diff --git a/src/common/utils.ts b/src/common/utils.ts index d9532d8..42dfc1a 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,2 +1,15 @@ -export const EMPTY = Object.freeze(Object.create(null)); +export const EMPTY_ARR: [] = Object.freeze([]) as []; + +export interface WithErrorsArr { + errors: string[]; +} + +export interface ValidateFn extends WithErrorsArr { + (value: I): value is O; + errors: string[]; +} + +export const assign = (target: TGT, source: SRC): TGT & SRC => { + return Object.assign(target, source); +}; diff --git a/src/index.test.ts b/src/index.test.ts index 22e3f8c..f6b2717 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -5,8 +5,6 @@ import { OCPP20 } from './index.js'; import assert from 'node:assert'; import { describe, it } from 'node:test'; -import './common/ajv.test.js'; - describe('schemas', () => { it('The OCPP16 namespace exports schemas', () => { diff --git a/src/index.ts b/src/index.ts index 514fb35..148e5ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ export { OCPP16 } from './ocpp16/index.js'; export { OCPP20 } from './ocpp20/index.js'; -export { setAjv } from './common/ajv.js'; \ No newline at end of file diff --git a/src/ocpp16/call.test.ts b/src/ocpp16/call.test.ts index e8fde19..89db101 100644 --- a/src/ocpp16/call.test.ts +++ b/src/ocpp16/call.test.ts @@ -1,25 +1,45 @@ import { OCPP16 } from './index.js'; import { describe, it } from 'node:test'; +import { deepStrictEqual } from 'node:assert'; -import '../common/ajv.test.js'; +import { assertNil } from '../common/utils.test.js'; -const { MessageType, Action, parse } = OCPP16; +const { MessageType, Action, validate } = OCPP16; -describe('OCPP16 - BootNotification', () => { +describe('OCPP16 - Call', () => { - it('type - minimal notification', () => { - [MessageType.CALL, 'test', Action.BootNotification, { - chargePointModel: 'test', - chargePointVendor: 'test', - }] satisfies OCPP16.BootNotificationCall; - }); + describe('types', () => { + + it('minimal notification', () => { + [MessageType.CALL, 'test', Action.BootNotification, { + chargePointModel: 'test', + chargePointVendor: 'test', + }] satisfies OCPP16.BootNotificationCall; + }); - it('parsing - minimal notification', () => { - parse([MessageType.CALL, 'test', Action.BootNotification, { - chargePointModel: 'test', - chargePointVendor: '55', - }]); }); + + describe('validation', () => { + + it('minimal notification', () => { + deepStrictEqual(validate([MessageType.CALL, 'test', Action.BootNotification, { + chargePointModel: 'test', + chargePointVendor: '55', + }]), true); + assertNil(validate.errors); + }); + + it('invalid notification (missing model)', () => { + deepStrictEqual(validate([MessageType.CALL, 'test', Action.BootNotification, { + chargePointVendor: '55', + }]), false); + deepStrictEqual(validate.errors, [ + "Invalid OCPP call: must have required property 'chargePointModel'" + ]); + }); + +}); + }); diff --git a/src/ocpp16/call.ts b/src/ocpp16/call.ts index 49fcbfe..063520c 100644 --- a/src/ocpp16/call.ts +++ b/src/ocpp16/call.ts @@ -1,8 +1,10 @@ +import type { JSONSchemaType } from 'ajv'; +import { EMPTY_ARR, assign, type ValidateFn } from '../common/utils.js'; import { Action, BaseMessage, MessageType } from './utils.js'; -import { ajvErrorsToString, getAjv } from '../common/ajv.js'; -import * as ensure from '../common/ensure.js'; +import { validate } from '../common/ajv.js'; + import * as schemas from './schemas.js'; import * as types from './types.js'; @@ -96,14 +98,36 @@ const schemasByCommand: Record = { [Action.UpdateFirmware]: schemas.UpdateFirmwareRequest, }; -export const parseCall = (arr: [MessageType.CALL, string, ...any]): Call => { - arr = ensure.length(arr, 4, 'Invalid OCPP call: bad length'); - const action = ensure.keyOf(arr[2], Action, 'Invalid OCPP call: unknown action'); - const payload = ensure.object(arr[3], 'Invalid OCPP call: bad payload'); - const schema = schemasByCommand[action]; - const ajv = getAjv(); - if (!ajv.validate(schema, payload)) { - throw new Error(`Invalid OCPP call: ${ajvErrorsToString(ajv)}`); - } - return arr as Call; +type BaseCall = BaseMessage; + +const basecall_schema: JSONSchemaType = { + type: 'array', + items: [ + { type: 'number', enum: [MessageType.CALL] }, + { type: 'string' }, + { type: 'string', enum: Object.values(Action) }, + { type: 'object', additionalProperties: true }, + ], + minItems: 4, + maxItems: 4 }; + +export const validateCall: ValidateFn = assign( + (arr: any): arr is Call => { + if (!validate(arr, basecall_schema, 'Invalid OCPP call')) { + validateCall.errors = validate.errors; + return false; + } + const [,, action, payload] = (arr as BaseCall); + const schema = schemasByCommand[action]; + if (!validate(payload, schema as JSONSchemaType, 'Invalid OCPP call')) { + validateCall.errors = validate.errors; + return false; + } + validateCall.errors = EMPTY_ARR; + return true; + }, + { errors: EMPTY_ARR }, +); + +validateCall.errors = EMPTY_ARR; diff --git a/src/ocpp16/callerror.ts b/src/ocpp16/callerror.ts index 8a95e15..babc0fa 100644 --- a/src/ocpp16/callerror.ts +++ b/src/ocpp16/callerror.ts @@ -1,13 +1,33 @@ -import * as ensure from '../common/ensure.js'; +import type { JSONSchemaType } from 'ajv'; +import { EMPTY_ARR, assign, type ValidateFn } from '../common/utils.js'; + +import { validate } from '../common/ajv.js'; import { MessageType, ErrorCode, BaseMessage } from './utils.js'; export type CallError = BaseMessage]> -export const parseCallError = (arr: [MessageType.CALLERROR, string, ...any]): CallError => { - arr = ensure.length(arr, 5, 'Invalid OCPP call error: bad length'); - ensure.keyOf(arr[2], ErrorCode, 'Invalid OCPP call error: unknown error code'); - ensure.string(arr[3], 'Invalid OCPP call error: bad error description'); - ensure.object(arr[4], 'Invalid OCPP call error: bad error details'); - return arr as CallError; +const callerror_schema: JSONSchemaType = { + type: 'array', + items: [ + { type: 'number', enum: [MessageType.CALLERROR] }, + { type: 'string' }, + { type: 'string', enum: Object.values(ErrorCode) }, + { type: 'string' }, + { type: 'object', additionalProperties: true }, + ], + minItems: 5, + maxItems: 5, }; + +export const validateCallError: ValidateFn = assign( + (arr: any): arr is CallError => { + if (!validate(arr, callerror_schema, 'Invalid OCPP call error')) { + validateCallError.errors = validate.errors; + return false; + } + validateCallError.errors = EMPTY_ARR; + return true; + }, + { errors: EMPTY_ARR }, +); diff --git a/src/ocpp16/callresult.test.ts b/src/ocpp16/callresult.test.ts index d6a6214..65267f7 100644 --- a/src/ocpp16/callresult.test.ts +++ b/src/ocpp16/callresult.test.ts @@ -2,8 +2,6 @@ import { OCPP16 } from './index.js'; import { describe, it } from 'node:test'; -import '../common/ajv.test.js'; - describe('OCPP16 - CheckedCallResult', () => { it('CheckedCallResult extends AuthorizeCallResult', () => { diff --git a/src/ocpp16/callresult.ts b/src/ocpp16/callresult.ts index a613c7b..74fb4db 100644 --- a/src/ocpp16/callresult.ts +++ b/src/ocpp16/callresult.ts @@ -1,16 +1,29 @@ -import { AuthorizeCall, Call } from './call.js'; +import type { JSONSchemaType } from 'ajv'; +import type { WithErrorsArr, ValidateFn } from '../common/utils.js'; -import { ajvErrorsToString, getAjv } from '../common/ajv.js'; -import { EMPTY } from '../common/utils.js'; -import * as ensure from '../common/ensure.js'; +import { Call } from './call.js'; + +import { validate } from '../common/ajv.js'; +import { EMPTY_ARR, assign } from '../common/utils.js'; import { Action, BaseMessage, MessageType } from './utils.js'; import * as schemas from './schemas.js'; import * as types from './types.js'; -export type UncheckedCallResult

| null> = BaseMessage; +export type UncheckedCallResult

= BaseMessage; + +const unchecked_call_result_schema: JSONSchemaType> = { + type: 'array', + items: [ + { type: 'number', enum: [MessageType.CALLRESULT] }, + { type: 'string' }, + { type: 'object', additionalProperties: true }, + ], + minItems: 3, + maxItems: 3, +}; export type AuthorizeCallResult = UncheckedCallResult; export type BootNotificationCallResult = UncheckedCallResult; @@ -103,11 +116,17 @@ const schemasByCommand: Record = { [Action.UpdateFirmware]: schemas.UpdateFirmwareResponse, }; -export const parseCallResult = (arr: [MessageType.CALLRESULT, string, ...any]): UncheckedCallResult => { - ensure.length(arr, 3, 'Invalid OCPP call result: bad length'); - ensure.object(arr[2], 'Invalid OCPP call result: bad payload'); - return arr as UncheckedCallResult; -}; +export const validateCallResult: ValidateFn> = assign( + (arr: any): arr is UncheckedCallResult => { + if (!validate>(arr, unchecked_call_result_schema, 'Invalid OCPP call result')) { + validateCallResult.errors = validate.errors; + return false; + } + validateCallResult.errors = EMPTY_ARR; + return true; + }, + { errors: EMPTY_ARR }, +); export interface CallResultTypesByAction extends Record { [Action.Authorize]: AuthorizeCallResult, @@ -142,13 +161,25 @@ export interface CallResultTypesByAction extends Record { export type CheckedCallResult = CallResultTypesByAction[C[2]]; -export const checkCallResult = (result: UncheckedCallResult, call: T): CheckedCallResult => { - ensure.equal(result[1], call[1], `Invalid OCPP call result: id ${result[1]} does not equal call id ${call[1]}`); - const schema = schemasByCommand[call[2]]; - const ajv = getAjv(); - if (!ajv.validate(schema, result[2])) { - throw new Error(`Invalid OCPP call result: ${ajvErrorsToString(ajv)}`); - } - // @ts-ignore - return result; -}; +export interface CheckCallResultFn extends WithErrorsArr { + (value: UncheckedCallResult, call: C): value is CheckedCallResult; + errors: string[]; +} + +export const checkCallResult: CheckCallResultFn = assign( + (result: UncheckedCallResult, call: C): result is CheckedCallResult => { + const [, call_id, payload] = result; + if (call_id !== call[1]) { + checkCallResult.errors = [`Invalid OCPP call result: id ${call_id} does not equal call id ${call[1]}`]; + return false; + } + const schema = schemasByCommand[call[2]]; + if (!validate(payload, schema as JSONSchemaType, 'Invalid OCPP call result')) { + checkCallResult.errors = validate.errors; + return false; + } + checkCallResult.errors = EMPTY_ARR; + return true; + }, + { errors: EMPTY_ARR }, +); diff --git a/src/ocpp16/index.ts b/src/ocpp16/index.ts index f079a43..d31b9b1 100644 --- a/src/ocpp16/index.ts +++ b/src/ocpp16/index.ts @@ -79,11 +79,12 @@ import type { CallError } from './callerror.js'; import type { UncheckedCallResult, CheckedCallResult, CallResult } from './callresult.js'; -import * as ensure from '../common/ensure.js'; +import { assign, EMPTY_ARR, type ValidateFn } from '../common/utils.js'; + import * as schemas_ from './schemas.js'; -import { parseCall as parseCall_ } from './call.js'; -import { parseCallError as parseCallError_ } from './callerror.js'; -import { parseCallResult as parseCallResult_, checkCallResult as checkCallResult_ } from './callresult.js'; +import { validateCall as validateCall_ } from './call.js'; +import { validateCallError as validateCallError_ } from './callerror.js'; +import { validateCallResult as validateCallResult_, checkCallResult as checkCallResult_ } from './callresult.js'; import { Action as Action_, MessageType as MessageType_, ErrorCode as ErrorCode_ } from './utils.js'; export declare namespace OCPP16 { @@ -181,39 +182,51 @@ export namespace OCPP16 { export const checkCallResult = checkCallResult_; export const schemas = schemas_; - export const maybeParse = (data: string | any[]): any[] => { - const parsed = typeof data === 'string' ? JSON.parse(data) : data; - return ensure.array(parsed, 'Invalid OCPP message: not an array'); - }; - - export const parse = (data: string | any[]): Call | CallError | UncheckedCallResult => { - const arr = maybeParse(data); - ensure.string(arr[1], 'Invalid OCPP message: invalid message id'); - switch (arr[0]) { - case MessageType_.CALL: - return parseCall_(arr as [MessageType_.CALL, string, ...any]); - case MessageType_.CALLERROR: - return parseCallError_(arr as [MessageType_.CALLERROR, string, ...any]); - case MessageType_.CALLRESULT: - return parseCallResult_(arr as [MessageType_.CALLRESULT, string, ...any]); - default: - throw new Error('Invalid OCPP message: invalid message type'); - } - }; - - export const stringify = (arr: Call | CallError | CallResult | UncheckedCallResult): string => { - return JSON.stringify(arr); - }; - - export const isCall = (msg: Call | CallError | UncheckedCallResult): msg is Call => { + export const validateCall = validateCall_; + export const validateCallError = validateCallError_; + export const validateCallResult = validateCallResult_; + + export const validate: ValidateFn> = assign( + (data: any): data is OCPP16.Call | OCPP16.CallError | OCPP16.UncheckedCallResult => { + switch (Array.isArray(data) ? data[0] : null) { + case MessageType_.CALL: + if (!validateCall_(data)) { + validate.errors = validateCall_.errors; + return false; + } + validate.errors = EMPTY_ARR; + return true; + case MessageType_.CALLERROR: + if (!validateCallError_(data)) { + validate.errors = validateCallError_.errors; + return false; + } + validate.errors = EMPTY_ARR; + return true; + case MessageType_.CALLRESULT: + if (!validateCallResult_(data)) { + validate.errors = validateCallResult_.errors; + return false; + } + validate.errors = EMPTY_ARR; + return true; + default: + validate.errors = ['Invalid OCPP message: invalid message type or not an array']; + return false; + } + }, + { errors: EMPTY_ARR }, + ); + + export const isCall = (msg: OCPP16.Call | OCPP16.CallError | OCPP16.UncheckedCallResult): msg is OCPP16.Call => { return msg[0] === MessageType_.CALL; }; - export const isCallError = (msg: Call | CallError | UncheckedCallResult): msg is CallError => { + export const isCallError = (msg: OCPP16.Call | OCPP16.CallError | OCPP16.UncheckedCallResult): msg is OCPP16.CallError => { return msg[0] === MessageType_.CALLERROR; }; - export const isCallResult = (msg: Call | CallError | UncheckedCallResult): msg is CallResult => { + export const isCallResult = (msg: OCPP16.Call | OCPP16.CallError | OCPP16.UncheckedCallResult): msg is OCPP16.CallResult => { return msg[0] === MessageType_.CALLRESULT; }; diff --git a/src/ocpp20/call.test.ts b/src/ocpp20/call.test.ts index 7d2c9b1..3fce90a 100644 --- a/src/ocpp20/call.test.ts +++ b/src/ocpp20/call.test.ts @@ -1,31 +1,53 @@ import { OCPP20 } from './index.js'; import { describe, it } from 'node:test'; +import { deepStrictEqual } from 'node:assert'; -import '../common/ajv.test.js'; +import { assertNil } from '../common/utils.test.js'; -const { MessageType, Action, parse } = OCPP20; +const { MessageType, Action, validate } = OCPP20; -describe('OCPP20 - BootNotification', () => { +describe('OCPP20 - Call', () => { + + describe('types', () => { + + it('minimal notification', () => { + [MessageType.CALL, 'test', Action.BootNotification, { + chargingStation: { + model: 'test', + vendorName: 'test', + }, + reason: 'PowerUp', + }] satisfies OCPP20.BootNotificationCall; + }); - it('type - minimal notification', () => { - [MessageType.CALL, 'test', Action.BootNotification, { - chargingStation: { - model: 'test', - vendorName: 'test', - }, - reason: 'PowerUp', - }] satisfies OCPP20.BootNotificationCall; }); - it('parsing - minimal notification', () => { - parse([MessageType.CALL, 'test', Action.BootNotification, { - chargingStation: { - model: 'test', - vendorName: 'test', - }, - reason: 'PowerUp', - }]); + describe('validation', () => { + + it('minimal notification', () => { + deepStrictEqual(validate([MessageType.CALL, 'test', Action.BootNotification, { + chargingStation: { + model: 'test', + vendorName: 'test', + }, + reason: 'PowerUp', + }]), true); + assertNil(validate.errors); + }); + + it('invalid notification (missing model)', () => { + deepStrictEqual(validate([MessageType.CALL, 'test', Action.BootNotification, { + chargingStation: { + vendorName: 'test', + }, + reason: 'PowerUp', + }]), false); + deepStrictEqual(validate.errors, [ + "Invalid OCPP call: /chargingStation must have required property 'model'" + ]); + }); + }); }); diff --git a/src/ocpp20/call.ts b/src/ocpp20/call.ts index 365fb3a..9843093 100644 --- a/src/ocpp20/call.ts +++ b/src/ocpp20/call.ts @@ -1,8 +1,9 @@ +import type { JSONSchemaType } from 'ajv'; +import { EMPTY_ARR, assign, type ValidateFn } from '../common/utils.js'; import { Action, BaseMessage, MessageType } from './utils.js'; -import { ajvErrorsToString, getAjv } from '../common/ajv.js'; -import * as ensure from '../common/ensure.js'; +import { validate } from '../common/ajv.js'; import * as schemas from './schemas.js'; import * as types from './types.js'; @@ -207,14 +208,36 @@ const schemasByCommand: Record = { [Action.UpdateFirmware]: schemas.UpdateFirmwareRequest, }; -export const parseCall = (arr: [MessageType.CALL, string, ...any]): Call => { - arr = ensure.length(arr, 4, 'Invalid OCPP call: bad length'); - const action = ensure.keyOf(arr[2], Action, 'Invalid OCPP call: unknown action'); - const payload = ensure.object(arr[3], 'Invalid OCPP call: bad payload'); - const schema = schemasByCommand[action]; - const ajv = getAjv(); - if (!ajv.validate(schema, payload)) { - throw new Error(`Invalid OCPP call: ${ajvErrorsToString(ajv)}`); - } - return arr as Call; +type BaseCall = BaseMessage; + +const basecall_schema: JSONSchemaType = { + type: 'array', + items: [ + { type: 'number', enum: [MessageType.CALL] }, + { type: 'string' }, + { type: 'string', enum: Object.values(Action) }, + { type: 'object', additionalProperties: true }, + ], + minItems: 4, + maxItems: 4 }; + +export const validateCall: ValidateFn = assign( + (arr: any): arr is Call => { + if (!validate(arr, basecall_schema, 'Invalid OCPP call')) { + validateCall.errors = validate.errors; + return false; + } + const [,, action, payload] = (arr as BaseCall); + const schema = schemasByCommand[action]; + if (!validate(payload, schema as JSONSchemaType, 'Invalid OCPP call')) { + validateCall.errors = validate.errors; + return false; + } + validateCall.errors = EMPTY_ARR; + return true; + }, + { errors: EMPTY_ARR } +); + +validateCall.errors = EMPTY_ARR; diff --git a/src/ocpp20/callerror.ts b/src/ocpp20/callerror.ts index 8a95e15..b7ae9cc 100644 --- a/src/ocpp20/callerror.ts +++ b/src/ocpp20/callerror.ts @@ -1,13 +1,33 @@ -import * as ensure from '../common/ensure.js'; +import type { JSONSchemaType } from 'ajv'; +import { assign, EMPTY_ARR, type ValidateFn } from '../common/utils.js'; + +import { validate } from '../common/ajv.js'; import { MessageType, ErrorCode, BaseMessage } from './utils.js'; export type CallError = BaseMessage]> -export const parseCallError = (arr: [MessageType.CALLERROR, string, ...any]): CallError => { - arr = ensure.length(arr, 5, 'Invalid OCPP call error: bad length'); - ensure.keyOf(arr[2], ErrorCode, 'Invalid OCPP call error: unknown error code'); - ensure.string(arr[3], 'Invalid OCPP call error: bad error description'); - ensure.object(arr[4], 'Invalid OCPP call error: bad error details'); - return arr as CallError; +const callerror_schema: JSONSchemaType = { + type: 'array', + items: [ + { type: 'number', enum: [MessageType.CALLERROR] }, + { type: 'string' }, + { type: 'string', enum: Object.values(ErrorCode) }, + { type: 'string' }, + { type: 'object', additionalProperties: true }, + ], + minItems: 5, + maxItems: 5, }; + +export const validateCallError: ValidateFn = assign( + (arr: any): arr is CallError => { + if (!validate(arr, callerror_schema, 'Invalid OCPP call error')) { + validateCallError.errors = validate.errors; + return false; + } + validateCallError.errors = EMPTY_ARR; + return true; + }, + { errors: EMPTY_ARR }, +); diff --git a/src/ocpp20/callresult.test.ts b/src/ocpp20/callresult.test.ts index bc8bd7e..8b61ca6 100644 --- a/src/ocpp20/callresult.test.ts +++ b/src/ocpp20/callresult.test.ts @@ -2,24 +2,26 @@ import { OCPP20 } from './index.js'; import { describe, it } from 'node:test'; -import '../common/ajv.test.js'; - describe('OCPP20 - CheckedCallResult', () => { - it('CheckedCallResult extends AuthorizeCallResult', () => { - const t: OCPP20.CheckedCallResult extends OCPP20.AuthorizeCallResult ? true : false = true; - }); + describe('types', () => { - it('AuthorizeCallResult extends CheckedCallResult', () => { - const t: OCPP20.AuthorizeCallResult extends OCPP20.CheckedCallResult ? true : false = true; - }); + it('CheckedCallResult extends AuthorizeCallResult', () => { + const t: OCPP20.CheckedCallResult extends OCPP20.AuthorizeCallResult ? true : false = true; + }); - it('CheckedCallResult does not extend AuthorizeCallResult', () => { - const t: OCPP20.CheckedCallResult extends OCPP20.AuthorizeCallResult ? true : false = false; - }); + it('AuthorizeCallResult extends CheckedCallResult', () => { + const t: OCPP20.AuthorizeCallResult extends OCPP20.CheckedCallResult ? true : false = true; + }); + + it('CheckedCallResult does not extend AuthorizeCallResult', () => { + const t: OCPP20.CheckedCallResult extends OCPP20.AuthorizeCallResult ? true : false = false; + }); + + it('AuthorizeCallResult does not extend CheckedCallResult', () => { + const t: OCPP20.AuthorizeCallResult extends OCPP20.CheckedCallResult ? true : false = false; + }); - it('AuthorizeCallResult does not extend CheckedCallResult', () => { - const t: OCPP20.AuthorizeCallResult extends OCPP20.CheckedCallResult ? true : false = false; }); }); diff --git a/src/ocpp20/callresult.ts b/src/ocpp20/callresult.ts index 7ff3829..8875371 100644 --- a/src/ocpp20/callresult.ts +++ b/src/ocpp20/callresult.ts @@ -1,7 +1,8 @@ -import { ajvErrorsToString, getAjv } from '../common/ajv.js'; -import * as ensure from '../common/ensure.js'; -import { EMPTY } from '../common/utils.js'; +import type { JSONSchemaType } from 'ajv'; +import { type WithErrorsArr, type ValidateFn, EMPTY_ARR, assign } from '../common/utils.js'; + +import { validate } from '../common/ajv.js'; import { Call } from './call.js'; import { Action, BaseMessage, MessageType } from './utils.js'; @@ -10,6 +11,17 @@ import * as types from './types.js'; export type UncheckedCallResult

| null> = BaseMessage; +const unchecked_call_result_schema: JSONSchemaType> = { + type: 'array', + items: [ + { type: 'number', enum: [MessageType.CALLRESULT] }, + { type: 'string' }, + { type: 'object', additionalProperties: true }, + ], + minItems: 3, + maxItems: 3, +}; + export type AuthorizeCallResult = UncheckedCallResult; export type BootNotificationCallResult = UncheckedCallResult; export type CancelReservationCallResult = UncheckedCallResult; @@ -210,11 +222,17 @@ const schemasByCommand: Record = { [Action.UpdateFirmware]: schemas.UpdateFirmwareResponse, }; -export const parseCallResult = (arr: [MessageType.CALLRESULT, string, ...any]): UncheckedCallResult => { - ensure.length(arr, 3, 'Invalid OCPP call result: bad length'); - ensure.object(arr[2], 'Invalid OCPP call result: bad payload'); - return arr as UncheckedCallResult; -}; +export const validateCallResult: ValidateFn> = assign( + (arr: any): arr is UncheckedCallResult => { + if (!validate>(arr, unchecked_call_result_schema, 'Invalid OCPP call result')) { + validateCallResult.errors = validate.errors; + return false; + } + validateCallResult.errors = EMPTY_ARR; + return true; + }, + { errors: EMPTY_ARR }, +); export interface CallResultTypesByAction extends Record { [Action.Authorize]: AuthorizeCallResult, @@ -285,13 +303,25 @@ export interface CallResultTypesByAction extends Record { export type CheckedCallResult = CallResultTypesByAction[C[2]]; -export const checkCallResult = (result: UncheckedCallResult, call: T): CheckedCallResult => { - ensure.equal(result[1], call[1], `Invalid OCPP call result: id ${result[1]} does not equal call id ${call[1]}`); - const schema = schemasByCommand[call[2]]; - const ajv = getAjv(); - if (!ajv.validate(schema, result[2])) { - throw new Error(`Invalid OCPP call result: ${ajvErrorsToString(ajv)}`); - } - // @ts-ignore - return result; -}; +export interface CheckCallResultFn extends WithErrorsArr { + (value: UncheckedCallResult, call: C): value is CheckedCallResult; + errors: string[]; +} + +export const checkCallResult: CheckCallResultFn = assign( + (result: UncheckedCallResult, call: C): result is CheckedCallResult => { + const [, call_id, payload] = result; + if (call_id !== call[1]) { + checkCallResult.errors = [`Invalid OCPP call result: id ${call_id} does not equal call id ${call[1]}`]; + return false; + } + const schema = schemasByCommand[call[2]]; + if (!validate(payload, schema as JSONSchemaType, 'Invalid OCPP call result')) { + checkCallResult.errors = validate.errors; + return false; + } + checkCallResult.errors = EMPTY_ARR; + return true; + }, + { errors: EMPTY_ARR }, +); diff --git a/src/ocpp20/index.ts b/src/ocpp20/index.ts index 9041220..4bb1566 100644 --- a/src/ocpp20/index.ts +++ b/src/ocpp20/index.ts @@ -155,11 +155,13 @@ import type { ChargingState, } from './utils.js'; +import { assign, EMPTY_ARR, type ValidateFn } from '../common/utils.js'; + import * as schemas_ from './schemas.js'; -import * as ensure from '../common/ensure.js'; -import { parseCall as parseCall_ } from './call.js'; -import { parseCallError as parseCallError_ } from './callerror.js'; -import { parseCallResult as parseCallResult_, checkCallResult as checkCallResult_ } from './callresult.js'; + +import { validateCall as validateCall_ } from './call.js'; +import { validateCallError as validateCallError_ } from './callerror.js'; +import { validateCallResult as validateCallResult_, checkCallResult as checkCallResult_ } from './callresult.js'; import { Action as Action_, MessageType as MessageType_, ErrorCode as ErrorCode_ } from './utils.js'; export declare namespace OCPP20 { @@ -333,39 +335,51 @@ export namespace OCPP20 { export const schemas = schemas_; export const checkCallResult = checkCallResult_; - export const maybeParse = (data: string | any[]): any[] => { - const parsed = typeof data === 'string' ? JSON.parse(data) : data; - return ensure.array(parsed, 'Invalid OCPP message: not an array'); - }; - - export const parse = (data: string | any[]): Call | CallError | UncheckedCallResult => { - const arr = maybeParse(data); - ensure.string(arr[1], 'Invalid OCPP message: invalid message id'); - switch (arr[0]) { - case MessageType_.CALL: - return parseCall_(arr as [MessageType_.CALL, string, ...any]); - case MessageType_.CALLERROR: - return parseCallError_(arr as [MessageType_.CALLERROR, string, ...any]); - case MessageType_.CALLRESULT: - return parseCallResult_(arr as [MessageType_.CALLRESULT, string, ...any]); - default: - throw new Error('Invalid OCPP message: invalid message type'); - } - }; + export const validateCall = validateCall_; + export const validateCallError = validateCallError_; + export const validateCallResult = validateCallResult_; - export const stringify = (arr: Call | CallError | CallResult | UncheckedCallResult): string => { - return JSON.stringify(arr); - }; + export const validate: ValidateFn> = assign( + (data: any): data is OCPP20.Call | OCPP20.CallError | OCPP20.UncheckedCallResult => { + switch (Array.isArray(data) ? data[0] : null) { + case MessageType_.CALL: + if (!validateCall_(data)) { + validate.errors = validateCall_.errors; + return false; + } + validate.errors = EMPTY_ARR; + return true; + case MessageType_.CALLERROR: + if (!validateCallError_(data)) { + validate.errors = validateCallError_.errors; + return false; + } + validate.errors = EMPTY_ARR; + return true; + case MessageType_.CALLRESULT: + if (!validateCallResult_(data)) { + validate.errors = validateCallResult_.errors; + return false; + } + validate.errors = EMPTY_ARR; + return true; + default: + validate.errors = ['Invalid OCPP message: invalid message type']; + return false; + } + }, + { errors: EMPTY_ARR }, + ); - export const isCall = (msg: Call | CallError | UncheckedCallResult): msg is Call => { + export const isCall = (msg: OCPP20.Call | OCPP20.CallError | OCPP20.UncheckedCallResult): msg is OCPP20.Call => { return msg[0] === MessageType_.CALL; }; - export const isCallError = (msg: Call | CallError | UncheckedCallResult): msg is CallError => { + export const isCallError = (msg: OCPP20.Call | OCPP20.CallError | OCPP20.UncheckedCallResult): msg is OCPP20.CallError => { return msg[0] === MessageType_.CALLERROR; }; - export const isCallResult = (msg: Call | CallError | UncheckedCallResult): msg is CallResult => { + export const isCallResult = (msg: OCPP20.Call | OCPP20.CallError | OCPP20.UncheckedCallResult): msg is OCPP20.CallResult => { return msg[0] === MessageType_.CALLRESULT; };