---
title: "Writing custom RDML importers"
author: "RDML7 authors"
date: "`r Sys.Date()`"
output:
  rmarkdown::html_vignette:
    toc: true
    toc_depth: 3
vignette: >
  %\VignetteIndexEntry{Writing custom RDML importers}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

# Overview

RDML7 provides two levels for writing file importers.

The **simple form** is intended for formats that can be represented as one
fluorescence table plus one metadata table:

```text
vendor file
    |
    v
parser
    |
    +-- fdata
    |
    `-- description
            |
            v
    buildRDMLFromFData()
            |
            v
         rdmlType
```

The **complex form** is intended for formats that contain several independent
fluorescence-series groups, for example amplification and melting data, or
several data blocks that should be parsed first and converted to RDML in one
central step:

```text
vendor file
    |
    v
parser
    |
    +-- rdmlImportSeries (adp)
    +-- rdmlImportSeries (mdp)
    `-- ...
            |
            v
       rdmlImportData
            |
            v
       buildRDMLImport()
            |
            v
         rdmlType
```

Both forms keep the vendor-specific parser separate from the nested RDML/S7
schema construction.

This separation is useful because a parser should primarily answer questions
such as:

- where are the fluorescence values in this file?
- which reaction belongs to which sample?
- which target and dye were used?
- which run and reaction identifiers are present?
- does the file contain amplification (`adp`), melting (`mdp`), or both?

The common RDML builder then handles creation of `experimentType`, `runType`,
`reactType`, `dataType`, `sampleType`, `targetType`, dyes, references, and
fluorescence-point objects.

# Registering a reader

After a reader has been written, register it with the minimal format registry:

```{r registry, eval=FALSE}
registerRDMLFormat(
  name = "myformat",
  extensions = "abc",
  reader = readMyFormat
)
```

A format may provide a reader, a writer, or both:

```r
registerRDMLFormat(
  name,
  extensions = character(),
  reader = NULL,
  writer = NULL
)
```

Automatic dispatch uses the file extension. A different registered format can
be selected explicitly with `format =`.

```{r explicit-format, eval=FALSE}
x <- readRDML(
  "experiment.abc",
  format = "myformat"
)
```

A reader may return either:

1. a finished `rdmlType`; or
2. an `rdmlImportData` intermediate object.

When a reader returns `rdmlImportData`, `readRDML()` converts it to `rdmlType`
with the common importer builder.

# The common table contract

Both simple and complex importers ultimately use the same table representation.

## Fluorescence table

`fdata` is a wide table. Its first column is the coordinate:

- `cyc` for amplification data;
- `tmp` for melting data.

All remaining columns are fluorescence series.

For example:

```{r fdata-example}
fdata <- data.frame(
  cyc = 1:5,
  r1 = c(100, 102, 110, 180, 650),
  r2 = c(101, 103, 112, 190, 690)
)

fdata
```

The fluorescence column names (`r1`, `r2`) are arbitrary identifiers used to
join the fluorescence data to the description table.

## Description table

The corresponding description table contains one row per fluorescence series.

```{r description-example}
description <- data.frame(
  fdataName = c("r1", "r2"),
  expId = c("exp1", "exp1"),
  runId = c("run1", "run1"),
  reactId = c("1", "2"),
  sample = c("sample1", "sample2"),
  sampleType = c("std", "unkn"),
  target = c("CD53", "CD53"),
  targetDyeId = c("FAM", "FAM"),
  stringsAsFactors = FALSE
)

description
```

The required columns are:

```text
fdataName
expId
runId
reactId
target
```

In practice, importers commonly also provide:

```text
sample
sampleType
targetDyeId
```

Other supported metadata can be added when present in the source file, for
example:

```text
targetType
quantity
cq
meltTemp
meltTemp
```

The exact optional fields accepted by the installed version of RDML7 are
documented by `setFData()`.

The key rule is:

```text
description$fdataName <-> names(fdata)[-1]
```

Each fluorescence column should have exactly one matching description row.

# Simple importer

## When to use the simple form

Use `buildRDMLFromFData()` when the file naturally contains **one fluorescence
series group**:

- one amplification table; or
- one melting table.

This is the preferred interface for most third-party readers.

Typical examples are:

- a CSV export containing amplification curves;
- a simple text export with one curve table;
- a custom laboratory format containing one qPCR run;
- a format that already has one row of metadata per fluorescence series.

The parser only needs to produce `fdata` and `description`.

## Minimal simple reader

