1use std::ops::Deref;
5
6use chrono::DateTime;
7use chrono::Utc;
8
9use http::Method;
10use http_endpoint::Bytes;
11
12use serde::Deserialize;
13use serde::Serialize;
14
15use serde_json::from_slice as from_json;
16use serde_json::to_vec as to_json;
17
18use uuid::Uuid;
19
20use crate::api::v2::account;
21use crate::api::v2::asset;
22use crate::Str;
23
24
25#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
27pub struct Id(pub Uuid);
28
29impl Deref for Id {
30 type Target = Uuid;
31
32 #[inline]
33 fn deref(&self) -> &Self::Target {
34 &self.0
35 }
36}
37
38
39#[derive(Debug, Deserialize, Eq, PartialEq)]
41pub struct Watchlist {
42 #[serde(rename = "id")]
44 pub id: Id,
45 #[serde(rename = "name")]
47 pub name: String,
48 #[serde(rename = "account_id")]
50 pub account_id: account::Id,
51 #[serde(rename = "created_at")]
53 pub created_at: DateTime<Utc>,
54 #[serde(rename = "updated_at")]
56 pub updated_at: DateTime<Utc>,
57 #[serde(rename = "assets")]
59 pub assets: Vec<asset::Asset>,
60 #[doc(hidden)]
62 #[serde(skip)]
63 pub _non_exhaustive: (),
64}
65
66
67#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
69pub struct CreateReq {
70 #[serde(rename = "name")]
72 pub name: String,
73 #[serde(rename = "symbols")]
75 pub symbols: Vec<String>,
76 #[doc(hidden)]
78 pub _non_exhaustive: (),
79}
80
81
82#[derive(Clone, Debug, Default, Eq, PartialEq)]
84pub struct CreateReqInit {
85 pub symbols: Vec<String>,
87 #[doc(hidden)]
89 pub _non_exhaustive: (),
90}
91
92impl CreateReqInit {
93 #[inline]
95 pub fn init<S>(self, name: S) -> CreateReq
96 where
97 S: Into<String>,
98 {
99 let Self {
100 symbols,
101 _non_exhaustive: (),
102 } = self;
103
104 CreateReq {
105 name: name.into(),
106 symbols,
107 _non_exhaustive: (),
108 }
109 }
110}
111
112
113pub type UpdateReq = CreateReq;
115pub type UpdateReqInit = CreateReqInit;
117
118
119Endpoint! {
120 pub Create(CreateReq),
122 Ok => Watchlist, [
123 OK,
125 ],
126 Err => CreateError, [
127 UNPROCESSABLE_ENTITY => InvalidInput,
130 ]
131
132 #[inline]
133 fn path(_input: &Self::Input) -> Str {
134 "/v2/watchlists".into()
135 }
136
137 #[inline]
138 fn method() -> Method {
139 Method::POST
140 }
141
142 fn body(input: &Self::Input) -> Result<Option<Bytes>, Self::ConversionError> {
143 let json = to_json(input)?;
144 let bytes = Bytes::from(json);
145 Ok(Some(bytes))
146 }
147}
148
149
150Endpoint! {
151 pub Get(Id),
154 Ok => Watchlist, [
155 OK,
157 ],
158 Err => GetError, [
159 NOT_FOUND => NotFound,
161 ]
162
163 fn path(input: &Self::Input) -> Str {
164 format!("/v2/watchlists/{}", input.as_simple()).into()
165 }
166}
167
168
169Endpoint! {
170 pub Update((Id, UpdateReq)),
173 Ok => Watchlist, [
174 OK,
176 ],
177 Err => UpdateError, [
178 NOT_FOUND => NotFound,
180 UNPROCESSABLE_ENTITY => InvalidInput,
183 ]
184
185 fn path(input: &Self::Input) -> Str {
186 let (id, _) = input;
187 format!("/v2/watchlists/{}", id.as_simple()).into()
188 }
189
190 #[inline]
191 fn method() -> Method {
192 Method::PUT
193 }
194
195 fn body(input: &Self::Input) -> Result<Option<Bytes>, Self::ConversionError> {
196 let (_, request) = input;
197 let json = to_json(request)?;
198 let bytes = Bytes::from(json);
199 Ok(Some(bytes))
200 }
201}
202
203
204EndpointNoParse! {
205 pub Delete(Id),
208 Ok => (), [
209 NO_CONTENT,
211 ],
212 Err => DeleteError, [
213 NOT_FOUND => NotFound,
215 ]
216
217 #[inline]
218 fn method() -> Method {
219 Method::DELETE
220 }
221
222 fn path(input: &Self::Input) -> Str {
223 format!("/v2/watchlists/{}", input.as_simple()).into()
224 }
225
226 #[inline]
227 fn parse(body: &[u8]) -> Result<Self::Output, Self::ConversionError> {
228 debug_assert_eq!(body, b"");
229 Ok(())
230 }
231
232 fn parse_err(body: &[u8]) -> Result<Self::ApiError, Vec<u8>> {
233 from_json::<Self::ApiError>(body).map_err(|_| body.to_vec())
234 }
235}
236
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 use crate::api::v2::account;
243 use crate::api_info::ApiInfo;
244 use crate::Client;
245 use crate::RequestError;
246
247 use test_log::test;
248
249
250 #[test(tokio::test)]
252 async fn create_get_delete() {
253 let api_info = ApiInfo::from_env().unwrap();
254 let client = Client::new(api_info);
255 let expected_symbols = vec!["AAPL".to_string(), "AMZN".to_string()];
256 let id = Uuid::new_v4().to_string();
257 let request = CreateReqInit {
258 symbols: expected_symbols.clone(),
259 ..Default::default()
260 }
261 .init(id.clone());
262
263 let created = client.issue::<Create>(&request).await.unwrap();
264 let result = client.issue::<Get>(&created.id).await;
265 client.issue::<Delete>(&created.id).await.unwrap();
266
267 let watchlist = result.unwrap();
268 let tracked_symbols = watchlist
269 .assets
270 .into_iter()
271 .map(|a| a.symbol)
272 .collect::<Vec<_>>();
273 assert_eq!(tracked_symbols, expected_symbols);
274
275 let account = client.issue::<account::Get>(&()).await.unwrap();
277 assert_eq!(watchlist.name, id);
278 assert_eq!(watchlist.account_id, account.id);
279 }
280
281 #[test(tokio::test)]
284 async fn create_duplicate_name() {
285 let api_info = ApiInfo::from_env().unwrap();
286 let client = Client::new(api_info);
287
288 let name = "the-name";
289 let request = CreateReqInit {
290 symbols: vec!["SPY".to_string()],
291 ..Default::default()
292 }
293 .init(name);
294
295 let created = client.issue::<Create>(&request).await.unwrap();
296 let result = client.issue::<Create>(&request).await;
297
298 client.issue::<Delete>(&created.id).await.unwrap();
299
300 let err = result.unwrap_err();
301 match err {
302 RequestError::Endpoint(CreateError::InvalidInput(_)) => (),
303 _ => panic!("Received unexpected error: {err:?}"),
304 };
305 }
306
307 #[test(tokio::test)]
310 async fn get_non_existent() {
311 let api_info = ApiInfo::from_env().unwrap();
312 let client = Client::new(api_info);
313
314 let request = CreateReqInit {
315 symbols: vec!["AAPL".to_string()],
316 ..Default::default()
317 }
318 .init(Uuid::new_v4().to_string());
319
320 let created = client.issue::<Create>(&request).await.unwrap();
321 let () = client.issue::<Delete>(&created.id).await.unwrap();
322
323 let err = client.issue::<Get>(&created.id).await.unwrap_err();
324 match err {
325 RequestError::Endpoint(GetError::NotFound(_)) => (),
326 _ => panic!("Received unexpected error: {err:?}"),
327 };
328 }
329
330 #[test(tokio::test)]
332 async fn update() {
333 let api_info = ApiInfo::from_env().unwrap();
334 let client = Client::new(api_info);
335 let symbols = vec!["AAPL".to_string()];
336 let id = Uuid::new_v4().to_string();
337 let request = CreateReqInit {
338 symbols: symbols.clone(),
339 ..Default::default()
340 }
341 .init(&id);
342
343 let created = client.issue::<Create>(&request).await.unwrap();
344
345 let id2 = Uuid::new_v4().to_string();
346 let symbols = vec!["AMZN".to_string(), "SPY".to_string()];
347
348 let req = UpdateReqInit {
349 symbols: symbols.clone(),
350 ..Default::default()
351 }
352 .init(&id2);
353
354 let result = client.issue::<Update>(&(created.id, req)).await;
355 let () = client.issue::<Delete>(&created.id).await.unwrap();
356
357 let watchlist = result.unwrap();
358 assert_eq!(watchlist.name, id2.to_string());
359 let symbols = watchlist
360 .assets
361 .iter()
362 .map(|asset| &asset.symbol)
363 .collect::<Vec<_>>();
364 assert_eq!(symbols, vec!["AMZN", "SPY"]);
365 }
366
367 #[test(tokio::test)]
370 async fn delete_non_existent() {
371 let api_info = ApiInfo::from_env().unwrap();
372 let client = Client::new(api_info);
373
374 let id = Id(Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap());
375 let err = client.issue::<Delete>(&id).await.unwrap_err();
376 match err {
377 RequestError::Endpoint(DeleteError::NotFound(_)) => (),
378 _ => panic!("Received unexpected error: {err:?}"),
379 };
380 }
381}