Skip to content

Commit 5ec3df7

Browse files
committed
fix(lib): support Vector/DataArray/Matrix/DataChoice/Geometry in DataRecord fields
DataRecord's parseField() only handled simple scalars and nested DataRecords. Complex types (Vector, DataArray, Matrix, DataChoice, Geometry) threw 'unsupported component type'. This broke control stream schema display for OSH FCU payloads containing Vector fields. Fix: parseDataRecord() now accepts an optional componentParser callback. parser.ts passes parseSWEComponent as the callback, breaking the circular dependency cleanly. When provided, any SWE component type can appear as a DataRecord field. Also fixes schema parse error display in SweSchemaDisplay.vue - errors were silently swallowed when raw schema was available but parsing failed. Adds test: control stream schema with Vector field (real OSH FCU pattern).
1 parent 2fa375a commit 5ec3df7

File tree

4 files changed

+91
-12
lines changed

4 files changed

+91
-12
lines changed

demo/src/components/SweSchemaDisplay.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,8 @@ function shortenUri(uri: string): string {
269269
{{ error }}
270270
</Message>
271271

272-
<!-- Parse warning -->
273-
<div v-if="error && schemaFields.length" class="schema-parse-warn">
272+
<!-- Parse warning (raw data available but parsing had issues) -->
273+
<div v-if="error && rawSchema" class="schema-parse-warn">
274274
<i class="pi pi-exclamation-triangle"></i>
275275
<span>{{ error }}</span>
276276
</div>

src/ogc-api/csapi/formats/schema-response.spec.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
SweBoolean,
1313
SweCount,
1414
SweCategory,
15+
Vector,
1516
} from './swecommon/types.js';
1617

