sim_lib_server/helpers/
options.rs1use std::time::Duration;
2
3use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
4
5use super::{capability_names_from_value, ensure_installed_codec, symbol_from_value};
6
7pub(crate) fn keyword(expr: &Expr) -> Result<String> {
8 let Expr::Symbol(symbol) = expr else {
9 return Err(Error::TypeMismatch {
10 expected: "keyword symbol",
11 found: "non-symbol",
12 });
13 };
14 let Some(keyword) = symbol.name.strip_prefix(':') else {
15 return Err(Error::Eval(format!(
16 "expected keyword option, found {symbol}"
17 )));
18 };
19 Ok(keyword.to_owned())
20}
21
22pub(crate) fn symbol_of(expr: &Expr, message: &'static str) -> Result<Symbol> {
23 match expr {
24 Expr::Symbol(symbol) => Ok(symbol.clone()),
25 _ => Err(Error::Eval(message.to_owned())),
26 }
27}
28
29pub(crate) fn literal_expr(expr: &Expr) -> &Expr {
30 match expr {
31 Expr::Quote { expr, .. } => expr,
32 _ => expr,
33 }
34}
35
36pub(crate) fn parse_server_options<F>(
37 cx: &mut Cx,
38 options: &[Expr],
39 name: &str,
40 mut f: F,
41) -> Result<()>
42where
43 F: FnMut(&mut Cx, &str, &Expr) -> Result<()>,
44{
45 if !options.len().is_multiple_of(2) {
46 return Err(Error::Eval(format!(
47 "{name} options must be key/value pairs"
48 )));
49 }
50 for pair in options.chunks(2) {
51 let key = keyword(&pair[0])?;
52 f(cx, key.as_str(), &pair[1])?;
53 }
54 Ok(())
55}
56
57#[allow(clippy::too_many_arguments)]
58pub(crate) fn parse_message_options(
59 cx: &mut Cx,
60 options: &[Expr],
61 name: &str,
62 codec: &mut Symbol,
63 deadline: &mut Option<Duration>,
64 required_capabilities: &mut Vec<CapabilityName>,
65 reply_codec_hint: Option<&mut Option<Symbol>>,
66 consistency: Option<&mut sim_kernel::Consistency>,
67) -> Result<()> {
68 if !options.len().is_multiple_of(2) {
69 return Err(Error::Eval(format!(
70 "{name} options must be key/value pairs"
71 )));
72 }
73 let mut reply_codec_hint = reply_codec_hint;
74 let mut consistency = consistency;
75 for pair in options.chunks(2) {
76 let key = keyword(&pair[0])?;
77 match key.as_str() {
78 "codec" => {
79 let value = cx.eval_expr(pair[1].clone())?;
80 *codec = symbol_from_value(cx, value, "message :codec expects a symbol")?;
81 ensure_installed_codec(cx, codec)?;
82 }
83 "deadline" | "timeout" => {
84 let value = cx.eval_expr(pair[1].clone())?;
85 *deadline = Some(parse_duration_value(cx, value)?)
86 }
87 "requires" => {
88 let value = cx.eval_expr(pair[1].clone())?;
89 *required_capabilities = capability_names_from_value(cx, value)?;
90 }
91 "reply-codec" => {
92 let value = cx.eval_expr(pair[1].clone())?;
93 let hint = symbol_from_value(cx, value, "message :reply-codec expects a symbol")?;
94 ensure_installed_codec(cx, &hint)?;
95 let Some(slot) = reply_codec_hint.as_deref_mut() else {
96 return Err(Error::Eval(format!("{name} does not support :reply-codec")));
97 };
98 *slot = Some(hint);
99 }
100 "consistency" => {
101 let value = cx.eval_expr(pair[1].clone())?;
102 let parsed = parse_consistency_value(cx, value)?;
103 let Some(slot) = consistency.as_deref_mut() else {
104 return Err(Error::Eval(format!("{name} does not support :consistency")));
105 };
106 *slot = parsed;
107 }
108 other => return Err(Error::Eval(format!("{name}: unknown option :{other}"))),
109 }
110 }
111 Ok(())
112}
113
114pub(crate) fn parse_duration_value(cx: &mut Cx, value: sim_kernel::Value) -> Result<Duration> {
115 parse_duration(&value.object().as_expr(cx)?)
116}
117
118pub(crate) fn usize_from_value(
119 cx: &mut Cx,
120 value: sim_kernel::Value,
121 message: &'static str,
122) -> Result<usize> {
123 match value.object().as_expr(cx)? {
124 Expr::String(text) => text
125 .parse::<usize>()
126 .map_err(|_| Error::Eval(message.to_owned())),
127 Expr::Number(number) => number
128 .canonical
129 .parse::<usize>()
130 .map_err(|_| Error::Eval(message.to_owned())),
131 _ => Err(Error::Eval(message.to_owned())),
132 }
133}
134
135pub fn parse_duration(expr: &Expr) -> Result<Duration> {
138 match expr {
139 Expr::String(text) => parse_duration_text(text),
140 Expr::Number(number) => {
141 let millis = number.canonical.parse::<u64>().map_err(|_| {
142 Error::Eval(format!(
143 "deadline {} is not an integer millisecond count",
144 number.canonical
145 ))
146 })?;
147 Ok(Duration::from_millis(millis))
148 }
149 _ => Err(Error::TypeMismatch {
150 expected: "deadline string or integer number",
151 found: "non-deadline",
152 }),
153 }
154}
155
156pub(crate) fn parse_optional_duration(expr: &Expr) -> Result<Option<Duration>> {
157 match expr {
158 Expr::Nil => Ok(None),
159 _ => parse_duration(expr).map(Some),
160 }
161}
162
163pub(crate) fn format_duration(duration: Duration) -> String {
164 if duration.subsec_nanos() == 0 && duration.as_secs() > 0 {
165 format!("{}s", duration.as_secs())
166 } else {
167 format!("{}ms", duration.as_millis())
168 }
169}
170
171fn parse_duration_text(text: &str) -> Result<Duration> {
172 let (number, unit) = if let Some(number) = text.strip_suffix("ms") {
173 (number, "ms")
174 } else if let Some(number) = text.strip_suffix('s') {
175 (number, "s")
176 } else if let Some(number) = text.strip_suffix('m') {
177 (number, "m")
178 } else if let Some(number) = text.strip_suffix('h') {
179 (number, "h")
180 } else {
181 return Err(Error::Eval(format!(
182 "deadline {text} must end with ms, s, m, or h"
183 )));
184 };
185
186 let value = number
187 .parse::<u64>()
188 .map_err(|_| Error::Eval(format!("deadline {text} has an invalid numeric prefix")))?;
189 Ok(match unit {
190 "ms" => Duration::from_millis(value),
191 "s" => Duration::from_secs(value),
192 "m" => Duration::from_secs(value.saturating_mul(60)),
193 "h" => Duration::from_secs(value.saturating_mul(60 * 60)),
194 _ => unreachable!(),
195 })
196}
197
198pub(crate) fn parse_consistency_value(
199 cx: &mut Cx,
200 value: sim_kernel::Value,
201) -> Result<sim_kernel::Consistency> {
202 let name = match value.object().as_expr(cx)? {
203 Expr::Symbol(symbol) => symbol.to_string(),
204 Expr::String(text) => text,
205 _ => {
206 return Err(Error::TypeMismatch {
207 expected: "consistency symbol or string",
208 found: "non-consistency",
209 });
210 }
211 };
212 match name.as_str() {
213 "local-only" => Ok(sim_kernel::Consistency::LocalOnly),
214 "local-first" => Ok(sim_kernel::Consistency::LocalFirst),
215 "remote-only" => Ok(sim_kernel::Consistency::RemoteOnly),
216 _ => Err(Error::Eval(format!(
217 "unsupported realize consistency {name}"
218 ))),
219 }
220}