hed-standard/hed-javascript

View on GitHub
common/schema/types.js

Summary

Maintainability
A
40 mins
Test Coverage
B
87%
/** HED schema classes */

import { getGenerationForSchemaVersion } from '../../utils/hedData'

/**
 * An imported HED schema object.
 */
export class Schema {
  /**
   * The schema XML data.
   * @type {Object}
   * @deprecated Unused. Will be removed in 4.0.0.
   */
  xmlData
  /**
   * The HED schema version.
   * @type {string}
   */
  version
  /**
   * The HED generation of this schema.
   * @type {Number}
   */
  generation
  /**
   * The HED library schema name.
   * @type {string}
   */
  library
  /**
   * This schema's prefix in the active schema set.
   * @type {string}
   */
  prefix

  /**
   * Constructor.
   *
   * @param {object} xmlData The schema XML data.
   */
  constructor(xmlData) {
    this.xmlData = xmlData
    const rootElement = xmlData.HED
    this.version = rootElement?.$?.version
    this.library = rootElement?.$?.library ?? ''

    if (this.library) {
      this.generation = 3
    } else if (this.version) {
      this.generation = getGenerationForSchemaVersion(this.version)
    }
  }

  /**
   * Determine if a HED tag has a particular attribute in this schema.
   *
   * @param {string} tag The HED tag to check.
   * @param {string} tagAttribute The attribute to check for.
   * @returns {boolean} Whether this tag has this attribute.
   * @abstract
   */
  // eslint-disable-next-line no-unused-vars
  tagHasAttribute(tag, tagAttribute) {}
}

/**
 * An imported HED 2 schema.
 */
export class Hed2Schema extends Schema {
  /**
   * The description of tag attributes.
   * @type {SchemaAttributes}
   */
  attributes

  /**
   * Constructor.
   *
   * @param {object} xmlData The schema XML data.
   * @param {SchemaAttributes} attributes A description of tag attributes.
   */
  constructor(xmlData, attributes) {
    super(xmlData)

    this.attributes = attributes
  }

  /**
   * Determine if a HED tag has a particular attribute in this schema.
   *
   * @param {string} tag The HED tag to check.
   * @param {string} tagAttribute The attribute to check for.
   * @returns {boolean} Whether this tag has this attribute.
   */
  tagHasAttribute(tag, tagAttribute) {
    return this.attributes.tagHasAttribute(tag, tagAttribute)
  }
}

/**
 * An imported HED 3 schema.
 */
export class Hed3Schema extends Schema {
  /**
   * The collection of schema entries.
   * @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}
   */
  withStandard

  /**
   * Constructor.
   *
   * @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) {
    super(xmlData)

    if (!this.library) {
      this.withStandard = this.version
    } else {
      this.withStandard = xmlData.HED?.$?.withStandard
    }
    this.entries = entries
    this.mapping = mapping
  }

  /**
   * Determine if a HED tag has a particular attribute in this schema.
   *
   * @param {string} tag The HED tag to check.
   * @param {string} tagAttribute The attribute to check for.
   * @returns {boolean} Whether this tag has this attribute.
   */
  tagHasAttribute(tag, tagAttribute) {
    return this.entries.tagHasAttribute(tag, tagAttribute)
  }
}

/**
 * An imported lazy partnered HED 3 schema.
 */
export class PartneredSchema extends Hed3Schema {
  /**
   * The actual HED 3 schema underlying this partnered schema.
   * @type {Hed3Schema}
   */
  actualSchema

  /**
   * Constructor.
   *
   * @param {Hed3Schema} actualSchema The actual HED 3 schema underlying this partnered schema.
   */
  constructor(actualSchema) {
    super({}, actualSchema.entries, actualSchema.mapping)
    this.actualSchema = actualSchema
    this.withStandard = actualSchema.withStandard
    this.library = undefined
    this.generation = 3
  }
}

/**
 * The collection of active HED schemas.
 */
export class Schemas {
  /**
   * The imported HED schemas.
   *
   * The empty string key ("") corresponds to the schema with no nickname,
   * while other keys correspond to the respective nicknames.
   *
   * This field is null for syntax-only validation.
   *
   * @type {Map<string, Schema>|null}
   */
  schemas

