snix_castore/directoryservice/
from_addr.rs

1use std::sync::Arc;
2
3use url::Url;
4
5use crate::composition::{
6    CompositionContext, DeserializeWithRegistry, REG, ServiceBuilder, with_registry,
7};
8
9use super::DirectoryService;
10
11/// Constructs a new instance of a [DirectoryService] from an URI.
12///
13/// The following URIs are supported:
14/// - `memory:`
15///   Uses a in-memory implementation.
16///   from one process at the same time.
17/// - `redb:`
18///   Uses a in-memory redb implementation.
19/// - `redb:///absolute/path/to/somewhere`
20///   Uses redb, using a path on the disk for persistency. Can be only opened
21///   from one process at the same time.
22/// - `grpc+unix:///absolute/path/to/somewhere`
23///   Connects to a local snix-store gRPC service via Unix socket.
24/// - `grpc+http://host:port`, `grpc+https://host:port`
25///   Connects to a (remote) snix-store gRPC service.
26pub async fn from_addr(
27    uri: &str,
28) -> Result<Arc<dyn DirectoryService>, Box<dyn std::error::Error + Send + Sync>> {
29    #[allow(unused_mut)]
30    let mut url = Url::parse(uri)
31        .map_err(|e| crate::Error::StorageError(format!("unable to parse url: {}", e)))?;
32
33    let directory_service_config = with_registry(&REG, || {
34        <DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn DirectoryService>>>>::try_from(
35            url,
36        )
37    })?
38    .0;
39    let directory_service = directory_service_config
40        .build("anonymous", &CompositionContext::blank(&REG))
41        .await?;
42
43    Ok(directory_service)
44}
45
46#[cfg(test)]
47mod tests {
48    use std::sync::LazyLock;
49
50    use super::from_addr;
51    use rstest::rstest;
52    use tempfile::TempDir;
53
54    static TMPDIR_REDB_1: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
55    static TMPDIR_REDB_2: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
56
57    #[rstest]
58    /// This uses an unsupported scheme.
59    #[case::unsupported_scheme("http://foo.example/test", false)]
60    /// This correctly sets the scheme, and doesn't set a path.
61    #[case::memory_valid("memory://", true)]
62    /// This sets a memory url host to `foo`
63    #[case::memory_invalid_host("memory://foo", false)]
64    /// This sets a memory url path to "/", which is invalid.
65    #[case::memory_invalid_root_path("memory:///", false)]
66    /// This sets a memory url path to "/foo", which is invalid.
67    #[case::memory_invalid_root_path_foo("memory:///foo", false)]
68    /// This configures redb in temporary mode.
69    #[case::redb_valid_temporary("redb://", true)]
70    /// This configures redb with /, which should fail.
71    #[case::redb_invalid_root("redb:///", false)]
72    /// This configures redb with a host, not path, which should fail.
73    #[case::redb_invalid_host("redb://foo.example", false)]
74    /// This configures redb with a valid path, which should succeed.
75    #[case::redb_valid_path(&format!("redb://{}", &TMPDIR_REDB_1.path().join("foo").to_str().unwrap()), true)]
76    /// This configures redb with a host, and a valid path path, which should fail.
77    #[case::redb_invalid_host_with_valid_path(&format!("redb://foo.example{}", &TMPDIR_REDB_2.path().join("bar").to_str().unwrap()), false)]
78    /// Correct scheme to connect to a unix socket.
79    #[case::grpc_valid_unix_socket("grpc+unix:///path/to/somewhere", true)]
80    /// Correct scheme for unix socket, but setting a host too, which is invalid.
81    #[case::grpc_invalid_unix_socket_and_host("grpc+unix://host.example/path/to/somewhere", false)]
82    /// Correct scheme to connect to localhost, with port 12345
83    #[case::grpc_valid_ipv6_localhost_port_12345("grpc+http://[::1]:12345", true)]
84    /// Correct scheme to connect to localhost over http, without specifying a port.
85    #[case::grpc_valid_http_host_without_port("grpc+http://localhost", true)]
86    /// Correct scheme to connect to localhost over http, without specifying a port.
87    #[case::grpc_valid_https_host_without_port("grpc+https://localhost", true)]
88    /// Correct scheme to connect to localhost over http, but with additional path, which is invalid.
89    #[case::grpc_invalid_host_and_path("grpc+http://localhost/some-path", false)]
90    /// A valid example for store composition using anonymous urls
91    #[cfg_attr(
92        feature = "xp-composition-url-refs",
93        case::anonymous_url_composition("cache://?near=memory://&far=memory://", true)
94    )]
95    /// Store composition with anonymous urls should fail if the feature is disabled
96    #[cfg_attr(
97        not(feature = "xp-composition-url-refs"),
98        case::anonymous_url_composition("cache://?near=memory://&far=memory://", false)
99    )]
100    /// A valid example for Bigtable
101    #[cfg_attr(
102        all(feature = "cloud", feature = "integration"),
103        case::bigtable_valid_url(
104            "bigtable://instance-1?project_id=project-1&table_name=table-1&family_name=cf1",
105            true
106        )
107    )]
108    /// A valid example for Bigtable, specifying a custom channel size and timeout
109    #[cfg_attr(
110        all(feature = "cloud", feature = "integration"),
111        case::bigtable_valid_url(
112            "bigtable://instance-1?project_id=project-1&table_name=table-1&family_name=cf1&channel_size=10&timeout=10",
113            true
114        )
115    )]
116    /// A invalid Bigtable example (missing fields)
117    #[cfg_attr(
118        all(feature = "cloud", feature = "integration"),
119        case::bigtable_invalid_url("bigtable://instance-1", false)
120    )]
121    #[tokio::test]
122    async fn test_from_addr_tokio(#[case] uri_str: &str, #[case] exp_succeed: bool) {
123        if exp_succeed {
124            from_addr(uri_str).await.expect("should succeed");
125        } else {
126            assert!(from_addr(uri_str).await.is_err(), "should fail");
127        }
128    }
129}