Skip to content

Implement missing SWE Common validators (Boolean, Vector, Matrix, DataStream, DataChoice, Geometry) #2

@Sam-Bolling

Description

@Sam-Bolling

Problem

The SWE Common validation system is missing dedicated validators for 6 component types: Boolean, Vector, Matrix, DataStream, DataChoice, and Geometry. These types currently pass through the generic validateSWEComponent() function and return { valid: true } without performing any actual validation. This allows structurally invalid sensor data to pass validation.

Context

File: src/ogc-api/csapi/validation/swe-validator.ts (357 lines)
Related Validation: CSAPI-Live-Testing Issue #14
Commits:

  • swe-validator.ts: d4754f0921728ded98d5c7fa618dbe96240ce52b
  • constraint-validator.ts: b6ff9127a16a51e805ac79f8647deb62a90faaf0

Current Coverage: 73.68% (Statements: 42/57, Branches: 23/38, Functions: 5/6)

The validation system has excellent implementations for simple components (Quantity, Count, Text, Category, Time) and range components (QuantityRange, TimeRange, CountRange, CategoryRange), but aggregate and complex types lack dedicated validators.

Detailed Findings

1. Boolean Validator - Missing

Status: ❌ Does NOT exist
Current Behavior: Generic validator handles Boolean type at line 331 but returns { valid: true } without validation
Evidence: No validateBoolean() function found in swe-validator.ts

What Should Be Validated:

  • Type must be 'Boolean'
  • Value (if present) must be boolean (true/false)
  • Required DataComponent properties (definition, label per OGC 24-014)

Example Invalid Data That Currently Passes:

{
  type: 'Boolean',
  value: "true"  // String instead of boolean - should fail
}

2. Vector Validator - Missing

Status: ❌ Does NOT exist
Current Behavior: Generic validator returns { valid: true } for Vector type without validation
Evidence: No validateVector() function exists
Claim: Assessment claimed "Vector component validator with reference frame" - NOT CONFIRMED

