use crate::{Config, Error, Result};
const ENDPOINT_OVERRIDE: &str = "CANTON_ENDPOINT";
const TOKEN_OVERRIDE: &str = "CANTON_TOKEN";
const DEFAULT_ROLE: &str = "app-provider";
struct Source<F: Fn(&str) -> Option<String>>(F);
impl<F: Fn(&str) -> Option<String>> Source<F> {
fn get(&self, name: &str) -> Option<String> {
(self.0)(name).filter(|value| !value.trim().is_empty())
}
fn grpc_endpoint(&self, role: Option<&str>) -> Option<String> {
self.get(ENDPOINT_OVERRIDE)
.or_else(|| self.get(&role_variable(role, "GRPC_LEDGER_API_URL")))
}
fn json_endpoint(&self, role: Option<&str>) -> Option<String> {
self.get(&role_variable(role, "JSON_LEDGER_API_URL"))
}
fn token(&self, role: Option<&str>) -> Option<String> {
self.get(TOKEN_OVERRIDE)
.or_else(|| self.get(&format!("{}_JWT", prefix(role.unwrap_or(DEFAULT_ROLE)))))
}
fn config(&self, role: Option<&str>) -> Result<Config> {
let endpoint = self
.grpc_endpoint(role)
.ok_or_else(|| missing_endpoint(role))?;
let config = Config::new(endpoint);
Ok(match self.token(role) {
Some(token) => config.with_token(token),
None => config,
})
}
}
fn process() -> Source<impl Fn(&str) -> Option<String>> {
Source(|name: &str| std::env::var(name).ok())
}
fn prefix(role: &str) -> String {
format!("CANTON_{}", role.to_uppercase().replace('-', "_"))
}
fn role_variable(role: Option<&str>, suffix: &str) -> String {
match role {
None => format!("CANTON_{suffix}"),
Some(role) => format!("{}_{suffix}", prefix(role)),
}
}
fn missing_endpoint(role: Option<&str>) -> Error {
let variable = role_variable(role, "GRPC_LEDGER_API_URL");
Error::InvalidRequest(format!(
"no ledger endpoint in the environment: set {variable} (or {ENDPOINT_OVERRIDE}). \
A local network exports it with `canton-devkit localnet env <instance>`; \
run that through `eval` first."
))
}
#[must_use]
pub fn grpc_endpoint(role: Option<&str>) -> Option<String> {
process().grpc_endpoint(role)
}
#[must_use]
pub fn json_endpoint(role: Option<&str>) -> Option<String> {
process().json_endpoint(role)
}
#[must_use]
pub fn token(role: Option<&str>) -> Option<String> {
process().token(role)
}
#[must_use]
pub fn party(alias: &str) -> Option<String> {
process().get(&format!("{}_PARTY", prefix(alias)))
}
#[must_use]
pub fn instance() -> Option<String> {
process().get("CANTON_INSTANCE")
}
#[must_use]
pub fn splice_version() -> Option<String> {
process().get("CANTON_SPLICE_VERSION")
}
impl Config {
pub fn from_env() -> Result<Self> {
process().config(None)
}
pub fn from_env_for(role: &str) -> Result<Self> {
process().config(Some(role))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::Auth;
use std::collections::HashMap;
fn devkit() -> Source<impl Fn(&str) -> Option<String>> {
let vars: HashMap<&str, &str> = [
("CANTON_INSTANCE", "demo"),
("CANTON_SPLICE_VERSION", "0.6.12"),
(
"CANTON_GRPC_LEDGER_API_URL",
"grpc-ledger-api.app-provider.demo.localhost:3901",
),
(
"CANTON_JSON_LEDGER_API_URL",
"http://json-ledger-api.app-provider.demo.localhost:3901",
),
(
"CANTON_APP_USER_GRPC_LEDGER_API_URL",
"grpc-ledger-api.app-user.demo.localhost:2901",
),
("CANTON_APP_PROVIDER_JWT", "provider.jwt.token"),
("CANTON_APP_USER_JWT", "user.jwt.token"),
("CANTON_APP_PROVIDER_PARTY", "app_provider::1220abcd"),
("CANTON_BOB_PARTY", "bob::1220abcd"),
("CANTON_ENDPOINT", ""),
("CANTON_TOKEN", ""),
]
.into_iter()
.collect();
Source(move |name: &str| vars.get(name).map(|value| (*value).to_string()))
}
#[test]
fn a_devkit_environment_becomes_a_working_configuration() {
let config = devkit().config(None).unwrap();
assert_eq!(
config.endpoint(),
"grpc-ledger-api.app-provider.demo.localhost:3901"
);
assert!(matches!(config.auth(), Auth::Static(t) if t == "provider.jwt.token"));
assert!(!config.endpoint().contains("://"));
}
#[test]
fn a_role_selects_that_participant_and_its_token() {
for spelling in ["app-user", "app_user", "APP-USER"] {
let config = devkit().config(Some(spelling)).unwrap();
assert_eq!(
config.endpoint(),
"grpc-ledger-api.app-user.demo.localhost:2901",
"spelling {spelling}"
);
assert!(matches!(config.auth(), Auth::Static(t) if t == "user.jwt.token"));
}
}
#[test]
fn the_json_lane_reads_the_same_environment() {
assert_eq!(
devkit().json_endpoint(None).as_deref(),
Some("http://json-ledger-api.app-provider.demo.localhost:3901")
);
assert_eq!(devkit().json_endpoint(Some("sv")), None);
}
#[test]
fn an_empty_variable_does_not_shadow_a_real_one() {
let source = devkit(); let config = source.config(None).unwrap();
assert_eq!(
config.endpoint(),
"grpc-ledger-api.app-provider.demo.localhost:3901"
);
assert!(matches!(config.auth(), Auth::Static(t) if t == "provider.jwt.token"));
}
#[test]
fn an_explicit_endpoint_wins_over_the_local_network() {
let source = Source(|name: &str| {
Some(
match name {
ENDPOINT_OVERRIDE => "https://ledger.example:443",
TOKEN_OVERRIDE => "deployment.token",
"CANTON_GRPC_LEDGER_API_URL" => "grpc-ledger-api.demo.localhost:3901",
_ => return None,
}
.to_string(),
)
});
let config = source.config(None).unwrap();
assert_eq!(config.endpoint(), "https://ledger.example:443");
assert!(matches!(config.auth(), Auth::Static(t) if t == "deployment.token"));
}
#[test]
fn an_empty_environment_says_which_command_produces_one() {
let empty = Source(|_: &str| None);
let error = empty.config(None).unwrap_err().to_string();
assert!(error.contains("CANTON_GRPC_LEDGER_API_URL"), "{error}");
assert!(error.contains("canton-devkit localnet env"), "{error}");
let error = empty.config(Some("sv")).unwrap_err().to_string();
assert!(error.contains("CANTON_SV_GRPC_LEDGER_API_URL"), "{error}");
}
#[test]
fn a_network_without_tokens_is_still_a_network() {
let source = Source(|name: &str| {
(name == "CANTON_GRPC_LEDGER_API_URL").then(|| "localhost:3901".to_string())
});
let config = source.config(None).unwrap();
assert!(matches!(config.auth(), Auth::None));
}
#[test]
fn party_ids_are_reachable_by_role_and_by_alias() {
let source = devkit();
let party = |alias: &str| source.get(&format!("{}_PARTY", prefix(alias)));
assert_eq!(
party("app-provider").as_deref(),
Some("app_provider::1220abcd")
);
assert_eq!(party("bob").as_deref(), Some("bob::1220abcd"));
assert_eq!(party("nobody"), None);
}
}