Skip to main content

oxigdal_services/wfs/
mod.rs

1//! WFS (Web Feature Service) 2.0/3.0 implementation
2//!
3//! Provides OGC-compliant Web Feature Service supporting:
4//! - GetCapabilities: Service metadata
5//! - DescribeFeatureType: Schema information
6//! - GetFeature: Feature retrieval with filtering
7//! - Transaction: Feature insert, update, delete operations
8//!
9//! # Standards
10//!
11//! - OGC WFS 2.0.0 (ISO 19142:2010)
12//! - OGC WFS 3.0 (OGC API - Features Part 1: Core)
13//!
14//! # Example
15//!
16//! ```no_run
17//! use oxigdal_services::wfs::{ServiceInfo, WfsState};
18//!
19//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
20//! let info = ServiceInfo {
21//!     title: "My WFS Service".to_string(),
22//!     abstract_text: None,
23//!     provider: "Provider".to_string(),
24//!     service_url: "http://localhost:8080/wfs".to_string(),
25//!     versions: vec!["2.0.0".to_string()],
26//! };
27//! let service = WfsState::new(info);
28//! // Add feature types and handle requests
29//! # Ok(())
30//! # }
31//! ```
32
33pub mod capabilities;
34pub mod database;
35pub mod features;
36pub mod transactions;
37
38use crate::error::{ServiceError, ServiceResult};
39use axum::{
40    extract::{Query, State},
41    response::Response,
42};
43use serde::Deserialize;
44use std::sync::Arc;
45
46/// WFS service state
47#[derive(Clone)]
48pub struct WfsState {
49    /// Service metadata
50    pub service_info: Arc<ServiceInfo>,
51    /// Feature type registry
52    pub feature_types: Arc<dashmap::DashMap<String, FeatureTypeInfo>>,
53    /// Transaction support enabled
54    pub transactions_enabled: bool,
55}
56
57/// Service metadata
58#[derive(Debug, Clone)]
59pub struct ServiceInfo {
60    /// Service title
61    pub title: String,
62    /// Service abstract/description
63    pub abstract_text: Option<String>,
64    /// Service provider
65    pub provider: String,
66    /// Service URL
67    pub service_url: String,
68    /// Supported versions
69    pub versions: Vec<String>,
70}
71
72/// Feature type information
73#[derive(Debug, Clone)]
74pub struct FeatureTypeInfo {
75    /// Type name
76    pub name: String,
77    /// Title
78    pub title: String,
79    /// Abstract
80    pub abstract_text: Option<String>,
81    /// Default CRS
82    pub default_crs: String,
83    /// Other supported CRS
84    pub other_crs: Vec<String>,
85    /// Bounding box (minx, miny, maxx, maxy)
86    pub bbox: Option<(f64, f64, f64, f64)>,
87    /// Feature source
88    pub source: FeatureSource,
89}
90
91/// Feature data source
92#[derive(Debug, Clone)]
93pub enum FeatureSource {
94    /// File-based source (GeoJSON, Shapefile, etc.)
95    File(std::path::PathBuf),
96    /// Database source (PostGIS, etc.) with connection string (legacy)
97    Database(String),
98    /// Database source with full configuration
99    DatabaseSource(database::DatabaseSource),
100    /// In-memory features
101    Memory(Vec<geojson::Feature>),
102}
103
104// Re-export database types
105pub use database::{
106    BboxFilter, CacheStats, CountCacheConfig, CountResult, CqlFilter, DatabaseFeatureCounter,
107    DatabaseSource, DatabaseType,
108};
109
110/// WFS request parameters
111#[derive(Debug, Deserialize)]
112#[serde(rename_all = "UPPERCASE")]
113pub struct WfsRequest {
114    /// Service name (must be "WFS")
115    pub service: Option<String>,
116    /// WFS version
117    pub version: Option<String>,
118    /// Request operation
119    pub request: String,
120    /// Additional parameters
121    #[serde(flatten)]
122    pub params: serde_json::Value,
123}
124
125impl WfsState {
126    /// Create new WFS service state
127    pub fn new(service_info: ServiceInfo) -> Self {
128        Self {
129            service_info: Arc::new(service_info),
130            feature_types: Arc::new(dashmap::DashMap::new()),
131            transactions_enabled: false,
132        }
133    }
134
135    /// Add a feature type
136    pub fn add_feature_type(&self, info: FeatureTypeInfo) -> ServiceResult<()> {
137        self.feature_types.insert(info.name.clone(), info);
138        Ok(())
139    }
140
141    /// Get feature type by name
142    pub fn get_feature_type(&self, name: &str) -> Option<FeatureTypeInfo> {
143        self.feature_types
144            .get(name)
145            .map(|entry| entry.value().clone())
146    }
147
148    /// Enable transaction support
149    pub fn enable_transactions(&mut self) {
150        self.transactions_enabled = true;
151    }
152}
153
154/// Main WFS request handler
155pub async fn handle_wfs_request(
156    State(state): State<WfsState>,
157    Query(params): Query<WfsRequest>,
158) -> Result<Response, ServiceError> {
159    // Validate service parameter
160    if let Some(ref service) = params.service {
161        if service.to_uppercase() != "WFS" {
162            return Err(ServiceError::InvalidParameter(
163                "SERVICE".to_string(),
164                format!("Expected 'WFS', got '{}'", service),
165            ));
166        }
167    }
168
169    // Route to appropriate handler based on request type
170    match params.request.to_uppercase().as_str() {
171        "GETCAPABILITIES" => {
172            let version = params.version.as_deref().unwrap_or("2.0.0");
173            capabilities::handle_get_capabilities(&state, version).await
174        }
175        "DESCRIBEFEATURETYPE" => {
176            let version = params.version.as_deref().unwrap_or("2.0.0");
177            features::handle_describe_feature_type(&state, version, &params.params).await
178        }
179        "GETFEATURE" => {
180            let version = params.version.as_deref().unwrap_or("2.0.0");
181            features::handle_get_feature(&state, version, &params.params).await
182        }
183        "TRANSACTION" => {
184            if !state.transactions_enabled {
185                return Err(ServiceError::UnsupportedOperation(
186                    "Transactions not enabled".to_string(),
187                ));
188            }
189            let version = params.version.as_deref().unwrap_or("2.0.0");
190            transactions::handle_transaction(&state, version, &params.params).await
191        }
192        _ => Err(ServiceError::UnsupportedOperation(params.request.clone())),
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_wfs_state_creation() {
202        let info = ServiceInfo {
203            title: "Test WFS".to_string(),
204            abstract_text: Some("Test service".to_string()),
205            provider: "COOLJAPAN OU".to_string(),
206            service_url: "http://localhost/wfs".to_string(),
207            versions: vec!["2.0.0".to_string()],
208        };
209
210        let state = WfsState::new(info);
211        assert_eq!(state.service_info.title, "Test WFS");
212        assert!(!state.transactions_enabled);
213    }
214
215    #[test]
216    fn test_add_feature_type() {
217        let info = ServiceInfo {
218            title: "Test WFS".to_string(),
219            abstract_text: None,
220            provider: "COOLJAPAN OU".to_string(),
221            service_url: "http://localhost/wfs".to_string(),
222            versions: vec!["2.0.0".to_string()],
223        };
224
225        let state = WfsState::new(info);
226
227        let feature_type = FeatureTypeInfo {
228            name: "test_layer".to_string(),
229            title: "Test Layer".to_string(),
230            abstract_text: None,
231            default_crs: "EPSG:4326".to_string(),
232            other_crs: vec![],
233            bbox: Some((-180.0, -90.0, 180.0, 90.0)),
234            source: FeatureSource::Memory(vec![]),
235        };
236
237        assert!(
238            state.add_feature_type(feature_type).is_ok(),
239            "Failed to add feature type"
240        );
241
242        let retrieved = state.get_feature_type("test_layer");
243        assert!(retrieved.is_some());
244        assert_eq!(
245            retrieved.as_ref().map(|ft| &ft.name),
246            Some(&"test_layer".to_string())
247        );
248    }
249}