From 296c5711296a2fb0b0f64796328844a4d0ba11b4 Mon Sep 17 00:00:00 2001 From: Alexander Jones Date: Sun, 16 Jun 2024 10:38:43 -0500 Subject: [PATCH] Merge short-to-long mapping into parsed schema data This is stage 1 of rewriting the converter. --- common/schema/types.js | 11 +-- converter/converter.js | 57 +++++++------ converter/schema.js | 47 ----------- converter/types.js | 63 -------------- parser/parsedHedTag.js | 11 +-- tests/schema.spec.js | 21 ++--- validator/event/hed3.js | 6 +- validator/schema/hed3.js | 170 ++++++++++++++++++++++++++------------ validator/schema/init.js | 4 +- validator/schema/types.js | 170 ++++++++++++++++++++++++++++++++------ 10 files changed, 309 insertions(+), 251 deletions(-) delete mode 100644 converter/schema.js delete mode 100644 converter/types.js diff --git a/common/schema/types.js b/common/schema/types.js index cf443537..1477b48b 100644 --- a/common/schema/types.js +++ b/common/schema/types.js @@ -106,11 +106,6 @@ export class Hed3Schema extends Schema { * @type {SchemaEntries} */ entries - /** - * The mapping between short and long tags. - * @type {Mapping} - */ - mapping /** * The standard HED schema version this schema is linked to. * @type {string} @@ -122,9 +117,8 @@ export class Hed3Schema extends Schema { * * @param {object} xmlData The schema XML data. * @param {SchemaEntries} entries A collection of schema entries. - * @param {Mapping} mapping A mapping between short and long tags. */ - constructor(xmlData, entries, mapping) { + constructor(xmlData, entries) { super(xmlData) if (!this.library) { @@ -133,7 +127,6 @@ export class Hed3Schema extends Schema { this.withStandard = xmlData.HED?.$?.withStandard } this.entries = entries - this.mapping = mapping } /** @@ -164,7 +157,7 @@ export class PartneredSchema extends Hed3Schema { * @param {Hed3Schema} actualSchema The actual HED 3 schema underlying this partnered schema. */ constructor(actualSchema) { - super({}, actualSchema.entries, actualSchema.mapping) + super({}, actualSchema.entries) this.actualSchema = actualSchema this.withStandard = actualSchema.withStandard this.library = undefined diff --git a/converter/converter.js b/converter/converter.js index ae39c90f..55f79b17 100644 --- a/converter/converter.js +++ b/converter/converter.js @@ -1,6 +1,5 @@ import castArray from 'lodash/castArray' -import { TagEntry } from './types' import generateIssue from './issues' import splitHedString from './splitHedString' @@ -23,14 +22,14 @@ export const removeSlashesAndSpaces = function (hedString) { * on for HED 3 schemas) allow for similar HED 2 validation with minimal code * duplication. * - * @param {Schema} schema The schema object containing a short-to-long mapping. + * @param {Hed3Schema} schema The schema object containing a short-to-long mapping. * @param {string} hedTag The HED tag to convert. * @param {string} hedString The full HED string (for error messages). * @param {number} offset The offset of this tag within the HED string. * @returns {[string, Issue[]]} The long-form tag and any issues. */ export const convertTagToLong = function (schema, hedTag, hedString, offset) { - const mapping = schema.mapping + const schemaTags = schema.entries.tags if (hedTag.startsWith('/')) { hedTag = hedTag.slice(1) @@ -43,7 +42,7 @@ export const convertTagToLong = function (schema, hedTag, hedString, offset) { const splitTag = cleanedTag.split('/') /** - * @type {TagEntry} + * @type {SchemaTag} */ let foundTagEntry = null let takesValueTag = false @@ -51,7 +50,15 @@ export const convertTagToLong = function (schema, hedTag, hedString, offset) { let foundUnknownExtension = false let foundEndingIndex = 0 - const generateParentNodeIssue = (tagEntries, startingIndex, endingIndex) => { + /** + * Generate an issue for an invalid parent node. + * + * @param {SchemaTag[]} schemaTags The invalid schema tags. + * @param {number} startingIndex The starting index in the string. + * @param {number} endingIndex The ending index in the string. + * @returns {[string, Issue[]]} The original HED string and the issue. + */ + const generateParentNodeIssue = (schemaTags, startingIndex, endingIndex) => { return [ hedTag, [ @@ -60,11 +67,11 @@ export const convertTagToLong = function (schema, hedTag, hedString, offset) { hedString, { parentTag: - tagEntries.length > 1 - ? tagEntries.map((tagEntry) => { - return tagEntry.longTag + schemaTags.length > 1 + ? schemaTags.map((tagEntry) => { + return tagEntry.longName }) - : tagEntries[0].longTag, + : schemaTags[0].longName, }, [startingIndex + offset, endingIndex + offset], ), @@ -79,16 +86,16 @@ export const convertTagToLong = function (schema, hedTag, hedString, offset) { const startingIndex = endingIndex endingIndex += tag.length - const tagEntries = castArray(mapping.shortToTags.get(tag)) + const tagEntries = castArray(schemaTags.getEntry(tag)) if (foundUnknownExtension) { - if (mapping.shortToTags.has(tag)) { + if (schemaTags.hasEntry(tag)) { return generateParentNodeIssue(tagEntries, startingIndex, endingIndex) } else { continue } } - if (!mapping.shortToTags.has(tag)) { + if (!schemaTags.hasEntry(tag)) { if (foundTagEntry === null) { return [hedTag, [generateIssue('invalidTag', hedString, {}, [startingIndex + offset, endingIndex + offset])]] } @@ -99,14 +106,14 @@ export const convertTagToLong = function (schema, hedTag, hedString, offset) { let tagFound = false for (const tagEntry of tagEntries) { - const tagString = tagEntry.longFormattedTag + const tagString = tagEntry.longName.toLowerCase() const mainHedPortion = cleanedTag.slice(0, endingIndex) if (tagString.endsWith(mainHedPortion)) { tagFound = true foundEndingIndex = endingIndex foundTagEntry = tagEntry - if (tagEntry.takesValue) { + if (tagEntry.valueTag) { takesValueTag = true } break @@ -118,21 +125,21 @@ export const convertTagToLong = function (schema, hedTag, hedString, offset) { } const remainder = hedTag.slice(foundEndingIndex) - const longTagString = foundTagEntry.longTag + remainder + const longTagString = foundTagEntry.longName + remainder return [longTagString, []] } /** * Convert a HED tag to short form. * - * @param {Schema} schema The schema object containing a short-to-long mapping. + * @param {Hed3Schema} schema The schema object containing a short-to-long mapping. * @param {string} hedTag The HED tag to convert. * @param {string} hedString The full HED string (for error messages). * @param {number} offset The offset of this tag within the HED string. * @returns {[string, Issue[]]} The short-form tag and any issues. */ export const convertTagToShort = function (schema, hedTag, hedString, offset) { - const mapping = schema.mapping + const schemaTags = schema.entries.tags if (hedTag.startsWith('/')) { hedTag = hedTag.slice(1) @@ -146,15 +153,15 @@ export const convertTagToShort = function (schema, hedTag, hedString, offset) { splitTag.reverse() /** - * @type {TagEntry} + * @type {SchemaTag} */ let foundTagEntry = null let index = hedTag.length let lastFoundIndex = index for (const tag of splitTag) { - if (mapping.shortToTags.has(tag)) { - foundTagEntry = mapping.shortToTags.get(tag) + if (schemaTags.hasEntry(tag)) { + foundTagEntry = schemaTags.getEntry(tag) lastFoundIndex = index index -= tag.length break @@ -173,12 +180,12 @@ export const convertTagToShort = function (schema, hedTag, hedString, offset) { } const mainHedPortion = cleanedTag.slice(0, lastFoundIndex) - const tagString = foundTagEntry.longFormattedTag + const tagString = foundTagEntry.longName.toLowerCase() if (!tagString.endsWith(mainHedPortion)) { return [ hedTag, [ - generateIssue('invalidParentNode', hedString, { parentTag: foundTagEntry.longTag }, [ + generateIssue('invalidParentNode', hedString, { parentTag: foundTagEntry.longName }, [ index + offset, lastFoundIndex + offset, ]), @@ -187,7 +194,7 @@ export const convertTagToShort = function (schema, hedTag, hedString, offset) { } const remainder = hedTag.slice(lastFoundIndex) - const shortTagString = foundTagEntry.shortTag + remainder + const shortTagString = foundTagEntry.name + remainder return [shortTagString, []] } @@ -196,7 +203,7 @@ export const convertTagToShort = function (schema, hedTag, hedString, offset) { * * This is for the internal string parsing for the validation side. * - * @param {Schema} schema The schema object containing a short-to-long mapping. + * @param {Hed3Schema} schema The schema object containing a short-to-long mapping. * @param {string} partialHedString The partial HED string to convert to long form. * @param {string} fullHedString The full HED string. * @param {number} offset The offset of the partial HED string within the full string. @@ -232,7 +239,7 @@ export const convertPartialHedStringToLong = function (schema, partialHedString, /** * Convert a HED string. * - * @param {Schema} schema The schema object containing a short-to-long mapping. + * @param {Hed3Schema} schema The schema object containing a short-to-long mapping. * @param {string} hedString The HED tag to convert. * @param {function (Schema, string, string, number): [string, Issue[]]} conversionFn The conversion function for a tag. * @returns {[string, Issue[]]} The converted string and any issues. diff --git a/converter/schema.js b/converter/schema.js deleted file mode 100644 index e80bccb0..00000000 --- a/converter/schema.js +++ /dev/null @@ -1,47 +0,0 @@ -import { Mapping, TagEntry } from './types' -import { getTagName } from '../utils/hedStrings' -import { generateIssue, IssueError } from '../common/issues/issues' - -/** - * Build a short-long mapping object from schema XML data. - * - * @param {SchemaEntries} entries The schema XML data. - * @returns {Mapping} The mapping object. - */ -export const buildMappingObject = function (entries) { - /** - * @type {Map} - */ - const shortTagData = new Map() - /** - * @type {Map} - */ - const longTagData = new Map() - /** - * @type {Set} - */ - const takesValueTags = new Set() - /** - * @type {SchemaEntryManager} - */ - const schemaTags = entries.definitions.get('tags') - for (const tag of schemaTags.values()) { - const shortTag = getTagName(tag.name) - const lowercaseShortTag = shortTag.toLowerCase() - if (shortTag === '#') { - takesValueTags.add(getTagName(tag.parent.name).toLowerCase()) - continue - } - const tagObject = new TagEntry(shortTag, tag.name) - longTagData.set(tag.name, tagObject) - if (!shortTagData.has(lowercaseShortTag)) { - shortTagData.set(lowercaseShortTag, tagObject) - } else { - throw new IssueError(generateIssue('duplicateTagsInSchema', {})) - } - } - for (const tag of takesValueTags) { - shortTagData.get(tag).takesValue = true - } - return new Mapping(shortTagData, longTagData) -} diff --git a/converter/types.js b/converter/types.js deleted file mode 100644 index 8ab59a3c..00000000 --- a/converter/types.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * A tag dictionary entry. - */ -export class TagEntry { - /** - * The short version of the tag. - * @type {string} - */ - shortTag - /** - * The long version of the tag. - * @type {string} - */ - longTag - /** - * The formatted long version of the tag. - * @type {string} - */ - longFormattedTag - /** - * Whether this tag takes a value. - * @type {boolean} - */ - takesValue - - /** - * Constructor. - * @param {string} shortTag The short version of the tag. - * @param {string} longTag The long version of the tag. - */ - constructor(shortTag, longTag) { - this.shortTag = shortTag - this.longTag = longTag - this.longFormattedTag = longTag.toLowerCase() - } -} - -/** - * A short-to-long mapping. - */ -export class Mapping { - /** - * A dictionary mapping short forms to TagEntry instances. - * @type {Map} - */ - shortToTags - /** - * A dictionary mapping long forms to TagEntry instances. - * @type {Map} - */ - longToTags - - /** - * Constructor. - * - * @param {Map} shortToTags A dictionary mapping short forms to TagEntry instances. - * @param {Map} longToTags A dictionary mapping long forms to TagEntry instances. - */ - constructor(shortToTags, longToTags) { - this.shortToTags = shortToTags - this.longToTags = longToTags - } -} diff --git a/parser/parsedHedTag.js b/parser/parsedHedTag.js index 9e47fffa..a01fa615 100644 --- a/parser/parsedHedTag.js +++ b/parser/parsedHedTag.js @@ -347,7 +347,7 @@ export class ParsedHed3Tag extends ParsedHedTag { * @returns {string} The nicely formatted version of this tag. */ format() { - let tagName = this.schema?.entries.definitions.get('tags').getEntry(this.formattedTag)?.name + let tagName = this.schema?.entries?.tags?.getLongNameEntry(this.formattedTag)?.longName if (tagName === undefined) { tagName = this.originalTag } @@ -365,7 +365,7 @@ export class ParsedHed3Tag extends ParsedHedTag { */ get existsInSchema() { return this._memoize('existsInSchema', () => { - return this.schema?.entries.definitions.get('tags').hasEntry(this.formattedTag) + return this.schema?.entries?.tags?.hasLongNameEntry(this.formattedTag) }) } @@ -395,7 +395,7 @@ export class ParsedHed3Tag extends ParsedHedTag { get takesValueTag() { return this._memoize('takesValueTag', () => { if (this.takesValueFormattedTag !== null) { - return this.schema?.entries.definitions.get('tags').getEntry(this.takesValueFormattedTag) + return this.schema?.entries?.tags?.getLongNameEntry(this.takesValueFormattedTag) } else { return null } @@ -420,9 +420,6 @@ export class ParsedHed3Tag extends ParsedHedTag { */ get hasUnitClass() { return this._memoize('hasUnitClass', () => { - if (!this.schema?.entries.definitions.has('unitClasses')) { - return false - } if (this.takesValueTag === null) { return false } @@ -475,7 +472,7 @@ export class ParsedHed3Tag extends ParsedHedTag { const tagUnitClasses = this.unitClasses const units = new Set() for (const unitClass of tagUnitClasses) { - const unitClassUnits = this.schema?.entries.unitClassMap.getEntry(unitClass.name).units + const unitClassUnits = this.schema?.entries.unitClasses.getEntry(unitClass.name).units for (const unit of unitClassUnits.values()) { units.add(unit) } diff --git a/tests/schema.spec.js b/tests/schema.spec.js index 48e5fc47..160e9b5b 100644 --- a/tests/schema.spec.js +++ b/tests/schema.spec.js @@ -349,24 +349,15 @@ describe('HED schemas', () => { it('should contain all of the tag group tags', async () => { const hedSchemas = await hedSchemaPromise - const tagGroupTags = ['property/organizational-property/def-expand'] - const schemaTagGroupTags = hedSchemas.baseSchema.entries.definitions - .get('tags') - .getEntriesWithBooleanAttribute('tagGroup') + const tagGroupTags = ['def-expand'] + const schemaTagGroupTags = hedSchemas.baseSchema.entries.tags.getEntriesWithBooleanAttribute('tagGroup') assert.hasAllKeys(schemaTagGroupTags, tagGroupTags) }) it('should contain all of the top-level tag group tags', async () => { const hedSchemas = await hedSchemaPromise - const tagGroupTags = [ - 'property/organizational-property/definition', - 'property/organizational-property/event-context', - 'property/data-property/data-marker/temporal-marker/onset', - 'property/data-property/data-marker/temporal-marker/offset', - ] - const schemaTagGroupTags = hedSchemas.baseSchema.entries.definitions - .get('tags') - .getEntriesWithBooleanAttribute('topLevelTagGroup') + const tagGroupTags = ['definition', 'event-context', 'onset', 'offset'] + const schemaTagGroupTags = hedSchemas.baseSchema.entries.tags.getEntriesWithBooleanAttribute('topLevelTagGroup') assert.hasAllKeys(schemaTagGroupTags, tagGroupTags) }) @@ -403,7 +394,7 @@ describe('HED schemas', () => { weightUnits: ['g', 'gram', 'pound', 'lb'], } - const schemaUnitClasses = hedSchemas.baseSchema.entries.definitions.get('unitClasses') + const schemaUnitClasses = hedSchemas.baseSchema.entries.unitClasses for (const [unitClassName, unitClass] of schemaUnitClasses) { const defaultUnit = unitClass.defaultUnit assert.strictEqual( @@ -426,7 +417,7 @@ describe('HED schemas', () => { takesValue: 88, } - const schemaTags = hedSchemas.baseSchema.entries.definitions.get('tags') + const schemaTags = hedSchemas.baseSchema.entries.tags for (const [attribute, count] of Object.entries(expectedAttributeTagCount)) { assert.lengthOf( schemaTags.getEntriesWithBooleanAttribute(attribute), diff --git a/validator/event/hed3.js b/validator/event/hed3.js index bc482615..6f715f57 100644 --- a/validator/event/hed3.js +++ b/validator/event/hed3.js @@ -96,9 +96,9 @@ export class Hed3Validator extends HedValidator { _checkForTagAttribute(attribute, fn) { const schemas = this.hedSchemas.schemas.values() for (const schema of schemas) { - const tags = schema.entries.definitions.get('tags').getEntriesWithBooleanAttribute(attribute) - for (const tag of tags) { - fn(tag.name) + const tags = schema.entries.tags.getEntriesWithBooleanAttribute(attribute) + for (const tag of tags.values()) { + fn(tag.longName) } } } diff --git a/validator/schema/hed3.js b/validator/schema/hed3.js index 3b437103..eb5187a1 100644 --- a/validator/schema/hed3.js +++ b/validator/schema/hed3.js @@ -1,3 +1,6 @@ +import zip from 'lodash/zip' +import semver from 'semver' + // TODO: Switch require once upstream bugs are fixed. // import xpath from 'xml2js-xpath' // Temporary @@ -5,24 +8,54 @@ import * as xpath from '../../utils/xpath' import { SchemaParser } from './parser' import { + nodeProperty, + SchemaAttribute, + schemaAttributeProperty, SchemaEntries, SchemaEntryManager, - SchemaAttribute, SchemaProperty, SchemaTag, + SchemaTagManager, SchemaUnit, SchemaUnitClass, SchemaUnitModifier, SchemaValueClass, - nodeProperty, - schemaAttributeProperty, + SchemaValueTag, } from './types' import { generateIssue, IssueError } from '../../common/issues/issues' -import { buildMappingObject } from '../../converter/schema' const lc = (str) => str.toLowerCase() export class Hed3SchemaParser extends SchemaParser { + /** + * @type {Map} + */ + properties + /** + * @type {Map} + */ + attributes + /** + * The schema's value classes. + * @type {SchemaEntryManager} + */ + valueClasses + /** + * The schema's unit classes. + * @type {SchemaEntryManager} + */ + unitClasses + /** + * The schema's unit modifiers. + * @type {SchemaEntryManager} + */ + unitModifiers + /** + * The schema's tags. + * @type {SchemaTagManager} + */ + tags + constructor(rootElement) { super(rootElement) this._versionDefinitions = {} @@ -36,7 +69,6 @@ export class Hed3SchemaParser extends SchemaParser { populateDictionaries() { this.parseProperties() this.parseAttributes() - this.definitions = new Map() this.parseUnitModifiers() this.parseUnitClasses() this.parseTags() @@ -57,10 +89,16 @@ export class Hed3SchemaParser extends SchemaParser { } } + /** + * Retrieve all the tags in the schema. + * + * @param {string} tagElementName The name of the tag element. + * @returns {Map} The tag names and XML elements. + */ getAllTags(tagElementName = 'node') { const tagElements = xpath.find(this.rootElement, '//' + tagElementName) - const tags = tagElements.map((element) => this.getTagPathFromTagElement(element)) - return [tags, tagElements] + const tags = tagElements.map((element) => this.getElementTagName(element)) + return new Map(zip(tagElements, tags)) } // Rewrite starts here. @@ -120,7 +158,7 @@ export class Hed3SchemaParser extends SchemaParser { const booleanAttributes = booleanAttributeDefinitions.get(name) valueClasses.set(name, new SchemaValueClass(name, booleanAttributes, valueAttributes)) } - this.definitions.set('valueClasses', new SchemaEntryManager(valueClasses)) + this.valueClasses = new SchemaEntryManager(valueClasses) } parseUnitModifiers() { @@ -130,7 +168,7 @@ export class Hed3SchemaParser extends SchemaParser { const booleanAttributes = booleanAttributeDefinitions.get(name) unitModifiers.set(name, new SchemaUnitModifier(name, booleanAttributes, valueAttributes)) } - this.definitions.set('unitModifiers', new SchemaEntryManager(unitModifiers)) + this.unitModifiers = new SchemaEntryManager(unitModifiers) } parseUnitClasses() { @@ -142,13 +180,13 @@ export class Hed3SchemaParser extends SchemaParser { const booleanAttributes = booleanAttributeDefinitions.get(name) unitClasses.set(name, new SchemaUnitClass(name, booleanAttributes, valueAttributes, unitClassUnits.get(name))) } - this.definitions.set('unitClasses', new SchemaEntryManager(unitClasses)) + this.unitClasses = new SchemaEntryManager(unitClasses) } parseUnits() { const unitClassUnits = new Map() const unitClassElements = this.getElementsByName('unitClassDefinition') - const unitModifiers = this.definitions.get('unitModifiers') + const unitModifiers = this.unitModifiers for (const element of unitClassElements) { const elementName = this.getElementTagName(element) const units = new Map() @@ -169,30 +207,35 @@ export class Hed3SchemaParser extends SchemaParser { } parseTags() { - const [tags, tagElements] = this.getAllTags() - const lowercaseTags = tags.map(lc) - this.tags = new Set(lowercaseTags) + const tags = this.getAllTags() + const shortTags = new Map() + for (const tagElement of tags.keys()) { + const shortKey = + this.getElementTagName(tagElement) === '#' + ? this.getParentTagName(tagElement) + '-#' + : this.getElementTagName(tagElement) + shortTags.set(tagElement, shortKey) + } const [booleanAttributeDefinitions, valueAttributeDefinitions] = this._parseAttributeElements( - tagElements, - (element) => this.getTagPathFromTagElement(element), + tags.keys(), + (element) => shortTags.get(element), ) const recursiveAttributes = Array.from(this.attributes.values()).filter((attribute) => - attribute.roleProperties.has(this.properties.get('recursiveProperty')), + attribute.roleProperties.has(this.properties.get('isInheritedProperty')), ) - const unitClasses = this.definitions.get('unitClasses') const tagUnitClassAttribute = this.attributes.get('unitClass') + const tagTakesValueAttribute = this.attributes.get('takesValue') const tagUnitClassDefinitions = new Map() const recursiveChildren = new Map() - tags.forEach((tagName, index) => { - const tagElement = tagElements[index] + for (const [tagElement, tagName] of shortTags) { const valueAttributes = valueAttributeDefinitions.get(tagName) if (valueAttributes.has(tagUnitClassAttribute)) { tagUnitClassDefinitions.set( tagName, valueAttributes.get(tagUnitClassAttribute).map((unitClassName) => { - return unitClasses.getEntry(unitClassName) + return this.unitClasses.getEntry(unitClassName) }), ) valueAttributes.delete(tagUnitClassAttribute) @@ -204,31 +247,46 @@ export class Hed3SchemaParser extends SchemaParser { } recursiveChildren.set(attribute, children) } - }) + } for (const [attribute, childTagElements] of recursiveChildren) { for (const tagElement of childTagElements) { - const tagName = this.getTagPathFromTagElement(tagElement) + const tagName = this.getElementTagName(tagElement) booleanAttributeDefinitions.get(tagName).add(attribute) } } const tagEntries = new Map() for (const [name, valueAttributes] of valueAttributeDefinitions) { + if (tagEntries.has(name)) { + throw new IssueError(generateIssue('duplicateTagsInSchema', {})) + } const booleanAttributes = booleanAttributeDefinitions.get(name) const unitClasses = tagUnitClassDefinitions.get(name) - tagEntries.set(lc(name), new SchemaTag(name, booleanAttributes, valueAttributes, unitClasses)) + if (booleanAttributes.has(tagTakesValueAttribute)) { + tagEntries.set(lc(name), new SchemaValueTag(name, booleanAttributes, valueAttributes, unitClasses)) + } else { + tagEntries.set(lc(name), new SchemaTag(name, booleanAttributes, valueAttributes, unitClasses)) + } } - for (const tagElement of tagElements) { - const tagName = this.getTagPathFromTagElement(tagElement) - const parentTagName = this.getParentTagPath(tagElement) + for (const tagElement of tags.keys()) { + const tagName = shortTags.get(tagElement) + const parentTagName = shortTags.get(tagElement.$parent) if (parentTagName) { tagEntries.get(lc(tagName))._parent = tagEntries.get(lc(parentTagName)) } + if (this.getElementTagName(tagElement) === '#') { + tagEntries.get(lc(parentTagName))._valueTag = tagEntries.get(lc(tagName)) + } + } + + const longNameTagEntries = new Map() + for (const tag of tagEntries.values()) { + longNameTagEntries.set(lc(tag.longName), tag) } - this.definitions.set('tags', new SchemaEntryManager(tagEntries)) + this.tags = new SchemaTagManager(tagEntries, longNameTagEntries) } _parseDefinitions(category) { @@ -300,18 +358,22 @@ export class HedV8SchemaParser extends Hed3SchemaParser { } _addCustomAttributes() { - const recursiveProperty = this.properties.get('recursiveProperty') + const isInheritedProperty = this.properties.get('isInheritedProperty') const extensionAllowedAttribute = this.attributes.get('extensionAllowed') - extensionAllowedAttribute._roleProperties.add(recursiveProperty) + if (this.rootElement.$.library === undefined && semver.lt(this.rootElement.$.version, '8.2.0')) { + extensionAllowedAttribute._roleProperties.add(isInheritedProperty) + } const inLibraryAttribute = this.attributes.get('inLibrary') if (inLibraryAttribute) { - inLibraryAttribute._roleProperties.add(recursiveProperty) + inLibraryAttribute._roleProperties.add(isInheritedProperty) } } _addCustomProperties() { - const recursiveProperty = new SchemaProperty('recursiveProperty', 'roleProperty') - this.properties.set('recursiveProperty', recursiveProperty) + if (this.rootElement.$.library === undefined && semver.lt(this.rootElement.$.version, '8.2.0')) { + const recursiveProperty = new SchemaProperty('isInheritedProperty', 'roleProperty') + this.properties.set('isInheritedProperty', recursiveProperty) + } } } @@ -362,28 +424,19 @@ export class Hed3PartneredSchemaMerger { /** * The source schema's tag collection. * - * @return {SchemaEntryManager} + * @return {SchemaTagManager} */ get sourceTags() { - return this.source.entries.definitions.get('tags') + return this.source.entries.tags } /** * The destination schema's tag collection. * - * @return {SchemaEntryManager} + * @return {SchemaTagManager} */ get destinationTags() { - return this.destination.entries.definitions.get('tags') - } - - /** - * The source schema's mapping from long tag names to TagEntry objects. - * - * @return {Map} - */ - get sourceLongToTags() { - return this.source.mapping.longToTags + return this.destination.entries.tags } /** @@ -393,7 +446,6 @@ export class Hed3PartneredSchemaMerger { */ mergeData() { this.mergeTags() - this.destination.mapping = buildMappingObject(this.destination.entries) return this.destination } @@ -417,15 +469,15 @@ export class Hed3PartneredSchemaMerger { return } - const shortName = this.sourceLongToTags.get(tag.name).shortTag - if (this.destination.mapping.shortToTags.has(shortName.toLowerCase())) { + const shortName = tag.name + if (this.destinationTags.hasEntry(shortName.toLowerCase())) { throw new IssueError(generateIssue('lazyPartneredSchemasShareTag', { tag: shortName })) } const rootedTagShortName = tag.getNamedAttributeValue('rooted') if (rootedTagShortName) { const parentTag = tag.parent - if (this.sourceLongToTags.get(parentTag?.name)?.shortTag?.toLowerCase() !== rootedTagShortName?.toLowerCase()) { + if (parentTag?.name?.toLowerCase() !== rootedTagShortName?.toLowerCase()) { throw new Error(`Node ${shortName} is improperly rooted.`) } } @@ -454,12 +506,24 @@ export class Hed3PartneredSchemaMerger { * @type {SchemaUnitClass[]} */ const unitClasses = tag.unitClasses.map( - (unitClass) => this.destination.entries.unitClassMap.getEntry(unitClass.name) ?? unitClass, + (unitClass) => this.destination.entries.unitClasses.getEntry(unitClass.name) ?? unitClass, ) - const newTag = new SchemaTag(tag.name, booleanAttributes, valueAttributes, unitClasses) - newTag._parent = this.destinationTags.getEntry(tag.parent?.name?.toLowerCase()) + let newTag + if (tag instanceof SchemaValueTag) { + newTag = new SchemaValueTag(tag.name, booleanAttributes, valueAttributes, unitClasses) + } else { + newTag = new SchemaTag(tag.name, booleanAttributes, valueAttributes, unitClasses) + } + const destinationParentTag = this.destinationTags.getEntry(tag.parent?.name?.toLowerCase()) + if (destinationParentTag) { + newTag._parent = destinationParentTag + if (newTag instanceof SchemaValueTag) { + newTag.parent._valueTag = newTag + } + } this.destinationTags._definitions.set(newTag.name.toLowerCase(), newTag) + this.destinationTags._definitionsByLongName.set(newTag.longName.toLowerCase(), newTag) } } diff --git a/validator/schema/init.js b/validator/schema/init.js index 57dde6a9..c7f9c170 100644 --- a/validator/schema/init.js +++ b/validator/schema/init.js @@ -3,7 +3,6 @@ import semver from 'semver' import { Schema, Schemas, Hed2Schema, Hed3Schema, SchemasSpec, PartneredSchema } from '../../common/schema/types' import loadSchema from '../../common/schema/loader' -import { buildMappingObject } from '../../converter/schema' import { setParent } from '../../utils/xml2js' import { Hed2SchemaParser } from '../hed2/schema/hed2SchemaParser' @@ -44,8 +43,7 @@ export const buildSchemaAttributesObject = function (xmlData) { const buildSchemaObject = function (xmlData) { const schemaAttributes = buildSchemaAttributesObject(xmlData) if (isHed3Schema(xmlData)) { - const mapping = buildMappingObject(schemaAttributes) - return new Hed3Schema(xmlData, schemaAttributes, mapping) + return new Hed3Schema(xmlData, schemaAttributes) } else { return new Hed2Schema(xmlData, schemaAttributes) } diff --git a/validator/schema/types.js b/validator/schema/types.js index 5131a8f3..09206e11 100644 --- a/validator/schema/types.js +++ b/validator/schema/types.js @@ -11,19 +11,34 @@ pluralize.addUncountableRule('hertz') export class SchemaEntries extends Memoizer { /** * The schema's properties. - * @type {SchemaEntryManager} + * @type {SchemaEntryManager} */ properties /** * The schema's attributes. - * @type {SchemaEntryManager} + * @type {SchemaEntryManager} */ attributes /** - * The schema's definitions. - * @type {Map} + * The schema's value classes. + * @type {SchemaEntryManager} */ - definitions + valueClasses + /** + * The schema's unit classes. + * @type {SchemaEntryManager} + */ + unitClasses + /** + * The schema's unit modifiers. + * @type {SchemaEntryManager} + */ + unitModifiers + /** + * The schema's tags. + * @type {SchemaTagManager} + */ + tags /** * Constructor. @@ -33,15 +48,10 @@ export class SchemaEntries extends Memoizer { super() this.properties = new SchemaEntryManager(schemaParser.properties) this.attributes = new SchemaEntryManager(schemaParser.attributes) - this.definitions = schemaParser.definitions - } - - /** - * Get the schema's unit classes. - * @returns {SchemaEntryManager} - */ - get unitClassMap() { - return this.definitions.get('unitClasses') + this.valueClasses = schemaParser.valueClasses + this.unitClasses = schemaParser.unitClasses + this.unitModifiers = schemaParser.unitModifiers + this.tags = schemaParser.tags } /** @@ -50,7 +60,7 @@ export class SchemaEntries extends Memoizer { get allUnits() { return this._memoize('allUnits', () => { const units = [] - for (const unitClass of this.unitClassMap.values()) { + for (const unitClass of this.unitClasses.values()) { const unitClassUnits = unitClass.units units.push(...unitClassUnits) } @@ -63,8 +73,7 @@ export class SchemaEntries extends Memoizer { * @returns {Map} */ get SIUnitModifiers() { - const unitModifiers = this.definitions.get('unitModifiers') - return unitModifiers.getEntriesWithBooleanAttribute('SIUnitModifier') + return this.unitModifiers.getEntriesWithBooleanAttribute('SIUnitModifier') } /** @@ -72,8 +81,7 @@ export class SchemaEntries extends Memoizer { * @returns {Map} */ get SIUnitSymbolModifiers() { - const unitModifiers = this.definitions.get('unitModifiers') - return unitModifiers.getEntriesWithBooleanAttribute('SIUnitSymbolModifier') + return this.unitModifiers.getEntriesWithBooleanAttribute('SIUnitSymbolModifier') } /** @@ -84,10 +92,10 @@ export class SchemaEntries extends Memoizer { * @returns {boolean} Whether this tag has this attribute. */ tagHasAttribute(tag, tagAttribute) { - if (!this.definitions.get('tags').hasEntry(tag)) { + if (!this.tags.hasLongNameEntry(tag)) { return false } - return this.definitions.get('tags').getEntry(tag).hasAttributeName(tagAttribute) + return this.tags.getLongNameEntry(tag).hasAttributeName(tagAttribute) } } @@ -197,10 +205,65 @@ export class SchemaEntryManager extends Memoizer { } } +/** + * A manager of {@link SchemaTag} objects. + * + * @extends {SchemaEntryManager} + */ +export class SchemaTagManager extends SchemaEntryManager { + /** + * The mapping of tags by long name. + * @type {Map} + */ + _definitionsByLongName + + /** + * Constructor. + * + * @param {Map} byShortName The mapping of tags by short name. + * @param {Map} byLongName The mapping of tags by long name. + */ + constructor(byShortName, byLongName) { + super(byShortName) + this._definitionsByLongName = byLongName + } + + /** + * Determine whether the tag with the given name exists. + * + * @param {string} longName The long name of the tag. + * @return {boolean} Whether the tag exists. + */ + hasLongNameEntry(longName) { + return this._definitionsByLongName.has(longName) + } + + /** + * Get the tag with the given name. + * + * @param {string} longName The long name of the tag to retrieve. + * @return {SchemaTag} The tag with that name. + */ + getLongNameEntry(longName) { + return this._definitionsByLongName.get(longName) + } + + /** + * Filter the map underlying this manager using the long name. + * + * @param {function ([string, T]): boolean} fn The filtering function. + * @returns {Map} The filtered map. + */ + filterByLongName(fn) { + const pairArray = Array.from(this._definitionsByLongName.entries()) + return new Map(pairArray.filter((entry) => fn(entry))) + } +} + /** * SchemaEntry class */ -export class SchemaEntry { +export class SchemaEntry extends Memoizer { /** * The name of this schema entry. * @type {string} @@ -208,6 +271,7 @@ export class SchemaEntry { _name constructor(name) { + super() this._name = name } @@ -599,16 +663,24 @@ export class SchemaValueClass extends SchemaEntryWithAttributes { * A tag in a HED schema. */ export class SchemaTag extends SchemaEntryWithAttributes { + /** + * This tag's parent tag. + * @type {SchemaTag} + * @private + */ + _parent /** * This tag's unit classes. * @type {SchemaUnitClass[]} + * @private */ _unitClasses /** - * This tag's parent tag. - * @type {SchemaTag} + * This tag's value-taking child. + * @type {SchemaValueTag} + * @private */ - _parent + _valueTag /** * Constructor. @@ -637,7 +709,15 @@ export class SchemaTag extends SchemaEntryWithAttributes { * @returns {boolean} */ get hasUnitClasses() { - return this._unitClasses.length !== 0 + return this.unitClasses.length !== 0 + } + + /** + * This tag's value-taking child. + * @returns {SchemaValueTag} + */ + get valueTag() { + return this._valueTag } /** @@ -647,4 +727,42 @@ export class SchemaTag extends SchemaEntryWithAttributes { get parent() { return this._parent } + + /** + * Return all of this tag's ancestors. + * @returns {*[]} + */ + get ancestors() { + return this._memoize('ancestors', () => { + if (this.parent) { + return [this.parent, ...this.parent.ancestors] + } + return [] + }) + } + + /** + * This tag's long name. + * @returns {string} + */ + get longName() { + const nameParts = this.ancestors.map((parentTag) => parentTag.name) + nameParts.reverse().push(this.name) + return nameParts.join('/') + } +} + +/** + * A value-taking tag in a HED schema. + */ +export class SchemaValueTag extends SchemaTag { + /** + * This tag's long name. + * @returns {string} + */ + get longName() { + const nameParts = this.ancestors.map((parentTag) => parentTag.name) + nameParts.reverse().push('#') + return nameParts.join('/') + } }