Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions base/workflow/NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Generated by roxygen2: do not edit by hand

S3method(as.data.frame,pecan_preprocess_result)
S3method(print,pecan_preprocess_result)
export(do_conversions)
export(run.write.configs)
export(runModule.get.trait.data)
export(runModule.run.write.configs)
export(runModule_start_model_runs)
export(standard_result)
export(start_model_runs)
export(validate_standard_result)
importFrom(dplyr,"%>%")
importFrom(rlang,"%||%")
179 changes: 179 additions & 0 deletions base/workflow/R/standard_result.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#' Standard Preprocessing Result Object
#'
#' @description
#' Construct a minimal, uniform result object for preprocessing functions.
#' This object is intended to standardize the current mix of return values such
#' as path strings, nested lists, and BETY identifier lists.
#'
#' @param tag Character scalar identifying the input type. One of
#' `"met"`, `"soil"`, `"ic"`, `"phenology"`, or `"fia"`.
#' @param paths Character vector of output file paths.
#' @param input_id Integer scalar BETY input ID, or `NA_integer_` when unused.
#' @param dbfile_id Integer scalar BETY dbfile ID, or `NA_integer_` when unused.
#' @param format Character scalar naming the output format.
#' @param source Character scalar naming the source dataset or provider.
#' @param status Character scalar. One of `"success"`, `"error"`, or
#' `"skipped"`.
#' @param error_message Character scalar or `NULL`. Error detail when
#' `status == "error"`.
#'
#' @return An object of class `pecan_preprocess_result`.
#' @export
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest adding the @md tag somewhere in this Roxygen doc. Otherwise, the syntax highlighting may not be visible/render correctly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be done for other functions as well.

#'
#' @examples
#' standard_result(
#' tag = "met",
#' paths = "/data/dbfiles/ERA5_772_2005.nc",
#' input_id = 42001L,
#' dbfile_id = 55003L,
#' format = "CF Meteorology",
#' source = "ERA5"
#' )
standard_result <- function(tag,
paths = character(),
input_id = NA_integer_,
dbfile_id = NA_integer_,
format = "",
source = "",
status = "success",
error_message = NULL) {
result <- list(
tag = tag,
paths = as.character(paths),
input_id = as.integer(input_id),
dbfile_id = as.integer(dbfile_id),
format = format,
source = source,
status = status,
error_message = error_message
)

class(result) <- c("pecan_preprocess_result", "list")
validate_standard_result(result)
}

