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}