Skip to content

Commit 09daea1

Browse files
authored
Merge pull request #272 from effigies/fix/datatype
feat: Load datatype and modality when building file context
2 parents 8d45151 + 9d71585 commit 09daea1

File tree

7 files changed

+115
-80
lines changed

7 files changed

+115
-80
lines changed

src/schema/context.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { BIDSFile } from '../types/filetree.ts'
1515
import { FileTree } from '../types/filetree.ts'
1616
import { ColumnsMap } from '../types/columns.ts'
1717
import { readEntities } from './entities.ts'
18+
import { findDatatype } from './datatypes.ts'
1819
import { DatasetIssues } from '../issues/datasetIssues.ts'
1920
import { walkBack } from '../files/inheritance.ts'
2021
import { parseGzip } from '../files/gzip.ts'
@@ -147,16 +148,20 @@ export class BIDSContext implements Context {
147148
dsContext?: BIDSContextDataset,
148149
fileTree?: FileTree,
149150
) {
151+
this.dataset = dsContext ? dsContext : new BIDSContextDataset({ tree: fileTree })
152+
150153
this.filenameRules = []
151154
this.file = file
152-
const bidsEntities = readEntities(file.name)
153-
this.suffix = bidsEntities.suffix
154-
this.extension = bidsEntities.extension
155-
this.entities = bidsEntities.entities
156-
this.dataset = dsContext ? dsContext : new BIDSContextDataset({ tree: fileTree })
155+
156+
const { entities, suffix, extension } = readEntities(file.name)
157+
const { datatype, modality } = findDatatype(file, this.dataset.schema)
158+
this.entities = entities
159+
this.suffix = suffix
160+
this.extension = extension
161+
this.datatype = datatype
162+
this.modality = modality
163+
157164
this.subject = {} as Subject
158-
this.datatype = ''
159-
this.modality = ''
160165
this.sidecar = {}
161166
this.sidecarKeyOrigin = {}
162167
this.columns = new ColumnsMap() as Record<string, string[]>

src/schema/datatypes.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { assert, assertObjectMatch } from '@std/assert'
2+
import { loadSchema } from '../setup/loadSchema.ts'
3+
import { BIDSContext, BIDSContextDataset } from '../schema/context.ts'
4+
import { pathsToTree } from '../files/filetree.ts'
5+
import type { BIDSFile } from '../types/filetree.ts'
6+
7+
import { findDatatype, modalityTable } from './datatypes.ts'
8+
9+
const schema = await loadSchema()
10+
11+
// Creates a file object as part of a minimal file tree
12+
const makeFile = (path: string): BIDSFile => pathsToTree([path]).get(path) as BIDSFile
13+
14+
Deno.test('test modalityTable', async (t) => {
15+
await t.step('empty schema', () => {
16+
assertObjectMatch(modalityTable({}), {})
17+
})
18+
19+
await t.step('real schema', async () => {
20+
const table = modalityTable(schema)
21+
// Memoization check
22+
assert(modalityTable(schema) == table)
23+
// spot check
24+
assertObjectMatch(table, {
25+
'anat': 'mri',
26+
'perf': 'mri',
27+
'eeg': 'eeg',
28+
})
29+
})
30+
})
31+
32+
Deno.test('test findDatatype', async (t) => {
33+
await t.step('root files', async () => {
34+
const path = makeFile('/participants.tsv')
35+
assertObjectMatch(findDatatype(path, schema), { datatype: '', modality: '' })
36+
})
37+
await t.step('non-datatype parent', async () => {
38+
const path = makeFile('/stimuli/image.png')
39+
assertObjectMatch(findDatatype(path, schema), { datatype: '', modality: '' })
40+
})
41+
await t.step('phenotype file', async () => {
42+
const path = makeFile('/phenotype/survey.tsv')
43+
assertObjectMatch(findDatatype(path, schema), { datatype: 'phenotype', modality: '' })
44+
})
45+
await t.step('data files', async () => {
46+
for (
47+
const [filename, datatype, modality] of [
48+
['/sub-01/anat/sub-01_T1w.nii.gz', 'anat', 'mri'],
49+
['/sub-01/eeg/sub-01_task-rest_eeg.edf', 'eeg', 'eeg'],
50+
['/sub-01/perf/sub-01_task-rest_bold.nii.gz', 'perf', 'mri'],
51+
]
52+
) {
53+
const path = makeFile(filename)
54+
assertObjectMatch(findDatatype(path, schema), { datatype, modality })
55+
}
56+
})
57+
})

src/schema/datatypes.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { BIDSFile } from '../types/filetree.ts'
2+
import type { Schema } from '../types/schema.ts'
3+
import { memoize } from '../utils/memoize.ts'
4+
5+
function _modalityTable(schema: Schema): Record<string, string> {
6+
const modalities: Record<string, string> = {}
7+
const rules = (schema.rules?.modalities ?? {}) as Record<string, { datatypes: string[] }>
8+
for (const [modality, { datatypes }] of Object.entries(rules)) {
9+
for (const datatype of datatypes) {
10+
modalities[datatype] = modality
11+
}
12+
}
13+
return modalities
14+
}
15+
16+
// Construct once per schema; should only be multiple in tests
17+
export const modalityTable = memoize(_modalityTable)
18+
19+
export function findDatatype(
20+
file: BIDSFile,
21+
schema: Schema,
22+
): { datatype: string; modality: string } {
23+
const lookup = modalityTable(schema)
24+
const datatype = file.parent?.name
25+
if (!schema?.objects?.datatypes[datatype]) {
26+
return { datatype: '', modality: '' }
27+
}
28+
const modality = lookup[datatype] ?? ''
29+
return { datatype, modality }
30+
}

src/schema/modalities.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/validators/filenameIdentify.test.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { assertEquals } from '@std/assert'
22
import { SEPARATOR_PATTERN } from '@std/path'
33
import { BIDSContext } from '../schema/context.ts'
4-
import {
5-
_findRuleMatches,
6-
datatypeFromDirectory,
7-
findDirRuleMatches,
8-
hasMatch,
9-
} from './filenameIdentify.ts'
4+
import { _findRuleMatches, findDirRuleMatches, hasMatch } from './filenameIdentify.ts'
105
import { BIDSFileDeno } from '../files/deno.ts'
116
import { FileIgnoreRules } from '../files/ignore.ts'
127
import { loadSchema } from '../setup/loadSchema.ts'
@@ -50,19 +45,6 @@ Deno.test('test _findRuleMatches', async (t) => {
5045
)
5146
})
5247

