Skip to content

Commit b6fbff8

Browse files
authored
Merge pull request #1951 from silx-kit/attributes-guards
Type-check attribute objects and values
2 parents 4823e0a + ddc753c commit b6fbff8

File tree

10 files changed

+239
-200
lines changed

10 files changed

+239
-200
lines changed

packages/app/src/__tests__/NexusPack.test.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,17 +206,13 @@ test('show error/fallback for malformed NeXus entity', async () => {
206206

207207
// Shape of signal dataset is not array
208208
await selectExplorerNode('signal_not_array');
209-
expect(
210-
screen.getByText('Expected dataset to have array shape'),
211-
).toBeVisible();
209+
expect(screen.getByText('Expected array shape')).toBeVisible();
212210
errorSpy.mockClear();
213211

214212
// Type of signal dataset is not numeric
215213
await selectExplorerNode('signal_not_numeric');
216214
expect(
217-
screen.getByText(
218-
'Expected dataset to have numeric, boolean, enum or complex type',
219-
),
215+
screen.getByText('Expected numeric, boolean, enum or complex type'),
220216
).toBeVisible();
221217
errorSpy.mockClear();
222218

packages/app/src/explorer/utils.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import {
22
assertDefined,
33
assertNonNull,
4-
assertStr,
4+
hasScalarShape,
5+
hasStringType,
56
isGroup,
67
} from '@h5web/shared/guards';
78
import { type ChildEntity } from '@h5web/shared/hdf5-models';
89
import { type KeyboardEvent } from 'react';
910

1011
import { type AttrValuesStore } from '../providers/models';
11-
import { hasAttribute } from '../utils';
12+
import { findAttribute, getAttributeValue, hasAttribute } from '../utils';
1213

1314
const SUPPORTED_NX_CLASSES = new Set(['NXdata', 'NXentry', 'NXprocess']);
1415

@@ -26,13 +27,17 @@ export function needsNxBadge(
2627
return true;
2728
}
2829

29-
const nxClass = attrValuesStore.getSingle(entity, 'NX_class');
30-
if (nxClass) {
31-
assertStr(nxClass);
32-
return SUPPORTED_NX_CLASSES.has(nxClass);
30+
const nxClassAttr = findAttribute(entity, 'NX_class');
31+
if (
32+
!nxClassAttr ||
33+
!hasScalarShape(nxClassAttr) ||
34+
!hasStringType(nxClassAttr)
35+
) {
36+
return false;
3337
}
3438

35-
return false;
39+
const nxClass = getAttributeValue(entity, nxClassAttr, attrValuesStore);
40+
return SUPPORTED_NX_CLASSES.has(nxClass);
3641
}
3742

3843
function getButtonList(

packages/app/src/hooks.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type DimensionMapping, getSliceSelection } from '@h5web/lib';
2-
import { assertDatasetValue, isDefined } from '@h5web/shared/guards';
2+
import { assertValue, isDefined } from '@h5web/shared/guards';
33
import {
44
type ArrayShape,
55
type Dataset,
@@ -48,7 +48,7 @@ export function useDatasetValue<D extends Dataset<ArrayShape | ScalarShape>>(
4848
// If `selection` is undefined, the entire dataset will be fetched
4949
const value = valuesStore.get({ dataset, selection });
5050

51-
assertDatasetValue(value, dataset);
51+
assertValue(value, dataset);
5252
return value;
5353
}
5454

@@ -74,7 +74,7 @@ export function useDatasetsValues<D extends Dataset<ArrayShape | ScalarShape>>(
7474
}
7575

7676
const value = valuesStore.get({ dataset, selection });
77-
assertDatasetValue(value, dataset);
77+
assertValue(value, dataset);
7878
return value;
7979
});
8080
}

packages/app/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export {
167167
assertPrintableType,
168168
assertCompoundType,
169169
assertScalarValue,
170-
assertDatasetValue,
170+
assertValue,
171171
} from '@h5web/shared/guards';
172172

173173
// Undocumented

packages/app/src/utils.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
1-
import { type Entity } from '@h5web/shared/hdf5-models';
1+
import { assertValue } from '@h5web/shared/guards';
2+
import {
3+
type Attribute,
4+
type Entity,
5+
type Value,
6+
} from '@h5web/shared/hdf5-models';
27