  /**
   * Constructor.
   * @param {Schema|Map<string, Schema>|null} schemas The imported HED schemas.
   */
  constructor(schemas) {
    if (schemas === null || schemas instanceof Map) {
      this.schemas = schemas
    } else if (schemas instanceof Schema) {
      this.schemas = new Map([['', schemas]])
    } else {
      throw new Error('Invalid type passed to Schemas constructor')
    }
    if (this.schemas) {
      this._addNicknamesToSchemas()
    }
  }

  _addNicknamesToSchemas() {
    for (const [nickname, schema] of this.schemas) {
      schema.prefix = nickname
    }
  }

  /**
   * Return the schema with the given nickname.
   *
   * @param {string} schemaName A nickname in the schema set.
   * @returns {Schema} The schema object corresponding to that nickname.
   */
  getSchema(schemaName) {
    return this.schemas?.get(schemaName)
  }

  /**
   * The base schema, i.e. the schema with no nickname, if one is defined.
   *
   * @returns {Schema}
   */
  get baseSchema() {
    return this.getSchema('')
  }

  /**
   * The standard schema, i.e. primary schema implementing the HED standard, if one is defined.
   *
   * @returns {Schema}
   */
  get standardSchema() {
    if (this.schemas === null) {
      return undefined
    }
    for (const schema of this.schemas.values()) {
      if (schema.library === '') {
        return schema
      }
    }
    return undefined
  }

  /**
   * The library schemas, i.e. the schema with nicknames, if any are defined.
   *
   * @returns {Map<string, Schema>|null}
   */
  get librarySchemas() {
    if (this.schemas !== null) {
      const schemasCopy = new Map(this.schemas)
      schemasCopy.delete('')
      return schemasCopy
    } else {
      return null
    }
  }

  /**
   * The HED generation of this schema.
   *
   * If baseSchema is null, generation is set to 0.
   * @type {Number}
   */
  get generation() {
    if (this.schemas === null || this.schemas.size === 0) {
      return 0
    } else if (this.librarySchemas.size > 0) {
      return 3
    } else if (this.baseSchema) {
      return this.baseSchema.generation
    } else {
      return 0
    }
  }

  /**
   * Whether this schema collection is for syntactic validation only.
   * @returns {boolean}
   */
  get isSyntaxOnly() {
    return this.generation === 0
  }

  /**
   * Whether this schema collection comprises HED 3 schemas.
   * @returns {boolean}
   */
  get isHed3() {
    return this.generation === 3
  }
}

/**
 * A schema version specification.
 */
export class SchemaSpec {
  /**
   * The nickname of this schema.
   * @type {string}
   */
  nickname
  /**
   * The version of this schema.
   * @type {string}
   */
  version
  /**
   * The library name of this schema.
   * @type {string}
   */
  library
  /**
   * The local path for this schema.
   * @type {string}
   */
  localPath

  /**
   * Constructor.
   *
   * @param {string} nickname The nickname of this schema.
   * @param {string} version The version of this schema.
   * @param {string?} library The library name of this schema.
   * @param {string?} localPath The local path for this schema.
   */
  constructor(nickname, version, library = '', localPath = '') {
    this.nickname = nickname
    this.version = version
    this.library = library
    this.localPath = localPath
  }

  /**
   * Compute the name for the bundled copy of this schema.
   *
   * @returns {string}
   */
  get localName() {
    if (!this.library) {
      return 'HED' + this.version
    } else {
      return 'HED_' + this.library + '_' + this.version
    }
  }
}

/**
 * A specification mapping schema nicknames to SchemaSpec objects.
 */
export class SchemasSpec {
  /**
   * The specification mapping data.
   * @type {Map<string, SchemaSpec[]>}
   */
  data

  /**
   * Constructor.
   */
  constructor() {
    this.data = new Map()
  }

  /**
   * Iterator over the specifications.
   *
   * @yields {[string, SchemaSpec[]]}
   */
  *[Symbol.iterator]() {
    for (const [key, value] of this.data.entries()) {
      yield [key, value]
    }
  }

  /**
   * Add a schema to this specification.
   *
   * @param {SchemaSpec} schemaSpec A schema specification.
   * @returns {SchemasSpec| map} This object.
   */
  addSchemaSpec(schemaSpec) {
    if (this.data.has(schemaSpec.nickname)) {
      this.data.get(schemaSpec.nickname).push(schemaSpec)
    } else {
      this.data.set(schemaSpec.nickname, [schemaSpec])
    }
    return this
  }
}