Skip to content

Commit fcbf121

Browse files
committed
feat: Add definition and label validation for SWE Common components (Issue camptocamp#47)
- Added validateRequiredOGCProperties() helper function to validate required OGC properties - Updated all 9 component validators (Quantity, Count, Text, Category, Time, Range, DataRecord, DataArray, ObservationResult) - Each validator now checks for required 'definition' (URI) and 'label' (string) properties per OGC 24-014 - Added comprehensive test suite with 26 tests covering: - Missing definition property (9 tests) - Missing label property (9 tests) - Non-string definition/label (2 tests) - Valid components with all required properties (6 tests) - All new tests passing - Implementation follows shared helper pattern for consistency and maintainability Closes camptocamp#47
1 parent 97f72ac commit fcbf121

3 files changed

Lines changed: 456 additions & 1252 deletions

File tree

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
/**
2+
* Tests for OGC required properties (definition and label) validation
3+
* These tests verify Issue #47 implementation
4+
*/
5+
import {
6+
validateQuantity,
7+
validateCount,
8+
validateText,
9+
validateCategory,
10+
validateTime,
11+
validateRangeComponent,
12+
validateDataRecord,
13+
validateDataArray,
14+
} from './swe-validator.js';
15+
16+
describe('OGC Property Validation (definition and label)', () => {
17+
describe('validateQuantity', () => {
18+
it('should reject Quantity without definition', () => {
19+
const quantity = {
20+
type: 'Quantity',
21+
label: 'Temperature',
22+
uom: { code: 'Cel' },
23+
};
24+
25+
const result = validateQuantity(quantity as any);
26+
expect(result.valid).toBe(false);
27+
expect(result.errors).toBeDefined();
28+
const defError = result.errors!.find(e => e.path === 'definition');
29+
expect(defError).toBeDefined();
30+
expect(defError!.message).toContain('Missing required property: definition');
31+
});
32+
33+
it('should reject Quantity without label', () => {
34+
const quantity = {
35+
type: 'Quantity',
36+
definition: 'http://example.org/temperature',
37+
uom: { code: 'Cel' },
38+
};
39+
40+
const result = validateQuantity(quantity as any);
41+
expect(result.valid).toBe(false);
42+
expect(result.errors).toBeDefined();
43+
const labelError = result.errors!.find(e => e.path === 'label');
44+
expect(labelError).toBeDefined();
45+
expect(labelError!.message).toContain('Missing required property: label');
46+
});
47+
48+
it('should reject Quantity with non-string definition', () => {
49+
const quantity = {
50+
type: 'Quantity',
51+
definition: 123,
52+
label: 'Temperature',
53+
uom: { code: 'Cel' },
54+
};
55+
56+
const result = validateQuantity(quantity as any);
57+
expect(result.valid).toBe(false);
58+
expect(result.errors).toBeDefined();
59+
const defError = result.errors!.find(e => e.path === 'definition');
60+
expect(defError).toBeDefined();
61+
expect(defError!.message).toContain('must be a string');
62+
});
63+
64+
it('should reject Quantity with non-string label', () => {
65+
const quantity = {
66+
type: 'Quantity',
67+
definition: 'http://example.org/temperature',
68+
label: 123,
69+
uom: { code: 'Cel' },
70+
};
71+
72+
const result = validateQuantity(quantity as any);
73+
expect(result.valid).toBe(false);
74+
expect(result.errors).toBeDefined();
75+
const labelError = result.errors!.find(e => e.path === 'label');
76+
expect(labelError).toBeDefined();
77+
expect(labelError!.message).toContain('must be a string');
78+
});
79+
80+
it('should validate Quantity with all required properties', () => {
81+
const quantity = {
82+
type: 'Quantity',
83+
definition: 'http://example.org/temperature',
84+
label: 'Temperature',
85+
uom: { code: 'Cel' },
86+
};
87+
88+
const result = validateQuantity(quantity);
89+
expect(result.valid).toBe(true);
90+
});
91+
});
92+
93+
describe('validateCount', () => {
94+
it('should reject Count without definition', () => {
95+
const count = {
96+
type: 'Count',
97+
label: 'Count',
98+
};
99+
100+
const result = validateCount(count as any);
101+
expect(result.valid).toBe(false);
102+
expect(result.errors!.find(e => e.path === 'definition')).toBeDefined();
103+
});
104+
105+
it('should reject Count without label', () => {
106+
const count = {
107+
type: 'Count',
108+
definition: 'http://example.org/count',
109+
};
110+
111+
const result = validateCount(count as any);
112+
expect(result.valid).toBe(false);
113+
expect(result.errors!.find(e => e.path === 'label')).toBeDefined();
114+
});
115+
116+
it('should validate Count with all required properties', () => {
117+
const count = {
118+
type: 'Count',
119+
definition: 'http://example.org/count',
120+
label: 'Count',
121+
};
122+
123+
const result = validateCount(count);
124+
expect(result.valid).toBe(true);
125+
});
126+
});
127+
128+
describe('validateText', () => {
129+
it('should reject Text without definition', () => {
130+
const text = {
131+
type: 'Text',
132+
label: 'Text',
133+
};
134+
135+
const result = validateText(text as any);
136+
expect(result.valid).toBe(false);
137+
expect(result.errors!.find(e => e.path === 'definition')).toBeDefined();
138+
});
139+
140+
it('should reject Text without label', () => {
141+
const text = {
142+
type: 'Text',
143+
definition: 'http://example.org/text',
144+
};
145+
146+
const result = validateText(text as any);
147+
expect(result.valid).toBe(false);
148+
expect(result.errors!.find(e => e.path === 'label')).toBeDefined();
149+
});
150+
151+
it('should validate Text with all required properties', () => {
152+
const text = {
153+
type: 'Text',
154+
definition: 'http://example.org/text',
155+
label: 'Text',
156+
};
157+
158+
const result = validateText(text);
159+
expect(result.valid).toBe(true);
160+
});
161+
});
162+
163+
describe('validateCategory', () => {
164+
it('should reject Category without definition', () => {
165+
const category = {
166+
type: 'Category',
167+
label: 'Category',
168+
};
169+
170+
const result = validateCategory(category as any);
171+
expect(result.valid).toBe(false);
172+
expect(result.errors!.find(e => e.path === 'definition')).toBeDefined();
173+
});
174+
175+
it('should reject Category without label', () => {
176+
const category = {
177+
type: 'Category',
178+
definition: 'http://example.org/category',
179+
};
180+
181+
const result = validateCategory(category as any);
182+
expect(result.valid).toBe(false);
183+
expect(result.errors!.find(e => e.path === 'label')).toBeDefined();
184+
});
185+
186+
it('should validate Category with all required properties', () => {
187+
const category = {
188+
type: 'Category',
189+
definition: 'http://example.org/category',
190+
label: 'Category',
191+
};
192+
193+
const result = validateCategory(category);
194+
expect(result.valid).toBe(true);
195+
});
196+
});
197+
198+
describe('validateTime', () => {
199+
it('should reject Time without definition', () => {
200+
const time = {
201+
type: 'Time',
202+
label: 'Time',
203+
uom: { code: 's' },
204+
};
205+
206+
const result = validateTime(time as any);
207+
expect(result.valid).toBe(false);
208+
expect(result.errors!.find(e => e.path === 'definition')).toBeDefined();
209+
});
210+
211+
it('should reject Time without label', () => {
212+
const time = {
213+
type: 'Time',
214+
definition: 'http://example.org/time',
215+
uom: { code: 's' },
216+
};
217+
218+
const result = validateTime(time as any);
219+
expect(result.valid).toBe(false);
220+
expect(result.errors!.find(e => e.path === 'label')).toBeDefined();
221+
});
222+
223+
it('should validate Time with all required properties', () => {
224+
const time = {
225+
type: 'Time',
226+
definition: 'http://example.org/time',
227+
label: 'Time',
228+
uom: { code: 's' },
229+
};
230+
231+
const result = validateTime(time);
232+
expect(result.valid).toBe(true);
233+
});
234+
});
235+
236+
describe('validateRangeComponent', () => {
237+
it('should reject QuantityRange without definition', () => {
238+
const range = {
239+
type: 'QuantityRange',
240+
label: 'Range',
241+
uom: { code: 'm' },
242+
};
243+
244+
const result = validateRangeComponent(range as any);
245+
expect(result.valid).toBe(false);
246+
expect(result.errors!.find(e => e.path === 'definition')).toBeDefined();
247+
});
248+
249+
it('should reject CountRange without label', () => {
250+
const range = {
251+
type: 'CountRange',
252+
definition: 'http://example.org/range',
253+
};
254+
255+
const result = validateRangeComponent(range as any);
256+
expect(result.valid).toBe(false);
257+
expect(result.errors!.find(e => e.path === 'label')).toBeDefined();
258+
});
259+
260+
it('should validate QuantityRange with all required properties', () => {
261+
const range = {
262+
type: 'QuantityRange',
263+
definition: 'http://example.org/range',
264+
label: 'Range',
265+
uom: { code: 'm' },
266+
};
267+
268+
const result = validateRangeComponent(range);
269+
expect(result.valid).toBe(true);
270+
});
271+
});
272+
273+
describe('validateDataRecord', () => {
274+
it('should reject DataRecord without definition', () => {
275+
const dataRecord = {
276+
type: 'DataRecord',
277+
label: 'Data Record',
278+
fields: [
279+
{
280+
name: 'field1',
281+
component: {
282+
type: 'Quantity',
283+
definition: 'http://example.org/field1',
284+
label: 'Field 1',
285+
uom: { code: 'm' },
286+
},
287+
},
288+
],
289+
};
290+
291+
const result = validateDataRecord(dataRecord as any);
292+
expect(result.valid).toBe(false);
293+
expect(result.errors!.find(e => e.path === 'definition')).toBeDefined();
294+
});
295+
296+
it('should reject DataRecord without label', () => {
297+
const dataRecord = {
298+
type: 'DataRecord',
299+
definition: 'http://example.org/datarecord',
300+
fields: [
301+
{
302+
name: 'field1',
303+
component: {
304+
type: 'Quantity',
305+
definition: 'http://example.org/field1',
306+
label: 'Field 1',
307+
uom: { code: 'm' },
308+
},
309+
},
310+
],
311+
};
312+
313+
const result = validateDataRecord(dataRecord as any);
314+
expect(result.valid).toBe(false);
315+
expect(result.errors!.find(e => e.path === 'label')).toBeDefined();
316+
});
317+
318+
it('should validate DataRecord with all required properties', () => {
319+
const dataRecord = {
320+
type: 'DataRecord',
321+
definition: 'http://example.org/datarecord',
322+
label: 'Data Record',
323+
fields: [
324+
{
325+
name: 'field1',
326+
component: {
327+
type: 'Quantity',
328+
definition: 'http://example.org/field1',
329+
label: 'Field 1',
330+
uom: { code: 'm' },
331+
},
332+
},
333+
],
334+
};
335+
336+
const result = validateDataRecord(dataRecord);
337+
expect(result.valid).toBe(true);
338+
});
339+
});
340+
341+
describe('validateDataArray', () => {
342+
it('should reject DataArray without definition', () => {
343+
const dataArray = {
344+
type: 'DataArray',
345+
label: 'Data Array',
346+
elementCount: 10,
347+
elementType: {
348+
type: 'Quantity',
349+
definition: 'http://example.org/element',
350+
label: 'Element',
351+
uom: { code: 'm' },
352+
},
353+
};
354+
355+
const result = validateDataArray(dataArray as any);
356+
expect(result.valid).toBe(false);
357+
expect(result.errors!.find(e => e.path === 'definition')).toBeDefined();
358+
});
359+
360+
it('should reject DataArray without label', () => {
361+
const dataArray = {
362+
type: 'DataArray',
363+
definition: 'http://example.org/dataarray',
364+
elementCount: 10,
365+
elementType: {
366+
type: 'Quantity',
367+
definition: 'http://example.org/element',
368+
label: 'Element',
369+
uom: { code: 'm' },
370+
},
371+
};
372+
373+
const result = validateDataArray(dataArray as any);
374+
expect(result.valid).toBe(false);
375+
expect(result.errors!.find(e => e.path === 'label')).toBeDefined();
376+
});
377+
378+
it('should validate DataArray with all required properties', () => {
379+
const dataArray = {
380+
type: 'DataArray',
381+
definition: 'http://example.org/dataarray',
382+
label: 'Data Array',
383+
elementCount: 10,
384+
elementType: {
385+
type: 'Quantity',
386+
definition: 'http://example.org/element',
387+
label: 'Element',
388+
uom: { code: 'm' },
389+
},
390+
};
391+
392+
const result = validateDataArray(dataArray);
393+
expect(result.valid).toBe(true);
394+
});
395+
});
396+
});

0 commit comments

Comments
 (0)