1pub mod adapter;
2pub mod config;
3pub mod embed;
4pub mod handlers;
5pub mod sessions;
6pub mod substrate;
7pub mod transport;
8pub mod wire;
9
10pub const PROTOCOL_VERSION: u16 = 1;
11
12use std::time::Duration;
13
14use chrono::{DateTime, Utc};
15use serde_json::Value;
16
17pub trait Clock: Send + Sync {
18 fn now(&self) -> DateTime<Utc>;
19}
20
21#[derive(Debug, Clone, Copy, Default)]
22pub struct SystemClock;
23
24impl Clock for SystemClock {
25 fn now(&self) -> DateTime<Utc> {
26 Utc::now()
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct RetryPolicy {
32 pub attempts: u8,
33 pub initial_backoff: Duration,
34 pub max_backoff: Duration,
35 pub jitter: f64,
40}
41
42impl Default for RetryPolicy {
43 fn default() -> Self {
44 Self {
45 attempts: 3,
46 initial_backoff: Duration::from_millis(300),
47 max_backoff: Duration::from_secs(5),
48 jitter: 0.2,
49 }
50 }
51}
52
53pub mod output {
54 use std::{
55 io::{self, IsTerminal, Write},
56 sync::OnceLock,
57 };
58
59 use anstyle::{AnsiColor, Style};
60 use anyhow::Context;
61
62 fn use_color() -> bool {
67 static USE: OnceLock<bool> = OnceLock::new();
68 *USE.get_or_init(|| std::env::var_os("NO_COLOR").is_none() && io::stdout().is_terminal())
69 }
70
71 pub fn paint(text: &str, style: Style) -> String {
75 if use_color() {
76 format!("{}{text}{}", style.render(), style.render_reset())
77 } else {
78 text.to_owned()
79 }
80 }
81
82 pub fn bold() -> Style {
83 Style::new().bold()
84 }
85 pub fn dim() -> Style {
86 Style::new().dimmed()
87 }
88 pub fn green() -> Style {
89 Style::new().fg_color(Some(AnsiColor::Green.into()))
90 }
91 pub fn yellow() -> Style {
92 Style::new().fg_color(Some(AnsiColor::Yellow.into()))
93 }
94 pub fn red() -> Style {
95 Style::new().fg_color(Some(AnsiColor::Red.into()))
96 }
97 pub fn cyan() -> Style {
98 Style::new().fg_color(Some(AnsiColor::Cyan.into()))
99 }
100
101 #[allow(clippy::print_stdout)]
102 pub fn line(message: &str) -> anyhow::Result<()> {
103 let mut stdout = io::stdout().lock();
104 writeln!(stdout, "{message}").context("failed to write command output")
105 }
106}
107
108#[derive(Debug, thiserror::Error)]
109pub enum Error {
110 #[error("validation failed: {message}")]
111 Validation {
112 message: String,
113 field: Option<String>,
114 value: Option<Value>,
115 expected: Option<String>,
116 },
117 #[error("not found: {message}")]
118 NotFound {
119 message: String,
120 kind: String,
121 pk: Value,
122 },
123 #[error("namespace unknown: {namespace}")]
124 NamespaceUnknown { namespace: String },
125 #[error("commit conflict after {attempts} attempt(s)")]
126 Conflict { attempts: u8 },
127 #[error("storage unavailable: {0}")]
128 Storage(#[from] anyhow::Error),
129 #[error("internal error: {0}")]
130 Internal(String),
131}
132
133impl Error {
134 pub fn validation(message: impl Into<String>) -> Self {
135 Self::Validation {
136 message: message.into(),
137 field: None,
138 value: None,
139 expected: None,
140 }
141 }
142
143 pub fn validation_field(
144 message: impl Into<String>,
145 field: impl Into<String>,
146 value: Option<Value>,
147 expected: Option<String>,
148 ) -> Self {
149 Self::Validation {
150 message: message.into(),
151 field: Some(field.into()),
152 value,
153 expected,
154 }
155 }
156
157 pub fn not_found(kind: impl Into<String>, pk: Value, message: impl Into<String>) -> Self {
158 Self::NotFound {
159 message: message.into(),
160 kind: kind.into(),
161 pk,
162 }
163 }
164
165 pub fn namespace_unknown(namespace: impl Into<String>) -> Self {
166 Self::NamespaceUnknown {
167 namespace: namespace.into(),
168 }
169 }
170
171 pub fn conflict(attempts: u8) -> Self {
172 Self::Conflict { attempts }
173 }
174
175 pub fn internal(message: impl Into<String>) -> Self {
176 Self::Internal(message.into())
177 }
178}