1use std::collections::BTreeMap;
12
13use serde_json::{Value, json};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum TypeMapping {
23 Literal(Value),
25 Transparent {
27 arg: usize,
29 },
30}
31
32impl TypeMapping {
33 pub fn as_literal(&self) -> Option<&Value> {
35 match self {
36 Self::Literal(v) => Some(v),
37 Self::Transparent { .. } => None,
38 }
39 }
40}
41
42impl From<Value> for TypeMapping {
43 fn from(schema: Value) -> Self {
44 Self::Literal(schema)
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct TypeTable {
51 map: BTreeMap<String, TypeMapping>,
52}
53
54impl Default for TypeTable {
55 fn default() -> Self {
56 Self::builtin()
57 }
58}
59
60impl TypeTable {
61 pub fn builtin() -> Self {
64 let mut map = BTreeMap::new();
65 let mut put = |k: &str, v: Value| {
66 map.insert(k.to_string(), TypeMapping::Literal(v));
67 };
68
69 let string = || json!({ "type": "string" });
70 put("alloc::string::String", string());
71 put("str", string());
72 put("std::path::PathBuf", string());
73 put("std::path::Path", string());
74 put("std::ffi::OsString", string());
75
76 put("core::net::IpAddr", json!({ "type": "string" }));
77 put(
78 "core::net::Ipv4Addr",
79 json!({ "type": "string", "format": "ipv4" }),
80 );
81 put(
82 "core::net::Ipv6Addr",
83 json!({ "type": "string", "format": "ipv6" }),
84 );
85 put("std::net::SocketAddr", string());
86
87 put(
90 "core::time::Duration",
91 json!({
92 "type": "object",
93 "properties": { "secs": { "type": "integer", "minimum": 0 },
94 "nanos": { "type": "integer", "minimum": 0 } },
95 "required": ["secs", "nanos"],
96 }),
97 );
98 put(
99 "std::time::SystemTime",
100 json!({
101 "type": "object",
102 "properties": { "secs_since_epoch": { "type": "integer" },
103 "nanos_since_epoch": { "type": "integer" } },
104 "required": ["secs_since_epoch", "nanos_since_epoch"],
105 }),
106 );
107
108 put("uuid::Uuid", json!({ "type": "string", "format": "uuid" }));
109 put("url::Url", json!({ "type": "string", "format": "uri" }));
110
111 let date_time = || json!({ "type": "string", "format": "date-time" });
112 put("chrono::DateTime", date_time());
113 put("chrono::naive::datetime::NaiveDateTime", date_time());
114 put(
115 "chrono::naive::date::NaiveDate",
116 json!({ "type": "string", "format": "date" }),
117 );
118 put(
119 "chrono::naive::time::NaiveTime",
120 json!({ "type": "string", "format": "time" }),
121 );
122 put("time::OffsetDateTime", date_time());
123
124 put("rust_decimal::Decimal", string());
127 put("bigdecimal::BigDecimal", string());
128
129 put("serde_json::value::Value", json!({}));
131
132 Self { map }
133 }
134
135 pub fn empty() -> Self {
137 Self {
138 map: BTreeMap::new(),
139 }
140 }
141
142 pub fn insert(&mut self, path: impl Into<String>, mapping: impl Into<TypeMapping>) {
148 self.map.insert(path.into(), mapping.into());
149 }
150
151 pub fn lookup(&self, canonical: &str) -> Option<&TypeMapping> {
155 if let Some(v) = self.map.get(canonical) {
156 return Some(v);
157 }
158 let segments: Vec<&str> = canonical.split("::").collect();
159 if let (Some(first), Some(last)) = (segments.first(), segments.last()) {
160 if segments.len() > 2
161 && let Some(v) = self.map.get(&format!("{first}::{last}"))
162 {
163 return Some(v);
164 }
165 if let Some((_, v)) = self
168 .map
169 .iter()
170 .find(|(k, _)| k.rsplit("::").next() == Some(*last))
171 {
172 return Some(v);
173 }
174 }
175 None
176 }
177
178 pub fn len(&self) -> usize {
180 self.map.len()
181 }
182
183 pub fn is_empty(&self) -> bool {
185 self.map.is_empty()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 fn literal<'a>(t: &'a TypeTable, path: &str) -> Option<&'a Value> {
195 t.lookup(path).and_then(TypeMapping::as_literal)
196 }
197
198 #[test]
199 fn maps_std_string_by_canonical_path() {
200 let t = TypeTable::builtin();
201 assert_eq!(
202 literal(&t, "alloc::string::String"),
203 Some(&json!({"type":"string"}))
204 );
205 }
206
207 #[test]
208 fn duration_maps_to_the_struct_serde_actually_emits() {
209 let t = TypeTable::builtin();
210 let d = literal(&t, "core::time::Duration").expect("Duration mapped");
211 assert_eq!(d["type"], "object");
212 assert_eq!(d["required"][0], "secs");
213 }
214
215 #[test]
216 fn accepts_the_short_form_users_write_in_config() {
217 let mut t = TypeTable::empty();
218 t.insert("uuid::Uuid", json!({"type":"string","format":"uuid"}));
219 let found = literal(&t, "uuid::builder::Uuid").expect("short form matches");
221 assert_eq!(found["format"], "uuid");
222 }
223
224 #[test]
225 fn a_transparent_entry_is_kept_apart_from_a_literal_one() {
226 let mut t = TypeTable::empty();
227 t.insert("async_graphql::types::json::Json", json!({}));
228 assert_eq!(
229 literal(&t, "async_graphql::types::json::Json"),
230 Some(&json!({})),
231 "an empty table is a literal open schema, not a directive"
232 );
233 t.insert(
234 "async_graphql::types::json::Json",
235 TypeMapping::Transparent { arg: 0 },
236 );
237 assert_eq!(
238 t.lookup("async_graphql::types::json::Json"),
239 Some(&TypeMapping::Transparent { arg: 0 })
240 );
241 assert_eq!(literal(&t, "async_graphql::types::json::Json"), None);
242 }
243
244 #[test]
245 fn unmapped_type_returns_none_rather_than_a_guess() {
246 let t = TypeTable::builtin();
247 assert_eq!(t.lookup("some_crate::Exotic"), None);
248 }
249
250 #[test]
251 fn user_entries_override_builtins() {
252 let mut t = TypeTable::builtin();
253 t.insert("core::time::Duration", json!({"type":"integer"}));
254 let d = literal(&t, "core::time::Duration").expect("still mapped");
255 assert_eq!(d["type"], "integer");
256 }
257}