From db85ad8a8fde5b04959abcbe329655f5eb3725f9 Mon Sep 17 00:00:00 2001 From: "Daniel W. Hieber" Date: Tue, 25 Aug 2020 20:03:32 -0500 Subject: [PATCH 1/4] add Word class --- src/models/Word.js | 29 +++++++++++++++++++ src/models/Word.test.js | 62 ++++++++++++++++++++++++++++++++++++++++ src/models/index.js | 1 + src/models/index.test.js | 2 ++ 4 files changed, 94 insertions(+) create mode 100644 src/models/Word.js create mode 100644 src/models/Word.test.js diff --git a/src/models/Word.js b/src/models/Word.js new file mode 100644 index 0000000..ae5be7e --- /dev/null +++ b/src/models/Word.js @@ -0,0 +1,29 @@ +import Model from '../core/Model.js'; +import MultiLangString from './MultiLangString.js'; +import Transcription from './Transcription.js'; + +/** + * A class representing a [DLx Word Token]{@link https://format.digitallinguistics.io/schemas/Word.html}. + * @extends Model + */ +class Word extends Model { + + constructor(data = {}) { + + super(); + + Model.defineModelProp(this, `literal`, MultiLangString); + Model.defineModelProp(this, `transcription`, Transcription); + Model.defineModelProp(this, `translation`, MultiLangString); + + Object.assign(this, data); + + // Required property + // TODO: replace with: this.transcription ??= new Transcription; + this.transcription = this.transcription ?? new Transcription; + + } + +} + +export default Word; diff --git a/src/models/Word.test.js b/src/models/Word.test.js new file mode 100644 index 0000000..96ef833 --- /dev/null +++ b/src/models/Word.test.js @@ -0,0 +1,62 @@ +import chai from 'chai'; +import MultiLangString from './MultiLangString.js'; +import Transcription from './Transcription.js'; +import Word from './Word.js'; + +const should = chai.should(); + +describe(`Word`, () => { + + const testData = { eng: `hello` }; + + it(`instantiates without data`, () => { + (() => new Word).should.not.throw(); + }); + + it(`transcription is a Transcription object`, () => { + + const word = new Word({ transcription: testData }); + + word.transcription.should.be.instanceOf(Transcription); + word.transcription.get(`eng`).should.equal(testData.eng); + + }); + + it(`transcription is an empty Transcription (Map) object if absent`, () => { + + const word = new Word; + + word.transcription.should.be.instanceOf(Transcription); + word.transcription.size.should.equal(0); + + }); + + it(`literal translation is a MultiLangString object`, () => { + + const word = new Word({ literal: testData }); + + word.literal.should.be.instanceOf(MultiLangString); + word.literal.get(`eng`).should.equal(testData.eng); + + }); + + it(`literal translation is undefined if absent`, () => { + const word = new Word; + should.not.exist(word.literal); + }); + + it(`free translation is a MultiLangString object`, () => { + + const word = new Word({ translation: testData }); + + word.translation.should.be.instanceOf(MultiLangString); + word.translation.get(`eng`).should.equal(testData.eng); + + }); + + it(`free translation is undefined if absent`, () => { + const word = new Word; + should.not.exist(word.translation); + }); + +}); diff --git a/src/models/index.js b/src/models/index.js index f6cbecc..c8b36f3 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -8,3 +8,4 @@ export { default as MultiLangString } from './MultiLangString.js'; export { default as Text } from './Text.js'; export { default as Transcription } from './Transcription.js'; export { default as Utterance } from './Utterance.js'; +export { default as Word } from './Word.js'; diff --git a/src/models/index.test.js b/src/models/index.test.js index 9229a97..9de4b51 100644 --- a/src/models/index.test.js +++ b/src/models/index.test.js @@ -10,6 +10,7 @@ import { Text, Transcription, Utterance, + Word, } from './index.js'; chai.should(); @@ -22,6 +23,7 @@ describe(`models`, () => { Text.name.should.equal(`Text`); Transcription.name.should.equal(`Transcription`); Utterance.name.should.equal(`Utterance`); + Word.name.should.equal(`Word`); }); }); From 09fe9354c89440bd2183da2f5c5203852c174361 Mon Sep 17 00:00:00 2001 From: "Daniel W. Hieber" Date: Wed, 26 Aug 2020 01:19:10 -0500 Subject: [PATCH 2/4] improve JSDoc comments --- src/core/Model.js | 47 +++++++++++++++---- src/core/Model.test.js | 23 +++++++-- src/models/Language.js | 88 ++++++----------------------------- src/models/MultiLangString.js | 8 ++-- src/models/Text.js | 18 ++----- src/models/Transcription.js | 8 ++-- src/models/Utterance.js | 24 +++------- src/models/Word.js | 8 ++++ 8 files changed, 94 insertions(+), 130 deletions(-) diff --git a/src/core/Model.js b/src/core/Model.js index b1c1833..6272292 100644 --- a/src/core/Model.js +++ b/src/core/Model.js @@ -1,3 +1,5 @@ +import Collection from './Collection.js'; + /** * A base Model class. Most other models are instances of this base Model. * @memberof core @@ -5,31 +7,31 @@ class Model { /** - * A utility function to define an array property on a class which ensures that each item in the array is an instance of the provided class/model whenever the property is set to a new array. This method does **not** prevent users from adding new items to the array which are not instances of the class. Use the Collection object to obtain this behavior. **NOTE:** _This method may only be called inside a class constructor. The private property (`#propName`) must also be defined on the class._ + * A utility function to define an array property on a class which ensures that the array cannot be overwritten. This method does **not** prevent users from adding new items to the array which are not instances of the class. Use the Collection object to obtain this behavior._ * @param {Object} object The object to define the property on * @param {String} propName The name of the property to define on the object * @return {Object} Returns the original object with the new property added */ static defineArrayProp(object, propName, ItemModel) { + const collection = new Collection(ItemModel); + Object.defineProperty(object, propName, { configurable: true, enumerable: true, get() { - return this[`#${propName}`]; + return collection; }, set(val) { - this[`#${propName}`] = val; - this[`#${propName}`].forEach((data, i, arr) => { - arr[i] = new ItemModel(data); // eslint-disable-line no-param-reassign - }); + collection.length = 0; + collection.push(...val.map(item => new ItemModel(item))); }, }); } /** - * A utility function to define a property on a class which must always be an instance of a certain class. **NOTE:** _This method may only be called inside a class constructor. The private property (`#propName`) must also be defined on the class._ + * A utility function to define a property on a class which must always be an instance of a certain class._ * @param {Object} object The object to define the property on * @param {String} propName The name of the property to define on the object * @param {Function} ItemModel The class to use for this property. Every time this property is set, this model will be instantiated. @@ -37,14 +39,41 @@ class Model { */ static defineModelProp(object, propName, ItemModel) { + let model; + + Object.defineProperty(object, propName, { + configurable: true, + enumerable: true, + get() { + return model; + }, + set(val) { + model = new ItemModel(val); + }, + }); + + } + + /** + * A utility function that defines a property on a class that validates its value when set. + * @param {Object} object [description] + * @param {String} propName [description] + * @param {Function} validate [description] + */ + static defineValidatedProp(object, propName, validate) { + + let value; + Object.defineProperty(object, propName, { configurable: true, enumerable: true, get() { - return this[`#${propName}`]; + return value; }, set(val) { - this[`#${propName}`] = new ItemModel(val); + if (val === undefined) return; + validate(val); + value = val; }, }); diff --git a/src/core/Model.test.js b/src/core/Model.test.js index 0fa41b3..892a2c5 100644 --- a/src/core/Model.test.js +++ b/src/core/Model.test.js @@ -14,13 +14,9 @@ describe(`Model`, () => { it(`defineArrayProp`, () => { class TestObject { - - #testProp; - constructor() { Model.defineArrayProp(this, `testProp`, TestModel); } - } const testObject = new TestObject; @@ -28,7 +24,6 @@ describe(`Model`, () => { testObject.testProp = arr; - testObject.testProp.should.equal(arr); testObject.testProp.forEach(item => item.should.be.instanceOf(TestModel)); }); @@ -53,4 +48,22 @@ describe(`Model`, () => { }); + it(`defineValidatedProp`, () => { + + const validate = val => { + if (val !== 1) throw new Error(`Bad value.`); + }; + + class TestObject { + constructor() { + Model.defineValidatedProp(this, `testProp`, validate); + } + } + + const testObject = new TestObject; + + (() => { testObject.testProp = 0; }).should.throw(); + + }); + }); diff --git a/src/models/Language.js b/src/models/Language.js index aa97199..a7c82d8 100644 --- a/src/models/Language.js +++ b/src/models/Language.js @@ -1,7 +1,3 @@ -/** - * @module models.Language - */ - import isAbbreviation from '../utilities/types/isAbbreviation.js'; import isGlottoCode from '../utilities/types/isGlottoCode.js'; import isISOCode from '../utilities/types/isISO.js'; @@ -11,6 +7,8 @@ import MultiLangString from './MultiLangString.js'; /** * Validates a language abbreviation. Throws a type error if the input is not a valid abbreviation. * @param {Any} input The input to validate + * @memberof models.Language + * @inner */ function validateAbbreviation(input) { if (!isAbbreviation(input)) { @@ -23,6 +21,8 @@ function validateAbbreviation(input) { /** * Validates a Glottolog language code. Throws a type error if the input is not a valid Glottolog code. * @param {Any} input The input to validate + * @memberof models.Language + * @inner */ function validateGlottoCode(input) { if (!isGlottoCode(input)) { @@ -35,6 +35,8 @@ function validateGlottoCode(input) { /** * Validates an ISO 639-3 language code. Throws a type error if the input is not a valid ISO 639-3 code. * @param {Any} input The input to validate + * @memberof models.Language + * @inner */ function validateISOCode(input) { if (!isISOCode(input)) { @@ -48,37 +50,13 @@ function validateISOCode(input) { * A class representing a language, formatted according to the [DLx Data Format for a language]{@link https://format.digitallinguistics.io/schemas/Language.html} * @memberof models * @extends core.Model + * @prop {String} abbreviation - The abbreviation for this language + * @prop {String} glottolog - The Glottocode for this language + * @prop {String} iso - The ISO 639-3 code for this language + * @prop {models.MultiLangString} name - The name of this language */ class Language extends Model { - /** - * The Abbreviation for this Language. - * @name models.Language#abbreviation - * @type {string} - */ - #abbreviation; - - /** - * The ISO 639-3 Code for this Language. - * @name models.Language#iso - * @type {string} - */ - #iso; - - /** - * The Glottolog Code for this Language. - * @name models.Language#glottolog - * @type {string} - */ - #glottolog; - - /** - * The name of this language, as a [MultiLangString]{@link models.MultiLangString} - * @name models.Language#name - * @type {Map} - */ - #name; - /** * Create a new Language * @param {Object} [data={}] The data to use for this Language object. Data should be formatted according to the [DLx Data Format's guidelines for Language data]{@link https://format.digitallinguistics.io/schemas/Language.html}. @@ -94,49 +72,9 @@ class Language extends Model { // Property Definitions Model.defineModelProp(this, `name`, MultiLangString); - - Object.defineProperties(this, { - - abbreviation: { - configurable: true, - enumerable: true, - get() { - return this.#abbreviation; - }, - set(val) { - if (val === undefined) return; - validateAbbreviation(val); - this.#abbreviation = val; - }, - }, - - glottolog: { - configurable: true, - enumerable: true, - get() { - return this.#glottolog; - }, - set(val) { - if (val === undefined) return; - validateGlottoCode(val); - this.#glottolog = val; - }, - }, - - iso: { - configurable: true, - enumerable: true, - get() { - return this.#iso; - }, - set(val) { - if (val === undefined) return; - validateISOCode(val); - this.#iso = val; - }, - }, - - }); + Model.defineValidatedProp(this, `abbreviation`, validateAbbreviation); + Model.defineValidatedProp(this, `glottolog`, validateGlottoCode); + Model.defineValidatedProp(this, `iso`, validateISOCode); // Initialization diff --git a/src/models/MultiLangString.js b/src/models/MultiLangString.js index 1caf32d..bc43451 100644 --- a/src/models/MultiLangString.js +++ b/src/models/MultiLangString.js @@ -1,12 +1,10 @@ -/** - * @module models.MultiLangString - */ - import isLanguageTag from '../utilities/types/isLanguageTag.js'; /** * Validates a language tag. Throws a type error if the input is not a valid IETF language tag. * @param {Any} input The input to validate + * @memberof models.MultiLangString + * @inner */ function validateLanguageTag(input) { if (!isLanguageTag(input)) { @@ -19,6 +17,8 @@ function validateLanguageTag(input) { /** * Validates a String for MultiLangStrings. Throws a type error if the input is not a String. * @param {Any} input The input to validate + * @memberof models.MultiLangString + * @inner */ function validateString(input) { if (typeof input !== `string`) { diff --git a/src/models/Text.js b/src/models/Text.js index 1da9e59..913138a 100644 --- a/src/models/Text.js +++ b/src/models/Text.js @@ -1,4 +1,3 @@ -import Collection from '../core/Collection.js'; import Model from '../core/Model.js'; import MultiLangString from './MultiLangString.js'; import Utterance from './Utterance.js'; @@ -7,21 +6,11 @@ import Utterance from './Utterance.js'; * A class representing a linguistic text, formatted according to the [DLx Data Format for a language]{@link https://format.digitallinguistics.io/schemas/Text.html} * @memberof models * @extends core.Model + * @prop {models.MultiLangString} title - The title of this text + * @prop {core.Collection} utterances - An array of utterances in this text */ class Text extends Model { - /** - * The title of this text, as a MultiLangString - * @type {models.MultiLangString} - */ - #title; - - /** - * The utterances in this text, each as an Utterance object. - * @type {core.Collection} - */ - #utterances; - /** * Create a new Text * @param {Object} [data={}] The data to use for this Text. Data should be formatted according to the [DLx Data Format's guidelines for Text data]{@link https://format.digitallinguistics.io/schemas/Text.html}. @@ -35,8 +24,7 @@ class Text extends Model { Object.assign(this, data); - this.title = this.title ?? new MultiLangString; - this.utterances = this.utterances ?? new Collection(Utterance); + this.title = this.title ?? new MultiLangString; } diff --git a/src/models/Transcription.js b/src/models/Transcription.js index 1e681f5..effd91f 100644 --- a/src/models/Transcription.js +++ b/src/models/Transcription.js @@ -1,12 +1,10 @@ -/** - * @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 + * @memberof models.Transcription + * @inner */ function validateAbbreviation(input) { if (!isAbbreviation(input)) { @@ -19,6 +17,8 @@ function validateAbbreviation(input) { /** * Validates a String for Transcription values. Throws a type error if the input is not a String. * @param {Any} input The input to validate + * @memberof models.Transcription + * @inner */ function validateString(input) { if (typeof input !== `string`) { diff --git a/src/models/Utterance.js b/src/models/Utterance.js index 62c266c..fbbf57f 100644 --- a/src/models/Utterance.js +++ b/src/models/Utterance.js @@ -1,32 +1,19 @@ import Model from '../core/Model.js'; import MultiLangString from './MultiLangString.js'; import Transcription from './Transcription.js'; +import Word from './Word.js'; /** * A class representing an utterance. * @memberof models * @extends core.Model + * @prop {models.Transcription} transcript - The transcript for this utterance + * @prop {models.Transcription} transcription - The transcription of this utterance + * @prop {models.MultiLangString} translation - The translation of this utterance + * @prop {core.Collection} words - An array of words in this utterance */ class Utterance extends Model { - /** - * The transcript of this utterance, as a Transcription object - * @type {models.Transcription} - */ - #transcript; - - /** - * The transcription of this utterance, as a Transcription object - * @type {models.Transcription} - */ - #transcription; - - /** - * The translation of this utterance, as a MultiLangString object - * @type {models.MultiLangString} - */ - #translation; - /** * Create a new Utterance * @param {Object} [data={}] The data to use for this Utterance @@ -38,6 +25,7 @@ class Utterance extends Model { Model.defineModelProp(this, `transcript`, Transcription); Model.defineModelProp(this, `transcription`, Transcription); Model.defineModelProp(this, `translation`, MultiLangString); + Model.defineArrayProp(this, `words`, Word); Object.assign(this, data); diff --git a/src/models/Word.js b/src/models/Word.js index ae5be7e..88b5fab 100644 --- a/src/models/Word.js +++ b/src/models/Word.js @@ -4,10 +4,18 @@ import Transcription from './Transcription.js'; /** * A class representing a [DLx Word Token]{@link https://format.digitallinguistics.io/schemas/Word.html}. + * @memberof models * @extends Model + * @prop {models.MultiLangString} literal - The literal translation of this word token + * @prop {models.Transcription} transcription - The transcription of this word token + * @prop {models.MultiLangString} translation - The translation of this word token */ class Word extends Model { + /** + * Create a new word token + * @param {Object} [data={}] The data for this word token + */ constructor(data = {}) { super(); From 97898d4b3f6877108870b9f5d66b9be53e78a1f2 Mon Sep 17 00:00:00 2001 From: "Daniel W. Hieber" Date: Wed, 26 Aug 2020 01:21:55 -0500 Subject: [PATCH 3/4] Word.analysis --- src/models/Word.js | 2 ++ src/models/Word.test.js | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/models/Word.js b/src/models/Word.js index 88b5fab..eeb0bb1 100644 --- a/src/models/Word.js +++ b/src/models/Word.js @@ -6,6 +6,7 @@ import Transcription from './Transcription.js'; * A class representing a [DLx Word Token]{@link https://format.digitallinguistics.io/schemas/Word.html}. * @memberof models * @extends Model + * @prop {models.Transcription} analysis - The morphological analysis of this word, with morpheme breaks * @prop {models.MultiLangString} literal - The literal translation of this word token * @prop {models.Transcription} transcription - The transcription of this word token * @prop {models.MultiLangString} translation - The translation of this word token @@ -20,6 +21,7 @@ class Word extends Model { super(); + Model.defineModelProp(this, `analysis`, Transcription); Model.defineModelProp(this, `literal`, MultiLangString); Model.defineModelProp(this, `transcription`, Transcription); Model.defineModelProp(this, `translation`, MultiLangString); diff --git a/src/models/Word.test.js b/src/models/Word.test.js index 96ef833..84593d8 100644 --- a/src/models/Word.test.js +++ b/src/models/Word.test.js @@ -59,4 +59,18 @@ describe(`Word`, () => { should.not.exist(word.translation); }); + it(`analysis is a Transcription object`, () => { + + const word = new Word({ analysis: testData }); + + word.analysis.should.be.instanceOf(Transcription); + word.analysis.get(`eng`).should.equal(testData.eng); + + }); + + it(`analysis is undefined if absent`, () => { + const word = new Word; + should.not.exist(word.analysis); + }); + }); From 856a15a199fdd26af27b08bdf8b202e11fbbb960 Mon Sep 17 00:00:00 2001 From: "Daniel W. Hieber" Date: Wed, 26 Aug 2020 11:39:23 -0500 Subject: [PATCH 4/4] Word.gloss --- src/models/Word.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/Word.js b/src/models/Word.js index eeb0bb1..48c3507 100644 --- a/src/models/Word.js +++ b/src/models/Word.js @@ -7,6 +7,7 @@ import Transcription from './Transcription.js'; * @memberof models * @extends Model * @prop {models.Transcription} analysis - The morphological analysis of this word, with morpheme breaks + * @prop {models.MultiLangString} gloss - The glossed analysis of this word, with morpheme breaks * @prop {models.MultiLangString} literal - The literal translation of this word token * @prop {models.Transcription} transcription - The transcription of this word token * @prop {models.MultiLangString} translation - The translation of this word token @@ -22,6 +23,7 @@ class Word extends Model { super(); Model.defineModelProp(this, `analysis`, Transcription); + Model.defineModelProp(this, `gloss`, MultiLangString); Model.defineModelProp(this, `literal`, MultiLangString); Model.defineModelProp(this, `transcription`, Transcription); Model.defineModelProp(this, `translation`, MultiLangString);