diff --git a/Cargo.lock b/Cargo.lock index 079c139..abf6163 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,23 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -283,9 +300,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -678,6 +700,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.8" @@ -887,6 +919,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -899,11 +932,13 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-util", "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "windows-registry", ] @@ -1321,6 +1356,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -1457,6 +1498,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.77" diff --git a/Cargo.toml b/Cargo.toml index 2392076..1acce02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] bon = "3.6.3" dotenvy = "0.15.7" -reqwest = { version = "0.12.15", features = ["json"] } +reqwest = { version = "0.12.15", features = ["json", "multipart", "stream"] } serde = "1.0.219" serde_json = "1.0.140" tokio = { version = "1.44.2", features = ["full"] } diff --git a/README.md b/README.md index 03568a4..daf9544 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Rust library crate to access CKAN Action API endpoints through Rust builders. Based on the CKAN Action API v3. Endpoints are expected to return with an output of type `serde_json::Value`. -## Example +## Examples + +Run `/status_show` endpoint for a CKAN instance and print the output: ```rust use dotenvy::dotenv; @@ -26,6 +28,47 @@ async fn main() -> Result<(), Box> { } ``` +> The following examples won't include the boilerplate code. + +List packages: + +```rust +let result = ckan.package_list().call().await?; +println!("{result:#?}"); +``` + +Create a new package (dataset) with custom fields: + +```rust +let custom_fields = serde_json::json!({ + "data_contact_email": "support@dathere.com", + "update_frequency": "daily", + "related_resources": [], +}); +let result = ckan.package_create() + .name("my-new-package".to_string()) + .custom_fields(custom_fields) + .private(false) + .call() + .await?; +println!("{result:#?}"); +``` + +Create a new resource with a new file from a file path: + +```rust + let path_buf = current_dir()?.join("data.csv"); + let result = ckan + .resource_create() + .package_id("3mz0qhbb-cdb0-ewst-x7c0-casnkwv0edub".to_string()) + .name("My new resource".to_string()) + .format("CSV".to_string()) + .upload(path_buf) + .call() + .await?; + println!("{result:#?}"); +``` + ## Notes - Add the `CKAN_API_TOKEN` environment variable to a `.env` file where the program runs to include the token when making requests to the CKAN API. diff --git a/src/lib.rs b/src/lib.rs index efc68c1..4ae5b25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ use bon::bon; use serde_json::json; -use std::collections::HashMap; +use std::{collections::HashMap, path::PathBuf}; pub struct CKAN { url: String, @@ -47,14 +47,28 @@ impl CKAN { &self, endpoint: String, body: Option, + upload: Option, ) -> Result> { let client = reqwest::Client::new(); let mut req_builder = client.post(endpoint); if self.token.is_some() { req_builder = req_builder.header("Authorization", self.token.clone().unwrap()); } - let res = req_builder.json(&body).send().await?.json().await?; - Ok(res) + if let Some(file_pathbuf) = upload { + let mut form = reqwest::multipart::Form::new(); + if let Some(body_as_value) = body { + for entry in body_as_value.as_object().unwrap().iter() { + form = form.text(entry.0.to_owned(), entry.1.as_str().unwrap().to_owned()); + } + } + form = form.file("upload", file_pathbuf).await?; + req_builder = req_builder.multipart(form); + let res = req_builder.send().await?.json().await?; + Ok(res) + } else { + let res = req_builder.json(&body).send().await?.json().await?; + Ok(res) + } } /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.get.package_list @@ -1270,4 +1284,555 @@ impl CKAN { .call() .await?) } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.package_create + #[builder] + pub async fn package_create( + &self, + name: String, + title: Option, + private: bool, + author: Option, + author_email: Option, + maintainer: Option, + maintainer_email: Option, + license_id: Option, + notes: Option, + url: Option, + version: Option, + state: Option, + _type: Option, + resources: Option>, + tags: Option>, + extras: Option>, + plugin_data: Option, + relationships_as_object: Option>, + relationships_as_subject: Option>, + groups: Option>, + owner_org: Option, + custom_fields: Option, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/package_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("name", json!(name)); + opsert("title", title, &mut map); + map.insert("private", json!(private)); + opsert("author", author, &mut map); + opsert("author_email", author_email, &mut map); + opsert("maintainer", maintainer, &mut map); + opsert("maintainer_email", maintainer_email, &mut map); + opsert("license_id", license_id, &mut map); + opsert("notes", notes, &mut map); + opsert("url", url, &mut map); + opsert("version", version, &mut map); + opsert("state", state, &mut map); + opsert("_type", _type, &mut map); + opsert("resources", resources, &mut map); + opsert("tags", tags, &mut map); + opsert("extras", extras, &mut map); + opsert("plugin_data", plugin_data, &mut map); + opsert("relationships_as_object", relationships_as_object, &mut map); + opsert( + "relationships_as_subject", + relationships_as_subject, + &mut map, + ); + let mut custom_map: HashMap = HashMap::new(); + opsert("groups", groups, &mut map); + opsert("owner_org", owner_org, &mut map); + if let Some(custom) = custom_fields { + if custom.is_object() { + let custom_temp_map = custom.as_object().unwrap(); + custom_map.extend( + custom_temp_map + .iter() + .map(|item| (item.0.to_owned(), item.1.to_owned())), + ); + } + } + map.extend( + custom_map + .iter() + .map(|item| (item.0.as_str(), item.1.to_owned())), + ); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.resource_create + #[builder] + pub async fn resource_create( + &self, + package_id: String, + url: Option, + description: Option, + format: Option, + hash: Option, + name: Option, + resource_type: Option, + mimetype: Option, + mimetype_inner: Option, + cache_url: Option, + size: Option, + created: Option, + last_modified: Option, + cache_last_updated: Option, + upload: Option, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/resource_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("package_id", json!(package_id)); + opsert("url", url, &mut map); + opsert("description", description, &mut map); + opsert("format", format, &mut map); + opsert("hash", hash, &mut map); + opsert("name", name, &mut map); + opsert("resource_type", resource_type, &mut map); + opsert("mimetype", mimetype, &mut map); + opsert("mimetype_inner", mimetype_inner, &mut map); + opsert("cache_url", cache_url, &mut map); + opsert("size", size, &mut map); + opsert("created", created, &mut map); + opsert("last_modified", last_modified, &mut map); + opsert("cache_last_updated", cache_last_updated, &mut map); + + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .maybe_upload(upload) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.package_delete + #[builder] + pub async fn package_delete( + &self, + id: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/package_delete"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("id", json!(id)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.resource_view_create + #[builder] + pub async fn resource_view_create( + &self, + resource_id: String, + title: String, + description: Option, + view_type: String, + config: Option, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/resource_view_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("resource_id", json!(resource_id)); + map.insert("title", json!(title)); + opsert("description", description, &mut map); + map.insert("view_type", json!(view_type)); + opsert("config", config, &mut map); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.create_default_resource_views + #[builder] + pub async fn create_default_resource_views( + &self, + resource: serde_json::Value, + package: Option, + create_datastore_views: Option, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/create_default_resource_views"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("resource", resource); + opsert("package", package, &mut map); + opsert("create_datastore_views", create_datastore_views, &mut map); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.package_create_default_resource_views + #[builder] + pub async fn package_create_default_resource_views( + &self, + package: serde_json::Value, + create_datastore_views: Option, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/package_create_default_resource_views"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("package", package); + opsert("create_datastore_views", create_datastore_views, &mut map); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.package_relationship_create + #[builder] + pub async fn package_relationship_create( + &self, + subject: String, + object: String, + _type: String, + comment: Option, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/package_relationship_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("subject", json!(subject)); + map.insert("object", json!(object)); + map.insert("type", json!(_type)); + opsert("comment", comment, &mut map); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.member_create + #[builder] + pub async fn member_create( + &self, + id: String, + object: String, + object_type: String, + capacity: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/member_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("id", json!(id)); + map.insert("object", json!(object)); + map.insert("object_type", json!(object_type)); + map.insert("capacity", json!(capacity)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.package_collaborator_create + #[builder] + pub async fn package_collaborator_create( + &self, + id: String, + user_id: String, + capacity: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/package_collaborator_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("id", json!(id)); + map.insert("user_id", json!(user_id)); + map.insert("capacity", json!(capacity)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.group_create + #[builder] + pub async fn group_create( + &self, + name: String, + id: Option, + title: Option, + description: Option, + image_url: Option, + _type: Option, + state: Option, + approval_status: Option, + extras: Option>, + packages: Option>, + groups: Option>, + users: Option>, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/group_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("name", json!(name)); + opsert("id", id, &mut map); + opsert("title", title, &mut map); + opsert("description", description, &mut map); + opsert("image_url", image_url, &mut map); + opsert("_type", _type, &mut map); + opsert("state", state, &mut map); + opsert("approval_status", approval_status, &mut map); + opsert("extras", extras, &mut map); + opsert("packages", packages, &mut map); + opsert("groups", groups, &mut map); + opsert("users", users, &mut map); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.organization_create + #[builder] + pub async fn organization_create( + &self, + name: String, + id: Option, + title: Option, + description: Option, + image_url: Option, + state: Option, + approval_status: Option, + extras: Option>, + packages: Option>, + users: Option>, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/organization_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("name", json!(name)); + opsert("id", id, &mut map); + opsert("title", title, &mut map); + opsert("description", description, &mut map); + opsert("image_url", image_url, &mut map); + opsert("state", state, &mut map); + opsert("approval_status", approval_status, &mut map); + opsert("extras", extras, &mut map); + opsert("packages", packages, &mut map); + opsert("users", users, &mut map); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.user_create + #[builder] + pub async fn user_create( + &self, + name: String, + email: String, + password: String, + id: Option, + fullname: Option, + about: Option, + image_url: Option, + plugin_extras: Option, + with_apitoken: Option, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/user_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("name", json!(name)); + map.insert("email", json!(email)); + map.insert("password", json!(password)); + opsert("id", id, &mut map); + opsert("fullname", fullname, &mut map); + opsert("about", about, &mut map); + opsert("image_url", image_url, &mut map); + opsert("plugin_extras", plugin_extras, &mut map); + opsert("with_apitoken", with_apitoken, &mut map); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.user_invite + #[builder] + pub async fn user_invite( + &self, + email: String, + group_id: String, + role: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/user_invite"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("email", json!(email)); + map.insert("group_id", json!(group_id)); + map.insert("role", json!(role)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.vocabulary_create + #[builder] + pub async fn vocabulary_create( + &self, + name: String, + tags: Vec, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/vocabulary_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("name", json!(name)); + map.insert("tags", json!(tags)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.tag_create + #[builder] + pub async fn tag_create( + &self, + name: String, + vocabulary_id: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/tag_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("name", json!(name)); + map.insert("vocabulary_id", json!(vocabulary_id)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.follow_user + #[builder] + pub async fn follow_user( + &self, + id: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/follow_user"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("id", json!(id)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.follow_dataset + #[builder] + pub async fn follow_dataset( + &self, + id: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/follow_dataset"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("id", json!(id)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.group_member_create + #[builder] + pub async fn group_member_create( + &self, + id: String, + username: String, + role: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/group_member_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("id", json!(id)); + map.insert("username", json!(username)); + map.insert("role", json!(role)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.organization_member_create + #[builder] + pub async fn organization_member_create( + &self, + id: String, + username: String, + role: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/organization_member_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("id", json!(id)); + map.insert("username", json!(username)); + map.insert("role", json!(role)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.follow_group + #[builder] + pub async fn follow_group( + &self, + id: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/follow_group"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("id", json!(id)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } + + /// https://docs.ckan.org/en/2.11/api/index.html#ckan.logic.action.create.api_token_create + #[builder] + pub async fn api_token_create( + &self, + user: String, + name: String, + ) -> Result> { + let endpoint = self.url.clone() + "/api/3/action/api_token_create"; + let mut map: HashMap<&str, serde_json::Value> = HashMap::new(); + map.insert("user", json!(user)); + map.insert("name", json!(name)); + let body = hashmap_to_json(&map)?; + Ok(Self::post(&self) + .endpoint(endpoint) + .body(body) + .call() + .await?) + } }