53-
Deno.test('test datatypeFromDirectory', (t) => {
54-
const filesToTest = [
55-
['/sub-01/ses-01/func/sub-01_ses-01_task-nback_run-01_bold.nii', 'func'],
56-
['/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii', 'anat'],
57-
]
58-
filesToTest.map((test) => {
59-
const file = new BIDSFileDeno(PATH, test[0], ignore)
60-
const context = new BIDSContext(file)
61-
datatypeFromDirectory(schema, context)
62-
assertEquals(context.datatype, test[1])
63-
})
64-
})
65-
6648
Deno.test('test hasMatch', async (t) => {
6749
await t.step('hasMatch', async () => {
6850
const fileName = '/sub-01/ses-01/func/sub-01_ses-01_task-nback_run-01_bold.nii'

src/validators/filenameIdentify.ts

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@
1515
import { globToRegExp, SEPARATOR_PATTERN } from '@std/path'
1616
import type { GenericSchema, Schema } from '../types/schema.ts'
1717
import type { BIDSContext } from '../schema/context.ts'
18-
import type { lookupModality } from '../schema/modalities.ts'
1918
import type { CheckFunction } from '../types/check.ts'
2019
import { lookupEntityLiteral } from './filenameValidate.ts'
2120

2221
const CHECKS: CheckFunction[] = [
23-
datatypeFromDirectory,
2422
findRuleMatches,
2523
hasMatch,
2624
cleanContext,
@@ -126,29 +124,6 @@ function matchStemRule(node, context): boolean {
126124
return true
127125
}
128126

129-
export async function datatypeFromDirectory(schema, context) {
130-
const subEntity = schema.objects.entities.subject.name
131-
const sesEntity = schema.objects.entities.session.name
132-
const parts = context.file.path.split(SEPARATOR_PATTERN)
133-
const datatypeIndex = parts.length - 2
134-
if (datatypeIndex < 1) {
135-
return Promise.resolve()
136-
}
137-
const dirDatatype = parts[datatypeIndex]
138-
if (dirDatatype === 'phenotype') {
139-
// Phenotype is a pseudo-datatype for now.
140-
context.datatype = dirDatatype
141-
return Promise.resolve()
142-
}
143-
for (const key in schema.rules.modalities) {
144-
if (schema.rules.modalities[key].datatypes.includes(dirDatatype)) {
145-
context.modality = key
146-
context.datatype = dirDatatype
147-
return Promise.resolve()
148-
}
149-
}
150-
}
151-
152127
export function hasMatch(schema, context) {
153128
if (
154129
context.filenameRules.length === 0 &&

src/validators/validateFiles.test.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { assert, assertEquals } from '@std/assert'
22
import { filenameIdentify } from './filenameIdentify.ts'
33
import { filenameValidate } from './filenameValidate.ts'
4-
import { BIDSContext } from '../schema/context.ts'
4+
import { BIDSContext, BIDSContextDataset } from '../schema/context.ts'
55
import { loadSchema } from '../setup/loadSchema.ts'
66
import type { GenericSchema, Schema } from '../types/schema.ts'
77
import type { DatasetIssues } from '../issues/datasetIssues.ts'
8-
import { pathToFile } from '../files/filetree.ts'
8+
import type { BIDSFile } from '../types/filetree.ts'
9+
import { pathsToTree } from '../files/filetree.ts'
910

10-
const schema = await loadSchema() as unknown as GenericSchema
11+
const schema = await loadSchema()
1112

12-
function validatePath(path: string): DatasetIssues {
13-
const context = new BIDSContext(pathToFile(path))
14-
filenameIdentify(schema, context)
15-
filenameValidate(schema, context)
16-
return context.dataset.issues
13+
function makeContext(path: string): BIDSContext {
14+
const tree = pathsToTree([path])
15+
const dataset = new BIDSContextDataset({ schema, tree })
16+
return new BIDSContext(tree.get(path) as BIDSFile, dataset)
1717
}
1818

1919
Deno.test('test valid paths', async (t) => {
@@ -54,11 +54,13 @@ Deno.test('test valid paths', async (t) => {
5454
]
5555
for (const filename of validFiles) {
5656
await t.step(filename, async () => {
57-
const issues = validatePath(filename)
57+
const context = makeContext(filename)
58+
await filenameIdentify(schema, context)
59+
await filenameValidate(schema as unknown as GenericSchema, context)
5860
assertEquals(
59-
issues.get({ location: filename }).length,
61+
context.dataset.issues.get({ location: filename }).length,
6062
0,
61-
Deno.inspect(issues),
63+
Deno.inspect(context.dataset.issues),
6264
)
6365
})
6466
}
@@ -111,9 +113,9 @@ Deno.test('test invalid paths', async (t) => {
111113
]
112114
for (const filename of invalidFiles) {
113115
await t.step(filename, async () => {
114-
const context = new BIDSContext(pathToFile(filename))
116+
const context = makeContext(filename)
115117
await filenameIdentify(schema, context)
116-
await filenameValidate(schema, context)
118+
await filenameValidate(schema as unknown as GenericSchema, context)
117119
assert(
118120
context.dataset.issues.get({
119121
location: context.file.path,

0 commit comments

Comments
 (0)