asana_sdk/
lib.rs

1//! A Rust Asana SDK, for the Asana Rest API
2//!
3//! This crate uses a model! macro to define flexible Model Structs with a very lean syntax.
4//! These generated structs are used to deserialize entities, select include fields and embed relationships from the Asana API.
5//!
6//! The Asana API returns flexible objects with varying field & relation includes, so this crate uses models provided by the user.
7//! This makes the crate also compatible with entities added to the API by Asana in the future.
8//!
9//! To make the interface as ergonomic as possible, it relies on two components:
10//!
11//! * A `model!()` macro to easily define deserialization Structs ([serde](https://docs.rs/serde/)), together with endpoint urls and field/relation inclusion querystrings.
12//! * Turbofish operators (`get::<Type>()`) to make API calls for defined models.
13//!
14//! ## Sample usage
15//!
16//! ```
17//! use reqwest::{Error};
18//! use asana_sdk::*;
19//! use asana_sdk::models::Model;
20//!
21//! #[tokio::main]
22//! async fn main() -> Result<(), Error> {
23//!
24//!     // Connect with your Asana PAT (token), from https://app.asana.com/0/developer-console
25//!     let mut asana = Asana::connect(String::from("1/your:personal-access-token"));
26//!
27//!     // A Model Struct linked to the "users" endpoint
28//!     model!(User "users" {
29//!         email: String,
30//!         name: String,
31//!     });
32//!
33//!     // Simple calls to get one or multiple users
34//!     let mut user:  User      = asana.get::<User>("me").await;
35//!     let mut users: Vec<User> = asana.list::<User>().await;
36//!
37//!     Ok(())
38//! }
39//! ```
40//!
41//! ### A few more advanced examples:
42//!
43//! Compound call to list all sections *within* a specific project
44//! ```
45//! model!(Section "sections" { name: String });
46//! model!(Project "projects" { name: String });
47//!
48//! let mut sections = asana
49//!     .from::<Project>("12345678")
50//!     .list::<Section>().await;
51//! ```
52//!
53//! A Struct for Tasks including Projects.
54//! TaskWithProjects is just an example name, you can give the Struct any name you want.
55//!
56//! The call will list all tasks from a specific section,
57//! and include all other projects the task is part of.
58//! ```
59//! model!(TaskWithProjects "tasks" {
60//!     name: String,
61//!     projects: Vec<Project>
62//! } Project);
63//!
64//! let mut tasks_with_projects = asana
65//!      .from::<Section>("12345678")
66//!      .list::<TaskWithProjects>().await;
67//! ```
68//!
69//! Note that all model Structs by default include gid & resource_type,
70//! So it's not mandatory to include other fields.
71//!
72//! Fields which might be null in the API should be deserialized into an Option<Type>
73//! ```
74//! model!(Assignee "assignee" {});
75//! model!(TaskWithAssignee "tasks" {
76//!     name: String,
77//!     assignee: Option<Assignee>
78//! } Assignee);
79//! ```
80
81use reqwest::{Method, Response};
82use std::vec::Vec;
83use log::*;
84
85pub mod models;
86use crate::models::*;
87
88pub struct Asana;
89const API_VERSION: &str = "1.0";
90
91pub struct Client {
92    client: reqwest::Client,
93    token: String,
94    endpoint: String,
95}
96
97impl Asana {
98    pub fn connect(token: String) -> Client {
99        Client {
100            token,
101            endpoint: String::from(""),
102            client: reqwest::Client::builder()
103                .user_agent("asana_sdk.rs/0.1.2")
104                .build().unwrap(),
105        }
106
107    }
108}
109
110impl Client {
111    pub async fn get<T: Model>(&mut self, gid: &str) -> T {
112        let model: Wrapper<T> = self
113            .call::<T>(Method::GET, Some(gid)).await
114            .json().await.unwrap();
115
116        model.data
117    }
118
119    pub async fn list<T: Model>(&mut self) -> Vec<T> {
120        let model: ListWrapper<T> =  self
121            .call::<T>(Method::GET, None).await
122            .json().await.unwrap();
123
124        self.endpoint.clear();
125
126        model.data
127    }
128
129    pub fn from<T: Model>(&mut self, relational_gid: &str) -> &mut Client {
130        self.endpoint = format!("{}/{}/", T::endpoint(), relational_gid);
131        self
132    }
133
134    async fn call<T: Model>(&mut self, method: Method, gid: Option<&str>) -> Response {
135        // Add both relational and main endpoints, and entity gid if supplied
136        let url = format!("{}{}/", self.endpoint, T::endpoint());
137        let url = format!("{}{}", url, match gid {
138            Some(gid) => format!("{}", gid),
139            None => "".to_string()
140        });
141
142        // Clear relational endpoint state from client
143        self.endpoint.clear();
144
145        // Add relational & root field inclusions as query parameters
146        let opts = format!("this.({}),{}", T::field_names().join("|"), T::opt_strings().join(","));
147        let url = format!("{}?opt_fields={}", url, opts);
148
149        let request_url = format!("https://app.asana.com/api/{}/{}", API_VERSION, url);
150        info!("{}", request_url);
151
152        self.client.request(method, &request_url)
153            .header("Authorization", format!("Bearer {}", &self.token))
154            .send().await.unwrap()
155    }
156}