vestaboard/
rw.rs

1//! # Vestaboard Read/Write api (requires the `rw` feature)
2//!
3//! this module contains the implementation for the Vestaboard Read/Write api. the
4//! `rw` flag must be enabled to use this module. the Read/Write api is used to send
5//! messages to a single Vestaboard. the read/write api must be enabled for the Vestaboard.
6//!
7//! ## config
8//! ```
9//! RWConfig {
10//!  read_write_key: String,
11//! }
12//! ```
13//!
14//! ## methods
15//! ```
16//! async fn read(&self) -> Result<RWApiReadMessage, RWApiError>
17//! async fn write(&self, message: BoardData<ROWS, COLS>) -> Result<String, RWApiError> // returns the message id
18//! ```
19//!
20//! ## types
21//! - [`RWConfig`] is the config type for the read/write api
22//! - [`RWApiReadMessage`] is the response type for the read method
23//! - [`RWApiWriteResponse`] is the response type for the write method
24//! - [`RWApiError`] is the error enum for the read/write api
25//!
26//! ## example
27//! ```
28//! let config = RWConfig {
29//!  read_write_key: "<YOUR_RW_API_KEY>",
30//! };
31//!
32//! // note that a type must be included because of <https://github.com/rust-lang/rust/issues/98931>
33//! let api: Vestaboard<RWConfig> = Vestaboard::new_rw_api(config);
34//! ```
35//!
36//! <https://docs.vestaboard.com/docs/read-write-api/introduction>
37
38use serde::Deserialize;
39use thiserror::Error;
40
41use crate::{BoardData, Vestaboard};
42
43const RW_API_URI: &str = "https://rw.vestaboard.com/";
44const RW_API_HEADER: &str = "X-Vestaboard-Read-Write-Key";
45
46/// configuration object for the Vestaboard Read/Write API \
47/// <https://docs.vestaboard.com/docs/read-write-api/introduction>
48#[derive(Debug, Clone)]
49pub struct RWConfig {
50  /// the read/write key for your Vestaboard \
51  /// <https://docs.vestaboard.com/docs/read-write-api/authentication>
52  pub read_write_key: String,
53}
54
55impl<const ROWS: usize, const COLS: usize> Vestaboard<RWConfig, ROWS, COLS> {
56  /// create a new [`Vestaboard`] instance for a read/write api enabled Vestaboard. \
57  /// requires the read/write api enabled on your vestaboard and an api key
58  ///
59  /// # args
60  /// ```
61  /// RWConfig {
62  ///   read_write_key: "<YOUR_RW_API_KEY>",
63  /// }
64  /// ```
65  ///
66  /// # returns
67  /// a new [`Vestaboard`] instance
68  ///
69  ///
70  /// <https://docs.vestaboard.com/docs/read-write-api/introduction>
71  pub fn new_rw_api(config: RWConfig) -> Self {
72    use std::str::FromStr;
73
74    let headers = reqwest::header::HeaderMap::from_iter([
75      (
76        reqwest::header::CONTENT_TYPE,
77        reqwest::header::HeaderValue::from_static("application/json"),
78      ),
79      (
80        reqwest::header::HeaderName::from_str(RW_API_HEADER).unwrap(),
81        reqwest::header::HeaderValue::from_str(&config.read_write_key).expect("failed to parse read/write key"),
82      ),
83    ]);
84
85    Vestaboard {
86      client: reqwest::Client::builder()
87        .default_headers(headers)
88        .user_agent(format!("vestaboard-rs/{}", env!("CARGO_PKG_VERSION")))
89        .build()
90        .expect("failed to build reqwest client"),
91      config,
92    }
93  }
94
95  /// read the current message on the Vestaboard
96  ///
97  /// # returns
98  /// the current message on the Vestaboard as a
99  ///
100  /// # errors
101  /// - [`ReqwestError`](RWApiError::Reqwest) if there is an error with the reqwest client
102  /// - [`DeserializeError`](RWApiError::Deserialize) if there is an error deserializing the response
103  /// - [`ParseBoardData`](RWApiError::ParseBoardData) if there is an error parsing the message layout into a [`BoardData`]
104  /// - [`ApiError`](RWApiError::ApiError) if there is an error with the r/w api
105  pub async fn read(&self) -> Result<RWApiReadMessage<ROWS, COLS>, RWApiError> {
106    use std::str::FromStr;
107
108    let res = self.client.get(RW_API_URI).send().await?;
109
110    if !res.status().is_success() {
111      return Err(RWApiError::ApiError(res.text().await?));
112    }
113
114    let res = res.json::<RWApiReadResponse>().await?;
115    let board = BoardData::from_str(&res.current_message.layout)?;
116
117    Ok(RWApiReadMessage {
118      layout: res.current_message.layout,
119      id: res.current_message.id,
120      board,
121    })
122  }
123
124  /// write a message to the Vestaboard
125  ///
126  /// # args
127  /// - `message`: the [`BoardData<ROWS, COLS>`] message to write to the Vestaboard
128  ///
129  /// # errors
130  /// - [`ReqwestError`](RWApiError::Reqwest) if there is an error with the reqwest client
131  /// - [`ApiError`](RWApiError::ApiError) if there is an error with the r/w api
132  pub async fn write(&self, message: BoardData<ROWS, COLS>) -> Result<RWApiWriteResponse, RWApiError> {
133    let res = self.client.post(RW_API_URI).json(&message).send().await?;
134
135    if !res.status().is_success() {
136      return Err(RWApiError::ApiError(res.text().await?));
137    }
138
139    Ok(res.json::<RWApiWriteResponse>().await?)
140  }
141}
142
143/// the current message on the Vestaboard
144pub struct RWApiReadMessage<const ROWS: usize, const COLS: usize> {
145  /// a string representation of a [`crate::board::Board<ROWS, COLS>`]
146  pub layout: String,
147  /// the id of the message that is on the Vestaboard
148  pub id: String,
149  /// the message on the Vestaboard
150  pub board: BoardData<ROWS, COLS>,
151}
152
153/// the current message of the Vestaboard
154#[derive(Debug, Clone, Deserialize)]
155struct RWApiRawMessage {
156  /// a string representation of a [`Board<ROWS, COLS>`]
157  pub layout: String,
158  /// the id of the message that is on the Vestaboard
159  pub id: String,
160}
161
162/// the response from the read endpoint of the Vestaboard Read/Write API
163#[derive(Debug, Clone, Deserialize)]
164#[serde(rename_all = "camelCase")]
165struct RWApiReadResponse {
166  /// the current message on the Vestaboard
167  pub current_message: RWApiRawMessage,
168}
169
170#[derive(Debug, Clone, Deserialize)]
171/// the response from the write endpoint of the Vestaboard Read/Write API
172pub struct RWApiWriteResponse {
173  /// the status of the message that was written to the Vestaboard, usually `ok`
174  pub status: String,
175  /// the id of the message that was written to the Vestaboard
176  pub id: String,
177  /// the unix timestamp in milliseconds that the message was written to the Vestaboard
178  pub created: usize,
179}
180
181/// errors that can occur when using the Vestaboard Read/Write API
182/// - [`RWApiError::Reqwest`] if there is an error with the reqwest client
183/// - [`RWApiError::Deserialize`] if there is an error deserializing the response
184#[derive(Error, Debug)]
185pub enum RWApiError {
186  /// reqwest error, see wrapped [`reqwest::Error`] for more details
187  #[error("reqwest error: {0}")]
188  Reqwest(#[from] reqwest::Error),
189  /// failed to deserialize, see wrapped serde_json::Error for more details
190  #[error("failed to parse response: {0}")]
191  Deserialize(#[from] serde_json::Error),
192  /// failed to parse the message layout into a [`BoardData`]
193  #[error("failed to parse message layout: {0}")]
194  ParseBoardData(#[from] crate::board::BoardError),
195  /// api error with wrapped message
196  #[error("api error: {0}")]
197  ApiError(String),
198}