| Title: | Connect R and 'Shiny' to 'QuPath' Through a Local Bridge |
|---|---|
| Description: | Development version of an interface between R and the 'QuPath' 0.7.0 platform for digital pathology image analysis. The planned design lets R talk to a local 'Java' bridge (a 'QuPath' extension and a headless worker) over an authenticated loopback protocol, adds a 'Shiny' app that is built on the same R functions, and packages analysis workflows so they can be shared and run again. This version contains only the package foundations: classified error conditions, a message catalogue, protocol JSON helpers and a JSON Schema subset validator with the protocol schemas, plus a hash-pinned external-method execution boundary. The bridge client, the app and the workflow engine are not implemented yet. |
| Authors: | R. Heller [aut, cre, cph] |
| Maintainer: | R. Heller <[email protected]> |
| License: | GPL (>= 3) |
| Version: | 0.0.0.9000 |
| Built: | 2026-11-03 18:16:24 UTC |
| Source: | https://github.com/CTTIR/qupflowR |
qp_abort() is the single way qupflowR raises errors. It looks up code
in the protocol error registry, builds the condition classes
c(class, <registry class>, "qp_error") and formats the message from the
message catalogue in the language chosen by the option
qupflowR.language (see qp_msg()).
qp_abort( code, ..., class = NULL, details = list(), request_id = NULL, action = NULL, message_key = NULL, bullets = NULL, parent = NULL, call = parent.frame() )qp_abort( code, ..., class = NULL, details = list(), request_id = NULL, action = NULL, message_key = NULL, bullets = NULL, parent = NULL, call = parent.frame() )
code |
Error code, a single string of upper-case letters, digits and
underscores such as |
... |
Named values for the placeholders of the message template, for
example |
class |
Additional condition classes placed before the registry class. |
details |
Named list with machine-readable context. It is passed
through |
request_id |
Protocol request ID (UUID string) or |
action |
Short machine-readable suggested action (for example
|
message_key |
Message key that replaces the registry message of the
code, or |
bullets |
Optional character vector of additional message lines. Names
are cli bullet types ( |
parent |
Parent condition or |
call |
Environment of the function reported as the error call. |
Does not return; signals a condition inheriting from qp_error
with the fields code, message_key, details, request_id, action,
retryable (logical, NA if unknown) and http_status (integer, NA
for codes that are never sent over HTTP). Its class is the registry
class of code, for example qp_busy_error for HOST_BUSY. An invalid
code, class, details, request_id, action, message_key or
bullets argument signals a qp_validation_error with code
INVALID_ARGUMENT instead.
qp_error_codes(), qp_condition_code(), qp_redact()
cnd <- tryCatch( qp_abort("HOST_BUSY", details = list(reason = "connection_limit")), qp_error = function(e) e ) class(cnd) cnd$code cnd$retryablecnd <- tryCatch( qp_abort("HOST_BUSY", details = list(reason = "connection_limit")), qp_error = function(e) e ) class(cnd) cnd$code cnd$retryable
Code of a qupflowR condition
qp_condition_code(cnd)qp_condition_code(cnd)
cnd |
A condition object. |
The condition's code as a single string, or NA_character_ when
cnd is not a qupflowR condition with a code. It signals no condition
of its own, whatever cnd is.
cnd <- tryCatch(qp_abort("REVISION_CONFLICT"), error = function(e) e) qp_condition_code(cnd) qp_condition_code(simpleCondition("plain"))cnd <- tryCatch(qp_abort("REVISION_CONFLICT"), error = function(e) e) qp_condition_code(cnd) qp_condition_code(simpleCondition("plain"))
The registry lists every error code used by the bridge protocol and by the
R-side workflow, storage, JSON and schema layers. The same file
(inst/schema/protocol/v1/error-codes.json) is read by the Java bridge.
qp_error_codes()qp_error_codes()
A tibble with one row per code and the columns code (character),
http_status (integer; NA for codes that are never sent over HTTP),
r_class (character), retryable (logical), message_key (character)
and source (character, the contract that defines the code). It takes
no arguments and signals no condition with the registry that is
installed with the package.
codes <- qp_error_codes() codes[codes$retryable, c("code", "http_status")]codes <- qp_error_codes() codes[codes$retryable, c("code", "http_status")]
Converts the error object of a decoded protocol v1 error envelope into
the matching qupflowR condition. Known codes get their registry class;
unknown codes keep their code with the class qp_error only. The host's
own message is added as an extra line, and details are redacted.
qp_error_from_response(envelope, signal = TRUE, call = parent.frame())qp_error_from_response(envelope, signal = TRUE, call = parent.frame())
envelope |
Decoded error envelope, a named list as returned by
|
signal |
If |
call |
Environment reported as the call of the condition. |
With signal = FALSE the condition object; otherwise the function
does not return. A malformed envelope signals a qp_version_error with
code PROTOCOL_VIOLATION.
envelope <- qp_json_decode(paste0( '{"protocol_version":"1.0",', '"request_id":"0d9a3c5e-8f7b-4a21-9c3d-2e1f0a9b8c7d",', '"instance_id":"5b1f8a2c-3d4e-4f60-8a9b-0c1d2e3f4a5b",', '"error":{"code":"HOST_BUSY","message_key":"error.host_busy",', '"message":"Too many open connections; retry later",', '"details":{"reason":"connection_limit"},"retryable":true},', '"warnings":[]}' )) cnd <- qp_error_from_response(envelope, signal = FALSE) class(cnd) cnd$retryableenvelope <- qp_json_decode(paste0( '{"protocol_version":"1.0",', '"request_id":"0d9a3c5e-8f7b-4a21-9c3d-2e1f0a9b8c7d",', '"instance_id":"5b1f8a2c-3d4e-4f60-8a9b-0c1d2e3f4a5b",', '"error":{"code":"HOST_BUSY","message_key":"error.host_busy",', '"message":"Too many open connections; retry later",', '"details":{"reason":"connection_limit"},"retryable":true},', '"warnings":[]}' )) cnd <- qp_error_from_response(envelope, signal = FALSE) class(cnd) cnd$retryable
Produces the canonical JSON text used as digest input. Two documents with the same JSON value have byte-identical canonical forms in R and in the Java bridge.
qp_json_canonical(x)qp_json_canonical(x)
x |
An R value following the conventions of |
Rules:
No insignificant whitespace and no trailing newline.
Object members are sorted by their names compared as sequences of Unicode code points (the same order as comparing the UTF-8 bytes). This differs from RFC 8785, which compares UTF-16 code units, for names that mix characters above U+FFFF with characters from U+E000 to U+FFFF. Names must be unique and non-empty.
Strings are UTF-8. " and \ are escaped as \" and \\; U+0008,
U+0009, U+000A, U+000C and U+000D as \b, \t, \n, \f and \r;
other characters below U+0020 as \u00xx with lower-case hex digits.
Every other character, including non-ASCII, is written literally.
Numbers must be integers with magnitude at most 2^53 - 1, written as
decimal digits with an optional minus sign, without exponent, fraction
or leading zeros; negative zero is written as 0. Non-integer numbers
are rejected (JSON_UNREPRESENTABLE, reason non_integer_number)
because shortest round-trip formatting of doubles is not guaranteed to
agree between languages; digested documents store fractional values as
decimal strings. In JSON text input every number token with a fraction
or exponent is rejected, even 1.0.
Arrays keep their order; true, false and null are literals.
A single UTF-8 string with class json. Use
charToRaw(qp_json_canonical(x)) for the digest input bytes. A value
that breaks rules 2 or 4 (or cannot be JSON at all) signals a
qp_serialization_error with code JSON_UNREPRESENTABLE; invalid JSON
text signals a qp_serialization_error with code JSON_INVALID. Both
carry the rule in details$reason.
qp_json_canonical(qp_as_json('{"b": 2, "a": [true, null, "\\u00e9"]}')) qp_json_canonical(list(z = qp_scalar(1L), a = qp_scalar("x")))qp_json_canonical(qp_as_json('{"b": 2, "a": [true, null, "\\u00e9"]}')) qp_json_canonical(list(z = qp_scalar(1L), a = qp_scalar("x")))
qp_json_encode() turns R values into JSON text and qp_json_decode()
turns JSON text into R values, following one explicit convention so that
qp_json_encode(qp_json_decode(txt)) gives back the same JSON value.
qp_json_encode(x, schema_hint = NULL) qp_json_decode(txt)qp_json_encode(x, schema_hint = NULL) qp_json_decode(txt)
x |
R value to encode. |
schema_hint |
|
txt |
JSON text: a single string, a raw vector of UTF-8 bytes, or text
marked with |
qp_json_encode() returns a single UTF-8 string with class json.
qp_json_decode() returns the decoded R value.
NULL becomes null.
A qp_scalar() value (class scalar, as made by jsonlite::unbox())
becomes a single number, string, true or false; NA becomes null.
Every other atomic vector becomes an array, at length 1 and 0 as well;
NA elements become null.
An unnamed list, including list(), becomes an array.
A named list, including qp_empty_object(), becomes an object.
Length-1 vectors are never unboxed silently: mark scalars with
qp_scalar() or pass schema_hint.
Doubles that hold whole numbers with magnitude up to 2^53 are written
as plain digits (5000000000, never 5e+09). Other doubles use the
shortest of 15, 16 or 17 significant digits that reads back to the same
double.
NaN, Inf and -Inf are rejected with a qp_serialization_error
(code JSON_UNREPRESENTABLE); JSON has no such literals. Measurement
values use value plus value_state, see qp_value_state().
Strings are written as UTF-8. Only ", \ and control characters are
escaped; invalid UTF-8 is rejected.
Object names must be non-empty where given, unique and valid UTF-8.
Factors, dates, matrices, data frames, named atomic vectors, raw vectors, functions and environments are rejected instead of being guessed.
Objects become named lists ({} becomes qp_empty_object()), null
becomes NULL.
Single values become length-1 vectors marked with qp_scalar().
Arrays whose elements are all strings, all numbers or all booleans
(with optional nulls) become atomic vectors with NA for null;
other arrays become unnamed lists. [] becomes list().
Numbers without fraction or exponent that fit a 32-bit integer become integers, all others doubles. Integers beyond 2^53 lose precision like in any double-based reader; the protocol does not use them.
Duplicate or empty object keys, a byte-order mark, invalid UTF-8,
escaped NUL characters, unpaired surrogate escapes, numbers outside the
double range and nesting deeper than 128 levels are rejected
(JSON_INVALID or JSON_UNREPRESENTABLE). So are comments, white
space other than space, tab, line feed and carriage return between
tokens, and an unterminated string, also after a complete value as in
[1]" (JSON_INVALID, reason syntax), as RFC 8259 and the Java bridge
require. R cannot tell an empty member name from a missing one, so empty
names are not supported in either direction.
The checks before parsing take time proportional to the length of the text.
qp_json_canonical(), qp_schema_validate()
qp_json_encode(list( image_id = qp_scalar("1"), object_ids = "8f6589c0-c587-4bfa-b740-8cab06bc8ce1", size_bytes = qp_scalar(5e9), name = NULL, metadata = qp_empty_object() )) x <- qp_json_decode('{"plane":{"c":null,"z":0,"t":1},"ids":["a"]}') x$ids qp_json_encode(x)qp_json_encode(list( image_id = qp_scalar("1"), object_ids = "8f6589c0-c587-4bfa-b740-8cab06bc8ce1", size_bytes = qp_scalar(5e9), name = NULL, metadata = qp_empty_object() )) x <- qp_json_decode('{"plane":{"c":null,"z":0,"t":1},"ids":["a"]}') x$ids qp_json_encode(x)
Looks up key in the message catalogue (inst/i18n/messages.json) and
fills the placeholders of the template with the named values in ....
The language comes from .lang, which defaults to the option
qupflowR.language ("de" when unset). A key missing in that language
falls back to English, and a key missing in the catalogue is returned
unchanged.
qp_msg(key, ..., .lang = getOption("qupflowR.language", "de"))qp_msg(key, ..., .lang = getOption("qupflowR.language", "de"))
key |
Message key such as |
... |
Named values for the placeholders. |
.lang |
Language code. Region suffixes are ignored ( |
Templates use cli inline markup, for example {.arg {arg}}. Values are
inserted as text: braces inside a value are shown literally and are never
evaluated. A placeholder without a value is shown as <name>. The result
is plain text without ANSI styling, so it can be used in the Shiny app.
A single string. A key that is not a single message key, or
unnamed values in ..., signal a qp_validation_error with code
INVALID_ARGUMENT; an unknown key is not an error.
qp_msg("error.host_busy", .lang = "en") qp_msg("error.invalid_argument", arg = "image_id", expected = "a single string", .lang = "en") qp_msg("error.host_busy", .lang = "de")qp_msg("error.host_busy", .lang = "en") qp_msg("error.invalid_argument", arg = "image_id", expected = "a single string", .lang = "en") qp_msg("error.host_busy", .lang = "de")
Removes credentials and bounds the size of values before they are stored in a condition, a log or a diagnostic report.
qp_redact(x, max_chars = 500L, max_items = 50L, max_depth = 5L)qp_redact(x, max_chars = 500L, max_items = 50L, max_depth = 5L)
x |
Value to redact, usually a named list. |
max_chars, max_items, max_depth
|
Positive whole numbers bounding the string length, the number of elements and the nesting depth. |
Rules:
Elements whose name contains token, secret, password, passwd,
authorization, cookie, credential(s), api_key or private_key
as a word part are replaced by "<redacted>". Names ending in _file,
_path or _dir (for example token_file) name locations, not
secrets, and are kept.
In every string, Bearer <credential> and token=, access_token=,
api_key=, password= or secret= query values are replaced, and a
string that consists only of a 43-character base64url value (the form
of a 256-bit bridge token) becomes "<redacted>".
Strings longer than max_chars characters are truncated with a note.
Vectors and lists longer than max_items keep their first max_items
elements and gain the attribute truncated_from (original length).
Lists nested deeper than max_depth and raw vectors, functions,
environments and other non-data objects are replaced by a short
description.
JSON scalar marks (qp_scalar()) are removed, so decoded protocol
details become plain R values.
The redacted value with the same structure where possible.
Bounds that are not positive whole numbers signal a
qp_validation_error with code INVALID_ARGUMENT.
qp_redact(list( token = "abc", header = "Authorization: Bearer abc.def", token_file = "/run/user/1000/qupflowr/token" ))qp_redact(list( token = "abc", header = "Authorization: Bearer abc.def", token_file = "/run/user/1000/qupflowr/token" ))
Execute an explicitly trusted method without rewriting its script or analytical settings. This compatibility boundary does not use the live Java bridge and does not imply support for a different QuPath version. No program starts until trust and all file hashes have been checked.
qp_run_external( program, args, pins, outputs, workspace, timeout = 600, trust = FALSE, env = character() )qp_run_external( program, args, pins, outputs, workspace, timeout = 600, trust = FALSE, env = character() )
program |
Absolute path to the executable, also included in |
args |
Character argument vector. Literal |
pins |
Data frame with |
outputs |
Non-empty vector of relative output file paths. Files must be non-empty and resolve inside the workspace. Their hashes are recorded. Presence does not validate analytical content: the pinned method must perform its own semantic validation before returning successfully. |
workspace |
A new, not-yet-existing directory in an existing parent. It is retained on both success and failure, with logs and a run receipt. |
timeout |
Finite positive time limit in seconds. |
trust |
Exactly |
env |
Named character vector of additional environment variables. Home, display, temporary-directory and Java-option overrides are forbidden. Do not supply secrets: the full explicit child environment is recorded. |
The child receives a stripped environment, private home/XDG/Java preference and temporary directories, no display, and a bounded timeout. Private temporary directories are removed after the child exits. This is process hygiene, not a sandbox for malicious trusted code. Only processes started by this call are cleaned up. Existing output directories are never reused. No downloads, dependency installation or silent runtime upgrade occur. A nonzero exit or missing output never counts as completion. The initial execution profile supports Linux only; other platforms fail before creating a workspace. In particular, no Windows atomic-replacement or process-cleanup behavior is claimed without platform qualification.
A list with status = "execution_complete", command, pinned file
and output hashes, process exit status, timestamps and workspace. This
proves execution and output presence, not scientific reproduction,
successful project saving or semantic validity of the outputs.
# Configure executable, script/model/input SHA-256 pins and output paths # before explicitly authorizing a method with trust = TRUE.# Configure executable, script/model/input SHA-256 pins and output paths # before explicitly authorizing a method with trust = TRUE.
qp_scalar() marks a length-1 atomic value as a JSON scalar (class
scalar, compatible with jsonlite::unbox()); NA encodes as null.
qp_empty_object() returns the empty named list that encodes as {}.
qp_as_json() marks a string as JSON text for qp_json_canonical() and
qp_schema_validate().
qp_scalar(x) qp_empty_object() qp_as_json(x)qp_scalar(x) qp_empty_object() qp_as_json(x)
x |
For |
qp_scalar(): x with class scalar (NULL stays NULL).
qp_empty_object(): structure(list(), names = character(0)).
qp_as_json(): a single UTF-8 string with class json. An x of the
wrong type or length signals a qp_validation_error with code
INVALID_ARGUMENT; text that is not UTF-8, contains NUL or starts with
a byte order mark signals a qp_serialization_error with code
JSON_INVALID.
qp_json_encode(list(a = qp_scalar(1L), b = 1L, c = qp_empty_object())) qp_as_json('{"a": 1}')qp_json_encode(list(a = qp_scalar(1L), b = 1L, c = qp_empty_object())) qp_as_json('{"a": 1}')
Assert that JSON matches a schema
qp_schema_assert(x, schema, call = parent.frame())qp_schema_assert(x, schema, call = parent.frame())
x |
Value to validate: an R value (it is encoded with
|
schema |
Schema name for |
call |
Environment reported as the call of the error. |
x, invisibly, when it is valid. Otherwise a
qp_validation_error with code VALIDATION_FAILED is signalled; its
details hold schema and up to 20 errors (each with path,
keyword and message).
try(qp_schema_assert(list(status = qp_scalar("ready")), "health"))try(qp_schema_assert(list(status = qp_scalar("ready")), "health"))
Locate an installed JSON Schema
qp_schema_path(name, set = "protocol", version = "v1")qp_schema_path(name, set = "protocol", version = "v1")
name |
Schema name without extension, for example |
set |
Schema set, a directory below |
version |
Major version directory (default |
The absolute path of <set>/<version>/<name>.schema.json inside
the installed package. A missing schema signals a qp_validation_error
with code SCHEMA_INVALID.
basename(qp_schema_path("envelope-error"))basename(qp_schema_path("envelope-error"))
Validates a value with the project's Draft-07 subset validator, the same rules the Java bridge applies to the shared fixture corpus.
qp_schema_validate(x, schema)qp_schema_validate(x, schema)
x |
Value to validate: an R value (it is encoded with
|
schema |
Schema name for |
Supported keywords: $schema, $id, $ref (local only), definitions,
title, description, type, properties, required,
additionalProperties, items (single schema), minItems, maxItems,
uniqueItems, enum, const, minimum, maximum, exclusiveMinimum,
exclusiveMaximum, minLength, maxLength, pattern, format,
oneOf, anyOf, allOf and not. Formats are asserted: uuid
(lower-case 8-4-4-4-12 hex), sha256 (64 lower-case hex digits),
date-time (RFC 3339 with upper-case T and Z, seconds 00-59) and
uri-reference (no white space, control characters, <, >, ", {,
}, |, backslash, ^ or backtick, and valid percent escapes).
Reporting rules, shared with Java: every failing keyword is reported with
the JSON pointer of the instance location. required is reported at the
object, additionalProperties: false at the extra member and a false
schema with the keyword false. oneOf, anyOf and not report a
single error at their location and none of their branch errors. Errors
of $ref targets and allOf branches are reported as they are. Keywords
that do not apply to the instance type are ignored, as in Draft-07.
A qp_validation_report: a list with valid (logical), schema
(file name) and errors, a tibble with the character columns path
(JSON pointer, "" for the root), keyword, message and
schema_path (file and JSON pointer of the failing keyword), sorted by
path and keyword. An unsupported or unresolvable schema signals a
qp_validation_error with code SCHEMA_INVALID.
health <- qp_as_json(paste0( '{"status":"ready","host_type":"worker","qupath_version":"0.7.0",', '"bridge_version":"0.1.0-SNAPSHOT","protocol_version":"1.0",', '"fx_thread_blocked":true}' )) report <- qp_schema_validate(health, "health") report$valid report$errorshealth <- qp_as_json(paste0( '{"status":"ready","host_type":"worker","qupath_version":"0.7.0",', '"bridge_version":"0.1.0-SNAPSHOT","protocol_version":"1.0",', '"fx_thread_blocked":true}' )) report <- qp_schema_validate(health, "health") report$valid report$errors
The protocol carries a measurement as value plus value_state. Only the
state finite has a JSON number; the states missing, nan, pos_inf
and neg_inf have value = null. In R they correspond to NA_real_,
NaN, Inf and -Inf.
qp_value_state(x) qp_value_from_state(value, value_state) qp_value_fields(x)qp_value_state(x) qp_value_from_state(value, value_state) qp_value_fields(x)
x |
Numeric vector (integer or double). |
value |
Numeric vector (with |
value_state |
Character vector of states, parallel to |
qp_value_state(): character vector of states parallel to x.
qp_value_from_state(): double vector. qp_value_fields(): named list
with the scalar-marked fields value (or NULL) and value_state, ready
for qp_json_encode(). Non-numeric input, unknown states, a state that
contradicts its value or vectors of different lengths signal a
qp_validation_error with code INVALID_ARGUMENT.
x <- c(1.5, NA, NaN, Inf, -Inf) state <- qp_value_state(x) state value <- ifelse(state == "finite", x, NA_real_) identical(qp_value_from_state(value, state), x) qp_json_encode(qp_value_fields(NaN))x <- c(1.5, NA, NaN, Inf, -Inf) state <- qp_value_state(x) state value <- ifelse(state == "finite", x, NA_real_) identical(qp_value_from_state(value, state), x) qp_json_encode(qp_value_fields(NaN))
Signal a classified qupflowR warning
qp_warn( code, ..., message_key = NULL, class = NULL, details = list(), bullets = NULL, call = parent.frame() )qp_warn( code, ..., message_key = NULL, class = NULL, details = list(), bullets = NULL, call = parent.frame() )
code |
Warning code, a single string of upper-case letters, digits and underscores. |
... |
Named values for the placeholders of the message template. |
message_key |
Message key; by default |
class |
Additional condition classes placed before |
details |
Named list with machine-readable context, redacted with
|
bullets |
Optional character vector of additional message lines shown
literally (see |
call |
Environment of the function reported as the warning call. |
NULL, invisibly. Signals a warning inheriting from qp_warning
with the fields code, message_key and details. An invalid code,
message_key, class, details or bullets argument signals a
qp_validation_error with code INVALID_ARGUMENT instead.
w <- tryCatch( qp_warn("SNAPSHOT_NEAR_EXPIRY", message_key = "warning.unknown_code"), qp_warning = function(w) w ) w$codew <- tryCatch( qp_warn("SNAPSHOT_NEAR_EXPIRY", message_key = "warning.unknown_code"), qp_warning = function(w) w ) w$code
Signal the warnings of a protocol response
qp_warnings_from_response(envelope, signal = TRUE, call = parent.frame())qp_warnings_from_response(envelope, signal = TRUE, call = parent.frame())
envelope |
Decoded protocol envelope (named list) whose |
signal |
If |
call |
Environment reported as the call of the warnings. |
A list of qp_warning conditions (invisibly when signalled). A
malformed warning entry becomes a warning with code PROTOCOL_VIOLATION.
An envelope that is not a named list, or an invalid signal, signals
a qp_validation_error with code INVALID_ARGUMENT.
envelope <- list(warnings = list(list( code = qp_scalar("SNAPSHOT_NEAR_EXPIRY"), message_key = qp_scalar("warning.snapshot_near_expiry"), message = qp_scalar("Snapshot expires soon") ))) ws <- qp_warnings_from_response(envelope, signal = FALSE) ws[[1]]$codeenvelope <- list(warnings = list(list( code = qp_scalar("SNAPSHOT_NEAR_EXPIRY"), message_key = qp_scalar("warning.snapshot_near_expiry"), message = qp_scalar("Snapshot expires soon") ))) ws <- qp_warnings_from_response(envelope, signal = FALSE) ws[[1]]$code