From: Jérôme Benoit Date: Thu, 2 Apr 2026 15:06:12 +0000 (+0200) Subject: refactor(ocpp): consolidate payload validation into shared utility X-Git-Tag: ocpp-server@v4.2.0~1 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=b292626fc0dc582881d78d38fbdbb0b315ff7149;p=e-mobility-charging-stations-simulator.git refactor(ocpp): consolidate payload validation into shared utility Extract 4 near-identical validation methods into 1 shared validatePayload function in OCPPServiceUtils. Each service method becomes a thin wrapper delegating to the shared logic. Preserves clone/date-conversion behavior per caller and error message capitalization for OCPP protocol compliance. --- diff --git a/src/charging-station/ocpp/OCPPIncomingRequestService.ts b/src/charging-station/ocpp/OCPPIncomingRequestService.ts index 57d1f5a3..4ed9a0b7 100644 --- a/src/charging-station/ocpp/OCPPIncomingRequestService.ts +++ b/src/charging-station/ocpp/OCPPIncomingRequestService.ts @@ -12,9 +12,7 @@ import { type OCPPVersion, } from '../../types/index.js' import { isAsyncFunction, logger } from '../../utils/index.js' -import { type Ajv, ajvErrorsToErrorType, createAjv } from './OCPPServiceUtils.js' - -const moduleName = 'OCPPIncomingRequestService' +import { type Ajv, createAjv, validatePayload } from './OCPPServiceUtils.js' export abstract class OCPPIncomingRequestService extends EventEmitter { private static readonly instances = new Map< @@ -177,28 +175,12 @@ export abstract class OCPPIncomingRequestService extends EventEmitter { commandName: IncomingRequestCommand, payload: T ): boolean { - if (chargingStation.stationInfo?.ocppStrictCompliance === false) { - return true - } - const validate = this.payloadValidatorFunctions.get(commandName) - if (validate == null) { - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestPayload: No JSON schema validation function found for command '${commandName}' PDU validation` - ) - return false - } - if (validate(payload)) { - return true - } - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestPayload: Command '${commandName}' incoming request PDU is invalid: %j`, - validate.errors - ) - throw new OCPPError( - ajvErrorsToErrorType(validate.errors), - 'Incoming request PDU is invalid', + return validatePayload( + chargingStation, commandName, - JSON.stringify(validate.errors, undefined, 2) + payload, + this.payloadValidatorFunctions.get(commandName), + 'incoming request' ) } } diff --git a/src/charging-station/ocpp/OCPPRequestService.ts b/src/charging-station/ocpp/OCPPRequestService.ts index 5385a9a1..1278e14e 100644 --- a/src/charging-station/ocpp/OCPPRequestService.ts +++ b/src/charging-station/ocpp/OCPPRequestService.ts @@ -22,7 +22,6 @@ import { type ResponseType, } from '../../types/index.js' import { - clone, ensureError, formatDurationMilliSeconds, getErrorMessage, @@ -31,14 +30,7 @@ import { logger, } from '../../utils/index.js' import { OCPPConstants } from './OCPPConstants.js' -import { - type Ajv, - ajvErrorsToErrorType, - convertDateToISOString, - createAjv, -} from './OCPPServiceUtils.js' - -const moduleName = 'OCPPRequestService' +import { type Ajv, createAjv, validatePayload } from './OCPPServiceUtils.js' const defaultRequestParams: RequestParams = { skipBufferingOnError: false, @@ -185,33 +177,15 @@ export abstract class OCPPRequestService { commandName: IncomingRequestCommand | RequestCommand, payload: T ): boolean { - if (chargingStation.stationInfo?.ocppStrictCompliance === false) { - return true - } - const validate = this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get( - commandName as IncomingRequestCommand - ) - if (validate == null) { - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation` - ) - return false - } - payload = clone(payload) - convertDateToISOString(payload) - if (validate(payload)) { - return true - } - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' incoming request response PDU is invalid: %j`, - validate.errors - ) - // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). - throw new OCPPError( - ajvErrorsToErrorType(validate.errors), - 'Incoming request response PDU is invalid', + return validatePayload( + chargingStation, commandName, - JSON.stringify(validate.errors, undefined, 2) + payload, + this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get( + commandName as IncomingRequestCommand + ), + 'incoming request response', + true ) } @@ -228,31 +202,13 @@ export abstract class OCPPRequestService { commandName: IncomingRequestCommand | RequestCommand, payload: T ): boolean { - if (chargingStation.stationInfo?.ocppStrictCompliance === false) { - return true - } - const validate = this.payloadValidatorFunctions.get(commandName as RequestCommand) - if (validate == null) { - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema validation function found for command '${commandName}' PDU validation` - ) - return false - } - payload = clone(payload) - convertDateToISOString(payload) - if (validate(payload)) { - return true - } - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`, - validate.errors - ) - // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). - throw new OCPPError( - ajvErrorsToErrorType(validate.errors), - 'Request PDU is invalid', + return validatePayload( + chargingStation, commandName, - JSON.stringify(validate.errors, undefined, 2) + payload, + this.payloadValidatorFunctions.get(commandName as RequestCommand), + 'request', + true ) } diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts index 2975025a..2a276db7 100644 --- a/src/charging-station/ocpp/OCPPResponseService.ts +++ b/src/charging-station/ocpp/OCPPResponseService.ts @@ -12,9 +12,7 @@ import { type ResponseHandler, } from '../../types/index.js' import { Constants, isAsyncFunction, logger } from '../../utils/index.js' -import { type Ajv, ajvErrorsToErrorType, createAjv } from './OCPPServiceUtils.js' - -const moduleName = 'OCPPResponseService' +import { type Ajv, createAjv, validatePayload } from './OCPPServiceUtils.js' export abstract class OCPPResponseService { private static readonly instances = new Map OCPPResponseService, OCPPResponseService>() @@ -157,28 +155,12 @@ export abstract class OCPPResponseService { commandName: RequestCommand, payload: T ): boolean { - if (chargingStation.stationInfo?.ocppStrictCompliance === false) { - return true - } - const validate = this.payloadValidatorFunctions.get(commandName) - if (validate == null) { - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation` - ) - return false - } - if (validate(payload)) { - return true - } - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: Command '${commandName}' response PDU is invalid: %j`, - validate.errors - ) - throw new OCPPError( - ajvErrorsToErrorType(validate.errors), - 'Response PDU is invalid', + return validatePayload( + chargingStation, commandName, - JSON.stringify(validate.errors, undefined, 2) + payload, + this.payloadValidatorFunctions.get(commandName), + 'response' ) } } diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index fedecd9d..b4826ec8 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -37,6 +37,7 @@ import { } from '../../types/index.js' import { ACElectricUtils, + clone, Constants, convertToFloat, convertToInt, @@ -131,6 +132,55 @@ export const ajvErrorsToErrorType = (errors: ErrorObject[] | null | undefined): return ErrorType.FORMAT_VIOLATION } +/** + * Validates an OCPP payload against a JSON schema validation function. + * Shared implementation used by request, response, and incoming request validation. + * @param chargingStation - The charging station instance + * @param commandName - OCPP command name to validate against + * @param payload - JSON payload to validate + * @param validate - Ajv validation function for the command + * @param context - Description of the validation context (e.g. 'request', 'response') + * @param clonePayload - Whether to clone payload and convert dates before validation + * @returns True if payload validation succeeds, false otherwise + */ +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters +export const validatePayload = ( + chargingStation: ChargingStation, + commandName: IncomingRequestCommand | RequestCommand, + payload: T, + validate: undefined | ValidateFunction, + context: string, + clonePayload = false +): boolean => { + if (chargingStation.stationInfo?.ocppStrictCompliance === false) { + return true + } + if (validate == null) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' ${context} PDU validation` + ) + return false + } + let payloadToValidate = payload + if (clonePayload) { + payloadToValidate = clone(payload) + convertDateToISOString(payloadToValidate) + } + if (validate(payloadToValidate)) { + return true + } + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.validatePayload: Command '${commandName}' ${context} PDU is invalid: %j`, + validate.errors + ) + throw new OCPPError( + ajvErrorsToErrorType(validate.errors), + `${context.charAt(0).toUpperCase()}${context.slice(1)} PDU is invalid`, + commandName, + JSON.stringify(validate.errors, undefined, 2) + ) +} + /** * Recursively converts Date values to ISO 8601 strings within a JSON-compatible object. * @param object - Object whose Date properties will be converted in-place