What Should Be Validated:

  • Type must be 'Vector'
  • Required property: referenceFrame (CRS identifier)
  • Required property: coordinates (array of coordinate components)
  • Each coordinate must be a valid SWE component (typically Quantity)
  • Minimum 2 coordinates required
  • Reference frame must be valid URI (e.g., http://www.opengis.net/def/crs/EPSG/0/4979)

Example Invalid Data That Currently Passes:

{
  type: 'Vector',
  // Missing referenceFrame - should fail
  // Missing coordinates - should fail
}

Valid Structure Example:

{
  type: 'Vector',
  definition: 'http://example.org/location',
  label: 'Location',
  referenceFrame: 'http://www.opengis.net/def/crs/EPSG/0/4979',
  coordinates: [
    {
      name: 'x',
      component: {
        type: 'Quantity',
        definition: 'http://example.org/x',
        label: 'X Coordinate',
        uom: { code: 'm' }
      }
    },
    {
      name: 'y',
      component: {
        type: 'Quantity',
        definition: 'http://example.org/y',
        label: 'Y Coordinate',
        uom: { code: 'm' }
      }
    }
  ]
}

3. Matrix Validator - Missing

Status: ❌ Does NOT exist
Current Behavior: Generic validator returns { valid: true } without validation
Evidence: No validateMatrix() function exists

What Should Be Validated:

  • Type must be 'Matrix'
  • Required property: elementType (component describing matrix elements)
  • Required property: elementCount (array of dimensions, e.g., [3, 3] for 3×3 matrix)
  • ElementCount must be array of integers
  • Each dimension must be > 0
  • ElementType should be a valid SWE component

Example Invalid Data That Currently Passes:

{
  type: 'Matrix',
  // Missing elementType - should fail
  // Missing elementCount - should fail
}

Valid Structure Example:

{
  type: 'Matrix',
  definition: 'http://example.org/transform',
  label: 'Transformation Matrix',
  elementCount: [3, 3],  // 3×3 matrix
  elementType: {
    type: 'Quantity',
    definition: 'http://example.org/element',
    label: 'Matrix Element',
    uom: { code: '1' }  // Dimensionless
  }
}

4. DataStream Validator - Missing

Status: ❌ Does NOT exist
Current Behavior: Generic validator returns { valid: true } without validation
Evidence: No validateDataStream() function exists

What Should Be Validated:

  • Type must be 'DataStream'
  • Required property: elementType (component describing stream elements)
  • Required property: encoding (describes how data is encoded)
  • Required property: values (stream data)
  • ElementType should be valid SWE component (often DataRecord or DataArray)
  • Encoding should specify format (TextEncoding, BinaryEncoding, XMLEncoding, JSONEncoding)

Example Invalid Data That Currently Passes:

{
  type: 'DataStream',
  // Missing elementType - should fail
  // Missing encoding - should fail
  // Missing values - should fail
}

Valid Structure Example:

{
  type: 'DataStream',
  definition: 'http://example.org/stream',
  label: 'Sensor Data Stream',
  elementType: {
    type: 'DataRecord',
    definition: 'http://example.org/record',
    label: 'Data Record',
    fields: [/* ... */]
  },
  encoding: {
    type: 'TextEncoding',
    tokenSeparator: ',',
    blockSeparator: '\n'
  },
  values: '1.2,3.4\n5.6,7.8\n'
}

5. DataChoice Validator - Missing

Status: ❌ Does NOT exist
Current Behavior: Generic validator returns { valid: true } without validation
Evidence: No validateDataChoice() function exists

What Should Be Validated:

  • Type must be 'DataChoice'
  • Required property: items (array of choice options)
  • Items array must have at least 1 element
  • Each item must have name and component properties
  • Each component should be a valid SWE component
  • Item names must be unique

Example Invalid Data That Currently Passes:

{
  type: 'DataChoice',
  // Missing items - should fail
}

Valid Structure Example:

{
  type: 'DataChoice',
  definition: 'http://example.org/choice',
  label: 'Data Choice',
  items: [
    {
      name: 'scalar',
      component: {
        type: 'Quantity',
        definition: 'http://example.org/scalar',
        label: 'Scalar Value',
        uom: { code: 'm' }
      }
    },
    {
      name: 'vector',
      component: {
        type: 'Vector',
        definition: 'http://example.org/vector',
        label: 'Vector Value',
        referenceFrame: 'http://www.opengis.net/def/crs/EPSG/0/4979',
        coordinates: [/* ... */]
      }
    }
  ]
}

6. Geometry Validator - Missing

Status: ❌ Does NOT exist
Current Behavior: Geometry type listed in valid types (line 347) but no dedicated validator
Evidence: No validateGeometryData() or validateGeometry() function exists

What Should Be Validated:

  • Type must be 'Geometry' or 'GeometryData'
  • Required property: geometry (GeoJSON geometry object)
  • Geometry must have valid type (Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon)
  • Geometry must have valid coordinates array
  • Coordinates must match geometry type structure
  • Optional property: crs (coordinate reference system)

Example Invalid Data That Currently Passes:

{
  type: 'Geometry',
  // Missing geometry - should fail
}

Valid Structure Example:

{
  type: 'Geometry',
  definition: 'http://example.org/geometry',
  label: 'Geometry',
  geometry: {
    type: 'Point',
    coordinates: [-122.4194, 37.7749]
  },
  crs: 'http://www.opengis.net/def/crs/EPSG/0/4326'
}

Solution

1. Implement 6 Missing Validator Functions

Add the following validators to swe-validator.ts:

validateBoolean()

export function validateBoolean(data: unknown, validateConstraints = true): ValidationResult {
  const errors: ValidationError[] = [];

  if (!hasDataComponentProperties(data)) {
    errors.push({ message: 'Missing required DataComponent properties' });
    return { valid: false, errors };
  }

  const component = data as any;

  if (component.type !== 'Boolean') {
    errors.push({ message: `Expected type 'Boolean', got '${component.type}'` });
  }

  // Validate value if present
  if (component.value !== undefined && component.value !== null) {
    if (typeof component.value !== 'boolean') {
      errors.push({
        path: 'value',
        message: `Expected boolean value, got ${typeof component.value}`
      });
    }
  }

  return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined };
}

