1use crate::model::{Field, MetaMap, Type};
2use convert_case::{Case, Casing};
3use eyre::{ContextCompat, Result};
4use serde::de::{Error, Unexpected};
5use serde::ser::SerializeStruct;
6use serde::*;
7use std::fmt::Write;
8
9#[derive(Debug, Serialize, Deserialize, Default, Clone)]
11#[non_exhaustive]
12pub struct EndpointSchema {
13 pub name: String,
15
16 pub code: u32,
18
19 pub parameters: Vec<Field>,
21
22 pub returns: Vec<Field>,
24
25 #[serde(default)]
27 pub stream_response: Option<Type>,
28
29 #[serde(default)]
31 pub description: String,
32
33 #[serde(default)]
35 pub json_schema: serde_json::Value,
36
37 pub roles: Vec<String>,
39
40 #[serde(default)]
42 pub errors: Vec<EndpointErrorSchema>,
43
44 #[serde(default, skip_serializing_if = "MetaMap::is_empty")]
47 pub meta: MetaMap,
48}
49
50impl EndpointSchema {
51 pub fn new(
53 name: impl Into<String>,
54 code: u32,
55 parameters: Vec<Field>,
56 returns: Vec<Field>,
57 ) -> Self {
58 Self {
59 name: name.into(),
60 code,
61 parameters,
62 returns,
63 stream_response: None,
64 description: "".to_string(),
65 json_schema: Default::default(),
66 roles: Vec::new(),
67 errors: Vec::new(),
68 meta: MetaMap::default(),
69 }
70 }
71
72 #[must_use]
74 pub fn with_meta(mut self, meta: MetaMap) -> Self {
75 self.meta = meta;
76 self
77 }
78
79 pub fn with_stream_response_type(mut self, stream_response: Type) -> Self {
81 self.stream_response = Some(stream_response);
82 self
83 }
84
85 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
87 self.description = desc.into();
88 self
89 }
90
91 pub fn with_roles(mut self, roles: Vec<String>) -> Self {
93 self.roles = roles;
94 self
95 }
96
97 pub fn with_errors(mut self, errors: Vec<EndpointErrorSchema>) -> Self {
99 self.errors = errors;
100 self
101 }
102}
103
104#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Eq, Ord)]
105#[non_exhaustive]
106pub struct EndpointErrorSchema {
107 pub name: String,
108 pub code: EndpointErrorCodeRef,
109 #[serde(default)]
110 pub message: String,
111 #[serde(default)]
112 pub fields: Vec<Field>,
113}
114
115impl EndpointErrorSchema {
116 pub fn new(name: impl Into<String>, code: EndpointErrorCodeRef) -> Self {
121 Self {
122 name: name.into(),
123 code,
124 message: String::new(),
125 fields: Vec::new(),
126 }
127 }
128
129 #[must_use]
131 pub fn with_message(mut self, message: impl Into<String>) -> Self {
132 self.message = message.into();
133 self
134 }
135
136 #[must_use]
138 pub fn with_fields(mut self, fields: Vec<Field>) -> Self {
139 self.fields = fields;
140 self
141 }
142}
143
144#[derive(Clone, Debug, Hash, PartialEq, PartialOrd, Eq, Ord)]
145pub struct EndpointErrorCodeRef {
146 pub ty: Type,
147 pub variant: String,
148}
149
150impl EndpointErrorCodeRef {
151 pub const ENUM_NAME: &'static str = "ErrorCode";
152
153 pub fn new(variant: impl Into<String>) -> Self {
154 Self {
155 ty: Type::enum_ref(Self::ENUM_NAME, true),
156 variant: variant.into(),
157 }
158 }
159
160 pub fn variant(&self) -> &str {
161 &self.variant
162 }
163
164 pub fn path(&self) -> String {
165 format!("{}::{}", Self::ENUM_NAME, self.variant)
166 }
167
168 fn validate_ty(ty: Type) -> std::result::Result<Type, String> {
169 match &ty {
170 Type::EnumRef { name, .. } if name == Self::ENUM_NAME => Ok(ty),
171 Type::EnumRef { name, .. } => {
172 Err(format!("expected {} enum ref, got {name}", Self::ENUM_NAME))
173 }
174 _ => Err(format!("expected {} enum ref", Self::ENUM_NAME)),
175 }
176 }
177
178 fn parse_path(path: &str) -> std::result::Result<Self, String> {
179 let (enum_name, variant) = path
180 .split_once("::")
181 .ok_or_else(|| format!("expected {}::Variant", Self::ENUM_NAME))?;
182
183 if enum_name != Self::ENUM_NAME {
184 return Err(format!(
185 "expected {} enum path, got {enum_name}",
186 Self::ENUM_NAME
187 ));
188 }
189
190 if variant.is_empty() || variant.contains("::") {
191 return Err(format!("expected {}::Variant", Self::ENUM_NAME));
192 }
193
194 Ok(Self::new(variant))
195 }
196}
197
198impl std::fmt::Display for EndpointErrorCodeRef {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 f.write_str(&self.path())
201 }
202}
203
204impl Serialize for EndpointErrorCodeRef {
205 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
206 where
207 S: Serializer,
208 {
209 let mut state = serializer.serialize_struct("EndpointErrorCodeRef", 2)?;
210 state.serialize_field("ty", &self.ty)?;
211 state.serialize_field("variant", &self.variant)?;
212 state.end()
213 }
214}
215
216impl<'de> Deserialize<'de> for EndpointErrorCodeRef {
217 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
218 where
219 D: Deserializer<'de>,
220 {
221 #[derive(Deserialize)]
222 #[serde(untagged)]
223 enum Helper {
224 Path(String),
225 Structured { ty: Type, variant: String },
226 }
227
228 match Helper::deserialize(deserializer)? {
229 Helper::Path(path) => Self::parse_path(&path)
230 .map_err(|err| D::Error::invalid_value(Unexpected::Str(&path), &err.as_str())),
231 Helper::Structured { ty, variant } => {
232 let ty = Self::validate_ty(ty).map_err(D::Error::custom)?;
233 Ok(Self { ty, variant })
234 }
235 }
236 }
237}
238
239pub fn encode_header<T: Serialize>(v: T, schema: EndpointSchema) -> Result<String> {
240 let mut s = String::new();
241 write!(s, "0{}", schema.name.to_ascii_lowercase())?;
242 let v = serde_json::to_value(&v)?;
243
244 for (i, f) in schema.parameters.iter().enumerate() {
245 let key = f.name.to_case(Case::Camel);
246 let value = v.get(&key).with_context(|| format!("key: {key}"))?;
247 if value.is_null() {
248 continue;
249 }
250 write!(
251 s,
252 ", {}{}",
253 i + 1,
254 urlencoding::encode(&value.to_string().replace('\"', ""))
255 )?;
256 }
257 Ok(s)
258}
259
260#[cfg(test)]
261mod forward_compat_tests {
262 use super::*;
263 use crate::model::MetaMap;
264
265 #[test]
271 fn unknown_future_fields_are_ignored() {
272 let json = r#"{
273 "name": "UserListSymbols",
274 "code": 10020,
275 "parameters": [],
276 "returns": [],
277 "roles": ["Admin"],
278 "x_future_openapi_binding": {"path": "/rpc/UserListSymbols"},
279 "some_field_from_2_5": 42
280 }"#;
281 let schema: EndpointSchema =
282 serde_json::from_str(json).expect("unknown fields must not break deserialization");
283 assert_eq!(schema.name, "UserListSymbols");
284 assert_eq!(schema.code, 10020);
285 assert!(schema.meta.is_empty());
286 }
287
288 #[test]
290 fn absent_meta_defaults_empty() {
291 let json = r#"{"name":"A","code":1,"parameters":[],"returns":[],"roles":[]}"#;
292 let schema: EndpointSchema = serde_json::from_str(json).unwrap();
293 assert!(schema.meta.is_empty());
294 }
295
296 #[test]
299 fn meta_round_trips_including_unknown_keys() {
300 let mut meta = MetaMap::default();
301 meta.insert("example", serde_json::json!({"symbol": "BTC"}));
302 meta.insert("x-openapi-tags", serde_json::json!(["trading"]));
303 meta.insert("deprecated", serde_json::json!(true));
304
305 let schema = EndpointSchema::new("A", 1, vec![], vec![]).with_meta(meta.clone());
306 let text = serde_json::to_string(&schema).unwrap();
307 let back: EndpointSchema = serde_json::from_str(&text).unwrap();
308
309 assert_eq!(back.meta, meta);
310 assert_eq!(
311 back.meta.get("x-openapi-tags"),
312 Some(&serde_json::json!(["trading"]))
313 );
314 }
315
316 #[test]
319 fn empty_meta_is_not_serialized() {
320 let schema = EndpointSchema::new("A", 1, vec![], vec![]);
321 let text = serde_json::to_string(&schema).unwrap();
322 assert!(
323 !text.contains("meta"),
324 "empty meta leaked into output: {text}"
325 );
326
327 let field = Field::new("x", Type::String);
328 let text = serde_json::to_string(&field).unwrap();
329 assert!(!text.contains("meta"), "empty field meta leaked: {text}");
330 }
331
332 #[test]
334 fn field_meta_round_trips_and_preserves_derives() {
335 let mut meta = MetaMap::default();
336 meta.insert("minimum", serde_json::json!(0));
337 let field = Field::new("amount", Type::Int64).with_meta(meta);
338
339 let back: Field = serde_json::from_str(&serde_json::to_string(&field).unwrap()).unwrap();
340 assert_eq!(back.meta.get("minimum"), Some(&serde_json::json!(0)));
341
342 let mut set = std::collections::BTreeSet::new();
344 set.insert(field.clone());
345 assert!(set.contains(&field));
346 assert_ne!(field, Field::new("amount", Type::Int64));
347 }
348}