Skip to main content

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}