JSON Model-to-Model Projection (JM2MP) Documentation

JSON Model-to-Model Projection (JM2MP) Documentation

source

adapters/registry.js

The module [Registry]{@link module:jm2mp/adapters/registry} declares the [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter} interface to implement by any external query language as part of JM2MP projection documents, which needs to be registered using an instance of the [AdapterRegistry]{@link module:jm2mp/adapters/registry.AdapterRegistry} class.

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 module [Registry]{@link module:jm2mp/adapters/registry} declares
 * the [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 * interface to implement by any external _query language_ as part of
 * `JM2MP` _projection documents_, which needs to be registered using
 * an instance of the
 * [AdapterRegistry]{@link module:jm2mp/adapters/registry.AdapterRegistry}
 * class.
**/

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

/**
 * @module jm2mp/adapters/registry
 * @description
 * This module declares the
 * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
 * interface to implement by any external _query language_ as part of
 * `JM2MP` _projection documents_, which needs to be registered using
 * an instance of the
 * [AdapterRegistry]{@link module:jm2mp/adapters/registry.AdapterRegistry}
 * class.
 *
 * This module serves as a central registry for _query language_
 * external adapters (like `JMESPath` or `JSONPath`, to name a few).
 * After an external adapters is registered, its offered syntax can be
 * used as part of any `JM2MP` _projection module_ through the `$get`
 * _template command_.
 *
 * To standardize any potential _adapters_, an **asynchronous**
 * execution mechanism has been chosen.
 *
 * The `evaluate` function will always return a `Promise<any>`.
 * This way, _adapters_ that are internally synchronous can simply
 * return `Promise.resolve(value)` implicitly, since they are declared
 * as `async`. This unifies the interface and avoids ambiguities in the evaluator.
 *
 * The fact is that, for now, the `JSONata@2.x` library is the only
 * asynchronous one. However, it is expected that in the future, as
 * asynchronous programming in JavaScript becomes more popular, the
 * number of external libraries that benefit from this design will
 * grow.
**/

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

import { AdapterError } from "../errors.js";

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

/**
 * @typedef {object} FallbackPolicyObject
 * @memberof module:jm2mp/adapters/registry
 * @description
 * FallbackPolicyObject
 * A descriptive object with human-readable documentation of the
 * adapter's behavior.
 * @property {!string} missing
 * How a missing value from each query is returned.
 * @property {!string} singleMatch
 * How a single matched value from each query is returned.
 * @property {!string} multipleMatches
 * How multiple matched values from each query are returned.
 * @property {!string} typeError
 * How type errors from each query are catched and maybe raised.
 * @property {!string} nullInput
 * How a null input (execution environment) is treated.
 * @property {!string} timeout
 * Is there any timeout to configure? Currenctly only affects to
 * `JSONata@2.x` adapter, due its _async_ internal implementation.
**/

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

/**
 * @typedef {Function} ValidateFunction
 * @memberof module:jm2mp/adapters/registry
 * @description
 * typedef {(path: any) => Promise<void>} ValidateFunction
 * @property {ValidateFunction} validate
 * It validates a `$path` expression statically (_template command_
 * `get`).
 * 
 * It throws a `ValidationError` if it is invalid.
 * 
 * It is _asynchronous_ to accommodate adapters whose parser is
 * _asynchronous_.
**/

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

/**
 * @typedef {Function} EvaluateFunction
 * @memberof module:jm2mp/adapters/registry
 * @description
 * typedef {(path: any, input: any, cache: Map<string, any>, env: object) => Promise<any>} EvaluateFunction
 * @property {EvaluateFunction} evaluate
 * It evaluates the expression on the provided `input`.
 *
 * It must be always `async`.
 *
 * Every adapter must guarantee satisfaction of (uniform contract):
 * - If input is null, returns null without invoking the underlying
 *   library.
 * - If the expression returns no result or undefined, it returns null.
 * - If the expression returns a single scalar, it returns that scalar
 *   not wrapped in an array.
 * - If the expression returns multiple matches, it returns an array
 *   wrapping all maches.
 * - Library errors are wrapped in `EvaluationError` or
 *   `ValidationError`, as appropriate.
 *
 * The `cache` parameter is a `Map` provided by the system to store
 * compiled expressions during the current evaluation.
 *
 * The `env` parameter is the complete language environment.
 * The native adapter needs it to resolve `$` or `@` or `%alias`;
 * the rest of adapters ignore it.
 *
 * @property {FallbackPolicyObject} fallbackPolicy
 * Human-readable documentation of the adapter's behavior.
 * 
 * Required fields: `missing`, `multipleMatches`, `typeError`, and
 * `nullInput`.
**/

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

/**
 * @typedef {object} QueryAdapter
 * @memberof module:jm2mp/adapters/registry
 * @description
 * Interface (contract) that every _query language_ **adapter** must
 * implement.
 * @property {string} name
 * Unique syntax identifier.
 * 
 * It is used in the `$syntax` clause of the `get` _templante command_.
 * @property {string} description
 * Shrot description for documenting purposes.
 * @property {ValidateFunction} validate
 * It validates (tests) the path expression.
 * @property {EvaluateFunction} evaluate
 * It evaluates (runs) the path expression.
**/

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

/**
 * @description
 * The **registry of adapter**. It maintains a mapping between names
 * and adapters.
 * 
 * This class is **not** a _singleton_. Each instance is independent,
 * which allows for isolated testing and different configurations
 * depending on the evaluation context.
**/
export class AdapterRegistry
{

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

  /**
   * @constructor
   * @description
   * Default constructor for the
   * [AdapterRegistry]{@link module:jm2mp/adapters/registry.AdapterRegistry}
   * class.
  **/
  constructor()
  {
    /**
     * @type {Map<string, QueryAdapter>}
     * @description
     * Field to reference all registered
     * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}'s.
    **/
    this._adapters = new Map();
  }

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

  /**
   * @description
   * It registers a new [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}.
   * @param {module:jm2mp/adapters/registry.QueryAdapter} adapter
   * The
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * to be registered.
   * @throws {module:jm2mp/errors.AdapterError}
   * It throws an [AdapterError]{@link module:jm2mp/errors.AdapterError}
   * if there is a previously registered
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * with the same `name`, and also if the specified `adapter`
   * does not comply with the required interface (contract).
  **/
  register(adapter)
  {
    // It validates the adapter's interface (contract).
    if (!adapter || typeof adapter !== "object") {
      throw new AdapterError("All adapters must be object.");
    }
    else if (typeof adapter.name !== "string" || adapter.name.length === 0) {
      throw new AdapterError("All adapters require a 'name' property which must be non empty.");
    }
    else if (typeof adapter.evaluate !== "function") {
      throw new AdapterError(
        `This "${adapter.name}" adapter does not implement an 'evaluate' function.`
      );
    }
    else if (typeof adapter.validate !== "function") {
      throw new AdapterError(
        `This "${adapter.name}" adapter does not implement a 'validate' function.`
      );
    }
    else if (this._adapters.has(adapter.name)) {
      throw new AdapterError(
        `This registry already contains a previously registered adapter with same name "${adapter.name}".`
      );
    }
    // The specified adapter is saved into this registry.
    else {
      this._adapters.set(adapter.name, adapter);
    }
  }

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

  /**
   * @description
   * It gets a previously registered
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * by its name; otherwise, an
   * [AdapterError]{@link module:jm2mp/errors.AdapterError} is raised,
   * informing the list of available adapters' names.
   * @param {string} name
   * The name of the
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * being searched for.
   * @returns {QueryAdapter}
   * Previously registered
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter};
   * otherwise, an [AdapterError]{@link module:jm2mp/errors.AdapterError}
   * exception will be raised.
   * @throws {module:jm2mp/errors.AdapterError}
   * Whenever no
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * with `name` were previously registered in this
   * [AdapterRegistry]{@link module:jm2mp/adapters/registry.AdapterRegistry}
   * instance.
  **/
  get(name)
  {
    const adapter = this._adapters.get(name);
    if (!adapter) {
      throw new AdapterError(
        `There is no adapter registered under the name "${name}. "` +
        `The available adapters are: "${[...this._adapters.keys()].join(", ") || "(none)"}."`
      );
    }
    return adapter;
  }

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

  /**
   * @description
   * It tests if a
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * has been registered with such 'name' in this
   * [AdapterRegistry]{@link module:jm2mp/adapters/registry.AdapterRegistry}
   * instance.
   * @param {string} name
   * The name of the
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * being searched for.
   * @returns {boolean}
   * `true` whenever a registered
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * with such 'name' is found in this
   * [AdapterRegistry]{@link module:jm2mp/adapters/registry.AdapterRegistry};
   * `false` otherwise.
  **/
  has(name)
  {
    return this._adapters.has(name);
  }

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

  /**
   * @description
   * It returns a list with the 'name's of all previously registered
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}s.
   * @returns {string[]}
   * An _array of strings_ with the 'name's of all
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * registered in this
   * [AdapterRegistry]{@link module:jm2mp/adapters/registry.AdapterRegistry}
   * instance; if no
   * [QueryAdapter]{@link module:jm2mp/adapters/registry.QueryAdapter}
   * has been registered, an **empty array** will be returned.
  **/
  names()
  {
    return [...this._adapters.keys()];
  }

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

}  // export class AdapterRegistry

/* ------------------------------------------------------------------ */
/* ------------------------------------------------------------------ */
/* End of file: ${JM2MP.JS}/src/adapters/registry.js                  */