Skip to main content

sim_lib_server/helpers/
options.rs

1use 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
135/// Parses a [`Duration`] from a duration string or an integer millisecond
136/// count.
137pub 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}