use std::collections::BTreeMap;
use serde_json::{Value, json};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeMapping {
Literal(Value),
Transparent {
arg: usize,
},
}
impl TypeMapping {
pub fn as_literal(&self) -> Option<&Value> {
match self {
Self::Literal(v) => Some(v),
Self::Transparent { .. } => None,
}
}
}
impl From<Value> for TypeMapping {
fn from(schema: Value) -> Self {
Self::Literal(schema)
}
}
#[derive(Debug, Clone)]
pub struct TypeTable {
map: BTreeMap<String, TypeMapping>,
}
impl Default for TypeTable {
fn default() -> Self {
Self::builtin()
}
}
impl TypeTable {
pub fn builtin() -> Self {
let mut map = BTreeMap::new();
let mut put = |k: &str, v: Value| {
map.insert(k.to_string(), TypeMapping::Literal(v));
};
let string = || json!({ "type": "string" });
put("alloc::string::String", string());
put("str", string());
put("std::path::PathBuf", string());
put("std::path::Path", string());
put("std::ffi::OsString", string());
put("core::net::IpAddr", json!({ "type": "string" }));
put(
"core::net::Ipv4Addr",
json!({ "type": "string", "format": "ipv4" }),
);
put(
"core::net::Ipv6Addr",
json!({ "type": "string", "format": "ipv6" }),
);
put("std::net::SocketAddr", string());
put(
"core::time::Duration",
json!({
"type": "object",
"properties": { "secs": { "type": "integer", "minimum": 0 },
"nanos": { "type": "integer", "minimum": 0 } },
"required": ["secs", "nanos"],
}),
);
put(
"std::time::SystemTime",
json!({
"type": "object",
"properties": { "secs_since_epoch": { "type": "integer" },
"nanos_since_epoch": { "type": "integer" } },
"required": ["secs_since_epoch", "nanos_since_epoch"],
}),
);
put("uuid::Uuid", json!({ "type": "string", "format": "uuid" }));
put("url::Url", json!({ "type": "string", "format": "uri" }));
let date_time = || json!({ "type": "string", "format": "date-time" });
put("chrono::DateTime", date_time());
put("chrono::naive::datetime::NaiveDateTime", date_time());
put(
"chrono::naive::date::NaiveDate",
json!({ "type": "string", "format": "date" }),
);
put(
"chrono::naive::time::NaiveTime",
json!({ "type": "string", "format": "time" }),
);
put("time::OffsetDateTime", date_time());
put("rust_decimal::Decimal", string());
put("bigdecimal::BigDecimal", string());
put("serde_json::value::Value", json!({}));
Self { map }
}
pub fn empty() -> Self {
Self {
map: BTreeMap::new(),
}
}
pub fn insert(&mut self, path: impl Into<String>, mapping: impl Into<TypeMapping>) {
self.map.insert(path.into(), mapping.into());
}
pub fn lookup(&self, canonical: &str) -> Option<&TypeMapping> {
if let Some(v) = self.map.get(canonical) {
return Some(v);
}
let segments: Vec<&str> = canonical.split("::").collect();
if let (Some(first), Some(last)) = (segments.first(), segments.last()) {
if segments.len() > 2
&& let Some(v) = self.map.get(&format!("{first}::{last}"))
{
return Some(v);
}
if let Some((_, v)) = self
.map
.iter()
.find(|(k, _)| k.rsplit("::").next() == Some(*last))
{
return Some(v);
}
}
None
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn literal<'a>(t: &'a TypeTable, path: &str) -> Option<&'a Value> {
t.lookup(path).and_then(TypeMapping::as_literal)
}
#[test]
fn maps_std_string_by_canonical_path() {
let t = TypeTable::builtin();
assert_eq!(
literal(&t, "alloc::string::String"),
Some(&json!({"type":"string"}))
);
}
#[test]
fn duration_maps_to_the_struct_serde_actually_emits() {
let t = TypeTable::builtin();
let d = literal(&t, "core::time::Duration").expect("Duration mapped");
assert_eq!(d["type"], "object");
assert_eq!(d["required"][0], "secs");
}
#[test]
fn accepts_the_short_form_users_write_in_config() {
let mut t = TypeTable::empty();
t.insert("uuid::Uuid", json!({"type":"string","format":"uuid"}));
let found = literal(&t, "uuid::builder::Uuid").expect("short form matches");
assert_eq!(found["format"], "uuid");
}
#[test]
fn a_transparent_entry_is_kept_apart_from_a_literal_one() {
let mut t = TypeTable::empty();
t.insert("async_graphql::types::json::Json", json!({}));
assert_eq!(
literal(&t, "async_graphql::types::json::Json"),
Some(&json!({})),
"an empty table is a literal open schema, not a directive"
);
t.insert(
"async_graphql::types::json::Json",
TypeMapping::Transparent { arg: 0 },
);
assert_eq!(
t.lookup("async_graphql::types::json::Json"),
Some(&TypeMapping::Transparent { arg: 0 })
);
assert_eq!(literal(&t, "async_graphql::types::json::Json"), None);
}
#[test]
fn unmapped_type_returns_none_rather_than_a_guess() {
let t = TypeTable::builtin();
assert_eq!(t.lookup("some_crate::Exotic"), None);
}
#[test]
fn user_entries_override_builtins() {
let mut t = TypeTable::builtin();
t.insert("core::time::Duration", json!({"type":"integer"}));
let d = literal(&t, "core::time::Duration").expect("still mapped");
assert_eq!(d["type"], "integer");
}
}