1#[cfg(feature = "ffi")]
2uniffi::setup_scaffolding!();
3
4pub mod bookmarks;
5pub mod tags;
6pub mod users;
7
8pub use bookmarks::{
9 Bookmark, CheckUrlResponse, CreateBookmarkBody, ListBookmarksArgs, ListBookmarksResponse,
10 UpdateBookmarkBody,
11};
12pub use tags::{ListTagsArgs, ListTagsResponse, TagData};
13use thiserror::Error;
14pub use users::{DateDisplay, LinkTarget, SelectedTheme, SortBy, TagSearchMethod, UserProfile};
15
16#[derive(Error, Debug)]
17#[cfg_attr(feature = "ffi", derive(uniffi::Error))]
18#[cfg_attr(feature = "ffi", uniffi(flat_error))]
19pub enum LinkDingError {
20 #[error("Invalid URL")]
21 InvalidUrl(#[from] http::uri::InvalidUri),
22 #[error("Error building URL")]
23 ParseUrlError(#[from] http::Error),
24 #[error("Error sending HTTP request")]
25 SendHttpError(#[from] ureq::Error),
26 #[error("Could not parse JSON response from API")]
27 JsonParse(#[from] std::io::Error),
28 #[error("Could not serialize JSON body")]
29 JsonSerialize(#[from] serde_json::Error),
30}
31
32#[derive(Debug, Clone)]
33#[cfg_attr(feature = "ffi", derive(uniffi::Enum))]
34pub enum Endpoint {
35 ListBookmarks(ListBookmarksArgs),
36 ListArchivedBookmarks(ListBookmarksArgs),
37 GetBookmark(i32),
38 CheckUrl(String),
39 CreateBookmark,
40 UpdateBookmark(i32),
41 ArchiveBookmark(i32),
42 UnarchiveBookmark(i32),
43 DeleteBookmark(i32),
44 ListTags(ListTagsArgs),
45 GetTag(i32),
46 CreateTag,
47 GetUserProfile,
48}
49
50impl TryInto<http::Uri> for Endpoint {
51 type Error = LinkDingError;
52
53 fn try_into(self) -> Result<http::Uri, Self::Error> {
54 match self.clone() {
55 Self::ListBookmarks(args) | Self::ListArchivedBookmarks(args) => {
56 let query_string = args.query_string();
57 let path: String = self.into();
58 http::Uri::try_from(format!("{}{}", path, query_string))
59 .map_err(LinkDingError::InvalidUrl)
60 }
61 Self::ListTags(args) => {
62 let query_string = args.query_string();
63 let path: String = self.into();
64 http::Uri::try_from(format!("{}{}", path, query_string))
65 .map_err(LinkDingError::InvalidUrl)
66 }
67 Self::CheckUrl(url) => {
68 let path: String = self.into();
69 http::Uri::try_from(format!("{}?url={}", path, url))
70 .map_err(LinkDingError::InvalidUrl)
71 }
72 Self::GetBookmark(_)
73 | Self::UpdateBookmark(_)
74 | Self::ArchiveBookmark(_)
75 | Self::UnarchiveBookmark(_)
76 | Self::DeleteBookmark(_)
77 | Self::CreateBookmark
78 | Self::GetTag(_)
79 | Self::CreateTag
80 | Self::GetUserProfile => {
81 let path: String = self.into();
82 http::Uri::try_from(path).map_err(LinkDingError::InvalidUrl)
83 }
84 }
85 }
86}
87
88impl QueryString for Endpoint {
89 fn query_string(&self) -> String {
90 match self {
91 Self::ListBookmarks(args) | Self::ListArchivedBookmarks(args) => args.query_string(),
92 Self::ListTags(args) => args.query_string(),
93 Self::GetBookmark(_)
94 | Self::CheckUrl(_)
95 | Self::CreateBookmark
96 | Self::UpdateBookmark(_)
97 | Self::ArchiveBookmark(_)
98 | Self::UnarchiveBookmark(_)
99 | Self::DeleteBookmark(_)
100 | Self::GetTag(_)
101 | Self::CreateTag
102 | Self::GetUserProfile => "".to_string(),
103 }
104 }
105}
106
107impl Into<String> for Endpoint {
108 fn into(self) -> String {
109 match &self {
110 Self::ListBookmarks(_) => "/api/bookmarks/".to_string(),
111 Self::ListArchivedBookmarks(_) => "/api/bookmarks/archived/".to_string(),
112 Self::GetBookmark(id) | Self::UpdateBookmark(id) | Self::DeleteBookmark(id) => {
113 format!("/api/bookmarks/{}/", &id)
114 }
115 Self::CheckUrl(_) => "/api/bookmarks/check/".to_string(),
116 Self::CreateBookmark => "/api/bookmarks/".to_string(),
117 Self::ArchiveBookmark(id) => format!("/api/bookmarks/{}/archive/", &id),
118 Self::UnarchiveBookmark(id) => format!("/api/bookmarks/{}/unarchive/", &id),
119 Self::ListTags(_) | Self::CreateTag => "/api/tags/".to_string(),
120 Self::GetTag(id) => format!("/api/tags/{}/", &id),
121 Self::GetUserProfile => "/api/user/profile/".to_string(),
122 }
123 }
124}
125
126impl Into<http::Method> for Endpoint {
127 fn into(self) -> http::Method {
128 match self {
129 Self::ListBookmarks(_) => http::Method::GET,
130 Self::ListArchivedBookmarks(_) => http::Method::GET,
131 Self::GetBookmark(_) => http::Method::GET,
132 Self::CheckUrl(_) => http::Method::GET,
133 Self::CreateBookmark => http::Method::POST,
134 Self::UpdateBookmark(_) => http::Method::PATCH,
135 Self::ArchiveBookmark(_) => http::Method::POST,
136 Self::UnarchiveBookmark(_) => http::Method::POST,
137 Self::DeleteBookmark(_) => http::Method::DELETE,
138 Self::ListTags(_) => http::Method::GET,
139 Self::GetTag(_) => http::Method::GET,
140 Self::CreateTag => http::Method::POST,
141 Self::GetUserProfile => http::Method::GET,
142 }
143 }
144}
145
146trait QueryString {
147 fn query_string(&self) -> String;
148}
149
150#[derive(Debug, Clone)]
174#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
175pub struct LinkDingClient {
176 token: String,
177 url: String,
178}
179
180impl LinkDingClient {
181 fn prepare_request(&self, endpoint: Endpoint) -> Result<http::request::Builder, LinkDingError> {
182 let uri: http::Uri = self.url.parse()?;
183 let path_and_query_uri: http::Uri = endpoint.clone().try_into()?;
184 let uri = http::uri::Builder::from(uri)
185 .path_and_query(path_and_query_uri.to_string())
186 .build()
187 .map_err(LinkDingError::ParseUrlError)?;
188
189 Ok(http::request::Builder::new()
190 .method(endpoint)
191 .uri(uri)
192 .header("Content-Type", "application/json")
193 .header("Accept", "application/json")
194 .header("Authorization", &format!("Token {}", &self.token)))
195 }
196}
197
198#[cfg_attr(feature = "ffi", uniffi::export)]
199impl LinkDingClient {
200 #[cfg_attr(feature = "ffi", uniffi::constructor)]
201 pub fn new(url: &str, token: &str) -> Self {
202 LinkDingClient {
203 token: token.to_string(),
204 url: url.to_string(),
205 }
206 }
207
208 pub fn list_bookmarks(
210 &self,
211 args: ListBookmarksArgs,
212 ) -> Result<ListBookmarksResponse, LinkDingError> {
213 let endpoint = Endpoint::ListBookmarks(args);
214 let request = self.prepare_request(endpoint)?.body(())?;
215 let body: ListBookmarksResponse = ureq::run(request)?.body_mut().read_json()?;
216 Ok(body)
217 }
218
219 pub fn list_archived_bookmarks(
221 &self,
222 args: ListBookmarksArgs,
223 ) -> Result<ListBookmarksResponse, LinkDingError> {
224 let endpoint = Endpoint::ListArchivedBookmarks(args);
225 let request = self.prepare_request(endpoint)?.body(())?;
226 let body: ListBookmarksResponse = ureq::run(request)?.body_mut().read_json()?;
227 Ok(body)
228 }
229
230 pub fn get_bookmark(&self, id: i32) -> Result<Bookmark, LinkDingError> {
232 let endpoint = Endpoint::GetBookmark(id);
233 let request = self.prepare_request(endpoint)?.body(())?;
234 let body: Bookmark = ureq::run(request)?.body_mut().read_json()?;
235 Ok(body)
236 }
237
238 pub fn check_url(&self, url: &str) -> Result<CheckUrlResponse, LinkDingError> {
244 let endpoint = Endpoint::CheckUrl(url.to_string());
245 let request = self.prepare_request(endpoint)?.body(())?;
246 let body: CheckUrlResponse = ureq::run(request)?.body_mut().read_json()?;
247 Ok(body)
248 }
249
250 pub fn create_bookmark(&self, body: CreateBookmarkBody) -> Result<Bookmark, LinkDingError> {
254 let endpoint = Endpoint::CreateBookmark;
255 let request = self
256 .prepare_request(endpoint)?
257 .body(serde_json::to_string(&body)?)?;
258 let body: Bookmark = ureq::run(request)?.body_mut().read_json()?;
259 Ok(body)
260 }
261
262 pub fn update_bookmark(
266 &self,
267 id: i32,
268 body: UpdateBookmarkBody,
269 ) -> Result<Bookmark, LinkDingError> {
270 let endpoint = Endpoint::UpdateBookmark(id);
271 let request = self
272 .prepare_request(endpoint)?
273 .body(serde_json::to_string(&body)?)?;
274 let body: Bookmark = ureq::run(request)?.body_mut().read_json()?;
275 Ok(body)
276 }
277
278 pub fn archive_bookmark(&self, id: i32) -> Result<bool, LinkDingError> {
280 let endpoint = Endpoint::ArchiveBookmark(id);
281 let request = self.prepare_request(endpoint)?.body(())?;
282 Ok(ureq::run(request)?.status() == http::StatusCode::NO_CONTENT)
283 }
284
285 pub fn unarchive_bookmark(&self, id: i32) -> Result<bool, LinkDingError> {
287 let endpoint = Endpoint::UnarchiveBookmark(id);
288 let request = self.prepare_request(endpoint)?.body(())?;
289 Ok(ureq::run(request)?.status() == http::StatusCode::NO_CONTENT)
290 }
291
292 pub fn delete_bookmark(&self, id: i32) -> Result<bool, LinkDingError> {
294 let endpoint = Endpoint::DeleteBookmark(id);
295 let request = self.prepare_request(endpoint)?.body(())?;
296 Ok(ureq::run(request)?.status() == http::StatusCode::NO_CONTENT)
297 }
298
299 pub fn list_tags(&self, args: ListTagsArgs) -> Result<ListTagsResponse, LinkDingError> {
301 let endpoint = Endpoint::ListTags(args);
302 let request = self.prepare_request(endpoint)?.body(())?;
303 let body: ListTagsResponse = ureq::run(request)?.body_mut().read_json()?;
304 Ok(body)
305 }
306
307 pub fn get_tag(&self, id: i32) -> Result<TagData, LinkDingError> {
309 let endpoint = Endpoint::GetTag(id);
310 let request = self.prepare_request(endpoint)?.body(())?;
311 let body: TagData = ureq::run(request)?.body_mut().read_json()?;
312 Ok(body)
313 }
314
315 pub fn create_tag(&self, name: &str) -> Result<TagData, LinkDingError> {
317 let endpoint = Endpoint::CreateTag;
318 let body = serde_json::json!({ "name": name });
319 let request = self
320 .prepare_request(endpoint)?
321 .body(serde_json::to_string(&body)?)?;
322 let body: TagData = ureq::run(request)?.body_mut().read_json()?;
323 Ok(body)
324 }
325
326 pub fn get_user_profile(&self) -> Result<UserProfile, LinkDingError> {
328 let endpoint = Endpoint::GetUserProfile;
329 let request = self.prepare_request(endpoint)?.body(())?;
330 let body: UserProfile = ureq::run(request)?.body_mut().read_json()?;
331 Ok(body)
332 }
333}