Use the same loop for new and existing clients: update inputs
-> plan -> review -> apply -> check -> test ->
commit. Schemas, service policy, callbacks, fixed fixtures,
output, .specmill/helpers/, and
.specmill/manifest.json form one reviewed change. For a
toolkit upgrade, follow the existing-client
upgrade workflow. Normal generation does not update client-owned
request helpers or session controls.
1. Review schema changes
Keep the old schema snapshot while downloading the new one using your
client’s own acquisition process. Use the same filename and selection
policy for both versions. For example, place catalogue.json
in schema-before/ and schema-after/:
selection <- list(methods = c('GET', 'POST'), exclude = '^/internal/')
diff <- specmill::schema_diff(
'/path/to/schema-before', '/path/to/schema-after',
policies = list('catalogue.json' = selection)
)
cat(specmill::format_diff_markdown(diff))
specmill::count_diff_changes(diff)schema_diff() compares method/path identities and
canonical data including reachable local references and response shapes.
Changed contracts enter the breaking-change report lane conservatively
for review. Parsing failures stop comparison. These labels do not prove
runtime compatibility.
For parsed operation records,
compare_operations(old, new) gives a smaller report of
additions, removals, changed parameters/bodies, and unknown response
compatibility. Unsupported operations remain parser diagnostics, so
review them alongside any comparison.
Compare schema revisions under a fixed policy, then review policy changes separately. Otherwise, changing an exclusion can look like an upstream deletion. Generation does not automatically delete an implementation merely because an operation disappears from an unsupported or changed schema. Eligible removal comes from explicit exclusions or reconciled renames and requires verified ownership.
Naming conventions are proposal-time settings.
generate_client() uses the explicit service YAML names and
does not reapply name_case. To propose names for new
endpoints, run
configure_client(..., name_case = 'snake_case'), review its
changes, and copy the accepted mappings into the existing YAML.
2. Generate and verify
root <- normalizePath('/path/to/client', winslash = '/', mustWork = TRUE)
plan <- specmill::generate_client(root, config = 'specmill.yml', mode = 'plan')
plan$files
plan$drift
plan$diagnostics
plan$validation
# After reviewing the change:
specmill::generate_client(root, config = 'specmill.yml', mode = 'apply')
specmill::generate_client(root, config = 'specmill.yml', mode = 'check')
testthat::test_local(root, filter = 'contract-', stop_on_failure = TRUE)Generation validates each schema locally against its official OpenAPI
JSON Schema before translating operations. Invalid documents and
unsupported coverage block apply and check; inspect
plan$validation for the full findings. References outside
the document must be bundled first.
Supply the same explicit callbacks environment on each
call when your policy uses callbacks. Run the relevant manual/helper
tests too. Reapplying unchanged inputs should produce only
unchanged/retained file actions. Review output diffs before committing;
do not hand-edit generated files to fix a policy error.
3. Add a thin sourceable command
Save the following as dev/generate.R in a client without
an existing command:
.client_root <- specmill::script_root('generate.R')
generate_main <- function(root = .client_root, args = commandArgs(trailingOnly = TRUE)) {
specmill::generation_command(root, args = args, kind = 'stubs')
}
if (sys.nframe() == 0L) generate_main()script_root() locates the package from the source frame
or Rscript invocation, so the command works from another working
directory. Add explicit callback loading inside
generate_main() if needed, as in the existing-client guide.
Source it in R and call generate_main(args = '--plan'), or
run:
The final command applies. Compatibility commands
intentionally have a different default from the core
generate_client(), which defaults to check.
| Command kind | Preview | Check | Apply |
|---|---|---|---|
stubs (wrappers and documentation) |
--plan |
--check |
No mode flag |
tests (fixed contract tests) |
--dry-run |
--check |
--generate or no mode flag |
--help describes accepted flags. The stubs command
accepts legacy --rebuild=<prefix> flags; YAML still
controls selection. The tests command accepts --force,
which never overrides ownership. Check/apply test commands fail if any
selected operation lacks a fixed contract. Plan can report the gaps.
For custom orchestration, use
generate_client(artifacts = ...) to limit reconciliation to
wrappers, tests, or
documentation. Shared ownership and manifest checks still
apply.
4. Inspect implementation and test gaps
inspection <- specmill::inspect_client(root)
inspection$coverage
inspection$manual_exports
inspection$diagnostics
inspection$helpers
inspection$protected_sourcesCoverage counts files and declarations, not passing tests or working endpoints. Selected unsupported operations remain in totals. Manual exported wrappers are separate from the schema denominator and still need their own tests.
For badges and baseline comparisons, create a separate client report policy:
# dev/coverage.yml
baseline: dev/coverage-baseline.json
groups:
catalogue:
services: '^catalogue$'
label: Catalogue wrappers
badge: man/figures/catalogue-coverage.jsonGroup services values are regex strings matching service
IDs. Groups must partition every configured service exactly once. Create
the baseline’s parent directory before apply. Report-policy filenames
passed as strings are resolved by R against the working directory; make
them explicit with file.path(root, ...):
specmill::coverage_report(root, file.path(root, 'dev/coverage.yml'))
specmill::coverage_report(root, file.path(root, 'dev/coverage.yml'), mode = 'apply')
specmill::test_gap_report(root, list(
helpers = list('api_request'), report_dir = 'dev/test-gaps',
readiness_report = 'dev/readiness-report.json'
))Reports default to read-only plans. Coverage apply writes badges and
its baseline; gap-report apply writes a dated JSON report. Gap reasons
distinguish missing contracts, absent/empty test files, and unsupported
operations. readiness_report is a pointer to your canonical
audit, not an audit created by this call.
5. Add client-specific checks to CI
CI should install the project’s reviewed toolkit and formatter, then run generation checks before tests. For a client with no callbacks, an R CI step can be:
checked <- specmill::generate_client('.', config = 'specmill.yml', mode = 'check')
stopifnot(length(checked$diagnostics) == 0L)
testthat::test_local('.', stop_on_failure = TRUE)If you intentionally retain unsupported operations, use an explicit
reviewed diagnostic allowance instead of removing the diagnostics gate.
Do not use continue-on-error for invalid configuration or
schema parsing before publishing generated output. Generation commands
write existing summary keys to GITHUB_OUTPUT when that
environment variable is set; core functions return structured results
and errors instead of choosing a process exit code.
check_public_boundary() accepts your forbidden
host/file/export/schema regexes and approved manual-export list.
check_client_hooks() validates supplied runtime hook
declarations against parsed client definitions. Their reference pages
explain arguments and return values. Keep these policies
client-specific.
credential_status() and
credential_preflight() detect absent/placeholder
credentials without logging values or making requests. They cannot prove
a key is authorized. Offline generation and fixed contract tests need no
live key.
6. Build documentation for your generated client
Set documentation: true and write useful
docs policy before building the client site. Generation
stages roxygen and reconciles man/ and
NAMESPACE; the help pages then become pkgdown reference
pages. Add human-written vignettes for authentication, common workflows,
result interpretation, and errors.
From the client package root, install the documentation tools and initialize pkgdown once:
install.packages(c('pkgdown', 'usethis', 'rmarkdown', 'knitr'))
usethis::use_pkgdown()
pkgdown::build_site(preview = FALSE)For a GitHub-hosted client,
usethis::use_pkgdown_github_pages() configures the
publishing workflow and Pages settings. Review the generated workflow
for your default branch. See usethis’s
pkgdown setup.
Keep its README short: purpose, installation, one example, and links
to guides. Put sequential workflows in vignettes and per-function detail
in reference docs. Render examples offline; mark real API calls
eval=FALSE or use fixed examples that do not contact
services.
For specmill’s own site, _pkgdown.yml
defines navigation and .github/workflows/pkgdown.yaml
builds on pull requests and publishes through GitHub Actions Pages after
default-branch pushes or manual dispatch. Local build command:
pkgdown::build_site(preview = FALSE)Open docs/index.html after building. Site output is
ignored by Git and package builds; source articles and help pages are
tracked. The workflow follows the r-lib
pkgdown example.
