Write a specification to JSON, an Excel workbook, or Define-XML
Source:R/spec_write.R
write_spec.RdSerialise a artoo_spec, dispatching on the file extension: a .json
path writes artoo's native, lossless JSON; a .xlsx path writes a
Pinnacle 21 (P21) style Excel workbook; a .xml path writes a
submission-grade Define-XML 2.1 or 2.0 document. Each is the inverse of
read_spec() on its format, which makes the spec converters free
compositions: read_spec("spec.xlsx") |> write_spec("define.xml") turns
a workbook into a define.xml in one line.
Arguments
- spec
The specification to serialise.
<artoo_spec>: required. Build one withartoo_spec()orread_spec().- path
Destination file.
<character(1)>: required. The extension picks the format:.json(native, lossless),.xlsx(P21 interchange; needs thewritexlpackage), or.xml(Define-XML; needs thexml2package). Any other extension aborts withartoo_error_input.- ...
Format-specific options. Ignored by the JSON and xlsx paths. Define-XML accepts:
version–<character(1)> | NULL."2.1"(default) or"2.0", resolved from the spec's owndefine_versionwhen unset. Writing 2.0 from a 2.1-shaped spec warns about each construct 2.0 cannot carry.created– theCreationDateTimestamp, formatted as UTC. Freeze it for a reproducible submission build; the default is the current time.stylesheet–<logical(1)> | <character(1)>: default TRUE.TRUEwrites thexml-stylesheetprocessing instruction and copies the bundled CDISC stylesheet beside the output; a string names a stylesheet without copying one;FALSEwrites neither.data–<list of <data.frame>> | <character(1)> | NULL. The datasets the define describes: a list named for each dataset, or one path to the folder holding them. artoo reads them and fills what the spec leaves blank; see Data-aware writing.data_format–<character> | NULL. Whendatais a folder, restrict it to these formats, named asartoo_formats()lists them. Use it when a folder holds one dataset in two formats.html–<logical(1)> | <character(1)>: default FALSE.TRUEalso renders the document through its stylesheet into a sibling.html; a string renders it to that path. Needs thexsltandcallrpackages.validate–<logical(1)>: default TRUE. Schema-validate before the file is put in place.
Value
The output path, invisibly. Read it back with read_spec().
Details
Native JSON is the lossless format. Each slot is written as an array
of row objects, with NA encoded as JSON null and numbers at full
precision, so read_spec() rebuilds an identical artoo_spec through
artoo_spec(). Object keys are emitted in a fixed order, so writing the
same spec twice yields byte-identical output.
P21 xlsx is the interchange format. Sheets are emitted with the
headers the P21 reader recognises (Define, Datasets, Variables,
ValueLevel, Codelists, Methods, Comments, Documents; empty optional
sheets are omitted), foreign keys repeated on every row (no merged
cells), and the spec's spec_standard() as the Datasets sheet's
Standard column. The study row writes back as the Define sheet's
Attribute/Value pairs (StudyName, StudyDescription,
ProtocolName). The Data Type column is written in the Define-XML /
ODM vocabulary the workbook expects: a character variable is text
(not the Dataset-JSON string), and decimal / double collapse to
float, boolean / URI to text.
Columns the P21 vocabulary does not model are not lost: a foreign column carried on a slot is re-emitted verbatim under its own header, so an xlsx round-trip keeps user columns.
Note: the xlsx writer emits every sheet the spec has content for, so
standards, where_clauses, arm_displays, arm_results and
dictionaries survive a round trip. method_expressions does not
survive whole: a workbook gives each method one row with one code cell,
so a method carrying several formal expressions keeps only the first and
the write says which methods it truncated. Write JSON when a spec has
multi-expression methods, or when you need back the OIDs a Define-XML
document chose.
Define-XML is the submission format. The .xml path emits
Define-XML 2.1 or 2.0 (needs the xml2 package) and SCHEMA-VALIDATES what it
built before the file reaches its destination, so an invalid document
never overwrites a good one. Value-level metadata is emitted whole: the
parent variable's def:ValueListRef, the def:ValueListDef, a real
ItemDef per value-level row, and the def:WhereClauseRef and
def:WhereClauseDef that say which rows it applies to.
Identifiers already on the spec are reused verbatim, so a document read
and written back keeps every OID a reviewer may have bookmarked; the rest
are minted readably (IG.DM, IT.DM.USUBJID, VL.VS.VSORRES). Two
schema-required attributes are derived rather than demanded:
def:Structure falls back to the dataset keys, and Purpose follows the
CDISC standard. A dataset with neither a structure nor keys aborts with
artoo_error_define rather than being given an invented one.
HTML. A define.xml renders through an XSLT stylesheet, and the written
document names one in a processing instruction and gets a copy of it
alongside (a reference to a stylesheet that is not there is itself a
conformance finding). An existing stylesheet beside the output is never
overwritten, so a customised rendering survives. Pass html = TRUE to also
materialise the rendered HTML: browsers are removing XSLT support, and a
reviewer working from a submission archive should not need one.
What a folder measures. A named list describes the frames in memory; a
folder describes the bytes on disk. Where the two differ the folder is
right about what will be submitted – a transport file pads to fixed width,
so a value's trailing blanks are part of it there and not in R. A gzipped
.json or .ndjson is matched like any other file; .xpt.gz is not,
because read_dataset() does not read one either.
A folder instead of a list. data also takes one path to the folder
holding the datasets. Each dataset the spec names is matched to a file
whose basename is that name, ignoring case: DM to dm.xpt, dm.json,
dm.parquet. The folder is inventoried, not descended. A dataset with no
file is normal and reported, not an error, and a file the spec does not
name is left alone. A dataset matching MORE than one file aborts rather
than choosing: two formats can disagree about byte width, so picking one
silently would change the document. Name the format with data_format to
resolve it. A file that cannot be read aborts too, naming every unreadable
file at once rather than stopping at the first.
Data-aware writing. Pass data and artoo reads the datasets the
define describes, which a spec-only tool cannot. A blank length is
filled from the real maximum byte width; a stated one shorter than the
data is widened, because a length below the real maximum is a conformance
finding, and the write says which variables it widened. A stated length
LONGER than the data is left alone: a length is a claim about the domain,
not about one extract. Value-level metadata is derived for the standard
findings shapes – a result keyed by its test code, TSVAL by TSPARMCD,
QVAL by QNAM, AVAL and AVALC by PARAMCD – with each derived row
carrying the type and width of the rows it covers. A variable the spec
already gives value-level rows to is never touched. A dataset with no
records is flagged def:HasNoData when it also carries a comment
explaining the absence, and left unflagged with a warning when it does
not.
Fields with no P21 column (itemoid, target_data_type,
per-variable key_sequence) likewise do not survive an xlsx round-trip;
persist to JSON when you need the spec back exactly. The Data Type
re-encoding is also non-injective: decimal, double, boolean, and
URI fold to float or text on a read-back. A Define-XML
partialDate / partialDatetime (and the other partial / incomplete
subtypes) is read as the base date / datetime – CDISC Dataset-JSON
v1.1 has no partial dataType – so it is written back as the base type.
See also
Inverse: read_spec() reads native JSON, a P21 Excel workbook, or
Define-XML back into a artoo_spec.
Check the Define-XML written: validate_define() schema-validates it,
lint_define() checks its reference integrity.
Build / inspect: artoo_spec(), spec_datasets(),
spec_variables(), spec_standard().
Examples
# ---- Example 1: persist a spec to JSON, then read it back ----
#
# Build a spec from the bundled CDISC-pilot tables, write it to a temp
# JSON file, and confirm read_spec() reconstructs it intact.
spec <- artoo_spec(
cdisc_adam_datasets, cdisc_adam_variables,
codelists = cdisc_codelists
)
path <- tempfile(fileext = ".json")
write_spec(spec, path)
identical(read_spec(path), spec)
#> [1] TRUE
# ---- Example 2: the same spec as a P21 workbook ----
#
# The .xlsx path emits P21-shaped sheets; reading the workbook back
# recovers the P21-representable surface (here: the dataset names).
if (requireNamespace("writexl", quietly = TRUE)) {
xlsx <- tempfile(fileext = ".xlsx")
write_spec(spec, xlsx)
spec_datasets(read_spec(xlsx))
}
#> [1] "ADSL"
# ---- Example 3: the same spec as a submission-grade define.xml ----
#
# Read the bundled minimal Define-XML, write it back with a frozen
# timestamp, and confirm the result is schema-valid. The write itself
# validates, so reaching this line already proves it.
if (requireNamespace("xml2", quietly = TRUE)) {
dm <- read_spec(
system.file("extdata", "define-minimal.xml", package = "artoo")
)
xml <- file.path(tempdir(), "define.xml")
write_spec(dm, xml, created = "2020-01-01 00:00:00")
validate_define(xml)
}
#> artoo Define-XML Schema Check
#> =============================
#>
#> Summary
#> -------
#> Document: define.xml
#> Define-XML version: 2.1
#> Schema valid: yes
#>
#> No findings.
#>
# ---- Example 4: let a folder of datasets fill the blanks ----
#
# A spec states a variable's length or leaves it blank. Point `data` at
# the folder holding the datasets and artoo matches each one the spec
# names to a file called after it, then fills the blanks from the real
# maximum byte width. It reports which files it used, because a define is
# a submission document and that is what a reader cannot recover from it.
if (requireNamespace("xml2", quietly = TRUE)) {
folder <- file.path(tempdir(), "datasets")
dir.create(folder, showWarnings = FALSE)
write_json(
apply_spec(cdisc_adsl, adam_spec, "ADSL", conformance = "off"),
file.path(folder, "adsl.json")
)
from_folder <- file.path(tempdir(), "from-folder.xml")
write_spec(adam_spec, from_folder, data = folder)
lint_define(from_folder)
}
#> 6 variables the spec declares are absent from the data (not added):
#> `TRTDURD`, `DISONDT`, `EOSSTT`, `DCSREAS`, `EOSDISP`, and `MMS1TSBL`.
#> Read 1 of 2 datasets from /tmp/RtmpRfFmyE/datasets.
#> ℹ Used adsl.json.
#> ℹ No file for "ADAE".
#> artoo Define-XML Reference Check
#> ================================
#>
#> Summary
#> -------
#> Document: from-folder.xml
#> Definitions: 198 References: 255
#> External codelists (exempt from the orphan check): 2
#>
#> Findings Summary
#> ----------------
#> error 0
#> warning 2
#> note 0
#>
#> Warnings
#> --------
#> [define_orphan_leaf] Document LF.ADQSADAS is defined but nothing references it.
#> [define_orphan_standard] Standard STD.5 is defined but nothing references it.
#>