diff --git a/base/workflow/DESCRIPTION b/base/workflow/DESCRIPTION index 1d0ca83bd70..942bb74ce54 100644 --- a/base/workflow/DESCRIPTION +++ b/base/workflow/DESCRIPTION @@ -48,5 +48,5 @@ Suggests: withr Copyright: Authors Encoding: UTF-8 -RoxygenNote: 7.3.3 X-schema.org-keywords: PEcAn, data-assimilation, ecosystem-modeling, reproducibility +Config/roxygen2/version: 8.0.0 diff --git a/base/workflow/NAMESPACE b/base/workflow/NAMESPACE index c05f334c5d5..0f324aefd21 100644 --- a/base/workflow/NAMESPACE +++ b/base/workflow/NAMESPACE @@ -1,10 +1,13 @@ # Generated by roxygen2: do not edit by hand export(do_conversions) +export(insert_preprocess_result) +export(preprocess_result) export(run.write.configs) export(runModule.get.trait.data) export(runModule.run.write.configs) export(runModule_start_model_runs) export(start_model_runs) +export(validate_preprocess_result) importFrom(dplyr,"%>%") importFrom(rlang,"%||%") diff --git a/base/workflow/R/preprocess_result.R b/base/workflow/R/preprocess_result.R new file mode 100644 index 00000000000..f5ac3c84bc7 --- /dev/null +++ b/base/workflow/R/preprocess_result.R @@ -0,0 +1,140 @@ +#' Preprocessing Result Constructor +#' +#' Uniform return type for all preprocessing pipeline functions. +#' Returns a named list that can be inserted directly into a PEcAn settings object. +#' Disk-only for now; memory-primary path can be added later without breaking +#' the interface. +#' +#' @param file Character. Path to the generated file. +#' @param format Character. Format identifier (e.g., "CFmet", "ED2met"). +#' @param mimetype Character. MIME type of the file. +#' @param dbfile.id Integer. BETY dbfile ID, if available. NULL if dbparms = NULL. +#' @param start_date POSIXct. Start date of the data period. +#' @param end_date POSIXct. End date of the data period. +#' @param source Character. Data source identifier (e.g., "Ameriflux", "CRUNCEP"). +#' +#' @return A named list with class attribute "preprocess_result" for potential +#' future subclassing, but behaves as a plain list for now. +#' +#' @examples +#' result <- preprocess_result( +#' file = "/data/flux/US-NR1.2004.nc", +#' format = "CFmet", +#' mimetype = "application/x-netcdf", +#' dbfile.id = 12345, +#' start_date = as.POSIXct("2004-01-01"), +#' end_date = as.POSIXct("2004-12-31"), +#' source = "Ameriflux" +#' ) +#' +#' @md +#' @export +preprocess_result <- function(file, format, mimetype, dbfile.id = NULL, + start_date = NULL, end_date = NULL, source = NULL) { + # Validation - using PEcAn.logger::logger.severe() for consistency with codebase + if (!is.character(file) || length(file) != 1) { + PEcAn.logger::logger.severe("file must be a single character path") + } + if (!file.exists(file)) { + PEcAn.logger::logger.warn("file does not exist:", file) + } + if (!is.character(format) || length(format) != 1) { + PEcAn.logger::logger.severe("format must be a single character string") + } + if (!is.character(mimetype) || length(mimetype) != 1) { + PEcAn.logger::logger.severe("mimetype must be a single character string") + } + if (!is.null(dbfile.id) && (!is.numeric(dbfile.id) || length(dbfile.id) != 1)) { + PEcAn.logger::logger.severe("dbfile.id must be a single integer or NULL") + } + + result <- list( + file = file, + format = format, + mimetype = mimetype, + dbfile.id = dbfile.id, + start_date = start_date, + end_date = end_date, + source = source + ) + + # Class attribute for future subclassing, but no S3 methods yet + class(result) <- c("preprocess_result", "list") + + result +} + +#' Insert Preprocess Result into Settings +#' +#' Takes a preprocess_result (or list of them) and patches it into the +#' appropriate location in a PEcAn settings object. +#' +#' @param settings List. PEcAn settings object. +#' @param result preprocess_result or list of preprocess_result. +#' @param input.type Character. Input type identifier, e.g., "met", "soil". +#' +#' @return Updated settings object. +#' +#' @md +#' @export +insert_preprocess_result <- function(settings, result, input.type) { + if (!is.list(result)) { + PEcAn.logger::logger.severe("result must be a list or preprocess_result") + } + + # Handle single result or list of results + if (inherits(result, "preprocess_result")) { + result <- list(result) + } + + # Ensure path exists in settings + if (is.null(settings$run$inputs)) { + settings$run$inputs <- list() + } + if (is.null(settings$run$inputs[[input.type]])) { + settings$run$inputs[[input.type]] <- list() + } + + # Insert results + settings$run$inputs[[input.type]]$path <- sapply(result, `[[`, "file") + settings$run$inputs[[input.type]]$format <- result[[1]]$format + settings$run$inputs[[input.type]]$mimetype <- result[[1]]$mimetype + + if (!is.null(result[[1]]$dbfile.id)) { + settings$run$inputs[[input.type]]$dbfile.id <- sapply(result, `[[`, "dbfile.id") + } + + settings +} + +#' Validate Preprocess Result +#' +#' Check that a preprocess_result has all required fields and valid values. +#' Useful for parity tests and pipeline validation. +#' +#' @param result List. Object to validate. +#' @param require.file.exists Logical. Whether to check file existence. +#' +#' @return Logical. TRUE if valid, throws error otherwise. +#' +#' @md +#' @export +validate_preprocess_result <- function(result, require.file.exists = TRUE) { + required_fields <- c("file", "format", "mimetype") + + for (field in required_fields) { + if (is.null(result[[field]])) { + PEcAn.logger::logger.severe("Missing required field:", field) + } + } + + if (!is.character(result$file) || length(result$file) != 1) { + PEcAn.logger::logger.severe("file must be a single character path") + } + + if (require.file.exists && !file.exists(result$file)) { + PEcAn.logger::logger.severe("file does not exist:", result$file) + } + + TRUE +} diff --git a/base/workflow/man/insert_preprocess_result.Rd b/base/workflow/man/insert_preprocess_result.Rd new file mode 100644 index 00000000000..151f9914cca --- /dev/null +++ b/base/workflow/man/insert_preprocess_result.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/preprocess_result.R +\name{insert_preprocess_result} +\alias{insert_preprocess_result} +\title{Insert Preprocess Result into Settings} +\usage{ +insert_preprocess_result(settings, result, input.type) +} +\arguments{ +\item{settings}{List. PEcAn settings object.} + +\item{result}{preprocess_result or list of preprocess_result.} + +\item{input.type}{Character. Input type identifier, e.g., "met", "soil".} +} +\value{ +Updated settings object. +} +\description{ +Takes a preprocess_result (or list of them) and patches it into the +appropriate location in a PEcAn settings object. +} diff --git a/base/workflow/man/preprocess_result.Rd b/base/workflow/man/preprocess_result.Rd new file mode 100644 index 00000000000..d4070711706 --- /dev/null +++ b/base/workflow/man/preprocess_result.Rd @@ -0,0 +1,53 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/preprocess_result.R +\name{preprocess_result} +\alias{preprocess_result} +\title{Preprocessing Result Constructor} +\usage{ +preprocess_result( + file, + format, + mimetype, + dbfile.id = NULL, + start_date = NULL, + end_date = NULL, + source = NULL +) +} +\arguments{ +\item{file}{Character. Path to the generated file.} + +\item{format}{Character. Format identifier (e.g., "CFmet", "ED2met").} + +\item{mimetype}{Character. MIME type of the file.} + +\item{dbfile.id}{Integer. BETY dbfile ID, if available. NULL if dbparms = NULL.} + +\item{start_date}{POSIXct. Start date of the data period.} + +\item{end_date}{POSIXct. End date of the data period.} + +\item{source}{Character. Data source identifier (e.g., "Ameriflux", "CRUNCEP").} +} +\value{ +A named list with class attribute "preprocess_result" for potential +future subclassing, but behaves as a plain list for now. +} +\description{ +Uniform return type for all preprocessing pipeline functions. +Returns a named list that can be inserted directly into a PEcAn settings object. +Disk-only for now; memory-primary path can be added later without breaking +the interface. +} +\examples{ +result <- preprocess_result( + file = "/data/flux/US-NR1.2004.nc", + format = "CFmet", + mimetype = "application/x-netcdf", + dbfile.id = 12345, + start_date = as.POSIXct("2004-01-01"), + end_date = as.POSIXct("2004-12-31"), + source = "Ameriflux" +) + +} diff --git a/base/workflow/man/validate_preprocess_result.Rd b/base/workflow/man/validate_preprocess_result.Rd new file mode 100644 index 00000000000..570a107c7e4 --- /dev/null +++ b/base/workflow/man/validate_preprocess_result.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/preprocess_result.R +\name{validate_preprocess_result} +\alias{validate_preprocess_result} +\title{Validate Preprocess Result} +\usage{ +validate_preprocess_result(result, require.file.exists = TRUE) +} +\arguments{ +\item{result}{List. Object to validate.} + +\item{require.file.exists}{Logical. Whether to check file existence.} +} +\value{ +Logical. TRUE if valid, throws error otherwise. +} +\description{ +Check that a preprocess_result has all required fields and valid values. +Useful for parity tests and pipeline validation. +} diff --git a/base/workflow/tests/testthat/test-preprocess_result.R b/base/workflow/tests/testthat/test-preprocess_result.R new file mode 100644 index 00000000000..66fe5e7e24a --- /dev/null +++ b/base/workflow/tests/testthat/test-preprocess_result.R @@ -0,0 +1,101 @@ + +context("Preprocess Result Constructor") + +test_that("preprocess_result creates valid object", { + tmpfile <- tempfile(fileext = ".nc") + file.create(tmpfile) + on.exit(unlink(tmpfile)) + + result <- preprocess_result( + file = tmpfile, + format = "CFmet", + mimetype = "application/x-netcdf", + dbfile.id = 12345, + start_date = as.POSIXct("2004-01-01"), + end_date = as.POSIXct("2004-12-31"), + source = "Ameriflux" + ) + + expect_type(result, "list") + expect_equal(result$file, tmpfile) + expect_equal(result$format, "CFmet") + expect_equal(result$dbfile.id, 12345) + expect_s3_class(result, "preprocess_result") + expect_s3_class(result, "list") +}) + +test_that("preprocess_result validates inputs with logger.severe", { + # logger.severe stops execution, so we expect error + expect_error( + preprocess_result(file = 123, format = "CFmet", mimetype = "application/x-netcdf"), + "file must be a single character path" + ) + + expect_error( + preprocess_result(file = "foo.nc", format = NULL, mimetype = "application/x-netcdf"), + "format must be a single character string" + ) +}) + +test_that("preprocess_result accepts missing files", { + result <- preprocess_result( + file = "/nonexistent/file.nc", + format = "CFmet", + mimetype = "application/x-netcdf" + ) + + expect_equal(result$file, "/nonexistent/file.nc") +}) + +test_that("insert_preprocess_result patches settings correctly", { + settings <- list(run = list(inputs = list())) + tmpfile <- tempfile(fileext = ".nc") + file.create(tmpfile) + on.exit(unlink(tmpfile)) + + result <- preprocess_result( + file = tmpfile, + format = "CFmet", + mimetype = "application/x-netcdf" + ) + + updated <- insert_preprocess_result(settings, result, "met") + + expect_equal(updated$run$inputs$met$path, tmpfile) + expect_equal(updated$run$inputs$met$format, "CFmet") +}) + +test_that("insert_preprocess_result handles multiple results", { + settings <- list(run = list(inputs = list())) + tmpfile1 <- tempfile(fileext = ".nc") + tmpfile2 <- tempfile(fileext = ".nc") + file.create(tmpfile1) + file.create(tmpfile2) + on.exit({ + unlink(tmpfile1) + unlink(tmpfile2) + }) + + results <- list( + preprocess_result(file = tmpfile1, format = "CFmet", mimetype = "application/x-netcdf"), + preprocess_result(file = tmpfile2, format = "CFmet", mimetype = "application/x-netcdf") + ) + + updated <- insert_preprocess_result(settings, results, "met") + + expect_equal(updated$run$inputs$met$path, c(tmpfile1, tmpfile2)) +}) + +test_that("validate_preprocess_result catches invalid results", { + expect_error( + validate_preprocess_result(list(format = "CFmet")), + "Missing required field: file" + ) + + tmpfile <- tempfile() + file.create(tmpfile) + on.exit(unlink(tmpfile)) + + valid <- preprocess_result(file = tmpfile, format = "CFmet", mimetype = "application/x-netcdf") + expect_true(validate_preprocess_result(valid)) +}) \ No newline at end of file