validateVector()

export function validateVector(data: unknown): ValidationResult {
  const errors: ValidationError[] = [];

  if (!hasDataComponentProperties(data)) {
    errors.push({ message: 'Missing required DataComponent properties' });
    return { valid: false, errors };
  }

  const component = data as any;

  if (component.type !== 'Vector') {
    errors.push({ message: `Expected type 'Vector', got '${component.type}'` });
  }

  if (!component.referenceFrame) {
    errors.push({ message: 'Missing required property: referenceFrame' });
  }

  if (!component.coordinates || !Array.isArray(component.coordinates)) {
    errors.push({ message: 'Missing or invalid property: coordinates (must be an array)' });
  } else if (component.coordinates.length < 2) {
    errors.push({ message: 'Vector must have at least 2 coordinates' });
  }

  return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined };
}

validateMatrix()

export function validateMatrix(data: unknown): ValidationResult {
  const errors: ValidationError[] = [];

  if (!hasDataComponentProperties(data)) {
    errors.push({ message: 'Missing required DataComponent properties' });
    return { valid: false, errors };
  }

  const component = data as any;

  if (component.type !== 'Matrix') {
    errors.push({ message: `Expected type 'Matrix', got '${component.type}'` });
  }

  if (!component.elementType) {
    errors.push({ message: 'Missing required property: elementType' });
  }

  if (!component.elementCount) {
    errors.push({ message: 'Missing required property: elementCount' });
  } else if (!Array.isArray(component.elementCount)) {
    errors.push({ message: 'elementCount must be an array of dimensions' });
  } else if (component.elementCount.length < 2) {
    errors.push({ message: 'Matrix must have at least 2 dimensions' });
  } else {
    // Validate each dimension is positive integer
    component.elementCount.forEach((dim: any, idx: number) => {
      if (!Number.isInteger(dim) || dim <= 0) {
        errors.push({
          path: `elementCount[${idx}]`,
          message: `Dimension must be a positive integer, got ${dim}`
        });
      }
    });
  }

  return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined };
}

validateDataStream()

export function validateDataStream(data: unknown): ValidationResult {
  const errors: ValidationError[] = [];

  if (!hasDataComponentProperties(data)) {
    errors.push({ message: 'Missing required DataComponent properties' });
    return { valid: false, errors };
  }

  const component = data as any;

  if (component.type !== 'DataStream') {
    errors.push({ message: `Expected type 'DataStream', got '${component.type}'` });
  }

  if (!component.elementType) {
    errors.push({ message: 'Missing required property: elementType' });
  }

  if (!component.encoding) {
    errors.push({ message: 'Missing required property: encoding' });
  }

  if (!component.values) {
    errors.push({ message: 'Missing required property: values' });
  }

  return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined };
}

validateDataChoice()

export function validateDataChoice(data: unknown): ValidationResult {
  const errors: ValidationError[] = [];

  if (!hasDataComponentProperties(data)) {
    errors.push({ message: 'Missing required DataComponent properties' });
    return { valid: false, errors };
  }

  const component = data as any;

  if (component.type !== 'DataChoice') {
    errors.push({ message: `Expected type 'DataChoice', got '${component.type}'` });
  }

  if (!component.items || !Array.isArray(component.items)) {
    errors.push({ message: 'Missing or invalid property: items (must be an array)' });
  } else if (component.items.length === 0) {
    errors.push({ message: 'DataChoice must have at least one item' });
  } else {
    // Check for duplicate names
    const names = new Set();
    component.items.forEach((item: any, idx: number) => {
      if (!item.name) {
        errors.push({
          path: `items[${idx}]`,
          message: 'Item missing required property: name'
        });
      } else if (names.has(item.name)) {
        errors.push({
          path: `items[${idx}]`,
          message: `Duplicate item name: ${item.name}`
        });
      } else {
        names.add(item.name);
      }

      if (!item.component) {
        errors.push({
          path: `items[${idx}]`,
          message: 'Item missing required property: component'
        });
      }
    });
  }

  return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined };
}

