specta-elm 0.0.1

Yey! now your Rust types in Elm ;)
Documentation
use crate::elm::RESERVED_TYPE_NAMES;
use crate::module::ModuleImports;
use crate::{Error, Exporter};
use std::borrow::Cow;

use specta::Types;
use specta::datatype::{DataType, NamedDataType};

pub mod elm;
pub mod enums;
pub mod fields;
pub mod list;
pub mod map;
pub mod primitive;
pub mod reference;
pub mod structs;
pub mod tuples;

#[derive(Debug, Clone)]
pub struct NDT<'a> {
    ndt: &'a NamedDataType,
}

impl<'a> From<&'a NamedDataType> for NDT<'a> {
    fn from(ndt: &'a NamedDataType) -> Self {
        Self { ndt }
    }
}

impl<'a> NDT<'a> {
    pub(crate) fn render<E: Exporter>(
        &'a self,
        s: &mut String,
        imports: &'a mut ModuleImports,
        exporter: &E,
        types: &Types,
        dt: &DataType,
        parent_name: Option<&str>,
    ) -> Result<(), Error> {
        let have_we_included_refs_here = false;
        if have_we_included_refs_here {
            todo!() // use exporter
        }

        match dt {
            DataType::Primitive(p) => {
                s.push_str(primitive::render(p, self.rust_type_path().to_string())?)
            }
            DataType::Generic(_) => panic!("No generics form Elm, thanks"),
            DataType::List(list) => {
                list::render(s, imports, exporter, types, list, self)?;
            }

            DataType::Map(map) => {
                map::render(s, imports, types, map, self, exporter)?;
            }
            DataType::Nullable(inner) => {
                let mut rendered = String::new();
                self.render(&mut rendered, imports, exporter, types, inner, parent_name)?;

                if !rendered.starts_with("Maybe") {
                    s.push_str("Maybe ");
                };

                s.push_str(&rendered);
            }
            DataType::Struct(st) => {
                structs::render(s, imports, exporter, types, self, st, parent_name)?
            }
            DataType::Enum(enm) => enums::render(s, imports, exporter, types, enm, self)?,
            DataType::Tuple(tuple) => {
                tuples::render(s, imports, exporter, types, self, tuple)?;
            }
            DataType::Intersection(parts) => {
                for (idx, ty) in parts.iter().enumerate() {
                    if idx != 0 {
                        s.push_str(" & ");
                    }

                    let needs_parentheses = parts.len() > 1 && intersection_part_is_union(ty);
                    if needs_parentheses {
                        s.push('(');
                    }
                    self.render(s, imports, exporter, types, ty, parent_name)?;
                    if needs_parentheses {
                        s.push(')');
                    }
                }
            }
            DataType::Reference(r) => reference::render(s, imports, exporter, types, r, self)?,
        }

        Ok(())
    }

    pub fn inner(&self) -> &'a NamedDataType {
        self.ndt
    }

    fn path_string(&self) -> String {
        self.ndt.module_path.to_string()
    }

    pub fn rust_type_path(&self) -> Cow<'static, str> {
        let ndt = self.ndt;
        if ndt.module_path.is_empty() {
            ndt.name.clone()
        } else {
            Cow::Owned(format!("{}::{}", ndt.module_path, ndt.name))
        }
    }

    pub fn sanitise_type_name(&self) -> Result<String, Error> {
        let path = self.path_string();
        let ident = &self.ndt.name;

        if ident.is_empty() {
            return Err(Error::empty_name(path));
        }

        if let Some(name) = RESERVED_TYPE_NAMES.iter().find(|v| **v == ident) {
            return Err(Error::forbidden_name(path, name));
        }

        if let Some(first_char) = ident.chars().next()
            && !first_char.is_alphabetic()
            && first_char != '_'
        {
            return Err(Error::invalid_name(path, ident.to_string()));
        }

        if ident
            .find(|c: char| !c.is_alphanumeric() && c != '_')
            .is_some()
        {
            return Err(Error::invalid_name(path, ident.to_string()));
        }

        Ok(ident.to_string())
    }
}

fn intersection_part_is_union(ty: &DataType) -> bool {
    match ty {
        DataType::Nullable(_) => true,
        DataType::Enum(enm) => enm.variants.len() > 1,
        DataType::Intersection(parts) if parts.len() == 1 => intersection_part_is_union(&parts[0]),
        _ => false,
    }
}