Skip to content

Commit 19b2a54

Browse files
committed
feat: Add SWE format parsing support for Datastream resources (Issue camptocamp#49)
- Implement parseSWE() method to parse SWE DataStream and DataRecord - Add extractSchema() helper for recursive schema extraction - Handle elementType.component wrapper structure correctly - Preserve encoding, constraints, UoM, and semantic information - Add 8 comprehensive tests covering all SWE structures - Leverage existing swe-common-parser.ts infrastructure Resolves: Sam-Bolling/CSAPI-Live-Testing#49
1 parent 769d165 commit 19b2a54

2 files changed

Lines changed: 417 additions & 3 deletions

File tree

src/ogc-api/csapi/parsers/resources.spec.ts

Lines changed: 283 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,289 @@ describe('Resource Parsers', () => {
299299
expect(() => parser.parse(sensorml, { contentType: 'application/sml+json' })).toThrow('Datastreams not defined in SensorML format');
300300
});
301301

302-
it('should throw on SWE format (not supported)', () => {
303-
expect(() => parser.parse({}, { contentType: 'application/swe+json' })).toThrow('SWE format not applicable');
302+
describe('parseSWE', () => {
303+
it('should parse SWE DataStream with DataRecord elementType', () => {
304+
const sweDataStream = {
305+
type: 'DataStream',
306+
id: 'ds-001',
307+
definition: 'http://example.org/datastreams/temp-humidity',
308+
label: 'Temperature and Humidity',
309+
description: 'Continuous temperature and humidity measurements',
310+
elementType: {
311+
component: {
312+
type: 'DataRecord',
313+
definition: 'http://example.org/observation-schema',
314+
label: 'Observation',
315+
fields: [
316+
{
317+
name: 'timestamp',
318+
component: {
319+
type: 'Time',
320+
definition: 'http://www.opengis.net/def/property/OGC/0/SamplingTime',
321+
label: 'Sampling Time',
322+
uom: {
323+
code: 'http://www.opengis.net/def/uom/ISO-8601/0/Gregorian'
324+
}
325+
}
326+
},
327+
{
328+
name: 'temperature',
329+
component: {
330+
type: 'Quantity',
331+
definition: 'http://example.org/def/temperature',
332+
label: 'Temperature',
333+
uom: { code: 'Cel' }
334+
}
335+
}
336+
]
337+
}
338+
},
339+
encoding: {
340+
type: 'JSONEncoding'
341+
}
342+
};
343+
344+
const result = parser.parse(sweDataStream, { contentType: 'application/swe+json' });
345+
346+
expect(result.data.type).toBe('Feature');
347+
expect(result.data.geometry).toBeNull();
348+
expect(result.data.properties.featureType).toBe('Datastream');
349+
expect(result.data.properties.name).toBe('Temperature and Humidity');
350+
expect(result.data.properties.schema).toBeDefined();
351+
expect(result.data.properties.schema.type).toBe('DataStream');
352+
expect(result.data.properties.schema.elementType.type).toBe('DataRecord');
353+
expect(result.data.properties.schema.elementType.fields).toHaveLength(2);
354+
expect(result.data.properties.schema.elementType.fields[0].name).toBe('timestamp');
355+
expect(result.data.properties.schema.elementType.fields[0].type).toBe('Time');
356+
expect(result.data.properties.schema.elementType.fields[1].name).toBe('temperature');
357+
expect(result.data.properties.schema.elementType.fields[1].type).toBe('Quantity');
358+
expect(result.data.properties.encoding).toEqual({ type: 'JSONEncoding' });
359+
expect(result.format.format).toBe('swe');
360+
});
361+
362+
it('should parse SWE DataRecord schema directly', () => {
363+
const sweDataRecord = {
364+
type: 'DataRecord',
365+
id: 'schema-001',
366+
definition: 'http://example.org/observation-schema',
367+
label: 'Observation Schema',
368+
fields: [
369+
{
370+
name: 'value',
371+
component: {
372+
type: 'Quantity',
373+
definition: 'http://example.org/def/measurement',
374+
label: 'Measurement',
375+
uom: { code: 'm' }
376+
}
377+
}
378+
]
379+
};
380+
381+
const result = parser.parse(sweDataRecord, { contentType: 'application/swe+json' });
382+
383+
expect(result.data.type).toBe('Feature');
384+
expect(result.data.properties.schema.type).toBe('DataRecord');
385+
expect(result.data.properties.schema.fields).toHaveLength(1);
386+
expect(result.data.properties.schema.fields[0].name).toBe('value');
387+
expect(result.data.properties.schema.fields[0].type).toBe('Quantity');
388+
expect(result.data.properties.schema.fields[0].uom).toEqual({ code: 'm' });
389+
});
390+
391+
it('should handle nested DataRecord in DataStream', () => {
392+
const nestedSchema = {
393+
type: 'DataStream',
394+
definition: 'http://example.org/complex-stream',
395+
label: 'Complex Stream',
396+
elementType: {
397+
component: {
398+
type: 'DataRecord',
399+
definition: 'http://example.org/complex-record',
400+
label: 'Complex Record',
401+
fields: [
402+
{
403+
name: 'metadata',
404+
component: {
405+
type: 'DataRecord',
406+
definition: 'http://example.org/metadata',
407+
label: 'Metadata',
408+
fields: [
409+
{
410+
name: 'quality',
411+
component: {
412+
type: 'Text',
413+
definition: 'http://example.org/quality',
414+
label: 'Quality Flag'
415+
}
416+
}
417+
]
418+
}
419+
}
420+
]
421+
}
422+
}
423+
};
424+
425+
const result = parser.parse(nestedSchema, { contentType: 'application/swe+json' });
426+
427+
expect(result.data.properties.schema.elementType.fields[0].name).toBe('metadata');
428+
expect(result.data.properties.schema.elementType.fields[0].type).toBe('DataRecord');
429+
});
430+
431+
it('should handle DataArray elementType', () => {
432+
const dataArraySchema = {
433+
type: 'DataStream',
434+
definition: 'http://example.org/trajectory',
435+
label: 'Trajectory',
436+
elementType: {
437+
component: {
438+
type: 'DataArray',
439+
definition: 'http://example.org/positions',
440+
label: 'Positions',
441+
elementCount: 100,
442+
elementType: {
443+
component: {
444+
type: 'Vector',
445+
definition: 'http://example.org/position',
446+
label: 'Position',
447+
referenceFrame: 'http://www.opengis.net/def/crs/EPSG/0/4979',
448+
coordinates: [
449+
{
450+
name: 'lat',
451+
component: {
452+
type: 'Quantity',
453+
definition: 'http://example.org/latitude',
454+
label: 'Latitude',
455+
uom: { code: 'deg' }
456+
}
457+
}
458+
]
459+
}
460+
}
461+
}
462+
}
463+
};
464+
465+
const result = parser.parse(dataArraySchema, { contentType: 'application/swe+json' });
466+
467+
expect(result.data.properties.schema.elementType.type).toBe('DataArray');
468+
expect(result.data.properties.schema.elementType.elementCount).toBe(100);
469+
expect(result.data.properties.schema.elementType.elementType.type).toBe('Vector');
470+
});
471+
472+
it('should preserve constraints in schema', () => {
473+
const constrainedSchema = {
474+
type: 'DataRecord',
475+
definition: 'http://example.org/constrained',
476+
label: 'Constrained Schema',
477+
fields: [
478+
{
479+
name: 'temperature',
480+
component: {
481+
type: 'Quantity',
482+
definition: 'http://example.org/temperature',
483+
label: 'Temperature',
484+
uom: { code: 'Cel' },
485+
constraint: {
486+
intervals: [[-40, 60]],
487+
significantFigures: 2
488+
}
489+
}
490+
}
491+
]
492+
};
493+
494+
const result = parser.parse(constrainedSchema, { contentType: 'application/swe+json' });
495+
496+
expect(result.data.properties.schema.fields[0].constraint).toEqual({
497+
intervals: [[-40, 60]],
498+
significantFigures: 2
499+
});
500+
});
501+
502+
it('should handle parsing errors gracefully', () => {
503+
const invalidSWE = {
504+
type: 'InvalidType',
505+
// Missing required properties
506+
};
507+
508+
expect(() => {
509+
parser.parse(invalidSWE, { contentType: 'application/swe+json' });
510+
}).toThrow();
511+
});
512+
513+
it('should include encoding information', () => {
514+
const streamWithEncoding = {
515+
type: 'DataStream',
516+
definition: 'http://example.org/stream',
517+
label: 'Stream',
518+
elementType: {
519+
component: {
520+
type: 'Quantity',
521+
definition: 'http://example.org/value',
522+
label: 'Value',
523+
uom: { code: 'm' }
524+
}
525+
},
526+
encoding: {
527+
type: 'TextEncoding',
528+
tokenSeparator: ',',
529+
blockSeparator: '\n'
530+
}
531+
};
532+
533+
const result = parser.parse(streamWithEncoding, { contentType: 'application/swe+json' });
534+
535+
expect(result.data.properties.encoding).toEqual({
536+
type: 'TextEncoding',
537+
tokenSeparator: ',',
538+
blockSeparator: '\n'
539+
});
540+
});
541+
542+
it('should handle Vector component extraction', () => {
543+
const vectorSchema = {
544+
type: 'DataStream',
545+
definition: 'http://example.org/location-stream',
546+
label: 'Location Stream',
547+
elementType: {
548+
component: {
549+
type: 'Vector',
550+
definition: 'http://example.org/location',
551+
label: 'Location',
552+
referenceFrame: 'http://www.opengis.net/def/crs/EPSG/0/4326',
553+
coordinates: [
554+
{
555+
name: 'lon',
556+
component: {
557+
type: 'Quantity',
558+
definition: 'http://example.org/longitude',
559+
label: 'Longitude',
560+
uom: { code: 'deg' }
561+
}
562+
},
563+
{
564+
name: 'lat',
565+
component: {
566+
type: 'Quantity',
567+
definition: 'http://example.org/latitude',
568+
label: 'Latitude',
569+
uom: { code: 'deg' }
570+
}
571+
}
572+
]
573+
}
574+
}
575+
};
576+
577+
const result = parser.parse(vectorSchema, { contentType: 'application/swe+json' });
578+
579+
expect(result.data.properties.schema.elementType.type).toBe('Vector');
580+
expect(result.data.properties.schema.elementType.referenceFrame).toBe('http://www.opengis.net/def/crs/EPSG/0/4326');
581+
expect(result.data.properties.schema.elementType.coordinates).toHaveLength(2);
582+
expect(result.data.properties.schema.elementType.coordinates[0].name).toBe('lon');
583+
expect(result.data.properties.schema.elementType.coordinates[1].name).toBe('lat');
584+
});
304585
});
305586
});
306587

0 commit comments

Comments
 (0)