oxigdal/cloud_detect.rs
1//! Cloud URI detection and transparent dispatch for [`crate::Dataset::open`].
2//!
3//! This module provides lightweight helpers that classify a path string as a
4//! cloud URI (`s3://`, `gs://`, `az://`, `http://`, `https://`) and, when the
5//! `cloud` feature is enabled, open a stub [`crate::Dataset`] descriptor that
6//! records the cloud path for subsequent streaming operations.
7//!
8//! When `cloud` is **not** enabled `open_cloud_dataset` returns a
9//! [`crate::OxiGdalError::NotSupported`] error with a helpful message.
10
11use crate::{Dataset, OxiGdalError, Result};
12#[cfg(feature = "cloud")]
13use crate::{DatasetFormat, DatasetInfo};
14
15// ─── Detection ───────────────────────────────────────────────────────────────
16
17/// Returns `true` if `path` starts with a known cloud storage URI scheme.
18///
19/// Recognised schemes: `s3://`, `gs://`, `az://`, `abfs://`, `http://`, `https://`.
20///
21/// # Examples
22///
23/// ```rust
24/// assert!(oxigdal::cloud_detect::is_cloud_uri("s3://my-bucket/data.tif"));
25/// assert!(oxigdal::cloud_detect::is_cloud_uri("gs://bucket/path/to/file.geojson"));
26/// assert!(!oxigdal::cloud_detect::is_cloud_uri("/local/path/to/file.tif"));
27/// assert!(!oxigdal::cloud_detect::is_cloud_uri("relative/path.tif"));
28/// ```
29pub fn is_cloud_uri(path: &str) -> bool {
30 path.starts_with("s3://")
31 || path.starts_with("gs://")
32 || path.starts_with("az://")
33 || path.starts_with("abfs://")
34 || path.starts_with("http://")
35 || path.starts_with("https://")
36}
37
38// ─── Dispatch ─────────────────────────────────────────────────────────────────
39
40/// Open a cloud-hosted dataset, returning a lightweight [`Dataset`] descriptor.
41///
42/// When the `cloud` feature is enabled this function constructs a `Dataset`
43/// whose metadata is inferred from the URL path extension (e.g. `"*.tif"` →
44/// [`DatasetFormat::GeoTiff`]). Actual pixel/feature data must be fetched
45/// via the `oxigdal-cloud` streaming interface.
46///
47/// When the `cloud` feature is **not** enabled, always returns
48/// [`OxiGdalError::NotSupported`].
49///
50/// # Errors
51///
52/// - [`OxiGdalError::NotSupported`] — `cloud` feature not compiled in.
53/// - [`OxiGdalError::InvalidParameter`] — `path` is not a valid cloud URI.
54pub fn open_cloud_dataset(path: &str) -> Result<Dataset> {
55 if !is_cloud_uri(path) {
56 return Err(OxiGdalError::InvalidParameter {
57 parameter: "path",
58 message: format!("'{}' is not a recognised cloud URI", path),
59 });
60 }
61
62 #[cfg(feature = "cloud")]
63 {
64 open_cloud_with_feature(path)
65 }
66
67 #[cfg(not(feature = "cloud"))]
68 {
69 let _ = path;
70 Err(OxiGdalError::NotSupported {
71 operation: format!(
72 "Dataset::open(\"{path}\") — cloud URIs require the 'cloud' feature flag",
73 ),
74 })
75 }
76}
77
78/// Feature-gated inner implementation that creates the cloud Dataset stub.
79#[cfg(feature = "cloud")]
80fn open_cloud_with_feature(path: &str) -> Result<Dataset> {
81 // Infer format from the URL extension for metadata purposes.
82 let guessed_format = DatasetFormat::from_extension(path);
83
84 let info = DatasetInfo {
85 format: guessed_format,
86 path: Some(path.to_string()),
87 width: None,
88 height: None,
89 band_count: 0,
90 layer_count: 0,
91 crs: None,
92 geotransform: None,
93 feature_count: None,
94 bounds: None,
95 };
96
97 // Validate that the oxigdal-cloud crate can at least parse the URI scheme
98 // (scheme-level parsing only, no network I/O).
99 // On error we surface the problem early with a good message.
100 validate_cloud_uri(path)?;
101
102 Ok(crate::Dataset::from_info(path.to_string(), info))
103}
104
105/// Validate the cloud URI via the oxigdal-cloud crate (scheme-level check only).
106///
107/// Returns `Ok(())` if the URI is well-formed for the detected scheme.
108#[cfg(feature = "cloud")]
109fn validate_cloud_uri(path: &str) -> Result<()> {
110 // Use oxigdal-cloud's URL parser for scheme validation.
111 // `CloudBackend::from_url` does not initiate network I/O — it only parses.
112 let _backend = oxigdal_cloud::CloudBackend::from_url(path).map_err(|e| {
113 OxiGdalError::InvalidParameter {
114 parameter: "path",
115 message: format!("invalid cloud URI '{}': {e}", path),
116 }
117 })?;
118 Ok(())
119}