```{r simple-reader, eval=FALSE}
readMyFormat <- function(fileName, ...) {

  # Vendor-specific parsing
  fdata <- ...
  description <- ...

  buildRDMLFromFData(
    fdata = fdata,
    description = description,
    fdataType = "adp"
  )
}
```

The reader does **not** need to construct nested S7 objects manually.

It should not need code such as:

```{r avoid-schema-construction, eval=FALSE}
# Do not do this in a normal file parser:
experimentType(
  run = list(
    runType(
      react = list(
        reactType(
          data = list(
            dataType(...)
          )
        )
      )
    )
  )
)
```

That construction belongs to the common RDML builder.

# Example: a simple text qPCR format

Consider a deliberately small text format with three sections:

```text
# Simple qPCR format

[meta]
experiment=cd53_efficiency
run=run1
target=CD53
dye=FAM

[reactions]
fdataName  reactId  sample       sampleType  quantity
r1         1        CD53_1       std         1
r2         2        CD53_0.1     std         0.1
r3         3        CD53_0.01    std         0.01
r4         4        CD53_0.001   std         0.001

[adp]
cyc  r1   r2   r3   r4
1    100  100  100  100
2    101  100  101  100
3    103  102  101  101
...
```

The format deliberately stores `quantity` as metadata rather than encoding it
only in the sample name.

A reader can parse these sections and then convert them to the common tables.

