Distributing models in a downstream package

Package layout

A downstream package owns concrete models. Store each definition as <name>-form.yaml under inst/forms/.

Use explicit subdirectories when shipping several versions of the same named model.

exampleforms/
  DESCRIPTION
  NAMESPACE
  R/survey.R
  inst/forms/survey-form.yaml

Declare Imports: form.model in DESCRIPTION. Supply a documented wrapper with explicit resource selection.

# In exampleforms/R/survey.R:
survey_model <- function() {
  form.model::select_form_model(
    "forms/survey-form.yaml",
    id = "survey", version = "2026.1",
    package = "exampleforms"
  )
}

package names the installed package containing the YAML resource.

Runnable equivalent

The bundled example uses exactly the same installed-resource mechanism.

path <- form_resource("examples/survey-form.yaml", "form.model")
stopifnot(file.exists(path))
m <- select_form_model(path, "survey", "2026.1")
form_references(m)
#> [1] "observers" "stations"

Selection reads only supplied paths, validates all of them, and requires exactly one matching identity tuple.

Supply an explicit list of files to support multiple versions. There is no directory scan or latest-version fallback.

Package-relative paths cannot escape the installed package, including through symbolic links.

Inspect fields and locations

form_fields(m, sheet = "Observations")
#> $Observations.station
#> $Observations.station$mandatory
#> [1] TRUE
#> 
#> $Observations.station$type
#> [1] "string"
#> 
#> $Observations.station$reference
#> $Observations.station$reference$id
#> [1] "stations"
#> 
#> $Observations.station$reference$severity
#> [1] "error"
#> 
#> 
#> $Observations.station$id
#> [1] "Observations.station"
#> 
#> $Observations.station$name
#> [1] "station"
#> 
#> $Observations.station$sheet
#> [1] "Observations"
#> 
#> $Observations.station$column
#> [1] 1
#> 
#> 
#> $Observations.count
#> $Observations.count$mandatory
#> [1] TRUE
#> 
#> $Observations.count$type
#> [1] "integer"
#> 
#> $Observations.count$minimum
#> [1] 0
#> 
#> $Observations.count$maximum
#> [1] 1000
#> 
#> $Observations.count$id
#> [1] "Observations.count"
#> 
#> $Observations.count$name
#> [1] "count"
#> 
#> $Observations.count$sheet
#> [1] "Observations"
#> 
#> $Observations.count$column
#> [1] 2
#> 
#> 
#> $Observations.length
#> $Observations.length$type
#> [1] "number"
#> 
#> $Observations.length$minimum
#> [1] 0
#> 
#> $Observations.length$id
#> [1] "Observations.length"
#> 
#> $Observations.length$name
#> [1] "length"
#> 
#> $Observations.length$sheet
#> [1] "Observations"
#> 
#> $Observations.length$mandatory
#> [1] FALSE
#> 
#> $Observations.length$column
#> [1] 3
#> 
#> 
#> $Observations.unit
#> $Observations.unit$type
#> [1] "string"
#> 
#> $Observations.unit$required_field
#> [1] "length"
#> 
#> $Observations.unit$enum
#> $Observations.unit$enum[[1]]
#> [1] "cm"
#> 
#> $Observations.unit$enum[[2]]
#> [1] "m"
#> 
#> 
#> $Observations.unit$id
#> [1] "Observations.unit"
#> 
#> $Observations.unit$name
#> [1] "unit"
#> 
#> $Observations.unit$sheet
#> [1] "Observations"
#> 
#> $Observations.unit$mandatory
#> [1] FALSE
#> 
#> $Observations.unit$column
#> [1] 4
#> 
#> 
#> $Observations.active
#> $Observations.active$mandatory
#> [1] TRUE
#> 
#> $Observations.active$type
#> [1] "boolean"
#> 
#> $Observations.active$id
#> [1] "Observations.active"
#> 
#> $Observations.active$name
#> [1] "active"
#> 
#> $Observations.active$sheet
#> [1] "Observations"
#> 
#> $Observations.active$column
#> [1] 5
#> 
#> 
#> $Observations.observed_at
#> $Observations.observed_at$type
#> [1] "datetime"
#> 
#> $Observations.observed_at$id
#> [1] "Observations.observed_at"
#> 
#> $Observations.observed_at$name
#> [1] "observed_at"
#> 
#> $Observations.observed_at$sheet
#> [1] "Observations"
#> 
#> $Observations.observed_at$mandatory
#> [1] FALSE
#> 
#> $Observations.observed_at$column
#> [1] 6
#> 
#> 
#> $Observations.latitude
#> $Observations.latitude$type
#> [1] "latitude"
#> 
#> $Observations.latitude$id
#> [1] "Observations.latitude"
#> 
#> $Observations.latitude$name
#> [1] "latitude"
#> 
#> $Observations.latitude$sheet
#> [1] "Observations"
#> 
#> $Observations.latitude$mandatory
#> [1] FALSE
#> 
#> $Observations.latitude$column
#> [1] 7
#> 
#> 
#> $Observations.longitude
#> $Observations.longitude$type
#> [1] "longitude"
#> 
#> $Observations.longitude$id
#> [1] "Observations.longitude"
#> 
#> $Observations.longitude$name
#> [1] "longitude"
#> 
#> $Observations.longitude$sheet
#> [1] "Observations"
#> 
#> $Observations.longitude$mandatory
#> [1] FALSE
#> 
#> $Observations.longitude$column
#> [1] 8
#> 
#> 
#> $Observations.observation_id
#> $Observations.observation_id$type
#> [1] "string"
#> 
#> $Observations.observation_id$pattern
#> [1] "^OBS-[0-9]{3}$"
#> 
#> $Observations.observation_id$unique
#> [1] TRUE
#> 
#> $Observations.observation_id$id
#> [1] "Observations.observation_id"
#> 
#> $Observations.observation_id$name
#> [1] "observation_id"
#> 
#> $Observations.observation_id$sheet
#> [1] "Observations"
#> 
#> $Observations.observation_id$mandatory
#> [1] FALSE
#> 
#> $Observations.observation_id$column
#> [1] 9
#> 
#> 
#> $Observations.quality
#> $Observations.quality$type
#> [1] "integer"
#> 
#> $Observations.quality$enum
#> $Observations.quality$enum[[1]]
#> [1] 1
#> 
#> $Observations.quality$enum[[2]]
#> [1] 2
#> 
#> $Observations.quality$enum[[3]]
#> [1] 3
#> 
#> 
#> $Observations.quality$id
#> [1] "Observations.quality"
#> 
#> $Observations.quality$name
#> [1] "quality"
#> 
#> $Observations.quality$sheet
#> [1] "Observations"
#> 
#> $Observations.quality$mandatory
#> [1] FALSE
#> 
#> $Observations.quality$column
#> [1] 10
#> 
#> 
#> $Observations.observer
#> $Observations.observer$type
#> [1] "string"
#> 
#> $Observations.observer$reference
#> $Observations.observer$reference$id
#> [1] "observers"
#> 
#> $Observations.observer$reference$severity
#> [1] "warning"
#> 
#> 
#> $Observations.observer$id
#> [1] "Observations.observer"
#> 
#> $Observations.observer$name
#> [1] "observer"
#> 
#> $Observations.observer$sheet
#> [1] "Observations"
#> 
#> $Observations.observer$mandatory
#> [1] FALSE
#> 
#> $Observations.observer$column
#> [1] 11
form_layout(m, id = "Observations")
#> $kind
#> [1] "table"
#> 
#> $start_row
#> [1] 6
#> 
#> $column_count
#> [1] 11
form_field(m, id = "Metadata.submission")[c("row", "column")]
#> $row
#> [1] 2
#> 
#> $column
#> [1] 3
form_references(m)
#> [1] "observers" "stations"

Fields retain declaration order; coordinates are one-based. Field IDs use <sheet>.<field>.

Accessors expose declarations without executing constraints or loading reference data.

See ?form_fields, ?form_field, ?form_layout and ?form_references for details.

Pass models and dependencies explicitly

A future validator accepts a model, submitted data, and caller-supplied reference snapshots.

A form package must not fetch snapshots, connect to a database, or write files during package loading.

Keep concrete domain definitions in the downstream package. Keep loader destinations and mappings in loader packages.

Run model checks in downstream tests for every shipped resource. Reserve validation equivalence tests for the full proof.