general.js

import * as bioc from "bioconductor";
import * as df from "./DataFrame.js";
import * as se from "./SummarizedExperiment.js";
import * as rse from "./RangedSummarizedExperiment.js";
import * as sce from "./SingleCellExperiment.js";

/**
 * @param {string} path - Path to the takane-formatted object directory containing the {@link DataFrame}.
 * @param {object} globals - Object containing `fs`, an object satisfying the {@link GlobalFsInterface}. 
 * @return {object} Object metadata.
 * @async
 */
export async function readObjectFile(path, globals) {
    let payload = await globals.fs.get(path + "/OBJECT", { asBuffer: true });
    let dec = new TextDecoder;
    return JSON.parse(dec.decode(payload));
}

/**
 * @type {object}
 * @desc Registry of reader functions.
 * Each key is a takane object type, and each value is a function that accepts the same arguments as {@linkcode readObject}.
 */
export const readObjectRegistry = {};

/**
 * This function will inspect {@linkcode readObjectRegistry} to check if any reader function is supplied for the takane object type at `path`.
 * If found, it will use that function, otherwise it will fall back to the default functions:
 * 
 * - {@linkcode readDataFrame}, to read {@link external:DataFrame DataFrame} objects.
 * - {@linkcode readSummarizedExperiment}, to read {@link external:SummarizedExperiment SummarizedExperiment} objects.
 * - {@linkcode readRangedSummarizedExperiment}, to read {@link external:RangedSummarizedExperiment RangedSummarizedExperiment} objects.
 * - {@linkcode readSingleCellExperiment}, to read {@link external:SingleCellExperiment SingleCellExperiment} objects.
 *
 * @param {string} path - Path to a takane-formatted object directory. 
 * @param {?object} metadata - Object metadata.
 * If `null`, this is automatically loaded by calling {@linkcode readObjectFile} on `path`.
 * @param {object} globals - Object containing `fs`, an object satisfying the {@link GlobalFsInterface}; and `h5`, an object satisfying the {@link GlobalH5Interface}.
 * @param {object} [options={}] - Further options, to be passed to the reader functions for individual takane object types.
 *
 * @return Some in-memory representation of the takane object at `path`.
 * @async
 */
export async function readObject(path, metadata, globals, options = {}) {
    if (metadata == null) {
        metadata = await readObjectFile(path, globals);
    }

    let objtype = metadata["type"];
    if (objtype in readObjectRegistry) {
        return readObjectRegistry[objtype](path, metadata, globals, options);

    } else {
        const defaults = {
            "data_frame": df.readDataFrame,
            "summarized_experiment": se.readSummarizedExperiment,
            "ranged_summarized_experiment": rse.readRangedSummarizedExperiment,
            "single_cell_experiment": sce.readSingleCellExperiment
        };

        if (objtype in defaults) {
            return defaults[objtype](path, metadata, globals, options);
        }

        throw new Error("type '" + objtype + "' is not supported");
    }
}

/**
 * @type {Array}
 * @desc Registry of saving functions.
 * Each entry should be an array of length 2, containing a Javascript class and its saving function.
 * Each saving function should accept the same arguments as {@linkcode saveObject}.
 * Subclasses should be placed after their parents in this array.
 */
export const saveObjectRegistry = [];

/**
 * This function will inspect {@linkcode saveObjectRegistry} to check if any saving function is supplied for `x`.
 * If found, it will use that function, otherwise it will fall back to the default functions:
 * 
 * - {@linkcode saveDataFrame}, to save {@link external:DataFrame DataFrame} objects.
 * - {@linkcode saveSummarizedExperiment}, to save {@link external:SummarizedExperiment SummarizedExperiment} objects.
 * - {@linkcode saveRangedSummarizedExperiment}, to save {@link external:RangedSummarizedExperiment RangedSummarizedExperiment} objects.
 * - {@linkcode saveSingleCellExperiment}, to save {@link external:SingleCellExperiment SingleCellExperiment} objects.
 *
 * @param {Any} x - The takane-compatible object to be saved.
 * @param {string} path - Path to the directory in which to save `x`.
 * @param {object} globals - Object containing `fs`, an object satisfying the {@link GlobalFsInterface}; and `h5`, an object satisfying the {@link GlobalH5Interface}.
 * @param {object} [options={}] - Further options.
 *
 * @return `x` is stored at `path`.
 * @async
 */
export async function saveObject(x, path, globals, options = {}) {
    for (var i = saveObjectRegistry.length; i > 0; i--) {
        const [cls, meth] = saveObjectRegistry[i - 1];
        if (x instanceof cls) {
            meth(x, path, globals, options);
            return;
        }
    }

    const defaults = [
        [bioc.SingleCellExperiment, sce.saveSingleCellExperiment],
        [bioc.RangedSummarizedExperiment, rse.saveRangedSummarizedExperiment],
        [bioc.SummarizedExperiment, se.saveSummarizedExperiment],
        [bioc.DataFrame, df.saveDataFrame]
    ];

    for (const [cls, fun] of defaults) {
        if (x instanceof cls) {
            fun(x, path, globals, options);
            return;
        }
    }

    throw new Error("object of type '" + x.constructor.name + "' is not supported");
}