From 8daf24fe10c2b1e0aa1e6776cadbacffd6f6846e Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 22 Sep 2025 14:35:55 -0400 Subject: [PATCH 1/4] test: Improve validateFiles.test.ts to build more complete contexts --- src/validators/validateFiles.test.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/validators/validateFiles.test.ts b/src/validators/validateFiles.test.ts index da28a5bed..057f24985 100644 --- a/src/validators/validateFiles.test.ts +++ b/src/validators/validateFiles.test.ts @@ -1,19 +1,19 @@ import { assert, assertEquals } from '@std/assert' import { filenameIdentify } from './filenameIdentify.ts' import { filenameValidate } from './filenameValidate.ts' -import { BIDSContext } from '../schema/context.ts' +import { BIDSContext, BIDSContextDataset } from '../schema/context.ts' import { loadSchema } from '../setup/loadSchema.ts' import type { GenericSchema, Schema } from '../types/schema.ts' import type { DatasetIssues } from '../issues/datasetIssues.ts' -import { pathToFile } from '../files/filetree.ts' +import type { BIDSFile } from '../types/filetree.ts' +import { pathsToTree } from '../files/filetree.ts' -const schema = await loadSchema() as unknown as GenericSchema +const schema = await loadSchema() -function validatePath(path: string): DatasetIssues { - const context = new BIDSContext(pathToFile(path)) - filenameIdentify(schema, context) - filenameValidate(schema, context) - return context.dataset.issues +function makeContext(path: string): BIDSContext { + const tree = pathsToTree([path]) + const dataset = new BIDSContextDataset({ schema, tree }) + return new BIDSContext(tree.get(path) as BIDSFile, dataset) } Deno.test('test valid paths', async (t) => { @@ -54,11 +54,13 @@ Deno.test('test valid paths', async (t) => { ] for (const filename of validFiles) { await t.step(filename, async () => { - const issues = validatePath(filename) + const context = makeContext(filename) + await filenameIdentify(schema, context) + await filenameValidate(schema as unknown as GenericSchema, context) assertEquals( - issues.get({ location: filename }).length, + context.dataset.issues.get({ location: filename }).length, 0, - Deno.inspect(issues), + Deno.inspect(context.dataset.issues), ) }) } @@ -111,9 +113,9 @@ Deno.test('test invalid paths', async (t) => { ] for (const filename of invalidFiles) { await t.step(filename, async () => { - const context = new BIDSContext(pathToFile(filename)) + const context = makeContext(filename) await filenameIdentify(schema, context) - await filenameValidate(schema, context) + await filenameValidate(schema as unknown as GenericSchema, context) assert( context.dataset.issues.get({ location: context.file.path, From f61b3858887027ed24a643df4521972216434926 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 22 Sep 2025 14:50:06 -0400 Subject: [PATCH 2/4] feat: Retrieve datatype and modality from BIDSFile and schema --- src/schema/datatypes.test.ts | 57 ++++++++++++++++++++++++++++++++++++ src/schema/datatypes.ts | 30 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/schema/datatypes.test.ts create mode 100644 src/schema/datatypes.ts diff --git a/src/schema/datatypes.test.ts b/src/schema/datatypes.test.ts new file mode 100644 index 000000000..b263d8df2 --- /dev/null +++ b/src/schema/datatypes.test.ts @@ -0,0 +1,57 @@ +import { assert, assertObjectMatch } from '@std/assert' +import { loadSchema } from '../setup/loadSchema.ts' +import { BIDSContext, BIDSContextDataset } from '../schema/context.ts' +import { pathsToTree } from '../files/filetree.ts' +import type { BIDSFile } from '../types/filetree.ts' + +import { findDatatype, modalityTable } from './datatypes.ts' + +const schema = await loadSchema() + +// Creates a file object as part of a minimal file tree +const makeFile = (path: string): BIDSFile => pathsToTree([path]).get(path) as BIDSFile + +Deno.test('test modalityTable', async (t) => { + await t.step('empty schema', () => { + assertObjectMatch(modalityTable({}), {}) + }) + + await t.step('real schema', async () => { + const table = modalityTable(schema) + // Memoization check + assert(modalityTable(schema) == table) + // spot check + assertObjectMatch(table, { + 'anat': 'mri', + 'perf': 'mri', + 'eeg': 'eeg', + }) + }) +}) + +Deno.test('test findDatatype', async (t) => { + await t.step('root files', async () => { + const path = makeFile('/participants.tsv') + assertObjectMatch(findDatatype(path, schema), { datatype: '', modality: '' }) + }) + await t.step('non-datatype parent', async () => { + const path = makeFile('/stimuli/image.png') + assertObjectMatch(findDatatype(path, schema), { datatype: '', modality: '' }) + }) + await t.step('phenotype file', async () => { + const path = makeFile('/phenotype/survey.tsv') + assertObjectMatch(findDatatype(path, schema), { datatype: 'phenotype', modality: '' }) + }) + await t.step('data files', async () => { + for ( + const [filename, datatype, modality] of [ + ['/sub-01/anat/sub-01_T1w.nii.gz', 'anat', 'mri'], + ['/sub-01/eeg/sub-01_task-rest_eeg.edf', 'eeg', 'eeg'], + ['/sub-01/perf/sub-01_task-rest_bold.nii.gz', 'perf', 'mri'], + ] + ) { + const path = makeFile(filename) + assertObjectMatch(findDatatype(path, schema), { datatype, modality }) + } + }) +}) diff --git a/src/schema/datatypes.ts b/src/schema/datatypes.ts new file mode 100644 index 000000000..3863228cb --- /dev/null +++ b/src/schema/datatypes.ts @@ -0,0 +1,30 @@ +import type { BIDSFile } from '../types/filetree.ts' +import type { Schema } from '../types/schema.ts' +import { memoize } from '../utils/memoize.ts' + +function _modalityTable(schema: Schema): Record { + const modalities: Record = {} + const rules = (schema.rules?.modalities ?? {}) as Record + for (const [modality, { datatypes }] of Object.entries(rules)) { + for (const datatype of datatypes) { + modalities[datatype] = modality + } + } + return modalities +} + +// Construct once per schema; should only be multiple in tests +export const modalityTable = memoize(_modalityTable) + +export function findDatatype( + file: BIDSFile, + schema: Schema, +): { datatype: string; modality: string } { + const lookup = modalityTable(schema) + const datatype = file.parent?.name + if (!schema?.objects?.datatypes[datatype]) { + return { datatype: '', modality: '' } + } + const modality = lookup[datatype] ?? '' + return { datatype, modality } +} From ebbaaa8c2c7df99a192647612ff406224c1f49b3 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 22 Sep 2025 14:51:13 -0400 Subject: [PATCH 3/4] feat: Load datatype and modality when building context --- src/schema/context.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/schema/context.ts b/src/schema/context.ts index 92f9a51c4..ecd6da765 100644 --- a/src/schema/context.ts +++ b/src/schema/context.ts @@ -15,6 +15,7 @@ import type { BIDSFile } from '../types/filetree.ts' import { FileTree } from '../types/filetree.ts' import { ColumnsMap } from '../types/columns.ts' import { readEntities } from './entities.ts' +import { findDatatype } from './datatypes.ts' import { DatasetIssues } from '../issues/datasetIssues.ts' import { walkBack } from '../files/inheritance.ts' import { parseGzip } from '../files/gzip.ts' @@ -147,16 +148,20 @@ export class BIDSContext implements Context { dsContext?: BIDSContextDataset, fileTree?: FileTree, ) { + this.dataset = dsContext ? dsContext : new BIDSContextDataset({ tree: fileTree }) + this.filenameRules = [] this.file = file - const bidsEntities = readEntities(file.name) - this.suffix = bidsEntities.suffix - this.extension = bidsEntities.extension - this.entities = bidsEntities.entities - this.dataset = dsContext ? dsContext : new BIDSContextDataset({ tree: fileTree }) + + const { entities, suffix, extension } = readEntities(file.name) + const { datatype, modality } = findDatatype(file, this.dataset.schema) + this.entities = entities + this.suffix = suffix + this.extension = extension + this.datatype = datatype + this.modality = modality + this.subject = {} as Subject - this.datatype = '' - this.modality = '' this.sidecar = {} this.sidecarKeyOrigin = {} this.columns = new ColumnsMap() as Record From 9d715852abff9e6835b1e3e7108a83ac99193d72 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 22 Sep 2025 14:51:32 -0400 Subject: [PATCH 4/4] chore: Remove outdated datatype/modality code --- src/schema/modalities.ts | 16 ---------------- src/validators/filenameIdentify.test.ts | 20 +------------------- src/validators/filenameIdentify.ts | 25 ------------------------- 3 files changed, 1 insertion(+), 60 deletions(-) delete mode 100644 src/schema/modalities.ts diff --git a/src/schema/modalities.ts b/src/schema/modalities.ts deleted file mode 100644 index 53baca638..000000000 --- a/src/schema/modalities.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Schema } from '../types/schema.ts' - -export function lookupModality(schema: Schema, datatype: string): string { - const modalities = schema.rules.modalities as Record - const datatypes = Object.keys(modalities).filter((key: string) => { - modalities[key].datatypes.includes(datatype) - }) - if (datatypes.length === 1) { - return datatypes[0] - } else if (datatypes.length === 0) { - return '' - } else { - // what if multiple modalites are found? - return '' - } -} diff --git a/src/validators/filenameIdentify.test.ts b/src/validators/filenameIdentify.test.ts index 5f83e7512..c2ff64754 100644 --- a/src/validators/filenameIdentify.test.ts +++ b/src/validators/filenameIdentify.test.ts @@ -1,12 +1,7 @@ import { assertEquals } from '@std/assert' import { SEPARATOR_PATTERN } from '@std/path' import { BIDSContext } from '../schema/context.ts' -import { - _findRuleMatches, - datatypeFromDirectory, - findDirRuleMatches, - hasMatch, -} from './filenameIdentify.ts' +import { _findRuleMatches, findDirRuleMatches, hasMatch } from './filenameIdentify.ts' import { BIDSFileDeno } from '../files/deno.ts' import { FileIgnoreRules } from '../files/ignore.ts' import { loadSchema } from '../setup/loadSchema.ts' @@ -50,19 +45,6 @@ Deno.test('test _findRuleMatches', async (t) => { ) }) -Deno.test('test datatypeFromDirectory', (t) => { - const filesToTest = [ - ['/sub-01/ses-01/func/sub-01_ses-01_task-nback_run-01_bold.nii', 'func'], - ['/sub-01/ses-01/anat/sub-01_ses-01_T1w.nii', 'anat'], - ] - filesToTest.map((test) => { - const file = new BIDSFileDeno(PATH, test[0], ignore) - const context = new BIDSContext(file) - datatypeFromDirectory(schema, context) - assertEquals(context.datatype, test[1]) - }) -}) - Deno.test('test hasMatch', async (t) => { await t.step('hasMatch', async () => { const fileName = '/sub-01/ses-01/func/sub-01_ses-01_task-nback_run-01_bold.nii' diff --git a/src/validators/filenameIdentify.ts b/src/validators/filenameIdentify.ts index 25df198e0..1159b9ef2 100644 --- a/src/validators/filenameIdentify.ts +++ b/src/validators/filenameIdentify.ts @@ -15,12 +15,10 @@ import { globToRegExp, SEPARATOR_PATTERN } from '@std/path' import type { GenericSchema, Schema } from '../types/schema.ts' import type { BIDSContext } from '../schema/context.ts' -import type { lookupModality } from '../schema/modalities.ts' import type { CheckFunction } from '../types/check.ts' import { lookupEntityLiteral } from './filenameValidate.ts' const CHECKS: CheckFunction[] = [ - datatypeFromDirectory, findRuleMatches, hasMatch, cleanContext, @@ -126,29 +124,6 @@ function matchStemRule(node, context): boolean { return true } -export async function datatypeFromDirectory(schema, context) { - const subEntity = schema.objects.entities.subject.name - const sesEntity = schema.objects.entities.session.name - const parts = context.file.path.split(SEPARATOR_PATTERN) - const datatypeIndex = parts.length - 2 - if (datatypeIndex < 1) { - return Promise.resolve() - } - const dirDatatype = parts[datatypeIndex] - if (dirDatatype === 'phenotype') { - // Phenotype is a pseudo-datatype for now. - context.datatype = dirDatatype - return Promise.resolve() - } - for (const key in schema.rules.modalities) { - if (schema.rules.modalities[key].datatypes.includes(dirDatatype)) { - context.modality = key - context.datatype = dirDatatype - return Promise.resolve() - } - } -} - export function hasMatch(schema, context) { if ( context.filenameRules.length === 0 &&