```{r simple-text-reader, eval=FALSE}
readSimpleQPCR <- function(fileName, ...) {

  lines <- readLines(
    fileName,
    warn = FALSE,
    encoding = "UTF-8"
  )

  getSection <- function(name) {
    marker <- paste0("[", name, "]")
    start <- match(marker, lines)

    if (is.na(start)) {
      stop(
        "Missing section ",
        marker,
        call. = FALSE
      )
    }

    sections <- grep(
      "^\\[[^]]+\\]$",
      lines
    )

    nextSection <- sections[
      sections > start
    ]

    end <- if (length(nextSection)) {
      nextSection[[1L]] - 1L
    } else {
      length(lines)
    }

    out <- lines[
      seq.int(
        start + 1L,
        end
      )
    ]

    out[
      nzchar(trimws(out))
    ]
  }

  # Metadata -------------------------------------------------------------
  metaParts <- strsplit(
    getSection("meta"),
    "=",
    fixed = TRUE
  )

  meta <- stats::setNames(
    vapply(
      metaParts,
      function(x) {
        paste(
          x[-1L],
          collapse = "="
        )
      },
      character(1)
    ),
    vapply(
      metaParts,
      `[[`,
      character(1),
      1L
    )
  )

  # Reaction metadata ----------------------------------------------------
  reactions <- utils::read.delim(
    text = paste(
      getSection("reactions"),
      collapse = "\n"
    ),
    stringsAsFactors = FALSE,
    check.names = FALSE
  )

  # Amplification curves -------------------------------------------------
  fdata <- utils::read.delim(
    text = paste(
      getSection("adp"),
      collapse = "\n"
    ),
    stringsAsFactors = FALSE,
    check.names = FALSE
  )

  description <- data.frame(
    fdataName = reactions$fdataName,
    expId = meta[["experiment"]],
    runId = meta[["run"]],
    reactId = as.character(
      reactions$reactId
    ),
    sample = reactions$sample,
    sampleType = reactions$sampleType,
    target = meta[["target"]],
    targetDyeId = meta[["dye"]],
    quantity = as.numeric(
      reactions$quantity
    ),
    stringsAsFactors = FALSE
  )

  buildRDMLFromFData(
    fdata = fdata,
    description = description,
    fdataType = "adp",
    publisher = "simpleqpcr"
  )
}
```

Register the reader:

```{r register-simple-reader, eval=FALSE}
registerRDMLFormat(
  name = "simpleqpcr",
  extensions = "sqpcr",
  reader = readSimpleQPCR
)
```

It can then be used through the normal RDML7 interface:

```{r use-simple-reader, eval=FALSE}
x <- readRDML(
  "cd53_efficiency.sqpcr"
)

summary(x)

asTable(x)

amp <- getFData(
  x,
  dpType = "adp",
  longTable = TRUE
)
```

The parser knows nothing about the internal structure of `sampleType`,
`quantityType`, `targetType`, `experimentType`, or `dataType`. It only maps the
source file into the standard fluorescence and description tables.

# Complex importer

## When to use the complex form

Use the canonical intermediate representation when a single source file
contains **more than one independently described fluorescence-series group**.

The most common case is:

```text
one source file
    |
    +-- amplification curves (adp)
    |
    `-- melting curves (mdp)
```

Other reasons include:

- separate curve blocks with different coordinate systems;
- several tables that should be validated before any RDML object is built;
- source metadata that belongs to the import as a whole rather than to one
  fluorescence table;
- a parser that should preserve original reaction identifiers;
- an importer that needs to declare conversion information centrally.

In this situation it is useful to parse the complete vendor file first and
only then construct the RDML hierarchy.

# `rdmlImportSeries`

One `rdmlImportSeries` represents one homogeneous fluorescence-series group.

For amplification data:

```{r complex-adp, eval=FALSE}
ampSeries <- newRDMLImportSeries(
  fdataType = "adp",
  fdata = ampFdata,
  description = ampDescription
)
```

For melting data:

```{r complex-mdp, eval=FALSE}
meltSeries <- newRDMLImportSeries(
  fdataType = "mdp",
  fdata = meltFdata,
  description = meltDescription
)
```

Each series has its own:

```text
fdataType
fdata
description
```

This is important because `adp` and `mdp` have different coordinates and may
contain different subsets of reactions.

# `rdmlImportData`

`rdmlImportData` represents the complete result of parsing one source file.

```{r complex-import-data, eval=FALSE}
parsed <- newRDMLImportData(
  series = list(
    ampSeries,
    meltSeries
  ),
  publisher = "Example vendor",
  serialNumber = "12345",
  format = "myformat"
)
```

The object groups the fluorescence-series blocks and source-level metadata
without yet constructing the nested RDML hierarchy.

This gives the parser a clean boundary:

```text
everything above rdmlImportData = vendor parsing
everything below rdmlImportData = RDML construction
```

# Building the RDML object explicitly

The parsed representation can be converted explicitly:

```{r build-complex, eval=FALSE}
x <- buildRDMLImport(
  parsed
)
```

The builder processes each series with the common fluorescence-data machinery
and returns one `rdmlType`.

Conceptually:

```text
rdmlImportData
    |
    +-- series 1 -> setFData(..., fdataType = "adp")
    |
    +-- series 2 -> setFData(..., fdataType = "mdp")
    |
    `-- ...
            |
            v
         rdmlType
```

This means all complex importers use the same RDML construction rules.

# Returning intermediate data directly from a reader

A complex reader does not have to call `buildRDMLImport()` itself.

It may return `rdmlImportData`:

```{r complex-reader, eval=FALSE}
readMyComplexFormat <- function(fileName, ...) {

  # Parse the source file.
  ampFdata <- ...
  ampDescription <- ...

  meltFdata <- ...
  meltDescription <- ...

  ampSeries <- newRDMLImportSeries(
    fdataType = "adp",
    fdata = ampFdata,
    description = ampDescription
  )

  meltSeries <- newRDMLImportSeries(
    fdataType = "mdp",
    fdata = meltFdata,
    description = meltDescription
  )

  newRDMLImportData(
    series = list(
      ampSeries,
      meltSeries
    ),
    publisher = "Example vendor",
    serialNumber = "12345",
    format = "mycomplexformat"
  )
}
```

Register it normally:

```{r register-complex-reader, eval=FALSE}
registerRDMLFormat(
  name = "mycomplexformat",
  extensions = "mcf",
  reader = readMyComplexFormat
)
```

Then:

```{r use-complex-reader, eval=FALSE}
x <- readRDML(
  "experiment.mcf"
)
```

`readRDML()` detects that the reader returned `rdmlImportData` and runs the
common builder before returning the final `rdmlType`.

This is the preferred complex-reader pattern because the reader remains a
pure parser.

# Why not always use the complex form?

The intermediate representation is useful, but it should not be required for
simple formats.

For a single amplification table, this:

```{r simple-again, eval=FALSE}
buildRDMLFromFData(
  fdata,
  description,
  fdataType = "adp"
)
```

is clearer than:

```{r overengineered-simple, eval=FALSE}
series <- newRDMLImportSeries(
  "adp",
  fdata,
  description
)

parsed <- newRDMLImportData(
  series = list(series)
)

buildRDMLImport(parsed)
```

The second form adds objects without adding information.

Use the lowest-level interface that faithfully represents the source format.

# Choosing between the two forms

| Source format | Recommended form |
|---|---|
| One amplification table | `buildRDMLFromFData()` |
| One melting table | `buildRDMLFromFData()` |
| Simple CSV or TSV curve export | `buildRDMLFromFData()` |
| One table plus straightforward metadata | `buildRDMLFromFData()` |
| Amplification + melting in one file | canonical importer |
| Several independent fluorescence blocks | canonical importer |
| Need to parse/validate all blocks before building RDML | canonical importer |
| Need source-level importer metadata | canonical importer |

A useful rule is:

> If the source can be described by one `fdata` table and one `description`
> table, use `buildRDMLFromFData()`. If it naturally requires a list of such
> pairs, use `rdmlImportData`.

# Relationship between the two APIs

The simple API is not a separate implementation.

Conceptually:

```text
buildRDMLFromFData()
        |
        v
newRDMLImportSeries()
        |
        v
newRDMLImportData()
        |
        v
buildRDMLImport()
```

In other words, the simple function is a convenience layer over the same
canonical construction path.

This is important because both simple and complex third-party importers get the
same behavior for:

- creation of experiments and runs;
- creation of reactions;
- sample and target references;
- dyes;
- sample types;
- quantities;
- Cq and melting metadata;
- amplification and melting curve storage;
- conflict handling.

# Reader responsibilities

A custom reader **should**:

1. parse the external file;
2. normalize identifiers to character values;
3. construct the standard `fdata` and `description` tables;
4. preserve meaningful source metadata;
5. report malformed input clearly;
6. return either `rdmlType` or `rdmlImportData`.

A custom reader normally **should not**:

1. manually construct the complete nested RDML hierarchy;
2. duplicate `setFData()` logic;
3. invent missing biological metadata;
4. silently change sample or target identifiers;
5. encode format detection logic inside the parser.

Format selection is handled by the registry; RDML construction is handled by
the common builder.

# Validating an importer

A useful development loop is:

```{r importer-validation, eval=FALSE}
registerRDMLFormat(
  name = "myformat",
  extensions = "abc",
  reader = readMyFormat
)

x <- readRDML(
  "example.abc"
)

summary(x)

tbl <- asTable(x)
tbl

validateRDML(x)

amp <- getFData(
  x,
  dpType = "adp",
  longTable = TRUE
)
```

For a format containing melting data, inspect those separately:

```{r importer-validation-mdp, eval=FALSE}
melt <- getFData(
  x,
  dpType = "mdp",
  longTable = TRUE
)
```

During importer development, verify at least:

- number of experiments and runs;
- reaction identifiers;
- sample identifiers;
- target identifiers;
- sample types;
- quantities, if provided;
- dye references;
- number of fluorescence curves;
- number and order of curve coordinates;
- `adp` versus `mdp` assignment.

# Testing a reader

A small test should verify both dispatch and biological metadata.

```{r reader-test, eval=FALSE}
test_that("myformat reader imports amplification curves", {

  x <- readRDML(
    test_path(
      "data",
      "example.abc"
    )
  )

  expect_true(
    S7::S7_inherits(
      x,
      rdmlType
    )
  )

  tbl <- asTable(x)

  expect_true(
    all(tbl$adp)
  )

  expect_setequal(
    unique(tbl$target),
    "CD53"
  )

  amp <- getFData(
    x,
    dpType = "adp"
  )

  expect_gt(
    nrow(amp),
    0L
  )
})
```

For a complex reader:

```{r complex-reader-test, eval=FALSE}
test_that("complex reader imports both amplification and melting data", {

  x <- readRDML(
    test_path(
      "data",
      "example.mcf"
    )
  )

  tbl <- asTable(x)

  expect_true(
    any(tbl$adp)
  )

  expect_true(
    any(tbl$mdp)
  )
})
```

# Summary

RDML7 intentionally provides two importer interfaces.

The simple form:

```r
buildRDMLFromFData(
  fdata,
  description,
  fdataType
)
```

is the preferred choice for a source represented by one fluorescence table.

The complex form:

```r
series <- newRDMLImportSeries(...)
parsed <- newRDMLImportData(
  series = list(...)
)

buildRDMLImport(parsed)
```

is useful when a file contains several independently described fluorescence
groups.

The format registry remains independent of both approaches:

```r
registerRDMLFormat(
  name,
  extensions,
  reader,
  writer
)
```

This keeps the three responsibilities separate:

```text
registry       -> which reader handles this file?
parser         -> what data are stored in this vendor format?
RDML builder   -> how are those data represented as rdmlType?
```

That separation is the main reason the importer API exists.
