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}