Skip to content

feat: define measures from documented R scripts - #3

Merged
simonpcouch merged 8 commits into
mainfrom
feat/roxygen2
Jun 23, 2026
Merged

feat: define measures from documented R scripts#3
simonpcouch merged 8 commits into
mainfrom
feat/roxygen2

Conversation

@gadenbuie

@gadenbuie gadenbuie commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds read_measures(), which derives measure() definitions from ordinary documented R functions, so a data science team can author governed measures as plain R scripts instead of hand-assembling measure() calls.

A function becomes a measure when its roxygen2 block carries a @measure tag — much like @export marks a function as part of a package's public interface. Its name comes from the function, its description from @title/@description/@return, and its arguments from the @param tags. Other documented functions in the file are ignored, so helper functions can live alongside measures. Argument types are declared with a leading code span in the @param text — string, integer, number, boolean, enum[...], or type[] for arrays. An argument is required when its formal has no default; untyped arguments are inferred from their defaults, falling back to string. The code-span syntax was chosen over a bare sigil so the type renders correctly in the generated .Rd rather than being mangled by the markdown roclet into a broken \link{}.

All files passed in a single read_measures() call are sourced in order into one shared environment, so a measure can call helper functions defined in a sibling file of the same call.

semantic_layer() now accepts character inputs directly and routes them through read_measures(), so file paths, directories, and inline measure() objects can be freely mixed in one call (e.g. semantic_layer("measures.R", my_inline_measure)). The single-list form continues to work. Separate path arguments stay isolated — semantic_layer("a.R", "b.R") reads each file in its own environment, while semantic_layer(c("a.R", "b.R")) shares one. One behavior change: a non-path string passed to semantic_layer() now errors from read_measures() ("Path does not exist") rather than the previous "must be created by measure()".

roxygen2 is added to Suggests, guarded by rlang::check_installed() so it is only required when reading measures from scripts. The @measure tag is registered at runtime via a roxy_tag_parse S3 method (tag_toggle), since roxygen2 is only a suggested dependency.

This branch also adopts roxygen2 8.0.0: DESCRIPTION records Config/roxygen2/version: 8.0.0 and the package-level man pages are regenerated to match.

Verification

# measures.R

#' Count orders
#'
#' @description Total orders, optionally filtered by region and period.
#'
#' @param region `string` The sales region. Omit for all regions.
#' @param period `enum[day, week, month]` Aggregation period.
#' @param top_n `integer` Maximum number of rows to return.
#'
#' @return An integer count of orders.
#' @measure
order_count <- function(region = NULL, period, top_n = 10L) {
  42L
}
library(commons)

# Either form works:
sl <- semantic_layer("measures.R")
sl <- semantic_layer(read_measures("measures.R"))

names(sl$measures)
#> [1] "order_count"

Full test suite passes (devtools::test(), 163 tests), including new coverage for code-span parsing, required-from-signature, default inference, directory and vector inputs, path routing through semantic_layer(), the @measure opt-in (untagged functions ignored), cross-file helpers via a shared environment, and isolation across separate path arguments.

Derive measures from documented functions in R scripts. Every top-level
function with a roxygen2 block becomes a measure: name from the function,
description from @title/@description/@return, and arguments from @PARAM.

Argument types are declared with a leading type code span, e.g.
`enum[day, week, month]` or `string[]`; required is taken from the
signature and untyped params are inferred from their defaults.
Add a 'Defining measures' section to the README showing the roxygen2 tag
syntax for read_measures(), and add @Seealso cross-links between measure(),
semantic_layer(), and read_measures().
Character inputs to semantic_layer() are now passed to read_measures(),
so file paths, directories, and inline measure() objects can be freely
mixed: semantic_layer("measures.R", my_inline_measure). Paths may be
scalar strings or character vectors.
read_measures() now treats @measure as a required opt-in marker: only
functions whose roxygen2 block carries @measure become measures, so
helper functions can live alongside measures in the same file. This
replaces the old "every documented function is a measure" rule and
mirrors how @export marks a function.

All files passed in a single read_measures() call are sourced in order
into one shared environment (parent = globalenv()), so a measure can
call helpers defined in sibling files of the same call. Separate
arguments to semantic_layer() remain isolated.

The @measure tag is registered at runtime via a roxy_tag_parse S3
method (tag_toggle), since roxygen2 is only a suggested dependency.
Bumps the recorded roxygen2 version to 8.0.0 and regenerates the
package-level and module .Rd files accordingly.
@gadenbuie
gadenbuie marked this pull request as ready for review June 23, 2026 16:52
@skaltman

Copy link
Copy Markdown
Collaborator

I like that how this makes the measures much easier to read and customize if you're not just wrapping an existing function!

@simonpcouch simonpcouch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm game, this is better! Let's do this!

I removed the new section in the README, which is otherwise a bit less in-the-weeds than that content. I also unexported read_measures(); I can see us providing this in the future, but I think there's still an intermediate abstraction of a "measure set" or "domain" (that includes all pieces, including some set of measures) or something that we'll need to figure out in the meantime.

Comment thread README.md Outdated
semantic layer. This layer informs how the agent will author fallback
SQL queries.

### Defining measures

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a lot of extra content for the README

Comment thread DESCRIPTION
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
Config/roxygen2/version: 8.0.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lol i am behind the times

Comment thread R/measures.R Outdated
Comment on lines +6 to +8
#' @param ... [measure()] objects, lists of measures, or paths to R scripts or
#' directories. Paths are passed to [read_measures()], so file and inline
#' measures can be freely mixed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

GARRICK SHAPED MAGIC YEAHH

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants