Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/Air/
AirServiceProvider.rs

1//! # AirServiceProvider
2//!
3//! High-level API surface for Air service methods.
4//!
5//! ## RESPONSIBILITIES
6//!
7//! - **Service Facade**: Provide convenient, high-level interface to Air daemon
8//! - **Authentication**: Manage user authentication and credentials
9//! - **Updates**: Check for and download application updates
10//! - **File Indexing**: Query Air's file search and indexing capabilities
11//! - **System Monitoring**: Retrieve system metrics and health data
12//! - **Graceful Degradation**: Handle Air unavailability with fallbacks
13//!
14//! ## ARCHITECTURAL ROLE
15//!
16//! AirServiceProvider acts as a facade over the raw `AirClient`, providing:
17//! - Simplified API for common operations
18//! - Automatic error handling and translation
19//! - Request ID generation for tracing
20//! - Connection state management
21//!
22//! ```text
23//! Application ──► AirServiceProvider ──► AirClient ──► gRPC ──► Air Daemon
24//! ```
25//!
26//! ### Dependencies
27//! - `AirClient`: Low-level gRPC client
28//! - `uuid`: For generating request identifiers
29//! - `CommonLibrary::Error::CommonError`: Error types
30//!
31//! ### Dependents
32//! - `Binary::Service::VineStart`: Initializes Air service
33//! - `MountainEnvironment`: Can delegate to Air when available
34//!
35//! ## IMPLEMENTATION
36//!
37//! This implementation provides a fully functional provider that wraps the
38//! AirClient type with automatic request ID generation and error handling.
39//!
40//! ## ERROR HANDLING
41//!
42//! All operations return `Result<T, CommonError>` with:
43//! - Translated gRPC errors to appropriate CommonError types
44//! - Request IDs included in logs for tracing
45//! - Graceful fallback to local operations when Air is unavailable
46//!
47//! ## PERFORMANCE
48//!
49//! - Request ID generation uses UUID v4 (cryptographically random)
50//! - Thread-safe operations via `Arc<AirClient>`
51//! - Non-blocking async operations via tokio
52//!
53//! ## VSCODE REFERENCE
54//!
55//! Patterns borrowed from VS Code:
56//! - `vs/platform/update/common/updateService.ts` - Update management
57//! - `vs/platform/authentication/common/authenticationService.ts` - Auth
58//!   handling
59//! - `vs/platform/filesystem/common/filesystem.ts` - File indexing
60//!
61//! ## MODULE CONTENTS
62//!
63//! - [`AirServiceProvider`]: Main provider struct
64//! - [`generate_request_id`]: Helper function for UUID generation
65
66use std::{collections::HashMap, sync::Arc};
67
68use CommonLibrary::Error::CommonError::CommonError;
69
70use super::AirClient::{
71	AirClient,
72	AirMetrics,
73	AirStatus,
74	DEFAULT_AIR_SERVER_ADDRESS,
75	DownloadStream,
76	DownloadStreamChunk,
77	ExtendedFileInfo,
78	FileInfo,
79	FileResult,
80	IndexInfo,
81	ResourceUsage,
82	UpdateInfo,
83};
84use crate::{Air::AirServiceProvider::GenerateRequestID::Fn as generate_request_id, dev_log};
85
86pub mod GenerateRequestID;
87
88// ============================================================================
89// AirServiceProvider - High-level API Implementation
90// ============================================================================
91
92/// AirServiceProvider provides a high-level, convenient interface to the Air
93/// daemon service.
94///
95/// This provider wraps the AirClient and provides simplified methods with
96/// automatic request ID generation and error handling. It acts as a facade
97/// pattern, hiding the complexity of gRPC communication from the rest of the
98/// Mountain application.
99///
100/// # Example
101///
102/// ```text
103/// use Mountain::Air::AirServiceProvider::{AirServiceProvider, DEFAULT_AIR_SERVER_ADDRESS};
104/// use CommonLibrary::Error::CommonError::CommonError;
105///
106/// # #[tokio::main]
107/// # async fn main() -> Result<(), CommonError> {
108/// let provider = AirServiceProvider::new(DEFAULT_AIR_SERVER_ADDRESS.to_string()).await?;
109///
110/// // Check for health
111/// let is_healthy = provider.health_check().await?;
112/// println!("Air service healthy: {}", is_healthy);
113///
114/// // Check for updates
115/// if let Some(update) =
116/// 	provider.check_for_updates("1.0.0".to_string(), "stable".to_string()).await?
117/// {
118/// 	println!("Update available: {}", update.version);
119/// }
120///
121/// # Ok(())
122/// # }
123/// ```
124#[derive(Debug, Clone)]
125pub struct AirServiceProvider {
126	/// The underlying Air client wrapped in Arc for thread safety
127	client:Arc<AirClient>,
128}
129
130impl AirServiceProvider {
131	/// Creates a new AirServiceProvider and connects to the Air daemon.
132	///
133	/// # Arguments
134	/// * `address` - The gRPC server address (defaults to `[::1]:50053`)
135	///
136	/// # Returns
137	/// * `Ok(Self)` - Successfully created provider
138	/// * `Err(CommonError)` - Connection failure
139	///
140	/// # Example
141	///
142	/// ```text
143	/// use Mountain::Air::AirServiceProvider::AirServiceProvider;
144	/// use CommonLibrary::Error::CommonError::CommonError;
145	///
146	/// # #[tokio::main]
147	/// # async fn main() -> Result<(), CommonError> {
148	/// let provider = AirServiceProvider::new("http://[::1]:50053".to_string()).await?;
149	/// # Ok(())
150	/// # }
151	/// ```
152	pub async fn new(address:String) -> Result<Self, CommonError> {
153		dev_log!("grpc", "[AirServiceProvider] Creating AirServiceProvider at: {}", address);
154
155		let client = AirClient::new(&address).await?;
156
157		dev_log!("grpc", "[AirServiceProvider] AirServiceProvider created successfully");
158
159		Ok(Self { client:Arc::new(client) })
160	}
161
162	/// Creates a new AirServiceProvider with the default address.
163	///
164	/// This is a convenience method that uses [`DEFAULT_AIR_SERVER_ADDRESS`].
165	///
166	/// # Returns
167	/// * `Ok(Self)` - Successfully created provider
168	/// * `Err(CommonError)` - Connection failure
169	///
170	/// # Example
171	///
172	/// ```text
173	/// use Mountain::Air::AirServiceProvider::AirServiceProvider;
174	/// use CommonLibrary::Error::CommonError::CommonError;
175	///
176	/// # #[tokio::main]
177	/// # async fn main() -> Result<(), CommonError> {
178	/// let provider = AirServiceProvider::new_default().await?;
179	/// # Ok(())
180	/// # }
181	/// ```
182	pub async fn new_default() -> Result<Self, CommonError> { Self::new(DEFAULT_AIR_SERVER_ADDRESS.to_string()).await }
183
184	/// Creates a new AirServiceProvider from an existing AirClient.
185	///
186	/// This is useful when you need to share a client or have special
187	/// connection requirements.
188	///
189	/// # Arguments
190	/// * `client` - The AirClient to wrap
191	///
192	/// # Returns
193	/// * `Self` - The new provider
194	pub fn from_client(client:Arc<AirClient>) -> Self {
195		dev_log!("grpc", "[AirServiceProvider] Creating AirServiceProvider from existing client");
196
197		Self { client }
198	}
199
200	/// Gets a reference to the underlying AirClient.
201	///
202	/// This provides access to the low-level client when needed.
203	///
204	/// # Returns
205	/// Reference to the AirClient
206	pub fn client(&self) -> &Arc<AirClient> { &self.client }
207
208	/// Checks if the provider is connected to Air.
209	///
210	/// # Returns
211	/// * `true` - Connected
212	/// * `false` - Not connected
213	pub fn is_connected(&self) -> bool { self.client.is_connected() }
214
215	/// Gets the address of the Air daemon.
216	///
217	/// # Returns
218	/// The address string
219	pub fn address(&self) -> &str { self.client.address() }
220
221	// =========================================================================
222	// Authentication Operations
223	// =========================================================================
224
225	/// Authenticates a user with the Air daemon.
226	///
227	/// This method handles request ID generation and provides a simplified
228	/// interface for authentication.
229	///
230	/// # Arguments
231	/// * `username` - User's username
232	/// * `password` - User's password
233	/// * `provider` - Authentication provider (e.g., "github", "gitlab",
234	///   "microsoft")
235	///
236	/// # Returns
237	/// * `Ok(token)` - Authentication token if successful
238	/// * `Err(CommonError)` - Authentication error
239	///
240	/// # Example
241	///
242	/// ```text
243	/// # use Mountain::Air::AirServiceProvider::AirServiceProvider;
244	/// # use CommonLibrary::Error::CommonError::CommonError;
245	/// # #[tokio::main]
246	/// # async fn main() -> Result<(), CommonError> {
247	/// # let provider = AirServiceProvider::new_default().await?;
248	/// let token = provider
249	/// 	.authenticate("[email protected]".to_string(), "password".to_string(), "github".to_string())
250	/// 	.await?;
251	/// println!("Auth token: {}", token);
252	/// # Ok(())
253	/// # }
254	/// ```
255	pub async fn authenticate(&self, username:String, password:String, provider:String) -> Result<String, CommonError> {
256		let request_id = generate_request_id();
257
258		dev_log!("grpc", "[AirServiceProvider] authenticate (request_id: {})", request_id);
259
260		self.client.authenticate(request_id, username, password, provider).await
261	}
262
263	// =========================================================================
264	// Update Operations
265	// =========================================================================
266
267	/// Checks for available updates.
268	///
269	/// Returns None if no update is available, Some with update info otherwise.
270	///
271	/// # Arguments
272	/// * `current_version` - Current application version
273	/// * `channel` - Update channel (e.g., "stable", "beta", "nightly")
274	///
275	/// # Returns
276	/// * `Ok(Some(update))` - Update available with information
277	/// * `Ok(None)` - No update available
278	/// * `Err(CommonError)` - Check error
279	///
280	/// # Example
281	///
282	/// ```text
283	/// # use Mountain::Air::AirServiceProvider::AirServiceProvider;
284	/// # use CommonLibrary::Error::CommonError::CommonError;
285	/// # #[tokio::main]
286	/// # async fn main() -> Result<(), CommonError> {
287	/// # let provider = AirServiceProvider::new_default().await?;
288	/// if let Some(update) =
289	/// 	provider.check_for_updates("1.0.0".to_string(), "stable".to_string()).await?
290	/// {
291	/// 	println!("Update available: version {}", update.version);
292	/// }
293	/// # Ok(())
294	/// # }
295	/// ```
296	pub async fn check_for_updates(
297		&self,
298
299		current_version:String,
300
301		channel:String,
302	) -> Result<Option<UpdateInfo::Struct>, CommonError> {
303		let request_id = generate_request_id();
304
305		dev_log!("grpc", "[AirServiceProvider] check_for_updates (request_id: {})", request_id);
306
307		let info = self.client.check_for_updates(request_id, current_version, channel).await?;
308
309		if info.update_available { Ok(Some(info)) } else { Ok(None) }
310	}
311
312	/// Downloads an update package.
313	///
314	/// # Arguments
315	/// * `url` - URL of the update package
316	/// * `destination_path` - Local path to save the downloaded file
317	/// * `checksum` - Optional SHA256 checksum for verification
318	///
319	/// # Returns
320	/// * `Ok(file_info)` - Downloaded file information
321	/// * `Err(CommonError)` - Download error
322	pub async fn download_update(
323		&self,
324
325		url:String,
326
327		destination_path:String,
328
329		checksum:String,
330	) -> Result<FileInfo::Struct, CommonError> {
331		let request_id = generate_request_id();
332
333		dev_log!("grpc", "[AirServiceProvider] download_update (request_id: {})", request_id);
334
335		self.client
336			.download_update(request_id, url, destination_path, checksum, HashMap::new())
337			.await
338	}
339
340	/// Applies an update package.
341	///
342	/// # Arguments
343	/// * `version` - Version of the update
344	/// * `update_path` - Path to the update package
345	///
346	/// # Returns
347	/// * `Ok(())` - Update applied successfully
348	/// * `Err(CommonError)` - Application error
349	pub async fn apply_update(&self, version:String, update_path:String) -> Result<(), CommonError> {
350		let request_id = generate_request_id();
351
352		dev_log!("grpc", "[AirServiceProvider] apply_update (request_id: {})", request_id);
353
354		self.client.apply_update(request_id, version, update_path).await
355	}
356
357	// =========================================================================
358	// Download Operations
359	// =========================================================================
360
361	/// Downloads a file.
362	///
363	/// # Arguments
364	/// * `url` - URL of the file to download
365	/// * `destination_path` - Local path to save the downloaded file
366	/// * `checksum` - Optional SHA256 checksum for verification
367	///
368	/// # Returns
369	/// * `Ok(file_info)` - Downloaded file information
370	/// * `Err(CommonError)` - Download error
371	pub async fn download_file(
372		&self,
373
374		url:String,
375
376		destination_path:String,
377
378		checksum:String,
379	) -> Result<FileInfo::Struct, CommonError> {
380		let request_id = generate_request_id();
381
382		dev_log!("grpc", "[AirServiceProvider] download_file (request_id: {})", request_id);
383
384		self.client
385			.download_file(request_id, url, destination_path, checksum, HashMap::new())
386			.await
387	}
388
389	/// Downloads a file as a stream.
390	///
391	/// This method initiates a streaming download from the given URL, returning
392	/// a stream of chunks that can be processed incrementally without loading
393	/// the entire file into memory.
394	///
395	/// # Arguments
396	/// * `url` - URL of the file to download
397	/// * `headers` - Optional HTTP headers
398	///
399	/// # Returns
400	/// * `Ok(stream)` - Stream that yields download chunks
401	/// * `Err(CommonError)` - Download error
402	///
403	/// # Example
404	///
405	/// ```text
406	/// # use Mountain::Air::AirServiceProvider::AirServiceProvider;
407	/// # use CommonLibrary::Error::CommonError::CommonError;
408	/// # #[tokio::main]
409	/// # async fn main() -> Result<(), CommonError> {
410	/// # let provider = AirServiceProvider::new_default().await?;
411	/// let mut stream = provider
412	/// 	.download_stream(
413	/// 		"https://example.com/large-file.zip".to_string(),
414	/// 		std::collections::HashMap::new(),
415	/// 	)
416	/// 	.await?;
417	///
418	/// let mut buffer = Vec::new();
419	/// while let Some(chunk) = stream.next().await {
420	/// 	let chunk = chunk?;
421	/// 	buffer.extend_from_slice(&chunk.data);
422	/// 	println!("Downloaded: {} / {} bytes", chunk.downloaded, chunk.total_size);
423	/// 	if chunk.completed {
424	/// 		break;
425	/// 	}
426	/// }
427	/// # Ok(())
428	/// # }
429	/// ```
430	pub async fn download_stream(
431		&self,
432
433		url:String,
434
435		headers:HashMap<String, String>,
436	) -> Result<DownloadStream::Struct, CommonError> {
437		let request_id = generate_request_id();
438
439		dev_log!(
440			"grpc",
441			"[AirServiceProvider] download_stream (request_id: {}, url: {})",
442			request_id,
443			url
444		);
445
446		self.client.download_stream(request_id, url, headers).await
447	}
448
449	// =========================================================================
450	// File Indexing Operations
451	// =========================================================================
452
453	/// Indexes files in a directory.
454	///
455	/// # Arguments
456	/// * `path` - Path to the directory to index
457	/// * `patterns` - File patterns to include (e.g., ["*.rs", "*.ts"])
458	/// * `exclude_patterns` - File patterns to exclude (e.g.,
459	///   ["node_modules/*"])
460	/// * `max_depth` - Maximum depth for recursion (0 for unlimited)
461	///
462	/// # Returns
463	/// * `Ok(index_info)` - Index information with file count and total size
464	/// * `Err(CommonError)` - Indexing error
465	///
466	/// # Example
467	///
468	/// ```text
469	/// # use Mountain::Air::AirServiceProvider::AirServiceProvider;
470	/// # use CommonLibrary::Error::CommonError::CommonError;
471	/// # #[tokio::main]
472	/// # async fn main() -> Result<(), CommonError> {
473	/// # let provider = AirServiceProvider::new_default().await?;
474	/// let info = provider
475	/// 	.index_files(
476	/// 		"/path/to/project".to_string(),
477	/// 		vec!["*.rs".to_string(), "*.ts".to_string()],
478	/// 		vec!["node_modules/*".to_string()],
479	/// 		10,
480	/// 	)
481	/// 	.await?;
482	/// println!("Indexed {} files ({} bytes)", info.files_indexed, info.total_size);
483	/// # Ok(())
484	/// # }
485	/// ```
486	pub async fn index_files(
487		&self,
488
489		path:String,
490
491		patterns:Vec<String>,
492
493		exclude_patterns:Vec<String>,
494
495		max_depth:u32,
496	) -> Result<IndexInfo::Struct, CommonError> {
497		let request_id = generate_request_id();
498
499		dev_log!(
500			"grpc",
501			"[AirServiceProvider] index_files (request_id: {}, path: {})",
502			request_id,
503			path
504		);
505
506		self.client
507			.index_files(request_id, path, patterns, exclude_patterns, max_depth)
508			.await
509	}
510
511	/// Searches for files matching a query.
512	///
513	/// # Arguments
514	/// * `query` - Search query string
515	/// * `path` - Path to search in (empty for entire workspace)
516	/// * `max_results` - Maximum number of results to return (0 for unlimited)
517	///
518	/// # Returns
519	/// * `Ok(results)` - Vector of file search results
520	/// * `Err(CommonError)` - Search error
521	///
522	/// # Example
523	///
524	/// ```text
525	/// # use Mountain::Air::AirServiceProvider::AirServiceProvider;
526	/// # use CommonLibrary::Error::CommonError::CommonError;
527	/// # #[tokio::main]
528	/// # async fn main() -> Result<(), CommonError> {
529	/// # let provider = AirServiceProvider::new_default().await?;
530	/// let results = provider
531	/// 	.search_files("fn main".to_string(), "/path/to/project".to_string(), 50)
532	/// 	.await?;
533	/// for result in results {
534	/// 	println!("Found: {} at line {}", result.path, result.line_number);
535	/// }
536	/// # Ok(())
537	/// # }
538	/// ```
539	pub async fn search_files(
540		&self,
541
542		query:String,
543
544		path:String,
545
546		max_results:u32,
547	) -> Result<Vec<FileResult::Struct>, CommonError> {
548		let request_id = generate_request_id();
549
550		dev_log!(
551			"grpc",
552			"[AirServiceProvider] search_files (request_id: {}, query: {})",
553			request_id,
554			query
555		);
556
557		self.client.search_files(request_id, query, path, max_results).await
558	}
559
560	/// Gets file information.
561	///
562	/// # Arguments
563	/// * `path` - Path to the file
564	///
565	/// # Returns
566	/// * `Ok(file_info)` - Extended file information
567	/// * `Err(CommonError)` - Request error
568	pub async fn get_file_info(&self, path:String) -> Result<ExtendedFileInfo::Struct, CommonError> {
569		let request_id = generate_request_id();
570
571		dev_log!(
572			"grpc",
573			"[AirServiceProvider] get_file_info (request_id: {}, path: {})",
574			request_id,
575			path
576		);
577
578		self.client.get_file_info(request_id, path).await
579	}
580
581	// =========================================================================
582	// Status and Monitoring Operations
583	// =========================================================================
584
585	/// Gets the status of the Air daemon.
586	///
587	/// # Returns
588	/// * `Ok(status)` - Air daemon status information
589	/// * `Err(CommonError)` - Request error
590	pub async fn get_status(&self) -> Result<AirStatus::Struct, CommonError> {
591		let request_id = generate_request_id();
592
593		dev_log!("grpc", "[AirServiceProvider] get_status (request_id: {})", request_id);
594
595		self.client.get_status(request_id).await
596	}
597
598	/// Performs a health check on the Air daemon.
599	///
600	/// # Returns
601	/// * `Ok(healthy)` - Health status (true if healthy)
602	/// * `Err(CommonError)` - Check error
603	///
604	/// # Example
605	///
606	/// ```text
607	/// # use Mountain::Air::AirServiceProvider::AirServiceProvider;
608	/// # use CommonLibrary::Error::CommonError::CommonError;
609	/// # #[tokio::main]
610	/// # async fn main() -> Result<(), CommonError> {
611	/// # let provider = AirServiceProvider::new_default().await?;
612	/// if provider.health_check().await? {
613	/// 	println!("Air service is healthy");
614	/// }
615	/// # Ok(())
616	/// # }
617	/// ```
618	pub async fn health_check(&self) -> Result<bool, CommonError> {
619		dev_log!("grpc", "[AirServiceProvider] health_check");
620
621		self.client.health_check().await
622	}
623
624	/// Gets metrics from the Air daemon.
625	///
626	/// # Arguments
627	/// * `metric_type` - Optional type of metrics (e.g., "performance",
628	///   "resources")
629	///
630	/// # Returns
631	/// * `Ok(metrics)` - Metrics data
632	/// * `Err(CommonError)` - Request error
633	pub async fn get_metrics(&self, metric_type:Option<String>) -> Result<AirMetrics::Struct, CommonError> {
634		let request_id = generate_request_id();
635
636		dev_log!("grpc", "[AirServiceProvider] get_metrics (request_id: {})", request_id);
637
638		self.client.get_metrics(request_id, metric_type).await
639	}
640
641	// =========================================================================
642	// Resource Management Operations
643	// =========================================================================
644
645	/// Gets resource usage information.
646	///
647	/// # Returns
648	/// * `Ok(usage)` - Resource usage data
649	/// * `Err(CommonError)` - Request error
650	pub async fn get_resource_usage(&self) -> Result<ResourceUsage::Struct, CommonError> {
651		let request_id = generate_request_id();
652
653		dev_log!("grpc", "[AirServiceProvider] get_resource_usage (request_id: {})", request_id);
654
655		self.client.get_resource_usage(request_id).await
656	}
657
658	/// Sets resource limits.
659	///
660	/// # Arguments
661	/// * `memory_limit_mb` - Memory limit in MB
662	/// * `cpu_limit_percent` - CPU limit as percentage (0-100)
663	/// * `disk_limit_mb` - Disk limit in MB
664	///
665	/// # Returns
666	/// * `Ok(())` - Limits set successfully
667	/// * `Err(CommonError)` - Set error
668	pub async fn set_resource_limits(
669		&self,
670
671		memory_limit_mb:u32,
672
673		cpu_limit_percent:u32,
674
675		disk_limit_mb:u32,
676	) -> Result<(), CommonError> {
677		let request_id = generate_request_id();
678
679		dev_log!("grpc", "[AirServiceProvider] set_resource_limits (request_id: {})", request_id);
680
681		self.client
682			.set_resource_limits(request_id, memory_limit_mb, cpu_limit_percent, disk_limit_mb)
683			.await
684	}
685
686	// =========================================================================
687	// Configuration Management Operations
688	// =========================================================================
689
690	/// Gets configuration.
691	///
692	/// # Arguments
693	/// * `section` - Configuration section (e.g., "grpc", "authentication",
694	///   "updates")
695	///
696	/// # Returns
697	/// * `Ok(config)` - Configuration data as key-value pairs
698	/// * `Err(CommonError)` - Request error
699	///
700	/// # Example
701	///
702	/// ```text
703	/// # use Mountain::Air::AirServiceProvider::AirServiceProvider;
704	/// # use CommonLibrary::Error::CommonError::CommonError;
705	/// # #[tokio::main]
706	/// # async fn main() -> Result<(), CommonError> {
707	/// # let provider = AirServiceProvider::new_default().await?;
708	/// let config = provider.get_configuration("grpc".to_string()).await?;
709	/// for (key, value) in config {
710	/// 	println!("{} = {}", key, value);
711	/// }
712	/// # Ok(())
713	/// # }
714	/// ```
715	pub async fn get_configuration(&self, section:String) -> Result<HashMap<String, String>, CommonError> {
716		let request_id = generate_request_id();
717
718		dev_log!(
719			"grpc",
720			"[AirServiceProvider] get_configuration (request_id: {}, section: {})",
721			request_id,
722			section
723		);
724
725		self.client.get_configuration(request_id, section).await
726	}
727
728	/// Updates configuration.
729	///
730	/// # Arguments
731	/// * `section` - Configuration section
732	/// * `updates` - Configuration updates as key-value pairs
733	///
734	/// # Returns
735	/// * `Ok(())` - Configuration updated successfully
736	/// * `Err(CommonError)` - Update error
737	pub async fn update_configuration(
738		&self,
739
740		section:String,
741
742		updates:HashMap<String, String>,
743	) -> Result<(), CommonError> {
744		let request_id = generate_request_id();
745
746		dev_log!(
747			"grpc",
748			"[AirServiceProvider] update_configuration (request_id: {}, section: {})",
749			request_id,
750			section
751		);
752
753		self.client.update_configuration(request_id, section, updates).await
754	}
755}
756
757// Request-id helper moved to `GenerateRequestID::Fn` in the sibling file
758// declared at the top of this module. Internal callers reach it via the
759// `use self::GenerateRequestID::Fn as generate_request_id;` shorthand.
760
761// ============================================================================
762// Tests
763// ============================================================================
764
765#[cfg(test)]
766mod tests {
767
768	use super::*;
769
770	#[test]
771	fn test_generate_request_id() {
772		let id1 = generate_request_id();
773
774		let id2 = generate_request_id();
775
776		// IDs should be unique
777		assert_ne!(id1, id2);
778
779		// IDs should be valid UUIDs (simple format = 32 chars)
780		assert_eq!(id1.len(), 32);
781
782		assert_eq!(id2.len(), 32);
783
784		// IDs should be hex characters
785		assert!(id1.chars().all(|c| c.is_ascii_hexdigit()));
786
787		assert!(id2.chars().all(|c| c.is_ascii_hexdigit()));
788	}
789
790	#[test]
791	fn test_default_address() {
792		assert_eq!(DEFAULT_AIR_SERVER_ADDRESS, "[::1]:50053");
793	}
794}