JSON Model-to-Model Projection (JM2MP) Documentation

JSON Model-to-Model Projection (JM2MP) Documentation

source

modules/normalizer.js

The file normalizer.js contains the module [normalizer]{@link module:jm2mp/modules/normalizer}, which implements the standardization process over the JM2MP projection modules content in the stage of resolution, before it will be evaluated.

Author

  • Luis Maria CAMARA ROSSI

Copyright

  • Universidad Nacional de Educación a Distancia (U.N.E.D.) 2026

License

  • BSD-3-Clause
/**
 * @author Luis Maria CAMARA ROSSI
 * @copyright Universidad Nacional de Educación a Distancia (U.N.E.D.) 2026
 * @license BSD-3-Clause
 * @file
 * The file `normalizer.js` contains the module
 * [normalizer]{@link module:jm2mp/modules/normalizer}, which implements
 * the standardization process over the `JM2MP` _projection modules_
 * content in the stage of resolution, before it will be evaluated.
**/

/**
 * @module jm2mp/modules/normalizer
 * @description
 * The module [normalizer]{@link module:jm2mp/modules/normalizer}
 * implements the standardization process over the `JM2MP` _projection
 * modules_ content in the stage of resolution, before it will be evaluated.
 *
 * Each module can declare a _default query language_ (in
 * `$.$options.$default-query-language` path). Such _syntax_ is applied
 * inside the _module_ to every `get` _template command_ that omit their
 * `$syntax`clause, adding to it in its absence.
 *
 * So, after normalization, every `get` _template command_ in the
 * _projection module_ will carry an explicit `$syntax`clause;
 * therefore, all the `get` _templates_ will be self-explanatory and can
 * be merged across different _projection modules_ each one with different
 * syntaxes without ambiguity.
 * 
 * The two meta-data properties that will be ignored by the normalizer are:
 * - `$options`: used for _module_ configuration (dependencies, default syntax, ...).
 * - `$schema`: used to add an optional reference to an external JSON Schema.
 *
 * These two meta-data properties are never considered as _named
 * templates_ and only affects to the current _projection module_.
 * The normalizer preserves them without processing their content.
**/

/* ------------------------------------------------------------------ */
/* ------------------------------------------------------------------ */

import { ThrowsValidationErrorWhenIsNotAModule } from "./helpers.js";

/* ------------------------------------------------------------------ */
/* ------------------------------------------------------------------ */

/**
 * @constant {Set.<string>}
 * @description
 * The set of keys (property names) considered as _module_ meta-data
 * and not as _names templates_.
 * 
 * This keys are preserved as is, without traversing its content during
 * the normalization process.
 */
export const MODULE_METADATA_KEYS = new Set(["$options", "$schema"]);

/* ------------------------------------------------------------------ */

/**
 * @description
 * It normalizes a complete _projection module_, adding `$syntax`clauses
 * to every `get`_template command_ that ommits it.
 *
 * It never modifies the original _module_; it always returns a new
 * structured object.
 * @param {object} module
 * The _projection module_ to mormalize (so it must be a JSON root object).
 * @returns {object}
 * The just created and normalized _projection module_.
**/
export function normalizeModule(module)
{
  // It validates if it is actually a valid module.
  ThrowsValidationErrorWhenIsNotAModule(module);
  // It gets the default query language declared for the module.
  const defaultSyntax = getDefaultSyntax(module);
  // It builds a new (resultant) projection module.
  const result = {};
  // It traverses all keys of the module-root-object.
  for (const key of Object.keys(module))
  {
    if (MODULE_METADATA_KEYS.has(key))
    {
      // Meta-data keys are preserved as-is, without traversing its
      // content. It is decision of the resolver if they will be
      // discarded when importing modules.
      result[key] = module[key];
    }
    // The rest of keys are named templates, including the root
    // template, so they must be recursively normalized.
    else
    {
      result[key] = normalizeNode(module[key], defaultSyntax);
    }
  }
  // It returns the new normalized module.
  return result;
}

/* ------------------------------------------------------------------ */

/**
 * @description
 * It extracts the optionally declared default _query language_,
 * which if if exists must be declared in path
 * `$.$options.$default-query-language`. If it is not declared or is
 * not considered as valid, the `native` syntax will be used by default.
 * @param {object} module
 * The _projection module_ to normalize (so it must be a JSON root
 * object).
 * @returns {string}
 * The name of the _query language_ to be used by default.
 */
