#![allow(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges), whose shapes belong to the artifacts and the SUT; the carriers \
here are cfg(test)-only, so #[expect] would be unfulfilled in the non-test build"
)]
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::ids::InstanceName;
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
pub enum AuthMode {
None,
Basic {
user_env: String,
password_env: String,
},
Bearer {
token_env: String,
},
BearerMint {
#[serde(default)]
subject: Option<String>,
#[serde(default)]
roles: Option<Vec<String>>,
#[serde(default)]
default_scopes: Vec<String>,
},
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SmartLane {
pub platform_instance: InstanceName,
pub mint: BearerMint,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BearerMint {
pub issuer: String,
#[serde(default)]
pub audience: Option<String>,
pub subject: String,
#[serde(default)]
pub roles: Vec<String>,
pub key_file: PathBuf,
pub kid: String,
pub ttl_seconds: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TerminologyPosture {
FailOpen,
FailClosed,
}
impl TerminologyPosture {
pub const ALL: &[TerminologyPosture] =
&[TerminologyPosture::FailOpen, TerminologyPosture::FailClosed];
#[must_use]
pub fn token(self) -> &'static str {
match self {
Self::FailOpen => "fail_open",
Self::FailClosed => "fail_closed",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SpecProfile {
Stable,
Development,
}
impl SpecProfile {
pub const ALL: &[SpecProfile] = &[SpecProfile::Stable, SpecProfile::Development];
#[must_use]
pub fn token(self) -> &'static str {
match self {
Self::Stable => "stable",
Self::Development => "development",
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TerminologyServer {
pub name: String,
#[serde(default)]
pub reachable: Option<bool>,
pub namespaces: Vec<String>,
}
impl TerminologyServer {
#[must_use]
pub fn is_reachable(&self) -> bool {
self.reachable.unwrap_or(true)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TerminologyLane {
pub posture: TerminologyPosture,
pub servers: Vec<TerminologyServer>,
}
impl TerminologyLane {
#[must_use]
pub fn server_for(&self, namespace: &str) -> Option<&TerminologyServer> {
self.servers
.iter()
.find(|s| s.namespaces.iter().any(|n| n == namespace))
}
#[must_use]
pub fn distinct_reachable_servers(&self, namespaces: &[String]) -> usize {
let mut names: Vec<&str> = Vec::new();
for namespace in namespaces {
if let Some(server) = self.server_for(namespace)
&& server.is_reachable()
&& !names.contains(&server.name.as_str())
{
names.push(&server.name);
}
}
names.len()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Instance {
pub base_url: String,
pub auth: AuthMode,
#[serde(default, deserialize_with = "crate::model::de::optional_ordered_map")]
pub headers: Option<Vec<(String, String)>>,
#[serde(default)]
pub signing: Option<crate::exec::signature::SigningMode>,
#[serde(default)]
pub terminology: Option<TerminologyLane>,
#[serde(default)]
pub spec_profile: Option<SpecProfile>,
#[serde(default)]
pub administrative: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Environment {
#[serde(default)]
pub exclusive_server: bool,
pub hardware_class: String,
pub cores: u32,
pub memory_gb: u32,
pub storage_class: String,
pub topology: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Containers {
pub sut: String,
pub db: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Ixit {
#[serde(deserialize_with = "crate::model::de::ordered_map")]
pub instances: Vec<(InstanceName, Instance)>,
#[serde(default)]
pub environment: Option<Environment>,
#[serde(default)]
pub containers: Option<Containers>,
#[serde(default)]
pub system_id: Option<String>,
#[serde(default)]
pub dump_location: Option<String>,
#[serde(default)]
pub signing: Option<crate::exec::signature::SigningMode>,
#[serde(default)]
pub smart: Option<SmartLane>,
#[serde(default)]
pub terminology: Option<TerminologyLane>,
#[serde(default)]
pub spec_profile: Option<SpecProfile>,
}
impl Ixit {
pub fn rebase_paths(&mut self, base: &Path) {
if let Some(smart) = &mut self.smart
&& smart.mint.key_file.is_relative()
{
smart.mint.key_file = base.join(&smart.mint.key_file);
}
}
#[must_use]
pub fn instance(&self, name: &InstanceName) -> Option<&Instance> {
self.instances
.iter()
.find(|(n, _)| n == name)
.map(|(_, i)| i)
}
#[must_use]
pub fn signing_of<'i>(
&'i self,
instance: &'i Instance,
) -> Option<&'i crate::exec::signature::SigningMode> {
instance.signing.as_ref().or(self.signing.as_ref())
}
#[must_use]
pub fn terminology_of<'i>(&'i self, instance: &'i Instance) -> Option<&'i TerminologyLane> {
instance.terminology.as_ref().or(self.terminology.as_ref())
}
#[must_use]
pub fn spec_profile_of(&self, instance: &Instance) -> Option<SpecProfile> {
instance.spec_profile.or(self.spec_profile)
}
pub fn default_instance(&self) -> Result<&Instance, String> {
let sut = InstanceName::parse("sut").map_err(|e| e.to_string())?;
self.instance(&sut)
.ok_or_else(|| "ixit declares no `sut` instance".to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ixit_parses_with_principals() {
let ixit: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://localhost:8080/ferroehr/rest/openehr/v1",
"auth": { "mode": "basic", "user_env": "SUT_USER", "password_env": "SUT_PASS" } },
"unauthenticated": { "base_url": "http://localhost:8080/ferroehr/rest/openehr/v1",
"auth": { "mode": "none" } },
"readonly": { "base_url": "http://localhost:8080/ferroehr/rest/openehr/v1",
"auth": { "mode": "bearer", "token_env": "SUT_RO_TOKEN" } }
},
"environment": { "hardware_class": "consumer-laptop", "cores": 8,
"memory_gb": 16, "storage_class": "nvme", "topology": "single-node" }
}))
.unwrap();
assert!(ixit.default_instance().is_ok());
assert!(
ixit.instance(&InstanceName::parse("readonly").unwrap())
.is_some()
);
assert!(matches!(
ixit.instance(&InstanceName::parse("unauthenticated").unwrap())
.unwrap()
.auth,
AuthMode::None
));
}
#[test]
fn containers_block_is_optional_and_parses() {
let bare: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
assert!(bare.containers.is_none());
let with: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } },
"containers": { "sut": "ferroehr-ferroehr-1", "db": "ferroehr-ferroehr-postgres-1" }
}))
.unwrap();
let containers = with.containers.unwrap();
assert_eq!(containers.sut, "ferroehr-ferroehr-1");
assert_eq!(containers.db, "ferroehr-ferroehr-postgres-1");
}
#[test]
fn declared_system_id_is_optional_and_parses() {
let bare: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
assert!(bare.system_id.is_none());
let declared: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } },
"system_id": "ferroehr.local"
}))
.unwrap();
assert_eq!(declared.system_id.as_deref(), Some("ferroehr.local"));
}
#[test]
fn smart_lane_is_optional_and_parses() {
let bare: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
assert!(bare.smart.is_none());
let mut declared: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://x/openehr/v1", "auth": { "mode": "none" } },
"smart_app": { "base_url": "http://x/openehr/v1", "auth": { "mode": "bearer_mint" } },
"smart_platform": { "base_url": "http://x", "auth": { "mode": "none" } }
},
"smart": {
"platform_instance": "smart_platform",
"mint": {
"issuer": "https://as.example.test",
"audience": "cnf-smart-sut",
"subject": "cnf-smart-app",
"roles": ["USER"],
"key_file": "../smart/cnf-smart-test.key.pem",
"kid": "cnf-smart-test",
"ttl_seconds": 300
}
}
}))
.unwrap();
let lane = declared.smart.as_ref().unwrap();
assert_eq!(lane.platform_instance.as_str(), "smart_platform");
assert_eq!(lane.mint.kid, "cnf-smart-test");
assert_eq!(lane.mint.audience.as_deref(), Some("cnf-smart-sut"));
assert!(matches!(
declared
.instance(&InstanceName::parse("smart_app").unwrap())
.unwrap()
.auth,
AuthMode::BearerMint { .. }
));
declared.rebase_paths(Path::new("/party/ferroehr"));
assert_eq!(
declared.smart.as_ref().unwrap().mint.key_file,
PathBuf::from("/party/ferroehr/../smart/cnf-smart-test.key.pem")
);
declared.rebase_paths(Path::new("/elsewhere"));
assert_eq!(
declared.smart.as_ref().unwrap().mint.key_file,
PathBuf::from("/party/ferroehr/../smart/cnf-smart-test.key.pem")
);
}
#[test]
fn instance_signing_overrides_the_party_default() {
let ixit: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://localhost:8080", "auth": { "mode": "none" } },
"sut_pgp": {
"base_url": "http://localhost:8081",
"auth": { "mode": "none" },
"signing": { "mode": "pgp", "public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" }
}
},
"signing": { "mode": "digest", "algorithm": "sha256", "encoding": "base64", "prefix": "sha256:" }
}))
.unwrap();
let default = ixit.default_instance().unwrap();
assert!(default.signing.is_none());
assert!(matches!(
ixit.signing_of(default),
Some(crate::exec::signature::SigningMode::Digest { .. })
));
let pgp = ixit
.instance(&InstanceName::parse("sut_pgp").unwrap())
.unwrap();
assert!(matches!(
ixit.signing_of(pgp),
Some(crate::exec::signature::SigningMode::Pgp { .. })
));
}
#[test]
fn instance_signing_is_absent_without_any_declaration() {
let ixit: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
assert!(ixit.signing_of(ixit.default_instance().unwrap()).is_none());
}
#[test]
fn terminology_lane_is_optional_and_resolves_instance_first() {
let bare: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
assert!(bare.terminology.is_none());
assert!(
bare.terminology_of(bare.default_instance().unwrap())
.is_none()
);
let declared: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://x", "auth": { "mode": "none" } },
"sut_closed": {
"base_url": "http://y",
"auth": { "mode": "none" },
"terminology": {
"posture": "fail_closed",
"servers": [
{ "name": "sct", "reachable": false, "namespaces": ["urn:cnf:sct"] }
]
}
}
},
"terminology": {
"posture": "fail_open",
"servers": [
{ "name": "sct", "namespaces": ["urn:cnf:sct", "urn:cnf:sct-vs"] },
{ "name": "loinc", "namespaces": ["urn:cnf:loinc"] },
{ "name": "down", "reachable": false, "namespaces": ["urn:cnf:down"] }
]
}
}))
.unwrap();
let party = declared.terminology.as_ref().unwrap();
assert_eq!(party.posture, TerminologyPosture::FailOpen);
assert!(party.server_for("urn:cnf:sct").unwrap().is_reachable());
assert!(!party.server_for("urn:cnf:down").unwrap().is_reachable());
assert!(party.server_for("urn:cnf:unknown").is_none());
assert_eq!(
party.distinct_reachable_servers(&[
"urn:cnf:sct".to_owned(),
"urn:cnf:sct-vs".to_owned()
]),
1
);
assert_eq!(
party.distinct_reachable_servers(&[
"urn:cnf:sct".to_owned(),
"urn:cnf:loinc".to_owned()
]),
2
);
assert_eq!(
party.distinct_reachable_servers(&["urn:cnf:down".to_owned()]),
0
);
assert_eq!(
declared
.terminology_of(declared.default_instance().unwrap())
.unwrap()
.posture,
TerminologyPosture::FailOpen
);
let closed = declared
.instance(&InstanceName::parse("sut_closed").unwrap())
.unwrap();
assert_eq!(
declared.terminology_of(closed).unwrap().posture,
TerminologyPosture::FailClosed
);
}
#[test]
fn missing_sut_is_an_error() {
let ixit: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "primary": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
assert!(ixit.default_instance().is_err());
}
}