Skip to main content

slack_morphism/api/
blocks.rs

1//!
2//! Support for Slack blocks.validate method
3//!
4
5use rsb_derive::Builder;
6use serde::{Deserialize, Serialize};
7use serde_with::{serde_as, skip_serializing_none};
8
9use crate::errors::{map_serde_error, SlackClientError};
10use crate::models::blocks::*;
11use crate::models::*;
12use crate::SlackClientSession;
13use crate::{ClientResult, SlackClientHttpConnector};
14
15const BLOCKS_VALIDATE_FAILURE_CODES: [&str; 3] =
16    ["invalid_blocks", "invalid_message", "invalid_view"];
17
18impl<'a, SCHC> SlackClientSession<'a, SCHC>
19where
20    SCHC: SlackClientHttpConnector + Send,
21{
22    ///
23    /// https://docs.slack.dev/reference/methods/blocks.validate
24    ///
25    /// A payload that fails validation comes back as `ok: false` with `error`
26    /// set to `invalid_blocks` / `invalid_message` / `invalid_view` and the
27    /// details in `errors`. The generic envelope turns every `ok: false` into
28    /// `SlackClientError::ApiError`, so this method intercepts those three
29    /// codes and re-parses the recorded response body into
30    /// [`SlackApiBlocksValidateResponse`]; the caller inspects `ok` and
31    /// `errors`. Any other error (`invalid_arguments`, auth, ...) is still
32    /// returned as `Err`.
33    ///
34    /// The docs state only that "special rate limits apply" with no figure,
35    /// so no client-side throttle is configured for this method.
36    ///
37    pub async fn blocks_validate(
38        &self,
39        req: &SlackApiBlocksValidateRequest,
40    ) -> ClientResult<SlackApiBlocksValidateResponse> {
41        match self
42            .http_session_api
43            .http_post("blocks.validate", req, None)
44            .await
45        {
46            Err(SlackClientError::ApiError(api_error))
47                if BLOCKS_VALIDATE_FAILURE_CODES.contains(&api_error.code.as_str()) =>
48            {
49                match api_error.http_response_body.as_deref() {
50                    Some(body) => {
51                        serde_json::from_str(body).map_err(|e| map_serde_error(e, Some(body)))
52                    }
53                    None => Err(SlackClientError::ApiError(api_error)),
54                }
55            }
56            other => other,
57        }
58    }
59}
60
61/// Exactly one of `blocks`, `message` or `view` must be set. Slack wants each
62/// as a JSON-encoded string inside the request body, which
63/// `serde_with::json::JsonString` produces.
64#[serde_as]
65#[skip_serializing_none]
66#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
67pub struct SlackApiBlocksValidateRequest {
68    #[serde_as(as = "Option<serde_with::json::JsonString>")]
69    pub blocks: Option<Vec<SlackBlock>>,
70    #[serde_as(as = "Option<serde_with::json::JsonString>")]
71    pub message: Option<SlackMessageContent>,
72    #[serde_as(as = "Option<serde_with::json::JsonString>")]
73    pub view: Option<SlackView>,
74}
75
76#[skip_serializing_none]
77#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
78pub struct SlackApiBlocksValidateResponse {
79    pub ok: bool,
80    /// Set when `ok` is false.
81    pub error: Option<SlackApiBlocksValidateErrorKind>,
82    pub errors: Option<Vec<SlackApiBlocksValidateError>>,
83}
84
85/// Which payload failed validation.
86/// https://docs.slack.dev/reference/methods/blocks.validate#errors
87#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum SlackApiBlocksValidateErrorKind {
90    InvalidBlocks,
91    InvalidMessage,
92    InvalidView,
93    /// A value this crate does not model yet, carried verbatim.
94    #[serde(untagged)]
95    Other(String),
96}
97
98#[skip_serializing_none]
99#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
100pub struct SlackApiBlocksValidateError {
101    /// JSON pointer to the invalid element, e.g. `/blocks/0/child_blocks/0/type`.
102    pub pointer: String,
103    pub code: SlackApiBlocksValidateErrorCode,
104    pub message: String,
105    /// Shape depends on `code`, e.g. `{"type":"enum","expected":[...],"got":"..."}`.
106    pub constraint: Option<serde_json::Value>,
107    pub field: Option<String>,
108}
109
110/// Per-item error code in `SlackApiBlocksValidateResponse.errors`. The docs
111/// list only `failed_constraint`; `missing_field` is observed live.
112/// https://docs.slack.dev/reference/methods/blocks.validate#validation-errors
113#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum SlackApiBlocksValidateErrorCode {
116    FailedConstraint,
117    MissingField,
118    /// A value this crate does not model yet, carried verbatim.
119    #[serde(untagged)]
120    Other(String),
121}
122
123#[cfg(test)]
124mod test {
125    use super::*;
126
127    #[test]
128    fn test_slack_api_blocks_validate_request_encodes_payload_as_json_string() {
129        let req =
130            SlackApiBlocksValidateRequest::new().with_blocks(vec![SlackDividerBlock::new().into()]);
131        let json = serde_json::to_value(&req).unwrap();
132        assert_eq!(
133            json,
134            serde_json::json!({ "blocks": "[{\"type\":\"divider\"}]" })
135        );
136        let decoded: SlackApiBlocksValidateRequest = serde_json::from_value(json).unwrap();
137        assert_eq!(decoded, req);
138    }
139
140    #[test]
141    fn test_slack_api_blocks_validate_error_response_round_trip() {
142        let payload = r#"{"ok":false,"error":"invalid_message","errors":[{"code":"failed_constraint","constraint":{"type":"enum","expected":["divider","image","section"],"got":"markdown"},"message":"unsupported type: markdown","pointer":"/blocks/0/child_blocks/0/type"}]}"#;
143
144        let envelope: crate::SlackEnvelopeMessage = serde_json::from_str(payload).unwrap();
145        assert_eq!(envelope.error.as_deref(), Some("invalid_message"));
146        assert_eq!(
147            envelope.errors.as_ref().unwrap()[0]["code"],
148            "failed_constraint"
149        );
150        assert!(envelope.errors_as_strings().unwrap()[0].contains("\"failed_constraint\""));
151
152        let response: SlackApiBlocksValidateResponse = serde_json::from_str(payload).unwrap();
153        assert!(!response.ok);
154        assert_eq!(
155            response.error,
156            Some(SlackApiBlocksValidateErrorKind::InvalidMessage)
157        );
158        let errors = response.errors.as_ref().unwrap();
159        assert_eq!(errors[0].pointer, "/blocks/0/child_blocks/0/type");
160        assert_eq!(
161            errors[0].code,
162            SlackApiBlocksValidateErrorCode::FailedConstraint
163        );
164        assert_eq!(errors[0].constraint.as_ref().unwrap()["got"], "markdown");
165        assert_eq!(errors[0].field, None);
166        assert_eq!(
167            serde_json::to_value(&response).unwrap(),
168            serde_json::from_str::<serde_json::Value>(payload).unwrap()
169        );
170
171        let ok: SlackApiBlocksValidateResponse = serde_json::from_str(r#"{"ok":true}"#).unwrap();
172        assert!(ok.ok && ok.errors.is_none());
173
174        let other_code: SlackApiBlocksValidateErrorCode =
175            serde_json::from_str(r#""something_new""#).unwrap();
176        assert_eq!(
177            other_code,
178            SlackApiBlocksValidateErrorCode::Other("something_new".into())
179        );
180        assert_eq!(
181            serde_json::to_string(&other_code).unwrap(),
182            r#""something_new""#
183        );
184
185        let other: SlackApiBlocksValidateErrorKind =
186            serde_json::from_str(r#""something_new""#).unwrap();
187        assert_eq!(
188            other,
189            SlackApiBlocksValidateErrorKind::Other("something_new".into())
190        );
191        assert_eq!(serde_json::to_string(&other).unwrap(), r#""something_new""#);
192    }
193}