@sailboat-computer/validation
Version:
Validation framework for sailboat computer v3
211 lines • 7.49 kB
JavaScript
"use strict";
/**
* Base validation rule implementation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RateOfChangeRule = exports.RangeValidationRule = exports.BaseValidationRule = void 0;
const types_1 = require("../types");
/**
* Abstract base class for validation rules
*/
class BaseValidationRule {
constructor(name, description, category, severity) {
this.name = name;
this.description = description;
this.category = category;
this.severity = severity;
}
/**
* Get rule configuration (can be overridden by subclasses)
*/
getConfiguration() {
return {
name: this.name,
description: this.description,
category: this.category,
severity: this.severity
};
}
/**
* Helper method to create a validation result
*/
createResult(passed, message, details) {
const result = {
rule: this.name,
passed,
severity: this.severity
};
if (message !== undefined) {
result.message = message;
}
if (details !== undefined) {
result.details = details;
}
return result;
}
/**
* Helper method to check if a numeric value is within range
*/
isInRange(value, min, max, inclusive = true) {
if (inclusive) {
return value >= min && value <= max;
}
else {
return value > min && value < max;
}
}
/**
* Helper method to check rate of change
*/
checkRateOfChange(currentValue, previousValue, timeDeltaSeconds, maxRatePerSecond) {
if (timeDeltaSeconds <= 0)
return true;
const rateOfChange = Math.abs(currentValue - previousValue) / timeDeltaSeconds;
return rateOfChange <= maxRatePerSecond;
}
/**
* Helper method to get previous sensor data from context
*/
getPreviousData(data, context, maxAgeSeconds = 60) {
const cutoffTime = data.timestamp.getTime() - (maxAgeSeconds * 1000);
return context.recentData
.filter(d => d.sensorId === data.sensorId &&
d.timestamp.getTime() >= cutoffTime &&
d.timestamp.getTime() < data.timestamp.getTime())
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())[0];
}
/**
* Helper method to extract numeric value from sensor data
*/
extractNumericValue(data) {
if (typeof data.value === 'number') {
return data.value;
}
if (typeof data.value === 'object' && data.value !== null) {
// Try common numeric field names
const obj = data.value;
for (const field of ['value', 'reading', 'measurement', 'data']) {
if (typeof obj[field] === 'number') {
return obj[field];
}
}
}
return null;
}
/**
* Helper method to check if sensor type matches expected types
*/
matchesSensorType(data, expectedTypes) {
return expectedTypes.some(type => data.sensorType.toLowerCase().includes(type.toLowerCase()));
}
/**
* Helper method to get operational context priority
*/
getContextPriority(context) {
switch (context.operationalContext.toLowerCase()) {
case 'emergency':
case 'safety':
return 5;
case 'sailing':
case 'motoring':
return 4;
case 'anchored':
return 3;
case 'maintenance':
return 2;
case 'docked':
default:
return 1;
}
}
}
exports.BaseValidationRule = BaseValidationRule;
/**
* Range validation rule for numeric sensor values
*/
class RangeValidationRule extends BaseValidationRule {
constructor(name, description, minValue, maxValue, sensorTypes, severity = types_1.AlertSeverity.ALARM) {
super(name, description, 'physical', severity);
this.minValue = minValue;
this.maxValue = maxValue;
this.sensorTypes = sensorTypes;
}
validate(data, context) {
const value = this.extractNumericValue(data);
if (value === null) {
return this.createResult(false, `Unable to extract numeric value from ${data.sensorType} sensor`, { rawValue: data.value });
}
const inRange = this.isInRange(value, this.minValue, this.maxValue);
return this.createResult(inRange, inRange
? undefined
: `Value ${value} ${data.unit} is outside valid range [${this.minValue}, ${this.maxValue}] ${data.unit}`, {
value,
minValue: this.minValue,
maxValue: this.maxValue,
unit: data.unit
});
}
isApplicable(data) {
return this.matchesSensorType(data, this.sensorTypes);
}
getConfiguration() {
return {
...super.getConfiguration(),
minValue: this.minValue,
maxValue: this.maxValue,
sensorTypes: this.sensorTypes
};
}
}
exports.RangeValidationRule = RangeValidationRule;
/**
* Rate of change validation rule
*/
class RateOfChangeRule extends BaseValidationRule {
constructor(name, description, maxRatePerSecond, sensorTypes, severity = types_1.AlertSeverity.WARNING) {
super(name, description, 'physical', severity);
this.maxRatePerSecond = maxRatePerSecond;
this.sensorTypes = sensorTypes;
}
validate(data, context) {
const currentValue = this.extractNumericValue(data);
if (currentValue === null) {
return this.createResult(false, `Unable to extract numeric value from ${data.sensorType} sensor`, { rawValue: data.value });
}
const previousData = this.getPreviousData(data, context);
if (!previousData) {
// No previous data to compare - pass validation
return this.createResult(true, 'No previous data available for rate of change validation', { currentValue });
}
const previousValue = this.extractNumericValue(previousData);
if (previousValue === null) {
return this.createResult(true, 'Previous data value not numeric - skipping rate validation', { currentValue });
}
const timeDelta = (data.timestamp.getTime() - previousData.timestamp.getTime()) / 1000;
const rateValid = this.checkRateOfChange(currentValue, previousValue, timeDelta, this.maxRatePerSecond);
const actualRate = timeDelta > 0 ?
Math.abs(currentValue - previousValue) / timeDelta : 0;
return this.createResult(rateValid, rateValid
? undefined
: `Rate of change ${actualRate.toFixed(3)} ${data.unit}/s exceeds maximum ${this.maxRatePerSecond} ${data.unit}/s`, {
currentValue,
previousValue,
timeDelta,
actualRate,
maxRate: this.maxRatePerSecond,
unit: data.unit
});
}
isApplicable(data) {
return this.matchesSensorType(data, this.sensorTypes);
}
getConfiguration() {
return {
...super.getConfiguration(),
maxRatePerSecond: this.maxRatePerSecond,
sensorTypes: this.sensorTypes
};
}
}
exports.RateOfChangeRule = RateOfChangeRule;
//# sourceMappingURL=base-rule.js.map