use zyris::enroll::{EnrollRequest, Progress};
use zyris::schemars::JsonSchema;
use zyris::serde::{Deserialize, Serialize};
use zyris::{
Account, AccountCredential, ConnectError, EnrollError, Link, Node, NodeKind, NodeSpec,
NodeToken, RegisterError, RotateError,
};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(crate = "zyris::serde")]
#[schemars(crate = "zyris::schemars")]
pub struct Greeting {
pub message: String,
pub node: String,
}
#[zyris::capability(name = "hello", version = 1)]
pub trait Hello {
async fn greet(&self, name: Option<String>) -> zyris::Result<Greeting>;
}
struct HelloWorld {
node: String,
}
#[zyris::async_trait]
impl Hello for HelloWorld {
async fn greet(&self, name: Option<String>) -> zyris::Result<Greeting> {
let message = match name.as_deref().map(str::trim).filter(|who| !who.is_empty()) {
Some(who) => format!("Hello, {who}!"),
None => "Hello World".to_string(),
};
Ok(Greeting { message, node: self.node.clone() })
}
}
const NODE_NAME: &str = "hello";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let server =
std::env::var("ZYRIS_SERVER_URL").unwrap_or_else(|_| zyris::DEFAULT_SERVER_URL.to_string());
let token = match std::env::var("ZYRIS_NODE_TOKEN") {
Ok(token) if !token.trim().is_empty() => token,
_ => take_a_node(&server).await?,
};
let link: Link = Node::builder()
.name(NODE_NAME)
.kind(NodeKind::Service)
.capability(HelloServer(HelloWorld { node: NODE_NAME.to_string() }))
.build()?
.connect(&server, &token)
.await
.map_err(say_connect)?;
println!("serving hello.greet as {}; Ctrl-C to stop", link.node_id());
tokio::select! {
closed = link.wait_closed() => {
closed.map_err(say_connect)?;
println!("the server closed the link");
}
_ = tokio::signal::ctrl_c() => {
println!("stopping");
link.disconnect().await;
}
}
Ok(())
}
async fn take_a_node(server: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut enrollment = zyris::enroll(
server,
EnrollRequest {
name: NODE_NAME.to_string(),
platform: std::env::consts::OS.to_string(),
scopes: vec!["nodes:write".to_string()],
},
)
.await
.map_err(say_enroll)?;
println!("open {} and enter {}", enrollment.code().verification_uri, enrollment.code().user_code);
let credential: AccountCredential = loop {
match enrollment.poll().await.map_err(say_enroll)? {
Progress::Waiting { .. } => {}
Progress::Granted(credential) => break credential,
Progress::Lapsed => {
enrollment.renew().await.map_err(say_enroll)?;
println!("that code expired; the new one is {}", enrollment.code().user_code);
}
Progress::Denied => return Err("the request was declined".into()),
}
};
let account = Account::restore(server, credential)
.on_rotate(|rotated: AccountCredential| async move {
let _ = rotated;
Ok::<(), RotateError>(())
})
.build();
let node: NodeToken = account
.register_node(NodeSpec {
name: NODE_NAME.to_string(),
platform: Some(std::env::consts::OS.to_string()),
scopes: vec![],
})
.await
.map_err(say_register)?;
println!("took node {} as {}", node.node_id, node.slug);
println!("set ZYRIS_NODE_TOKEN to skip this next time");
Ok(node.token)
}
fn say_enroll(error: EnrollError) -> String {
match error {
EnrollError::Denied => "the request was declined".to_string(),
EnrollError::Lapsed => "the code expired".to_string(),
EnrollError::Revoked => "revoked; authorize this node again".to_string(),
EnrollError::ScopeUnknown { scope } => {
format!("this deployment does not know the scope {scope}")
}
EnrollError::Unreachable(transport) => format!("worth retrying: {transport}"),
}
}
fn say_register(error: RegisterError) -> String {
match error {
RegisterError::Forbidden => "this credential lacks nodes:write".to_string(),
RegisterError::ScopeExceeded { requested, granted } => {
format!("asked for {requested:?}, the account grants {granted:?}")
}
RegisterError::Revoked => "the account credential is dead; authorize it again".to_string(),
RegisterError::Unreachable(transport) => format!("worth retrying: {transport}"),
}
}
fn say_connect(error: ConnectError) -> String {
match error {
ConnectError::Revoked => "revoked; authorize this node again".to_string(),
ConnectError::Unauthorized => "the server did not accept this token".to_string(),
ConnectError::VersionMismatch { ours, theirs } => {
let theirs = theirs.unwrap_or_else(|| "an unnamed version".to_string());
format!("we speak {ours}, it speaks {theirs}; retrying will not help")
}
ConnectError::Unreachable(transport) => format!("worth retrying: {transport}"),
ConnectError::NoTlsProvider => "this build named no TLS provider".to_string(),
}
}