Home Manual Reference Source Test

test/nomenclatural-codes.js

/*
 * Test nomenclatural code lookups and fallback behavior. While most of the
 * nomenclatural code logic is in TaxonNameWrapper, we need to provide fallback
 * nomenclatural codes in TaxonConceptWrapper, TaxonomicUnitWrapper and
 * PhylorefWrapper as well. This test file makes sure that this functionality
 * works correctly at all of these levels.
 */

const fs = require('fs');
const path = require('path');

const { cloneDeep } = require('lodash');

const chai = require('chai');
const phyx = require('../src');
const owlterms = require('../src/utils/owlterms');

// Use Chai's expect API.
const expect = chai.expect;

/* The list of expected fields in nomenclatural details. */
const EXPECTED_NOMEN_DETAIL_FIELDS = ['iri', 'shortName', 'label', 'title'];

/* Some example taxon names to use. */
const ranaLuteiventris = {
  '@type': [
    phyx.TaxonomicUnitWrapper.TYPE_TAXON_CONCEPT,
    phyx.TaxonomicUnitWrapper.TYPE_SPECIMEN,
  ],
  hasName: {
    label: 'Rana luteiventris',
  },
  occurrenceID: 'MVZ 225749',
};

/*
 * The nomenclatural codes are set up in TaxonNameWrapper, so that's where
 * most of the nomenclatural code behavior code exists.
 */
describe('TaxonNameWrapper', function () {
  describe('#getNomenclaturalCodes', function () {
    it('should provide a non-empty list with the expected keys', function () {
      const nomenCodes = phyx.TaxonNameWrapper.getNomenclaturalCodes();

      expect(nomenCodes)
        .to.be.an('array')
        .that.is.not.empty;

      nomenCodes.forEach((nomenCode) => {
        expect(nomenCode).to.have.all.keys(EXPECTED_NOMEN_DETAIL_FIELDS);
      });
    });
  });

  describe('#getNomenCodeDetails', function () {
    it('should provide details for some built-in codes', function () {
      const codesToTest = {
        'Code not known': owlterms.UNKNOWN_CODE,
        ICZN: owlterms.ICZN_CODE,
        ICN: owlterms.ICN_CODE,
        ICNP: owlterms.ICNP_CODE,
        ICTV: owlterms.ICTV_CODE,
        ICNCP: owlterms.ICNCP_CODE,
      };

      Object.keys(codesToTest).forEach((code) => {
        const uri = codesToTest[code];
        const details = phyx.TaxonNameWrapper.getNomenCodeDetails(uri);
        expect(details).to.have.all.keys(EXPECTED_NOMEN_DETAIL_FIELDS);
        expect(details.shortName).to.equal(code);
      });
    });
  });

  describe('#nomenclaturalCodeDetails', function () {
    it('should provide nomenclatural code details for an example taxon name', function () {
      const wrapper = new phyx.TaxonNameWrapper(ranaLuteiventris.hasName);
      expect(wrapper.nomenclaturalCode).to.equal(owlterms.UNKNOWN_CODE);
      expect(wrapper.nomenclaturalCodeDetails.shortName).to.equal('Code not known');

      const wrapperWithDefault = new phyx.TaxonNameWrapper(
        ranaLuteiventris.hasName,
        owlterms.ICZN_CODE
      );
      expect(wrapperWithDefault.nomenclaturalCode).to.equal(owlterms.ICZN_CODE);
      expect(wrapperWithDefault.nomenclaturalCodeDetails.shortName).to.equal('ICZN');

      const nameWithNomenCode = cloneDeep(ranaLuteiventris.hasName);
      nameWithNomenCode.nomenclaturalCode = owlterms.ICZN_CODE;
      const wrapperWithExplicit = new phyx.TaxonNameWrapper(nameWithNomenCode, owlterms.ICN_CODE);
      expect(wrapperWithExplicit.nomenclaturalCode).to.equal(owlterms.ICZN_CODE);
      expect(wrapperWithExplicit.nomenclaturalCodeDetails.shortName).to.equal('ICZN');
    });
  });
});

