1use std::{fs, io, path::Path};
2
3use serde::{Deserialize, de};
4use thiserror::Error;
5
6#[derive(Debug, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub struct MessageSpec {
10 pub name: String,
11 #[serde(rename = "type")]
12 pub message_type: MessageType,
13 #[serde(default)]
14 pub api_key: Option<i16>,
15 pub valid_versions: VersionRange,
16 #[serde(default)]
22 pub latest_version_unstable: bool,
23 #[serde(default)]
24 pub flexible_versions: FlexibleVersions,
25 #[serde(default)]
26 pub fields: Vec<FieldSpec>,
27 #[serde(default)]
28 pub common_structs: Vec<CommonStruct>,
29 #[serde(default)]
34 pub internal: bool,
35}
36
37#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
38#[serde(rename_all = "lowercase")]
39pub enum MessageType {
40 Request,
41 Response,
42 Header,
43 Data,
44}
45
46#[derive(Debug, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct FieldSpec {
49 pub name: String,
50 #[serde(rename = "type")]
51 pub field_type: String,
52 pub versions: VersionRange,
53 #[serde(default)]
54 pub nullable_versions: Option<VersionRange>,
55 #[serde(default)]
56 pub tagged_versions: Option<VersionRange>,
57 #[serde(default)]
58 pub tag: Option<u32>,
59 #[serde(default)]
60 pub fields: Vec<Self>,
61 #[serde(default)]
62 pub default: Option<serde_json::Value>,
63 #[serde(default = "default_entity_type")]
64 pub entity_type: String,
65 #[serde(default)]
66 pub map_key: bool,
67 #[serde(default)]
68 pub about: String,
69 #[serde(default)]
74 pub flexible_versions: Option<FlexibleVersions>,
75}
76
77fn default_entity_type() -> String {
78 String::new()
79}
80
81#[derive(Debug, Deserialize)]
82pub struct CommonStruct {
83 pub name: String,
84 pub versions: VersionRange,
85 pub fields: Vec<FieldSpec>,
86}
87
88impl MessageSpec {
89 #[must_use]
96 pub fn latest_stable_version(&self) -> i16 {
97 if self.latest_version_unstable {
98 self.valid_versions.max.saturating_sub(1)
99 } else {
100 self.valid_versions.max
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
107pub struct VersionRange {
108 pub min: i16,
109 pub max: i16, }
111
112impl<'de> Deserialize<'de> for VersionRange {
113 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
114 let s = String::deserialize(d)?;
115 if s == "none" {
117 return Ok(Self {
118 min: i16::MAX,
119 max: i16::MIN,
120 });
121 }
122 parse_version_range(&s).map_err(de::Error::custom)
123 }
124}
125
126fn parse_version_range(s: &str) -> Result<VersionRange, String> {
127 if let Some(rest) = s.strip_suffix('+') {
128 let min: i16 = rest
129 .parse()
130 .map_err(|e| format!("bad version `{s}`: {e}"))?;
131 return Ok(VersionRange { min, max: i16::MAX });
132 }
133 if let Some((lo, hi)) = s.split_once('-') {
134 let min: i16 = lo.parse().map_err(|e| format!("bad version `{s}`: {e}"))?;
135 let max: i16 = hi.parse().map_err(|e| format!("bad version `{s}`: {e}"))?;
136 return Ok(VersionRange { min, max });
137 }
138 let single: i16 = s.parse().map_err(|e| format!("bad version `{s}`: {e}"))?;
139 Ok(VersionRange {
140 min: single,
141 max: single,
142 })
143}
144
145impl VersionRange {
146 #[must_use]
147 pub fn contains(&self, v: i16) -> bool {
148 v >= self.min && v <= self.max
149 }
150 #[must_use]
151 pub fn is_empty(&self) -> bool {
152 self.min > self.max
153 }
154}
155
156#[derive(Debug, Clone, Copy, Default)]
157pub enum FlexibleVersions {
158 #[default]
159 None,
160 Range(VersionRange),
161}
162
163impl<'de> Deserialize<'de> for FlexibleVersions {
164 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
165 let s = String::deserialize(d)?;
166 if s == "none" {
167 return Ok(Self::None);
168 }
169 Ok(Self::Range(
170 parse_version_range(&s).map_err(de::Error::custom)?,
171 ))
172 }
173}
174
175impl FlexibleVersions {
176 #[must_use]
177 pub fn is_flexible(&self, v: i16) -> bool {
178 match self {
179 Self::None => false,
180 Self::Range(r) => r.contains(v),
181 }
182 }
183}
184
185#[derive(Debug, Error)]
186pub enum IrError {
187 #[error("I/O: {0}")]
188 Io(#[from] io::Error),
189 #[error("parse {file}: {source}")]
190 Parse {
191 file: String,
192 #[source]
193 source: serde_json::Error,
194 },
195}
196
197pub fn load_dir(dir: &Path) -> Result<Vec<MessageSpec>, IrError> {
202 let mut out = Vec::new();
203 for entry in fs::read_dir(dir)? {
204 let entry = entry?;
205 let path = entry.path();
206 if path.extension().and_then(|s| s.to_str()) != Some("json") {
207 continue;
208 }
209 let raw = fs::read_to_string(&path)?;
210 let stripped = strip_line_comments(&raw);
211 let spec: MessageSpec = serde_json::from_str(&stripped).map_err(|e| IrError::Parse {
212 file: path.display().to_string(),
213 source: e,
214 })?;
215 out.push(spec);
216 }
217 out.sort_by(|a, b| a.name.cmp(&b.name));
218 Ok(out)
219}
220
221fn strip_line_comments(src: &str) -> String {
225 let mut out = String::with_capacity(src.len());
226 for line in src.lines() {
227 let bytes = line.as_bytes();
228 let mut in_str = false;
229 let mut escaped = false;
230 let mut end = bytes.len();
231 for (i, &b) in bytes.iter().enumerate() {
232 if in_str {
233 if escaped {
234 escaped = false;
235 } else if b == b'\\' {
236 escaped = true;
237 } else if b == b'"' {
238 in_str = false;
239 }
240 } else if b == b'"' {
241 in_str = true;
242 } else if b == b'/' && bytes.get(i + 1) == Some(&b'/') {
243 end = i;
244 break;
245 }
246 }
247 out.push_str(line.get(..end).unwrap_or(line));
250 out.push('\n');
251 }
252 out
253}
254
255#[cfg(test)]
256mod tests {
257 use assert2::assert;
258
259 use super::*;
260
261 #[test]
262 fn version_range_parsing() {
263 for (input, want) in [
264 (
265 "0+",
266 VersionRange {
267 min: 0,
268 max: i16::MAX,
269 },
270 ),
271 (
272 "3+",
273 VersionRange {
274 min: 3,
275 max: i16::MAX,
276 },
277 ),
278 ("0-2", VersionRange { min: 0, max: 2 }),
279 ("4", VersionRange { min: 4, max: 4 }),
280 ] {
281 assert!(parse_version_range(input).unwrap() == want);
282 }
283 assert!(parse_version_range("none").is_err()); }
285
286 #[test]
287 fn comment_strip() {
288 let src = "{\n// hi\n \"x\": 1 // trailing\n}";
289 let out = strip_line_comments(src);
290 assert!(out == "{\n\n \"x\": 1 \n}\n");
291 }
292
293 #[test]
294 fn comment_strip_preserves_double_slash_in_string() {
295 let src = "{ \"default\": \"http://example.com\" } // tail";
296 let out = strip_line_comments(src);
297 assert!(out.contains("http://example.com"));
298 assert!(!out.contains("tail"));
299 }
300}