|
| 1 | +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 2 | +### Datapackage envelope assembler |
| 3 | +### |
| 4 | +### The public, source-agnostic seam between "write resources" and "write the |
| 5 | +### manifest". The experiment-level writeParquet() methods accumulate a list of |
| 6 | +### Frictionless resource descriptors and then assemble the datapackage.json |
| 7 | +### envelope; this function is that envelope step, factored out so that (a) the |
| 8 | +### SummarizedExperiment and MultiAssayExperiment methods single-source it and |
| 9 | +### (b) producers that build resources piecemeal -- streaming a dataset too large |
| 10 | +### to hold in memory, or promoting from a foreign store -- can emit a conformant |
| 11 | +### datapackage.json without hand-rolling it. It is the write half of the |
| 12 | +### ingest contract; the read half is readParquet() (container level) and the |
| 13 | +### DuckDBMatrix()/DuckDBArray()/DuckDBTable() constructors (component level), |
| 14 | +### which attach existing Parquet in place. |
| 15 | + |
| 16 | +#' Write a Frictionless datapackage.json envelope |
| 17 | +#' |
| 18 | +#' Assembles and writes the top-level \code{datapackage.json} manifest from a |
| 19 | +#' list of already-written Frictionless resource descriptors. This is the |
| 20 | +#' envelope step shared by the experiment-level \code{\link{writeParquet}} |
| 21 | +#' methods, exposed so that producers who accumulate resources incrementally |
| 22 | +#' (streaming large datasets, or promoting from another store) can emit a |
| 23 | +#' conformant manifest without reconstructing an in-memory Bioconductor object. |
| 24 | +#' |
| 25 | +#' Each entry of \code{resources} is a Frictionless resource descriptor -- a list |
| 26 | +#' with \code{name}, \code{path}, \code{dimension}, \code{layout}, \code{format}, |
| 27 | +#' \code{mediatype}, and \code{schema} -- exactly as returned by the primitive |
| 28 | +#' \code{\link{writeParquet}} methods (array, \code{data.frame}, \code{DataFrame}, |
| 29 | +#' \code{SelfHits}). \code{NULL} entries are dropped, so the \code{NULL} returned |
| 30 | +#' by append/streaming parts (see \code{\link{writeParquet}}) can be accumulated |
| 31 | +#' and passed straight through. Descriptors are written verbatim otherwise; strip |
| 32 | +#' any private, non-Frictionless keys before calling. |
| 33 | +#' |
| 34 | +#' @param model Character(1) package-level schema identifier that selects the |
| 35 | +#' \code{\link{readParquet}} reader used to reconstruct the container (e.g. |
| 36 | +#' \code{"summarized_experiment"}, \code{"single_cell_experiment"}, |
| 37 | +#' \code{"multi_assay_experiment"}). See the storage-layout vignette for the |
| 38 | +#' documented \code{model} values. |
| 39 | +#' @param resources A list of Frictionless resource descriptors (each a list), |
| 40 | +#' as returned/accumulated from \code{\link{writeParquet}}. \code{NULL} entries |
| 41 | +#' are removed. |
| 42 | +#' @param path Character(1) directory to write \code{datapackage.json} into; |
| 43 | +#' created recursively if it does not exist. |
| 44 | +#' @param main_exp_name Optional character(1) naming the main experiment (used by |
| 45 | +#' the \code{single_cell_experiment} reader). Omitted from the manifest when |
| 46 | +#' \code{NULL}. |
| 47 | +#' @param annotations Optional list of non-relational metadata elements (as |
| 48 | +#' produced during metadata serialization). Omitted when \code{NULL}. |
| 49 | +#' |
| 50 | +#' @return Invisibly, the assembled package list that was written. |
| 51 | +#' |
| 52 | +#' @examples |
| 53 | +#' # Assemble a manifest from a hand-built resource descriptor. |
| 54 | +#' tf <- tempfile() |
| 55 | +#' resources <- list(list( |
| 56 | +#' name = "features", path = "features", |
| 57 | +#' dimension = "feature", layout = "data_frame", |
| 58 | +#' format = "parquet", |
| 59 | +#' mediatype = "application/vnd.apache.parquet", |
| 60 | +#' schema = list(fields = list(list(name = "id", type = "integer"))))) |
| 61 | +#' writeDatapackage("summarized_experiment", resources, tf) |
| 62 | +#' cat(readLines(file.path(tf, "datapackage.json")), sep = "\n") |
| 63 | +#' |
| 64 | +#' @seealso \code{\link{writeParquet}} for writing resources, and |
| 65 | +#' \code{\link{readParquet}} for reading a written package (the |
| 66 | +#' \code{DuckDBMatrix}/\code{DuckDBArray}/\code{DuckDBTable} constructors attach |
| 67 | +#' an existing coord-array in place). |
| 68 | +#' |
| 69 | +#' @author Patrick Aboyoun |
| 70 | +#' |
| 71 | +#' @importFrom jsonlite write_json |
| 72 | +#' @importFrom S4Vectors isSingleString |
| 73 | +#' @export |
| 74 | +writeDatapackage <- function(model, resources, path, |
| 75 | + main_exp_name = NULL, annotations = NULL) |
| 76 | +{ |
| 77 | + if (!isSingleString(model)) |
| 78 | + stop("'model' must be a single non-NA string") |
| 79 | + if (!isSingleString(path)) |
| 80 | + stop("'path' must be a single non-NA string") |
| 81 | + if (!is.list(resources)) |
| 82 | + stop("'resources' must be a list of Frictionless resource descriptors") |
| 83 | + if (!is.null(main_exp_name) && !isSingleString(main_exp_name)) |
| 84 | + stop("'main_exp_name' must be NULL or a single string") |
| 85 | + |
| 86 | + # Drop NULL descriptors (append/streaming parts return NULL). |
| 87 | + resources <- Filter(Negate(is.null), resources) |
| 88 | + |
| 89 | + package <- list(model = model, resources = resources) |
| 90 | + if (!is.null(main_exp_name)) |
| 91 | + package[["main_exp_name"]] <- main_exp_name |
| 92 | + if (!is.null(annotations)) |
| 93 | + package[["annotations"]] <- annotations |
| 94 | + |
| 95 | + # Declare the Frictionless profile as the leading key. |
| 96 | + package <- c(list("$schema" = .BIOCDUCKDB_PROFILE), package) |
| 97 | + |
| 98 | + dir.create(path, recursive = TRUE, showWarnings = FALSE) |
| 99 | + write_json(package, path = file.path(path, "datapackage.json"), |
| 100 | + auto_unbox = TRUE, pretty = TRUE) |
| 101 | + |
| 102 | + invisible(package) |
| 103 | +} |
0 commit comments