#' Validate a Standard Preprocessing Result
#'
#' @description
#' Validate the structure and field types of a standard preprocessing result.
#' Returns the input object invisibly when valid and throws an error otherwise.
#'
#' @param x Object to validate.
#'
#' @return `x`, invisibly, when validation succeeds.
#' @export
validate_standard_result <- function(x) {
allowed_tags <- c("met", "soil", "ic", "phenology", "fia")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the idea of hard coding the allowable tags within a low-level function. If the goal is to replace the BETY input type table, note that this table had been designed specifically to allow it to be extensible without recompiling any code. Burying it here means there's yet one more thing people need to remember to do when adding new models or expanding existing models. This specific list is also really incomplete and misleading (e.g., ED2 alone has 9 input tags: met, lu, thsum, veg, soil, pss, site, css, initcond; on the other side AFAIK no model has "fia" as in input tag)

allowed_status <- c("success", "error", "skipped")

if (!inherits(x, "pecan_preprocess_result")) {
stop("`x` must inherit from 'pecan_preprocess_result'.", call. = FALSE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using PEcAn.logger::logger.severe() instead of these stop() for consistency with rest of the codebase

}
if (!is.list(x)) {
stop("`x` must be a list.", call. = FALSE)
}
if (!is.character(x$tag) || length(x$tag) != 1L || is.na(x$tag) || !(x$tag %in% allowed_tags)) {
stop("`tag` must be one of: met, soil, ic, phenology, fia.", call. = FALSE)
}
if (!is.character(x$paths)) {
stop("`paths` must be a character vector.", call. = FALSE)
}
if (!is.integer(x$input_id) || length(x$input_id) != 1L) {
stop("`input_id` must be an integer scalar.", call. = FALSE)
}
if (!is.integer(x$dbfile_id) || length(x$dbfile_id) != 1L) {
stop("`dbfile_id` must be an integer scalar.", call. = FALSE)
}
if (!is.character(x$format) || length(x$format) != 1L) {
stop("`format` must be a character scalar.", call. = FALSE)
}
if (!is.character(x$source) || length(x$source) != 1L) {
stop("`source` must be a character scalar.", call. = FALSE)
}
if (!is.character(x$status) || length(x$status) != 1L || !(x$status %in% allowed_status)) {
stop("`status` must be one of: success, error, skipped.", call. = FALSE)
}
if (!is.null(x$error_message) && (!is.character(x$error_message) || length(x$error_message) != 1L)) {
stop("`error_message` must be NULL or a character scalar.", call. = FALSE)
}
if (identical(x$status, "error") && is.null(x$error_message)) {
stop("`error_message` must be provided when `status` is 'error'.", call. = FALSE)
}
if (!identical(x$status, "error") && !is.null(x$error_message)) {
stop("`error_message` must be NULL unless `status` is 'error'.", call. = FALSE)
}

# TODO: When callers migrate, validate caller-specific invariants here.
invisible(x)
}

#' Print a Standard Preprocessing Result
#'
#' @param x A `pecan_preprocess_result` object.
#' @param ... Unused.
#'
#' @return `x`, invisibly.
#' @export
print.pecan_preprocess_result <- function(x, ...) {
validate_standard_result(x)

cat(sprintf("PEcAn preprocessing result [%s]\n", x$tag))
cat(sprintf(" status: %s\n", x$status))
cat(sprintf(" source: %s\n", x$source))
cat(sprintf(" format: %s\n", x$format))

if (length(x$paths) == 0L) {
cat(" paths: <none>\n")
} else {
cat(sprintf(" paths: %s\n", paste(x$paths, collapse = ", ")))
}

cat(sprintf(
" db_ids: input=%s, dbfile=%s\n",
ifelse(is.na(x$input_id), "NA", as.character(x$input_id)),
ifelse(is.na(x$dbfile_id), "NA", as.character(x$dbfile_id))
))

if (identical(x$status, "error")) {
cat(sprintf(" error: %s\n", x$error_message))
}

# TODO: Expand printed details only after all preprocessors return this type.
invisible(x)
}

#' Coerce a Standard Preprocessing Result to a Data Frame
#'
#' @param x A `pecan_preprocess_result` object.
#' @param ... Unused.
#'
#' @return A data frame with one row per path.
#' @export
as.data.frame.pecan_preprocess_result <- function(x, ...) {
validate_standard_result(x)

n_paths <- length(x$paths)
if (n_paths == 0L) {
path_values <- NA_character_
n_rows <- 1L
} else {
path_values <- x$paths
n_rows <- n_paths
}

# TODO: Revisit row shape if callers need one row per result instead of per path.
data.frame(
tag = rep.int(x$tag, n_rows),
path = path_values,
input_id = rep.int(x$input_id, n_rows),
dbfile_id = rep.int(x$dbfile_id, n_rows),
format = rep.int(x$format, n_rows),
source = rep.int(x$source, n_rows),
status = rep.int(x$status, n_rows),
error_message = rep.int(
if (is.null(x$error_message)) NA_character_ else x$error_message,
n_rows
),
stringsAsFactors = FALSE
)
}
19 changes: 19 additions & 0 deletions base/workflow/man/as.data.frame.pecan_preprocess_result.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions base/workflow/man/print.pecan_preprocess_result.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions base/workflow/man/standard_result.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions base/workflow/man/validate_standard_result.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading