use crate::{
Exporter,
elm::ElmCoreLibImport,
module::ModuleImports,
types::{NDT, reference},
};
use std::borrow::Cow;
use specta::{
Types,
datatype::{DataType, Enum, Map, Primitive, Variant},
};
use crate::Error;
pub fn render<E: Exporter>(
s: &mut String,
imports: &mut ModuleImports,
types: &Types,
map: &Map,
ndt: &NDT,
exporter: &E,
) -> Result<(), Error> {
let location = &[ndt.rust_type_path()];
let path = map_key_path(location);
validate_map_key(map.key_ty(), types, format!("{path}.<map_key>"))?;
let rendered_key = map_key_render_type(map.key_ty().clone());
s.push_str("Dict ");
imports.require_core(ElmCoreLibImport::Dict);
map_key_datatype(s, imports, types, &rendered_key, ndt, None, exporter)?;
s.push(' ');
ndt.render(s, imports, exporter, types, map.value_ty(), None)?;
Ok(())
}
fn map_key_path(location: &[Cow<'static, str>]) -> String {
if location.is_empty() {
return "HashMap".to_string();
}
location.join(".")
}
fn map_key_datatype<'a, E: Exporter>(
s: &mut String,
imports: &'a mut ModuleImports,
types: &Types,
key_ty: &DataType,
ndt: &'a NDT,
parent_name: Option<&str>,
exporter: &E,
) -> Result<(), Error> {
Ok(match key_ty {
DataType::Reference(r) => reference::render(s, imports, exporter, types, r, ndt),
key_ty => ndt.render(s, imports, exporter, types, key_ty, parent_name),
}?)
}
fn map_key_render_type(dt: DataType) -> DataType {
if matches!(dt, DataType::Primitive(Primitive::bool)) {
return bool_key_literal_datatype();
}
dt
}
fn bool_key_literal_datatype() -> DataType {
let mut bool_enum = Enum::default();
bool_enum
.variants
.push((Cow::Borrowed("true"), Variant::unit()));
bool_enum
.variants
.push((Cow::Borrowed("false"), Variant::unit()));
DataType::Enum(bool_enum)
}
use std::collections::HashSet;
use specta::datatype::{Fields, NamedReferenceType, Reference};
fn validate_map_key(key_ty: &DataType, types: &Types, path: String) -> Result<(), Error> {
validate_map_key_inner(key_ty, types, path, &mut HashSet::new())
}
fn validate_map_key_inner(
key_ty: &DataType,
types: &Types,
path: String,
visiting_named_refs: &mut HashSet<Reference>,
) -> Result<(), Error> {
fn unwrap_synthetic_variant_fields<'a>(
variant_name: &str,
fields: &'a Fields,
) -> Option<&'a Fields> {
let Fields::Named(named) = fields else {
return None;
};
let mut live_fields = named
.fields
.iter()
.filter_map(|(name, field)| field.ty.as_ref().map(|ty| (name.as_ref(), ty)));
let (field_name, DataType::Enum(inner)) = live_fields.next()? else {
return None;
};
if field_name != variant_name || live_fields.next().is_some() {
return None;
}
match inner.variants.as_slice() {
[(inner_name, inner_variant)] if inner_name == variant_name => {
Some(&inner_variant.fields)
}
_ => None,
}
}
match key_ty {
DataType::Primitive(primitive) if primitive_is_valid_key(primitive.clone()) => Ok(()),
DataType::Primitive(other) => Err(Error::invalid_map_key(
path,
invalid_primitive_reason(other.clone()),
)),
DataType::Enum(enm) => {
for (variant_name, variant) in &enm.variants {
let fields = unwrap_synthetic_variant_fields(variant_name, &variant.fields)
.unwrap_or(&variant.fields);
match fields {
Fields::Unit => {}
Fields::Unnamed(unnamed) => {
let mut non_skipped =
unnamed.fields.iter().filter_map(|field| field.ty.as_ref());
let Some(inner_ty) = non_skipped.next() else {
return Err(Error::invalid_map_key(
&path,
format!(
"enum key variant '{variant_name}' must serialize as a newtype value"
),
));
};
if non_skipped.next().is_some() {
return Err(Error::invalid_map_key(
&path,
format!(
"enum key variant '{variant_name}' must serialize as a newtype value"
),
));
}
validate_map_key_inner(
inner_ty,
types,
format!("{path}.{variant_name}"),
visiting_named_refs,
)?;
}
Fields::Named(_) => {
return Err(Error::invalid_map_key(
&path,
format!(
"enum key variant '{variant_name}' serializes as a struct variant, which serde_json rejects"
),
));
}
}
}
Ok(())
}
DataType::Struct(strct) => {
let Fields::Unnamed(unnamed) = &strct.fields else {
return Err(Error::invalid_map_key(
path,
"struct keys must serialize as a newtype struct to be valid serde_json map keys",
));
};
let mut non_skipped = unnamed.fields.iter().filter_map(|field| field.ty.as_ref());
let Some(inner_ty) = non_skipped.next() else {
return Err(Error::invalid_map_key(
path,
"newtype struct map keys must have exactly one serializable field",
));
};
if non_skipped.next().is_some() {
return Err(Error::invalid_map_key(
path,
"newtype struct map keys must have exactly one serializable field",
));
}
validate_map_key_inner(inner_ty, types, path, visiting_named_refs)
}
DataType::Reference(Reference::Named(reference)) => {
let reference_key = Reference::Named(reference.clone());
if !visiting_named_refs.insert(reference_key.clone()) {
return Err(Error::invalid_map_key(
path,
"recursive map key reference cycle detected",
));
}
let result = match &reference.inner {
NamedReferenceType::Reference { generics, .. } => {
if let Some(ndt) = types.get(reference) {
if let Some(ty) = ndt.ty.as_ref() {
validate_map_key_inner(&ty, types, path, visiting_named_refs)
} else {
Err(Error::dangling_named_reference(
path,
format!("{reference:?}"),
))
}
} else {
Err(Error::dangling_named_reference(
path,
format!("{reference:?}"),
))
}
}
NamedReferenceType::Inline { dt, .. } => {
let inline_path = path.clone();
validate_map_key_inner(dt, types, path, visiting_named_refs)
.map_err(|err| err.with_inline_trace(types.get(reference), inline_path))
}
NamedReferenceType::Recursive(_) => Err(Error::invalid_map_key(
path,
format!("recursive inline named map key reference {reference:?}"),
)),
};
visiting_named_refs.remove(&reference_key);
result
}
DataType::Generic(_) => Err(Error::invalid_map_key(
path,
"generics are not supported by specta-elm",
)),
DataType::Reference(Reference::Opaque(_)) => Err(Error::invalid_map_key(
path,
"opaque references are not supported by specta-elm",
)),
DataType::Tuple(_) => Err(Error::invalid_map_key(
path,
"tuple keys are not supported by serde_json map key serialization",
)),
DataType::List(_)
| DataType::Map(_)
| DataType::Nullable(_)
| DataType::Intersection(_) => Err(Error::invalid_map_key(
path,
"collection, map, and nullable keys are not supported by serde_json map key serialization",
)),
}
}
fn primitive_is_valid_key(primitive: Primitive) -> bool {
matches!(
primitive,
Primitive::bool
| Primitive::i8
| Primitive::i16
| Primitive::i32
| Primitive::i64
| Primitive::i128
| Primitive::isize
| Primitive::u8
| Primitive::u16
| Primitive::u32
| Primitive::u64
| Primitive::u128
| Primitive::usize
| Primitive::f32
| Primitive::f64
| Primitive::str
| Primitive::char
)
}
fn invalid_primitive_reason(primitive: Primitive) -> &'static str {
match primitive {
Primitive::f16 | Primitive::f128 => {
"f16 and f128 keys are not supported by serde_json map key serialization"
}
_ => "unsupported primitive key type for serde_json map key serialization",
}
}
#[cfg(test)]
#[allow(dead_code)]
pub mod test {
use std::collections::HashMap;
use specta::{Type, Types};
use crate::{Elm, SingleFileExporter};
#[derive(Type)]
pub struct MapTrioska {
map: HashMap<String, AMap>,
}
#[derive(Type)]
pub struct AMap {
map: HashMap<String, String>,
}
#[test]
fn maps() {
let types = Types::default().register::<AMap>().register::<MapTrioska>();
let mut elm = Elm::init(types, "gen/test_ok");
elm.export(SingleFileExporter("Map.elm")).unwrap();
}
}