/*
 * Make sure we can set a default nomenclatural code in TaxonConceptWrapper.
 */
describe('TaxonConceptWrapper', function () {
  describe('#nomenCode', function () {
    const wrapper = new phyx.TaxonConceptWrapper(ranaLuteiventris);

    it('should return UNKNOWN_CODE if one is not set', function () {
      expect(wrapper.nomenCode).to.equal(owlterms.UNKNOWN_CODE);
    });

    it('should return the default nomenclatural code if one is provided', function () {
      const wrapperWithDefault = new phyx.TaxonConceptWrapper(ranaLuteiventris, owlterms.ICZN_CODE);
      expect(wrapperWithDefault.nomenCode).to.equal(owlterms.ICZN_CODE);
      expect(wrapperWithDefault.nomenCodeDetails.shortName).to.equal('ICZN');
    });
  });
});

/*
 * There are two ways in which nomenclatural codes can be set at the Phyx level:
 *  (1) If there is a `defaultNomenclaturalCodeIRI` field at the Phyx level,
 *      that will be used to provide a nomenclatural code for all specifiers
 *      without a nomenclatural code as well as for all the phylogeny nodes.
 *  (2) If no `defaultNomenclaturalCodeIRI` is provided, but all the specifiers
 *      on all the phylorefs in the file have the same nomenclatural code, then
 *      that code will be used on all the phylogeny nodes.
 */
