Variant 3 — Plumber Drop-In (drogonR::pr_run)

# Variant 3 — Plumber Drop-In (`drogonR::pr_run`)

If you already have a plumber service, the shim lets you run it under drogonR by changing one line. The shim parses the plumber router into drogonR routes and dispatches them through `dr_serve()`. Existing handlers, paths, and parameter types keep working.

For the overall picture see `vignette("drogonR", package = "drogonR")`.

## The one-line swap

Existing plumber code:

    library(plumber)
    pr <- pr() |>
      pr_get ("/users/<id:int>", function(id) list(id = id, ok = TRUE)) |>
      pr_post("/users",          function(req) {
        body <- jsonlite::fromJSON(req$postBody)
        list(created = body$name)
      })

    plumber::pr_run(pr, port = 8080L, docs = FALSE)   # <-- before

Becomes:

    drogonR::pr_run(pr, port = 8080L, docs = FALSE)   # <-- after

That’s the whole change. `docs`, `swagger`, `swaggerCallback`, `quiet` are silently accepted and ignored (the shim has no swagger surface, so the flags are inapplicable but valid). Other arguments — `threads`, `workers`, `max_queue` — forward to `dr_serve()`.

## What the shim supports

- `@get`, `@post`, `@put`, `@delete` annotations and the `pr_get/post/put/delete()` helpers.
- Path placeholders `<name>` and `<name:type>`. Recognised types are `int` / `integer`, `dbl` / `double` / `numeric`, `bool` / `logical`; anything else passes through as character. Coercion runs only on path parameters — query and body values keep plumber’s untyped shape (string for query, parsed-JSON for body).
- Handler argument resolution by name: `path > query > JSON body`, with `req` injected if the handler declares a `req` parameter.
- Plumber 1.x default serialisation: `jsonlite::toJSON(auto_unbox = FALSE)` for every return value. Bare strings become JSON arrays (`["hello"]`), exactly as plumber sends them — byte-level parity with `plumber::pr_run()`. If you’ve already built a JSON string with `jsonlite::toJSON()`, it’s emitted verbatim.
- Returning a `dr_response()` / `dr_json()` / `dr_text()` from a handler opts out of the default serializer and is forwarded as-is — useful for incrementally migrating hot endpoints to drogonR’s response shape without leaving the shim.

## What the shim rejects

Each of these triggers an explicit error at `pr_run()` time, before any route is registered, so failure is loud:

- `@filter` / `pr_filter()` — user-defined filters. Rewrite as middleware via `dr_use()` (see `vignette("mode-native")`).
- `pr_hook()` / `@hook` — preroute / postroute / postserialize hooks. Same migration path as filters.
- `pr_mount()` / sub-routers — composing one router from several. Flatten into a single `pr()` (or move to native `dr_app()`).
- Custom parsers / serialisers — every response goes through the default plumber JSON serializer. Build the response yourself with `dr_response(headers = list("Content-Type" = "..."))` if you need another format.
- `PlumberResponse` / `PlumberFile` return values — return a list / data.frame for JSON, a string for text, or a `dr_response()` for full control.
- The `res` parameter in handlers — plumber-style mutation of a passed-in `res` object isn’t supported. The shim warns once per affected route at `pr_run()` time. Set status / headers via the return value (`dr_response(...)`).
- Async handlers, websockets, OpenAPI / swagger assets — out of scope for the shim.

## A minimal end-to-end example

    library(plumber)
    library(drogonR)

    pr <- pr() |>
      pr_get ("/health",                function() list(ok = TRUE)) |>
      pr_get ("/users/<id:int>",        function(id) {
        list(id = id, type = typeof(id))     # id arrives as integer
      }) |>
      pr_post("/echo",                  function(req) {
        list(received = jsonlite::fromJSON(req$postBody))
      })

    drogonR::pr_run(pr, port = 8080L, docs = FALSE)

Three responses your client will see:

    GET /health         -> {"ok":[true]}
    GET /users/42       -> {"id":[42],"type":["integer"]}
    POST /echo {"a":1}  -> {"received":{"a":[1]}}

The bracketed scalars are plumber’s default serialiser (`auto_unbox = FALSE`) — preserved on purpose so existing clients don’t break. To get unboxed JSON, return `dr_json(x, auto_unbox = TRUE)` from the handler; that bypasses the default serializer.

## When to migrate to native

The shim is fine for steady-state plumber apps. Reach for the native API (`vignette("mode-native")`) if you want:

- per-request middleware (auth, logging, CORS, rate limiting),
- explicit control over status / headers / content-type without per-handler `dr_response()` calls,
- response helpers (`dr_text`, `dr_html`, `dr_redirect`, `dr_file`),
- the full request shape (`req$params`, `dr_query()`, `dr_body()`),
- path placeholders with `:id` / `<id>` / `{id}` syntax (the shim rewrites plumber’s `<id:type>` form internally).

If your hot endpoint is C/C++-bound (model inference, embeddings), register that endpoint with `dr_get_cpp()` and leave the rest under the shim — see `vignette("mode-cpp-shared", package = "drogonR")`.