Files
uscogdata/R/config.R
T
jared 748ca4a56e
R-CMD-check / check (push) Successful in 2m42s
R-CMD-check / check (pull_request) Successful in 2m33s
fix(#3): normalize the corpus URL's trailing slash at resolution
Investigating "gov search doesn't work" (#3) turned up two separate things.

THE REPORTED SYMPTOM IS ALREADY FIXED.
#3 reported `cog_gov_search("Orange")` dying in jsonlite with
`lexical error: invalid char in json text. <html> <head>`. That was fixed the
same day the issue was filed, by 8743472 "fix(manifest): actionable errors when
USCOGDATA_URL is unset or returns non-JSON" (issue filed 2026-05-27 11:30;
commit 2026-05-27). The issue was simply never closed. Verified now: injecting
an HTML manifest.json raises a typed `uscogdata_invalid_manifest` condition
naming the likely causes, with the raw parse error demoted to a footnote, and
`cog_gov_search("Orange")` returns 62 rows against the live corpus.

THE ROOT CAUSE OF THAT HTML WAS STILL LIVE -- and is what this commit fixes.

Every consumer builds locations by CONCATENATION:
  manifest.R:95   paste0(url, "manifest.json")
  mirror.R:48,125 paste0(url, e$path)
  views.R         the parquet glob
and mirror.R:104 documents the invariant outright ('url ends in "/"'). The
error messages tell users to set `"<url-or-local-path>/"`. But `.resolve_url()`
was a bare `.cfg("url")` passthrough -- the invariant was assumed everywhere and
enforced nowhere.

So a URL entered without the slash failed silently and misleadingly:
  HTTPS -> ".../downloadmanifest.json"; the host answers with an HTML 404 page,
           which lands in the JSON parser as EXACTLY the #3 symptom -- and the
           guard then blames "login page / 404 / wrong share" when the real
           cause was one missing character.
  local -> ".../corpusdata/long/**/*.parquet" and a DuckDB "No files found".

Reproduced both: pointing USCOGDATA_URL at the bundled fixture without a
trailing slash gave
  No files found that match ".../fixture_corpusdata/long/**/*.parquet"

Normalizing once at resolution fixes every consumer at the same time, rather
than having each call site re-derive the invariant. An empty setting passes
through untouched so manifest.R's "not configured" guard still fires instead of
the value degrading into a bare "/" filesystem root.

RED->GREEN: 4 tests added, 2 failed first (append-missing-slash, local-path
normalization); the already-correct cases (slash present, empty setting) passed
throughout and pin them against regression. Same fixture path that produced the
DuckDB error above now returns 62 rows.

Suite: FAIL 0 | WARN 0 | SKIP 0 | PASS 471 (was 463; +8 = the new tests).
2026-07-25 18:37:42 -04:00

53 lines
1.9 KiB
R

# R/config.R
#' Package-private mutable state
#' @noRd
.uscogdata_env <- new.env(parent = emptyenv())
.uscogdata_defaults <- list(
url = "https://cloud.civilytics.org/s/REPLACE_WITH_SHARE_TOKEN/download/",
cache_dir = NULL,
manifest_ttl_secs = 3600L
)
#' Resolve a config value: env var > option > default
#' @noRd
.cfg <- function(key) {
env_var <- paste0("USCOGDATA_", toupper(key))
v <- Sys.getenv(env_var, unset = NA)
if (!is.na(v) && nzchar(v)) return(v)
opt <- getOption(paste0("uscogdata.", key), default = NULL)
if (!is.null(opt)) return(opt)
.uscogdata_defaults[[key]]
}
#' Resolve the corpus URL, guaranteeing the trailing slash the package assumes.
#'
#' Every consumer builds locations by CONCATENATION -- `paste0(url,
#' "manifest.json")` in manifest.R, `paste0(url, e$path)` in mirror.R, and the
#' parquet glob in views.R -- and mirror.R:104 documents the invariant outright
#' ('url ends in "/"'). Nothing enforced it, so a URL entered without the slash
#' failed silently and misleadingly:
#'
#' HTTPS -> ".../downloadmanifest.json"; the host answers with an HTML 404
#' page, which lands in the JSON parser as the lexical error
#' reported in issue #3 -- pointing the user at "login page / wrong
#' share" when the real cause was one missing character.
#' local -> ".../corpusdata/long/**/*.parquet" and a DuckDB "No files found".
#'
#' Normalizing here fixes every consumer at once, rather than each call site
#' re-deriving the same invariant. An empty setting is passed through
#' untouched so manifest.R's "not configured" guard still fires instead of the
#' value degrading into a bare "/" filesystem root.
#' @noRd
.resolve_url <- function() {
url <- .cfg("url")
if (is.null(url) || !nzchar(url) || grepl("/$", url)) return(url)
paste0(url, "/")
}
.resolve_cache_dir <- function() {
v <- .cfg("cache_dir")
if (is.null(v)) tools::R_user_dir("uscogdata", "cache") else v
}