describe('PhyxWrapper', function () {
  it('should use the defaultNomenclaturalCodeIRI for phylogeny nodes', function () {
    // The examples/correct/alligatoridae_default_nomen_code.json file has
    // a `defaultNomenclaturalCodeIRI`.
    const json = JSON.parse(fs.readFileSync(
      path.resolve(__dirname, './examples/correct/alligatoridae_default_nomen_code.json')
    ));

    // Make sure this is the right example file.
    expect(json, 'Expected alligatoridae_default_nomen_code.json to include a defaultNomenclaturalCodeIRI value.')
      .to.include.key('defaultNomenclaturalCodeIRI');
    const defaultNomenclaturalCodeIRI = json.defaultNomenclaturalCodeIRI;

    const jsonld = new phyx.PhyxWrapper(json).asJSONLD();

    // Step 1. Check the phyloreferences. Neither specifier has a nomenclatural code,
    // but they should pick up the default nomenclatural code for the Phyx file.
    expect(jsonld.phylorefs).to.be.an('array').of.length(1);
    const phyloref1 = jsonld.phylorefs[0];
    expect(phyloref1).to.be.an('object').and.to.include.key('equivalentClass');

    const equivalentClass = phyloref1.equivalentClass;
    const specifierExprs = equivalentClass.someValuesFrom.intersectionOf;
    expect(specifierExprs).to.be.an('array').with.length(2);

    specifierExprs.forEach((specifierExpr) => {
      const nameExprs = specifierExpr.someValuesFrom.someValuesFrom.intersectionOf;

      expect(nameExprs).to.be.an('array').with.length(2).and.to.deep.include(
        {
          '@type': 'owl:Restriction',
          onProperty: 'http://rs.tdwg.org/ontology/voc/TaxonName#nomenclaturalCode',
          hasValue: {
            '@id': defaultNomenclaturalCodeIRI,
          },
        }
      );
    });

    // Step 2. Check the phylogenies.
    expect(jsonld).to.include.key('phylogenies');
    expect(jsonld.phylogenies).to.be.an('array').with.length(1);

    const phylogeny1 = jsonld.phylogenies[0];
    expect(phylogeny1).to.include.key('nodes');

    phylogeny1.nodes.forEach((node) => {
      const nodeType = node['rdf:type'];

      // There should be at least one type definition: obo:CDAO_0000140.
      expect(nodeType[0]).to.deep.equal({
        '@id': 'obo:CDAO_0000140',
      });

      // The second type definition -- if it exists -- must be a name entry,
      // which should include the appropriate nomenclatural code.
      if (nodeType.length > 1) {
        const nameEntry = nodeType[1];
        expect(nameEntry.someValuesFrom.someValuesFrom.intersectionOf).to.deep.include(
          {
            '@type': 'owl:Restriction',
            onProperty: 'http://rs.tdwg.org/ontology/voc/TaxonName#nomenclaturalCode',
            hasValue: {
              '@id': defaultNomenclaturalCodeIRI,
            },
          }
        );
      }
    });
  });

  it('should use the inferred nomenclatural code for phylogeny nodes', function () {
    // The examples/correct/alligatoridae_inferred_nomen_code.json file does not have
    // a `defaultNomenclaturalCodeIRI`, but the nomenclatural code can be inferred from
    // its specifiers.
    const json = JSON.parse(fs.readFileSync(
      path.resolve(__dirname, './examples/correct/alligatoridae_inferred_nomen_code.json')
    ));

    // Make sure this is the right example file.
    expect(json, 'Expected alligatoridae_inferred_nomen_code.json to not include a defaultNomenclaturalCodeIRI value.')
      .to.not.include.key('defaultNomenclaturalCodeIRI');

    const wrapped = new phyx.PhyxWrapper(json);
    const inferredNomenCode = wrapped.defaultNomenCode;
    expect(inferredNomenCode).to.equal(owlterms.ICZN_CODE);

    const jsonld = wrapped.asJSONLD();

    // Step 1. Check the phyloreferences. Since only *Caiman crocodilus* has a
    // nomenclatural code set, we should make sure that the other specifier
    // picks up the inferred nomenclatural code of the entire file.
    expect(jsonld.phylorefs).to.be.an('array').of.length(1);
    const phyloref1 = jsonld.phylorefs[0];
    expect(phyloref1).to.be.an('object').and.to.include.key('equivalentClass');

    const equivalentClass = phyloref1.equivalentClass;
    const specifierExprs = equivalentClass.someValuesFrom.intersectionOf;
    expect(specifierExprs).to.be.an('array').with.length(2);

    specifierExprs.forEach((specifierExpr) => {
      const nameExprs = specifierExpr.someValuesFrom.someValuesFrom.intersectionOf;

      expect(nameExprs).to.be.an('array').with.length(2).and.to.deep.include(
        {
          '@type': 'owl:Restriction',
          onProperty: 'http://rs.tdwg.org/ontology/voc/TaxonName#nomenclaturalCode',
          hasValue: {
            '@id': inferredNomenCode,
          },
        }
      );
    });

    // Step 2. Check the phylogenies.
    expect(jsonld).to.include.key('phylogenies');
    expect(jsonld.phylogenies).to.be.an('array').with.length(1);

    const phylogeny1 = jsonld.phylogenies[0];
    expect(phylogeny1).to.include.key('nodes');

    phylogeny1.nodes.forEach((node) => {
      const nodeType = node['rdf:type'];

      // There should be at least one type definition: obo:CDAO_0000140.
      expect(nodeType[0]).to.deep.equal({
        '@id': 'obo:CDAO_0000140',
      });

      // The second type definition -- if it exists -- must be a name entry,
      // which should include the appropriate nomenclatural code.
      if (nodeType.length > 1) {
        const nameEntry = nodeType[1];
        expect(nameEntry.someValuesFrom.someValuesFrom.intersectionOf).to.deep.include(
          {
            '@type': 'owl:Restriction',
            onProperty: 'http://rs.tdwg.org/ontology/voc/TaxonName#nomenclaturalCode',
            hasValue: {
              '@id': inferredNomenCode,
            },
          }
        );
      }
    });
  });
});