1use crate::ids::{BlockId, DatabaseId};
2use crate::models::error::ErrorResponse;
3use crate::models::search::{DatabaseQuery, SearchRequest};
4use crate::models::{Database, ListResponse, Object, Page};
5use ids::{AsIdentifier, PageId};
6use models::block::Block;
7use models::PageCreateRequest;
8use reqwest::header::{HeaderMap, HeaderValue};
9use reqwest::{header, Client, ClientBuilder, RequestBuilder};
10use tracing::Instrument;
11
12pub mod ids;
13pub mod models;
14
15pub use chrono;
16
17const NOTION_API_VERSION: &str = "2022-02-22";
18
19#[derive(Debug, thiserror::Error)]
21pub enum Error {
22 #[error("Invalid Notion API Token: {}", source)]
23 InvalidApiToken { source: header::InvalidHeaderValue },
24
25 #[error("Unable to build reqwest HTTP client: {}", source)]
26 ErrorBuildingClient { source: reqwest::Error },
27
28 #[error("Error sending HTTP request: {}", source)]
29 RequestFailed {
30 #[from]
31 source: reqwest::Error,
32 },
33
34 #[error("Error reading response: {}", source)]
35 ResponseIoError { source: reqwest::Error },
36
37 #[error("Error parsing json response: {}", source)]
38 JsonParseError { source: serde_json::Error },
39
40 #[error("Unexpected API Response")]
41 UnexpectedResponse { response: Object },
42
43 #[error("API Error {}({}): {}", .error.code, .error.status, .error.message)]
44 ApiError { error: ErrorResponse },
45}
46
47#[derive(Clone)]
50pub struct NotionApi {
51 client: Client,
52}
53
54impl NotionApi {
55 pub fn new(api_token: String) -> Result<Self, Error> {
58 let mut headers = HeaderMap::new();
59 headers.insert(
60 "Notion-Version",
61 HeaderValue::from_static(NOTION_API_VERSION),
62 );
63
64 let mut auth_value = HeaderValue::from_str(&format!("Bearer {}", api_token))
65 .map_err(|source| Error::InvalidApiToken { source })?;
66 auth_value.set_sensitive(true);
67 headers.insert(header::AUTHORIZATION, auth_value);
68
69 let client = ClientBuilder::new()
70 .default_headers(headers)
71 .build()
72 .map_err(|source| Error::ErrorBuildingClient { source })?;
73
74 Ok(Self { client })
75 }
76
77 async fn make_json_request(
78 &self,
79 request: RequestBuilder,
80 ) -> Result<Object, Error> {
81 let request = request.build()?;
82 let url = request.url();
83 tracing::trace!(
84 method = request.method().as_str(),
85 url = url.as_str(),
86 "Sending request"
87 );
88 let json = self
89 .client
90 .execute(request)
91 .instrument(tracing::trace_span!("Sending request"))
92 .await
93 .map_err(|source| Error::RequestFailed { source })?
94 .text()
95 .instrument(tracing::trace_span!("Reading response"))
96 .await
97 .map_err(|source| Error::ResponseIoError { source })?;
98
99 tracing::debug!("JSON Response: {}", json);
100 #[cfg(test)]
101 {
102 dbg!(serde_json::from_str::<serde_json::Value>(&json)
103 .map_err(|source| Error::JsonParseError { source })?);
104 }
105 let result =
106 serde_json::from_str(&json).map_err(|source| Error::JsonParseError { source })?;
107
108 match result {
109 Object::Error { error } => Err(Error::ApiError { error }),
110 response => Ok(response),
111 }
112 }
113
114 pub async fn list_databases(&self) -> Result<ListResponse<Database>, Error> {
118 let builder = self.client.get("https://api.notion.com/v1/databases");
119
120 match self.make_json_request(builder).await? {
121 Object::List { list } => Ok(list.expect_databases()?),
122 response => Err(Error::UnexpectedResponse { response }),
123 }
124 }
125
126 pub async fn search<T: Into<SearchRequest>>(
130 &self,
131 query: T,
132 ) -> Result<ListResponse<Object>, Error> {
133 let result = self
134 .make_json_request(
135 self.client
136 .post("https://api.notion.com/v1/search")
137 .json(&query.into()),
138 )
139 .await?;
140
141 match result {
142 Object::List { list } => Ok(list),
143 response => Err(Error::UnexpectedResponse { response }),
144 }
145 }
146
147 pub async fn get_database<T: AsIdentifier<DatabaseId>>(
149 &self,
150 database_id: T,
151 ) -> Result<Database, Error> {
152 let result = self
153 .make_json_request(self.client.get(format!(
154 "https://api.notion.com/v1/databases/{}",
155 database_id.as_id()
156 )))
157 .await?;
158
159 match result {
160 Object::Database { database } => Ok(database),
161 response => Err(Error::UnexpectedResponse { response }),
162 }
163 }
164
165 pub async fn get_page<T: AsIdentifier<PageId>>(
167 &self,
168 page_id: T,
169 ) -> Result<Page, Error> {
170 let result = self
171 .make_json_request(self.client.get(format!(
172 "https://api.notion.com/v1/pages/{}",
173 page_id.as_id()
174 )))
175 .await?;
176
177 match result {
178 Object::Page { page } => Ok(page),
179 response => Err(Error::UnexpectedResponse { response }),
180 }
181 }
182
183 pub async fn create_page<T: Into<PageCreateRequest>>(
185 &self,
186 page: T,
187 ) -> Result<Page, Error> {
188 let result = self
189 .make_json_request(
190 self.client
191 .post("https://api.notion.com/v1/pages")
192 .json(&page.into()),
193 )
194 .await?;
195
196 match result {
197 Object::Page { page } => Ok(page),
198 response => Err(Error::UnexpectedResponse { response }),
199 }
200 }
201
202 pub async fn query_database<D, T>(
204 &self,
205 database: D,
206 query: T,
207 ) -> Result<ListResponse<Page>, Error>
208 where
209 T: Into<DatabaseQuery>,
210 D: AsIdentifier<DatabaseId>,
211 {
212 let result = self
213 .make_json_request(
214 self.client
215 .post(&format!(
216 "https://api.notion.com/v1/databases/{database_id}/query",
217 database_id = database.as_id()
218 ))
219 .json(&query.into()),
220 )
221 .await?;
222 match result {
223 Object::List { list } => Ok(list.expect_pages()?),
224 response => Err(Error::UnexpectedResponse { response }),
225 }
226 }
227
228 pub async fn get_block_children<T: AsIdentifier<BlockId>>(
229 &self,
230 block_id: T,
231 ) -> Result<ListResponse<Block>, Error> {
232 let result = self
233 .make_json_request(self.client.get(&format!(
234 "https://api.notion.com/v1/blocks/{block_id}/children",
235 block_id = block_id.as_id()
236 )))
237 .await?;
238
239 match result {
240 Object::List { list } => Ok(list.expect_blocks()?),
241 response => Err(Error::UnexpectedResponse { response }),
242 }
243 }
244}