3-
import { type AttrName } from './providers/models';
8+
import { type AttrName, type AttrValuesStore } from './providers/models';
49

510
export function hasAttribute(entity: Entity, attributeName: AttrName): boolean {
611
return entity.attributes.some((attr) => attr.name === attributeName);
712
}
813

14+
export function findAttribute(
15+
entity: Entity,
16+
attributeName: AttrName,
17+
): Attribute | undefined {
18+
return entity.attributes.find((attr) => attr.name === attributeName);
19+
}
20+
21+
export function getAttributeValue<A extends Attribute>(
22+
entity: Entity,
23+
attribute: A,
24+
attrValuesStore: AttrValuesStore,
25+
): Value<A> {
26+
const value = attrValuesStore.get(entity)[attribute.name];
27+
assertValue(value, attribute);
28+
return value;
29+
}
30+
931
export function enableBigIntSerialization(): void {
1032
// eslint-disable-next-line no-extend-native
1133
Object.defineProperty(BigInt.prototype, 'toJSON', {

packages/app/src/vis-packs/core/hooks.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { type DimensionMapping } from '@h5web/lib';
22
import { createMemo } from '@h5web/shared/createMemo';
3-
import { isDefined } from '@h5web/shared/guards';
3+
import {
4+
hasNumericType,
5+
hasScalarShape,
6+
isDefined,
7+
} from '@h5web/shared/guards';
48
import {
59
type ArrayValue,
610
type Dataset,
@@ -13,7 +17,6 @@ import {
1317
type IgnoreValue,
1418
type NumArray,
1519
} from '@h5web/shared/vis-models';
16-
import { castArray } from '@h5web/shared/vis-utils';
1720
import { type NdArray } from 'ndarray';
1821
import { useMemo } from 'react';
1922

@@ -22,6 +25,7 @@ import {
2225
bigIntTypedArrayFromDType,
2326
typedArrayFromDType,
2427
} from '../../providers/utils';
28+
import { findAttribute, getAttributeValue } from '../../utils';
2529
import { applyMapping, getBaseArray, toNumArray } from './utils';
2630

2731
export const useToNumArray = createMemo(toNumArray);
@@ -99,27 +103,38 @@ export function useMappedArrays(
99103
export function useIgnoreFillValue(dataset: Dataset): IgnoreValue | undefined {
100104
const { attrValuesStore } = useDataContext();
101105

102-
const rawFillValue = attrValuesStore.getSingle(dataset, '_FillValue');
103-
104106
return useMemo(() => {
105-
const wrappedFillValue = castArray(rawFillValue);
107+
const fillValueAttr = findAttribute(dataset, '_FillValue');
108+
109+
if (
110+
!fillValueAttr ||
111+
!hasScalarShape(fillValueAttr) ||
112+
!hasNumericType(fillValueAttr)
113+
) {
114+
return undefined;
115+
}
116+
117+
const rawFillValue = getAttributeValue(
118+
dataset,
119+
fillValueAttr,
120+
attrValuesStore,
121+
);
106122

107123
const DTypedArray = bigIntTypedArrayFromDType(dataset.type)
108124
? Float64Array // matches `useToNumArray` logic
109125
: typedArrayFromDType(dataset.type);
110126

111127
// Cast fillValue in the type of the dataset values to be able to use `===` for the comparison
112-
const fillValue =
113-
DTypedArray && typeof wrappedFillValue[0] === 'number'
114-
? new DTypedArray(wrappedFillValue as number[])[0]
115-
: undefined;
128+
const fillValue = DTypedArray
129+
? new DTypedArray([Number(rawFillValue)])[0]
130+
: undefined;
116131

117132
if (fillValue === undefined) {
118133
return undefined;
119134
}
120135

121136
return (val) => val === fillValue;
122-
}, [dataset, rawFillValue]);
137+
}, [dataset, attrValuesStore]);
123138
}
124139

125140
export function useExportEntries<F extends ExportFormat[]>(

packages/app/src/vis-packs/nexus/containers/NxHeatmapContainer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { DimensionMapper, getSliceSelection } from '@h5web/lib';
22
import {
3-
assertDatasetValue,
43
assertGroup,
54
assertMinDims,
65
assertNumericLikeType,
6+
assertValue,
77
hasComplexType,
88
} from '@h5web/shared/guards';
99
import { useState } from 'react';
@@ -81,7 +81,7 @@ function NxHeatmapContainer(props: VisContainerProps) {
8181
const { signal, axisValues, title } = nxValues;
8282

8383
if (hasComplexType(selectedDataset)) {
84-
assertDatasetValue(signal, selectedDataset);
84+
assertValue(signal, selectedDataset);
8585

8686
return (
8787
<MappedComplexHeatmapVis
@@ -98,7 +98,7 @@ function NxHeatmapContainer(props: VisContainerProps) {
9898
}
9999

100100
assertNumericLikeType(selectedDataset);
101-
assertDatasetValue(signal, selectedDataset);
101+
assertValue(signal, selectedDataset);
102102
return (
103103
<MappedHeatmapVis
104104
dataset={selectedDataset}

packages/shared/src/guards.test.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { assertDatasetValue, assertScalarValue } from './guards';
3+
import { assertScalarValue, assertValue } from './guards';
44
import {
55
boolType,
66
compoundType,
@@ -108,43 +108,37 @@ describe('assertScalarValue', () => {
108108
});
109109
});
110110

111-
describe('assertDatasetValue', () => {
111+
describe('assertValue', () => {
112112
it('should not throw when value satisfies dataset type and shape', () => {
113113
expect(() =>
114-
assertDatasetValue(0, dataset('foo', intType(), [])),
114+
assertValue(0, dataset('foo', intType(), [])),
115115
).not.toThrowError();
116116

117117
expect(() =>
118-
assertDatasetValue(0n, dataset('foo', intType(false, 64), [])),
118+
assertValue(0n, dataset('foo', intType(false, 64), [])),
119119
).not.toThrowError();
120120

121121
expect(() =>
122-
assertDatasetValue('', dataset('foo', strType(), [])),
122+
assertValue('', dataset('foo', strType(), [])),
123123
).not.toThrowError();
124124

125125
expect(() =>
126-
assertDatasetValue(
127-
[true, false],
128-
dataset('foo', boolType(intType()), [2]),
129-
),
126+
assertValue([true, false], dataset('foo', boolType(intType()), [2])),
130127
).not.toThrowError();
131128

132129
expect(() =>
133-
assertDatasetValue(
134-
Float32Array.from([0, 1]),
135-
dataset('foo', floatType(), [2]),
136-
),
130+
assertValue(Float32Array.from([0, 1]), dataset('foo', floatType(), [2])),
137131
).not.toThrowError();
138132

139133
expect(() =>
140-
assertDatasetValue(
134+
assertValue(
141135
BigInt64Array.from([0n, 1n]),
142136
dataset('foo', intType(true, 64), [2]),
143137
),
144138
).not.toThrowError();
145139

146140
expect(() =>
147-
assertDatasetValue(
141+
assertValue(
148142
Float32Array.from([0, 1]), // big ints can be returned as any kind of numbers
149143
dataset('foo', intType(true, 64), [2]),
150144
),
@@ -154,18 +148,15 @@ describe('assertDatasetValue', () => {
154148
describe('assertDatasetValue', () => {
155149
it("should throw when value doesn't satisfy dataset type and shape", () => {
156150
expect(() =>
157-
assertDatasetValue(
158-
true,
159-
dataset('foo', enumType(intType(), { FOO: 0 }), []),
160-
),
151+
assertValue(true, dataset('foo', enumType(intType(), { FOO: 0 }), [])),
161152
).toThrowError('Expected number');
162153

163154
expect(() =>
164-
assertDatasetValue(['foo', 'bar'], dataset('foo', intType(), [2])),
155+
assertValue(['foo', 'bar'], dataset('foo', intType(), [2])),
165156
).toThrowError('Expected number');
166157

167158
expect(() =>
168-
assertDatasetValue(
159+
assertValue(
169160
BigInt64Array.from([0n, 1n]),
170161
dataset('foo', intType(), [2]),
171162
),

0 commit comments

Comments
 (0)