use std::collections::{BTreeMap, HashMap};
use std::error;
use std::fmt;
use serde::Serialize;
mod identifier;
mod render;
mod resolve;
use identifier::validate_ts_identifier;
pub use render::{property_name, render_type_ref, render_types, string_literal};
pub use resolve::{ResolvedTypeDef, ResolvedTypes, TypeResolver};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Error {
message: String,
}
impl Error {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TypePhase {
Serialize,
Deserialize,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TsExtraType {
type_ref: TypeRef,
type_defs: Vec<TypeDef>,
}
impl TsExtraType {
pub fn of<T>() -> Result<Self>
where
T: Type,
{
Self::of_for::<T>(TypePhase::Serialize)
}
pub fn of_for<T>(phase: TypePhase) -> Result<Self>
where
T: Type,
{
let mut registry = TypeRegistry::default();
T::collect_type_defs_for(phase, &mut registry)?;
Ok(Self {
type_ref: T::type_ref_for(phase),
type_defs: registry.into_defs(),
})
}
#[doc(hidden)]
pub fn __type_ref(&self) -> &TypeRef {
&self.type_ref
}
#[doc(hidden)]
pub fn __type_defs(&self) -> &[TypeDef] {
&self.type_defs
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TsDrafter {
entries: Vec<String>,
}
impl TsDrafter {
pub fn new() -> Self {
Self::default()
}
pub fn export_const(
&mut self,
name: impl Into<String>,
value: impl Serialize,
) -> Result<&mut Self> {
self.add_const("export ", name, value)
}
pub fn const_(&mut self, name: impl Into<String>, value: impl Serialize) -> Result<&mut Self> {
self.add_const("", name, value)
}
pub fn export_type(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Result<&mut Self> {
self.add_type("export ", name, value)
}
pub fn type_(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Result<&mut Self> {
self.add_type("", name, value)
}
pub fn export_string_enum(
&mut self,
name: impl Into<String>,
variants: impl IntoIterator<Item = impl Into<String>>,
) -> Result<&mut Self> {
let mut value = String::new();
for (index, variant) in variants.into_iter().enumerate() {
if index > 0 {
value.push_str(" | ");
}
value.push_str(&string_literal(&variant.into()));
}
if value.is_empty() {
value.push_str("never");
}
self.export_type(name, value)
}
pub fn export_string_enum_object(
&mut self,
const_name: impl Into<String>,
type_name: impl Into<String>,
variants: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
) -> Result<&mut Self> {
let const_name = const_name.into();
let type_name = type_name.into();
validate_ts_identifier(&type_name, "TypeScript declaration name")?;
let mut object = BTreeMap::<String, String>::new();
for (key, value) in variants {
object.insert(key.into(), value.into());
}
self.export_const(&const_name, object)?;
self.entries.push(format!(
"export type {type_name} = (typeof {const_name})[keyof typeof {const_name}];"
));
Ok(self)
}
pub fn raw(&mut self, content: impl Into<String>) -> &mut Self {
self.entries.push(content.into());
self
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn add_const(
&mut self,
prefix: &str,
name: impl Into<String>,
value: impl Serialize,
) -> Result<&mut Self> {
let name = name.into();
validate_ts_identifier(&name, "TypeScript declaration name")?;
let value = serde_json::to_value(value)
.map_err(|err| Error::new(format!("error serializing TypeScript const: {err}")))?;
let mut rendered = serde_json::to_string_pretty(&value)
.map_err(|err| Error::new(format!("error rendering TypeScript const: {err}")))?;
if matches!(
value,
serde_json::Value::Array(_) | serde_json::Value::Object(_)
) {
rendered.push_str(" as const");
}
self.entries
.push(format!("{prefix}const {name} = {rendered};"));
Ok(self)
}
fn add_type(
&mut self,
prefix: &str,
name: impl Into<String>,
value: impl Into<String>,
) -> Result<&mut Self> {
let name = name.into();
validate_ts_identifier(&name, "TypeScript declaration name")?;
self.entries
.push(format!("{prefix}type {name} = {};", value.into()));
Ok(self)
}
}
impl fmt::Display for TsDrafter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, entry) in self.entries.iter().enumerate() {
if index > 0 {
f.write_str("\n\n")?;
}
f.write_str(entry)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TypeRef {
Unit,
Null,
Unknown,
Bool,
String,
Number,
Integer,
Named {
key: String,
name: String,
},
Array(Box<TypeRef>),
Map(Box<TypeRef>, Box<TypeRef>),
Nullable(Box<TypeRef>),
Union(Vec<TypeRef>),
StringLiteral(String),
Raw(Vec<RawTsPart>),
}
impl TypeRef {
pub fn named(name: impl Into<String>) -> Self {
let name = name.into();
Self::named_with_key(name.clone(), name)
}
pub fn named_with_key(key: impl Into<String>, name: impl Into<String>) -> Self {
Self::Named {
key: key.into(),
name: name.into(),
}
}
pub fn array(inner: TypeRef) -> Self {
Self::Array(Box::new(inner))
}
pub fn map(key: TypeRef, value: TypeRef) -> Self {
Self::Map(Box::new(key), Box::new(value))
}
pub fn nullable(inner: TypeRef) -> Self {
Self::Nullable(Box::new(inner))
}
pub fn union(types: Vec<TypeRef>) -> Self {
Self::Union(types)
}
pub fn string_literal(value: impl Into<String>) -> Self {
Self::StringLiteral(value.into())
}
pub fn raw(ts: impl Into<String>) -> Self {
Self::Raw(vec![RawTsPart::Text(ts.into())])
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RawTsPart {
Text(String),
TypeRef(TypeRef),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TypeDef {
Alias {
key: String,
name: String,
target: TypeRef,
},
Record {
key: String,
name: String,
fields: Vec<FieldDef>,
},
StringEnum {
key: String,
name: String,
variants: Vec<String>,
},
Raw {
key: String,
name: String,
body: Vec<RawTsPart>,
},
}
impl TypeDef {
pub fn key(&self) -> &str {
match self {
Self::Alias { key, .. }
| Self::Record { key, .. }
| Self::StringEnum { key, .. }
| Self::Raw { key, .. } => key,
}
}
pub fn name(&self) -> &str {
match self {
Self::Alias { name, .. }
| Self::Record { name, .. }
| Self::StringEnum { name, .. }
| Self::Raw { name, .. } => name,
}
}
pub fn alias(name: impl Into<String>, target: TypeRef) -> Self {
let name = name.into();
Self::alias_with_key(name.clone(), name, target)
}
pub fn alias_with_key(
key: impl Into<String>,
name: impl Into<String>,
target: TypeRef,
) -> Self {
Self::Alias {
key: key.into(),
name: name.into(),
target,
}
}
pub fn record(name: impl Into<String>, fields: Vec<FieldDef>) -> Self {
let name = name.into();
Self::record_with_key(name.clone(), name, fields)
}
pub fn record_with_key(
key: impl Into<String>,
name: impl Into<String>,
fields: Vec<FieldDef>,
) -> Self {
Self::Record {
key: key.into(),
name: name.into(),
fields,
}
}
pub fn string_enum(
name: impl Into<String>,
variants: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
let name = name.into();
Self::string_enum_with_key(name.clone(), name, variants)
}
pub fn string_enum_with_key(
key: impl Into<String>,
name: impl Into<String>,
variants: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self::StringEnum {
key: key.into(),
name: name.into(),
variants: variants.into_iter().map(Into::into).collect(),
}
}
pub fn raw(name: impl Into<String>, body: impl Into<String>) -> Self {
let name = name.into();
Self::Raw {
key: name.clone(),
name,
body: vec![RawTsPart::Text(body.into())],
}
}
fn has_same_body_as(&self, other: &Self) -> bool {
match (self, other) {
(Self::Alias { target: left, .. }, Self::Alias { target: right, .. }) => left == right,
(Self::Record { fields: left, .. }, Self::Record { fields: right, .. }) => {
left == right
}
(
Self::StringEnum { variants: left, .. },
Self::StringEnum {
variants: right, ..
},
) => left == right,
(Self::Raw { body: left, .. }, Self::Raw { body: right, .. }) => left == right,
_ => false,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FieldDef {
name: String,
type_ref: TypeRef,
optional: bool,
}
impl FieldDef {
pub fn required<T: Type>(name: impl Into<String>) -> Self {
Self::new(name, T::type_ref(), false)
}
pub fn required_for<T: Type>(name: impl Into<String>, phase: TypePhase) -> Self {
Self::new(name, T::type_ref_for(phase), false)
}
pub fn optional<T: Type>(name: impl Into<String>) -> Self {
Self::new(name, T::type_ref(), true)
}
pub fn optional_for<T: Type>(name: impl Into<String>, phase: TypePhase) -> Self {
Self::new(name, T::type_ref_for(phase), true)
}
pub fn new(name: impl Into<String>, type_ref: TypeRef, optional: bool) -> Self {
Self {
name: name.into(),
type_ref,
optional,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn type_ref(&self) -> &TypeRef {
&self.type_ref
}
pub fn is_optional(&self) -> bool {
self.optional
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TypeRegistry {
defs: Vec<TypeDef>,
}
impl TypeRegistry {
pub fn define(&mut self, def: TypeDef) {
self.try_define(def)
.expect("conflicting Vorma type definitions");
}
pub fn try_define(&mut self, def: TypeDef) -> Result<bool> {
if let Some(existing) = self
.defs
.iter()
.find(|existing| existing.key() == def.key())
{
if existing == &def {
return Ok(false);
}
return Err(Error::new(format!(
"conflicting TypeScript type definitions for {}",
def.name()
)));
}
self.defs.push(def);
Ok(true)
}
pub fn into_defs(self) -> Vec<TypeDef> {
self.defs
}
}
pub trait Type: Send + Sync + 'static {
fn type_ref() -> TypeRef;
fn collect_type_defs(_registry: &mut TypeRegistry) -> Result<()> {
Ok(())
}
fn type_ref_for(_phase: TypePhase) -> TypeRef {
Self::type_ref()
}
fn collect_type_defs_for(phase: TypePhase, registry: &mut TypeRegistry) -> Result<()> {
let _ = phase;
Self::collect_type_defs(registry)
}
}
impl Type for () {
fn type_ref() -> TypeRef {
TypeRef::Unit
}
}
impl Type for crate::mux::None {
fn type_ref() -> TypeRef {
TypeRef::Unit
}
}
impl Type for bool {
fn type_ref() -> TypeRef {
TypeRef::Bool
}
}
impl Type for String {
fn type_ref() -> TypeRef {
TypeRef::String
}
}
impl Type for &'static str {
fn type_ref() -> TypeRef {
TypeRef::String
}
}
impl Type for serde_json::Value {
fn type_ref() -> TypeRef {
TypeRef::Unknown
}
}
macro_rules! integer_type {
($($ty:ty),* $(,)?) => {
$(
impl Type for $ty {
fn type_ref() -> TypeRef {
TypeRef::Integer
}
}
)*
};
}
integer_type!(
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
);
macro_rules! number_type {
($($ty:ty),* $(,)?) => {
$(
impl Type for $ty {
fn type_ref() -> TypeRef {
TypeRef::Number
}
}
)*
};
}
number_type!(f32, f64);
impl<T: Type> Type for Option<T> {
fn type_ref() -> TypeRef {
TypeRef::nullable(T::type_ref())
}
fn type_ref_for(phase: TypePhase) -> TypeRef {
TypeRef::nullable(T::type_ref_for(phase))
}
fn collect_type_defs(registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs(registry)
}
fn collect_type_defs_for(phase: TypePhase, registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs_for(phase, registry)
}
}
impl<T: Type> Type for Vec<T> {
fn type_ref() -> TypeRef {
TypeRef::array(T::type_ref())
}
fn type_ref_for(phase: TypePhase) -> TypeRef {
TypeRef::array(T::type_ref_for(phase))
}
fn collect_type_defs(registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs(registry)
}
fn collect_type_defs_for(phase: TypePhase, registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs_for(phase, registry)
}
}
impl<T: Type, const N: usize> Type for [T; N] {
fn type_ref() -> TypeRef {
TypeRef::array(T::type_ref())
}
fn type_ref_for(phase: TypePhase) -> TypeRef {
TypeRef::array(T::type_ref_for(phase))
}
fn collect_type_defs(registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs(registry)
}
fn collect_type_defs_for(phase: TypePhase, registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs_for(phase, registry)
}
}
impl<T: Type> Type for Box<T> {
fn type_ref() -> TypeRef {
T::type_ref()
}
fn type_ref_for(phase: TypePhase) -> TypeRef {
T::type_ref_for(phase)
}
fn collect_type_defs(registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs(registry)
}
fn collect_type_defs_for(phase: TypePhase, registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs_for(phase, registry)
}
}
impl<T: Type> Type for BTreeMap<String, T> {
fn type_ref() -> TypeRef {
TypeRef::map(TypeRef::String, T::type_ref())
}
fn type_ref_for(phase: TypePhase) -> TypeRef {
TypeRef::map(TypeRef::String, T::type_ref_for(phase))
}
fn collect_type_defs(registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs(registry)
}
fn collect_type_defs_for(phase: TypePhase, registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs_for(phase, registry)
}
}
impl<T: Type> Type for HashMap<String, T> {
fn type_ref() -> TypeRef {
TypeRef::map(TypeRef::String, T::type_ref())
}
fn type_ref_for(phase: TypePhase) -> TypeRef {
TypeRef::map(TypeRef::String, T::type_ref_for(phase))
}
fn collect_type_defs(registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs(registry)
}
fn collect_type_defs_for(phase: TypePhase, registry: &mut TypeRegistry) -> Result<()> {
T::collect_type_defs_for(phase, registry)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drafter_rejects_invalid_declaration_names() {
let mut drafter = TsDrafter::new();
let err = drafter.export_const("Bad-Name", true).unwrap_err();
assert!(
err.to_string()
.contains("invalid TypeScript declaration name")
);
let err = drafter.export_type("type", "string").unwrap_err();
assert!(err.to_string().contains("reserved word"));
let err = drafter
.export_string_enum_object("FLAGS", "Bad Name", [("Ok", "ok")])
.unwrap_err();
assert!(
err.to_string()
.contains("invalid TypeScript declaration name")
);
}
#[test]
fn render_type_ref_renders_empty_union_as_never() {
assert_eq!(
render_type_ref(&TypeRef::union(Vec::new()), &BTreeMap::new()),
"never"
);
}
#[test]
#[should_panic(expected = "unresolved TypeScript type reference")]
fn render_type_ref_panics_on_unresolved_named_type() {
render_type_ref(
&TypeRef::named_with_key("missing", "Missing"),
&BTreeMap::new(),
);
}
}