diff --git a/.github/skills/document/SKILL.md b/.github/skills/document/SKILL.md index 9914cdd0..06e97e1d 100644 --- a/.github/skills/document/SKILL.md +++ b/.github/skills/document/SKILL.md @@ -8,22 +8,21 @@ description: Document package functions. Use when asked to document functions. *All* R functions in `R/` should be documented in roxygen2 `#'` style, including internal/unexported functions. -- Run `air format .` then `devtools::document()` after changing any roxygen2 docs. +- Run `air format .` then `devtools::document()` after changing any roxygen2 docs. If it says "Could not resolve link to topic" for a function we updated, run `devtools::document()` again to make sure the error was transient. - Use sentence case for all headings. - Wrap roxygen comments at 80 characters. - Files matching `R/import-standalone-*.R` are imported from other packages and have their own conventions. Do not modify their documentation. -- After documenting functions, run `devtools::document(roclets = c('rd', 'collate', 'namespace'))`. - If `_pkgdown.yml` exists and contains a `reference` section: - Whenever you add a new (non-internal) documentation topic, also add the topic to `_pkgdown.yml`. - Use `pkgdown::check_pkgdown()` to check that all topics are included in the reference index. ## Shared parameters -**Parameters used in more than one function** go in `R/aaa-shared_params.R` under `@name .shared-params`. Functions inherit them with `@inheritParams .shared-params`. See `R/aaa-shared_params.R` for current definitions (if it exists). +**Parameters used in more than one function** go in `R/aaa-shared_params.R` under `@name .shared-params`. Functions inherit them with `@inheritParams .shared-params`. See `R/aaa-shared_params.R` for current definitions. Note: definitions of dot-prefixed parameters (e.g. `#' @param .call`) are also imported by roxygen for their dot-less equivalent (e.g. `#' @param call`); if we use both versions, we only need to document the dot-prefixed version in `.shared-params`. -Shared params blocks: alphabetize parameters, use `@name .shared-params` (with leading dot), include `@keywords internal`, end with `NULL`. +Shared params blocks: alphabetize parameters (ignoring dot prefixes), use `@name .shared-params` (with leading dot), include `@keywords internal`, end with `NULL`. -Multiple shared-params groups (e.g. `.shared-params-io`, `.shared-params-parsing`) are appropriate when parameters are only shared within a file and closely related files. +Multiple shared-params groups (e.g. `#' @name .shared-params-io`, `#' @name .shared-params-parsing`) are appropriate when parameters are only shared within a file and closely related files. ## Parameter documentation format diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index e299ee30..ae9a04bb 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -41,10 +41,10 @@ jobs: any::rcmdcheck any::rlang any::roxygen2 + any::stbl any::testthat any::usethis any::withr - wranglezone/stbl local::. - name: Install air diff --git a/DESCRIPTION b/DESCRIPTION index 20c57d67..bc3ac030 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -24,8 +24,7 @@ Imports: glue, lifecycle, purrr (>= 1.0.2), - rlang (>= 1.1.0), - stbl, + rlang (>= 1.2.0), tibble (>= 3.2.1), tidyselect (>= 1.2.0), vctrs (>= 0.7.2), @@ -38,6 +37,7 @@ Suggests: memoise (>= 2.0.1), rmarkdown (>= 2.16), spelling (>= 2.2), + stbl, testthat (>= 3.1.4), tidyr, yaml (>= 2.3.6) diff --git a/NAMESPACE b/NAMESPACE index d7933187..876d0a12 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -49,7 +49,9 @@ export(camel_case_to_snake_case) export(get_spec) export(guess_tspec) export(guess_tspec_df) +export(guess_tspec_list) export(guess_tspec_object) +export(guess_tspec_object_list) export(nest_tree) export(parse_openapi_schema) export(parse_openapi_spec) @@ -94,12 +96,10 @@ importFrom(rlang,as_function) importFrom(rlang,caller_arg) importFrom(rlang,caller_env) importFrom(rlang,check_bool) -importFrom(rlang,check_dots_empty) importFrom(rlang,check_string) importFrom(rlang,current_call) importFrom(rlang,current_env) importFrom(rlang,is_empty) -importFrom(rlang,is_named) importFrom(rlang,is_true) importFrom(rlang,list2) importFrom(rlang,zap) diff --git a/NEWS.md b/NEWS.md index ef068675..eccde6ea 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,9 +1,14 @@ # tibblify (development version) +* All arguments of functions that accept meaningful named `...` are now prefixed with `.` to minimize conflicts with column and object names in `...`. The un-dotted versions of the arguments are still accepted, but calling functions directly with un-dotted arguments will produce a warning once per session (see `?lifecycle::deprecate_soft()`). Un-dotted arguments will be phased out in a future version of this package, so we recommend switching to the dot-prefixed versions. See `?tspec_df` and `?tib_scalar()` for details. +* All code has been refactored for maintainability. While we were careful to ensure that output is unchanged, it is possible that a corner case is no longer handled how it was in version 0.3.0. Please notify us () if something has changed for the worse in an unexpected way (#243). +* The `guess_tspec()` variants `guess_tspec_list()` and `guess_tspec_object_list()` are now exported (along with `guess_tspec_df()` and `guess_tspec_object()`, which were already exported). `guess_tspec()` should correctly guess the format in most cases, but you can call the variant directly if you think `guess_tspec()` is dispatching incorrectly (#249). * `untibblify()` now automatically uses the `tib_spec` attribute when present, so tibblified objects can be round-tripped without explicitly passing the spec (#235). * `parse_openapi_spec()` supports many more fields and works for many more APIs (#190, #200, @jonthegeek and @mgirlich). * The underlying C implementation has been updated to better comply with R's C API. We also fixed various bugs during this update (#203, #204, #222). -* Documentation of all functions has been updated for clarity (#228, #245, #246). +* All vignettes and the documentation of all functions has been updated for clarity (#243). + +(roughly sorted into "Breaking changes", "Potential breaking changes", "New features", "Bug fixes", and "Documentation" as of 2026-04-10, but I left out the headers to make it easier to add more bullets during development) # tibblify 0.3.1 diff --git a/R/aaa-conditions.R b/R/aaa-conditions.R deleted file mode 100644 index 35f21bb5..00000000 --- a/R/aaa-conditions.R +++ /dev/null @@ -1,22 +0,0 @@ -#' Raise a package-scoped error -#' -#' @inheritParams .shared-params -#' @inheritParams stbl::pkg_abort -#' @returns Does not return. -#' @keywords internal -.pkg_abort <- function( - message, - subclass, - call = caller_env(), - message_env = caller_env(), - ... -) { - stbl::pkg_abort( - "tibblify", - message, - subclass, - call = call, - message_env = message_env, - ... - ) -} diff --git a/R/aaa-shared_params.R b/R/aaa-shared_params.R index 47f0c4eb..d0f92c73 100644 --- a/R/aaa-shared_params.R +++ b/R/aaa-shared_params.R @@ -3,27 +3,40 @@ #' These parameters are used in multiple functions. They are defined here to #' make them easier to import and to find. #' +#' @param arg (`character(1)`) An argument name. This name will be mentioned in +#' error messages as the input that is at the origin of a problem. #' @param .call (`environment`) The environment to use for error messages. #' @param .children (`character(1)`) The name of the field that contains the #' children. #' @param .children_to (`character(1)`) The column name in which to store the #' children. +#' @param col (`any`) A column from a data frame, which may be a vector, a +#' list, or a nested data frame. #' @param .elt_transform (`function` or `NULL`) A function to apply to each #' element before casting to `.ptype_inner`. +#' @param empty_list_unspecified (`logical(1)`) Treat empty lists as +#' unspecified? #' @param .fill (`vector` or `NULL`) Optionally, a value to use if the field #' does not exist. #' @param .format (`character(1)` or `NULL`) Passed to the `format` argument of #' [as.Date()]. +#' @param inform_unspecified (`logical(1)`) Inform about fields whose type could +#' not be determined? #' @param .key (`character`) The path of names to the field in the object. +#' @param local_env (`environment`) A local environment used to track state +#' across recursive calls, such as whether empty lists were encountered. #' @param name (`character(1)`) The name of the field. #' @param .ptype (`vector(0)`) A prototype of the desired output type of the #' field. #' @param .ptype_inner (`vector(0)`) A prototype of the input field. #' @param .required (`logical(1)`) Throw an error if the field does not exist? +#' @param simplify_list (`logical(1)`) Should scalar lists be simplified to +#' vectors? #' @param spec_list (`list`) A list of specifications. #' @param tib_list (`list`) A list of tib fields. #' @param .transform (`function` or `NULL`) A function to apply to the whole #' vector after casting to `.ptype_inner`. +#' @param value (`list`) An object list whose fields will be guessed. #' @param .values_to (`character(1)` or `NULL`) For `NULL` (the default), the #' field is converted to a `.ptype` vector. If a string is provided, the field #' is converted to a tibble and the values go into the specified column. diff --git a/R/guess_tspec.R b/R/guess_tspec.R new file mode 100644 index 00000000..0f88639c --- /dev/null +++ b/R/guess_tspec.R @@ -0,0 +1,67 @@ +#' Guess the `tibblify()` specification +#' +#' @description +#' +#' `guess_tspec()` automatically dispatches to the other `guess_tspec_*()` +#' functions based on the shape of the input. If you are unhappy with its +#' output, calling a specific `guess_tspec_*()` function may yield better +#' results, or at least clearer error messages about why that type isn't +#' supported. +#' +#' - Use `guess_tspec_df()` if the input is a data frame. +#' - Use `guess_tspec_object()` if the input is an object (such as a JSON +#' object that has been read into R as a named list). +#' - Use `guess_tspec_object_list()` if the input is a list of objects (such as +#' a JSON object that has been read into R as a list of named lists). +#' - Use `guess_tspec_list()` if the input object is a list but you aren't sure +#' how it should be processed. +#' +#' See `vignette("supported-structures")` for a discussion of the input types +#' supported by tibblify. +#' +#' @param x (`list` or `data.frame`) A nested list or a data frame. +#' @param ... These dots are for future extensions and must be empty. +#' @inheritParams .shared-params +#' +#' @returns A specification object that can be used in [tibblify()]. +#' @export +#' +#' @examples +#' guess_tspec(list(x = 1, y = "a")) +#' guess_tspec(list(list(x = 1), list(x = 2))) +#' +#' guess_tspec(gh_users) +guess_tspec <- function( + x, + ..., + empty_list_unspecified = FALSE, + simplify_list = FALSE, + inform_unspecified = should_inform_unspecified(), + call = rlang::caller_env() +) { + rlang::check_dots_empty(call = call) + if (is.data.frame(x)) { + return(guess_tspec_df( + x, + empty_list_unspecified = empty_list_unspecified, + simplify_list = simplify_list, + inform_unspecified = inform_unspecified, + call = call + )) + } + if (vctrs::obj_is_list(x)) { + return(guess_tspec_list( + x, + empty_list_unspecified = empty_list_unspecified, + simplify_list = simplify_list, + inform_unspecified = inform_unspecified, + call = call + )) + } + stop_input_type( + x, + c("a data frame", "a list"), + arg = caller_arg(x), + call = call + ) +} diff --git a/R/guess_tspec_df.R b/R/guess_tspec_df.R new file mode 100644 index 00000000..ee52e882 --- /dev/null +++ b/R/guess_tspec_df.R @@ -0,0 +1,251 @@ +#' @export +#' @rdname guess_tspec +guess_tspec_df <- function( + x, + ..., + empty_list_unspecified = FALSE, + simplify_list = FALSE, + inform_unspecified = should_inform_unspecified(), + call = rlang::current_call(), + arg = rlang::caller_arg(x) +) { + rlang::check_dots_empty() + rlang::check_bool(empty_list_unspecified, call = call) + rlang::check_bool(simplify_list, call = call) + rlang::check_bool(inform_unspecified, call = call) + + local_env <- rlang::new_environment(list(empty_list_used = FALSE)) + if (is.data.frame(x)) { + # TODO inform that `simplify_list` is not used for data frames + fields <- .imap_col_to_spec(x, empty_list_unspecified, local_env) + spec <- tspec_df( + !!!fields, + .vector_allows_empty_list = .read_empty_list_argument(local_env) + ) + } else { + .check_list(x, arg = arg) + .check_object_list(x, arg = arg, call = call) + spec <- guess_tspec_object_list( + x, + empty_list_unspecified = empty_list_unspecified, + simplify_list = simplify_list, + call = call + ) + } + return(.maybe_inform_unspecified(spec, inform_unspecified, call = call)) +} + +#' Convert a column to a tib field specification +#' +#' @inheritParams .shared-params +#' @returns A tib field specification. +#' @keywords internal +.col_to_spec <- function(col, name, empty_list_unspecified, local_env) { + col_type <- .tib_type_of(col, name, other = FALSE) + + # Can only be df, vector, or list + if (col_type == "df") { + return(.df_col_to_spec(col, name, empty_list_unspecified, local_env)) + } + if (col_type == "vector") { + return(.vector_col_to_spec(col, name)) + } + + return(.list_col_to_spec(col, name, empty_list_unspecified, local_env)) +} + +#' Apply column-to-spec conversion across a data frame +#' +#' @param col_list (`list`) A named list of columns, typically a data frame. +#' @inheritParams .shared-params +#' @returns A named list of tib field specifications. +#' @keywords internal +.imap_col_to_spec <- function(col_list, empty_list_unspecified, local_env) { + purrr::imap(col_list, \(col, name) { + .col_to_spec(col, name, empty_list_unspecified, local_env) + }) +} + +#' Convert a nested data frame column to a tib_row specification +#' +#' @inheritParams .shared-params +#' @returns A [tib_row()] specification. +#' @keywords internal +.df_col_to_spec <- function(col, name, empty_list_unspecified, local_env) { + fields_spec <- .imap_col_to_spec(col, empty_list_unspecified, local_env) + return(tib_row(name, !!!fields_spec)) +} + +#' Convert a vector column to a tib scalar or unspecified specification +#' +#' @inheritParams .shared-params +#' @returns A [tib_scalar()] or [tib_unspecified()] specification. +#' @keywords internal +.vector_col_to_spec <- function(col, name) { + ptype <- .tib_ptype(col) + if (.is_unspecified(ptype)) { + return(tib_unspecified(name)) + } + return(tib_scalar(name, ptype)) +} + +## list_col_to_spec ------------------------------------------------------------ + +#' Convert a list column to a tib field specification +#' +#' Inspects the elements of `col` to determine whether they share a common ptype +#' and dispatches to the appropriate spec builder. +#' +#' @inheritParams .shared-params +#' @returns A tib field specification. +#' @keywords internal +.list_col_to_spec <- function(col, name, empty_list_unspecified, local_env) { + # `col` must be a list, so we need to check what its elements are + list_of_col <- vctrs::is_list_of(col) + if (list_of_col) { + ptype <- attr(col, "ptype", exact = TRUE) + ptype_type <- .tib_type_of(ptype, name, other = FALSE) + } else { + # TODO this could use sampling for performance + ptype_common <- .get_ptype_common(col, empty_list_unspecified) + if (!ptype_common$has_common_ptype) { + return(tib_variant(name)) + } + ptype <- ptype_common$ptype + if (is.null(ptype)) { + return(tib_unspecified(name)) + } + ptype_type <- .tib_type_of(ptype, name, other = FALSE) + .mark_empty_list_argument(ptype_common$had_empty_lists, local_env) + } + + if (ptype_type == "vector") { + return(tib_vector(name, ptype)) + } + if (ptype_type == "df") { + return(.col_to_spec_df( + ptype, + col = col, + name = name, + list_of_col, + empty_list_unspecified, + local_env + )) + } + + cli::cli_abort( + "List columns that only consists of lists are not supported yet." + ) +} + +#' Convert a df-typed list column to a tib_df specification +#' +#' Delegates to [.list_of_col_to_spec_df()] or [.non_list_of_col_to_spec_df()] +#' based on whether `col` is a `list_of()` column. +#' +#' @param list_of_col (`logical(1)`) Whether `col` is a `list_of()` column. +#' @inheritParams .shared-params +#' @returns A [tib_df()] specification. +#' @keywords internal +.col_to_spec_df <- function( + ptype, + col, + name, + list_of_col, + empty_list_unspecified, + local_env +) { + if (list_of_col) { + fields_spec <- .list_of_col_to_spec_df( + col, + ptype, + empty_list_unspecified, + local_env + ) + } else { + fields_spec <- .non_list_of_col_to_spec_df( + col, + ptype, + empty_list_unspecified, + local_env + ) + } + tib_df(name, !!!fields_spec) +} + +#' Build field specs from a list_of df column +#' +#' @inheritParams .shared-params +#' @returns A named list of tib field specifications. +#' @keywords internal +.list_of_col_to_spec_df <- function( + col, + ptype, + empty_list_unspecified, + local_env +) { + col_required <- TRUE # Tests are the same with this being ~anything. + has_non_vec_cols <- purrr::detect_index(ptype, ~ !.is_vec(.x)) > 0 + if (has_non_vec_cols) { + # non-vector columns need to be inspected further to actually get their + # specification + col_flat <- vctrs::list_unchop(col, ptype = ptype) + } else { + col_flat <- ptype + } + .imap_col_to_spec( + col_flat, + empty_list_unspecified, + local_env + ) +} + +#' Build field specs from a non-list_of df column +#' +#' @inheritParams .shared-params +#' @returns A named list of tib field specifications with `required` set. +#' @keywords internal +.non_list_of_col_to_spec_df <- function( + col, + ptype, + empty_list_unspecified, + local_env +) { + col_flat <- vctrs::list_unchop(col, ptype = ptype) + fields_spec <- .imap_col_to_spec( + col_flat, + empty_list_unspecified, + local_env + ) |> + .df_guess_required(col, ptype) + fields_spec +} + +#' Guess whether each field is required in a df-typed list column +#' +#' @param fields_spec (`list`) A named list of tib field specifications. +#' @param col (`list`) A list column whose elements are data frames. +#' @inheritParams .shared-params +#' @returns `fields_spec` with `$required` updated for each field. +#' @keywords internal +.df_guess_required <- function(fields_spec, col, ptype) { + for (col_name in colnames(ptype)) { + fields_spec[[col_name]]$required <- .col_guess_required(col_name, col) + } + fields_spec +} + +#' Guess whether a field is required across a list of data frames +#' +#' @param col_name (`character(1)`) The column name to check. +#' @param df_list (`list`) A list of data frames. +#' @returns (`logical(1)`) `TRUE` if `col_name` is present in every data frame, +#' `FALSE` otherwise. +#' @keywords internal +.col_guess_required <- function(col_name, df_list) { + bad_idx <- purrr::detect_index( + df_list, + \(df) !col_name %in% colnames(df) + ) + bad_idx == 0 +} diff --git a/R/guess_tspec_list.R b/R/guess_tspec_list.R new file mode 100644 index 00000000..e133c54a --- /dev/null +++ b/R/guess_tspec_list.R @@ -0,0 +1,41 @@ +#' @export +#' @rdname guess_tspec +guess_tspec_list <- function( + x, + ..., + empty_list_unspecified = FALSE, + simplify_list = FALSE, + inform_unspecified = should_inform_unspecified(), + arg = caller_arg(x), + call = current_call() +) { + rlang::check_dots_empty() + .check_list(x) + if (is_empty(x)) { + msg <- "{.arg {arg}} must not be empty." + cli::cli_abort(msg, call = call) + } + + # if `x` is both an object list and an object, we default to treating it as + # and object_list. Users can manually choose in the very rare case that we're + # wrong. + if (.is_object_list(x)) { + return(guess_tspec_object_list( + x, + empty_list_unspecified = empty_list_unspecified, + simplify_list = simplify_list, + inform_unspecified = inform_unspecified, + call = call + )) + } + if (.is_object(x)) { + return(guess_tspec_object( + x, + empty_list_unspecified = empty_list_unspecified, + simplify_list = simplify_list, + inform_unspecified = inform_unspecified, + call = call + )) + } + .abort_not_tibblifiable(x, arg, call) +} diff --git a/R/guess_tspec_object.R b/R/guess_tspec_object.R new file mode 100644 index 00000000..2c8e6348 --- /dev/null +++ b/R/guess_tspec_object.R @@ -0,0 +1,297 @@ +#' @rdname guess_tspec +#' @export +guess_tspec_object <- function( + x, + ..., + empty_list_unspecified = FALSE, + simplify_list = FALSE, + inform_unspecified = should_inform_unspecified(), + call = rlang::current_call() +) { + rlang::check_dots_empty() + check_bool(empty_list_unspecified, call = call) + check_bool(simplify_list, call = call) + check_bool(inform_unspecified, call = call) + .check_not_df(x, call) + .check_list(x) + .check_object_names(x, call) + if (rlang::is_empty(x)) { + return(tspec_object()) + } + + local_env <- rlang::new_environment(list(empty_list_used = FALSE)) + fields <- .imap_guess_object_field_spec( + x, + empty_list_unspecified, + simplify_list, + local_env + ) + spec <- tspec_object( + .vector_allows_empty_list = .read_empty_list_argument(local_env), + !!!fields + ) + return(.maybe_inform_unspecified(spec, inform_unspecified, call = call)) +} + +#' Abort if `x` is a data frame +#' +#' @inheritParams .shared-params +#' @returns `x` (invisibly). +#' @keywords internal +.check_not_df <- function(x, call) { + if (is.data.frame(x)) { + msg <- c( + "{.arg x} must not be a dataframe.", + i = "Did you want to use {.fn guess_tspec_df} instead?" + ) + cli::cli_abort(msg, call = call) + } + return(invisible(x)) +} + +#' Guess the field spec for a single object field +#' +#' Dispatches to the appropriate helper based on the detected type of `value`. +#' +#' @inheritParams .shared-params +#' @returns A tib field specification. +#' @keywords internal +.guess_object_field_spec <- function( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) { + if (is.null(value) || identical(unname(value), list())) { + return(tib_unspecified(name)) + } + value_type <- .tib_type_of(value, name, other = TRUE) + if (value_type == "other") { + return(tib_variant(name)) + } + if (value_type == "vector") { + return(.guess_object_field_spec_vector(value, name)) + } + if (value_type == "df") { + return(.guess_object_field_spec_df( + value, + name, + empty_list_unspecified, + local_env + )) + } + + if (value_type != "list") { + # nocov start + cli::cli_abort( + "{.fn .tib_type_of} returned an unexpected type", + .internal = TRUE + ) + # nocov end + } + + if (.is_list_of_null(value)) { + return(tib_unspecified(name)) + } + if (.is_object_list(value)) { + return(.guess_object_field_spec_object_list( + value, + name, + empty_list_unspecified, + simplify_list, + local_env + )) + } + if (simplify_list) { + input_form_result <- .guess_vector_input_form(value, name) + if (input_form_result$can_simplify) { + return(input_form_result$tib_spec) + } + } + if (.is_object(value)) { + return(.guess_object_field_spec_object( + value, + name, + empty_list_unspecified, + simplify_list, + local_env + )) + } + + return(tib_variant(name)) +} + +#' Guess the field spec for a vector-typed field +#' +#' @inheritParams .shared-params +#' @returns A tib field specification. +#' @keywords internal +.guess_object_field_spec_vector <- function(value, name) { + ptype <- .tib_ptype(value) + if (.is_unspecified(ptype)) { + return(tib_unspecified(name)) + } + .tib_scalar_or_vector_spec(name, ptype, vctrs::vec_size(value) == 1) +} + +#' Guess the field spec for a data-frame-typed field +#' +#' @inheritParams .shared-params +#' @returns A `tib_df` field specification. +#' @keywords internal +.guess_object_field_spec_df <- function( + value, + name, + empty_list_unspecified, + local_env +) { + field_spec <- .imap_col_to_spec(value, empty_list_unspecified, local_env) + return(tib_df(name, !!!field_spec)) +} + +#' Guess the field spec for a nested object field +#' +#' @inheritParams .shared-params +#' @returns A `tib_row` field specification. +#' @keywords internal +.guess_object_field_spec_object <- function( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) { + .guess_object_field_spec_expand_fields( + value, + empty_list_unspecified, + simplify_list, + local_env, + .key = name, + tib_fn = tib_row, + fields_fn = .imap_guess_object_field_spec + ) +} + +#' Map `.guess_object_field_spec` over a named list +#' +#' @inheritParams .shared-params +#' @returns A named list of tib field specifications, one per element of `x`. +#' @keywords internal +.imap_guess_object_field_spec <- function( + x, + empty_list_unspecified, + simplify_list, + local_env +) { + purrr::imap( + x, + function(value, name) { + .guess_object_field_spec( + value, + name, + empty_list_unspecified, + simplify_list, + local_env + ) + } + ) +} + +#' Abort for missing or duplicate names +#' +#' @inheritParams .shared-params +#' @returns `NULL` (invisibly). +#' @keywords internal +.check_object_names <- function(x, call) { + .check_named(x, call = call) |> + .check_names_not_duplicated(call = call) +} + +#' Abort for missing names +#' +#' @inheritParams .shared-params +#' @returns `NULL` (invisibly). +#' @keywords internal +.check_named <- function(x, call) { + if (!rlang::is_named2(x)) { + msg <- "{.arg x} must be fully named." + cli::cli_abort(msg, call = call) + } + return(invisible(x)) +} + +#' Abort for duplicate names +#' +#' @inheritParams .shared-params +#' @returns `NULL` (invisibly). +#' @keywords internal +.check_names_not_duplicated <- function(x, call) { + if (vctrs::vec_duplicate_any(names(x))) { + msg <- "Names of {.arg x} must be unique." + cli::cli_abort(msg, call = call) + } + return(invisible(x)) +} + +#' Guess whether a list field can be simplified to a vector spec +#' +#' @inheritParams .shared-params +#' @returns A list with: +#' - `can_simplify` (`logical(1)`): Whether the field can be simplified. +#' - `tib_spec`: A tib field specification (present only when `can_simplify` +#' is `TRUE`). +#' @keywords internal +.guess_vector_input_form <- function(value, name) { + ptype_result <- .get_ptype_common(value, empty_list_unspecified = FALSE) + if (!ptype_result$has_common_ptype) { + return(list(can_simplify = FALSE)) + } + ptype <- ptype_result$ptype + if (is.null(ptype)) { + return(.guess_vector_input_form_null(value, name)) + } + if (!.is_vec(ptype)) { + return(list(can_simplify = FALSE)) + } + if (.is_field_scalar(value)) { + return(.guess_vector_input_form_field_scalar(value, name, ptype)) + } + return( + list(can_simplify = TRUE, tib_spec = tib_variant(name, .required = TRUE)) + ) +} + +#' Guess input form for a list field whose common ptype is `NULL` +#' +#' @inheritParams .shared-params +#' @returns A list with: +#' - `can_simplify` (`logical(1)`): Whether the field can be simplified. +#' - `tib_spec`: A tib field specification (present only when `can_simplify` +#' is `TRUE`). +#' @keywords internal +.guess_vector_input_form_null <- function(value, name) { + if (rlang::is_named(value)) { + return(list(can_simplify = FALSE)) + } + tib_spec <- tib_unspecified(name, .required = TRUE) + return(list(can_simplify = TRUE, tib_spec = tib_spec)) +} + +#' Build a tib spec for a field-scalar input form +#' +#' @inheritParams .shared-params +#' @returns A list with `can_simplify = TRUE` and `tib_spec`, a `tib_vector` +#' field specification. +#' @keywords internal +.guess_vector_input_form_field_scalar <- function(value, name, ptype) { + return(list( + can_simplify = TRUE, + tib_spec = tib_vector( + name, + ptype, + .required = TRUE, + .input_form = .tib_vector_input_form(value) + ) + )) +} diff --git a/R/guess_tspec_object_list.R b/R/guess_tspec_object_list.R new file mode 100644 index 00000000..bff58e67 --- /dev/null +++ b/R/guess_tspec_object_list.R @@ -0,0 +1,29 @@ +#' @export +#' @rdname guess_tspec +guess_tspec_object_list <- function( + x, + ..., + empty_list_unspecified = FALSE, + simplify_list = FALSE, + inform_unspecified = should_inform_unspecified(), + arg = caller_arg(x), + call = current_call() +) { + rlang::check_dots_empty() + rlang::check_bool(empty_list_unspecified, call = call) + rlang::check_bool(simplify_list, call = call) + rlang::check_bool(inform_unspecified, call = call) + .check_list(x, arg = arg, call = call) + .check_object_list(x, arg = arg, call = call) + + local_env <- rlang::new_environment(list(empty_list_used = FALSE)) + spec <- .guess_object_field_spec_expand_fields_df( + x, + empty_list_unspecified, + simplify_list, + local_env, + .vector_allows_empty_list = .read_empty_list_argument(local_env), + tib_fn = tspec_df + ) + return(.maybe_inform_unspecified(spec, inform_unspecified, call = call)) +} diff --git a/R/guess_tspec_object_utils.R b/R/guess_tspec_object_utils.R new file mode 100644 index 00000000..b9e32687 --- /dev/null +++ b/R/guess_tspec_object_utils.R @@ -0,0 +1,376 @@ +#' Create a scalar or vector tib spec +#' +#' @param is_scalar (`logical(1)`) If `TRUE`, return a [tib_scalar()] spec, +#' otherwise a [tib_vector()] spec. +#' @inheritParams .shared-params +#' @returns A [tib_scalar()] or [tib_vector()] spec. +#' @keywords internal +.tib_scalar_or_vector_spec <- function(name, ptype, is_scalar) { + if (is_scalar) { + tib_scalar(name, ptype) + } else { + tib_vector(name, ptype) + } +} + +#' Determine the vector input form of a value +#' +#' @inheritParams .shared-params +#' @returns `"object"` if `value` is named, `"scalar_list"` otherwise. +#' @keywords internal +.tib_vector_input_form <- function(value) { + ifelse(rlang::is_named(value), "object", "scalar_list") +} + +#' Expand an object list into a tib spec +#' +#' @param ... Additional arguments passed to `tib_fn`. +#' @param tib_fn (`function`) The tib constructor to wrap the fields in +#' (e.g. [tib_df()] or [tib_row()]). +#' @param fields_fn (`function`) The function used to generate field specs from +#' `value`; defaults to [.guess_object_list_spec()]. +#' @inheritParams .shared-params +#' @returns A tib spec created by `tib_fn`. +#' @keywords internal +.guess_object_field_spec_expand_fields <- function( + value, + empty_list_unspecified, + simplify_list, + local_env, + ..., + tib_fn = tib_df, + fields_fn = .guess_object_list_spec +) { + fields <- fields_fn( + value, + empty_list_unspecified, + simplify_list, + local_env + ) + return(tib_fn( + !!!fields, + ... + )) +} + +#' Expand an object list into a tib_df spec +#' +#' @param ... Additional arguments passed to `tib_fn`. +#' @param tib_fn (`function`) The tib constructor to wrap the fields in; +#' defaults to [tib_df()]. +#' @inheritParams .shared-params +#' @returns A [tib_df()] spec, with `.names_to` set when `value` is named and +#' non-empty. +#' @keywords internal +.guess_object_field_spec_expand_fields_df <- function( + value, + empty_list_unspecified, + simplify_list, + local_env, + ..., + tib_fn = tib_df +) { + .guess_object_field_spec_expand_fields( + value, + empty_list_unspecified, + simplify_list, + local_env, + tib_fn = tib_fn, + .names_to = if (rlang::is_named(value) && !is_empty(value)) ".names", + ... + ) +} + +#' Guess the spec for an object list field +#' +#' @inheritParams .shared-params +#' @returns A [tib_df()] spec keyed by `name`. +#' @keywords internal +.guess_object_field_spec_object_list <- function( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) { + .guess_object_field_spec_expand_fields_df( + value, + empty_list_unspecified, + simplify_list, + local_env, + .key = name + ) +} + +#' Guess the spec for a single field in an object list +#' +#' @inheritParams .shared-params +#' @returns A tib field spec. +#' @keywords internal +.guess_object_list_field_spec <- function( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) { + ptype_result <- .get_ptype_common(value, empty_list_unspecified) + if (!ptype_result$has_common_ptype) { + return(tib_variant(name)) + } + + ptype <- ptype_result$ptype + if (is.null(ptype)) { + return(tib_unspecified(name)) + } + ptype_type <- .tib_type_of(ptype, name, other = FALSE) + if (ptype_type == "vector") { + return(.guess_object_list_field_spec_vector( + value, + name, + ptype, + ptype_result$had_empty_lists, + local_env + )) + } + if (ptype_type == "df") { + cli::cli_abort("A list of dataframes is not yet supported.") + } + + # every element is a list or NULL at this point + if (all(vctrs::list_sizes(value) == 0) || .list_is_list_of_null(value)) { + return(tib_unspecified(name)) + } + + value_flat <- .vec_flatten(value, list(), name_spec = NULL) + if (.is_list_of_object_lists(value)) { + return(.guess_object_field_spec_object_list( + value_flat, + name, + empty_list_unspecified, + simplify_list, + local_env + )) + } + + if (!simplify_list) { + return(.guess_object_list_field_spec_dont_simplify( + value, + name, + empty_list_unspecified, + simplify_list, + local_env + )) + } + + ptype_result <- .get_ptype_common(value_flat, empty_list_unspecified) + if (ptype_result$has_common_ptype && .is_field_scalar(value_flat)) { + return(.guess_object_list_field_spec_flat_to_vector( + value_flat, + name, + ptype_result + )) + } + + if (.is_object_list(value)) { + return(.guess_object_list_field_spec_object_list_row( + value, + name, + empty_list_unspecified, + simplify_list, + local_env + )) + } + + return(tib_variant(name)) +} + +#' Guess the spec for a vector-typed field in an object list +#' +#' @param had_empty_lists (`logical(1)` or `NULL`) Whether empty lists were +#' dropped when computing the common ptype. +#' @inheritParams .shared-params +#' @returns A [tib_scalar()] or [tib_vector()] spec. +#' @keywords internal +.guess_object_list_field_spec_vector <- function( + value, + name, + ptype, + had_empty_lists, + local_env +) { + is_scalar <- .is_field_scalar(value) + if (!is_scalar) { + .mark_empty_list_argument(is_true(had_empty_lists), local_env) + } + .tib_scalar_or_vector_spec(name, ptype, is_scalar) +} + +#' Guess the spec for a list field without list simplification +#' +#' @inheritParams .shared-params +#' @returns A tib field spec. +#' @keywords internal +.guess_object_list_field_spec_dont_simplify <- function( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) { + if (.is_object_list(value)) { + return( + .guess_object_list_field_spec_object_list_row( + value, + name, + empty_list_unspecified, + simplify_list, + local_env + ) + ) + } + return(tib_variant(name)) +} + +#' Guess a vector spec from a flat list of field values +#' +#' @param value_flat (`list`) The flattened values of a list field. +#' @param ptype_result (`list`) The result of [.get_ptype_common()] applied to +#' `value_flat`. +#' @inheritParams .shared-params +#' @returns A [tib_vector()] spec. +#' @keywords internal +.guess_object_list_field_spec_flat_to_vector <- function( + value_flat, + name, + ptype_result +) { + return(tib_vector( + name, + ptype_result$ptype, + .input_form = .tib_vector_input_form(value_flat) + )) +} + +#' Guess a row spec from an object list field +#' +#' @inheritParams .shared-params +#' @returns A [tib_row()] spec. +#' @keywords internal +.guess_object_list_field_spec_object_list_row <- function( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) { + return( + .guess_object_field_spec_expand_fields( + value, + empty_list_unspecified, + simplify_list, + local_env, + tib_fn = tib_row, + .key = name + ) + ) +} + +#' Guess field specs for an object list +#' +#' @param object_list (`list`) A list of named lists (objects) whose fields +#' are to be guessed. +#' @inheritParams .shared-params +#' @returns A named list of tib field specs. +#' @keywords internal +.guess_object_list_spec <- function( + object_list, + empty_list_unspecified, + simplify_list, + local_env +) { + required <- .get_required(object_list) + # need to remove empty elements for `purrr::transpose()` to work. + object_list <- vctrs::list_drop_empty(object_list) + x_t <- purrr::list_transpose( + unname(object_list), + template = names(required), + simplify = FALSE + ) + fields <- purrr::map2( + x_t, + names(required), + \(value, name) { + .guess_object_list_field_spec( + value, + name, + empty_list_unspecified, + simplify_list, + local_env + ) + } + ) + .update_required_fields(fields, required) +} + +#' Determine which fields are required in an object list +#' +#' @param sample_size (`integer(1)`) Maximum number of records to sample when +#' `x` is large. +#' @inheritParams .shared-params +#' @returns A named logical vector indicating which fields are present in every +#' element of `x`. +#' @keywords internal +.get_required <- function(x, sample_size = 10e3) { + n <- vctrs::vec_size(x) + x <- unname(x) + if (n > sample_size) { + n <- sample_size + x <- vctrs::vec_slice(x, sample(n, sample_size)) + } + names_count <- vctrs::vec_count( + vctrs::list_unchop(lapply(x, names), ptype = character()), + "location" + ) + empty_loc <- lengths(x) == 0L + if (any(empty_loc)) { + rlang::rep_named(names_count$key, FALSE) + } else { + rlang::set_names(names_count$count == n, names_count$key) + } +} + +#' Is every element of a field scalar? +#' +#' @inheritParams .shared-params +#' @returns `TRUE` if every element has size 1 or is `NULL`, `FALSE` otherwise. +#' @keywords internal +.is_field_scalar <- function(value) { + sizes <- vctrs::list_sizes(value) + if (any(sizes > 1)) { + return(FALSE) + } + + # early exit for performance + if (!any(sizes == 0)) { + return(TRUE) + } + + # check that all elements are `NULL` + size_0_is_null <- vctrs::vec_detect_missing(value[sizes == 0]) + all(size_0_is_null) +} + +#' Update the required status of field specs +#' +#' @param fields (`list`) A named list of tib field specs. +#' @param required (`logical`) A named logical vector of required statuses, as +#' returned by [.get_required()]. +#' @returns `fields` with the `$required` component of each spec updated. +#' @keywords internal +.update_required_fields <- function(fields, required) { + for (field_name in names(required)) { + fields[[field_name]]$required <- required[[field_name]] + } + fields +} diff --git a/R/guess_tspec_utils.R b/R/guess_tspec_utils.R new file mode 100644 index 00000000..336cae8e --- /dev/null +++ b/R/guess_tspec_utils.R @@ -0,0 +1,143 @@ +#' Convert POSIXlt to POSIXct +#' +#' @inheritParams .shared-params +#' @returns An object of class `POSIXct` if the input is `POSIXlt` (to be in +#' line with ), otherwise the +#' input unchanged. +#' @keywords internal +.cast_posixlt_ptype <- function(x) { + if (inherits(x, "POSIXlt")) { + return(vctrs::vec_cast(x, vctrs::new_datetime())) + } + x +} + +#' Remove empty lists from an object +#' +#' @inheritParams .shared-params +#' @returns The input object with empty lists removed. If any were removed, the +#' returned object has an attribute `had_empty_lists` set to `TRUE`. +#' @keywords internal +.drop_empty_lists <- function(x) { + # TODO this could be implement in C for performance + # + # For performance reasons don't check for every single element if it is an + # empty list. Instead, only look at the ones with vec size 0. + empty_flag <- vctrs::list_sizes(x) == 0 + empty_list_flag <- purrr::map_lgl(x[empty_flag], ~ identical(.x, list())) + empty_flag[empty_flag] <- empty_list_flag + if (any(empty_flag)) { + x <- x[!empty_flag] + attr(x, "had_empty_lists") <- TRUE + } + x +} + +#' Is the object unspecified? +#' +#' @inheritParams .shared-params +#' @returns `TRUE` if the object has class `"vctrs_unspecified"`, `FALSE` +#' otherwise. +#' @keywords internal +.is_unspecified <- function(x) { + inherits(x, "vctrs_unspecified") +} + +#' Is the object a vector? +#' +#' @inheritParams .shared-params +#' @returns `TRUE` if the object is a non-list vector, `FALSE` otherwise. +#' @keywords internal +.is_vec <- function(x) { + # `obj_is_vector()` considers `list()` to be a vector but we don't + vctrs::obj_is_vector(x) && !is.list(x) +} + +#' Find the common ptype of a list of objects +#' +#' @inheritParams .shared-params +#' @returns A list with component `has_common_ptype` (`TRUE` if so, `FALSE` +#' otherwise) and optional components `ptype` (an object representing the +#' common `ptype`, if there is one) and `had_empty_lists` (`TRUE` if +#' `empty_list_unspecified` is `TRUE` and the `x` input had such empty lists). +#' @keywords internal +.get_ptype_common <- function(x, empty_list_unspecified) { + rlang::try_fetch( + { + if (empty_list_unspecified) { + x <- .drop_empty_lists(x) + } + ptype <- vctrs::vec_ptype_common(!!!x) + list( + has_common_ptype = TRUE, + ptype = .cast_posixlt_ptype(ptype), + had_empty_lists = attr(x, "had_empty_lists", exact = TRUE) + ) + }, + vctrs_error_incompatible_type = function(cnd) { + list(has_common_ptype = FALSE) + }, + vctrs_error_scalar_type = function(cnd) { + list(has_common_ptype = FALSE) + } + ) +} + +#' Mark that the empty list argument was used +#' +#' @param used_empty_list_arg (`logical(1)`) Whether any empty lists were +#' dropped during ptype detection due to `empty_list_unspecified`. +#' @inheritParams .shared-params +#' @returns Called for its side effect of setting `local_env$empty_list_used` to +#' `TRUE` when `used_empty_list_arg` is `TRUE`. +#' @keywords internal +.mark_empty_list_argument <- function(used_empty_list_arg, local_env) { + if (is_true(used_empty_list_arg)) { + local_env$empty_list_used <- TRUE + } +} + +#' Read whether the empty list argument was used +#' +#' @inheritParams .shared-params +#' @returns `TRUE` if `local_env$empty_list_used` is `TRUE`, `FALSE` otherwise. +#' @keywords internal +.read_empty_list_argument <- function(local_env) { + rlang::is_true(local_env$empty_list_used) +} + +#' Determine the tib type of an object +#' +#' @param other (`logical(1)`) If `TRUE`, return `"other"` for unrecognized +#' types rather than throwing an error. +#' @inheritParams .shared-params +#' @returns One of `"df"`, `"list"`, `"vector"`, or `"other"`. +#' @keywords internal +.tib_type_of <- function(x, name, other) { + if (is.data.frame(x)) { + "df" + } else if (vctrs::obj_is_list(x)) { + "list" + } else if (vctrs::vec_is(x)) { + "vector" + } else { + if (!other) { + msg <- c( + "Column {name} must be a dataframe, a list, or a vector.", + x = "Column {name} has classes {.cls class(x)}." + ) + cli::cli_abort(msg, .internal = TRUE) + } + "other" + } +} + +#' Get the ptype of an object +#' +#' @inheritParams .shared-params +#' @returns The ptype of `x`, with `POSIXlt` coerced to `POSIXct`. +#' @keywords internal +.tib_ptype <- function(x) { + ptype <- vctrs::vec_ptype(x) + .cast_posixlt_ptype(ptype) +} diff --git a/R/guess_utils.R b/R/guess_utils.R deleted file mode 100644 index 1ee7c469..00000000 --- a/R/guess_utils.R +++ /dev/null @@ -1,90 +0,0 @@ -is_vec <- function(x) { - # `vec_is()` considers `list()` to be a vector but we don't - if (vctrs::vec_is_list(x)) { - return(FALSE) - } - - vctrs::vec_is(x) -} - -get_ptype_common <- function(x, empty_list_unspecified) { - try_fetch( - { - if (empty_list_unspecified) { - x <- drop_empty_lists(x) - } - - ptype <- vctrs::vec_ptype_common(!!!x) - list( - has_common_ptype = TRUE, - ptype = special_ptype_handling(ptype), - had_empty_lists = x %@% had_empty_lists - ) - }, - vctrs_error_incompatible_type = function(cnd) { - list(has_common_ptype = FALSE) - }, - vctrs_error_scalar_type = function(cnd) { - list(has_common_ptype = FALSE) - } - ) -} - -drop_empty_lists <- function(x) { - # TODO this could be implement in C for performance - # for performance reasons don't check for every single element if it is - # an empty list. Instead, only look at the ones with vec size 0. - empty_flag <- vctrs::list_sizes(x) == 0 - empty_list_flag <- purrr::map_lgl(x[empty_flag], ~ identical(.x, list())) - empty_flag[empty_flag] <- empty_list_flag - if (any(empty_flag)) { - x <- x[!empty_flag] - x %@% had_empty_lists <- TRUE - } - - x -} - -special_ptype_handling <- function(ptype) { - # convert POSIXlt to POSIXct to be in line with vctrs - # https://github.com/r-lib/vctrs/issues/1576 - if (inherits(ptype, "POSIXlt")) { - return(vctrs::vec_cast(ptype, vctrs::new_datetime())) - } - - ptype -} - -tib_type_of <- function(x, name, other) { - if (is.data.frame(x)) { - "df" - } else if (vctrs::vec_is_list(x)) { - "list" - } else if (vctrs::vec_is(x)) { - "vector" - } else { - if (!other) { - msg <- c( - "Column {name} is not a dataframe, a list or a vector.", - i = "Instead it has classes {.cls class(x)}." - ) - cli::cli_abort(msg, .internal = TRUE) - } - "other" - } -} - -tib_ptype <- function(x) { - ptype <- vctrs::vec_ptype(x) - special_ptype_handling(ptype) -} - -is_unspecified <- function(x) { - inherits(x, "vctrs_unspecified") -} - -mark_empty_list_argument <- function(used_empty_list_arg) { - if (is_true(used_empty_list_arg)) { - options(tibblify.used_empty_list_arg = TRUE) - } -} diff --git a/R/parse_open_api.R b/R/parse_open_api.R index 5497c673..9c63babd 100644 --- a/R/parse_open_api.R +++ b/R/parse_open_api.R @@ -611,7 +611,7 @@ handle_one_of_tspec <- function(schema, openapi_spec) { ) } -if (is_installed("memoise")) { +if (rlang::is_installed("memoise")) { parse_schema_memoised <- memoise::memoise( parse_schema, omit_args = "openapi_spec" diff --git a/R/shape_utils.R b/R/shape_utils.R index de81bf67..c239609c 100644 --- a/R/shape_utils.R +++ b/R/shape_utils.R @@ -1,121 +1,101 @@ -is_object <- function(x) { +#' Is x an object? +#' +#' @inheritParams .shared-params +#' @returns (`logical(1)`) `TRUE` if `x` is an object (a fully named list with +#' unique names), `FALSE` otherwise. +#' @keywords internal +.is_object <- function(x) { .Call(ffi_is_object, x) } -should_guess_object <- function(x) { - # TODO upper limit on width of object? - .Call(ffi_is_object, x) +#' Is x a list of objects? +#' +#' @inheritParams .shared-params +#' @returns (`logical(1)`) `TRUE` if `x` is a list of objects, `FALSE` +#' otherwise. +#' @keywords internal +.is_object_list <- function(x) { + .Call(ffi_is_object_list, x) } -is_object_list <- function(x) { - .Call(ffi_is_object_list, x) +#' bort if `x` is not a list of objects +#' +#' @inheritParams .shared-params +#' @returns `x` (invisibly). Called for side effect. +#' @keywords internal +.check_object_list <- function(x, arg = caller_arg(x), call = caller_env()) { + if (!.is_object_list(x)) { + cli::cli_abort( + "{.arg {arg}} must be a list of objects.", + class = c( + "tibblify-error-not_object_list", + "tibblify-error", + "tibblify-condition" + ), + call = call + ) + } + invisible(x) } -is_list_of_object_lists <- function(x) { +#' Is x a list of object lists? +#' +#' @inheritParams .shared-params +#' @returns (`logical(1)`) `TRUE` if every non-`NULL` element of `x` is a list +#' of objects, `FALSE` otherwise. +#' @keywords internal +.is_list_of_object_lists <- function(x) { for (x_i in x) { - if (!is_object_list(x_i) && !is.null(x_i)) { + if (!.is_object_list(x_i) && !is.null(x_i)) { return(FALSE) } } - TRUE } -is_list_of_null <- function(x) { +#' Is x a list of NULLs? +#' +#' @inheritParams .shared-params +#' @returns (`logical(1)`) `TRUE` if every element of `x` is `NULL`, `FALSE` +#' otherwise. +#' @keywords internal +.is_list_of_null <- function(x) { .Call(ffi_is_null_list, x) } -list_is_list_of_null <- function(x) { +#' For each element, is it a list of NULLs? +#' +#' @inheritParams .shared-params +#' @returns (`logical`) A logical vector the same length as `x`, where each +#' element is `TRUE` if the corresponding element of `x` is itself a list of +#' `NULL`s. +#' @keywords internal +.list_is_list_of_null <- function(x) { .Call(ffi_list_is_list_null, x) } -should_guess_object_list <- function(x) { - if (!.Call(ffi_is_object_list, x)) { - return(FALSE) - } - - # TODO why is this here? - if (vctrs::vec_size(x) <= 1 && is_object(x)) { - return(FALSE) - } - - names_list <- lapply(x, names) - names_list <- vctrs::list_drop_empty(names_list) - n <- vctrs::vec_size(names_list) - - # TODO why is this here? - if (n == 0) { - return(FALSE) - } - - all_names <- vctrs::list_unchop( - names_list, - ptype = character(), - name_spec = "{inner}" - ) - names_count <- vctrs::vec_count(all_names, "location") - - n_min <- floor(0.9 * n) - any(names_count$count >= n_min) && mean(names_count$count >= 0.5) -} - -get_overview <- function(x) { - classes <- .compat_map_chr(x, ~ class(.x)[1]) - paste0(" ", names(classes), ": ", classes, collapse = "\n") -} - -guess_type <- function(x, arg = caller_arg(x), error_call = caller_env()) { - object <- is_object(x) - object_list <- is_object_list(x) - - if (object && object_list) { - if (!is_interactive()) { - # TODO should show name - msg <- c( - "Can't guess type of {.arg {arg}}.", - x = "It is both an object and a named list of objects.", - i = "Provide a spec to {.fn tibblify} or use {.fn guess_spec} interactively." - ) - cli::cli_abort(msg, call = error_call) - } - - return(choose_type(x, arg)) - } - - if (is_object(x)) { - return("object") - } - - if (is_object_list(x)) { - return("object list") - } - - abort_not_tibblifiable(x, arg, error_call) -} - -abort_not_tibblifiable <- function( +#' Abort when x is neither an object nor a list of objects +#' +#' @inheritParams .shared-params +#' @returns Nothing. Called for its side effect of throwing an error. +#' @keywords internal +.abort_not_tibblifiable <- function( x, arg = caller_arg(x), - error_call = caller_env() + call = caller_env() ) { - lgl_to_bullet <- function(x) { - bullets <- c("x", "v") - x2 <- as.integer(x) + 1L - bullets[x2] - } - object_cnd <- c( "An object", "is a list,", "is fully named,", "and has unique names." ) - object_bullets <- lgl_to_bullet(c( - vctrs::vec_is_list(x), - is_named2(x), + object_bullets <- .lgl_to_bullet(c( + vctrs::obj_is_list(x), + rlang::is_named2(x), anyDuplicated(names(x)) == 0 )) - o_msg <- set_names(object_cnd, c("", object_bullets)) + o_msg <- rlang::set_names(object_cnd, c("", object_bullets)) object_list_cnd <- c( "A list of objects is", @@ -123,12 +103,12 @@ abort_not_tibblifiable <- function( "a list and", "each element is {.code NULL} or an object." ) - object_list_bullets <- lgl_to_bullet(c( + object_list_bullets <- .lgl_to_bullet(c( is.data.frame(x), - vctrs::vec_is_list(x), - purrr::detect_index(x, ~ !is.null(.x) && !is_object(.x)) == 0 + vctrs::obj_is_list(x), + purrr::detect_index(x, ~ !is.null(.x) && !.is_object(.x)) == 0 )) - ol_msg <- set_names(object_list_cnd, c("", object_list_bullets)) + ol_msg <- rlang::set_names(object_list_cnd, c("", object_list_bullets)) msg <- c( "{.arg {arg}} is neither an object nor a list of objects.", @@ -136,27 +116,26 @@ abort_not_tibblifiable <- function( ol_msg ) - cli::cli_abort(msg, call = error_call) -} - -choose_type <- function(x, arg) { - n <- length(x) - if (n > 3) { - x <- x[1:3] - } - - # TODO nicer overview - overviews <- .compat_map_chr(x, get_overview) - x_overview <- paste0(names(x), "\n", overviews, collapse = "\n") - - msg <- c( - "{.arg {arg}} is an object and a named object list.", - "The structure of {.arg {arg}} is:" + cli::cli_abort( + msg, + class = c( + "tibblify-error-untibblifiable_object", + "tibblify-error", + "tibblify-condition" + ), + call = call ) - cli::cli_alert_info(msg) - inform(x_overview) +} - title <- cli::format_message("How do you want to parse {.arg {arg}}?") - choice <- utils::menu(c("object", "object list"), title = title) - return(choice) +#' Convert a logical vector to cli bullet symbols +#' +#' @param x (`logical`) A logical vector where `TRUE` maps to a check mark +#' bullet and `FALSE` to a cross bullet. +#' @returns (`character`) A character vector of cli bullet names (`"v"` or +#' `"x"`) the same length as `x`. +#' @keywords internal +.lgl_to_bullet <- function(x) { + bullets <- c("x", "v") + x2 <- as.integer(x) + 1L + bullets[x2] } diff --git a/R/should_inform_unspecified.R b/R/should_inform_unspecified.R new file mode 100644 index 00000000..20019e82 --- /dev/null +++ b/R/should_inform_unspecified.R @@ -0,0 +1,10 @@ +#' Determine whether to inform about unspecified fields in spec +#' +#' Wrapper around `getOption("tibblify.show_unspecified")` to return `TRUE` +#' unless the option is explicitly set to `FALSE`. +#' +#' @returns `FALSE` if the option is set to `FALSE`, `TRUE` otherwise. +#' @export +should_inform_unspecified <- function() { + !rlang::is_false(getOption("tibblify.show_unspecified")) +} diff --git a/R/spec_guess.R b/R/spec_guess.R deleted file mode 100644 index 6eb4d2f1..00000000 --- a/R/spec_guess.R +++ /dev/null @@ -1,142 +0,0 @@ -#' Guess the `tibblify()` specification -#' -#' Use `guess_tspec()` if you don't know the input type. -#' Use `guess_tspec_df()` if the input is a data frame or an object list. -#' Use `guess_tspec_objecte()` is the input is an object. -#' -#' @param x A nested list. -#' @param ... These dots are for future extensions and must be empty. -#' @param empty_list_unspecified Treat empty lists as unspecified? -#' @param simplify_list Should scalar lists be simplified to vectors? -#' @param inform_unspecified Inform about fields whose type could not be -#' determined? -#' @param call The execution environment of a currently running function, e.g. -#' `caller_env()`. The function will be mentioned in error messages as the -#' source of the error. See the `call` argument of [`rlang::abort()`] for more -#' information. -#' @param arg An argument name as a string. This argument will be mentioned in -#' error messages as the input that is at the origin of a problem. -#' -#' @return A specification object that can used in `tibblify()`. -#' @export -#' -#' @examples -#' guess_tspec(list(x = 1, y = "a")) -#' guess_tspec(list(list(x = 1), list(x = 2))) -#' -#' guess_tspec(gh_users) -guess_tspec <- function( - x, - ..., - empty_list_unspecified = FALSE, - simplify_list = FALSE, - inform_unspecified = should_inform_unspecified(), - call = rlang::caller_env() -) { - check_dots_empty() - check_bool(empty_list_unspecified, call = call) - check_bool(simplify_list, call = call) - check_bool(inform_unspecified, call = call) - - if (is.data.frame(x)) { - guess_tspec_df( - x, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list, - inform_unspecified = inform_unspecified, - call = call - ) - } else if (vctrs::vec_is_list(x)) { - guess_tspec_list( - x, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list, - inform_unspecified = inform_unspecified, - call = call - ) - } else { - stop_input_type( - x, - c("a data frame", "a list"), - arg = caller_arg(x), - call = call - ) - } -} - -guess_tspec_list <- function( - x, - ..., - empty_list_unspecified = FALSE, - simplify_list = FALSE, - inform_unspecified = should_inform_unspecified(), - arg = caller_arg(x), - call = current_call() -) { - check_dots_empty() - check_bool(empty_list_unspecified, call = call) - check_bool(simplify_list, call = call) - check_bool(inform_unspecified, call = call) - - check_list(x) - if (is_empty(x)) { - msg <- "{.arg {arg}} must not be empty." - cli::cli_abort(msg, call = call) - } - - # if `x` is both, an object list and an object, it should be very rare that - # it should be parsed as an object. - if (is_object_list(x)) { - spec <- guess_tspec_object_list( - x, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list, - call = call - ) - } else if (is_object(x)) { - spec <- guess_tspec_object( - x, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list, - call = call - ) - } else { - abort_not_tibblifiable(x, arg, call) - } - - if (inform_unspecified) { - .spec_inform_unspecified(spec) - } - - spec -} - -#' Determine whether to inform about unspecified fields in spec -#' -#' @description -#' Wrapper around `getOption("tibblify.show_unspecified")` that implements some -#' fall back logic if the option is unset. This returns: -#' -#' * `TRUE` if the option is set to `TRUE` -#' * `FALSE` if the option is set to `FALSE` -#' * `FALSE` if the option is unset and we appear to be running tests -#' * `TRUE` otherwise -#' -#' @return `TRUE` or `FALSE`. -#' @export -should_inform_unspecified <- function() { - opt <- getOption("tibblify.show_unspecified", NA) - if (is_true(opt)) { - TRUE - } else if (is_false(opt)) { - FALSE - } else if (is.na(opt) && is_testing()) { - FALSE - } else { - TRUE - } -} - -is_testing <- function() { - identical(Sys.getenv("TESTTHAT"), "true") -} diff --git a/R/spec_guess_df.R b/R/spec_guess_df.R deleted file mode 100644 index 6bba74df..00000000 --- a/R/spec_guess_df.R +++ /dev/null @@ -1,182 +0,0 @@ -#' @export -#' @rdname guess_tspec -guess_tspec_df <- function( - x, - ..., - empty_list_unspecified = FALSE, - simplify_list = FALSE, - inform_unspecified = should_inform_unspecified(), - call = rlang::current_call(), - arg = rlang::caller_arg(x) -) { - check_dots_empty() - check_bool(empty_list_unspecified, call = call) - check_bool(simplify_list, call = call) - check_bool(inform_unspecified, call = call) - - # FIXME should use global variable? - withr::local_options(list(tibblify.used_empty_list_arg = NULL)) - if (is.data.frame(x)) { - # TODO inform that `simplify_list` is not used for data frames - fields <- purrr::imap(x, col_to_spec, empty_list_unspecified) - spec <- tspec_df( - !!!fields, - .vector_allows_empty_list = is_true(getOption( - "tibblify.used_empty_list_arg" - )) - ) - } else { - check_list(x, arg = arg) - - if (!is_object_list(x)) { - msg <- "Not every element of {.arg {arg}} is an object." - cli::cli_abort(msg, call = call) - } - - spec <- guess_tspec_object_list( - x, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list, - call = call - ) - } - - if (inform_unspecified) { - .spec_inform_unspecified(spec) - } - spec -} - -col_to_spec <- function(col, name, empty_list_unspecified) { - col_type <- tib_type_of(col, name, other = FALSE) - - if (col_type == "df") { - fields_spec <- purrr::imap(col, col_to_spec, empty_list_unspecified) - return(tib_row(name, !!!fields_spec)) - } - - if (col_type == "vector") { - ptype <- tib_ptype(col) - if (is_unspecified(ptype)) { - return(tib_unspecified(name)) - } - - return(tib_scalar(name, ptype)) - } - - if (col_type != "list") { - cli::cli_abort( - "{.fn tib_type_of} returned an unexpected type", - .internal = TRUE - ) - } - - # `col` must be a list, so we need to check what its elements are - list_of_col <- vctrs::is_list_of(col) - if (list_of_col) { - ptype <- col %@% ptype - ptype_type <- tib_type_of(ptype, name, other = FALSE) - used_empty_list_argument <- FALSE - } else { - # TODO this could use sampling for performance - ptype_common <- get_ptype_common(col, empty_list_unspecified) - # no common ptype can be one of two reasons: - # * it contains non-vector elements - # * it contains incompatible types - # in both cases `tib_variant()` is used - if (!ptype_common$has_common_ptype) { - return(tib_variant(name)) - } - - ptype <- ptype_common$ptype - if (is.null(ptype)) { - # this means that every element is `NULL` - return(tib_unspecified(name)) - } - - ptype_type <- tib_type_of(ptype, name, other = FALSE) - used_empty_list_argument <- ptype_common$had_empty_lists - } - - # At this point each element has type `ptype_type` - # TODO should this care about names? - if (ptype_type == "vector") { - # TODO why? - mark_empty_list_argument(used_empty_list_argument) - return(tib_vector(name, ptype)) - } - - if (ptype_type == "df") { - out <- col_to_spec_df( - ptype, - col = col, - name = name, - list_of_col = list_of_col, - empty_list_unspecified = empty_list_unspecified - ) - return(out) - } - - if (ptype_type == "list") { - # TODO this could share code with other guessers - cli::cli_abort( - "List columns that only consists of lists are not supported yet." - ) - } - - if (col_type != "list") { - cli::cli_abort( - "{.fn get_col_type} returned an unexpected type", - .internal = TRUE - ) - } -} - -col_to_spec_df <- function( - ptype, - col, - name, - list_of_col, - empty_list_unspecified -) { - if (list_of_col) { - col_required <- TRUE - has_non_vec_cols <- purrr::detect_index( - ptype, - ~ !is_vec(.x) || is.data.frame(.x) - ) > - 0 - if (has_non_vec_cols) { - # non-vector columns need to be inspected further to actually get their - # specification - col_flat <- vctrs::list_unchop(col, ptype = ptype) - } else { - col_flat <- ptype - } - } else { - col_required <- df_guess_required(col, colnames(ptype)) - col_flat <- vctrs::list_unchop(col, ptype = ptype) - } - - fields_spec <- purrr::imap(col_flat, col_to_spec, empty_list_unspecified) - for (col in names(col_required)) { - fields_spec[[col]]$required <- col_required[[col]] - } - - tib_df(name, !!!fields_spec) -} - -df_guess_required <- function(df_list, all_cols) { - col_required <- rep_named(all_cols, TRUE) - for (col in all_cols) { - bad_idx <- purrr::detect_index( - df_list, - function(df) !col %in% colnames(df) - ) - col_required[[col]] <- bad_idx == 0 - } - - col_required -} - -globalVariables("had_empty_lists") diff --git a/R/spec_guess_object.R b/R/spec_guess_object.R deleted file mode 100644 index e6e05058..00000000 --- a/R/spec_guess_object.R +++ /dev/null @@ -1,184 +0,0 @@ -#' @rdname guess_tspec -#' @export -guess_tspec_object <- function( - x, - ..., - empty_list_unspecified = FALSE, - simplify_list = FALSE, - call = rlang::current_call() -) { - check_dots_empty() - withr::local_options(list(tibblify.used_empty_list_arg = NULL)) - if (is.data.frame(x)) { - msg <- c( - "{.arg x} must not be a dataframe.", - i = "Did you want to use {.fn guess_tspec_df} instead?" - ) - cli::cli_abort(msg, call = call) - } - check_list(x) - - check_object_names(x, call) - - if (is_empty(x)) { - return(tspec_object()) - } - - fields <- purrr::imap( - x, - function(value, name) { - guess_object_field_spec( - value, - name, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list - ) - } - ) - - tspec_object( - .vector_allows_empty_list = is_true(getOption( - "tibblify.used_empty_list_arg" - )), - !!!fields - ) -} - -guess_object_field_spec <- function( - value, - name, - empty_list_unspecified, - simplify_list -) { - if (is.null(value) || identical(unname(value), list())) { - return(tib_unspecified(name)) - } - - value_type <- tib_type_of(value, name, other = TRUE) - - if (value_type == "other") { - return(tib_variant(name)) - } - - if (value_type == "vector") { - ptype <- tib_ptype(value) - if (is_unspecified(ptype)) { - return(tib_unspecified(name)) - } - - if (vctrs::vec_size(value) == 1) { - return(tib_scalar(name, ptype)) - } else { - return(tib_vector(name, ptype)) - } - } - - if (value_type == "df") { - field_spec <- purrr::imap(value, col_to_spec, empty_list_unspecified) - return(tib_df(name, !!!field_spec)) - } - - if (value_type != "list") { - cli::cli_abort( - "{.fn tib_type_of} returned an unexpected type", - .internal = TRUE - ) # nocov - } - - if (is_list_of_null(value)) { - return(tib_unspecified(name)) - } - - object_list <- is_object_list(value) - object <- is_object(value) - if (object_list && object) { - # TODO should ask user what to do - } - - if (object_list) { - fields <- guess_object_list_spec( - value, - empty_list_unspecified, - simplify_list - ) - names_to <- if (is_named(value) && !is_empty(value)) ".names" - - spec <- tib_df(name, !!!fields, .names_to = names_to) - return(spec) - } - - if (simplify_list) { - input_form_result <- guess_vector_input_form(value, name) - if (input_form_result$can_simplify) { - return(input_form_result$tib_spec) - } - } - - if (object) { - fields <- purrr::imap( - value, - guess_object_field_spec, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list - ) - return(tib_row(name, !!!fields)) - } - - tib_variant(name) -} - -check_object_names <- function(x, call) { - if (!is_named2(x)) { - msg <- "{.arg x} must be fully named." - cli::cli_abort(msg, call = call) - } - - x_nms <- names(x) - if (vctrs::vec_duplicate_any(x_nms)) { - msg <- "Names of {.arg x} must be unique." - cli::cli_abort(msg, call = call) - } -} - -guess_vector_input_form <- function(value, name) { - ptype_result <- get_ptype_common(value, empty_list_unspecified = FALSE) - if (!ptype_result$has_common_ptype) { - return(list(can_simplify = FALSE)) - } - - ptype <- ptype_result$ptype - if (is.null(ptype)) { - if (is_named(value)) { - return(list(can_simplify = FALSE)) - } - - tib_spec <- tib_unspecified(name, .required = TRUE) - return(list(can_simplify = TRUE, tib_spec = tib_spec)) - } - - if (!is_vec(ptype)) { - return(list(can_simplify = FALSE)) - } - - if (is_field_scalar(value)) { - if (is_named(value)) { - tib_spec <- tib_vector( - name, - ptype, - .required = TRUE, - .input_form = "object" - ) - } else { - tib_spec <- tib_vector( - name, - ptype, - .required = TRUE, - .input_form = "scalar_list" - ) - } - - return(list(can_simplify = TRUE, tib_spec = tib_spec)) - } - - list(can_simplify = TRUE, tib_spec = tib_variant(name, .required = TRUE)) -} diff --git a/R/spec_guess_object_list.R b/R/spec_guess_object_list.R deleted file mode 100644 index c0d198ea..00000000 --- a/R/spec_guess_object_list.R +++ /dev/null @@ -1,229 +0,0 @@ -# Guess the specification of an object list -# The caller has to make sure that `x` is really a list of objects! -guess_tspec_object_list <- function( - x, - ..., - empty_list_unspecified = FALSE, - simplify_list = FALSE, - arg = caller_arg(x), - call = current_call() -) { - check_dots_empty() - check_list(x) - - withr::local_options(list(tibblify.used_empty_list_arg = NULL)) - - fields <- guess_object_list_spec( - x, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list - ) - - tspec_df( - !!!fields, - .names_to = if (is_named(x)) ".names", - .vector_allows_empty_list = is_true(getOption( - "tibblify.used_empty_list_arg" - )) - ) -} - -guess_object_list_spec <- function( - object_list, - empty_list_unspecified, - simplify_list -) { - required <- get_required(object_list) - - # need to remove empty elements for `purrr::transpose()` to work... - object_list <- vctrs::list_drop_empty(object_list) - x_t <- purrr::list_transpose( - unname(object_list), - template = names(required), - simplify = FALSE - ) - - fields <- purrr::map2( - x_t, - names(required), - function(value, name) { - guess_object_list_field_spec( - value, - name, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list - ) - } - ) - - .update_required_fields(fields, required) -} - -.update_required_fields <- function(fields, required) { - for (field_name in names(required)) { - fields[[field_name]]$required <- required[[field_name]] - } - - fields -} - -guess_object_list_field_spec <- function( - value, - name, - empty_list_unspecified, - simplify_list -) { - ptype_result <- get_ptype_common(value, empty_list_unspecified) - - # no common ptype can be one of two reasons: - # * it contains non-vector elements - # * it contains incompatible types - # in both cases `tib_variant()` is used - if (!ptype_result$has_common_ptype) { - return(tib_variant(name)) - } - - # now we know that every element essentially has type `ptype` - ptype <- ptype_result$ptype - if (is.null(ptype)) { - return(tib_unspecified(name)) - } - - ptype_type <- tib_type_of(ptype, name, other = FALSE) - if (ptype_type == "vector") { - out <- guess_object_list_vector_spec( - value, - name, - ptype, - ptype_result$had_empty_lists - ) - return(out) - } - - if (ptype_type == "df") { - # TODO should this actually be supported? - # TODO fix error call? - cli::cli_abort("a list of dataframes is not yet supported") - } - - # every element is a list or NULL at this point - if (all(vctrs::list_sizes(value) == 0)) { - return(tib_unspecified(name)) - } - - if (list_is_list_of_null(value)) { - return(tib_unspecified(name)) - } - - object <- is_object_list(value) - object_list <- is_list_of_object_lists(value) - - if (object_list && object) { - # TODO return `tib_undecided(c("row", "df"))` - # choice <- user_choose_row_or_df( - # name, - # value_flat, - # empty_list_unspecified = empty_list_unspecified, - # simplify_list = simplify_list - # ) - - object <- FALSE - } - - value_flat <- .vec_flatten(value, list(), name_spec = NULL) - if (object_list) { - fields <- guess_object_list_spec( - value_flat, - empty_list_unspecified, - simplify_list - ) - names_to <- if (is_named(value_flat) && !is_empty(value_flat)) ".names" - - spec <- tib_df(name, !!!fields, .names_to = names_to) - return(spec) - } - - if (!simplify_list) { - if (object) { - fields <- guess_object_list_spec( - value, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list - ) - return(tib_row(name, !!!fields)) - } - - return(tib_variant(name)) - } - - ptype_result <- get_ptype_common(value_flat, empty_list_unspecified) - could_be_vector <- ptype_result$has_common_ptype && - is_field_scalar(value_flat) - - if (could_be_vector) { - if (is_named(value_flat)) { - return(tib_vector(name, ptype_result$ptype, .input_form = "object")) - } else { - return(tib_vector(name, ptype_result$ptype, .input_form = "scalar_list")) - } - } - - if (object) { - fields <- guess_object_list_spec( - value, - empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list - ) - return(tib_row(name, !!!fields)) - } - - tib_variant(name) -} - -guess_object_list_vector_spec <- function(value, name, ptype, had_empty_lists) { - if (is_field_scalar(value)) { - tib_scalar(name, ptype) - } else { - mark_empty_list_argument(is_true(had_empty_lists)) - tib_vector(name, ptype) - } -} - -get_required <- function(x, sample_size = 10e3) { - n <- vctrs::vec_size(x) - x <- unname(x) - if (n > sample_size) { - n <- sample_size - x <- vctrs::vec_slice(x, sample(n, sample_size)) - } - - all_names <- vctrs::list_unchop(lapply(x, names), ptype = character()) - names_count <- vctrs::vec_count(all_names, "location") - - empty_loc <- lengths(x) == 0L - if (any(empty_loc)) { - rep_named(names_count$key, FALSE) - } else { - set_names(names_count$count == n, names_count$key) - } -} - -is_field_scalar <- function(value) { - sizes <- vctrs::list_sizes(value) - if (any(sizes > 1)) { - return(FALSE) - } - - # early exit for performance - if (!any(sizes == 0)) { - return(TRUE) - } - - # check that all elements are `NULL` - size_0_is_null <- vctrs::vec_detect_missing(value[sizes == 0]) - all(size_0_is_null) -} - -is_field_row <- function(value) { - should_guess_object_list(value) -} diff --git a/R/spec_inform_unspecified.R b/R/spec_inform_unspecified.R index ede1c791..a9e95e92 100644 --- a/R/spec_inform_unspecified.R +++ b/R/spec_inform_unspecified.R @@ -78,3 +78,20 @@ unspecified_paths } + +#' Potentially inform users about unspecified fields +#' +#' @inheritParams tibblify +#' @inheritParams .shared-params +#' @returns The `spec` object. +#' @keywords internal +.maybe_inform_unspecified <- function( + spec, + inform_unspecified, + call = caller_env() +) { + if (inform_unspecified) { + .spec_inform_unspecified(spec, call = call) + } + spec +} diff --git a/R/tib_spec_basics.R b/R/tib_spec_basics.R index 5faf086c..f144526f 100644 --- a/R/tib_spec_basics.R +++ b/R/tib_spec_basics.R @@ -209,7 +209,7 @@ tib_scalar <- function( ptype_inner = deprecated(), transform = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) # Previously ptype_inner would have been auto-filled from ptype, so resolve # that. @@ -322,7 +322,7 @@ tib_vector <- function( values_to = deprecated(), names_to = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .ptype <- .deprecate_arg(.ptype, ptype) .required <- .deprecate_arg(.required, required) @@ -461,7 +461,7 @@ tib_unspecified <- function( key = deprecated(), required = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .tib_collector( diff --git a/R/tib_spec_class.R b/R/tib_spec_class.R index fb521bf2..0bae9c11 100644 --- a/R/tib_spec_class.R +++ b/R/tib_spec_class.R @@ -15,7 +15,7 @@ tib_lgl <- function( ptype_inner = deprecated(), transform = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -46,7 +46,7 @@ tib_int <- function( ptype_inner = deprecated(), transform = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -77,7 +77,7 @@ tib_dbl <- function( ptype_inner = deprecated(), transform = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -108,7 +108,7 @@ tib_chr <- function( ptype_inner = deprecated(), transform = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -139,7 +139,7 @@ tib_date <- function( ptype_inner = deprecated(), transform = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -168,7 +168,7 @@ tib_chr_date <- function( fill = deprecated(), format = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -210,7 +210,7 @@ tib_lgl_vec <- function( values_to = deprecated(), names_to = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -257,7 +257,7 @@ tib_int_vec <- function( values_to = deprecated(), names_to = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -304,7 +304,7 @@ tib_dbl_vec <- function( values_to = deprecated(), names_to = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -351,7 +351,7 @@ tib_chr_vec <- function( values_to = deprecated(), names_to = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) @@ -398,7 +398,7 @@ tib_date_vec <- function( values_to = deprecated(), names_to = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) diff --git a/R/tib_spec_other.R b/R/tib_spec_other.R index 6891bbb9..288bc051 100644 --- a/R/tib_spec_other.R +++ b/R/tib_spec_other.R @@ -15,7 +15,7 @@ tib_variant <- function( transform = deprecated(), elt_transform = deprecated() ) { - check_dots_empty() + rlang::check_dots_empty() .key <- .deprecate_arg(.key, key) .required <- .deprecate_arg(.required, required) .fill <- .deprecate_arg(.fill, fill) diff --git a/R/tibblify-package.R b/R/tibblify-package.R index c5d7c34e..fd7d3215 100644 --- a/R/tibblify-package.R +++ b/R/tibblify-package.R @@ -2,6 +2,7 @@ "_PACKAGE" ## usethis namespace: start +## Full rlang import required by R/import-standalone-*.R #' @import rlang #' @importFrom glue glue #' @importFrom lifecycle deprecated @@ -10,12 +11,10 @@ #' @importFrom rlang caller_arg #' @importFrom rlang caller_env #' @importFrom rlang check_bool -#' @importFrom rlang check_dots_empty #' @importFrom rlang check_string #' @importFrom rlang current_call #' @importFrom rlang current_env #' @importFrom rlang is_empty -#' @importFrom rlang is_named #' @importFrom rlang is_true #' @importFrom rlang list2 #' @importFrom vctrs list_unchop diff --git a/R/untibblify.R b/R/untibblify.R index 1d3c1a57..712cb7d7 100644 --- a/R/untibblify.R +++ b/R/untibblify.R @@ -24,7 +24,7 @@ untibblify <- function(x, spec = get_spec(x)) { if (is.data.frame(x)) { untibblify_df(x, spec, call) - } else if (vctrs::vec_is_list(x)) { + } else if (vctrs::obj_is_list(x)) { untibblify_list(x, spec, call) } else { cls <- class(x)[[1]] diff --git a/R/utils.R b/R/utils.R index 7cdab85e..ec7ef7e8 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,4 +1,4 @@ -check_list <- function( +.check_list <- function( x, ..., allow_null = FALSE, @@ -6,7 +6,7 @@ check_list <- function( call = caller_env() ) { if (!missing(x)) { - if (vctrs::vec_is_list(x)) { + if (vctrs::obj_is_list(x)) { return(invisible(NULL)) } if (allow_null && is.null(x)) { diff --git a/R/zzz.R b/R/zzz.R index 5941186c..e29eab09 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -5,7 +5,7 @@ vctrs_ns <- loadNamespace("vctrs") # Pass BOTH namespaces to the C initializer - .Call(tibblify_initialize, ns_env("tibblify"), vctrs_ns) + .Call(tibblify_initialize, rlang::ns_env("tibblify"), vctrs_ns) } # nocov end diff --git a/man/dot-abort_not_tibblifiable.Rd b/man/dot-abort_not_tibblifiable.Rd new file mode 100644 index 00000000..160b1971 --- /dev/null +++ b/man/dot-abort_not_tibblifiable.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shape_utils.R +\name{.abort_not_tibblifiable} +\alias{.abort_not_tibblifiable} +\title{Abort when x is neither an object nor a list of objects} +\usage{ +.abort_not_tibblifiable(x, arg = caller_arg(x), call = caller_env()) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{arg}{(\code{character(1)}) An argument name. This name will be mentioned in +error messages as the input that is at the origin of a problem.} + +\item{call}{(\code{environment}) The environment to use for error messages.} +} +\value{ +Nothing. Called for its side effect of throwing an error. +} +\description{ +Abort when x is neither an object nor a list of objects +} +\keyword{internal} diff --git a/man/dot-cast_posixlt_ptype.Rd b/man/dot-cast_posixlt_ptype.Rd new file mode 100644 index 00000000..92fcc28b --- /dev/null +++ b/man/dot-cast_posixlt_ptype.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_utils.R +\name{.cast_posixlt_ptype} +\alias{.cast_posixlt_ptype} +\title{Convert POSIXlt to POSIXct} +\usage{ +.cast_posixlt_ptype(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +An object of class \code{POSIXct} if the input is \code{POSIXlt} (to be in +line with \url{https://github.com/r-lib/vctrs/issues/1576}), otherwise the +input unchanged. +} +\description{ +Convert POSIXlt to POSIXct +} +\keyword{internal} diff --git a/man/dot-check_named.Rd b/man/dot-check_named.Rd new file mode 100644 index 00000000..005152ac --- /dev/null +++ b/man/dot-check_named.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.check_named} +\alias{.check_named} +\title{Abort for missing names} +\usage{ +.check_named(x, call) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{call}{(\code{environment}) The environment to use for error messages.} +} +\value{ +\code{NULL} (invisibly). +} +\description{ +Abort for missing names +} +\keyword{internal} diff --git a/man/dot-check_names_not_duplicated.Rd b/man/dot-check_names_not_duplicated.Rd new file mode 100644 index 00000000..940d5580 --- /dev/null +++ b/man/dot-check_names_not_duplicated.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.check_names_not_duplicated} +\alias{.check_names_not_duplicated} +\title{Abort for duplicate names} +\usage{ +.check_names_not_duplicated(x, call) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{call}{(\code{environment}) The environment to use for error messages.} +} +\value{ +\code{NULL} (invisibly). +} +\description{ +Abort for duplicate names +} +\keyword{internal} diff --git a/man/dot-check_not_df.Rd b/man/dot-check_not_df.Rd new file mode 100644 index 00000000..b47ad8ba --- /dev/null +++ b/man/dot-check_not_df.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.check_not_df} +\alias{.check_not_df} +\title{Abort if \code{x} is a data frame} +\usage{ +.check_not_df(x, call) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{call}{(\code{environment}) The environment to use for error messages.} +} +\value{ +\code{x} (invisibly). +} +\description{ +Abort if \code{x} is a data frame +} +\keyword{internal} diff --git a/man/dot-check_object_list.Rd b/man/dot-check_object_list.Rd new file mode 100644 index 00000000..0f9ee11b --- /dev/null +++ b/man/dot-check_object_list.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shape_utils.R +\name{.check_object_list} +\alias{.check_object_list} +\title{bort if \code{x} is not a list of objects} +\usage{ +.check_object_list(x, arg = caller_arg(x), call = caller_env()) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{arg}{(\code{character(1)}) An argument name. This name will be mentioned in +error messages as the input that is at the origin of a problem.} + +\item{call}{(\code{environment}) The environment to use for error messages.} +} +\value{ +\code{x} (invisibly). Called for side effect. +} +\description{ +bort if \code{x} is not a list of objects +} +\keyword{internal} diff --git a/man/dot-check_object_names.Rd b/man/dot-check_object_names.Rd new file mode 100644 index 00000000..d1d9bbed --- /dev/null +++ b/man/dot-check_object_names.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.check_object_names} +\alias{.check_object_names} +\title{Abort for missing or duplicate names} +\usage{ +.check_object_names(x, call) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{call}{(\code{environment}) The environment to use for error messages.} +} +\value{ +\code{NULL} (invisibly). +} +\description{ +Abort for missing or duplicate names +} +\keyword{internal} diff --git a/man/dot-col_guess_required.Rd b/man/dot-col_guess_required.Rd new file mode 100644 index 00000000..74cea750 --- /dev/null +++ b/man/dot-col_guess_required.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.col_guess_required} +\alias{.col_guess_required} +\title{Guess whether a field is required across a list of data frames} +\usage{ +.col_guess_required(col_name, df_list) +} +\arguments{ +\item{col_name}{(\code{character(1)}) The column name to check.} + +\item{df_list}{(\code{list}) A list of data frames.} +} +\value{ +(\code{logical(1)}) \code{TRUE} if \code{col_name} is present in every data frame, +\code{FALSE} otherwise. +} +\description{ +Guess whether a field is required across a list of data frames +} +\keyword{internal} diff --git a/man/dot-col_to_spec.Rd b/man/dot-col_to_spec.Rd new file mode 100644 index 00000000..0d4d9415 --- /dev/null +++ b/man/dot-col_to_spec.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.col_to_spec} +\alias{.col_to_spec} +\title{Convert a column to a tib field specification} +\usage{ +.col_to_spec(col, name, empty_list_unspecified, local_env) +} +\arguments{ +\item{col}{(\code{any}) A column from a data frame, which may be a vector, a +list, or a nested data frame.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A tib field specification. +} +\description{ +Convert a column to a tib field specification +} +\keyword{internal} diff --git a/man/dot-col_to_spec_df.Rd b/man/dot-col_to_spec_df.Rd new file mode 100644 index 00000000..9468fa8a --- /dev/null +++ b/man/dot-col_to_spec_df.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.col_to_spec_df} +\alias{.col_to_spec_df} +\title{Convert a df-typed list column to a tib_df specification} +\usage{ +.col_to_spec_df( + ptype, + col, + name, + list_of_col, + empty_list_unspecified, + local_env +) +} +\arguments{ +\item{ptype}{(\code{vector(0)}) A prototype of the desired output type of the +field.} + +\item{col}{(\code{any}) A column from a data frame, which may be a vector, a +list, or a nested data frame.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{list_of_col}{(\code{logical(1)}) Whether \code{col} is a \code{list_of()} column.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A \code{\link[=tib_df]{tib_df()}} specification. +} +\description{ +Delegates to \code{\link[=.list_of_col_to_spec_df]{.list_of_col_to_spec_df()}} or \code{\link[=.non_list_of_col_to_spec_df]{.non_list_of_col_to_spec_df()}} +based on whether \code{col} is a \code{list_of()} column. +} +\keyword{internal} diff --git a/man/dot-df_col_to_spec.Rd b/man/dot-df_col_to_spec.Rd new file mode 100644 index 00000000..63e79bfe --- /dev/null +++ b/man/dot-df_col_to_spec.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.df_col_to_spec} +\alias{.df_col_to_spec} +\title{Convert a nested data frame column to a tib_row specification} +\usage{ +.df_col_to_spec(col, name, empty_list_unspecified, local_env) +} +\arguments{ +\item{col}{(\code{any}) A column from a data frame, which may be a vector, a +list, or a nested data frame.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A \code{\link[=tib_row]{tib_row()}} specification. +} +\description{ +Convert a nested data frame column to a tib_row specification +} +\keyword{internal} diff --git a/man/dot-df_guess_required.Rd b/man/dot-df_guess_required.Rd new file mode 100644 index 00000000..ab0f2edb --- /dev/null +++ b/man/dot-df_guess_required.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.df_guess_required} +\alias{.df_guess_required} +\title{Guess whether each field is required in a df-typed list column} +\usage{ +.df_guess_required(fields_spec, col, ptype) +} +\arguments{ +\item{fields_spec}{(\code{list}) A named list of tib field specifications.} + +\item{col}{(\code{list}) A list column whose elements are data frames.} + +\item{ptype}{(\code{vector(0)}) A prototype of the desired output type of the +field.} +} +\value{ +\code{fields_spec} with \verb{$required} updated for each field. +} +\description{ +Guess whether each field is required in a df-typed list column +} +\keyword{internal} diff --git a/man/dot-drop_empty_lists.Rd b/man/dot-drop_empty_lists.Rd new file mode 100644 index 00000000..6214e30c --- /dev/null +++ b/man/dot-drop_empty_lists.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_utils.R +\name{.drop_empty_lists} +\alias{.drop_empty_lists} +\title{Remove empty lists from an object} +\usage{ +.drop_empty_lists(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +The input object with empty lists removed. If any were removed, the +returned object has an attribute \code{had_empty_lists} set to \code{TRUE}. +} +\description{ +Remove empty lists from an object +} +\keyword{internal} diff --git a/man/dot-get_ptype_common.Rd b/man/dot-get_ptype_common.Rd new file mode 100644 index 00000000..9463f06d --- /dev/null +++ b/man/dot-get_ptype_common.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_utils.R +\name{.get_ptype_common} +\alias{.get_ptype_common} +\title{Find the common ptype of a list of objects} +\usage{ +.get_ptype_common(x, empty_list_unspecified) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} +} +\value{ +A list with component \code{has_common_ptype} (\code{TRUE} if so, \code{FALSE} +otherwise) and optional components \code{ptype} (an object representing the +common \code{ptype}, if there is one) and \code{had_empty_lists} (\code{TRUE} if +\code{empty_list_unspecified} is \code{TRUE} and the \code{x} input had such empty lists). +} +\description{ +Find the common ptype of a list of objects +} +\keyword{internal} diff --git a/man/dot-get_required.Rd b/man/dot-get_required.Rd new file mode 100644 index 00000000..4fe2395a --- /dev/null +++ b/man/dot-get_required.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.get_required} +\alias{.get_required} +\title{Determine which fields are required in an object list} +\usage{ +.get_required(x, sample_size = 10000) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{sample_size}{(\code{integer(1)}) Maximum number of records to sample when +\code{x} is large.} +} +\value{ +A named logical vector indicating which fields are present in every +element of \code{x}. +} +\description{ +Determine which fields are required in an object list +} +\keyword{internal} diff --git a/man/dot-guess_object_field_spec.Rd b/man/dot-guess_object_field_spec.Rd new file mode 100644 index 00000000..7608ebda --- /dev/null +++ b/man/dot-guess_object_field_spec.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.guess_object_field_spec} +\alias{.guess_object_field_spec} +\title{Guess the field spec for a single object field} +\usage{ +.guess_object_field_spec( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A tib field specification. +} +\description{ +Dispatches to the appropriate helper based on the detected type of \code{value}. +} +\keyword{internal} diff --git a/man/dot-guess_object_field_spec_df.Rd b/man/dot-guess_object_field_spec_df.Rd new file mode 100644 index 00000000..ed135f15 --- /dev/null +++ b/man/dot-guess_object_field_spec_df.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.guess_object_field_spec_df} +\alias{.guess_object_field_spec_df} +\title{Guess the field spec for a data-frame-typed field} +\usage{ +.guess_object_field_spec_df(value, name, empty_list_unspecified, local_env) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A \code{tib_df} field specification. +} +\description{ +Guess the field spec for a data-frame-typed field +} +\keyword{internal} diff --git a/man/dot-guess_object_field_spec_expand_fields.Rd b/man/dot-guess_object_field_spec_expand_fields.Rd new file mode 100644 index 00000000..2638740d --- /dev/null +++ b/man/dot-guess_object_field_spec_expand_fields.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.guess_object_field_spec_expand_fields} +\alias{.guess_object_field_spec_expand_fields} +\title{Expand an object list into a tib spec} +\usage{ +.guess_object_field_spec_expand_fields( + value, + empty_list_unspecified, + simplify_list, + local_env, + ..., + tib_fn = tib_df, + fields_fn = .guess_object_list_spec +) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} + +\item{...}{Additional arguments passed to \code{tib_fn}.} + +\item{tib_fn}{(\code{function}) The tib constructor to wrap the fields in +(e.g. \code{\link[=tib_df]{tib_df()}} or \code{\link[=tib_row]{tib_row()}}).} + +\item{fields_fn}{(\code{function}) The function used to generate field specs from +\code{value}; defaults to \code{\link[=.guess_object_list_spec]{.guess_object_list_spec()}}.} +} +\value{ +A tib spec created by \code{tib_fn}. +} +\description{ +Expand an object list into a tib spec +} +\keyword{internal} diff --git a/man/dot-guess_object_field_spec_expand_fields_df.Rd b/man/dot-guess_object_field_spec_expand_fields_df.Rd new file mode 100644 index 00000000..14f184f0 --- /dev/null +++ b/man/dot-guess_object_field_spec_expand_fields_df.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.guess_object_field_spec_expand_fields_df} +\alias{.guess_object_field_spec_expand_fields_df} +\title{Expand an object list into a tib_df spec} +\usage{ +.guess_object_field_spec_expand_fields_df( + value, + empty_list_unspecified, + simplify_list, + local_env, + ..., + tib_fn = tib_df +) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} + +\item{...}{Additional arguments passed to \code{tib_fn}.} + +\item{tib_fn}{(\code{function}) The tib constructor to wrap the fields in; +defaults to \code{\link[=tib_df]{tib_df()}}.} +} +\value{ +A \code{\link[=tib_df]{tib_df()}} spec, with \code{.names_to} set when \code{value} is named and +non-empty. +} +\description{ +Expand an object list into a tib_df spec +} +\keyword{internal} diff --git a/man/dot-guess_object_field_spec_object.Rd b/man/dot-guess_object_field_spec_object.Rd new file mode 100644 index 00000000..980555f2 --- /dev/null +++ b/man/dot-guess_object_field_spec_object.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.guess_object_field_spec_object} +\alias{.guess_object_field_spec_object} +\title{Guess the field spec for a nested object field} +\usage{ +.guess_object_field_spec_object( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A \code{tib_row} field specification. +} +\description{ +Guess the field spec for a nested object field +} +\keyword{internal} diff --git a/man/dot-guess_object_field_spec_object_list.Rd b/man/dot-guess_object_field_spec_object_list.Rd new file mode 100644 index 00000000..4542b97c --- /dev/null +++ b/man/dot-guess_object_field_spec_object_list.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.guess_object_field_spec_object_list} +\alias{.guess_object_field_spec_object_list} +\title{Guess the spec for an object list field} +\usage{ +.guess_object_field_spec_object_list( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A \code{\link[=tib_df]{tib_df()}} spec keyed by \code{name}. +} +\description{ +Guess the spec for an object list field +} +\keyword{internal} diff --git a/man/dot-guess_object_field_spec_vector.Rd b/man/dot-guess_object_field_spec_vector.Rd new file mode 100644 index 00000000..cab2908c --- /dev/null +++ b/man/dot-guess_object_field_spec_vector.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.guess_object_field_spec_vector} +\alias{.guess_object_field_spec_vector} +\title{Guess the field spec for a vector-typed field} +\usage{ +.guess_object_field_spec_vector(value, name) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} +} +\value{ +A tib field specification. +} +\description{ +Guess the field spec for a vector-typed field +} +\keyword{internal} diff --git a/man/dot-guess_object_list_field_spec.Rd b/man/dot-guess_object_list_field_spec.Rd new file mode 100644 index 00000000..854d3f40 --- /dev/null +++ b/man/dot-guess_object_list_field_spec.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.guess_object_list_field_spec} +\alias{.guess_object_list_field_spec} +\title{Guess the spec for a single field in an object list} +\usage{ +.guess_object_list_field_spec( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A tib field spec. +} +\description{ +Guess the spec for a single field in an object list +} +\keyword{internal} diff --git a/man/dot-guess_object_list_field_spec_dont_simplify.Rd b/man/dot-guess_object_list_field_spec_dont_simplify.Rd new file mode 100644 index 00000000..075731d6 --- /dev/null +++ b/man/dot-guess_object_list_field_spec_dont_simplify.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.guess_object_list_field_spec_dont_simplify} +\alias{.guess_object_list_field_spec_dont_simplify} +\title{Guess the spec for a list field without list simplification} +\usage{ +.guess_object_list_field_spec_dont_simplify( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A tib field spec. +} +\description{ +Guess the spec for a list field without list simplification +} +\keyword{internal} diff --git a/man/dot-guess_object_list_field_spec_flat_to_vector.Rd b/man/dot-guess_object_list_field_spec_flat_to_vector.Rd new file mode 100644 index 00000000..a076e147 --- /dev/null +++ b/man/dot-guess_object_list_field_spec_flat_to_vector.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.guess_object_list_field_spec_flat_to_vector} +\alias{.guess_object_list_field_spec_flat_to_vector} +\title{Guess a vector spec from a flat list of field values} +\usage{ +.guess_object_list_field_spec_flat_to_vector(value_flat, name, ptype_result) +} +\arguments{ +\item{value_flat}{(\code{list}) The flattened values of a list field.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{ptype_result}{(\code{list}) The result of \code{\link[=.get_ptype_common]{.get_ptype_common()}} applied to +\code{value_flat}.} +} +\value{ +A \code{\link[=tib_vector]{tib_vector()}} spec. +} +\description{ +Guess a vector spec from a flat list of field values +} +\keyword{internal} diff --git a/man/dot-guess_object_list_field_spec_object_list_row.Rd b/man/dot-guess_object_list_field_spec_object_list_row.Rd new file mode 100644 index 00000000..5fd4bb4c --- /dev/null +++ b/man/dot-guess_object_list_field_spec_object_list_row.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.guess_object_list_field_spec_object_list_row} +\alias{.guess_object_list_field_spec_object_list_row} +\title{Guess a row spec from an object list field} +\usage{ +.guess_object_list_field_spec_object_list_row( + value, + name, + empty_list_unspecified, + simplify_list, + local_env +) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A \code{\link[=tib_row]{tib_row()}} spec. +} +\description{ +Guess a row spec from an object list field +} +\keyword{internal} diff --git a/man/dot-guess_object_list_field_spec_vector.Rd b/man/dot-guess_object_list_field_spec_vector.Rd new file mode 100644 index 00000000..5bf956a0 --- /dev/null +++ b/man/dot-guess_object_list_field_spec_vector.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.guess_object_list_field_spec_vector} +\alias{.guess_object_list_field_spec_vector} +\title{Guess the spec for a vector-typed field in an object list} +\usage{ +.guess_object_list_field_spec_vector( + value, + name, + ptype, + had_empty_lists, + local_env +) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{ptype}{(\code{vector(0)}) A prototype of the desired output type of the +field.} + +\item{had_empty_lists}{(\code{logical(1)} or \code{NULL}) Whether empty lists were +dropped when computing the common ptype.} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A \code{\link[=tib_scalar]{tib_scalar()}} or \code{\link[=tib_vector]{tib_vector()}} spec. +} +\description{ +Guess the spec for a vector-typed field in an object list +} +\keyword{internal} diff --git a/man/dot-guess_object_list_spec.Rd b/man/dot-guess_object_list_spec.Rd new file mode 100644 index 00000000..7f8a7b0c --- /dev/null +++ b/man/dot-guess_object_list_spec.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.guess_object_list_spec} +\alias{.guess_object_list_spec} +\title{Guess field specs for an object list} +\usage{ +.guess_object_list_spec( + object_list, + empty_list_unspecified, + simplify_list, + local_env +) +} +\arguments{ +\item{object_list}{(\code{list}) A list of named lists (objects) whose fields +are to be guessed.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A named list of tib field specs. +} +\description{ +Guess field specs for an object list +} +\keyword{internal} diff --git a/man/dot-guess_vector_input_form.Rd b/man/dot-guess_vector_input_form.Rd new file mode 100644 index 00000000..abbd3d4f --- /dev/null +++ b/man/dot-guess_vector_input_form.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.guess_vector_input_form} +\alias{.guess_vector_input_form} +\title{Guess whether a list field can be simplified to a vector spec} +\usage{ +.guess_vector_input_form(value, name) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} +} +\value{ +A list with: +\itemize{ +\item \code{can_simplify} (\code{logical(1)}): Whether the field can be simplified. +\item \code{tib_spec}: A tib field specification (present only when \code{can_simplify} +is \code{TRUE}). +} +} +\description{ +Guess whether a list field can be simplified to a vector spec +} +\keyword{internal} diff --git a/man/dot-guess_vector_input_form_field_scalar.Rd b/man/dot-guess_vector_input_form_field_scalar.Rd new file mode 100644 index 00000000..5fd38787 --- /dev/null +++ b/man/dot-guess_vector_input_form_field_scalar.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.guess_vector_input_form_field_scalar} +\alias{.guess_vector_input_form_field_scalar} +\title{Build a tib spec for a field-scalar input form} +\usage{ +.guess_vector_input_form_field_scalar(value, name, ptype) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{ptype}{(\code{vector(0)}) A prototype of the desired output type of the +field.} +} +\value{ +A list with \code{can_simplify = TRUE} and \code{tib_spec}, a \code{tib_vector} +field specification. +} +\description{ +Build a tib spec for a field-scalar input form +} +\keyword{internal} diff --git a/man/dot-guess_vector_input_form_null.Rd b/man/dot-guess_vector_input_form_null.Rd new file mode 100644 index 00000000..8931ae40 --- /dev/null +++ b/man/dot-guess_vector_input_form_null.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.guess_vector_input_form_null} +\alias{.guess_vector_input_form_null} +\title{Guess input form for a list field whose common ptype is \code{NULL}} +\usage{ +.guess_vector_input_form_null(value, name) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} + +\item{name}{(\code{character(1)}) The name of the field.} +} +\value{ +A list with: +\itemize{ +\item \code{can_simplify} (\code{logical(1)}): Whether the field can be simplified. +\item \code{tib_spec}: A tib field specification (present only when \code{can_simplify} +is \code{TRUE}). +} +} +\description{ +Guess input form for a list field whose common ptype is \code{NULL} +} +\keyword{internal} diff --git a/man/dot-imap_col_to_spec.Rd b/man/dot-imap_col_to_spec.Rd new file mode 100644 index 00000000..6ccb8b13 --- /dev/null +++ b/man/dot-imap_col_to_spec.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.imap_col_to_spec} +\alias{.imap_col_to_spec} +\title{Apply column-to-spec conversion across a data frame} +\usage{ +.imap_col_to_spec(col_list, empty_list_unspecified, local_env) +} +\arguments{ +\item{col_list}{(\code{list}) A named list of columns, typically a data frame.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A named list of tib field specifications. +} +\description{ +Apply column-to-spec conversion across a data frame +} +\keyword{internal} diff --git a/man/dot-imap_guess_object_field_spec.Rd b/man/dot-imap_guess_object_field_spec.Rd new file mode 100644 index 00000000..9589a1f4 --- /dev/null +++ b/man/dot-imap_guess_object_field_spec.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object.R +\name{.imap_guess_object_field_spec} +\alias{.imap_guess_object_field_spec} +\title{Map \code{.guess_object_field_spec} over a named list} +\usage{ +.imap_guess_object_field_spec( + x, + empty_list_unspecified, + simplify_list, + local_env +) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A named list of tib field specifications, one per element of \code{x}. +} +\description{ +Map \code{.guess_object_field_spec} over a named list +} +\keyword{internal} diff --git a/man/dot-is_field_scalar.Rd b/man/dot-is_field_scalar.Rd new file mode 100644 index 00000000..828b93c8 --- /dev/null +++ b/man/dot-is_field_scalar.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.is_field_scalar} +\alias{.is_field_scalar} +\title{Is every element of a field scalar?} +\usage{ +.is_field_scalar(value) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} +} +\value{ +\code{TRUE} if every element has size 1 or is \code{NULL}, \code{FALSE} otherwise. +} +\description{ +Is every element of a field scalar? +} +\keyword{internal} diff --git a/man/dot-is_list_of_null.Rd b/man/dot-is_list_of_null.Rd new file mode 100644 index 00000000..bd876198 --- /dev/null +++ b/man/dot-is_list_of_null.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shape_utils.R +\name{.is_list_of_null} +\alias{.is_list_of_null} +\title{Is x a list of NULLs?} +\usage{ +.is_list_of_null(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +(\code{logical(1)}) \code{TRUE} if every element of \code{x} is \code{NULL}, \code{FALSE} +otherwise. +} +\description{ +Is x a list of NULLs? +} +\keyword{internal} diff --git a/man/dot-is_list_of_object_lists.Rd b/man/dot-is_list_of_object_lists.Rd new file mode 100644 index 00000000..e06e11bf --- /dev/null +++ b/man/dot-is_list_of_object_lists.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shape_utils.R +\name{.is_list_of_object_lists} +\alias{.is_list_of_object_lists} +\title{Is x a list of object lists?} +\usage{ +.is_list_of_object_lists(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +(\code{logical(1)}) \code{TRUE} if every non-\code{NULL} element of \code{x} is a list +of objects, \code{FALSE} otherwise. +} +\description{ +Is x a list of object lists? +} +\keyword{internal} diff --git a/man/dot-is_object.Rd b/man/dot-is_object.Rd new file mode 100644 index 00000000..d9b2ca1f --- /dev/null +++ b/man/dot-is_object.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shape_utils.R +\name{.is_object} +\alias{.is_object} +\title{Is x an object?} +\usage{ +.is_object(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +(\code{logical(1)}) \code{TRUE} if \code{x} is an object (a fully named list with +unique names), \code{FALSE} otherwise. +} +\description{ +Is x an object? +} +\keyword{internal} diff --git a/man/dot-is_object_list.Rd b/man/dot-is_object_list.Rd new file mode 100644 index 00000000..f44fd630 --- /dev/null +++ b/man/dot-is_object_list.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shape_utils.R +\name{.is_object_list} +\alias{.is_object_list} +\title{Is x a list of objects?} +\usage{ +.is_object_list(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +(\code{logical(1)}) \code{TRUE} if \code{x} is a list of objects, \code{FALSE} +otherwise. +} +\description{ +Is x a list of objects? +} +\keyword{internal} diff --git a/man/dot-is_unspecified.Rd b/man/dot-is_unspecified.Rd new file mode 100644 index 00000000..5e6600c1 --- /dev/null +++ b/man/dot-is_unspecified.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_utils.R +\name{.is_unspecified} +\alias{.is_unspecified} +\title{Is the object unspecified?} +\usage{ +.is_unspecified(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +\code{TRUE} if the object has class \code{"vctrs_unspecified"}, \code{FALSE} +otherwise. +} +\description{ +Is the object unspecified? +} +\keyword{internal} diff --git a/man/dot-is_vec.Rd b/man/dot-is_vec.Rd new file mode 100644 index 00000000..2b9c9b31 --- /dev/null +++ b/man/dot-is_vec.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_utils.R +\name{.is_vec} +\alias{.is_vec} +\title{Is the object a vector?} +\usage{ +.is_vec(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +\code{TRUE} if the object is a non-list vector, \code{FALSE} otherwise. +} +\description{ +Is the object a vector? +} +\keyword{internal} diff --git a/man/dot-lgl_to_bullet.Rd b/man/dot-lgl_to_bullet.Rd new file mode 100644 index 00000000..a671708a --- /dev/null +++ b/man/dot-lgl_to_bullet.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shape_utils.R +\name{.lgl_to_bullet} +\alias{.lgl_to_bullet} +\title{Convert a logical vector to cli bullet symbols} +\usage{ +.lgl_to_bullet(x) +} +\arguments{ +\item{x}{(\code{logical}) A logical vector where \code{TRUE} maps to a check mark +bullet and \code{FALSE} to a cross bullet.} +} +\value{ +(\code{character}) A character vector of cli bullet names (\code{"v"} or +\code{"x"}) the same length as \code{x}. +} +\description{ +Convert a logical vector to cli bullet symbols +} +\keyword{internal} diff --git a/man/dot-list_col_to_spec.Rd b/man/dot-list_col_to_spec.Rd new file mode 100644 index 00000000..e9b1b76a --- /dev/null +++ b/man/dot-list_col_to_spec.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.list_col_to_spec} +\alias{.list_col_to_spec} +\title{Convert a list column to a tib field specification} +\usage{ +.list_col_to_spec(col, name, empty_list_unspecified, local_env) +} +\arguments{ +\item{col}{(\code{any}) A column from a data frame, which may be a vector, a +list, or a nested data frame.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A tib field specification. +} +\description{ +Inspects the elements of \code{col} to determine whether they share a common ptype +and dispatches to the appropriate spec builder. +} +\keyword{internal} diff --git a/man/dot-list_is_list_of_null.Rd b/man/dot-list_is_list_of_null.Rd new file mode 100644 index 00000000..b85577b3 --- /dev/null +++ b/man/dot-list_is_list_of_null.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shape_utils.R +\name{.list_is_list_of_null} +\alias{.list_is_list_of_null} +\title{For each element, is it a list of NULLs?} +\usage{ +.list_is_list_of_null(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +(\code{logical}) A logical vector the same length as \code{x}, where each +element is \code{TRUE} if the corresponding element of \code{x} is itself a list of +\code{NULL}s. +} +\description{ +For each element, is it a list of NULLs? +} +\keyword{internal} diff --git a/man/dot-list_of_col_to_spec_df.Rd b/man/dot-list_of_col_to_spec_df.Rd new file mode 100644 index 00000000..35b3933a --- /dev/null +++ b/man/dot-list_of_col_to_spec_df.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.list_of_col_to_spec_df} +\alias{.list_of_col_to_spec_df} +\title{Build field specs from a list_of df column} +\usage{ +.list_of_col_to_spec_df(col, ptype, empty_list_unspecified, local_env) +} +\arguments{ +\item{col}{(\code{any}) A column from a data frame, which may be a vector, a +list, or a nested data frame.} + +\item{ptype}{(\code{vector(0)}) A prototype of the desired output type of the +field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A named list of tib field specifications. +} +\description{ +Build field specs from a list_of df column +} +\keyword{internal} diff --git a/man/dot-mark_empty_list_argument.Rd b/man/dot-mark_empty_list_argument.Rd new file mode 100644 index 00000000..ef9ccb50 --- /dev/null +++ b/man/dot-mark_empty_list_argument.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_utils.R +\name{.mark_empty_list_argument} +\alias{.mark_empty_list_argument} +\title{Mark that the empty list argument was used} +\usage{ +.mark_empty_list_argument(used_empty_list_arg, local_env) +} +\arguments{ +\item{used_empty_list_arg}{(\code{logical(1)}) Whether any empty lists were +dropped during ptype detection due to \code{empty_list_unspecified}.} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +Called for its side effect of setting \code{local_env$empty_list_used} to +\code{TRUE} when \code{used_empty_list_arg} is \code{TRUE}. +} +\description{ +Mark that the empty list argument was used +} +\keyword{internal} diff --git a/man/dot-maybe_inform_unspecified.Rd b/man/dot-maybe_inform_unspecified.Rd new file mode 100644 index 00000000..d47bf994 --- /dev/null +++ b/man/dot-maybe_inform_unspecified.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/spec_inform_unspecified.R +\name{.maybe_inform_unspecified} +\alias{.maybe_inform_unspecified} +\title{Potentially inform users about unspecified fields} +\usage{ +.maybe_inform_unspecified(spec, inform_unspecified, call = caller_env()) +} +\arguments{ +\item{spec}{(\code{tspec}) A specification of how to convert \code{x}. Generated with +\code{\link[=tspec_df]{tspec_df()}}, \code{\link[=tspec_row]{tspec_row()}}, \code{\link[=tspec_object]{tspec_object()}}, \code{\link[=tspec_recursive]{tspec_recursive()}}, or +\code{\link[=guess_tspec]{guess_tspec()}}. If \code{spec} is \code{NULL} (the default), \code{guess_tspec(x, inform_unspecified = TRUE)} will be used to guess the \code{spec}.} + +\item{inform_unspecified}{(\code{logical(1)}) Inform about fields whose type could +not be determined?} + +\item{call}{(\code{environment}) The environment to use for error messages.} +} +\value{ +The \code{spec} object. +} +\description{ +Potentially inform users about unspecified fields +} +\keyword{internal} diff --git a/man/dot-non_list_of_col_to_spec_df.Rd b/man/dot-non_list_of_col_to_spec_df.Rd new file mode 100644 index 00000000..45edce13 --- /dev/null +++ b/man/dot-non_list_of_col_to_spec_df.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.non_list_of_col_to_spec_df} +\alias{.non_list_of_col_to_spec_df} +\title{Build field specs from a non-list_of df column} +\usage{ +.non_list_of_col_to_spec_df(col, ptype, empty_list_unspecified, local_env) +} +\arguments{ +\item{col}{(\code{any}) A column from a data frame, which may be a vector, a +list, or a nested data frame.} + +\item{ptype}{(\code{vector(0)}) A prototype of the desired output type of the +field.} + +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +A named list of tib field specifications with \code{required} set. +} +\description{ +Build field specs from a non-list_of df column +} +\keyword{internal} diff --git a/man/dot-pkg_abort.Rd b/man/dot-pkg_abort.Rd deleted file mode 100644 index 866945b0..00000000 --- a/man/dot-pkg_abort.Rd +++ /dev/null @@ -1,36 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/aaa-conditions.R -\name{.pkg_abort} -\alias{.pkg_abort} -\title{Raise a package-scoped error} -\usage{ -.pkg_abort( - message, - subclass, - call = caller_env(), - message_env = caller_env(), - ... -) -} -\arguments{ -\item{message}{(\code{character}) The message for the new error. Messages will be -formatted with \code{\link[cli:cli_bullets]{cli::cli_bullets()}}.} - -\item{subclass}{(\code{character}) Class(es) to assign to the error. Will be -prefixed by "\{package\}-error-".} - -\item{call}{(\code{environment}) The environment to use for error messages.} - -\item{message_env}{(\code{environment}) The execution environment to use to -evaluate variables in error messages.} - -\item{...}{Additional parameters passed to \code{\link[cli:cli_abort]{cli::cli_abort()}} and on to -\code{\link[rlang:abort]{rlang::abort()}}.} -} -\value{ -Does not return. -} -\description{ -Raise a package-scoped error -} -\keyword{internal} diff --git a/man/dot-read_empty_list_argument.Rd b/man/dot-read_empty_list_argument.Rd new file mode 100644 index 00000000..b7fc06d2 --- /dev/null +++ b/man/dot-read_empty_list_argument.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_utils.R +\name{.read_empty_list_argument} +\alias{.read_empty_list_argument} +\title{Read whether the empty list argument was used} +\usage{ +.read_empty_list_argument(local_env) +} +\arguments{ +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} +} +\value{ +\code{TRUE} if \code{local_env$empty_list_used} is \code{TRUE}, \code{FALSE} otherwise. +} +\description{ +Read whether the empty list argument was used +} +\keyword{internal} diff --git a/man/dot-shared-params.Rd b/man/dot-shared-params.Rd index 47fc0fea..2e45307e 100644 --- a/man/dot-shared-params.Rd +++ b/man/dot-shared-params.Rd @@ -4,6 +4,9 @@ \alias{.shared-params} \title{Shared parameters} \arguments{ +\item{arg}{(\code{character(1)}) An argument name. This name will be mentioned in +error messages as the input that is at the origin of a problem.} + \item{.call}{(\code{environment}) The environment to use for error messages.} \item{.children}{(\code{character(1)}) The name of the field that contains the @@ -12,17 +15,29 @@ children.} \item{.children_to}{(\code{character(1)}) The column name in which to store the children.} +\item{col}{(\code{any}) A column from a data frame, which may be a vector, a +list, or a nested data frame.} + \item{.elt_transform}{(\code{function} or \code{NULL}) A function to apply to each element before casting to \code{.ptype_inner}.} +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} + \item{.fill}{(\code{vector} or \code{NULL}) Optionally, a value to use if the field does not exist.} \item{.format}{(\code{character(1)} or \code{NULL}) Passed to the \code{format} argument of \code{\link[=as.Date]{as.Date()}}.} +\item{inform_unspecified}{(\code{logical(1)}) Inform about fields whose type could +not be determined?} + \item{.key}{(\code{character}) The path of names to the field in the object.} +\item{local_env}{(\code{environment}) A local environment used to track state +across recursive calls, such as whether empty lists were encountered.} + \item{name}{(\code{character(1)}) The name of the field.} \item{.ptype}{(\code{vector(0)}) A prototype of the desired output type of the @@ -32,6 +47,9 @@ field.} \item{.required}{(\code{logical(1)}) Throw an error if the field does not exist?} +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} + \item{spec_list}{(\code{list}) A list of specifications.} \item{tib_list}{(\code{list}) A list of tib fields.} @@ -39,6 +57,8 @@ field.} \item{.transform}{(\code{function} or \code{NULL}) A function to apply to the whole vector after casting to \code{.ptype_inner}.} +\item{value}{(\code{list}) An object list whose fields will be guessed.} + \item{.values_to}{(\code{character(1)} or \code{NULL}) For \code{NULL} (the default), the field is converted to a \code{.ptype} vector. If a string is provided, the field is converted to a tibble and the values go into the specified column.} diff --git a/man/dot-tib_ptype.Rd b/man/dot-tib_ptype.Rd new file mode 100644 index 00000000..52ce98b3 --- /dev/null +++ b/man/dot-tib_ptype.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_utils.R +\name{.tib_ptype} +\alias{.tib_ptype} +\title{Get the ptype of an object} +\usage{ +.tib_ptype(x) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} +} +\value{ +The ptype of \code{x}, with \code{POSIXlt} coerced to \code{POSIXct}. +} +\description{ +Get the ptype of an object +} +\keyword{internal} diff --git a/man/dot-tib_scalar_or_vector_spec.Rd b/man/dot-tib_scalar_or_vector_spec.Rd new file mode 100644 index 00000000..1a31569c --- /dev/null +++ b/man/dot-tib_scalar_or_vector_spec.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.tib_scalar_or_vector_spec} +\alias{.tib_scalar_or_vector_spec} +\title{Create a scalar or vector tib spec} +\usage{ +.tib_scalar_or_vector_spec(name, ptype, is_scalar) +} +\arguments{ +\item{name}{(\code{character(1)}) The name of the field.} + +\item{ptype}{(\code{vector(0)}) A prototype of the desired output type of the +field.} + +\item{is_scalar}{(\code{logical(1)}) If \code{TRUE}, return a \code{\link[=tib_scalar]{tib_scalar()}} spec, +otherwise a \code{\link[=tib_vector]{tib_vector()}} spec.} +} +\value{ +A \code{\link[=tib_scalar]{tib_scalar()}} or \code{\link[=tib_vector]{tib_vector()}} spec. +} +\description{ +Create a scalar or vector tib spec +} +\keyword{internal} diff --git a/man/dot-tib_type_of.Rd b/man/dot-tib_type_of.Rd new file mode 100644 index 00000000..0c16eacc --- /dev/null +++ b/man/dot-tib_type_of.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_utils.R +\name{.tib_type_of} +\alias{.tib_type_of} +\title{Determine the tib type of an object} +\usage{ +.tib_type_of(x, name, other) +} +\arguments{ +\item{x}{(\code{any}) The object to check.} + +\item{name}{(\code{character(1)}) The name of the field.} + +\item{other}{(\code{logical(1)}) If \code{TRUE}, return \code{"other"} for unrecognized +types rather than throwing an error.} +} +\value{ +One of \code{"df"}, \code{"list"}, \code{"vector"}, or \code{"other"}. +} +\description{ +Determine the tib type of an object +} +\keyword{internal} diff --git a/man/dot-tib_vector_input_form.Rd b/man/dot-tib_vector_input_form.Rd new file mode 100644 index 00000000..630566cd --- /dev/null +++ b/man/dot-tib_vector_input_form.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.tib_vector_input_form} +\alias{.tib_vector_input_form} +\title{Determine the vector input form of a value} +\usage{ +.tib_vector_input_form(value) +} +\arguments{ +\item{value}{(\code{list}) An object list whose fields will be guessed.} +} +\value{ +\code{"object"} if \code{value} is named, \code{"scalar_list"} otherwise. +} +\description{ +Determine the vector input form of a value +} +\keyword{internal} diff --git a/man/dot-update_required_fields.Rd b/man/dot-update_required_fields.Rd new file mode 100644 index 00000000..9bec913d --- /dev/null +++ b/man/dot-update_required_fields.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_object_utils.R +\name{.update_required_fields} +\alias{.update_required_fields} +\title{Update the required status of field specs} +\usage{ +.update_required_fields(fields, required) +} +\arguments{ +\item{fields}{(\code{list}) A named list of tib field specs.} + +\item{required}{(\code{logical}) A named logical vector of required statuses, as +returned by \code{\link[=.get_required]{.get_required()}}.} +} +\value{ +\code{fields} with the \verb{$required} component of each spec updated. +} +\description{ +Update the required status of field specs +} +\keyword{internal} diff --git a/man/dot-vector_col_to_spec.Rd b/man/dot-vector_col_to_spec.Rd new file mode 100644 index 00000000..e176d957 --- /dev/null +++ b/man/dot-vector_col_to_spec.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/guess_tspec_df.R +\name{.vector_col_to_spec} +\alias{.vector_col_to_spec} +\title{Convert a vector column to a tib scalar or unspecified specification} +\usage{ +.vector_col_to_spec(col, name) +} +\arguments{ +\item{col}{(\code{any}) A column from a data frame, which may be a vector, a +list, or a nested data frame.} + +\item{name}{(\code{character(1)}) The name of the field.} +} +\value{ +A \code{\link[=tib_scalar]{tib_scalar()}} or \code{\link[=tib_unspecified]{tib_unspecified()}} specification. +} +\description{ +Convert a vector column to a tib scalar or unspecified specification +} +\keyword{internal} diff --git a/man/guess_tspec.Rd b/man/guess_tspec.Rd index b4accd0a..a1d0abd8 100644 --- a/man/guess_tspec.Rd +++ b/man/guess_tspec.Rd @@ -1,10 +1,12 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/spec_guess.R, R/spec_guess_df.R, -% R/spec_guess_object.R +% Please edit documentation in R/guess_tspec.R, R/guess_tspec_df.R, +% R/guess_tspec_list.R, R/guess_tspec_object.R, R/guess_tspec_object_list.R \name{guess_tspec} \alias{guess_tspec} \alias{guess_tspec_df} +\alias{guess_tspec_list} \alias{guess_tspec_object} +\alias{guess_tspec_object_list} \title{Guess the \code{tibblify()} specification} \usage{ guess_tspec( @@ -26,41 +28,75 @@ guess_tspec_df( arg = rlang::caller_arg(x) ) +guess_tspec_list( + x, + ..., + empty_list_unspecified = FALSE, + simplify_list = FALSE, + inform_unspecified = should_inform_unspecified(), + arg = caller_arg(x), + call = current_call() +) + guess_tspec_object( x, ..., empty_list_unspecified = FALSE, simplify_list = FALSE, + inform_unspecified = should_inform_unspecified(), call = rlang::current_call() ) + +guess_tspec_object_list( + x, + ..., + empty_list_unspecified = FALSE, + simplify_list = FALSE, + inform_unspecified = should_inform_unspecified(), + arg = caller_arg(x), + call = current_call() +) } \arguments{ -\item{x}{A nested list.} +\item{x}{(\code{list} or \code{data.frame}) A nested list or a data frame.} \item{...}{These dots are for future extensions and must be empty.} -\item{empty_list_unspecified}{Treat empty lists as unspecified?} +\item{empty_list_unspecified}{(\code{logical(1)}) Treat empty lists as +unspecified?} -\item{simplify_list}{Should scalar lists be simplified to vectors?} +\item{simplify_list}{(\code{logical(1)}) Should scalar lists be simplified to +vectors?} -\item{inform_unspecified}{Inform about fields whose type could not be -determined?} +\item{inform_unspecified}{(\code{logical(1)}) Inform about fields whose type could +not be determined?} -\item{call}{The execution environment of a currently running function, e.g. -\code{caller_env()}. The function will be mentioned in error messages as the -source of the error. See the \code{call} argument of \code{\link[rlang:abort]{rlang::abort()}} for more -information.} +\item{call}{(\code{environment}) The environment to use for error messages.} -\item{arg}{An argument name as a string. This argument will be mentioned in +\item{arg}{(\code{character(1)}) An argument name. This name will be mentioned in error messages as the input that is at the origin of a problem.} } \value{ -A specification object that can used in \code{tibblify()}. +A specification object that can be used in \code{\link[=tibblify]{tibblify()}}. } \description{ -Use \code{guess_tspec()} if you don't know the input type. -Use \code{guess_tspec_df()} if the input is a data frame or an object list. -Use \code{guess_tspec_objecte()} is the input is an object. +\code{guess_tspec()} automatically dispatches to the other \verb{guess_tspec_*()} +functions based on the shape of the input. If you are unhappy with its +output, calling a specific \verb{guess_tspec_*()} function may yield better +results, or at least clearer error messages about why that type isn't +supported. +\itemize{ +\item Use \code{guess_tspec_df()} if the input is a data frame. +\item Use \code{guess_tspec_object()} if the input is an object (such as a JSON +object that has been read into R as a named list). +\item Use \code{guess_tspec_object_list()} if the input is a list of objects (such as +a JSON object that has been read into R as a list of named lists). +\item Use \code{guess_tspec_list()} if the input object is a list but you aren't sure +how it should be processed. +} + +See \code{vignette("supported-structures")} for a discussion of the input types +supported by tibblify. } \examples{ guess_tspec(list(x = 1, y = "a")) diff --git a/man/should_inform_unspecified.Rd b/man/should_inform_unspecified.Rd index e2722049..0e46ec78 100644 --- a/man/should_inform_unspecified.Rd +++ b/man/should_inform_unspecified.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/spec_guess.R +% Please edit documentation in R/should_inform_unspecified.R \name{should_inform_unspecified} \alias{should_inform_unspecified} \title{Determine whether to inform about unspecified fields in spec} @@ -7,15 +7,9 @@ should_inform_unspecified() } \value{ -\code{TRUE} or \code{FALSE}. +\code{FALSE} if the option is set to \code{FALSE}, \code{TRUE} otherwise. } \description{ -Wrapper around \code{getOption("tibblify.show_unspecified")} that implements some -fall back logic if the option is unset. This returns: -\itemize{ -\item \code{TRUE} if the option is set to \code{TRUE} -\item \code{FALSE} if the option is set to \code{FALSE} -\item \code{FALSE} if the option is unset and we appear to be running tests -\item \code{TRUE} otherwise -} +Wrapper around \code{getOption("tibblify.show_unspecified")} to return \code{TRUE} +unless the option is explicitly set to \code{FALSE}. } diff --git a/src/.gitignore b/src/.gitignore index 22034c46..2082b3c3 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1,3 +1,5 @@ *.o *.so *.dll +*.gcno +*.gcda diff --git a/tests/testthat/_snaps/guess_tspec.md b/tests/testthat/_snaps/guess_tspec.md new file mode 100644 index 00000000..c7bd4a64 --- /dev/null +++ b/tests/testthat/_snaps/guess_tspec.md @@ -0,0 +1,5 @@ +# guess_tspec errors informatively when x is neither df nor list + + Code + expect_error(guess_tspec("a")) + diff --git a/tests/testthat/_snaps/spec_guess_df.md b/tests/testthat/_snaps/guess_tspec_df.md similarity index 85% rename from tests/testthat/_snaps/spec_guess_df.md rename to tests/testthat/_snaps/guess_tspec_df.md index 5152771d..ff73a83e 100644 --- a/tests/testthat/_snaps/spec_guess_df.md +++ b/tests/testthat/_snaps/guess_tspec_df.md @@ -3,9 +3,9 @@ Code (expect_error(guess_tspec_df(list(a = 1)))) Output - + Error in `guess_tspec_df()`: - ! Not every element of `list(a = 1)` is an object. + ! `list(a = 1)` must be a list of objects. Code (expect_error(guess_tspec_df(1:3))) Output diff --git a/tests/testthat/_snaps/guess_tspec_list.md b/tests/testthat/_snaps/guess_tspec_list.md new file mode 100644 index 00000000..c0319ac8 --- /dev/null +++ b/tests/testthat/_snaps/guess_tspec_list.md @@ -0,0 +1,40 @@ +# guess_tspec_list errors informatively for empty input + + Code + (expect_error(guess_tspec_list(list()))) + Output + + Error in `guess_tspec_list()`: + ! `list()` must not be empty. + +# guess_tspec_list errors informatively for bad objects + + Code + (expect_error(guess_tspec_list(list(a = 1, 1)))) + Output + + Error in `guess_tspec_list()`: + ! `list(a = 1, 1)` is neither an object nor a list of objects. + An object + v is a list, + x is fully named, + v and has unique names. + A list of objects is + x a data frame or + v a list and + x each element is `NULL` or an object. + Code + (expect_error(guess_tspec_list(list(a = 1, a = 1)))) + Output + + Error in `guess_tspec_list()`: + ! `list(a = 1, a = 1)` is neither an object nor a list of objects. + An object + v is a list, + v is fully named, + x and has unique names. + A list of objects is + x a data frame or + v a list and + x each element is `NULL` or an object. + diff --git a/tests/testthat/_snaps/spec_guess.md b/tests/testthat/_snaps/guess_tspec_object.md similarity index 54% rename from tests/testthat/_snaps/spec_guess.md rename to tests/testthat/_snaps/guess_tspec_object.md index a0b55fc8..90225159 100644 --- a/tests/testthat/_snaps/spec_guess.md +++ b/tests/testthat/_snaps/guess_tspec_object.md @@ -1,260 +1,56 @@ -# checks input +# guess_tspec_object gives nice errors Code - expect_error(guess_tspec("a")) - ---- - + (expect_error(guess_tspec_object(tibble(a = 1)))) + Output + + Error in `guess_tspec_object()`: + ! `x` must not be a dataframe. + i Did you want to use `guess_tspec_df()` instead? Code - (expect_error(guess_tspec_list(list()))) + (expect_error(guess_tspec_object(1:3))) Output - Error in `guess_tspec_list()`: - ! `list()` must not be empty. + Error in `guess_tspec_object()`: + ! `x` must be a list, not an integer vector. --- Code - (expect_error(guess_tspec_list(list(a = 1, 1)))) + (expect_error(guess_tspec_object(list(1, a = 1)))) Output - Error in `guess_tspec_list()`: - ! `list(a = 1, 1)` is neither an object nor a list of objects. - An object - v is a list, - x is fully named, - v and has unique names. - A list of objects is - x a data frame or - v a list and - x each element is `NULL` or an object. + Error in `guess_tspec_object()`: + ! `x` must be fully named. Code - (expect_error(guess_tspec_list(list(a = 1, a = 1)))) + (expect_error(guess_tspec_object(list(a = 1, a = 1)))) Output - Error in `guess_tspec_list()`: - ! `list(a = 1, a = 1)` is neither an object nor a list of objects. - An object - v is a list, - v is fully named, - x and has unique names. - A list of objects is - x a data frame or - v a list and - x each element is `NULL` or an object. - -# can guess spec for discog - - Code - print(guess_tspec(discog)) - Output - tspec_df( - tib_int("instance_id"), - tib_chr("date_added"), - tib_row( - "basic_information", - tib_df( - "labels", - tib_chr("name"), - tib_chr("entity_type"), - tib_chr("catno"), - tib_chr("resource_url"), - tib_int("id"), - tib_chr("entity_type_name"), - ), - tib_int("year"), - tib_chr("master_url"), - tib_df( - "artists", - tib_chr("join"), - tib_chr("name"), - tib_chr("anv"), - tib_chr("tracks"), - tib_chr("role"), - tib_chr("resource_url"), - tib_int("id"), - ), - tib_int("id"), - tib_chr("thumb"), - tib_chr("title"), - tib_df( - "formats", - tib_variant("descriptions", required = FALSE), - tib_chr("text", required = FALSE), - tib_chr("name"), - tib_chr("qty"), - ), - tib_chr("cover_image"), - tib_chr("resource_url"), - tib_int("master_id"), - ), - tib_int("id"), - tib_int("rating"), - ) - -# can guess spec for gh_users - - Code - print(guess_tspec(gh_users)) - Output - tspec_df( - tib_chr("login"), - tib_int("id"), - tib_chr("avatar_url"), - tib_chr("gravatar_id"), - tib_chr("url"), - tib_chr("html_url"), - tib_chr("followers_url"), - tib_chr("following_url"), - tib_chr("gists_url"), - tib_chr("starred_url"), - tib_chr("subscriptions_url"), - tib_chr("organizations_url"), - tib_chr("repos_url"), - tib_chr("events_url"), - tib_chr("received_events_url"), - tib_chr("type"), - tib_lgl("site_admin"), - tib_chr("name"), - tib_chr("company"), - tib_chr("blog"), - tib_chr("location"), - tib_chr("email"), - tib_lgl("hireable"), - tib_chr("bio"), - tib_int("public_repos"), - tib_int("public_gists"), - tib_int("followers"), - tib_int("following"), - tib_chr("created_at"), - tib_chr("updated_at"), - ) - -# can guess spec for gh_repos - - Code - print(guess_tspec(gh_repos)) - Output - tspec_df( - tib_int("id"), - tib_chr("name"), - tib_chr("full_name"), - tib_row( - "owner", - tib_chr("login"), - tib_int("id"), - tib_chr("avatar_url"), - tib_chr("gravatar_id"), - tib_chr("url"), - tib_chr("html_url"), - tib_chr("followers_url"), - tib_chr("following_url"), - tib_chr("gists_url"), - tib_chr("starred_url"), - tib_chr("subscriptions_url"), - tib_chr("organizations_url"), - tib_chr("repos_url"), - tib_chr("events_url"), - tib_chr("received_events_url"), - tib_chr("type"), - tib_lgl("site_admin"), - ), - tib_lgl("private"), - tib_chr("html_url"), - tib_chr("description"), - tib_lgl("fork"), - tib_chr("url"), - tib_chr("forks_url"), - tib_chr("keys_url"), - tib_chr("collaborators_url"), - tib_chr("teams_url"), - tib_chr("hooks_url"), - tib_chr("issue_events_url"), - tib_chr("events_url"), - tib_chr("assignees_url"), - tib_chr("branches_url"), - tib_chr("tags_url"), - tib_chr("blobs_url"), - tib_chr("git_tags_url"), - tib_chr("git_refs_url"), - tib_chr("trees_url"), - tib_chr("statuses_url"), - tib_chr("languages_url"), - tib_chr("stargazers_url"), - tib_chr("contributors_url"), - tib_chr("subscribers_url"), - tib_chr("subscription_url"), - tib_chr("commits_url"), - tib_chr("git_commits_url"), - tib_chr("comments_url"), - tib_chr("issue_comment_url"), - tib_chr("contents_url"), - tib_chr("compare_url"), - tib_chr("merges_url"), - tib_chr("archive_url"), - tib_chr("downloads_url"), - tib_chr("issues_url"), - tib_chr("pulls_url"), - tib_chr("milestones_url"), - tib_chr("notifications_url"), - tib_chr("labels_url"), - tib_chr("releases_url"), - tib_chr("deployments_url"), - tib_chr("created_at"), - tib_chr("updated_at"), - tib_chr("pushed_at"), - tib_chr("git_url"), - tib_chr("ssh_url"), - tib_chr("clone_url"), - tib_chr("svn_url"), - tib_chr("homepage"), - tib_int("size"), - tib_int("stargazers_count"), - tib_int("watchers_count"), - tib_chr("language"), - tib_lgl("has_issues"), - tib_lgl("has_downloads"), - tib_lgl("has_wiki"), - tib_lgl("has_pages"), - tib_int("forks_count"), - tib_unspecified("mirror_url"), - tib_int("open_issues_count"), - tib_int("forks"), - tib_int("open_issues"), - tib_int("watchers"), - tib_chr("default_branch"), - ) + Error in `guess_tspec_object()`: + ! Names of `x` must be unique. -# can guess spec for got_chars (#83) +# guess_tspec_object informs about unspecified elements Code - spec + guess_tspec_object(x, inform_unspecified = TRUE) + Message + The spec contains 9 unspecified fields: + * blockNames + * events->description + * events->subjectCode + * events->subtitle + * performances->logo + * performances->name + * performances->seatCategories->areas->blockIds + * performances->seatMapImage + * subjectNames Output - tspec_df( - tib_chr("url"), - tib_int("id"), - tib_chr("name"), - tib_chr("gender"), - tib_chr("culture"), - tib_chr("born"), - tib_chr("died"), - tib_lgl("alive"), - tib_chr_vec("titles"), - tib_variant("aliases"), - tib_chr("father"), - tib_chr("mother"), - tib_chr("spouse"), - tib_variant("allegiances"), - tib_variant("books"), - tib_chr_vec("povBooks"), - tib_chr_vec("tvSeries"), - tib_chr_vec("playedBy"), - ) + object omitted -# can guess spec for citm_catalog +# guess_tspec_object can guess spec for citm_catalog Code - guess_tspec(x) + guess_tspec_object(x, simplify_list = FALSE) Output tspec_object( tib_row( @@ -338,124 +134,10 @@ ), ) ---- - - Code - guess_tspec_list(x, simplify_list = FALSE) - Output - tspec_object( - tib_row( - "areaNames", - tib_chr("205705993"), - tib_chr("205705994"), - tib_chr("205705995"), - ), - tib_row( - "audienceSubCategoryNames", - tib_chr("337100890"), - ), - tib_unspecified("blockNames"), - tib_df( - "events", - .names_to = ".names", - tib_unspecified("description"), - tib_int("id"), - tib_chr("logo"), - tib_chr("name"), - tib_int_vec("subTopicIds"), - tib_unspecified("subjectCode"), - tib_unspecified("subtitle"), - tib_int_vec("topicIds"), - ), - tib_df( - "performances", - tib_int("eventId"), - tib_int("id"), - tib_unspecified("logo"), - tib_unspecified("name"), - tib_df( - "prices", - tib_int("amount"), - tib_int("audienceSubCategoryId"), - tib_int("seatCategoryId"), - ), - tib_df( - "seatCategories", - tib_df( - "areas", - tib_int("areaId"), - tib_unspecified("blockIds"), - ), - tib_int("seatCategoryId"), - ), - tib_unspecified("seatMapImage"), - tib_dbl("start"), - tib_chr("venueCode"), - ), - tib_row( - "seatCategoryNames", - tib_chr("338937235"), - tib_chr("338937236"), - tib_chr("338937238"), - ), - tib_row( - "subTopicNames", - tib_chr("337184262"), - tib_chr("337184263"), - tib_chr("337184267"), - ), - tib_unspecified("subjectNames"), - tib_row( - "topicNames", - tib_chr("107888604"), - tib_chr("324846098"), - tib_chr("324846099"), - tib_chr("324846100"), - ), - tib_row( - "topicSubTopics", - tib_int_vec("107888604"), - tib_int("324846098"), - tib_int_vec("324846099"), - tib_int_vec("324846100"), - ), - tib_row( - "venueNames", - tib_chr("PLEYEL_PLEYEL"), - ), - ) - -# can guess spec for gsoc-2018 - - Code - guess_tspec(x) - Output - tspec_df( - .names_to = ".names", - tib_chr("@context"), - tib_chr("@type"), - tib_chr("name"), - tib_chr("description"), - tib_row( - "sponsor", - tib_chr("@type"), - tib_chr("name"), - tib_chr("disambiguatingDescription"), - tib_chr("description"), - tib_chr("url"), - tib_chr("logo"), - ), - tib_row( - "author", - tib_chr("@type"), - tib_chr("name"), - ), - ) - -# can guess spec for twitter +# guess_tspec_object can guess spec for twitter Code - guess_tspec(x) + guess_tspec_object(read_sample_json("twitter.json")) Output tspec_object( tib_df( diff --git a/tests/testthat/_snaps/guess_tspec_object_list.md b/tests/testthat/_snaps/guess_tspec_object_list.md new file mode 100644 index 00000000..2233e88a --- /dev/null +++ b/tests/testthat/_snaps/guess_tspec_object_list.md @@ -0,0 +1,114 @@ +# guess_tspec_object_list can guess spec for discog + + Code + guess_tspec_object_list(discog) + Output + tspec_df( + tib_int("instance_id"), + tib_chr("date_added"), + tib_row( + "basic_information", + tib_df( + "labels", + tib_chr("name"), + tib_chr("entity_type"), + tib_chr("catno"), + tib_chr("resource_url"), + tib_int("id"), + tib_chr("entity_type_name"), + ), + tib_int("year"), + tib_chr("master_url"), + tib_df( + "artists", + tib_chr("join"), + tib_chr("name"), + tib_chr("anv"), + tib_chr("tracks"), + tib_chr("role"), + tib_chr("resource_url"), + tib_int("id"), + ), + tib_int("id"), + tib_chr("thumb"), + tib_chr("title"), + tib_df( + "formats", + tib_variant("descriptions", required = FALSE), + tib_chr("text", required = FALSE), + tib_chr("name"), + tib_chr("qty"), + ), + tib_chr("cover_image"), + tib_chr("resource_url"), + tib_int("master_id"), + ), + tib_int("id"), + tib_int("rating"), + ) + +# guess_tspec_object_list can guess spec for gh_users + + Code + guess_tspec_object_list(gh_users) + Output + tspec_df( + tib_chr("login"), + tib_int("id"), + tib_chr("avatar_url"), + tib_chr("gravatar_id"), + tib_chr("url"), + tib_chr("html_url"), + tib_chr("followers_url"), + tib_chr("following_url"), + tib_chr("gists_url"), + tib_chr("starred_url"), + tib_chr("subscriptions_url"), + tib_chr("organizations_url"), + tib_chr("repos_url"), + tib_chr("events_url"), + tib_chr("received_events_url"), + tib_chr("type"), + tib_lgl("site_admin"), + tib_chr("name"), + tib_chr("company"), + tib_chr("blog"), + tib_chr("location"), + tib_chr("email"), + tib_lgl("hireable"), + tib_chr("bio"), + tib_int("public_repos"), + tib_int("public_gists"), + tib_int("followers"), + tib_int("following"), + tib_chr("created_at"), + tib_chr("updated_at"), + ) + +# guess_tspec_object_list can guess spec for gsoc-2018 + + Code + guess_tspec_object_list(read_sample_json("gsoc-2018.json")) + Output + tspec_df( + .names_to = ".names", + tib_chr("@context"), + tib_chr("@type"), + tib_chr("name"), + tib_chr("description"), + tib_row( + "sponsor", + tib_chr("@type"), + tib_chr("name"), + tib_chr("disambiguatingDescription"), + tib_chr("description"), + tib_chr("url"), + tib_chr("logo"), + ), + tib_row( + "author", + tib_chr("@type"), + tib_chr("name"), + ), + ) + diff --git a/tests/testthat/_snaps/shape_utils.md b/tests/testthat/_snaps/shape_utils.md new file mode 100644 index 00000000..509fead2 --- /dev/null +++ b/tests/testthat/_snaps/shape_utils.md @@ -0,0 +1,72 @@ +# .abort_not_tibblifiable throws informative errors + + Code + (expect_pkg_error_classes(.abort_not_tibblifiable(letters), "tibblify", + "untibblifiable_object")) + Output + + Error: + ! `letters` is neither an object nor a list of objects. + An object + x is a list, + x is fully named, + v and has unique names. + A list of objects is + x a data frame or + x a list and + x each element is `NULL` or an object. + +--- + + Code + (expect_pkg_error_classes(.abort_not_tibblifiable(list(1, 2, 3)), "tibblify", + "untibblifiable_object")) + Output + + Error: + ! `list(1, 2, 3)` is neither an object nor a list of objects. + An object + v is a list, + x is fully named, + v and has unique names. + A list of objects is + x a data frame or + v a list and + x each element is `NULL` or an object. + +--- + + Code + (expect_pkg_error_classes(.abort_not_tibblifiable(list(a = 1, a = 2)), + "tibblify", "untibblifiable_object")) + Output + + Error: + ! `list(a = 1, a = 2)` is neither an object nor a list of objects. + An object + v is a list, + v is fully named, + x and has unique names. + A list of objects is + x a data frame or + v a list and + x each element is `NULL` or an object. + +--- + + Code + (expect_pkg_error_classes(.abort_not_tibblifiable(list(list(a = 1), letters)), + "tibblify", "untibblifiable_object")) + Output + + Error: + ! `list(list(a = 1), letters)` is neither an object nor a list of objects. + An object + v is a list, + x is fully named, + v and has unique names. + A list of objects is + x a data frame or + v a list and + x each element is `NULL` or an object. + diff --git a/tests/testthat/_snaps/spec_guess_object.md b/tests/testthat/_snaps/spec_guess_object.md deleted file mode 100644 index 1c4059f5..00000000 --- a/tests/testthat/_snaps/spec_guess_object.md +++ /dev/null @@ -1,31 +0,0 @@ -# gives nice errors - - Code - (expect_error(guess_tspec_object(tibble(a = 1)))) - Output - - Error in `guess_tspec_object()`: - ! `x` must not be a dataframe. - i Did you want to use `guess_tspec_df()` instead? - Code - (expect_error(guess_tspec_object(1:3))) - Output - - Error in `guess_tspec_object()`: - ! `x` must be a list, not an integer vector. - ---- - - Code - (expect_error(guess_tspec_object(list(1, a = 1)))) - Output - - Error in `guess_tspec_object()`: - ! `x` must be fully named. - Code - (expect_error(guess_tspec_object(list(a = 1, a = 1)))) - Output - - Error in `guess_tspec_object()`: - ! Names of `x` must be unique. - diff --git a/tests/testthat/_snaps/tibblify.md b/tests/testthat/_snaps/tibblify.md index f7dc2747..714c62d7 100644 --- a/tests/testthat/_snaps/tibblify.md +++ b/tests/testthat/_snaps/tibblify.md @@ -44,7 +44,7 @@ ! The names of an object can't be empty. x `x` has an empty name at location 3. Code - (expect_error(tibblify(set_names(list(1, 2), c("x", NA)), spec))) + (expect_error(tibblify(rlang::set_names(list(1, 2), c("x", NA)), spec))) Output Error in `tibblify()`: @@ -89,7 +89,8 @@ ! The names of an object can't be empty. x `x$row` has an empty name at location 3. Code - (expect_error(tibblify(list(row = set_names(list(1, 2), c("x", NA))), spec2))) + (expect_error(tibblify(list(row = rlang::set_names(list(1, 2), c("x", NA))), + spec2))) Output Error in `tibblify()`: diff --git a/tests/testthat/helper-guess.R b/tests/testthat/helper-guess.R index 70b12077..0c218fb4 100644 --- a/tests/testthat/helper-guess.R +++ b/tests/testthat/helper-guess.R @@ -4,12 +4,14 @@ guess_ol_field <- function( value, name = "x", empty_list_unspecified = FALSE, - simplify_list = FALSE + simplify_list = FALSE, + local_env = list() ) { - guess_object_list_field_spec( + .guess_object_list_field_spec( value = value, name = name, empty_list_unspecified = empty_list_unspecified, - simplify_list = simplify_list + simplify_list = simplify_list, + local_env = local_env ) } diff --git a/tests/testthat/setup.R b/tests/testthat/setup.R deleted file mode 100644 index eb74434c..00000000 --- a/tests/testthat/setup.R +++ /dev/null @@ -1,4 +0,0 @@ -pre_test_options <- options( - tibblify.show_unspecified = FALSE, - tibblify.print_names = FALSE -) diff --git a/tests/testthat/teardown.R b/tests/testthat/teardown.R deleted file mode 100644 index 6c7ef9f2..00000000 --- a/tests/testthat/teardown.R +++ /dev/null @@ -1 +0,0 @@ -options(pre_test_options) diff --git a/tests/testthat/test-guess_tspec.R b/tests/testthat/test-guess_tspec.R new file mode 100644 index 00000000..71d1d8d0 --- /dev/null +++ b/tests/testthat/test-guess_tspec.R @@ -0,0 +1,47 @@ +# `guess_tspec()` is really just a dispatcher. Keep tests simple. + +test_that("guess_tspec errors informatively when x is neither df nor list", { + expect_snapshot({ + expect_error(guess_tspec("a")) + }) +}) + +test_that("guess_tspec dispatches correctly for object lists", { + local_mocked_bindings( + guess_tspec_list = function(x, ...) { + expect_type(x, "list") + cli::cli_inform("list", class = "list-called") + } + ) + expect_message(guess_tspec(discog), class = "list-called") + expect_message(guess_tspec(gh_users), class = "list-called") + expect_message(guess_tspec(got_chars), class = "list-called") +}) + +test_that("guess_tspec dispatches correctly for objects", { + local_mocked_bindings( + guess_tspec_list = function(x, ...) { + expect_type(x, "list") + cli::cli_inform("list", class = "list-called") + } + ) + x <- read_sample_json("citm_catalog.json") + expect_message(guess_tspec(x), class = "list-called") + x <- read_sample_json("twitter.json") + expect_message(guess_tspec(x), class = "list-called") +}) + +test_that("guess_tspec dispatches correctly for dfs", { + local_mocked_bindings( + guess_tspec_df = function(x, ...) { + expect_type(x, "list") + expect_s3_class(x, "data.frame") + cli::cli_inform("df", class = "df-called") + } + ) + x <- data.frame(a = 1:3, b = 2:4) + expect_message(guess_tspec(x), class = "df-called") + + x <- tibble::tibble(a = 1:3, b = 2:4) + expect_message(guess_tspec(x), class = "df-called") +}) diff --git a/tests/testthat/test-spec_guess_df.R b/tests/testthat/test-guess_tspec_df.R similarity index 81% rename from tests/testthat/test-spec_guess_df.R rename to tests/testthat/test-guess_tspec_df.R index b6693793..480c640d 100644 --- a/tests/testthat/test-spec_guess_df.R +++ b/tests/testthat/test-guess_tspec_df.R @@ -2,18 +2,18 @@ test_that("can guess scalar columns", { expect_equal( - col_to_spec(TRUE, "lgl", FALSE), + .col_to_spec(TRUE, "lgl", FALSE), tib_lgl("lgl") ) expect_equal( - col_to_spec(vctrs::new_datetime(), "dtt", FALSE), + .col_to_spec(vctrs::new_datetime(), "dtt", FALSE), tib_scalar("dtt", .ptype = vctrs::new_datetime()) ) # also for record types x_rat <- new_rational(1, 2) expect_equal( - col_to_spec(x_rat, "x", FALSE), + .col_to_spec(x_rat, "x", FALSE), tib_scalar("x", x_rat) ) }) @@ -21,7 +21,7 @@ test_that("can guess scalar columns", { test_that("scalar POSIXlt is converted to POSIXct", { x_posixlt <- as.POSIXlt(vctrs::new_date(0)) expect_equal( - col_to_spec(x_posixlt, "x", FALSE), + .col_to_spec(x_posixlt, "x", FALSE), tib_scalar("x", .ptype = vctrs::new_datetime()) ) }) @@ -29,19 +29,19 @@ test_that("scalar POSIXlt is converted to POSIXct", { test_that("can guess scalar NA columns", { # typed NA creates tib_scalar expect_equal( - col_to_spec(NA_integer_, "int", FALSE), + .col_to_spec(NA_integer_, "int", FALSE), tib_int("int") ) na_date <- vctrs::vec_init(vctrs::new_date()) expect_equal( - col_to_spec(na_date, "date", FALSE), + .col_to_spec(na_date, "date", FALSE), tib_scalar("date", .ptype = vctrs::new_date()) ) # simple NA creates tib_unspecified expect_equal( - col_to_spec(NA, "lgl", FALSE), + .col_to_spec(NA, "lgl", FALSE), tib_unspecified("lgl") ) }) @@ -51,18 +51,18 @@ test_that("can guess scalar NA columns", { test_that("can guess vector columns", { expect_equal( - col_to_spec(list(TRUE), "lgl_vec", FALSE), + .col_to_spec(list(TRUE), "lgl_vec", FALSE), tib_lgl_vec("lgl_vec") ) expect_equal( - col_to_spec(list(vctrs::new_datetime()), "dtt_vec", FALSE), + .col_to_spec(list(vctrs::new_datetime()), "dtt_vec", FALSE), tib_vector("dtt_vec", .ptype = vctrs::new_datetime()) ) # also for record types x_rat <- new_rational(1, 2) expect_equal( - col_to_spec(list(x_rat), "x", FALSE), + .col_to_spec(list(x_rat), "x", FALSE), tib_vector("x", x_rat) ) }) @@ -70,7 +70,7 @@ test_that("can guess vector columns", { test_that("vector POSIXlt is converted to POSIXct", { x_posixlt <- as.POSIXlt(vctrs::new_date(0)) expect_equal( - col_to_spec(list(x_posixlt), "x", FALSE), + .col_to_spec(list(x_posixlt), "x", FALSE), tib_vector("x", .ptype = vctrs::new_datetime()) ) }) @@ -78,14 +78,14 @@ test_that("vector POSIXlt is converted to POSIXct", { test_that("can guess vector NA columns", { # TODO maybe this could also be `tib_unspecified()`? expect_equal( - col_to_spec(list(c(NA, NA), NA), "x", FALSE), + .col_to_spec(list(c(NA, NA), NA), "x", FALSE), tib_lgl_vec("x") ) }) test_that("respect empty_list_unspecified for vector columns (#95)", { expect_equal( - col_to_spec(list(1:2, list()), "int_vec", FALSE), + .col_to_spec(list(1:2, list()), "int_vec", FALSE), tib_variant("int_vec") ) @@ -102,19 +102,19 @@ test_that("respect empty_list_unspecified for vector columns (#95)", { test_that("can guess list of NULL columns", { expect_equal( - col_to_spec(list(NULL, NULL), "x", FALSE), + .col_to_spec(list(NULL, NULL), "x", FALSE), tib_unspecified("x") ) }) test_that("can guess list of columns", { expect_equal( - col_to_spec(vctrs::list_of(1L, 2:3), "x", FALSE), + .col_to_spec(vctrs::list_of(1L, 2:3), "x", FALSE), tib_int_vec("x") ) expect_equal( - col_to_spec(vctrs::list_of(.ptype = integer()), "x", FALSE), + .col_to_spec(vctrs::list_of(.ptype = integer()), "x", FALSE), tib_int_vec("x") ) }) @@ -124,7 +124,7 @@ test_that("can guess list of columns", { test_that("can guess mixed columns", { expect_equal( - col_to_spec(list(1, "a"), "x", FALSE), + .col_to_spec(list(1, "a"), "x", FALSE), tib_variant("x") ) }) @@ -132,7 +132,7 @@ test_that("can guess mixed columns", { test_that("can guess non-vector objects", { model <- lm(Sepal.Length ~ Sepal.Width, data = iris) expect_equal( - col_to_spec(list(1, model), "x", FALSE), + .col_to_spec(list(1, model), "x", FALSE), tib_variant("x") ) }) @@ -143,7 +143,7 @@ test_that("can guess non-vector objects", { test_that("can guess tibble columns", { # scalar expect_equal( - col_to_spec(tibble(int = 1L, chr = "a"), "df", FALSE), + .col_to_spec(tibble(int = 1L, chr = "a"), "df", FALSE), tib_row( "df", int = tib_int("int"), @@ -153,19 +153,19 @@ test_that("can guess tibble columns", { # vector expect_equal( - col_to_spec(tibble(int_vec = list(1L)), "df", FALSE), + .col_to_spec(tibble(int_vec = list(1L)), "df", FALSE), tib_row("df", int_vec = tib_int_vec("int_vec")) ) # mixed expect_equal( - col_to_spec(tibble(x = list(1L, "a")), "df", FALSE), + .col_to_spec(tibble(x = list(1L, "a")), "df", FALSE), tib_row("df", x = tib_variant("x")) ) # tibble -> recursion expect_equal( - col_to_spec(tibble(x = tibble(y = 1L)), "df", FALSE), + .col_to_spec(tibble(x = tibble(y = 1L)), "df", FALSE), tib_row("df", x = tib_row("x", y = tib_int("y"))) ) }) @@ -334,7 +334,7 @@ test_that("can guess required for list of tibble columns", { test_that("can guess spec for nested df columns", { # row in row element expect_equal( - col_to_spec(tibble(df2 = tibble(int2 = 1L, chr2 = "a")), "df", FALSE), + .col_to_spec(tibble(df2 = tibble(int2 = 1L, chr2 = "a")), "df", FALSE), tib_row( "df", df2 = tib_row( @@ -347,7 +347,7 @@ test_that("can guess spec for nested df columns", { # df in row element expect_equal( - col_to_spec( + .col_to_spec( tibble( df2 = list( tibble(dbl2 = 1L), @@ -372,7 +372,11 @@ test_that("can guess spec for nested df columns", { test_that("can guess spec for nested list of df columns", { # row in df element expect_equal( - col_to_spec(list(tibble(df2 = tibble(int2 = 1L, chr2 = "a"))), "df", FALSE), + .col_to_spec( + list(tibble(df2 = tibble(int2 = 1L, chr2 = "a"))), + "df", + FALSE + ), tib_df( "df", df2 = tib_row( @@ -385,7 +389,7 @@ test_that("can guess spec for nested list of df columns", { # df in df element expect_equal( - col_to_spec( + .col_to_spec( list( tibble( df2 = list( @@ -434,6 +438,13 @@ test_that("can guess 0 row tibbles (#79, #80)", { ) }) +test_that("errors on list columns that only consist of lists", { + expect_error( + guess_tspec_df(tibble(x = list(list(1, 2), list(3, 4)))), + "not supported yet" + ) +}) + test_that("gives nice errors", { expect_snapshot({ (expect_error(guess_tspec_df(list(a = 1)))) @@ -446,3 +457,17 @@ test_that("inform about unspecified elements", { guess_tspec_df(tibble(lgl = NA), inform_unspecified = TRUE) }) }) + +test_that("guess_tspec_df dispatches properly for object lists", { + local_mocked_bindings( + guess_tspec_object_list = function( + x, + empty_list_unspecified, + simplify_list, + call + ) { + cli::cli_inform("guess_tspec_object_list called") + } + ) + expect_message(guess_tspec_df(discog), "guess_tspec_object_list called") +}) diff --git a/tests/testthat/test-guess_tspec_list.R b/tests/testthat/test-guess_tspec_list.R new file mode 100644 index 00000000..e688c5bb --- /dev/null +++ b/tests/testthat/test-guess_tspec_list.R @@ -0,0 +1,41 @@ +test_that("guess_tspec_list errors informatively for empty input", { + expect_snapshot({ + (expect_error(guess_tspec_list(list()))) + }) +}) + +test_that("guess_tspec_list errors informatively for bad objects", { + expect_snapshot({ + # not fully named + (expect_error(guess_tspec_list(list(a = 1, 1)))) + # not unique names + (expect_error(guess_tspec_list(list(a = 1, a = 1)))) + }) +}) + +test_that("guess_tspec_list dispatches appropriately", { + local_mocked_bindings( + guess_tspec_object_list = function(x, ...) { + cli::cli_inform("object_list", class = "object_list") + }, + guess_tspec_object = function(x, ...) { + cli::cli_inform("object", class = "object") + } + ) + # guess_tspec_object() + read_sample_json("citm_catalog.json") |> + guess_tspec_list() |> + expect_message(class = "object") + read_sample_json("twitter.json") |> + guess_tspec_list() |> + expect_message(class = "object") + + # guess_tspec_object_list() + guess_tspec_list(discog) |> + expect_message(class = "object_list") + guess_tspec_list(gh_users) |> + expect_message(class = "object_list") + read_sample_json("gsoc-2018.json") |> + guess_tspec_list() |> + expect_message(class = "object_list") +}) diff --git a/tests/testthat/test-spec_guess_object.R b/tests/testthat/test-guess_tspec_object.R similarity index 68% rename from tests/testthat/test-spec_guess_object.R rename to tests/testthat/test-guess_tspec_object.R index 28b11a1c..da8943f5 100644 --- a/tests/testthat/test-spec_guess_object.R +++ b/tests/testthat/test-guess_tspec_object.R @@ -1,4 +1,16 @@ -test_that("can guess scalar elements", { +test_that("guess_tspec_object gives nice errors", { + expect_snapshot({ + (expect_error(guess_tspec_object(tibble(a = 1)))) + (expect_error(guess_tspec_object(1:3))) + }) + + expect_snapshot({ + (expect_error(guess_tspec_object(list(1, a = 1)))) + (expect_error(guess_tspec_object(list(a = 1, a = 1)))) + }) +}) + +test_that("guess_tspec_object can guess scalar elements", { expect_equal( guess_tspec_object(list(x = TRUE)), tspec_object(x = tib_lgl("x")) @@ -17,15 +29,7 @@ test_that("can guess scalar elements", { ) }) -test_that("POSIXlt is converted to POSIXct", { - x_posixlt <- as.POSIXlt(vctrs::new_date(0)) - expect_equal( - guess_tspec_object(list(x = x_posixlt)), - tspec_object(x = tib_scalar("x", vctrs::new_datetime(tzone = ""))) - ) -}) - -test_that("can handle non-vector elements (#76, #84)", { +test_that("guess_tspec_object can handle non-vector elements (#76, #84)", { model <- lm(Sepal.Length ~ Sepal.Width, data = iris) expect_equal( guess_tspec_object(list(x = model)), @@ -33,7 +37,7 @@ test_that("can handle non-vector elements (#76, #84)", { ) }) -test_that("can guess vector elements", { +test_that("guess_tspec_object can guess vector elements", { expect_equal( guess_tspec_object(list(x = c(TRUE, FALSE))), tspec_object(x = tib_lgl_vec("x")) @@ -54,15 +58,7 @@ test_that("can guess vector elements", { ) }) -test_that("POSIXlt is converted to POSIXct for vector elements", { - x_posixlt <- as.POSIXlt(vctrs::new_date(0)) - expect_equal( - guess_tspec_object(list(x = c(x_posixlt, x_posixlt))), - tspec_object(x = tib_vector("x", .ptype = vctrs::new_datetime())) - ) -}) - -test_that("can guess tib_vector for a scalar list (#94)", { +test_that("guess_tspec_object can guess tib_vector for a scalar list (#94)", { expect_equal( guess_tspec_object(list(x = list(TRUE, TRUE, NULL)), simplify_list = FALSE), tspec_object(x = tib_variant("x")) @@ -95,7 +91,7 @@ test_that("can guess tib_vector for a scalar list (#94)", { ) }) -test_that("can guess tib_vector for input form = object (#94)", { +test_that("guess_tspec_object can guess tib_vector for input form = object (#94)", { expect_equal( guess_tspec_object( list(x = list(a = TRUE, b = TRUE)), @@ -105,14 +101,22 @@ test_that("can guess tib_vector for input form = object (#94)", { ) }) -test_that("can guess mixed elements", { +test_that("guess_tspec_object falls back to tib_variant for rcrd ptype with simplify_list (#noissue)", { + x_rat <- new_rational(1, 2) + expect_equal( + guess_tspec_object(list(x = list(x_rat, x_rat)), simplify_list = TRUE), + tspec_object(x = tib_variant("x")) + ) +}) + +test_that("guess_tspec_object can guess mixed elements", { expect_equal( guess_tspec_object(list(x = list(TRUE, "a"))), tspec_object(x = tib_variant("x")) ) }) -test_that("can handle non-vector elements in list (#76, #84)", { +test_that("guess_tspec_object can handle non-vector elements in list (#76, #84)", { model <- lm(Sepal.Length ~ Sepal.Width, data = iris) expect_equal( guess_tspec_object(list(x = list(model, model))), @@ -120,21 +124,21 @@ test_that("can handle non-vector elements in list (#76, #84)", { ) }) -test_that("can guess df element (#81)", { +test_that("guess_tspec_object can guess df element (#81)", { expect_equal( guess_tspec_object(list(x = tibble(a = 1L))), tspec_object(x = tib_df("x", a = tib_int("a"))) ) }) -test_that("can guess tib_row", { +test_that("guess_tspec_object can guess tib_row", { expect_equal( guess_tspec_object(list(x = list(a = 1L, b = "a"))), tspec_object(x = tib_row("x", a = tib_int("a"), b = tib_chr("b"))) ) }) -test_that("can guess tib_row with a scalar list (#94)", { +test_that("guess_tspec_object can guess tib_row with a scalar list (#94)", { expect_equal( guess_tspec_object( list(x = list(a = list(1L, 2L), b = "a")), @@ -150,7 +154,7 @@ test_that("can guess tib_row with a scalar list (#94)", { ) }) -test_that("can guess tib_df", { +test_that("guess_tspec_object can guess tib_df", { expect_equal( guess_tspec_object( list( @@ -188,7 +192,7 @@ test_that("can guess tib_df", { ) }) -test_that("respect empty_list_unspecified for list of object elements (#83)", { +test_that("guess_tspec_object respects empty_list_unspecified for list of object elements (#83)", { x <- list( x = list( list(a = 1L, b = 1:2), @@ -220,7 +224,7 @@ test_that("respect empty_list_unspecified for list of object elements (#83)", { ) }) -test_that("can guess required for tib_df", { +test_that("guess_tspec_object can guess required for tib_df", { expect_equal( guess_tspec_object( list( @@ -261,7 +265,8 @@ test_that("order of fields for tib_df does not matter", { ) }) -test_that("can guess tib_unspecified for an object (#83)", { +test_that("guess_tspec_object can guess tib_unspecified for an object (#83)", { + withr::local_options(tibblify.show_unspecified = FALSE) # `NULL` is the missing element in lists expect_equal( guess_tspec_object(list(x = NULL)), @@ -318,14 +323,55 @@ test_that("can guess tib_unspecified for an object (#83)", { ) }) -test_that("gives nice errors", { - expect_snapshot({ - (expect_error(guess_tspec_object(tibble(a = 1)))) - (expect_error(guess_tspec_object(1:3))) - }) +test_that("guess_tspec_object informs about unspecified elements", { + local_mocked_bindings( + format.tspec_object = function(...) "object omitted" + ) + x <- read_sample_json("citm_catalog.json") + guess_tspec_object(x, inform_unspecified = TRUE) |> + expect_snapshot() +}) - expect_snapshot({ - (expect_error(guess_tspec_object(list(1, a = 1)))) - (expect_error(guess_tspec_object(list(a = 1, a = 1)))) - }) +test_that("guess_tspec_object returns an empty spec for an empty list", { + expect_equal( + guess_tspec_object(list()), + tspec_object() + ) +}) + +test_that(".guess_vector_input_form can guess input form for a list of NULLs", { + # This path is likely unreachable, but this way it still works if this helper + # is used in another context. + expect_equal( + .guess_vector_input_form(list(a = NULL, b = NULL), name = NULL), + list(can_simplify = FALSE) + ) + expect_equal( + .guess_vector_input_form(list(NULL, NULL), name = "c"), + list( + can_simplify = TRUE, + tib_spec = tib_unspecified("c", .required = TRUE) + ) + ) +}) + +# specific cases ---- + +test_that("guess_tspec_object can guess spec for citm_catalog", { + withr::local_options(tibblify.show_unspecified = FALSE) + x <- read_sample_json("citm_catalog.json") + x$areaNames <- x$areaNames[1:3] + x$events <- x$events[1:3] + x$performances <- x$performances[1:3] + x$seatCategoryNames <- x$seatCategoryNames[1:3] + x$subTopicNames <- x$subTopicNames[1:3] + guess_tspec_object(x, simplify_list = FALSE) |> + expect_snapshot() +}) + +test_that("guess_tspec_object can guess spec for twitter", { + withr::local_options(tibblify.show_unspecified = FALSE) + read_sample_json("twitter.json") |> + guess_tspec_object() |> + expect_snapshot() }) diff --git a/tests/testthat/test-guess_tspec_object_list.R b/tests/testthat/test-guess_tspec_object_list.R new file mode 100644 index 00000000..adc07ac7 --- /dev/null +++ b/tests/testthat/test-guess_tspec_object_list.R @@ -0,0 +1,142 @@ +test_that("respect empty_list_unspecified for object elements (#95)", { + x <- list(list(x = list(y = 1:2)), list(x = list(y = list()))) + expect_equal( + guess_tspec_object_list(x, empty_list_unspecified = FALSE), + tspec_df(x = tib_row("x", y = tib_variant("y"))) + ) + + expect_equal( + guess_tspec_object_list(x, empty_list_unspecified = TRUE), + tspec_df( + .vector_allows_empty_list = TRUE, + x = tib_row("x", y = tib_int_vec("y")) + ) + ) +}) + +test_that("can guess tib_df", { + expect_equal( + guess_tspec_object_list( + list( + list( + x = list( + list(a = 1L), + list(a = 2L) + ) + ), + list( + x = list( + list(a = 1.5) + ) + ) + ) + ), + tspec_df(x = tib_df("x", a = tib_dbl("a"))) + ) +}) + +test_that("can guess tib_unspecified", { + withr::local_options(tibblify.show_unspecified = FALSE) + expect_equal( + guess_tspec_object_list(list(list(x = NULL), list(x = NULL))), + tspec_df(x = tib_unspecified("x")) + ) + expect_equal( + guess_tspec_object_list( + list( + list(x = list(NULL, NULL)), + list(x = list(NULL)) + ) + ), + tspec_df(x = tib_unspecified("x")) + ) + + # in a row + # TODO + # expect_equal( + # guess_tspec_object_list(list(list(x = list(a = NULL)), list(x = list(a = NULL)))), + # tspec_df(x = tib_row("x", a = tib_unspecified("a"))) + # ) + + # in a df + expect_equal( + guess_tspec_object_list( + list( + list( + x = list( + list(a = NULL), + list(a = NULL) + ) + ), + list( + x = list( + list(a = NULL), + list(a = NULL) + ) + ) + ) + ), + tspec_df(x = tib_df("x", a = tib_unspecified("a"))) + ) +}) + +test_that("order of fields does not matter", { + expect_equal( + guess_tspec_object_list(list( + list(x = TRUE, y = 1:3), + list(z = "a", y = 2L, x = FALSE) + )), + tspec_df( + x = tib_lgl("x"), + y = tib_int_vec("y"), + z = tib_chr("z", .required = FALSE) + ) + ) + + expect_equal( + guess_tspec_object_list( + list( + list(x = list(a = 1L, b = "a")), + list(x = list(b = "b", a = 2L)) + ) + ), + tspec_df(x = tib_row("x", a = tib_int("a"), b = tib_chr("b"))) + ) +}) + +test_that("can guess object_list of length one (#50)", { + expect_equal( + guess_tspec_object_list(list(list(x = 1, y = 2))), + tspec_df( + x = tib_dbl("x"), + y = tib_dbl("y"), + ) + ) +}) + +test_that("guess_tspec_object_list errors informatively list of non-objects", { + expect_error( + { + guess_tspec_object_list( + list(letters) + ) + }, + class = "tibblify-error-not_object_list" + ) +}) + +# specific cases ---- + +test_that("guess_tspec_object_list can guess spec for discog", { + expect_snapshot(guess_tspec_object_list(discog)) +}) + +test_that("guess_tspec_object_list can guess spec for gh_users", { + expect_snapshot(guess_tspec_object_list(gh_users)) +}) + +test_that("guess_tspec_object_list can guess spec for gsoc-2018", { + read_sample_json("gsoc-2018.json") |> + guess_tspec_object_list() |> + expect_snapshot() +}) diff --git a/tests/testthat/test-guess_tspec_object_utils.R b/tests/testthat/test-guess_tspec_object_utils.R new file mode 100644 index 00000000..ddc915d5 --- /dev/null +++ b/tests/testthat/test-guess_tspec_object_utils.R @@ -0,0 +1,368 @@ +# .guess_object_list_field_spec() ------------------------------------------ + +test_that("can guess scalar elements", { + expect_equal( + guess_ol_field(list(TRUE, FALSE)), + tib_lgl("x") + ) + + expect_equal( + guess_ol_field(list(vctrs::new_datetime(1), vctrs::new_datetime(2))), + tib_scalar("x", vctrs::new_datetime()) + ) + + # also for record types + x_rat <- new_rational(1, 2) + expect_equal( + guess_ol_field(list(x = x_rat, x_rat)), + tib_scalar("x", x_rat) + ) +}) + +test_that("can guess scalar elements with NULL", { + expect_equal( + guess_ol_field(list(1L, NULL)), + tib_int("x") + ) +}) + +test_that("POSIXlt is converted to POSIXct", { + x_posixlt <- as.POSIXlt(vctrs::new_date(0)) + expect_equal( + guess_ol_field(list(x_posixlt, x_posixlt)), + tib_scalar("x", vctrs::new_datetime(tzone = "UTC")) + ) +}) + +test_that("respect empty_list_unspecified for scalar elements (#83, #95)", { + x <- list(x = 1L, list()) + expect_equal( + guess_ol_field(x, empty_list_unspecified = FALSE), + tib_variant("x") + ) + + # this should be `tib_vector` because a list cannot occur for a scalar + x <- list(list(x = 1L), list(x = list())) + expect_equal( + guess_tspec_object_list(x, empty_list_unspecified = TRUE), + tspec_df( + .vector_allows_empty_list = TRUE, + tib_int_vec("x") + ) + ) +}) + +test_that("can guess vector elements", { + expect_equal( + guess_ol_field(list(c(TRUE, FALSE), FALSE)), + tib_lgl_vec("x") + ) + + expect_equal( + guess_ol_field( + list( + vctrs::new_datetime(1), + c(vctrs::new_datetime(2), vctrs::new_datetime(3)) + ) + ), + tib_vector("x", vctrs::new_datetime()) + ) + + expect_equal( + guess_ol_field(list(x = 1L, integer())), + tib_int_vec("x") + ) +}) + +test_that("respect empty_list_unspecified for vector elements (#95)", { + expect_equal( + guess_ol_field(list(x = 1:2, list()), empty_list_unspecified = FALSE), + tib_variant("x") + ) + + x <- list(list(x = 1:2), list(x = list())) + expect_equal( + guess_tspec_object_list(x, empty_list_unspecified = TRUE), + tspec_df( + .vector_allows_empty_list = TRUE, + x = tib_int_vec("x") + ) + ) +}) + +test_that("can guess vector input form (#94)", { + expect_equal( + guess_ol_field( + list(list(1, 2), list()), + simplify_list = TRUE, + empty_list_unspecified = FALSE + ), + tib_dbl_vec("x", .input_form = "scalar_list") + ) + + # checks size + expect_equal( + guess_ol_field( + list(list(1, 1:2)), + simplify_list = TRUE, + empty_list_unspecified = FALSE + ), + tib_variant("x") + ) + + expect_equal( + guess_ol_field( + list(list(1, integer())), + simplify_list = TRUE, + empty_list_unspecified = FALSE + ), + tib_variant("x") + ) +}) + +test_that("can guess object input form", { + x <- list( + list(x = list(a = 1, b = 2)), + list(x = list(c = 1, d = 1, e = 1, f = 1)), + list(x = list(g = 1, h = 1, i = 1, j = 1)) + ) + expect_equal( + guess_tspec_object_list( + x, + simplify_list = TRUE, + empty_list_unspecified = FALSE + ), + tspec_df(x = tib_dbl_vec("x", .input_form = "object")) + ) +}) + +test_that("can guess tib_variant", { + expect_equal( + guess_ol_field(list(list(TRUE, "a"), list(FALSE, "b"))), + tib_variant("x") + ) + + expect_equal( + guess_ol_field(list("a", 1)), + tib_variant("x") + ) +}) + +test_that("can handle non-vector elements (#76, #84)", { + model <- lm(Sepal.Length ~ Sepal.Width, data = iris) + expect_equal( + guess_ol_field(list(model, 1L)), + tib_variant("x") + ) +}) + +test_that("can guess object elements", { + expect_equal( + guess_ol_field(list(list(a = 1L, b = "a"), list(a = 2L, b = "b"))), + tib_row("x", a = tib_int("a"), b = tib_chr("b")) + ) + + expect_equal( + guess_ol_field(list(list(a = 1L), list(a = 2:3))), + tib_row("x", a = tib_int_vec("a")) + ) + + expect_equal( + guess_ol_field(list(list(a = 1L), list(a = "a"))), + tib_row("x", a = tib_variant("a")) + ) +}) + +test_that(".guess_object_list_field_spec returns tib_variant for no common ptype", { + # Incompatible types + expect_equal( + .guess_object_list_field_spec(list(TRUE, "a"), "x", FALSE, FALSE), + tib_variant("x") + ) + # Non-vector elements (functions trigger scalar type error) + expect_equal( + .guess_object_list_field_spec(list(mean, sum), "x", FALSE, FALSE), + tib_variant("x") + ) +}) + +test_that(".guess_object_list_field_spec returns tib_unspecified when ptype is NULL", { + expect_equal( + .guess_object_list_field_spec(list(NULL, NULL), "x", FALSE, FALSE), + tib_unspecified("x") + ) +}) + +test_that(".guess_object_list_field_spec returns tib_scalar/tib_vector for vector ptype", { + # Scalar (one element per row) + expect_equal( + .guess_object_list_field_spec(list(1L, 2L), "x", FALSE, FALSE), + tib_int("x") + ) + # Vector (multiple elements per row) + expect_equal( + .guess_object_list_field_spec(list(1:2, 3:4), "x", FALSE, FALSE), + tib_int_vec("x") + ) +}) + +test_that(".guess_object_list_field_spec errors for a list of dataframes", { + expect_error( + .guess_object_list_field_spec( + list(data.frame(a = 1L), data.frame(a = 2L)), + "x", + FALSE, + FALSE + ), + "A list of dataframes is not yet supported" + ) +}) + +test_that(".guess_object_list_field_spec returns tib_unspecified for all-empty or all-null lists", { + # All empty lists + expect_equal( + .guess_object_list_field_spec(list(list(), list()), "x", FALSE, FALSE), + tib_unspecified("x") + ) + # Each element is a list containing only NULLs + expect_equal( + .guess_object_list_field_spec( + list(list(NULL), list(NULL)), + "x", + FALSE, + FALSE + ), + tib_unspecified("x") + ) +}) + +test_that(".guess_object_list_field_spec returns tib_df for a list of object lists", { + # Unnamed value_flat -> no .names_to + expect_equal( + .guess_object_list_field_spec( + list(list(list(a = 1L), list(a = 2L))), + "x", + FALSE, + FALSE + ), + tib_df("x", tib_int("a")) + ) + # Named value_flat -> .names_to = ".names" + expect_equal( + .guess_object_list_field_spec( + list(list(p = list(a = 1L), q = list(a = 2L))), + "x", + FALSE, + FALSE + ), + tib_df("x", tib_int("a"), .names_to = ".names") + ) +}) + +test_that(".guess_object_list_field_spec returns tib_row when !simplify_list and object", { + expect_equal( + .guess_object_list_field_spec( + list(list(a = 1L, b = "x"), list(a = 2L, b = "y")), + "x", + FALSE, + FALSE + ), + tib_row("x", tib_int("a"), tib_chr("b")) + ) +}) + +test_that(".guess_object_list_field_spec returns tib_variant when !simplify_list and !object", { + # Unnamed (non-object) lists + expect_equal( + .guess_object_list_field_spec( + list(list(1L, 2L), list(3L, 4L)), + "x", + FALSE, + FALSE + ), + tib_variant("x") + ) +}) + +test_that(".guess_object_list_field_spec simplifies to tib_vector with input_form = 'object'", { + # Named elements in value_flat -> input_form = "object" + expect_equal( + .guess_object_list_field_spec( + list(list(p = 1L, q = 2L), list(p = 3L, q = 4L)), + "x", + FALSE, + TRUE + ), + tib_int_vec("x", .input_form = "object") + ) +}) + +test_that(".guess_object_list_field_spec simplifies to tib_vector with input_form = 'scalar_list'", { + # Unnamed single-element lists -> input_form = "scalar_list" + expect_equal( + .guess_object_list_field_spec(list(list(1L), list(2L)), "x", FALSE, TRUE), + tib_int_vec("x", .input_form = "scalar_list") + ) +}) + +test_that(".guess_object_list_field_spec returns tib_row when simplify_list but not vectorisable", { + # Mixed-type named elements can't be combined -> falls back to tib_row + expect_equal( + .guess_object_list_field_spec( + list(list(a = 1L, b = "x"), list(a = 2L, b = "y")), + "x", + FALSE, + TRUE + ), + tib_row("x", tib_int("a"), tib_chr("b")) + ) +}) + +test_that(".guess_object_list_field_spec returns tib_variant when simplify_list and !object and not vectorisable", { + # Unnamed lists with non-scalar sizes + expect_equal( + .guess_object_list_field_spec( + list(list(1L, 1:2), list(3L)), + "x", + FALSE, + TRUE + ), + tib_variant("x") + ) +}) + +test_that(".guess_object_list_field_spec respects empty_list_unspecified for vector ptype", { + # empty_list_unspecified = TRUE: empty lists are dropped, had_empty_lists is set + local_env <- rlang::new_environment(list(empty_list_used = FALSE)) + result <- .guess_object_list_field_spec( + list(list(), 1L, 2L), + "x", + TRUE, + FALSE, + local_env + ) + expect_equal(result, tib_int_vec("x")) + expect_true(.read_empty_list_argument(local_env)) +}) + +# .get_required() ---------------------------------------------------------- + +test_that(".get_required subsamples when x is larger than sample_size", { + x <- list( + list(a = 1, b = 2), + list(a = 3, b = 4), + list(a = 5, b = 6) + ) + result <- .get_required(x, sample_size = 2) + expect_named(result, c("a", "b"), ignore.order = TRUE) + expect_type(result, "logical") +}) + +test_that(".get_required returns all FALSE when any record is empty", { + x <- list( + list(a = 1, b = 2), + list() + ) + result <- .get_required(x) + expect_equal(result, c(a = FALSE, b = FALSE)) +}) diff --git a/tests/testthat/test-guess_tspec_utils.R b/tests/testthat/test-guess_tspec_utils.R new file mode 100644 index 00000000..8bf91f99 --- /dev/null +++ b/tests/testthat/test-guess_tspec_utils.R @@ -0,0 +1,125 @@ +# .cast_posixlt_ptype ---------------------------------------------------------- + +test_that(".cast_posixlt_ptype casts POSIXlt to POSIXct", { + expect_equal( + .cast_posixlt_ptype(as.POSIXlt(vctrs::new_date())), + vctrs::new_datetime() + ) + expect_equal( + .cast_posixlt_ptype(as.POSIXlt(vctrs::new_datetime())), + vctrs::new_datetime() + ) + expect_equal( + .cast_posixlt_ptype(vctrs::new_datetime()), + vctrs::new_datetime() + ) + expect_equal( + .cast_posixlt_ptype("x"), + "x" + ) +}) + +# .is_unspecified -------------------------------------------------------------- + +test_that(".is_unspecified identifies objects tagged as unspecified", { + expect_true(.is_unspecified(vctrs::unspecified(1))) + expect_false(.is_unspecified(1)) +}) + +# .is_vec ---------------------------------------------------------------------- + +test_that(".is_vec differentiates between lists and other vectors", { + expect_true(.is_vec(letters)) + expect_true(.is_vec(1:10)) + expect_false(.is_vec(list(1:10))) + expect_false(.is_vec(data.frame(a = 1:10))) +}) + +# .get_ptype_common ------------------------------------------------------------ +test_that(".get_ptype_common returns common ptype when valid", { + expect_mapequal( + .get_ptype_common(list(list(), letters), TRUE), + list( + has_common_ptype = TRUE, + ptype = character(), + had_empty_lists = TRUE + ) + ) + expect_mapequal( + .get_ptype_common(list(list(), letters), FALSE), + list(has_common_ptype = FALSE) + ) + expect_mapequal( + .get_ptype_common(list(LETTERS, letters), FALSE), + list( + has_common_ptype = TRUE, + ptype = character(), + had_empty_lists = NULL + ) + ) +}) + +test_that(".get_ptype_common indicates no common ptype for un-ignorable list", { + expect_mapequal( + .get_ptype_common(list(list(), letters), empty_list_unspecified = FALSE), + list(has_common_ptype = FALSE) + ) +}) + +test_that(".get_ptype_common indicates no common ptype for incompatible type", { + expect_mapequal( + .get_ptype_common(list(1L, "a"), empty_list_unspecified = FALSE), + list(has_common_ptype = FALSE) + ) +}) + +test_that(".get_ptype_common handles scalar type error (non-vector elements)", { + expect_mapequal( + .get_ptype_common(list(mean, sum), empty_list_unspecified = FALSE), + list(has_common_ptype = FALSE) + ) +}) + +# .mark_empty_list_argument ---------------------------------------------------- + +test_that(".mark_empty_list_argument sets the value in the env when called with TRUE", { + local_env <- rlang::new_environment(list(empty_list_used = FALSE)) + .mark_empty_list_argument(TRUE, local_env) + expect_true(.read_empty_list_argument(local_env)) +}) + +test_that(".mark_empty_list_argument does nothing when called with non-TRUE", { + local_env <- rlang::new_environment(list(empty_list_used = FALSE)) + .mark_empty_list_argument(FALSE, local_env) + expect_false(.read_empty_list_argument(local_env)) + .mark_empty_list_argument(NULL, local_env) + expect_false(.read_empty_list_argument(local_env)) +}) + +# .tib_type_of ----------------------------------------------------------------- + +test_that(".tib_type_of returns the correct type string", { + expect_equal(.tib_type_of(data.frame(a = 1), "x", FALSE), "df") + expect_equal(.tib_type_of(list(1, 2), "x", FALSE), "list") + expect_equal(.tib_type_of(1:3, "x", FALSE), "vector") + expect_equal(.tib_type_of(mean, "x", TRUE), "other") +}) + +test_that(".tib_type_of errors for non-vector/list/df when other = FALSE", { + expect_error( + .tib_type_of(mean, "myfield", FALSE), + "Column myfield must be a dataframe, a list, or a vector" + ) +}) + +# .tib_ptype ------------------------------------------------------------------- + +test_that(".tib_ptype returns the ptype of a vector", { + expect_equal(.tib_ptype(1:5), integer()) + expect_equal(.tib_ptype(letters), character()) +}) + +test_that(".tib_ptype converts POSIXlt to POSIXct", { + posixlt_val <- as.POSIXlt(vctrs::new_date(0)) + expect_equal(.tib_ptype(posixlt_val), vctrs::new_datetime(tzone = "")) +}) diff --git a/tests/testthat/test-shape_utils.R b/tests/testthat/test-shape_utils.R index 3b8c5e22..5cc3f98b 100644 --- a/tests/testthat/test-shape_utils.R +++ b/tests/testthat/test-shape_utils.R @@ -1,69 +1,136 @@ -test_that("is_object() works", { +test_that(".is_object() works", { # must be a list - expect_false(is_object(structure(list(x = 1), class = "dummy"))) + expect_false(.is_object(structure(list(x = 1), class = "dummy"))) # must be fully named - expect_false(is_object(list(1))) - expect_false(is_object(list(x = 1, 1))) + expect_false(.is_object(list(1))) + expect_false(.is_object(list(x = 1, 1))) # names must not be NA - expect_false(is_object(purrr::set_names(list(1, 1), c(NA, "x")))) + expect_false(.is_object(purrr::set_names(list(1, 1), c(NA, "x")))) # must not have duplicate names - expect_false(is_object(list(x = 1, x = 1))) + expect_false(.is_object(list(x = 1, x = 1))) # valid objects - expect_true(is_object(list())) - expect_true(is_object(list(x = 1))) - expect_true(is_object(list(x = 1, y = "a"))) + expect_true(.is_object(list())) + expect_true(.is_object(list(x = 1))) + expect_true(.is_object(list(x = 1, y = "a"))) listy_list <- list() class(listy_list) <- "list" - expect_true(is_object(listy_list)) + expect_true(.is_object(listy_list)) }) -test_that("is_object_list() works", { +test_that(".is_object_list() works", { # must be a list - expect_false(is_object_list(structure(list(x = 1), class = "dummy"))) + expect_false(.is_object_list(structure(list(x = 1), class = "dummy"))) # must be a list of objects dummy <- structure(list(x = 1), class = "dummy") - expect_false(is_object_list(list(dummy))) - expect_false(is_object_list(list(x = 1))) + expect_false(.is_object_list(list(dummy))) + expect_false(.is_object_list(list(x = 1))) # valid object lists - expect_true(is_object_list(list())) - expect_true(is_object_list(mtcars)) - expect_true(is_object_list(tibble::tibble())) + expect_true(.is_object_list(list())) + expect_true(.is_object_list(mtcars)) + expect_true(.is_object_list(tibble::tibble())) - expect_true(is_object_list(list(list(x = 1), list(x = 2)))) - expect_true(is_object_list(list(list(x = 1), list(x = "a")))) + expect_true(.is_object_list(list(list(x = 1), list(x = 2)))) + expect_true(.is_object_list(list(list(x = 1), list(x = "a")))) # can handle NULL - expect_true(is_object_list(list(list(x = 1), NULL))) + expect_true(.is_object_list(list(list(x = 1), NULL))) +}) + +test_that(".check_object_list() checks that x is a list of objects", { + expect_error( + .check_object_list(structure(list(x = 1), class = "dummy")), + "must be a list of objects", + class = "tibblify-error-not_object_list" + ) + expect_error( + .check_object_list(list(structure(list(x = 1), class = "dummy"))), + "must be a list of objects", + class = "tibblify-error-not_object_list" + ) + expect_error( + .check_object_list(list(x = 1)), + "must be a list of objects", + class = "tibblify-error-not_object_list" + ) + expect_equal( + .check_object_list(list(list(x = 1), NULL)), + list(list(x = 1), NULL) + ) }) test_that("detect lists of length 1 (#50)", { - expect_true(is_object_list(list(list(x = 1, y = 2)))) + expect_true(.is_object_list(list(list(x = 1, y = 2)))) +}) + +test_that(".is_list_of_object_lists works", { + expect_true(.is_list_of_object_lists(list( + list(list(x = 1, y = 2)), + list(list(x = 1, y = 2)) + ))) + expect_false(.is_list_of_object_lists(list( + list(list(x = 1, y = 2)), + list(x = 1, y = 2) + ))) + expect_true(.is_list_of_object_lists(list( + list(list(x = 1, y = 2)), + NULL + ))) }) -test_that("is_list_of_null() works", { - expect_true(is_list_of_null(list())) - expect_true(is_list_of_null(list(NULL))) - expect_true(is_list_of_null(list(NULL, NULL))) +test_that(".is_list_of_null() works", { + expect_true(.is_list_of_null(list())) + expect_true(.is_list_of_null(list(NULL))) + expect_true(.is_list_of_null(list(NULL, NULL))) - expect_false(is_list_of_null(list(NULL, 1))) + expect_false(.is_list_of_null(list(NULL, 1))) - expect_error(is_list_of_null("not a list"), "is not a list") + expect_error(.is_list_of_null("not a list"), "is not a list") }) -test_that("list_is_list_of_null() works", { - expect_true(list_is_list_of_null(list())) - expect_true(list_is_list_of_null(list(NULL))) - expect_true(list_is_list_of_null(list(NULL, list()))) - expect_true(list_is_list_of_null(list(list(NULL)))) +test_that(".list_is_list_of_null() works", { + expect_true(.list_is_list_of_null(list())) + expect_true(.list_is_list_of_null(list(NULL))) + expect_true(.list_is_list_of_null(list(NULL, list()))) + expect_true(.list_is_list_of_null(list(list(NULL)))) - expect_false(list_is_list_of_null(list(list(NULL, 1)))) + expect_false(.list_is_list_of_null(list(list(NULL, 1)))) + + expect_error(.list_is_list_of_null("not a list"), "is not a list") +}) + +test_that(".lgl_to_bullet converts lgl vectors to bullets", { + expect_equal( + .lgl_to_bullet(c(TRUE, FALSE, FALSE, TRUE)), + c("v", "x", "x", "v") + ) +}) - expect_error(list_is_list_of_null("not a list"), "is not a list") +test_that(".abort_not_tibblifiable throws informative errors", { + stbl::expect_pkg_error_snapshot( + .abort_not_tibblifiable(letters), + package = "tibblify", + "untibblifiable_object" + ) + stbl::expect_pkg_error_snapshot( + .abort_not_tibblifiable(list(1, 2, 3)), + package = "tibblify", + "untibblifiable_object" + ) + stbl::expect_pkg_error_snapshot( + .abort_not_tibblifiable(list(a = 1, a = 2)), + package = "tibblify", + "untibblifiable_object" + ) + stbl::expect_pkg_error_snapshot( + .abort_not_tibblifiable(list(list(a = 1), letters)), + package = "tibblify", + "untibblifiable_object" + ) }) diff --git a/tests/testthat/test-should_inform_unspecified.R b/tests/testthat/test-should_inform_unspecified.R new file mode 100644 index 00000000..036dd26c --- /dev/null +++ b/tests/testthat/test-should_inform_unspecified.R @@ -0,0 +1,11 @@ +test_that("should_inform_unspecified defaults to TRUE", { + withr::local_options(tibblify.show_unspecified = NULL) + expect_true(should_inform_unspecified()) +}) + +test_that("should_inform_unspecified respects the option", { + withr::local_options(tibblify.show_unspecified = TRUE) + expect_true(should_inform_unspecified()) + withr::local_options(tibblify.show_unspecified = FALSE) + expect_false(should_inform_unspecified()) +}) diff --git a/tests/testthat/test-spec_guess.R b/tests/testthat/test-spec_guess.R deleted file mode 100644 index 0f69b7a3..00000000 --- a/tests/testthat/test-spec_guess.R +++ /dev/null @@ -1,83 +0,0 @@ -test_that("checks input", { - expect_snapshot({ - expect_error(guess_tspec("a")) - }) -}) - -test_that("can guess spec for discog", { - expect_snapshot(guess_tspec(discog) |> print()) -}) - -test_that("can guess spec for gh_users", { - expect_snapshot(guess_tspec(gh_users) |> print()) -}) - -test_that("can guess spec for gh_repos", { - expect_snapshot(guess_tspec(gh_repos) |> print()) -}) - -test_that("can guess spec for got_chars (#83)", { - spec <- guess_tspec(got_chars) - expect_snapshot(spec) - expect_equal(spec$fields$aliases, tib_variant("aliases")) - expect_equal(spec$fields$allegiances, tib_variant("allegiances")) - expect_equal(spec$fields$books, tib_variant("books")) - - spec2 <- guess_tspec(got_chars, empty_list_unspecified = TRUE) - expect_equal(spec2$fields$aliases, tib_chr_vec("aliases")) - expect_equal(spec2$fields$allegiances, tib_chr_vec("allegiances")) - expect_equal(spec2$fields$books, tib_chr_vec("books")) -}) - -test_that("can guess spec for citm_catalog", { - x <- read_sample_json("citm_catalog.json") - x$areaNames <- x$areaNames[1:3] - x$events <- x$events[1:3] - x$performances <- x$performances[1:3] - x$seatCategoryNames <- x$seatCategoryNames[1:3] - x$subTopicNames <- x$subTopicNames[1:3] - - # TODO `$seatCategoryNames`, `$subTopicNames`, `$topicNames` can be simplifed to a character vector - # TODO think about `$topicSubTopics` - expect_snapshot(guess_tspec(x)) - - # These fields are empty - # • blockNames - # • events->description - # • events->subjectCode - # • events->subtitle - # • performances->logo - # • performances->name - # • performances->seatCategories->areas->blockIds - # • performances->seatMapImage - # • subjectNames - - expect_snapshot(guess_tspec_list(x, simplify_list = FALSE)) -}) - -test_that("can guess spec for gsoc-2018", { - x <- read_sample_json("gsoc-2018.json") - expect_snapshot(guess_tspec(x)) -}) - -test_that("can guess spec for twitter", { - x <- read_sample_json("twitter.json") - expect_snapshot(guess_tspec(x)) -}) - -# guess_tspec_list() ------------------------------------------------------ - -test_that("checks input", { - # errors for empty input - expect_snapshot({ - (expect_error(guess_tspec_list(list()))) - }) - - # neither object nor object list - expect_snapshot({ - # not fully named - (expect_error(guess_tspec_list(list(a = 1, 1)))) - # not unique names - (expect_error(guess_tspec_list(list(a = 1, a = 1)))) - }) -}) diff --git a/tests/testthat/test-spec_guess_object_list.R b/tests/testthat/test-spec_guess_object_list.R deleted file mode 100644 index 32e876ef..00000000 --- a/tests/testthat/test-spec_guess_object_list.R +++ /dev/null @@ -1,294 +0,0 @@ -# get_required() ---------------------------------------------------------- - -# guess_object_list_field_spec() ------------------------------------------ - -test_that("can guess scalar elements", { - expect_equal( - guess_ol_field(list(TRUE, FALSE)), - tib_lgl("x") - ) - - expect_equal( - guess_ol_field(list(vctrs::new_datetime(1), vctrs::new_datetime(2))), - tib_scalar("x", vctrs::new_datetime()) - ) - - # also for record types - x_rat <- new_rational(1, 2) - expect_equal( - guess_ol_field(list(x = x_rat, x_rat)), - tib_scalar("x", x_rat) - ) -}) - -test_that("can guess scalar elements with NULL", { - expect_equal( - guess_ol_field(list(1L, NULL)), - tib_int("x") - ) -}) - -test_that("POSIXlt is converted to POSIXct", { - x_posixlt <- as.POSIXlt(vctrs::new_date(0)) - expect_equal( - guess_ol_field(list(x_posixlt, x_posixlt)), - tib_scalar("x", vctrs::new_datetime(tzone = "UTC")) - ) -}) - -test_that("respect empty_list_unspecified for scalar elements (#83, #95)", { - x <- list(x = 1L, list()) - expect_equal( - guess_ol_field(x, empty_list_unspecified = FALSE), - tib_variant("x") - ) - - # this should be `tib_vector` because a list cannot occur for a scalar - x <- list(list(x = 1L), list(x = list())) - expect_equal( - guess_tspec_object_list(x, empty_list_unspecified = TRUE), - tspec_df( - .vector_allows_empty_list = TRUE, - tib_int_vec("x") - ) - ) -}) - -test_that("can guess vector elements", { - expect_equal( - guess_ol_field(list(c(TRUE, FALSE), FALSE)), - tib_lgl_vec("x") - ) - - expect_equal( - guess_ol_field( - list( - vctrs::new_datetime(1), - c(vctrs::new_datetime(2), vctrs::new_datetime(3)) - ) - ), - tib_vector("x", vctrs::new_datetime()) - ) - - expect_equal( - guess_ol_field(list(x = 1L, integer())), - tib_int_vec("x") - ) -}) - -test_that("respect empty_list_unspecified for vector elements (#95)", { - expect_equal( - guess_ol_field(list(x = 1:2, list()), empty_list_unspecified = FALSE), - tib_variant("x") - ) - - x <- list(list(x = 1:2), list(x = list())) - expect_equal( - guess_tspec_object_list(x, empty_list_unspecified = TRUE), - tspec_df( - .vector_allows_empty_list = TRUE, - x = tib_int_vec("x") - ) - ) -}) - -test_that("can guess vector input form (#94)", { - expect_equal( - guess_ol_field( - list(list(1, 2), list()), - simplify_list = TRUE, - empty_list_unspecified = FALSE - ), - tib_dbl_vec("x", .input_form = "scalar_list") - ) - - # checks size - expect_equal( - guess_ol_field( - list(list(1, 1:2)), - simplify_list = TRUE, - empty_list_unspecified = FALSE - ), - tib_variant("x") - ) - - expect_equal( - guess_ol_field( - list(list(1, integer())), - simplify_list = TRUE, - empty_list_unspecified = FALSE - ), - tib_variant("x") - ) -}) - -test_that("can guess object input form (#94)", { - # there need to be enough different elements to be recognized as .input_form = "object" - # TODO should ask the user? - skip("improve guessing logic") - x <- list( - list(x = list(a = 1, b = 2)), - list(x = list(c = 1, d = 1, e = 1, f = 1)), - list(x = list(g = 1, h = 1, i = 1, j = 1)) - ) - expect_equal( - guess_tspec_object_list( - x, - simplify_list = TRUE, - empty_list_unspecified = FALSE - ), - tspec_df(x = tib_dbl_vec("x", .input_form = "object")) - ) -}) - -test_that("can guess tib_variant", { - expect_equal( - guess_ol_field(list(list(TRUE, "a"), list(FALSE, "b"))), - tib_variant("x") - ) - - expect_equal( - guess_ol_field(list("a", 1)), - tib_variant("x") - ) -}) - -test_that("can handle non-vector elements (#76, #84)", { - model <- lm(Sepal.Length ~ Sepal.Width, data = iris) - expect_equal( - guess_ol_field(list(model, 1L)), - tib_variant("x") - ) -}) - -test_that("can guess object elements", { - expect_equal( - guess_ol_field(list(list(a = 1L, b = "a"), list(a = 2L, b = "b"))), - tib_row("x", a = tib_int("a"), b = tib_chr("b")) - ) - - expect_equal( - guess_ol_field(list(list(a = 1L), list(a = 2:3))), - tib_row("x", a = tib_int_vec("a")) - ) - - expect_equal( - guess_ol_field(list(list(a = 1L), list(a = "a"))), - tib_row("x", a = tib_variant("a")) - ) -}) - -test_that("respect empty_list_unspecified for object elements (#95)", { - x <- list(list(x = list(y = 1:2)), list(x = list(y = list()))) - expect_equal( - guess_tspec_object_list(x, empty_list_unspecified = FALSE), - tspec_df(x = tib_row("x", y = tib_variant("y"))) - ) - - expect_equal( - guess_tspec_object_list(x, empty_list_unspecified = TRUE), - tspec_df( - .vector_allows_empty_list = TRUE, - x = tib_row("x", y = tib_int_vec("y")) - ) - ) -}) - -test_that("can guess tib_df", { - expect_equal( - guess_tspec_object_list( - list( - list( - x = list( - list(a = 1L), - list(a = 2L) - ) - ), - list( - x = list( - list(a = 1.5) - ) - ) - ) - ), - tspec_df(x = tib_df("x", a = tib_dbl("a"))) - ) -}) - -test_that("can guess tib_unspecified", { - expect_equal( - guess_tspec_object_list(list(list(x = NULL), list(x = NULL))), - tspec_df(x = tib_unspecified("x")) - ) - expect_equal( - guess_tspec_object_list( - list( - list(x = list(NULL, NULL)), - list(x = list(NULL)) - ) - ), - tspec_df(x = tib_unspecified("x")) - ) - - # in a row - # TODO - # expect_equal( - # guess_tspec_object_list(list(list(x = list(a = NULL)), list(x = list(a = NULL)))), - # tspec_df(x = tib_row("x", a = tib_unspecified("a"))) - # ) - - # in a df - expect_equal( - guess_tspec_object_list( - list( - list( - x = list( - list(a = NULL), - list(a = NULL) - ) - ), - list( - x = list( - list(a = NULL), - list(a = NULL) - ) - ) - ) - ), - tspec_df(x = tib_df("x", a = tib_unspecified("a"))) - ) -}) - -test_that("order of fields does not matter", { - expect_equal( - guess_tspec_object_list(list( - list(x = TRUE, y = 1:3), - list(z = "a", y = 2L, x = FALSE) - )), - tspec_df( - x = tib_lgl("x"), - y = tib_int_vec("y"), - z = tib_chr("z", .required = FALSE) - ) - ) - - expect_equal( - guess_tspec_object_list( - list( - list(x = list(a = 1L, b = "a")), - list(x = list(b = "b", a = 2L)) - ) - ), - tspec_df(x = tib_row("x", a = tib_int("a"), b = tib_chr("b"))) - ) -}) - -test_that("can guess object_list of length one (#50)", { - expect_equal( - guess_tspec(list(list(x = 1, y = 2))), - tspec_df( - x = tib_dbl("x"), - y = tib_dbl("y"), - ) - ) -}) diff --git a/tests/testthat/test-spec_inform_unspecified.R b/tests/testthat/test-spec_inform_unspecified.R index be21d5ea..359e12d0 100644 --- a/tests/testthat/test-spec_inform_unspecified.R +++ b/tests/testthat/test-spec_inform_unspecified.R @@ -30,10 +30,18 @@ test_that("informing about unspecified looks good (#38)", { }) test_that(".spec_inform_unspecified is silent when nothing unspecified (#38)", { - spec <- tspec_df( - tib_int("1int") - ) - expect_no_message({ - expect_identical(.spec_inform_unspecified(spec), spec) - }) + spec <- tspec_df(tib_int("1int")) + .spec_inform_unspecified(spec) |> + expect_identical(spec) |> + expect_no_message() +}) + +test_that(".maybe_inform_unspecified handles informing", { + spec <- tspec_df(tib_int("1int"), tib_unspecified("1un")) + .maybe_inform_unspecified(spec, FALSE) |> + expect_identical(spec) |> + expect_no_message() + .maybe_inform_unspecified(spec, TRUE) |> + expect_identical(spec) |> + expect_message("1 unspecified") }) diff --git a/tests/testthat/test-tibblify.R b/tests/testthat/test-tibblify.R index aed04dbd..7d98681f 100644 --- a/tests/testthat/test-tibblify.R +++ b/tests/testthat/test-tibblify.R @@ -30,7 +30,7 @@ test_that("tibblify checks object names", { (expect_error(tibblify(list(z = 1, y = 2, 3, a = 4), spec))) # `NA` name - (expect_error(tibblify(set_names(list(1, 2), c("x", NA)), spec))) + (expect_error(tibblify(rlang::set_names(list(1, 2), c("x", NA)), spec))) # duplicate name (expect_error(tibblify(list(x = 1, x = 2), spec))) @@ -51,7 +51,7 @@ test_that("tibblify checks object names", { # `NA` name (expect_error(tibblify( - list(row = set_names(list(1, 2), c("x", NA))), + list(row = rlang::set_names(list(1, 2), c("x", NA))), spec2 ))) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index e51f3fc2..1bc93865 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -1,3 +1,3 @@ -test_that("check_list works for allowed NULL", { - expect_null(check_list(NULL, allow_null = TRUE)) +test_that(".check_list works for allowed NULL", { + expect_null(.check_list(NULL, allow_null = TRUE)) })