function getDefaultSyntax(module)
{
  // Bu default, 'native' query language will be used.
  let default_query_language = 'native' ;
  // It tries to locate an optional default syntax.
  const module_options = ( ("$options" in module)
                           ? module["$options"]
                           : undefined );
  if (module_options
      && (typeof module_options === "object")
      && !Array.isArray(module_options))
  {
    const default_query_language_declared = module_options["$default-query-language"];
    // It validates the found default syntax.
    if ( ((typeof default_query_language_declared) === "string") &&
         (default_query_language_declared.length > 0) )
    {
      default_query_language = default_query_language_declared;
    }
  }
  // It returns the result.
  return default_query_language;
}

/* ------------------------------------------------------------------ */

/**
 * @description
 * It recursively normalizes a node of the entire tree of JSON values
 * (named templates and command templates of the projection module).
 * 
 * It only modifies nodes that represent actual `get` _template
 * commands_ without a `$syntax` clause; every other node is just
 * traversed to normalize their children.
 * @param {*} node
 * The current node (JSON value) a normalize.
 * @param {string} defaultSyntax
 * The default _query language_ declared (or considered) for this
 * _projection module_.
 * @returns {*}
 * The normalized node.
 */
function normalizeNode(node, defaultSyntax)
{
  let result;
  // Scalar (primitive) data types do not need normalization.
  if (node === null || typeof node !== "object")
  {
    result = node;
  }
  // For arrays, it normalizes all their children (recursively).
  else if (Array.isArray(node))
  {
    result = node.map( (child) => normalizeNode(child, defaultSyntax) );
  }
  // For objects...
  else
  {
    // ...first, you must distinguish between a literal object and a
    // _template command_ (by checking for the presence of a `$op` key).
    const isOperation = Object.hasOwn(node, "$op") &&
                        ((typeof node.$op) === "string");
    if (isOperation && (node.$op === "get"))
    {
      // In case of 'get' template commands without '$syntax', such
      // clause will be added with the default value for the module.
      result = normalizeGet(node, defaultSyntax);
    }
    else
    {
      // In case of any other command template, it recursively traverses
      // al its keys (childrens).
      result = {};
      for (const key of Object.keys(node))
      {
        result[key] = normalizeNode(node[key], defaultSyntax);
      }
    }
  }
  return result;
}

/* ------------------------------------------------------------------ */

/**
 * @description
 * It normalizes a `get` _template command_, adding a new `$syntax`
 * clause if missing; rest of clauses will be also traversed (for if
 * inner get commands are present).
 * @param {object} getOp
 * The current `get` _template command_ to normalize.
 * @param {string} defaultSyntax
 * The default _query language_ for the _projection module_.
 * @returns {object}
 * A new object, equivalent to 'getOp' but with a `$syntax` clause
 * present.
 */
function normalizeGet(getOp, defaultSyntax)
{
  // The resultant template command.
  const result = {};
  // It copies all its properties, recursively traversing
  // inner projections.
  for (const key of Object.keys(getOp))
  {
    // $op:get as-is (literal)
    if (key === "$op")
    {
      result[key] = getOp[key];
    }
    // $path as-is (considered as literal)
    else if (key === "$path")
    {
      result[key] = getOp[key];
    }
    // If $syntax is already present, it is preserved (explicitly authored).
    else if (key === "$syntax")
    {
      result[key] = getOp[key];
    }
    // $from is a projection, so it must be traversed.
    else if (key === "$from")
    {
      result[key] = normalizeNode(getOp[key], defaultSyntax);
    }
    // Any other key will be also traversed, but in this version of
    // JM2MP will be considered as invalid during validation stage.
    else
    {
      result[key] = normalizeNode(getOp[key], defaultSyntax);
    }
  }
  // If no $syntax clause is found, then the default syntax will be considered.
  if ( ! Object.hasOwn(result, "$syntax") )
  {
    result.$syntax = defaultSyntax;
  }
  // it returns the resultant template command.
  return result;
}

/* ------------------------------------------------------------------ */
/* ------------------------------------------------------------------ */
/* End of file: ${JM2MP.JS}/src/modules/normalizer.js                 */