data_gov_ckan/client.rs
1use crate::models;
2use serde::de::DeserializeOwned;
3use std::sync::Arc;
4
5/// Configuration for the CKAN client
6#[derive(Debug, Clone)]
7pub struct Configuration {
8 /// Base URL for the CKAN API (e.g., `https://catalog.data.gov/api/3`)
9 pub base_path: String,
10 /// User agent string for HTTP requests
11 pub user_agent: Option<String>,
12 /// HTTP client instance
13 pub client: reqwest::Client,
14 /// Basic authentication credentials (username, optional password)
15 pub basic_auth: Option<BasicAuth>,
16 /// OAuth access token
17 pub oauth_access_token: Option<String>,
18 /// Bearer token for authentication
19 pub bearer_access_token: Option<String>,
20 /// API key for CKAN authentication
21 pub api_key: Option<ApiKey>,
22}
23
24/// Basic authentication credentials
25pub type BasicAuth = (String, Option<String>);
26
27/// API key configuration
28#[derive(Debug, Clone)]
29pub struct ApiKey {
30 /// Optional prefix for the API key (e.g., "Bearer")
31 pub prefix: Option<String>,
32 /// The actual API key value
33 pub key: String,
34}
35
36impl Configuration {
37 /// Create a new configuration with default values
38 pub fn new() -> Configuration {
39 Configuration::default()
40 }
41}
42
43impl Default for Configuration {
44 fn default() -> Self {
45 Configuration {
46 base_path: "https://catalog.data.gov/api/3".to_owned(),
47 user_agent: Some(concat!("data-gov-rs/", env!("CARGO_PKG_VERSION")).to_owned()),
48 client: reqwest::Client::new(),
49 basic_auth: None,
50 oauth_access_token: None,
51 bearer_access_token: None,
52 api_key: None,
53 }
54 }
55}
56
57/// Async CKAN client focused on data.gov's read APIs.
58///
59/// `CkanClient` wraps the generated request glue and exposes ergonomic async
60/// methods for popular CKAN endpoints such as `package_search`,
61/// `package_show`, organization listings, and autocomplete helpers. The client
62/// is cheap to clone and share across tasks because it only holds an
63/// [`Arc<Configuration>`].
64///
65/// ```rust,no_run
66/// use data_gov_ckan::{CkanClient, Configuration};
67/// use std::sync::Arc;
68///
69/// # #[tokio::main]
70/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
71/// let client = CkanClient::new(Arc::new(Configuration::default()));
72/// let results = client.package_search(Some("climate"), Some(5), None, None).await?;
73/// println!("{} datasets", results.count.unwrap_or(0));
74/// # Ok(()) }
75/// ```
76pub struct CkanClient {
77 configuration: Arc<Configuration>,
78}
79
80impl std::fmt::Debug for CkanClient {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("CkanClient")
83 .field("base_path", &self.configuration.base_path)
84 .finish()
85 }
86}
87
88/// Errors that can occur when interacting with the CKAN API
89///
90/// This enum provides detailed error information for different types of failures
91/// that can occur during CKAN API operations.
92///
93/// # Examples
94///
95/// ```rust
96/// # use data_gov_ckan::{CkanClient, CkanError};
97/// # async fn example() {
98/// match some_api_call().await {
99/// Ok(result) => println!("Success: {:?}", result),
100/// Err(CkanError::RequestError(e)) => {
101/// eprintln!("Network or HTTP error: {}", e);
102/// },
103/// Err(CkanError::ParseError(e)) => {
104/// eprintln!("Failed to parse API response: {}", e);
105/// },
106/// Err(CkanError::ApiError { status, message }) => {
107/// eprintln!("CKAN API returned error {}: {}", status, message);
108/// }
109/// }
110/// # async fn some_api_call() -> Result<(), CkanError> { Ok(()) }
111/// # }
112/// ```
113#[derive(Debug)]
114pub enum CkanError {
115 /// Network, HTTP, or other request-level errors
116 ///
117 /// This includes connection failures, timeouts, DNS resolution issues,
118 /// and HTTP protocol errors (like 500 Internal Server Error).
119 RequestError(Box<dyn std::error::Error + Send + Sync>),
120
121 /// JSON parsing or deserialization errors
122 ///
123 /// Occurs when the CKAN API returns data that doesn't match expected schema,
124 /// invalid JSON, or when response format has changed.
125 ParseError(serde_json::Error),
126
127 /// CKAN-specific API errors with status codes
128 ///
129 /// These are semantic errors from CKAN itself, like:
130 /// - 404: Dataset not found
131 /// - 403: Insufficient permissions
132 /// - 400: Invalid parameters
133 /// - 409: Resource conflicts
134 ApiError {
135 /// HTTP status code from the CKAN API
136 status: u16,
137 /// Human-readable error message from CKAN
138 message: String,
139 },
140}
141
142impl std::fmt::Display for CkanError {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 match self {
145 CkanError::RequestError(e) => write!(f, "Request error: {}", e),
146 CkanError::ParseError(e) => write!(f, "Parse error: {}", e),
147 CkanError::ApiError { status, message } => {
148 write!(f, "CKAN API error ({}): {}", status, message)
149 }
150 }
151 }
152}
153
154impl std::error::Error for CkanError {}
155
156impl CkanClient {
157 /// Create a new CKAN client instance
158 ///
159 /// Creates a client configured to work with a specific CKAN instance.
160 /// For data.gov, use the base URL: `https://catalog.data.gov/api/3`
161 ///
162 /// # Arguments
163 ///
164 /// * `configuration` - API configuration including base URL, user agent, and credentials
165 ///
166 /// # Examples
167 ///
168 /// ```rust
169 /// # use data_gov_ckan::{CkanClient, Configuration, ApiKey};
170 /// # use std::sync::Arc;
171 ///
172 /// // Basic client for read-only operations
173 /// let config = Arc::new(Configuration {
174 /// base_path: "https://catalog.data.gov/api/3".to_string(),
175 /// user_agent: Some("my-rust-app/1.0".to_string()),
176 /// client: reqwest::Client::new(),
177 /// basic_auth: None,
178 /// oauth_access_token: None,
179 /// bearer_access_token: None,
180 /// api_key: None,
181 /// });
182 ///
183 /// let client = CkanClient::new(config);
184 ///
185 /// // Client with API key for write operations
186 /// let authenticated_config = Arc::new(Configuration {
187 /// base_path: "https://catalog.data.gov/api/3".to_string(),
188 /// user_agent: Some("my-rust-app/1.0".to_string()),
189 /// client: reqwest::Client::new(),
190 /// basic_auth: None,
191 /// oauth_access_token: None,
192 /// bearer_access_token: None,
193 /// api_key: Some(ApiKey {
194 /// prefix: None,
195 /// key: "your-api-key-here".to_string(),
196 /// }),
197 /// });
198 ///
199 /// let auth_client = CkanClient::new(authenticated_config);
200 /// ```
201 pub fn new(configuration: Arc<Configuration>) -> Self {
202 Self { configuration }
203 }
204
205 /// Call a CKAN action API endpoint and deserialize the result.
206 ///
207 /// All CKAN action endpoints follow the same pattern: GET a URL under
208 /// `/action/<name>` with query parameters, receive a JSON wrapper with
209 /// `{ success: bool, result: ... }`, and extract the `result` field.
210 /// This helper encapsulates that entire flow.
211 async fn call_action<T: DeserializeOwned>(
212 &self,
213 action: &str,
214 params: &[(&str, &str)],
215 ) -> Result<T, CkanError> {
216 let url = format!("{}/action/{}", self.configuration.base_path, action);
217
218 let response = self
219 .configuration
220 .client
221 .get(&url)
222 .query(params)
223 .send()
224 .await
225 .map_err(|e| CkanError::RequestError(Box::new(e)))?;
226
227 if !response.status().is_success() {
228 let status = response.status().as_u16();
229 let error_text = response
230 .text()
231 .await
232 .unwrap_or_else(|_| "Unknown error".to_string());
233 return Err(CkanError::ApiError {
234 status,
235 message: error_text,
236 });
237 }
238
239 let wrapper: models::ActionResponse = response
240 .json()
241 .await
242 .map_err(|e| CkanError::RequestError(Box::new(e)))?;
243
244 if !wrapper.success {
245 return Err(CkanError::ApiError {
246 status: 400,
247 message: "CKAN API reported failure".to_string(),
248 });
249 }
250
251 match wrapper.result {
252 Some(value) => serde_json::from_value(value).map_err(CkanError::ParseError),
253 None => Err(CkanError::ApiError {
254 status: 500,
255 message: "No result data in API response".to_string(),
256 }),
257 }
258 }
259
260 /// Search for datasets (packages) with advanced filtering and faceting
261 ///
262 /// This is the primary method for discovering datasets in the CKAN catalog.
263 /// It provides powerful search capabilities with full-text search, filtering,
264 /// faceting, and pagination.
265 ///
266 /// # Arguments
267 ///
268 /// * `q` - Search query string (searches title, description, tags, etc.)
269 /// * `rows` - Maximum number of results to return (default: 10, max typically: 1000)
270 /// * `start` - Starting offset for pagination (0-based)
271 /// * `fq` - Additional filter queries in Solr format
272 ///
273 /// # Returns
274 ///
275 /// Returns search results with datasets, facets, and pagination information.
276 ///
277 /// # Examples
278 ///
279 /// ```rust
280 /// # use data_gov_ckan::{CkanClient, Configuration};
281 /// # use std::sync::Arc;
282 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
283 /// # let client = CkanClient::new(Arc::new(Configuration {
284 /// # base_path: "https://catalog.data.gov/api/3".to_string(),
285 /// # user_agent: Some("test".to_string()),
286 /// # client: reqwest::Client::new(),
287 /// # basic_auth: None, oauth_access_token: None, bearer_access_token: None, api_key: None,
288 /// # }));
289 ///
290 /// // Basic text search
291 /// let results = client.package_search(
292 /// Some("climate change"),
293 /// Some(20),
294 /// Some(0),
295 /// None
296 /// ).await?;
297 ///
298 /// println!("Found {} total datasets", results.count.unwrap_or(0));
299 /// for package in results.results.unwrap_or_default() {
300 /// println!("Title: {}", package.title.unwrap_or_default());
301 /// println!("Organization: {}", package.organization.as_ref()
302 /// .and_then(|org| org.title.as_deref())
303 /// .unwrap_or("Unknown"));
304 /// }
305 ///
306 /// // Search with organization filter
307 /// let epa_datasets = client.package_search(
308 /// Some("water quality"),
309 /// Some(10),
310 /// Some(0),
311 /// Some("organization:epa-gov")
312 /// ).await?;
313 ///
314 /// // Search with multiple filters
315 /// let recent_climate_data = client.package_search(
316 /// Some("climate"),
317 /// Some(15),
318 /// Some(0),
319 /// Some("res_format:CSV AND metadata_modified:[2020-01-01T00:00:00Z TO NOW]")
320 /// ).await?;
321 /// # Ok(())
322 /// # }
323 /// ```
324 ///
325 /// # Pagination
326 ///
327 /// ```rust,ignore
328 /// // Paginate through all results
329 /// let mut start = 0;
330 /// let page_size = 50;
331 ///
332 /// loop {
333 /// let results = client.package_search(
334 /// Some("energy"),
335 /// Some(page_size),
336 /// Some(start),
337 /// None
338 /// ).await?;
339 ///
340 /// let packages = results.results.unwrap_or_default();
341 /// if packages.is_empty() {
342 /// break;
343 /// }
344 ///
345 /// // Process this page of results
346 /// for package in packages {
347 /// println!("Processing: {}", package.name);
348 /// }
349 ///
350 /// start += page_size;
351 /// }
352 /// ```
353 ///
354 /// # Advanced Filtering
355 ///
356 /// The `fq` parameter supports Solr query syntax for advanced filtering. The
357 /// `q` and `fq` parameters are passed through to CKAN's Solr-backed
358 /// package_search endpoint, so you can use familiar Solr constructs such as:
359 ///
360 /// - `organization:epa-gov` - Filter by organization
361 /// - `res_format:CSV` - Filter by resource format
362 /// - `tags:healthcare` - Filter by tags
363 /// - `metadata_modified:[2020-01-01T00:00:00Z TO NOW]` - Date ranges
364 /// - Combine with `AND`, `OR`, `NOT` operators
365 ///
366 /// Examples:
367 ///
368 /// - Text search with wildcard: `q=climat*`
369 /// - Phrase search: `q="air quality"`
370 /// - Complex filter: `fq=organization:epa-gov AND res_format:CSV`
371 pub async fn package_search(
372 &self,
373 q: Option<&str>,
374 rows: Option<i32>,
375 start: Option<i32>,
376 fq: Option<&str>,
377 ) -> Result<models::PackageSearchResult, CkanError> {
378 let rows_str = rows.map(|r| r.to_string());
379 let start_str = start.map(|s| s.to_string());
380
381 let mut params: Vec<(&str, &str)> = Vec::new();
382 if let Some(q) = q {
383 params.push(("q", q));
384 }
385 if let Some(ref r) = rows_str {
386 params.push(("rows", r));
387 }
388 if let Some(ref s) = start_str {
389 params.push(("start", s));
390 }
391 if let Some(fq) = fq {
392 params.push(("fq", fq));
393 }
394
395 self.call_action("package_search", ¶ms).await
396 }
397
398 /// Retrieve a specific dataset by its ID or name
399 ///
400 /// Fetches complete metadata for a single dataset, including all resources,
401 /// tags, organization details, and custom metadata fields.
402 ///
403 /// # Arguments
404 ///
405 /// * `id` - Dataset ID (UUID) or name (URL-friendly slug)
406 ///
407 /// # Returns
408 ///
409 /// Returns the complete dataset record with all metadata and resources.
410 ///
411 /// # Examples
412 ///
413 /// ```rust
414 /// # use data_gov_ckan::{CkanClient, Configuration};
415 /// # use std::sync::Arc;
416 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
417 /// # let client = CkanClient::new(Arc::new(Configuration {
418 /// # base_path: "https://catalog.data.gov/api/3".to_string(),
419 /// # user_agent: Some("test".to_string()),
420 /// # client: reqwest::Client::new(),
421 /// # basic_auth: None, oauth_access_token: None, bearer_access_token: None, api_key: None,
422 /// # }));
423 ///
424 /// // Get dataset by name
425 /// let dataset = client.package_show("consumer-complaint-database").await?;
426 ///
427 /// println!("Dataset: {}", dataset.title.unwrap_or_default());
428 /// println!("Description: {}", dataset.notes.unwrap_or_default());
429 ///
430 /// // List all resources in the dataset
431 /// println!("Resources:");
432 /// for resource in dataset.resources.unwrap_or_default() {
433 /// println!(" - {} ({})",
434 /// resource.name.as_deref().unwrap_or("Unnamed"),
435 /// resource.format.as_deref().unwrap_or("Unknown format")
436 /// );
437 ///
438 /// if let Some(url) = resource.url {
439 /// println!(" URL: {}", url);
440 /// }
441 ///
442 /// if let Some(size) = resource.size {
443 /// println!(" Size: {} bytes", size);
444 /// }
445 /// }
446 ///
447 /// // Show dataset tags
448 /// if let Some(tags) = dataset.tags {
449 /// let tag_names: Vec<String> = tags.into_iter()
450 /// .filter_map(|tag| tag.display_name)
451 /// .collect();
452 /// println!("Tags: {}", tag_names.join(", "));
453 /// }
454 ///
455 /// // Show organization
456 /// if let Some(org) = dataset.organization {
457 /// println!("Organization: {}", org.title.unwrap_or_default());
458 /// }
459 ///
460 /// // Get dataset by UUID
461 /// let dataset_by_id = client.package_show("a1b2c3d4-e5f6-7890-abcd-ef1234567890").await?;
462 /// # Ok(())
463 /// # }
464 /// ```
465 ///
466 /// # Error Handling
467 ///
468 /// ```rust,ignore
469 /// match client.package_show("nonexistent-dataset").await {
470 /// Ok(dataset) => {
471 /// println!("Found dataset: {}", dataset.title.unwrap_or_default());
472 /// },
473 /// Err(CkanError::ApiError { status: 404, .. }) => {
474 /// println!("Dataset not found");
475 /// },
476 /// Err(e) => {
477 /// println!("Other error: {}", e);
478 /// }
479 /// }
480 /// ```
481 ///
482 /// # Dataset Metadata
483 ///
484 /// The returned dataset includes rich metadata:
485 ///
486 /// - **Basic Info**: Title, description, notes, license
487 /// - **Resources**: Files, APIs, documentation associated with dataset
488 /// - **Organization**: Publishing agency/department information
489 /// - **Tags**: Subject tags and keywords for discovery
490 /// - **Temporal**: Creation date, modification date, temporal coverage
491 /// - **Spatial**: Geographic coverage and bounding boxes
492 /// - **Custom Fields**: Agency-specific metadata extensions
493 pub async fn package_show(&self, id: &str) -> Result<models::Package, CkanError> {
494 self.call_action("package_show", &[("id", id)]).await
495 }
496
497 /// List all organizations in the CKAN instance
498 pub async fn organization_list(
499 &self,
500 sort: Option<&str>,
501 limit: Option<i32>,
502 offset: Option<i32>,
503 ) -> Result<Vec<String>, CkanError> {
504 let limit_str = limit.map(|l| l.to_string());
505 let offset_str = offset.map(|o| o.to_string());
506
507 let mut params: Vec<(&str, &str)> = Vec::new();
508 if let Some(sort) = sort {
509 params.push(("sort", sort));
510 }
511 if let Some(ref l) = limit_str {
512 params.push(("limit", l));
513 }
514 if let Some(ref o) = offset_str {
515 params.push(("offset", o));
516 }
517
518 self.call_action("organization_list", ¶ms).await
519 }
520
521 /// List all groups in the CKAN instance
522 pub async fn group_list(
523 &self,
524 sort: Option<&str>,
525 limit: Option<i32>,
526 offset: Option<i32>,
527 ) -> Result<Vec<String>, CkanError> {
528 let limit_str = limit.map(|l| l.to_string());
529 let offset_str = offset.map(|o| o.to_string());
530
531 let mut params: Vec<(&str, &str)> = Vec::new();
532 if let Some(sort) = sort {
533 params.push(("sort", sort));
534 }
535 if let Some(ref l) = limit_str {
536 params.push(("limit", l));
537 }
538 if let Some(ref o) = offset_str {
539 params.push(("offset", o));
540 }
541
542 self.call_action("group_list", ¶ms).await
543 }
544
545 /// Get dataset autocomplete suggestions for type-ahead functionality
546 ///
547 /// Provides quick dataset name/title suggestions as the user types, perfect for
548 /// implementing search boxes with autocomplete dropdowns.
549 ///
550 /// # Arguments
551 ///
552 /// * `incomplete` - Partial dataset name or title to search for (e.g., "climat")
553 /// * `limit` - Maximum number of suggestions to return (default: 10, reasonable max: 20)
554 ///
555 /// # Returns
556 ///
557 /// Returns suggestions containing dataset names and titles that match the input.
558 ///
559 /// # Examples
560 ///
561 /// ```rust
562 /// # use data_gov_ckan::{CkanClient, Configuration};
563 /// # use std::sync::Arc;
564 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
565 /// # let client = CkanClient::new(Arc::new(Configuration {
566 /// # base_path: "https://catalog.data.gov/api/3".to_string(),
567 /// # user_agent: Some("test".to_string()),
568 /// # client: reqwest::Client::new(),
569 /// # basic_auth: None, oauth_access_token: None, bearer_access_token: None, api_key: None,
570 /// # }));
571 ///
572 /// // Get suggestions as user types "elect"
573 /// let suggestions = client.dataset_autocomplete(Some("elect"), Some(5)).await?;
574 ///
575 /// for suggestion in &suggestions {
576 /// println!("Dataset: {} - {}",
577 /// suggestion.name.as_deref().unwrap_or("Unknown"),
578 /// suggestion.title.as_deref().unwrap_or("No title"));
579 /// }
580 /// # Ok(())
581 /// # }
582 /// ```
583 ///
584 /// # UI Integration
585 ///
586 /// This is designed for real-time search suggestions:
587 ///
588 /// ```rust,ignore
589 /// // In your web frontend or CLI app
590 /// async fn on_search_input_change(input: &str, client: &CkanClient) -> Result<(), Box<dyn std::error::Error>> {
591 /// if input.len() >= 2 { // Start suggesting after 2+ characters
592 /// let suggestions = client.dataset_autocomplete(Some(input), Some(10)).await?;
593 /// // Display suggestions in dropdown/list
594 /// for suggestion in &suggestions {
595 /// println!("Suggestion: {}", suggestion.title.as_deref().unwrap_or("Unknown"));
596 /// }
597 /// }
598 /// Ok(())
599 /// }
600 /// ```
601 pub async fn dataset_autocomplete(
602 &self,
603 incomplete: Option<&str>,
604 limit: Option<i32>,
605 ) -> Result<Vec<models::DatasetAutocomplete>, CkanError> {
606 let limit_str = limit.map(|l| l.to_string());
607
608 let mut params: Vec<(&str, &str)> = Vec::new();
609 if let Some(q) = incomplete {
610 params.push(("q", q));
611 }
612 if let Some(ref l) = limit_str {
613 params.push(("limit", l));
614 }
615
616 self.call_action("package_autocomplete", ¶ms).await
617 }
618
619 /// Get tag autocomplete suggestions
620 pub async fn tag_autocomplete(
621 &self,
622 incomplete: Option<&str>,
623 limit: Option<i32>,
624 vocabulary_id: Option<&str>,
625 ) -> Result<Vec<String>, CkanError> {
626 let limit_str = limit.map(|l| l.to_string());
627
628 let mut params: Vec<(&str, &str)> = Vec::new();
629 if let Some(q) = incomplete {
630 params.push(("q", q));
631 }
632 if let Some(ref l) = limit_str {
633 params.push(("limit", l));
634 }
635 if let Some(vid) = vocabulary_id {
636 params.push(("vocabulary_id", vid));
637 }
638
639 self.call_action("tag_autocomplete", ¶ms).await
640 }
641
642 /// Get user autocomplete suggestions
643 pub async fn user_autocomplete(
644 &self,
645 q: Option<&str>,
646 limit: Option<i32>,
647 ignore_self: Option<bool>,
648 ) -> Result<Vec<models::UserAutocomplete>, CkanError> {
649 let limit_str = limit.map(|l| l.to_string());
650 let ignore_self_str = ignore_self.map(|b| b.to_string());
651
652 let mut params: Vec<(&str, &str)> = Vec::new();
653 if let Some(q) = q {
654 params.push(("q", q));
655 }
656 if let Some(ref l) = limit_str {
657 params.push(("limit", l));
658 }
659 if let Some(ref i) = ignore_self_str {
660 params.push(("ignore_self", i));
661 }
662
663 self.call_action("user_autocomplete", ¶ms).await
664 }
665
666 /// Get group autocomplete suggestions for filtering and organization
667 ///
668 /// Groups in CKAN represent thematic collections of datasets. This endpoint
669 /// provides autocomplete functionality for group names and titles.
670 ///
671 /// # Arguments
672 ///
673 /// * `q` - Partial group name or title to search for (e.g., "agri")
674 /// * `limit` - Maximum number of suggestions to return (default: 10)
675 ///
676 /// # Returns
677 ///
678 /// Returns group suggestions with names, display names, and metadata.
679 ///
680 /// # Examples
681 ///
682 /// ```rust
683 /// # use data_gov_ckan::{CkanClient, Configuration};
684 /// # use std::sync::Arc;
685 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
686 /// # let client = CkanClient::new(Arc::new(Configuration {
687 /// # base_path: "https://catalog.data.gov/api/3".to_string(),
688 /// # user_agent: Some("test".to_string()),
689 /// # client: reqwest::Client::new(),
690 /// # basic_auth: None, oauth_access_token: None, bearer_access_token: None, api_key: None,
691 /// # }));
692 ///
693 /// // Find agriculture-related groups
694 /// let groups = client.group_autocomplete(Some("agri"), Some(5)).await?;
695 ///
696 /// println!("Found {} groups", groups.len());
697 /// for group in groups {
698 /// println!("Group: {} ({})",
699 /// group.title.as_deref().unwrap_or("Unknown"),
700 /// group.name.as_deref().unwrap_or("Unknown"));
701 /// }
702 /// # Ok(())
703 /// # }
704 /// ```
705 ///
706 /// # Common Use Cases
707 ///
708 /// ```rust,ignore
709 /// // Building category filters for search UI
710 /// let science_groups = client.group_autocomplete(Some("science"), Some(10)).await?;
711 ///
712 /// // Finding groups for dataset categorization
713 /// let energy_groups = client.group_autocomplete(Some("energy"), Some(5)).await?;
714 /// ```
715 pub async fn group_autocomplete(
716 &self,
717 q: Option<&str>,
718 limit: Option<i32>,
719 ) -> Result<Vec<models::GroupAutocomplete>, CkanError> {
720 let limit_str = limit.map(|l| l.to_string());
721
722 let mut params: Vec<(&str, &str)> = Vec::new();
723 if let Some(q) = q {
724 params.push(("q", q));
725 }
726 if let Some(ref l) = limit_str {
727 params.push(("limit", l));
728 }
729
730 self.call_action("group_autocomplete", ¶ms).await
731 }
732
733 /// Get organization autocomplete suggestions
734 pub async fn organization_autocomplete(
735 &self,
736 q: Option<&str>,
737 limit: Option<i32>,
738 ) -> Result<Vec<models::OrganizationAutocomplete>, CkanError> {
739 let limit_str = limit.map(|l| l.to_string());
740
741 let mut params: Vec<(&str, &str)> = Vec::new();
742 if let Some(q) = q {
743 params.push(("q", q));
744 }
745 if let Some(ref l) = limit_str {
746 params.push(("limit", l));
747 }
748
749 self.call_action("organization_autocomplete", ¶ms).await
750 }
751
752 /// Get resource format autocomplete suggestions
753 pub async fn resource_format_autocomplete(
754 &self,
755 incomplete: Option<&str>,
756 limit: Option<i32>,
757 ) -> Result<Vec<String>, CkanError> {
758 let limit_str = limit.map(|l| l.to_string());
759
760 let mut params: Vec<(&str, &str)> = Vec::new();
761 if let Some(q) = incomplete {
762 params.push(("q", q));
763 }
764 if let Some(ref l) = limit_str {
765 params.push(("limit", l));
766 }
767
768 self.call_action("format_autocomplete", ¶ms).await
769 }
770}