1use std::fmt;
14
15use serde::de::{self, Deserialize, Deserializer, Visitor};
16use serde_json::{Map, Value};
17
18use crate::error::ApiError;
19
20pub const IGNORED_FIELDS: &str = "ignored_fields";
22
23pub const CLIENT_IDENTITY: &[&str] = &["origin", "integration"];
25
26#[derive(Debug)]
27enum Probe {
28 Fields(&'static [&'static str]),
29 Other,
30}
31
32impl fmt::Display for Probe {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.write_str("field probe")
35 }
36}
37
38impl std::error::Error for Probe {}
39
40impl de::Error for Probe {
41 fn custom<T: fmt::Display>(_: T) -> Self {
42 Probe::Other
43 }
44}
45
46struct FieldProbe;
47
48impl<'de> Deserializer<'de> for FieldProbe {
49 type Error = Probe;
50
51 fn deserialize_any<V: Visitor<'de>>(self, _: V) -> Result<V::Value, Probe> {
52 Err(Probe::Other)
53 }
54
55 fn deserialize_struct<V: Visitor<'de>>(
56 self,
57 _name: &'static str,
58 fields: &'static [&'static str],
59 _: V,
60 ) -> Result<V::Value, Probe> {
61 Err(Probe::Fields(fields))
62 }
63
64 serde::forward_to_deserialize_any! {
65 bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
66 bytes byte_buf option unit unit_struct newtype_struct seq tuple
67 tuple_struct map enum identifier ignored_any
68 }
69}
70
71pub fn names<'de, T: Deserialize<'de>>() -> &'static [&'static str] {
74 match T::deserialize(FieldProbe) {
75 Err(Probe::Fields(f)) => f,
76 _ => &[],
77 }
78}
79
80pub fn unknown(obj: &Map<String, Value>, known: &[&[&str]]) -> Vec<String> {
82 obj.keys()
83 .filter(|k| !known.iter().any(|set| set.contains(&k.as_str())))
84 .cloned()
85 .collect()
86}
87
88pub fn reject_unknown(obj: &Map<String, Value>, known: &[&[&str]]) -> Result<(), ApiError> {
90 let bad = unknown(obj, known);
91 if bad.is_empty() {
92 return Ok(());
93 }
94 Err(ApiError::invalid(format!(
95 "unknown field{} {}",
96 if bad.len() == 1 { "" } else { "s" },
97 bad.iter()
98 .map(|b| format!("`{b}`"))
99 .collect::<Vec<_>>()
100 .join(", ")
101 ))
102 .with_details(serde_json::json!({ "unknownFields": bad })))
103}
104
105pub fn ignored_warning(ignored: &[String]) -> Option<String> {
107 (!ignored.is_empty()).then(|| format!("{IGNORED_FIELDS}: {}", ignored.join(", ")))
108}
109
110pub fn scrape_request() -> [&'static [&'static str]; 3] {
113 [
114 &["url"],
115 names::<crate::types::ScrapeOptions>(),
116 CLIENT_IDENTITY,
117 ]
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::types::{CrawlRequest, MapRequest, ScrapeOptions};
124 use serde_json::json;
125
126 #[test]
127 fn captures_renamed_field_names() {
128 let f = names::<ScrapeOptions>();
129 assert!(f.contains(&"onlyMainContent") && f.contains(&"ifNoneMatch"));
130 assert!(!f.contains(&"only_main_content"));
131 assert!(names::<CrawlRequest>().contains(&"scrapeOptions"));
132 assert!(names::<MapRequest>().contains(&"includeSubdomains"));
133 }
134
135 #[test]
136 fn batch_own_fields_match_the_struct() {
137 use crate::types::BatchScrapeRequest;
138 let b: BatchScrapeRequest = serde_json::from_value(json!({"urls": ["u"]})).unwrap();
139 let v = serde_json::to_value(&b).unwrap();
140 let mut own: Vec<&str> = v
141 .as_object()
142 .unwrap()
143 .keys()
144 .map(String::as_str)
145 .filter(|k| !names::<ScrapeOptions>().contains(k))
146 .collect();
147 own.sort_unstable();
148 let mut want = BatchScrapeRequest::OWN_FIELDS.to_vec();
149 want.sort_unstable();
150 assert_eq!(own, want);
151 }
152
153 #[test]
154 fn unknown_lists_every_stray_key() {
155 let body = json!({"url": "u", "formats": [], "zeroDataRetention": true, "origin": "sdk", "lockdown": 1});
156 let obj = body.as_object().unwrap();
157 assert_eq!(
158 unknown(obj, &scrape_request()),
159 ["lockdown", "zeroDataRetention"]
160 );
161 let err = reject_unknown(obj, &scrape_request()).unwrap_err();
162 assert!(err.message.contains("`lockdown`, `zeroDataRetention`"));
163 assert_eq!(
164 ignored_warning(&["a".into(), "b".into()]).unwrap(),
165 "ignored_fields: a, b"
166 );
167 }
168}