lib/export.js
function namespaceToSchema(ns, dataElementsSpecs, baseSchemaURL, baseTypeURL) {
  const dataElements = dataElementsSpecs.byNamespace(ns.namespace);
  const schemaId = `${baseSchemaURL}/${namespaceToURLPathSegment(ns.namespace)}`;
  let schema = {
    $schema: 'http://json-schema.org/draft-04/schema#',
    id: schemaId,
    title: "TODO: Figure out what the title should be.",
    definitions: {}
  };
  const entryRef = makeRef(new Identifier('shr.base', 'Entry'), ns, baseSchemaURL);
  if (ns.description) {
    schema.description = ns.description;
  }
  const nonEntryEntryTypeField = {
    $ref: makeRef(new Identifier('shr.base', 'EntryType'), ns, baseSchemaURL)
  };

  const defs = dataElements.sort(function(l,r) {return l.identifier.name.localeCompare(r.identifier.name);});
  const entryRefs = [];
  for (const def of defs) {
    const lastLogger = logger;
    logger = logger.child({ shrId: def.identifier.fqn});
    try {
      logger.debug('Exporting element');
      let schemaDef = {
        type: 'object',
        properties: {}
      };
      let wholeDef = schemaDef;
      const tbdParentDescriptions = [];
      let requiredProperties = [];
      let needsEntryType = false;
      if (def.isEntry || def.basedOn.length) {
        wholeDef = { allOf: [] };
        let hasEntryParent = false;
        for (const supertypeId of def.basedOn) {
          if (supertypeId instanceof TBD) {
            if (supertypeId.text) {
              tbdParentDescriptions.push(supertypeId.text);
            } else {
              tbdParentDescriptions.push('TBD');
            }
          } else {
            const parent = dataElementsSpecs.findByIdentifier(supertypeId);
            if (!parent) {
              logger.error('Could not find definition for %s which is a supertype of %s', supertypeId, def);
            } else {
              hasEntryParent = hasEntryParent || parent.isEntry;
            }
            wholeDef.allOf.push({ $ref: makeRef(supertypeId, ns, baseSchemaURL)});
          }
        }
        if (def.isEntry && (!hasEntryParent)) {
          wholeDef.allOf.splice(0, 0, { $ref: entryRef });
        }
        wholeDef.allOf.push(schemaDef);
      } else {
        needsEntryType = true;
      }

      const tbdFieldDescriptions = [];
      if (def.value) {
        if (def.value.inheritance !== INHERITED) {
          let { value, required, tbd } = convertDefinition(def.value, dataElementsSpecs, ns, baseSchemaURL, baseTypeURL);
          if (required) {
            requiredProperties.push('Value');
          }
          schemaDef.properties.Value = value;
          if (tbd) {
            schemaDef.properties.Value.description = def.value.text ? ('TBD: ' + def.value.text) : tbdValueToString(def.value);
          }
        }
      }
      if (def.fields.length) {
        const fieldNameMap = {};
        const clashingNames = {};
        for (const field of def.fields) {
          if (!(field instanceof TBD)) {
            if (!isValidField(field)) {
              continue;
            } else if (field.inheritance === INHERITED) {
              if (fieldNameMap[field.identifier.name]) {
                logger.error(`ERROR: clashing property names: %s and %s ERROR_CODE: 12038`, fieldNameMap[field.identifier.name].fqn, field.identifier.fqn);
                clashingNames[field.identifier.name] = true;
              } else {
                fieldNameMap[field.identifier.name] = field.identifier;
              }
              continue;
            }

            if (fieldNameMap[field.identifier.name]) {
              logger.error(`ERROR: clashing property names: %s and %s ERROR_CODE: 12038`, fieldNameMap[field.identifier.name].fqn, field.identifier.fqn);
              clashingNames[field.identifier.name] = true;
              continue;
            } else {
              fieldNameMap[field.identifier.name] = field.identifier;
            }
          }
          const card = field.effectiveCard;
          if (card && card.isZeroedOut) {
            continue;
          }
          let {value, required, tbd} = convertDefinition(field, dataElementsSpecs, ns, baseSchemaURL, baseTypeURL);
          if (tbd) {
            tbdFieldDescriptions.push(tbdValueToString(field));
            continue;
          }

          if (field.identifier.fqn === 'shr.base.EntryType') {
            needsEntryType = false;
          }

          schemaDef.properties[field.identifier.name] = value;
          if (required) {
            requiredProperties.push(field.identifier.name);
          }
        }

        for (const clashingName in clashingNames) {
          delete schemaDef.properties[clashingName];
        }
        requiredProperties = requiredProperties.filter(propName => !(propName in clashingNames));
      } else if (!def.value) {
        schemaDef.type = 'object';
        schemaDef.description = 'Empty DataElement?';
      }
      let descriptionList = [];
      if (def.description) {
        descriptionList.push(def.description);
      }
      if (def.concepts.length) {
        wholeDef.concepts = def.concepts.map((concept) => makeConceptEntry(concept));
      }
      if (tbdParentDescriptions.length) {
        tbdParentDescriptions[0] = 'TBD Parents: ' + tbdParentDescriptions[0];
        descriptionList = descriptionList.concat(tbdParentDescriptions);
      }
      if (tbdFieldDescriptions.length) {
        tbdFieldDescriptions[0] = 'TBD Fields: ' + tbdFieldDescriptions[0];
        descriptionList = descriptionList.concat(tbdFieldDescriptions);
      }
      if (descriptionList.length) {
        wholeDef.description = descriptionList.join('\n');
      }
      if (needsEntryType) {
        schemaDef.properties['EntryType'] = nonEntryEntryTypeField;
        if (def.identifier.fqn !== 'shr.base.EntryType') {
          requiredProperties.push('EntryType');
        }
      }

      if (requiredProperties.length) {
        schemaDef.required = requiredProperties;
      }

      schema.definitions[def.identifier.name] = wholeDef;
      if (def.isEntry && (!def.isAbstract)) {
        entryRefs.push({ $ref: makeRef(def.identifier, ns, baseSchemaURL)});
      }
    } finally {
      logger = lastLogger;
    }
  }

  if (entryRefs.length) {
    schema.type = 'object';
    schema.anyOf = entryRefs;
  }
  return { schemaId, schema };
}
function namespaceToSchema(ns, dataElementsSpecs, baseSchemaURL, baseTypeURL) {
  const dataElements = dataElementsSpecs.byNamespace(ns.namespace);
  const schemaId = `${baseSchemaURL}/${namespaceToURLPathSegment(ns.namespace)}`;
  let schema = {
    $schema: 'http://json-schema.org/draft-04/schema#',
    id: schemaId,
    title: "TODO: Figure out what the title should be.",
    definitions: {}
  };
  const entryRef = makeRef(new Identifier('shr.base', 'Entry'), ns, baseSchemaURL);
  if (ns.description) {
    schema.description = ns.description;
  }
  const nonEntryEntryTypeField = {
    $ref: makeRef(new Identifier('shr.base', 'EntryType'), ns, baseSchemaURL)
  };

  const defs = dataElements.sort(function(l,r) {return l.identifier.name.localeCompare(r.identifier.name);});
  const entryRefs = [];
  for (const def of defs) {
    const lastLogger = logger;
    logger = logger.child({ shrId: def.identifier.fqn});
    try {
      logger.debug('Exporting element');
      let schemaDef = {
        type: 'object',
        properties: {}
      };
      let wholeDef = schemaDef;
      const tbdParentDescriptions = [];
      let requiredProperties = [];
      let needsEntryType = false;
      if (def.isEntry || def.basedOn.length) {
        wholeDef = { allOf: [] };
        let hasEntryParent = false;
        for (const supertypeId of def.basedOn) {
          if (supertypeId instanceof TBD) {
            if (supertypeId.text) {
              tbdParentDescriptions.push(supertypeId.text);
            } else {
              tbdParentDescriptions.push('TBD');
            }
          } else {
            const parent = dataElementsSpecs.findByIdentifier(supertypeId);
            if (!parent) {
              logger.error('Could not find definition for %s which is a supertype of %s', supertypeId, def);
            } else {
              hasEntryParent = hasEntryParent || parent.isEntry;
            }
            wholeDef.allOf.push({ $ref: makeRef(supertypeId, ns, baseSchemaURL)});
          }
        }
        if (def.isEntry && (!hasEntryParent)) {
          wholeDef.allOf.splice(0, 0, { $ref: entryRef });
        }
        wholeDef.allOf.push(schemaDef);
      } else {
        needsEntryType = true;
      }

      const tbdFieldDescriptions = [];
      if (def.value) {
        if (def.value.inheritance !== INHERITED) {
          let { value, required, tbd } = convertDefinition(def.value, dataElementsSpecs, ns, baseSchemaURL, baseTypeURL);
          if (required) {
            requiredProperties.push('Value');
          }
          schemaDef.properties.Value = value;
          if (tbd) {
            schemaDef.properties.Value.description = def.value.text ? ('TBD: ' + def.value.text) : tbdValueToString(def.value);
          }
        }
      }
      if (def.fields.length) {
        const fieldNameMap = {};
        const clashingNames = {};
        for (const field of def.fields) {
          if (!(field instanceof TBD)) {
            if (!isValidField(field)) {
              continue;
            } else if (field.inheritance === INHERITED) {
              if (fieldNameMap[field.identifier.name]) {
                logger.error(`ERROR: clashing property names: %s and %s ERROR_CODE: 12038`, fieldNameMap[field.identifier.name].fqn, field.identifier.fqn);
                clashingNames[field.identifier.name] = true;
              } else {
                fieldNameMap[field.identifier.name] = field.identifier;
              }
              continue;
            }

            if (fieldNameMap[field.identifier.name]) {
              logger.error(`ERROR: clashing property names: %s and %s ERROR_CODE: 12038`, fieldNameMap[field.identifier.name].fqn, field.identifier.fqn);
              clashingNames[field.identifier.name] = true;
              continue;
            } else {
              fieldNameMap[field.identifier.name] = field.identifier;
            }
          }
          const card = field.effectiveCard;
          if (card && card.isZeroedOut) {
            continue;
          }
          let {value, required, tbd} = convertDefinition(field, dataElementsSpecs, ns, baseSchemaURL, baseTypeURL);
          if (tbd) {
            tbdFieldDescriptions.push(tbdValueToString(field));
            continue;
          }

          if (field.identifier.fqn === 'shr.base.EntryType') {
            needsEntryType = false;
          }

          schemaDef.properties[field.identifier.name] = value;
          if (required) {
            requiredProperties.push(field.identifier.name);
          }
        }

        for (const clashingName in clashingNames) {
          delete schemaDef.properties[clashingName];
        }
        requiredProperties = requiredProperties.filter(propName => !(propName in clashingNames));
      } else if (!def.value) {
        schemaDef.type = 'object';
        schemaDef.description = 'Empty DataElement?';
      }
      let descriptionList = [];
      if (def.description) {
        descriptionList.push(def.description);
      }
      if (def.concepts.length) {
        wholeDef.concepts = def.concepts.map((concept) => makeConceptEntry(concept));
      }
      if (tbdParentDescriptions.length) {
        tbdParentDescriptions[0] = 'TBD Parents: ' + tbdParentDescriptions[0];
        descriptionList = descriptionList.concat(tbdParentDescriptions);
      }
      if (tbdFieldDescriptions.length) {
        tbdFieldDescriptions[0] = 'TBD Fields: ' + tbdFieldDescriptions[0];
        descriptionList = descriptionList.concat(tbdFieldDescriptions);
      }
      if (descriptionList.length) {
        wholeDef.description = descriptionList.join('\n');
      }
      if (needsEntryType) {
        schemaDef.properties['EntryType'] = nonEntryEntryTypeField;
        if (def.identifier.fqn !== 'shr.base.EntryType') {
          requiredProperties.push('EntryType');
        }
      }

      if (requiredProperties.length) {
        schemaDef.required = requiredProperties;
      }

      schema.definitions[def.identifier.name] = wholeDef;
      if (def.isEntry && (!def.isAbstract)) {
        entryRefs.push({ $ref: makeRef(def.identifier, ns, baseSchemaURL)});
      }
    } finally {
      logger = lastLogger;
    }
  }

  if (entryRefs.length) {
    schema.type = 'object';
    schema.anyOf = entryRefs;
  }
  return { schemaId, schema };
}
Converts a namespace into a JSON Schema.
@param {Namespace} ns - the namespace of the schema.
@param {DataElementSpecifications} dataElementsSpecs - the elements in the namespace.
@param {string} baseSchemaURL - the root URL for the schema identifier.
@param {string} baseTypeURL - the root URL for the EntryType field.
@return {{schemaId: string, schema: Object}} The schema id and the JSON Schema definition.