diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f99bbad..f60e14a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: - name: install Node.js uses: actions/setup-node@master with: - node-version: 13.x + node-version: 14.x - name: install dependencies run: npm ci diff --git a/src/models/Transcription.js b/src/models/Transcription.js new file mode 100644 index 0000000..1e681f5 --- /dev/null +++ b/src/models/Transcription.js @@ -0,0 +1,80 @@ +/** + * @module models.Transcription + */ + +import isAbbreviation from '../utilities/types/isAbbreviation.js'; + +/** + * Validates an abbreviation. Throws a type error if the input is not a valid abbreviation. + * @param {Any} input The input to validate + */ +function validateAbbreviation(input) { + if (!isAbbreviation(input)) { + const e = new TypeError(`Each orthography key must be a valid abbreviation.`); + e.name = `TranscriptionOrthoError`; + throw e; + } +} + +/** + * Validates a String for Transcription values. Throws a type error if the input is not a String. + * @param {Any} input The input to validate + */ +function validateString(input) { + if (typeof input !== `string`) { + const e = new TypeError(`Each piece of data in a Transcription must be a String of text in a particular orthography.`); + e.name = `TranscriptionStringError`; + throw e; + } +} + +/** + * A class representing a Transcription, as a JavaScript Map Object. See the [DLx Data Format]{@link https://format.digitallinguistics.io/schemas/Transcription.html} for information about formatting Transcription objects. + * @memberof models + * @extends Map + * + * @example + * const transcription = new Transcription({ + * latin: `hello`, + * IPA: `hɛˈloʊ`, + * }); + * + * console.log(transcription.get(`ipa`)); // hɛˈloʊ + */ +class Transcription extends Map { + + /** + * Create a new Transcription + * @param {Map|Object} [data={}] The data to use for this Transcription, as either a Map or an Object. + */ + constructor(data = {}) { + + if (typeof data !== `object`) { + const e = new TypeError(`The data passed to the Transcription class must be a Map or Object.`); + e.name = `TranscriptionDataError`; + throw e; + } + + // eslint-disable-next-line no-param-reassign + data = data instanceof Map ? Object.fromEntries(data) : data; + + Object.keys(data).forEach(validateAbbreviation); + Object.values(data).forEach(validateString); + + super(Object.entries(data)); + + } + + set(key, val) { + validateAbbreviation(key); + validateString(val); + return super.set(key, val); + } + + toJSON() { + return Object.fromEntries(this); + } + +} + +export default Transcription; diff --git a/src/models/Transcription.test.js b/src/models/Transcription.test.js new file mode 100644 index 0000000..18b273e --- /dev/null +++ b/src/models/Transcription.test.js @@ -0,0 +1,78 @@ +/** + * @module models.MultiLangString + */ + +import Transcription from './Transcription.js'; + +describe(`Transcription`, () => { + + const testData = { + IPA: `hɛˈloʊ`, + Latin: `hello`, + }; + + it(`is an empty Map when no data is provided`, () => { + + const txn = new Transcription; + + txn.should.be.instanceOf(Map); + txn.size.should.equal(0); + + }); + + it(`maps orthographies to transcriptions`, () => { + + const txn = new Transcription(testData); + + txn.get(`IPA`).should.equal(`hɛˈloʊ`); + txn.get(`Latin`).should.equal(`hello`); + + }); + + it(`only allows abbreviations as keys`, () => { + + () => new Transcription({ 'bad key': 'hello' }) + .should.throw() + .with.property(`name`, `TranscriptionOrthoError`); + + }); + + it(`only allows strings as values`, () => { + + () => new Transcription({ eng: 0 }) + .should.throw() + .with.property(`name`, `TranscriptionStringError`); + + }); + + it(`validates new keys`, () => { + + const txn = new Transcription; + + () => txn.set(`bad key`, `hello`) + .should.throw() + .with.property(`name`, `TranscriptionOrthoError`); + + }); + + it(`validates new values`, () => { + + const txn = new Transcription; + + () => txn.set(`eng`, undefined) + .should.throw() + .with.property(`name`, `TranscriptionStringError`); + + }); + + it(`stringifies as an Object`, () => { + + const txn = new Transcription(testData); + const pojo = JSON.parse(JSON.stringify(txn)); + + pojo.IPA.should.equal(testData.IPA); + pojo.Latin.should.equal(testData.Latin); + + }); + +}); diff --git a/src/models/Utterance.js b/src/models/Utterance.js new file mode 100644 index 0000000..d1b1da1 --- /dev/null +++ b/src/models/Utterance.js @@ -0,0 +1,53 @@ +import Model from '../core/Model.js'; +import MultiLangString from './MultiLangString.js'; +import Transcription from './Transcription.js'; + +/** + * A class representing an utterance. + * @memberof models + * @extends core.Model + */ +class Utterance extends Model { + + /** + * The transcript of this utterance, as a Transcription object + * @type {Transcription} + */ + #transcript; + + /** + * The transcription of this utterance, as a Transcription object + * @type {Transcription} + */ + #transcription; + + /** + * The translation of this utterance, as a MultiLangString object + * @type {MultiLangString} + */ + #translation; + + /** + * Create a new Utterance + * @param {Object} [data={}] The data to use for this Utterance + */ + constructor(data = {}) { + + super(); + + Model.defineModelProp(this, `transcript`, Transcription); + Model.defineModelProp(this, `transcription`, Transcription); + Model.defineModelProp(this, `translation`, MultiLangString); + + Object.assign(this, data); + + // Required properties + // TODO: replace with: this.transcription ??= new Transcription; + this.transcription = this.transcription ?? new Transcription; + this.translation = this.translation ?? new MultiLangString; + + } + +} + +export default Utterance; diff --git a/src/models/Utterance.test.js b/src/models/Utterance.test.js new file mode 100644 index 0000000..8647994 --- /dev/null +++ b/src/models/Utterance.test.js @@ -0,0 +1,63 @@ +import chai from 'chai'; +import MultiLangString from './MultiLangString.js'; +import Transcription from './Transcription.js'; +import Utterance from './Utterance.js'; + +const should = chai.should(); + +describe(`Utterance`, () => { + + const testData = { eng: 'Hello world!' }; + + it(`instantiates without data`, () => { + (() => new Utterance).should.not.throw(); + }); + + it(`transcript is a Transcription object`, () => { + + const utterance = new Utterance({ transcript: testData }); + + utterance.transcript.should.be.instanceOf(Transcription); + utterance.transcript.get(`eng`).should.equal(testData.eng); + + }); + + it(`transcript is undefined if absent`, () => { + const utterance = new Utterance; + should.not.exist(utterance.transcript); + }); + + it(`transcription is a Transcription object`, () => { + const utterance = new Utterance({ transcription: testData }); + utterance.transcription.should.be.instanceOf(Transcription); + utterance.transcription.get(`eng`).should.equal(testData.eng); + }); + + it(`transcription is an empty Transcription (Map) object if absent`, () => { + + const utterance = new Utterance; + + utterance.transcription.should.be.instanceOf(Transcription); + utterance.transcription.size.should.equal(0); + + }); + + it(`translation is a MultiLangString object`, () => { + + const utterance = new Utterance({ translation: testData }); + + utterance.translation.should.be.instanceOf(MultiLangString); + utterance.translation.get(`eng`).should.equal(testData.eng); + + }); + + it(`translation is an empty MultiLangString if absent`, () => { + + const utterance = new Utterance; + + utterance.translation.should.be.instanceOf(MultiLangString); + utterance.translation.size.should.equal(0); + + }); + +}); diff --git a/src/models/index.js b/src/models/index.js index 08d90fb..d0325d1 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -5,3 +5,5 @@ export { default as Language } from './Language.js'; export { default as MultiLangString } from './MultiLangString.js'; +export { default as Transcription } from './Transcription.js'; +export { default as Utterance } from './Utterance.js'; diff --git a/src/models/index.test.js b/src/models/index.test.js index 4b753f0..a62106b 100644 --- a/src/models/index.test.js +++ b/src/models/index.test.js @@ -2,8 +2,14 @@ max-nested-callbacks, */ -import chai from 'chai'; -import { Language, MultiLangString } from './index.js'; +import chai from 'chai'; + +import { + Language, + MultiLangString, + Transcription, + Utterance, +} from './index.js'; chai.should(); @@ -12,6 +18,8 @@ describe(`models`, () => { it(`has the expected exports`, () => { MultiLangString.name.should.equal(`MultiLangString`); Language.name.should.equal(`Language`); + Transcription.name.should.equal(`Transcription`); + Utterance.name.should.equal(`Utterance`); }); });