datadog_agent_remote_config/json.rs
1//! Decoding support for products whose payloads are JSON.
2
3use std::fmt;
4
5use serde::de::DeserializeOwned;
6
7use crate::ApplyError;
8
9/// Deserializes a JSON payload, for a [`decode`](crate::ProductDecoder::decode) implementation to call.
10///
11/// Payloads are opaque bytes and need not be JSON, so decoding is left to each product. This helper serves the
12/// products whose payloads are. It takes no configuration ID: the client attributes a `decode` error to the
13/// configuration being decoded.
14///
15/// # Errors
16///
17/// Returns [`JsonError`] if `payload` is not valid JSON or does not match the shape of `T`.
18///
19/// # Examples
20///
21/// ```
22/// use datadog_agent_remote_config::{decode_json, ConfigId, JsonError, ProductDecoder};
23/// use serde::Deserialize;
24///
25/// #[derive(Deserialize)]
26/// struct Limits {
27/// max_spans: u32,
28/// }
29///
30/// #[derive(Default)]
31/// struct LimitsDecoder {
32/// limits: Option<Limits>,
33/// }
34///
35/// impl ProductDecoder for LimitsDecoder {
36/// const PRODUCT: &'static str = "EXAMPLE_LIMITS";
37///
38/// type Snapshot = Option<Limits>;
39/// type Error = JsonError;
40///
41/// fn decode(&mut self, _id: &ConfigId, payload: &[u8]) -> Result<(), Self::Error> {
42/// self.limits = Some(decode_json(payload)?);
43/// Ok(())
44/// }
45///
46/// fn build(self) -> Result<Self::Snapshot, Self::Error> {
47/// Ok(self.limits)
48/// }
49/// }
50///
51/// let mut decoder = LimitsDecoder::default();
52/// decoder.decode(&ConfigId::new("limits.v1"), br#"{"max_spans": 100}"#).unwrap();
53/// assert!(decoder.decode(&ConfigId::new("limits.v2"), b"{").is_err());
54/// assert_eq!(decoder.build().unwrap().unwrap().max_spans, 100);
55/// ```
56pub fn decode_json<T>(payload: &[u8]) -> Result<T, JsonError>
57where
58 T: DeserializeOwned,
59{
60 serde_json::from_slice(payload).map_err(JsonError)
61}
62
63/// A payload that [`decode_json`] could not deserialize.
64///
65/// The message gives the position of the failure and nothing of the payload's contents, so it is safe to report to the
66/// Agent. The underlying [`serde_json::Error`], which may quote payload values, is available as the error's
67/// [`source`](std::error::Error::source) for local diagnostics.
68#[derive(Debug)]
69pub struct JsonError(serde_json::Error);
70
71impl fmt::Display for JsonError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(
74 f,
75 "Payload is malformed JSON at line {}, column {}.",
76 self.0.line(),
77 self.0.column()
78 )
79 }
80}
81
82impl std::error::Error for JsonError {
83 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
84 Some(&self.0)
85 }
86}
87
88impl ApplyError for JsonError {
89 fn apply_error(&self) -> String {
90 self.to_string()
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use std::error::Error as _;
97
98 use serde::Deserialize;
99
100 use crate::{decode_json, ApplyError};
101
102 #[derive(Debug, Deserialize, PartialEq)]
103 struct Limits {
104 max_spans: u32,
105 }
106
107 #[test]
108 fn decodes_a_json_payload() {
109 let limits: Limits = decode_json(br#"{"max_spans": 100}"#).unwrap();
110
111 assert_eq!(limits, Limits { max_spans: 100 });
112 }
113
114 #[test]
115 fn reports_malformed_json_without_its_contents() {
116 for (payload, expected) in [
117 (br#"{"max_spans": 1"#.as_slice(), "line 1, column 15"),
118 (br#"{"max_spans": "hunter2"}"#.as_slice(), "line 1, column 23"),
119 ] {
120 let error = decode_json::<Limits>(payload).unwrap_err();
121
122 assert_eq!(error.to_string(), format!("Payload is malformed JSON at {expected}."));
123 assert_eq!(error.apply_error(), error.to_string());
124 assert!(!error.to_string().contains("hunter2"));
125 let source = error.source().expect("the serde_json error is the source");
126 assert!(source.is::<serde_json::Error>());
127 }
128 }
129}