validateGeometry()

export function validateGeometry(data: unknown): ValidationResult {
  const errors: ValidationError[] = [];

  if (!hasDataComponentProperties(data)) {
    errors.push({ message: 'Missing required DataComponent properties' });
    return { valid: false, errors };
  }

  const component = data as any;

  const validTypes = ['Geometry', 'GeometryData'];
  if (!validTypes.includes(component.type)) {
    errors.push({ message: `Expected type 'Geometry' or 'GeometryData', got '${component.type}'` });
  }

  if (!component.geometry) {
    errors.push({ message: 'Missing required property: geometry' });
  } else {
    const geom = component.geometry;
    
    if (!geom.type) {
      errors.push({
        path: 'geometry.type',
        message: 'Missing geometry type'
      });
    } else {
      const validGeomTypes = ['Point', 'LineString', 'Polygon', 'MultiPoint', 'MultiLineString', 'MultiPolygon'];
      if (!validGeomTypes.includes(geom.type)) {
        errors.push({
          path: 'geometry.type',
          message: `Invalid geometry type: ${geom.type}`
        });
      }
    }

    if (!geom.coordinates) {
      errors.push({
        path: 'geometry.coordinates',
        message: 'Missing geometry coordinates'
      });
    } else if (!Array.isArray(geom.coordinates)) {
      errors.push({
        path: 'geometry.coordinates',
        message: 'Geometry coordinates must be an array'
      });
    }
  }

  return { valid: errors.length === 0, errors: errors.length > 0 ? errors : undefined };
}

2. Update Generic Validator Dispatcher

Update validateSWEComponent() (lines 323-370) to route to new validators:

export function validateSWEComponent(component: unknown): ValidationResult {
  // ... existing type check ...

  const componentType = (component as any).type;

  switch (componentType) {
    // Existing cases...
    case 'Boolean':
      return validateBoolean(component);
    case 'Vector':
      return validateVector(component);
    case 'Matrix':
      return validateMatrix(component);
    case 'DataStream':
      return validateDataStream(component);
    case 'DataChoice':
      return validateDataChoice(component);
    case 'Geometry':
    case 'GeometryData':
      return validateGeometry(component);
    // ... other cases ...
    default:
      return { valid: true };
  }
}

3. Add Comprehensive Tests

Add test suites for each new validator in swe-validator.spec.ts:

Test Coverage Goals:

  • Valid component structure (pass)
  • Missing required properties (fail)
  • Wrong type (fail)
  • Invalid property types (fail)
  • Edge cases (empty arrays, null values, etc.)
  • Integration with generic validator

Example Test Structure:

describe('validateVector()', () => {
  it('should validate valid Vector', () => {
    const vector = {
      type: 'Vector',
      definition: 'http://example.org/location',
      label: 'Location',
      referenceFrame: 'http://www.opengis.net/def/crs/EPSG/0/4979',
      coordinates: [
        { name: 'x', component: { type: 'Quantity', uom: { code: 'm' } } },
        { name: 'y', component: { type: 'Quantity', uom: { code: 'm' } } }
      ]
    };

    const result = validateVector(vector);
    expect(result.valid).toBe(true);
  });

  it('should reject Vector without referenceFrame', () => {
    const vector = {
      type: 'Vector',
      coordinates: []
    };

    const result = validateVector(vector as any);
    expect(result.valid).toBe(false);
    expect(result.errors).toContainEqual({ message: 'Missing required property: referenceFrame' });
  });

  it('should reject Vector with less than 2 coordinates', () => {
    const vector = {
      type: 'Vector',
      referenceFrame: 'http://www.opengis.net/def/crs/EPSG/0/4979',
      coordinates: [{ name: 'x', component: {} }]
    };

    const result = validateVector(vector);
    expect(result.valid).toBe(false);
    expect(result.errors).toContainEqual({ message: 'Vector must have at least 2 coordinates' });
  });
});

