nordnet_api/resources/tradables.rs
1//! Resource methods for the `tradables` API group.
2//!
3//! # Operations
4//!
5//! | Method | Op | Path |
6//! |--------|----|------|
7//! | GET | `get_tradable_info` | `/tradables/info/{tradables}` |
8//! | GET | `list_tradable_trades` | `/tradables/trades/{tradables}` |
9//! | GET | `get_suitability` | `/tradables/validation/suitability/{tradables}` |
10//!
11//!
12//! ## Path encoding — [`TradableKey`]
13//!
14//! Each operation takes a single [`TradableKey`] (e.g. `11:101` for
15//! ERIC B). The Nordnet API also accepts a comma-separated list of keys at
16//! the path slot, but the typed surface stays single-key for now — Phase 4
17//! is expected to add a small helper for the multi-key shape.
18//!
19//!
20//! ## Naming — `list_tradable_trades`
21//!
22//! The Nordnet documentation calls this op `list_trades`. Renamed to
23//! `list_tradable_trades` so it can co-exist on [`Client`] alongside the
24//! same-named `list_trades` ops planned for the `accounts` and
25//! `instruments` groups (Rust resolves all three onto a single `Client`
26//! impl). Phase 3X may pick a uniform naming scheme.
27//!
28//!
29//! ## 204 No Content
30//!
31//! Every op may return HTTP 204 (No Content). The base [`Client::get`]
32//! treats an empty body as a [`Error::Decode`]; each method here maps that
33//! specific case to an empty `Vec`, mirroring the
34//! [`Client::get_country`] precedent.
35//!
36//!
37//! ## 403 No Content (`get_suitability`)
38//!
39//! `GET /tradables/validation/suitability/{tradables}` returns HTTP 403 with
40//! an empty body for anonymous sessions. The base client maps any 403 to
41//! [`Error::Forbidden`] (with the empty body string preserved), so callers
42//! can distinguish this from a parse error.
43
44use crate::client::Client;
45use crate::error::Error;
46use nordnet_model::models::tradables::{
47 TradableEligibility, TradableInfo, TradableKey, TradablePublicTrades,
48};
49
50impl Client {
51 /// `GET /tradables/info/{tradables}` — Returns trading calendar and
52 /// allowed trading types for the given tradable.
53 ///
54 /// Returns an empty `Vec` when the API responds with 204 No Content
55 /// (no matching tradables).
56 ///
57 /// # Errors
58 ///
59 /// Returns [`Error::BadRequest`] (400), [`Error::Unauthorized`] (401),
60 /// [`Error::TooManyRequests`] (429), or [`Error::ServiceUnavailable`]
61 /// (503) as documented.
62 #[doc(alias = "GET /tradables/info/{tradables}")]
63 pub async fn get_tradable_info(&self, key: &TradableKey) -> Result<Vec<TradableInfo>, Error> {
64 let path = format!("/tradables/info/{key}");
65 match self.get::<Vec<TradableInfo>>(&path).await {
66 Ok(v) => Ok(v),
67 // 204 No Content — base client surfaces this as a Decode error
68 // over an empty body. Map it to an empty Vec.
69 Err(Error::Decode { ref body, .. }) if body.trim().is_empty() => Ok(vec![]),
70 Err(e) => Err(e),
71 }
72 }
73
74 /// `GET /tradables/trades/{tradables}` — Returns the public trades
75 /// (all trades executed on the marketplace) for the given tradable.
76 ///
77 /// # Parameters
78 ///
79 /// - `key` — the tradable to look up.
80 /// - `count` — optional. Number of trades to return. The API accepts
81 /// either a positive integer (`"5"`, `"10"`, ...) or the literal
82 /// string `"all"`; the default is `"5"`. Passed through verbatim.
83 ///
84 /// Returns an empty `Vec` when the API responds with 204 No Content.
85 ///
86 /// # Errors
87 ///
88 /// Returns [`Error::BadRequest`] (400), [`Error::Unauthorized`] (401),
89 /// [`Error::TooManyRequests`] (429), or [`Error::ServiceUnavailable`]
90 /// (503) as documented.
91 ///
92 /// # Naming
93 ///
94 /// The Nordnet docs call this op `list_trades`; renamed here to
95 /// `list_tradable_trades` so it can co-exist with the same-named ops
96 /// planned for the `accounts` and `instruments` groups (see module
97 /// doc).
98 #[doc(alias = "GET /tradables/trades/{tradables}")]
99 pub async fn list_tradable_trades(
100 &self,
101 key: &TradableKey,
102 count: Option<&str>,
103 ) -> Result<Vec<TradablePublicTrades>, Error> {
104 let path = match count {
105 Some(c) => format!("/tradables/trades/{key}?count={c}"),
106 None => format!("/tradables/trades/{key}"),
107 };
108 match self.get::<Vec<TradablePublicTrades>>(&path).await {
109 Ok(v) => Ok(v),
110 Err(Error::Decode { ref body, .. }) if body.trim().is_empty() => Ok(vec![]),
111 Err(e) => Err(e),
112 }
113 }
114
115 /// `GET /tradables/validation/suitability/{tradables}` — Returns the
116 /// customer's trading eligibility for the given tradable.
117 ///
118 /// Returns an empty `Vec` when the API responds with 204 No Content.
119 ///
120 /// # Errors
121 ///
122 /// Returns [`Error::BadRequest`] (400), [`Error::Unauthorized`] (401),
123 /// [`Error::Forbidden`] (403; documented for anonymous sessions and
124 /// returned with an empty body), [`Error::TooManyRequests`] (429), or
125 /// [`Error::ServiceUnavailable`] (503) as documented.
126 #[doc(alias = "GET /tradables/validation/suitability/{tradables}")]
127 pub async fn get_suitability(
128 &self,
129 key: &TradableKey,
130 ) -> Result<Vec<TradableEligibility>, Error> {
131 let path = format!("/tradables/validation/suitability/{key}");
132 match self.get::<Vec<TradableEligibility>>(&path).await {
133 Ok(v) => Ok(v),
134 Err(Error::Decode { ref body, .. }) if body.trim().is_empty() => Ok(vec![]),
135 Err(e) => Err(e),
136 }
137 }
138}