Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix sidecar syntax issues #193

Merged
merged 3 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 52 additions & 5 deletions bids/types/json.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import isPlainObject from 'lodash/isPlainObject'

import { sidecarValueHasHed } from '../utils'
import { parseHedString } from '../../parser/main'
import ParsedHedString from '../../parser/parsedHedString'
import { BidsFile } from './basic'
import BidsHedSidecarValidator from '../validator/bidsHedSidecarValidator'
import { generateIssue, IssueError } from '../../common/issues/issues'

const ILLEGAL_SIDECAR_KEYS = new Set(['hed', 'n/a'])

/**
* A BIDS JSON file.
Expand Down Expand Up @@ -74,16 +79,41 @@ export class BidsSidecar extends BidsJsonFile {
_filterHedStrings() {
const sidecarHedTags = Object.entries(this.jsonData)
.map(([sidecarKey, sidecarValue]) => {
const trimmedSidecarKey = sidecarKey.trim()
if (ILLEGAL_SIDECAR_KEYS.has(trimmedSidecarKey.toLowerCase())) {
throw new IssueError(generateIssue('illegalSidecarHedKey', {}))
}
if (sidecarValueHasHed(sidecarValue)) {
return [sidecarKey.trim(), new BidsSidecarKey(sidecarKey.trim(), sidecarValue.HED)]
return [trimmedSidecarKey, new BidsSidecarKey(trimmedSidecarKey, sidecarValue.HED, this)]
} else {
this._verifyKeyHasNoDeepHed(sidecarKey, sidecarValue)
return null
}
})
.filter((x) => x !== null)
this.sidecarKeys = new Map(sidecarHedTags)
}

/**
* Verify that a column has no deeply nested "HED" keys.
*
* @param {string} key An object key.
* @param {*} value An object value.
* @throws {IssueError} If an invalid "HED" key is found.
* @private
*/
_verifyKeyHasNoDeepHed(key, value) {
if (key.toUpperCase() === 'HED') {
throw new IssueError(generateIssue('illegalSidecarHedDeepKey', {}))
}
if (!isPlainObject(value)) {
return
}
for (const [subkey, subvalue] of Object.entries(value)) {
this._verifyKeyHasNoDeepHed(subkey, subvalue)
}
}

_categorizeHedStrings() {
this.hedValueStrings = []
this.hedCategoricalStrings = []
Expand Down Expand Up @@ -176,7 +206,9 @@ export class BidsSidecar extends BidsJsonFile {
this.columnSpliceMapping.set(sidecarKey, keyReferences)
}
} else {
throw new Error('Unexpected type found in sidecar parsedHedData map.')
throw new IssueError(
generateIssue('internalConsistencyError', { message: 'Unexpected type found in sidecar parsedHedData map.' }),
)
}
}
}
Expand Down Expand Up @@ -224,19 +256,26 @@ export class BidsSidecarKey {
* @type {ParsedHedString}
*/
parsedValueString
/**
* Weak reference to the sidecar.
* @type {WeakRef<BidsSidecar>}
*/
sidecar

/**
* Constructor.
*
* @param {string} key The name of this key.
* @param {string|Object<string, string>} data The data for this key.
* @param {BidsSidecar} sidecar The parent sidecar.
*/
constructor(key, data) {
constructor(key, data, sidecar) {
this.name = key
this.sidecar = new WeakRef(sidecar)
if (typeof data === 'string') {
this.valueString = data
} else if (data !== Object(data)) {
throw new Error('Non-object passed as categorical data.')
} else if (!isPlainObject(data)) {
throw new IssueError(generateIssue('illegalSidecarHedType', { key: key, file: sidecar.file.relativePath }))
} else {
this.categoryMap = data
}
Expand Down Expand Up @@ -266,6 +305,14 @@ export class BidsSidecarKey {
const issues = []
this.parsedCategoryMap = new Map()
for (const [value, string] of Object.entries(this.categoryMap)) {
const trimmedValue = value.trim()
if (ILLEGAL_SIDECAR_KEYS.has(trimmedValue.toLowerCase())) {
throw new IssueError(generateIssue('illegalSidecarHedCategoricalValue', {}))
} else if (typeof string !== 'string') {
throw new IssueError(
generateIssue('illegalSidecarHedType', { key: value, file: this.sidecar.deref()?.file?.relativePath }),
)
}
const [parsedString, parsingIssues] = parseHedString(string, hedSchemas)
this.parsedCategoryMap.set(value, parsedString)
issues.push(...Object.values(parsingIssues).flat())
Expand Down
9 changes: 6 additions & 3 deletions bids/types/tsv.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { convertParsedTSVData, parseTSV } from '../tsvParser'
import { BidsSidecar } from './json'
import ParsedHedString from '../../parser/parsedHedString'
import BidsHedTsvValidator from '../validator/bidsHedTsvValidator'
import { generateIssue, IssueError } from '../../common/issues/issues'

/**
* A BIDS TSV file.
Expand Down Expand Up @@ -57,11 +58,11 @@ export class BidsTsvFile extends BidsFile {
} else if (isPlainObject(tsvData)) {
this.parsedTsv = convertParsedTSVData(tsvData)
} else {
throw new Error('parsedTsv has an invalid type')
throw new IssueError(generateIssue('internalError', { message: 'parsedTsv has an invalid type' }))
}

this.potentialSidecars = potentialSidecars
this.mergedSidecar = new BidsSidecar(name, mergedDictionary, null)
this.mergedSidecar = new BidsSidecar(name, mergedDictionary, this.file)
this.sidecarHedData = this.mergedSidecar.hedData
this._parseHedColumn()
}
Expand Down Expand Up @@ -195,7 +196,9 @@ export class BidsTsvRow extends ParsedHedString {
get onset() {
const value = Number(this.rowCells.get('onset'))
if (Number.isNaN(value)) {
throw new Error('Attempting to access the onset of a TSV row without one.')
throw new IssueError(
generateIssue('internalError', { message: 'Attempting to access the onset of a TSV row without one.' }),
)
}
return value
}
Expand Down
8 changes: 8 additions & 0 deletions bids/validator/bidsHedSidecarValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ export class BidsHedSidecarValidator {
),
)
}
if (!this.sidecar.parsedHedData.has(referredKey) && referredKey !== 'HED') {
issues.push(
BidsHedIssue.fromHedIssue(
generateIssue('undefinedCurlyBraces', { column: referredKey }),
this.sidecar.file,
),
)
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions common/issues/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,21 @@ export default {
level: 'error',
message: stringTemplate`The HED data for sidecar key "${'key'}" of file "${'file'}" is not either a key-value dictionary or a string.`,
},
illegalSidecarHedKey: {
hedCode: 'SIDECAR_INVALID',
level: 'error',
message: stringTemplate`The string 'HED' or 'n/a' was illegally used as a sidecar key.`,
},
illegalSidecarHedCategoricalValue: {
hedCode: 'SIDECAR_INVALID',
level: 'error',
message: stringTemplate`The string 'HED' or 'n/a' was illegally used as a sidecar categorical value.`,
},
illegalSidecarHedDeepKey: {
hedCode: 'SIDECAR_INVALID',
level: 'error',
message: stringTemplate`The key 'HED' was illegally used within a non-HED sidecar column.`,
},
// Generic errors
genericError: {
hedCode: 'GENERIC_ERROR',
Expand Down
60 changes: 50 additions & 10 deletions spec_tests/jsonTests.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { buildSchemas } from '../validator/schema/init'
import { SchemaSpec, SchemasSpec } from '../common/schema/types'
import path from 'path'
import { BidsSidecar, BidsTsvFile } from '../bids'
import { generateIssue, IssueError } from '../common/issues/issues'
const fs = require('fs')

const displayLog = process.env.DISPLAY_LOG === 'true'
Expand Down Expand Up @@ -162,40 +163,79 @@ describe('HED validation using JSON tests', () => {
const status = expectError ? 'Expect fail' : 'Expect pass'
const header = `\n[${eCode} ${eName}](${status})\tCOMBO\t"${side}"\n"${events}"`
const mergedSide = getMergedSidecar(side, defs)
const bidsSide = new BidsSidecar(`sidecar`, mergedSide, null)
const bidsTsv = new BidsTsvFile(`events`, events, null, [side], mergedSide)
const sidecarIssues = bidsSide.validate(schema)
const eventsIssues = bidsTsv.validate(schema)
let sidecarIssues = []
try {
const bidsSide = new BidsSidecar(`sidecar`, mergedSide, { relativePath: 'combo test sidecar' })
sidecarIssues = bidsSide.validate(schema)
} catch (e) {
sidecarIssues = [convertIssue(e)]
}
let eventsIssues = []
try {
const bidsTsv = new BidsTsvFile(`events`, events, { relativePath: 'combo test tsv' }, [side], mergedSide)
eventsIssues = bidsTsv.validate(schema)
} catch (e) {
eventsIssues = [convertIssue(e)]
}
const allIssues = [...sidecarIssues, ...eventsIssues]
assertErrors(eCode, altCodes, expectError, iLog, header, allIssues)
}

const eventsValidator = function (eCode, altCodes, eName, events, schema, defs, expectError, iLog) {
const status = expectError ? 'Expect fail' : 'Expect pass'
const header = `\n[${eCode} ${eName}](${status})\tEvents:\n"${events}"`
const bidsTsv = new BidsTsvFile(`events`, events, null, [], defs)
const eventsIssues = bidsTsv.validate(schema)
let eventsIssues = []
try {
const bidsTsv = new BidsTsvFile(`events`, events, { relativePath: 'events test' }, [], defs)
eventsIssues = bidsTsv.validate(schema)
} catch (e) {
eventsIssues = [convertIssue(e)]
}
assertErrors(eCode, altCodes, expectError, iLog, header, eventsIssues)
}

const sideValidator = function (eCode, altCodes, eName, side, schema, defs, expectError, iLog) {
const status = expectError ? 'Expect fail' : 'Expect pass'
const header = `\n[${eCode} ${eName}](${status})\tSIDECAR "${side}"`
const side1 = getMergedSidecar(side, defs)
const bidsSide = new BidsSidecar(`sidecar`, side1, null)
const sidecarIssues = bidsSide.validate(schema)
let sidecarIssues = []
try {
const bidsSide = new BidsSidecar(`sidecar`, side1, { relativePath: 'sidecar test' })
sidecarIssues = bidsSide.validate(schema)
} catch (e) {
sidecarIssues = [convertIssue(e)]
}
assertErrors(eCode, altCodes, expectError, iLog, header, sidecarIssues)
}

const stringValidator = function (eCode, altCodes, eName, str, schema, defs, expectError, iLog) {
const status = expectError ? 'Expect fail' : 'Expect pass'
const header = `\n[${eCode} ${eName}](${status})\tSTRING: "${str}"`
const hTsv = `HED\n${str}\n`
const bidsTsv = new BidsTsvFile(`events`, hTsv, null, [], defs)
const stringIssues = bidsTsv.validate(schema)
let stringIssues = []
try {
const bidsTsv = new BidsTsvFile(`events`, hTsv, { relativePath: 'string test tsv' }, [], defs)
stringIssues = bidsTsv.validate(schema)
} catch (e) {
stringIssues = [convertIssue(e)]
}
assertErrors(eCode, altCodes, expectError, iLog, header, stringIssues)
}

/**
* Convert an Error into an Issue.
*
* @param {Error} issueError A thrown error.
* @returns {Issue} A HED issue.
*/
const convertIssue = function (issueError) {
if (issueError instanceof IssueError) {
return issueError.issue
} else {
return generateIssue('internalError', { message: issueError.message })
}
}

beforeAll(async () => {
hedSchema = schemaMap.get(schema)
defs = { definitions: { HED: { defList: definitions.join(',') } } }
Expand Down
14 changes: 12 additions & 2 deletions tests/bids.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,18 @@ describe('BIDS datasets', () => {
}),
standaloneSidecars[1].file,
),
BidsHedIssue.fromHedIssue(
generateIssue('undefinedCurlyBraces', {
column: 'event_code',
}),
standaloneSidecars[1].file,
),
BidsHedIssue.fromHedIssue(
generateIssue('undefinedCurlyBraces', {
column: 'response_time',
}),
standaloneSidecars[1].file,
),
BidsHedIssue.fromHedIssue(
generateIssue('recursiveCurlyBracesWithKey', {
column: 'response_time',
Expand Down Expand Up @@ -626,14 +638,12 @@ describe('BIDS datasets', () => {
column: 'response_time',
}),
combinedDatasets[0].file,
{ tsvLine: 3 },
),
BidsHedIssue.fromHedIssue(
generateIssue('undefinedCurlyBraces', {
column: 'response_time',
}),
combinedDatasets[1].file,
{ tsvLine: 3 },
),
BidsHedIssue.fromHedIssue(
generateIssue('duplicateTag', {
Expand Down
Loading