Skip to main content

sim_run_core/
source.rs

1use std::{fmt, path::PathBuf, str::FromStr};
2
3use crate::{CliError, CratesIoSpec};
4use sim_kernel::{LibSourceSpec as KernelLibSourceSpec, Symbol};
5
6/// Library source syntax accepted by the command line.
7///
8/// Each variant maps to a `kind:value` spelling parsed via [`FromStr`].
9///
10/// # Examples
11///
12/// ```
13/// use sim_run_core::LibSourceSpec;
14///
15/// assert_eq!(
16///     "symbol:codec/lisp".parse::<LibSourceSpec>().unwrap(),
17///     LibSourceSpec::Symbol("codec/lisp".to_owned()),
18/// );
19/// assert!("codec/lisp".parse::<LibSourceSpec>().is_err());
20/// ```
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum LibSourceSpec {
23    /// A `symbol:NAME` source resolved through the kernel loader.
24    Symbol(String),
25    /// A `path:PATH` source read from the local filesystem.
26    Path(PathBuf),
27    /// A `url:URL` source fetched from a remote location.
28    Url(String),
29    /// A `bytes:TEXT` source holding inline library bytes.
30    Bytes(Vec<u8>),
31    /// A `host:NAME` source provided by the host environment.
32    Host(String),
33    /// A `crates.io:NAME@REQ` source resolved outside the kernel.
34    CratesIo(CratesIoSpec),
35}
36
37impl LibSourceSpec {
38    pub(crate) fn to_kernel_data_source(&self) -> Option<KernelLibSourceSpec> {
39        match self {
40            Self::Symbol(symbol) => Some(KernelLibSourceSpec::Symbol(symbol_from_text(symbol))),
41            Self::Path(path) => Some(KernelLibSourceSpec::Path(path.clone())),
42            Self::Url(url) => Some(KernelLibSourceSpec::Url(url.clone())),
43            Self::Bytes(bytes) => Some(KernelLibSourceSpec::Bytes(bytes.clone())),
44            Self::Host(_) | Self::CratesIo(_) => None,
45        }
46    }
47
48    pub(crate) fn from_kernel_data_source(source: KernelLibSourceSpec) -> Self {
49        match source {
50            KernelLibSourceSpec::Symbol(symbol) => Self::Symbol(symbol.to_string()),
51            KernelLibSourceSpec::Path(path) => Self::Path(path),
52            KernelLibSourceSpec::Url(url) => Self::Url(url),
53            KernelLibSourceSpec::Bytes(bytes) => Self::Bytes(bytes),
54        }
55    }
56}
57
58impl fmt::Display for LibSourceSpec {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Symbol(symbol) => write!(f, "symbol:{symbol}"),
62            Self::Path(path) => write!(f, "path:{}", path.display()),
63            Self::Url(url) => write!(f, "url:{url}"),
64            Self::Bytes(bytes) => write!(f, "bytes:{} bytes", bytes.len()),
65            Self::Host(name) => write!(f, "host:{name}"),
66            Self::CratesIo(spec) => write!(f, "crates.io:{spec}"),
67        }
68    }
69}
70
71pub(crate) fn symbol_from_text(text: &str) -> Symbol {
72    match text.split_once('/') {
73        Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
74            Symbol::qualified(namespace.to_owned(), name.to_owned())
75        }
76        _ => Symbol::new(text.to_owned()),
77    }
78}
79
80impl FromStr for LibSourceSpec {
81    type Err = CliError;
82
83    fn from_str(source: &str) -> Result<Self, Self::Err> {
84        let Some((kind, rest)) = source.split_once(':') else {
85            return Err(CliError::new("library source must use kind:value syntax"));
86        };
87        if rest.is_empty() {
88            return Err(CliError::new(format!("{kind}: source value is empty")));
89        }
90        match kind {
91            "symbol" => Ok(Self::Symbol(rest.to_owned())),
92            "path" => Ok(Self::Path(PathBuf::from(rest))),
93            "url" => Ok(Self::Url(rest.to_owned())),
94            "bytes" => Ok(Self::Bytes(rest.as_bytes().to_vec())),
95            "host" => Ok(Self::Host(rest.to_owned())),
96            "crates.io" => Ok(Self::CratesIo(rest.parse::<CratesIoSpec>()?)),
97            _ => Err(CliError::new(format!(
98                "unsupported library source kind: {kind}"
99            ))),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn parses_supported_source_specs() {
110        assert_eq!(
111            "symbol:codec/lisp".parse::<LibSourceSpec>().unwrap(),
112            LibSourceSpec::Symbol("codec/lisp".to_owned())
113        );
114        assert_eq!(
115            "path:./libs/demo.wasm".parse::<LibSourceSpec>().unwrap(),
116            LibSourceSpec::Path(PathBuf::from("./libs/demo.wasm"))
117        );
118        assert_eq!(
119            "url:https://example.invalid/demo.wasm"
120                .parse::<LibSourceSpec>()
121                .unwrap(),
122            LibSourceSpec::Url("https://example.invalid/demo.wasm".to_owned())
123        );
124        assert_eq!(
125            "bytes:abc".parse::<LibSourceSpec>().unwrap(),
126            LibSourceSpec::Bytes(b"abc".to_vec())
127        );
128        assert_eq!(
129            "host:test/demo".parse::<LibSourceSpec>().unwrap(),
130            LibSourceSpec::Host("test/demo".to_owned())
131        );
132        assert_eq!(
133            "crates.io:sim-codec-lisp@0.1.0"
134                .parse::<LibSourceSpec>()
135                .unwrap(),
136            LibSourceSpec::CratesIo("sim-codec-lisp@0.1.0".parse().unwrap())
137        );
138    }
139
140    #[test]
141    fn source_parse_errors_are_typed() {
142        assert_eq!(
143            "codec/lisp"
144                .parse::<LibSourceSpec>()
145                .unwrap_err()
146                .to_string(),
147            "library source must use kind:value syntax"
148        );
149        assert_eq!(
150            "symbol:".parse::<LibSourceSpec>().unwrap_err().to_string(),
151            "symbol: source value is empty"
152        );
153        assert_eq!(
154            "crate:sim"
155                .parse::<LibSourceSpec>()
156                .unwrap_err()
157                .to_string(),
158            "unsupported library source kind: crate"
159        );
160    }
161}