4. Update Test Count and Coverage

Expected Impact:

  • Test count: 78 → ~110 tests (add ~32 tests, 5-6 per validator)
  • Code coverage: 73.68% → 90%+ (target: validate all 15 component types)
  • Function coverage: 83.33% (5/6) → 100% (all functions tested)

Acceptance Criteria

  • All 6 validators implemented with consistent API (data parameter, optional validateConstraints)
  • validateSWEComponent() routes to all 6 new validators
  • Each validator checks required properties per SWE Common 3.0 specification
  • Boolean validator checks value is boolean type (if present)
  • Vector validator requires referenceFrame and ≥2 coordinates
  • Matrix validator requires elementType and validates elementCount array dimensions
  • DataStream validator requires elementType, encoding, and values
  • DataChoice validator requires items array with unique names
  • Geometry validator requires geometry object with valid type and coordinates
  • All validators follow error accumulation pattern (validate all issues, not fail-fast)
  • All validators include path property in errors for debugging
  • Comprehensive test suite added (~32 tests, 5-6 per validator)
  • Tests cover valid structure, missing properties, invalid types, edge cases
  • Code coverage increases to 90%+ (currently 73.68%)
  • Function coverage reaches 100% (currently 83.33%)
  • All tests pass in CI pipeline
  • Documentation updated with validator usage examples

Notes

Related Issues:

Implementation Order:

  1. Boolean (simplest - just type check)
  2. Vector (requires referenceFrame and coordinates)
  3. Matrix (similar to DataArray but with multi-dimensional elementCount)
  4. DataChoice (similar to DataRecord but with items)
  5. DataStream (complex - elementType, encoding, values)
  6. Geometry (complex - GeoJSON geometry validation)

Future Enhancements (separate issues):

OGC Specification References:

  • OGC 24-014: OGC API - Connected Systems Part 1: Feature Resources
  • SWE Common 3.0: Sensor Web Enablement Common Data Model
  • Component requirements: definition (URI), label (string), optional description

Justification

Why This Matters:

  1. Data Quality: Invalid sensor data structures currently pass validation, leading to runtime errors during data processing
  2. Specification Compliance: OGC SWE Common 3.0 requires validation of all component types
  3. Error Detection: Missing validators allow structurally broken data to enter the system
  4. Developer Experience: Clear validation errors help developers identify data structure issues quickly
  5. Coverage Gap: 40% of claimed validators don't exist (6 of 15 component types)
  6. Consistency: Existing validators are well-implemented; missing validators leave gaps in coverage

Evidence of Need:

  • Current coverage: 73.68% (15 statements uncovered)
  • Current function coverage: 83.33% (1 function untested)
  • 6 component types return { valid: true } without validation
  • Validation report confirms: "Boolean, Vector, Matrix, DataStream, DataChoice, Geometry validators do NOT exist"
  • Tests demonstrate these types are used in real-world SWE Common data

Risk of Not Implementing:

  • Invalid Vector data (missing referenceFrame) passes validation
  • Matrix without dimensions accepted
  • DataStream without encoding accepted
  • Broken geometry objects pass validation
  • Parsers cannot rely on validated data structure
  • Downstream systems receive invalid sensor data

Related Work Items: camptocamp#23
Priority: Medium
Type: Enhancement
Estimated Effort: 3-5 hours (implementation + tests)
Target Coverage: 90%+

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions