Source: bids/types/json.js

  1. import isPlainObject from 'lodash/isPlainObject'
  2. import { parseHedString } from '../../parser/parser'
  3. import ParsedHedString from '../../parser/parsedHedString'
  4. import { BidsFile } from './file'
  5. import BidsHedSidecarValidator from '../validator/sidecarValidator'
  6. import { IssueError } from '../../issues/issues'
  7. import { DefinitionManager, Definition } from '../../parser/definitionManager'
  8. const ILLEGAL_SIDECAR_KEYS = new Set(['hed', 'n/a'])
  9. /**
  10. * A BIDS JSON file.
  11. */
  12. export class BidsJsonFile extends BidsFile {
  13. /**
  14. * This file's JSON data.
  15. * @type {Object}
  16. */
  17. jsonData
  18. /**
  19. * Constructor.
  20. *
  21. * @param {string} name - The name of the JSON file.
  22. * @param {Object} file - The object representing this file.
  23. * @param {Object} jsonData - The JSON data for this file.
  24. */
  25. constructor(name, file, jsonData) {
  26. super(name, file, BidsHedSidecarValidator)
  27. this.jsonData = jsonData
  28. }
  29. }
  30. export class BidsSidecar extends BidsJsonFile {
  31. /**
  32. * The extracted keys for this sidecar (string --> BidsSidecarKey)
  33. * @type {Map}
  34. */
  35. sidecarKeys
  36. /**
  37. * The extracted HED data for this sidecar (string --> string | Object: string, string
  38. * @type {Map}
  39. */
  40. hedData
  41. /**
  42. * The parsed HED data for this sidecar (string --> ParsedHedString | Map: string --> ParsedHedString).
  43. * @type {Map}
  44. */
  45. parsedHedData
  46. /**
  47. * The extracted HED value strings.
  48. * @type {string[]}
  49. */
  50. hedValueStrings
  51. /**
  52. * The extracted HED categorical strings.
  53. * @type {string[]}
  54. */
  55. hedCategoricalStrings
  56. /**
  57. * The mapping of column splice references (string --> Set of string).
  58. * @type {Map}
  59. */
  60. columnSpliceMapping
  61. /**
  62. * The set of column splice references.
  63. * @type {Set<string>}
  64. */
  65. columnSpliceReferences
  66. /**
  67. * The object that manages definitions.
  68. * @type {DefinitionManager}
  69. */
  70. definitions
  71. /**
  72. * Constructor.
  73. *
  74. * @param {string} name The name of the sidecar file.
  75. * @param {Object} file The file object representing this file.
  76. * @param {Object} sidecarData The raw JSON data.
  77. * @param {DefinitionManager } defManager - The external definitions to use
  78. */
  79. constructor(name, file, sidecarData = {}, defManager = undefined) {
  80. super(name, file, sidecarData)
  81. this.columnSpliceMapping = new Map()
  82. this.columnSpliceReferences = new Set()
  83. this._setDefinitions(defManager)
  84. this._filterHedStrings()
  85. this._categorizeHedStrings()
  86. }
  87. /**
  88. * Determine whether this file has any HED data.
  89. *
  90. * @returns {boolean}
  91. */
  92. get hasHedData() {
  93. return this.sidecarKeys.size > 0
  94. }
  95. /**
  96. * The extracted HED strings.
  97. * @returns {string[]}
  98. */
  99. get hedStrings() {
  100. return this.hedValueStrings.concat(this.hedCategoricalStrings)
  101. }
  102. /**
  103. * Parse this sidecar's HED strings within the sidecar structure.
  104. *
  105. * The parsed strings are placed into {@link parsedHedData}.
  106. *
  107. * @param {Schemas} hedSchemas - The HED schema collection.
  108. * @param {boolean} fullValidation - True if full validation should be performed.
  109. * @returns {Array} [Issue[], Issue[]] Any errors and warnings found
  110. */
  111. parseHed(hedSchemas, fullValidation = false) {
  112. this.parsedHedData = new Map()
  113. const errors = []
  114. const warnings = []
  115. for (const [name, sidecarKey] of this.sidecarKeys.entries()) {
  116. const [errorIssues, warningIssues] =
  117. sidecarKey.parseHed(hedSchemas, fullValidation && !this.columnSpliceReferences.has(name))
  118. errors.push(...errorIssues)
  119. warnings.push(...warningIssues)
  120. if (sidecarKey.isValueKey) {
  121. this.parsedHedData.set(name, sidecarKey.parsedValueString)
  122. } else {
  123. this.parsedHedData.set(name, sidecarKey.parsedCategoryMap)
  124. }
  125. }
  126. this._generateSidecarColumnSpliceMap()
  127. return [errors, warnings]
  128. }
  129. /**
  130. * Set the definition manager for this sidecar.
  131. * @param defManager
  132. * @private
  133. */
  134. _setDefinitions(defManager) {
  135. if (defManager instanceof DefinitionManager) {
  136. this.definitions = defManager
  137. } else if (!defManager) {
  138. this.definitions = new DefinitionManager()
  139. } else {
  140. IssueError.generateAndThrowInternalError(
  141. 'Improper format for defManager parameter -- must be null or DefinitionManager',
  142. )
  143. }
  144. }
  145. /**
  146. * Create the sidecar key map from the JSON.
  147. * @private
  148. */
  149. _filterHedStrings() {
  150. this.sidecarKeys = new Map(
  151. Object.entries(this.jsonData)
  152. .map(([key, value]) => {
  153. const trimmedKey = key.trim()
  154. const lowerKey = trimmedKey.toLowerCase()
  155. if (ILLEGAL_SIDECAR_KEYS.has(lowerKey)) {
  156. IssueError.generateAndThrow('illegalSidecarHedKey')
  157. }
  158. if (BidsSidecar._sidecarValueHasHed(value)) {
  159. return [trimmedKey, new BidsSidecarKey(trimmedKey, value.HED, this)]
  160. }
  161. BidsSidecar._verifyKeyHasNoDeepHed(key, value)
  162. return null
  163. })
  164. .filter(Boolean),
  165. )
  166. }
  167. /**
  168. * Determine whether a sidecar value has HED data.
  169. *
  170. * @param {Object} sidecarValue A BIDS sidecar value.
  171. * @returns {boolean} Whether the sidecar value has HED data.
  172. * @private
  173. */
  174. static _sidecarValueHasHed(sidecarValue) {
  175. return sidecarValue !== null && typeof sidecarValue === 'object' && sidecarValue.HED !== undefined
  176. }
  177. /**
  178. * Verify that a column has no deeply nested "HED" keys.
  179. *
  180. * @param {string} key An object key.
  181. * @param {*} value An object value.
  182. * @throws {IssueError} If an invalid "HED" key is found.
  183. * @private
  184. */
  185. static _verifyKeyHasNoDeepHed(key, value) {
  186. if (key.toUpperCase() === 'HED') {
  187. IssueError.generateAndThrow('illegalSidecarHedDeepKey')
  188. }
  189. if (!isPlainObject(value)) {
  190. return
  191. }
  192. for (const [subkey, subvalue] of Object.entries(value)) {
  193. BidsSidecar._verifyKeyHasNoDeepHed(subkey, subvalue)
  194. }
  195. }
  196. /**
  197. * Categorize the column strings into value strings and categorical strings
  198. * @private
  199. */
  200. _categorizeHedStrings() {
  201. this.hedValueStrings = []
  202. this.hedCategoricalStrings = []
  203. this.hedData = new Map()
  204. for (const [key, sidecarValue] of this.sidecarKeys.entries()) {
  205. if (sidecarValue.isValueKey) {
  206. this.hedValueStrings.push(sidecarValue.valueString)
  207. this.hedData.set(key, sidecarValue.valueString)
  208. } else if (sidecarValue.categoryMap) {
  209. this.hedCategoricalStrings.push(...sidecarValue.categoryMap.values())
  210. this.hedData.set(key, sidecarValue.categoryMap)
  211. }
  212. }
  213. }
  214. /**
  215. * Generate a mapping of an individual BIDS sidecar's curly brace references.
  216. *
  217. * @private
  218. */
  219. _generateSidecarColumnSpliceMap() {
  220. this.columnSpliceMapping = new Map()
  221. this.columnSpliceReferences = new Set()
  222. for (const [sidecarKey, hedData] of this.parsedHedData) {
  223. if (hedData instanceof ParsedHedString) {
  224. this._parseValueSplice(sidecarKey, hedData)
  225. } else if (hedData instanceof Map) {
  226. this._parseCategorySplice(sidecarKey, hedData)
  227. } else if (hedData) {
  228. IssueError.generateAndThrowInternalError('Unexpected type found in sidecar parsedHedData map.')
  229. }
  230. }
  231. }
  232. /**
  233. *
  234. * @param {BidsSidecarKey} sidecarKey - The column to be checked for column splices.
  235. * @param {ParsedHedString} hedData - The parsed HED string to check for column splices.
  236. * @private
  237. */
  238. _parseValueSplice(sidecarKey, hedData) {
  239. if (hedData.columnSplices.length > 0) {
  240. const keyReferences = this._processColumnSplices(new Set(), hedData.columnSplices)
  241. this.columnSpliceMapping.set(sidecarKey, keyReferences)
  242. }
  243. }
  244. /**
  245. *
  246. * @param {BidsSidecarKey} sidecarKey - The column to be checked for column splices.
  247. * @param {ParsedHedString} hedData - The parsed HED string to check for column splices.
  248. * @private
  249. */
  250. _parseCategorySplice(sidecarKey, hedData) {
  251. let keyReferences = null
  252. for (const valueString of hedData.values()) {
  253. if (valueString?.columnSplices.length > 0) {
  254. keyReferences = this._processColumnSplices(keyReferences, valueString.columnSplices)
  255. }
  256. }
  257. if (keyReferences instanceof Set) {
  258. this.columnSpliceMapping.set(sidecarKey, keyReferences)
  259. }
  260. }
  261. /**
  262. * Add a list of columnSplices to a key set.
  263. * @param {Set<string>|null} keyReferences
  264. * @param {ParsedHedColumnSplice[]} columnSplices
  265. * @returns {Set<string>}
  266. * @private
  267. */
  268. _processColumnSplices(keyReferences, columnSplices) {
  269. keyReferences ??= new Set()
  270. for (const columnSplice of columnSplices) {
  271. keyReferences.add(columnSplice.originalTag)
  272. this.columnSpliceReferences.add(columnSplice.originalTag)
  273. }
  274. return keyReferences
  275. }
  276. }
  277. export class BidsSidecarKey {
  278. /**
  279. * The name of this key.
  280. * @type {string}
  281. */
  282. name
  283. /**
  284. * The unparsed category mapping.
  285. * @type {Map<string, string>}
  286. */
  287. categoryMap
  288. /**
  289. * The parsed category mapping.
  290. * @type {Map<string, ParsedHedString>}
  291. */
  292. parsedCategoryMap
  293. /**
  294. * The unparsed value string.
  295. * @type {string}
  296. */
  297. valueString
  298. /**
  299. * The parsed value string.
  300. * @type {ParsedHedString}
  301. */
  302. parsedValueString
  303. /**
  304. * Weak reference to the sidecar.
  305. * @type {BidsSidecar}
  306. */
  307. sidecar
  308. /**
  309. * Indication of whether this key is for definitions.
  310. * @type {Boolean}
  311. */
  312. hasDefinitions
  313. /**
  314. * Constructor.
  315. *
  316. * @param {string} key The name of this key.
  317. * @param {string|Object<string, string>} data The data for this key.
  318. * @param {BidsSidecar} sidecar The parent sidecar.
  319. */
  320. constructor(key, data, sidecar) {
  321. this.name = key
  322. this.hasDefinitions = false // May reset to true when definitions for this key are checked
  323. this.sidecar = sidecar
  324. if (typeof data === 'string') {
  325. this.valueString = data
  326. } else if (!isPlainObject(data)) {
  327. IssueError.generateAndThrow('illegalSidecarHedType', { key: key, file: sidecar.file.relativePath })
  328. } else {
  329. this.categoryMap = new Map(Object.entries(data))
  330. }
  331. }
  332. /**
  333. * Parse the HED data for this key.
  334. *
  335. * ###Note: This sets the parsedHedData as a side effect.
  336. *
  337. * @param {Schemas} hedSchemas The HED schema collection.
  338. * @param {boolean} fullValidation True if full validation should be performed.
  339. * @returns {Array} - [Issue[], Issues[]] Errors and warnings that result from parsing.
  340. */
  341. parseHed(hedSchemas, fullValidation = false) {
  342. if (this.isValueKey) {
  343. return this._parseValueString(hedSchemas, fullValidation)
  344. }
  345. return this._parseCategory(hedSchemas, fullValidation) // This is a Map of string to ParsedHedString
  346. }
  347. /**
  348. * Parse the value string in a sidecar.
  349. *
  350. * ### Note:
  351. * The value strings cannot contain definitions.
  352. *
  353. * @param {Schemas} hedSchemas - The HED schemas to use.
  354. * @param {boolean} fullValidation - True if full validation should be performed.
  355. * @returns {Array} - [Issue[], Issue[]] - Errors due for the value.
  356. * @private
  357. */
  358. _parseValueString(hedSchemas, fullValidation) {
  359. const [parsedString, errorIssues, warningIssues] = parseHedString(
  360. this.valueString,
  361. hedSchemas,
  362. false,
  363. true,
  364. fullValidation,
  365. )
  366. this.parsedValueString = parsedString
  367. return [errorIssues, warningIssues]
  368. }
  369. /**
  370. * Parse the categorical values associated with this key.
  371. * @param {Schemas} hedSchemas - The HED schemas used to check against.
  372. * @param {boolean} fullValidation - True if full validation should be performed.
  373. * @returns {Array} - Array[Issue[], Issue[]] A list of error issues and warning issues.
  374. * @private
  375. */
  376. _parseCategory(hedSchemas, fullValidation) {
  377. this.parsedCategoryMap = new Map()
  378. const errors = []
  379. const warnings = []
  380. for (const [value, string] of this.categoryMap) {
  381. const trimmedValue = value.trim()
  382. if (ILLEGAL_SIDECAR_KEYS.has(trimmedValue.toLowerCase())) {
  383. IssueError.generateAndThrow('illegalSidecarHedCategoricalValue')
  384. } else if (typeof string !== 'string') {
  385. IssueError.generateAndThrow('illegalSidecarHedType', {
  386. key: value,
  387. file: this.sidecar?.file?.relativePath,
  388. })
  389. }
  390. const [parsedString, errorIssues, warningIssues] =
  391. parseHedString(string, hedSchemas, true, true, fullValidation)
  392. this.parsedCategoryMap.set(value, parsedString)
  393. warnings.push(...warningIssues)
  394. errors.push(...errorIssues)
  395. if (errorIssues.length === 0) {
  396. errors.push(...this._checkDefinitions(parsedString))
  397. }
  398. }
  399. return [errors, warnings]
  400. }
  401. /**
  402. * Check for definitions in the HED string.
  403. * @param {ParsedHedString} parsedString - The string to check for definitions.
  404. * @returns {Issue[]} - Errors that occur.
  405. * @private
  406. */
  407. _checkDefinitions(parsedString) {
  408. const errors = []
  409. for (const group of parsedString.tagGroups) {
  410. if (!group.isDefinitionGroup) {
  411. continue
  412. }
  413. this.hasDefinitions = true
  414. const [def, defIssues] = Definition.createDefinitionFromGroup(group)
  415. if (defIssues.length > 0) {
  416. errors.push(...defIssues)
  417. } else {
  418. errors.push(...this.sidecar.definitions.addDefinition(def))
  419. }
  420. }
  421. return errors
  422. }
  423. /**
  424. * Whether this key is a value key.
  425. * @returns {boolean}
  426. */
  427. get isValueKey() {
  428. return Boolean(this.valueString)
  429. }
  430. }