1718
describe('parseDatastreamSchemaResponse', () => {
@@ -368,7 +369,65 @@ describe('parseControlStreamSchemaResponse', () => {
368369
});
369370

370371
// ========================================
371-
// Test 4: Non-object input
372+
// Test 4: DataRecord with Vector field (real OSH payload)
373+
// ========================================
374+
375+
it('parses a control stream schema with Vector fields (OSH FCU pattern)', () => {
376+
const raw = {
377+
commandFormat: 'application/json',
378+
parametersSchema: {
379+
type: 'DataRecord',
380+
name: 'mavControl',
381+
label: 'Location Control',
382+
description: 'Interfaces with MAVLINK and OSH to effectuate control over the platform',
383+
fields: [
384+
{
385+
type: 'Vector',
386+
name: 'locationVectorLLA',
387+
referenceFrame: '',
388+
coordinates: [
389+
{ type: 'Quantity', name: 'Latitude', uom: { code: 'deg' } },
390+
{ type: 'Quantity', name: 'Longitude', uom: { code: 'deg' } },
391+
{ type: 'Quantity', name: 'AltitudeAGL', uom: { code: 'm' } },
392+
],
393+
},
394+
{ type: 'Boolean', name: 'returnToStart' },
395+
{ type: 'Count', name: 'hoverSeconds' },
396+
],
397+
},
398+
};
399+
const schema = parseControlStreamSchemaResponse(raw);
400+
expect(schema.commandFormat).toBe('application/json');
401+
expect(schema.parametersSchema).toBeDefined();
402+
403+
const dr = schema.parametersSchema as DataRecord;
404+
expect(dr.type).toBe('DataRecord');
405+
expect(dr.fields).toHaveLength(3);
406+
407+
// Field 0: Vector with 3 coordinates
408+
const vf = dr.fields[0] as TypedDataField;
409+
expect(vf.name).toBe('locationVectorLLA');
410+
const vec = vf.component as Vector;
411+
expect(vec.type).toBe('Vector');
412+
expect(vec.coordinates).toHaveLength(3);
413+
expect(vec.coordinates[0].name).toBe('Latitude');
414+
expect(((vec.coordinates[0] as TypedDataField).component as SweQuantity).uom).toEqual({ code: 'deg' });
415+
expect(vec.coordinates[2].name).toBe('AltitudeAGL');
416+
expect(((vec.coordinates[2] as TypedDataField).component as SweQuantity).uom).toEqual({ code: 'm' });
417+
418+
// Field 1: Boolean
419+
const bf = dr.fields[1] as TypedDataField;
420+
expect(bf.name).toBe('returnToStart');
421+
expect((bf.component as SweBoolean).type).toBe('Boolean');
422+
423+
// Field 2: Count
424+
const cf = dr.fields[2] as TypedDataField;
425+
expect(cf.name).toBe('hoverSeconds');
426+
expect((cf.component as SweCount).type).toBe('Count');
427+
});
428+
429+
// ========================================
430+
// Test 5: Non-object input
372431
// ========================================
373432

374433
it('throws on non-object input', () => {

src/ogc-api/csapi/formats/swecommon/data-record.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,20 @@ import type {
3838
DataRecord,
3939
DataField,
4040
TypedDataField,
41+
AnyComponent,
4142
} from './types.js';
4243

4344
import { parseSimpleComponent, SweCommonParseError } from './components.js';
4445
import { isRecord, parseBaseProperties } from './_helpers.js';
4546

47+
/**
48+
* Callback type for parsing any SWE component.
49+
* Used to break the circular dependency between data-record.ts and parser.ts.
50+
* When provided, enables DataRecord fields to contain complex types like
51+
* Vector, DataArray, Matrix, DataChoice, and Geometry.
52+
*/
53+
export type ComponentParser = (json: unknown) => AnyComponent;
54+
4655
// ========================================
4756
// Internal Helpers
4857
// ========================================
@@ -86,7 +95,7 @@ function isLinkReference(json: Record<string, unknown>): boolean {
8695
*
8796
* @see https://docs.ogc.org/is/24-014/24-014.html — SoftNamedProperty
8897
*/
89-
function parseField(json: unknown, index: number): DataField | TypedDataField {
98+
function parseField(json: unknown, index: number, componentParser?: ComponentParser): DataField | TypedDataField {
9099
if (!isRecord(json)) {
91100
throw new SweCommonParseError(
92101
`DataRecord field at index ${index} must be a non-null object`,
@@ -122,11 +131,11 @@ function parseField(json: unknown, index: number): DataField | TypedDataField {
122131
);
123132
}
124133

125-
// Recursive DataRecord
134+
// Recursive DataRecord — pass componentParser through for nested records
126135
if (type === 'DataRecord') {
127136
return {
128137
name,
129-
component: parseDataRecord(json),
138+
component: parseDataRecord(json, componentParser),
130139
} as TypedDataField;
131140
}
132141

@@ -138,10 +147,21 @@ function parseField(json: unknown, index: number): DataField | TypedDataField {
138147
} as TypedDataField;
139148
}
140149

141-
// Future complex types (DataArray, Vector, Matrix, DataChoice, Geometry)
142-
// are not yet implemented — throw with field context
150+
// Complex types (Vector, DataArray, Matrix, DataChoice, Geometry)
151+
// Delegate to the injected componentParser when available.
152+
// This breaks the circular dependency: parser.ts provides parseSWEComponent
153+
// as the callback, enabling DataRecord fields to contain any SWE component.
154+
if (componentParser) {
155+
return {
156+
name,
157+
component: componentParser(json),
158+
} as TypedDataField;
159+
}
160+
143161
throw new SweCommonParseError(
144-
`DataRecord field "${name}" has unsupported component type: "${type}"`,
162+
`DataRecord field "${name}" has unsupported component type: "${type}". ` +
163+
`Complex types (Vector, DataArray, Matrix, DataChoice, Geometry) require ` +
164+
`a componentParser callback — use parseSWEComponent from parser.ts.`,
145165
`fields[${index}].type`
146166
);
147167
}
@@ -180,7 +200,7 @@ function parseField(json: unknown, index: number): DataField | TypedDataField {
180200
* @see https://docs.ogc.org/is/24-014/24-014.html — DataRecord
181201
* @see OAS: DataRecord (L7593), fields array (L531-L544)
182202
*/
183-
export function parseDataRecord(json: unknown): DataRecord {
203+
export function parseDataRecord(json: unknown, componentParser?: ComponentParser): DataRecord {
184204
if (!isRecord(json)) {
185205
throw new SweCommonParseError(
186206
'DataRecord input must be a non-null object'
@@ -202,7 +222,7 @@ export function parseDataRecord(json: unknown): DataRecord {
202222
}
203223

204224
const fields: DataField[] = (json.fields as unknown[]).map(
205-
(fieldJson, index) => parseField(fieldJson, index)
225+
(fieldJson, index) => parseField(fieldJson, index, componentParser)
206226
);
207227

208228
const result: DataRecord = {

src/ogc-api/csapi/formats/swecommon/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ export function parseSWEComponent(json: unknown): AnyComponent {
741741

742742
// Complex components — delegate to dedicated parsers
743743
case 'DataRecord':
744-
return parseDataRecord(json);
744+
return parseDataRecord(json, parseSWEComponent);
745745

746746
case 'DataArray':
747747
return parseDataArray(json);

0 commit comments

Comments
 (0)