diff --git a/Cargo.lock b/Cargo.lock index bcea946ba..6e809011f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4699,6 +4699,25 @@ dependencies = [ "serde", ] +[[package]] +name = "wezterm-dynamic" +version = "0.1.0" +dependencies = [ + "maplit", + "ordered-float", + "thiserror", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "wezterm-font" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8dccf1f17..65ba24391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "bidi/generate", "strip-ansi-escapes", "wezterm", + "wezterm-dynamic", "wezterm-gui", "wezterm-mux-server", "wezterm-ssh" diff --git a/wezterm-dynamic/Cargo.toml b/wezterm-dynamic/Cargo.toml new file mode 100644 index 000000000..7c5c12c96 --- /dev/null +++ b/wezterm-dynamic/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "wezterm-dynamic" +version = "0.1.0" +edition = "2021" + +[dependencies] +wezterm-dynamic-derive = { version="0.1", path="derive" } +ordered-float = "3.0" +thiserror = "1.0" +strsim = "0.10" +log = "0.4" + +[dev-dependencies] +maplit = "1.0" diff --git a/wezterm-dynamic/derive/Cargo.toml b/wezterm-dynamic/derive/Cargo.toml new file mode 100644 index 000000000..7d65e9a96 --- /dev/null +++ b/wezterm-dynamic/derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "wezterm-dynamic-derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0.2" +syn = "1.0" diff --git a/wezterm-dynamic/derive/src/attr.rs b/wezterm-dynamic/derive/src/attr.rs new file mode 100644 index 000000000..7b26e7827 --- /dev/null +++ b/wezterm-dynamic/derive/src/attr.rs @@ -0,0 +1,315 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Attribute, Error, Field, Lit, Meta, NestedMeta, Path, Result}; + +pub struct ContainerInfo { + pub into: Option, + pub try_from: Option, + pub debug: bool, +} + +pub fn container_info(attrs: &[Attribute]) -> Result { + let mut into = None; + let mut try_from = None; + let mut debug = false; + + for attr in attrs { + if !attr.path.is_ident("dynamic") { + continue; + } + + let list = match attr.parse_meta()? { + Meta::List(list) => list, + other => return Err(Error::new_spanned(other, "unsupported attribute")), + }; + + for meta in &list.nested { + match meta { + NestedMeta::Meta(Meta::Path(path)) => { + if path.is_ident("debug") { + debug = true; + continue; + } + } + NestedMeta::Meta(Meta::NameValue(value)) => { + if value.path.is_ident("into") { + if let Lit::Str(s) = &value.lit { + into = Some(s.parse()?); + continue; + } + } + if value.path.is_ident("try_from") { + if let Lit::Str(s) = &value.lit { + try_from = Some(s.parse()?); + continue; + } + } + } + _ => {} + } + return Err(Error::new_spanned(meta, "unsupported attribute")); + } + } + + Ok(ContainerInfo { + into, + try_from, + debug, + }) +} + +pub enum DefValue { + None, + Default, + Path(Path), +} + +pub struct FieldInfo<'a> { + pub field: &'a Field, + pub name: String, + pub skip: bool, + pub flatten: bool, + pub allow_default: DefValue, + pub into: Option, + pub try_from: Option, +} + +impl<'a> FieldInfo<'a> { + pub fn to_dynamic(&self) -> TokenStream { + let name = &self.name; + let ident = &self.field.ident; + if self.skip { + quote!() + } else if self.flatten { + quote!( + self.#ident.place_dynamic(place); + ) + } else if let Some(into) = &self.into { + quote!( + let target : #into = (&self.#ident).into(); + place.insert(#name.to_dynamic(), target.to_dynamic()); + ) + } else { + quote!( + place.insert(#name.to_dynamic(), self.#ident.to_dynamic()); + ) + } + } + + pub fn from_dynamic(&self, struct_name: &str) -> TokenStream { + let name = &self.name; + let ident = &self.field.ident; + let ty = &self.field.ty; + if self.skip { + quote!() + } else if self.flatten { + quote!( + #ident: + <#ty>::from_dynamic(value, options) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })?, + ) + } else if let Some(try_from) = &self.try_from { + match &self.allow_default { + DefValue::Default => { + quote!( + #ident: match obj.get_by_str(#name) { + Some(v) => { + use std::convert::TryFrom; + let target = <#try_from>::from_dynamic(v, options) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })?; + <#ty>::try_from(target) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })? + } + None => { + <#ty>::default() + } + }, + ) + } + DefValue::Path(default) => { + quote!( + #ident: match obj.get_by_str(&#name) { + Some(v) => { + use std::convert::TryFrom; + let target = <#try_from>::from_dynamic(v, options) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })?; + <#ty>::try_from(target) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })? + } + None => { + #default() + } + }, + ) + } + DefValue::None => { + quote!( + #ident: { + use std::convert::TryFrom; + let target = <#try_from>::from_dynamic(obj.get_by_str(#name).unwrap_or(&Value::Null), options) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })?; + <#ty>::try_from(target) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })? + }, + ) + } + } + } else { + match &self.allow_default { + DefValue::Default => { + quote!( + #ident: match obj.get_by_str(#name) { + Some(v) => { + <#ty>::from_dynamic(v, options) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })? + } + None => { + <#ty>::default() + } + }, + ) + } + DefValue::Path(default) => { + quote!( + #ident: match obj.get_by_str(#name) { + Some(v) => { + <#ty>::from_dynamic(v, options) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })? + } + None => { + #default() + } + }, + ) + } + DefValue::None => { + quote!( + #ident: + <#ty>::from_dynamic( + obj.get_by_str(#name).unwrap_or(&Value::Null), + options + ) + .map_err(|source| wezterm_dynamic::Error::ErrorInField { + type_name: #struct_name, + field_name: #name, + error: source.to_string(), + })?, + ) + } + } + } + } +} + +pub fn field_info(field: &Field) -> Result { + let mut name = field.ident.as_ref().unwrap().to_string(); + let mut skip = false; + let mut flatten = false; + let mut allow_default = DefValue::None; + let mut try_from = None; + let mut into = None; + + for attr in &field.attrs { + if !attr.path.is_ident("dynamic") { + continue; + } + + let list = match attr.parse_meta()? { + Meta::List(list) => list, + other => return Err(Error::new_spanned(other, "unsupported attribute")), + }; + + for meta in &list.nested { + match meta { + NestedMeta::Meta(Meta::NameValue(value)) => { + if value.path.is_ident("rename") { + if let Lit::Str(s) = &value.lit { + name = s.value(); + continue; + } + } + if value.path.is_ident("default") { + if let Lit::Str(s) = &value.lit { + allow_default = DefValue::Path(s.parse()?); + continue; + } + } + if value.path.is_ident("into") { + if let Lit::Str(s) = &value.lit { + into = Some(s.parse()?); + continue; + } + } + if value.path.is_ident("try_from") { + if let Lit::Str(s) = &value.lit { + try_from = Some(s.parse()?); + continue; + } + } + } + NestedMeta::Meta(Meta::Path(path)) => { + if path.is_ident("skip") { + skip = true; + continue; + } + if path.is_ident("flatten") { + flatten = true; + continue; + } + if path.is_ident("default") { + allow_default = DefValue::Default; + continue; + } + } + _ => {} + } + return Err(Error::new_spanned(meta, "unsupported attribute")); + } + } + + Ok(FieldInfo { + field, + name, + skip, + flatten, + allow_default, + try_from, + into, + }) +} diff --git a/wezterm-dynamic/derive/src/bound.rs b/wezterm-dynamic/derive/src/bound.rs new file mode 100644 index 000000000..17c5640f1 --- /dev/null +++ b/wezterm-dynamic/derive/src/bound.rs @@ -0,0 +1,16 @@ +use proc_macro2::TokenStream; +use syn::{parse_quote, Generics, WhereClause, WherePredicate}; + +pub fn where_clause_with_bound(generics: &Generics, bound: TokenStream) -> WhereClause { + let new_predicates = generics.type_params().map::(|param| { + let param = ¶m.ident; + parse_quote!(#param : #bound) + }); + + let mut generics = generics.clone(); + generics + .make_where_clause() + .predicates + .extend(new_predicates); + generics.where_clause.unwrap() +} diff --git a/wezterm-dynamic/derive/src/fromdynamic.rs b/wezterm-dynamic/derive/src/fromdynamic.rs new file mode 100644 index 000000000..cc3849270 --- /dev/null +++ b/wezterm-dynamic/derive/src/fromdynamic.rs @@ -0,0 +1,359 @@ +use crate::{attr, bound}; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{ + parse_quote, Data, DataEnum, DataStruct, DeriveInput, Error, Fields, FieldsNamed, Ident, Result, +}; + +pub fn derive(input: DeriveInput) -> Result { + match &input.data { + Data::Struct(DataStruct { + fields: Fields::Named(fields), + .. + }) => derive_struct(&input, fields), + Data::Enum(enumeration) => derive_enum(&input, enumeration), + Data::Struct(_) => Err(Error::new( + Span::call_site(), + "currently only structs with named fields are supported", + )), + Data::Union(_) => Err(Error::new( + Span::call_site(), + "currently only structs and enums are supported by this derive", + )), + } +} + +fn derive_struct(input: &DeriveInput, fields: &FieldsNamed) -> Result { + let info = attr::container_info(&input.attrs)?; + let ident = &input.ident; + let literal = ident.to_string(); + let (impl_generics, ty_generics, _where_clause) = input.generics.split_for_impl(); + let dummy = Ident::new( + &format!("_IMPL_FROMDYNAMIC_FOR_{}", ident), + Span::call_site(), + ); + + let placements = fields + .named + .iter() + .map(attr::field_info) + .collect::>>()?; + let needs_default = placements.iter().any(|f| f.skip); + let field_names = placements + .iter() + .filter_map(|f| { + if f.skip || f.flatten { + None + } else { + Some(f.name.to_string()) + } + }) + .collect::>(); + + // If any of the fields are flattened, then we don't have enough + // structure in the FromDynamic interface to know precisely which + // fields were legitimately used by any recursively flattened item, + // or, in the recursive item, to know which of the fields were used + // by the parent. + // We need to disable warning or raising errors for unknown fields + // in that case to avoid false positives. + let adjust_options = if placements.iter().any(|f| f.flatten) { + quote!(let options = options.flatten();) + } else { + quote!() + }; + + let field_names = quote!( + &[ #( #field_names, )* ] + ); + + let placements = placements + .into_iter() + .map(|f| f.from_dynamic(&literal)) + .collect::>(); + + let bound = parse_quote!(wezterm_dynamic::FromDynamic); + let bounded_where_clause = bound::where_clause_with_bound(&input.generics, bound); + + let obj = if needs_default { + quote!( + Ok(Self { + #( + #placements + )* + .. Self::default() + }) + ) + } else { + quote!( + Ok(Self { + #( + #placements + )* + }) + ) + }; + + let from_dynamic = match info.try_from { + Some(try_from) => { + quote!( + use std::convert::TryFrom; + let target = <#try_from>::from_dynamic(value, options)?; + <#ident>::try_from(target).map_err(|e| wezterm_dynamic::Error::Message(format!("{:#}", e))) + ) + } + None => { + quote!( + match value { + Value::Object(obj) => { + wezterm_dynamic::Error::raise_unknown_fields(options, #literal, &obj, Self::possible_field_names())?; + #obj + } + other => Err(wezterm_dynamic::Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: #literal + }), + } + ) + } + }; + + let tokens = quote! { + #[allow(non_upper_case_globals)] + const #dummy: () = { + impl #impl_generics wezterm_dynamic::FromDynamic for #ident #ty_generics #bounded_where_clause { + fn from_dynamic(value: &wezterm_dynamic::Value, options: wezterm_dynamic::FromDynamicOptions) -> std::result::Result { + use wezterm_dynamic::{Value, BorrowedKey, ObjectKeyTrait}; + #adjust_options + #from_dynamic + } + + } + impl #impl_generics #ident #ty_generics #bounded_where_clause { + pub const fn possible_field_names() -> &'static [&'static str] { + #field_names + } + } + }; + }; + + if info.debug { + eprintln!("{}", tokens); + } + Ok(tokens) +} + +fn derive_enum(input: &DeriveInput, enumeration: &DataEnum) -> Result { + if input.generics.lt_token.is_some() || input.generics.where_clause.is_some() { + return Err(Error::new( + Span::call_site(), + "Enums with generics are not supported", + )); + } + let info = attr::container_info(&input.attrs)?; + + let ident = &input.ident; + let literal = ident.to_string(); + let dummy = Ident::new( + &format!("_IMPL_FROMDYNAMIC_FOR_{}", ident), + Span::call_site(), + ); + + let variant_names = enumeration + .variants + .iter() + .map(|variant| variant.ident.to_string()) + .collect::>(); + + let from_dynamic = match info.try_from { + Some(try_from) => { + quote!( + use std::convert::TryFrom; + let target = <#try_from>::from_dynamic(value, options)?; + <#ident>::try_from(target).map_err(|e| wezterm_dynamic::Error::Message(format!("{:#}", e))) + ) + } + None => { + let units = enumeration + .variants + .iter() + .filter_map(|variant| match &variant.fields { + Fields::Unit => { + let ident = &variant.ident; + let literal = ident.to_string(); + Some(quote!( + #literal => { + return Ok(Self::#ident); + } + )) + } + _ => None, + }) + .collect::>(); + + let variants = enumeration.variants.iter().map(|variant| { + let ident = &variant.ident; + let literal = ident.to_string(); + + match &variant.fields { + Fields::Unit => { + // Already handled separately + quote!() + } + Fields::Named(fields) => { + let var_fields = fields + .named + .iter() + .map(|f| { + let ident = f.ident.as_ref().unwrap(); + let name = ident.to_string(); + let ty = &f.ty; + quote!( + #ident: <#ty>::from_dynamic( + obj.get_by_str(#name) + .ok_or_else(|| wezterm_dynamic::Error::ErrorInField { + type_name: #literal, + field_name: #name, + error: "missing field".to_string(), + })?, + options + )?, + ) + }) + .collect::>(); + + quote!( + #literal => { + match value { + Value::Object(obj) => { + Ok(Self::#ident { + #( #var_fields )* + }) + } + other => return Err(wezterm_dynamic::Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "Object", + }), + } + } + ) + } + Fields::Unnamed(fields) => { + if fields.unnamed.len() == 1 { + let ty = fields.unnamed.iter().map(|f| &f.ty).next().unwrap(); + quote!( + #literal => { + Ok(Self::#ident(<#ty>::from_dynamic(value, options)?)) + } + ) + } else { + let var_fields = fields + .unnamed + .iter() + .enumerate() + .map(|(idx, f)| { + let ty = &f.ty; + quote!( + <#ty>::from_dynamic( + arr.get(#idx) + .ok_or_else(|| wezterm_dynamic::Error::Message( + format!("missing idx {} of enum struct {}", #idx, #literal)))?, + options + )?, + ) + }) + .collect::>(); + quote!( + #literal => { + match value { + Value::Array(arr) => { + Ok(Self::#ident ( + #( #var_fields )* + )) + } + other => return Err(wezterm_dynamic::Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "Array", + }), + } + } + ) + } + } + } + }).collect::>(); + + quote!( + match value { + Value::String(s) => { + match s.as_str() { + #( #units )* + _ => Err(wezterm_dynamic::Error::InvalidVariantForType { + variant_name: s.clone(), + type_name: #literal, + possible: #ident::variants(), + }) + } + } + Value::Object(place) => { + if place.len() == 1 { + let (name, value) : (&Value, &Value) = place.iter().next().unwrap(); + + match name { + Value::String(name) => { + match name.as_str() { + #( #variants )* + _ => Err(wezterm_dynamic::Error::InvalidVariantForType { + variant_name: name.to_string(), + type_name: #literal, + possible: #ident::variants(), + }) + } + } + _ => Err(wezterm_dynamic::Error::InvalidVariantForType { + variant_name: name.variant_name().to_string(), + type_name: #literal, + possible: #ident::variants(), + }) + } + } else { + Err(wezterm_dynamic::Error::IncorrectNumberOfEnumKeys { + type_name: #literal, + num_keys: place.len(), + }) + } + } + other => Err(wezterm_dynamic::Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: #literal + }), + } + ) + } + }; + + let tokens = quote! { + #[allow(non_upper_case_globals)] + const #dummy: () = { + impl wezterm_dynamic::FromDynamic for #ident { + fn from_dynamic(value: &wezterm_dynamic::Value, options: wezterm_dynamic::FromDynamicOptions) -> std::result::Result { + use wezterm_dynamic::{Value, BorrowedKey, ObjectKeyTrait}; + #from_dynamic + } + } + + impl #ident { + fn variants() -> &'static [&'static str] { + &[ + #( #variant_names, )* + ] + } + } + }; + }; + + if info.debug { + eprintln!("{}", tokens); + } + Ok(tokens) +} diff --git a/wezterm-dynamic/derive/src/lib.rs b/wezterm-dynamic/derive/src/lib.rs new file mode 100644 index 000000000..f7f2c057e --- /dev/null +++ b/wezterm-dynamic/derive/src/lib.rs @@ -0,0 +1,21 @@ +use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput}; + +mod attr; +mod bound; +mod fromdynamic; +mod todynamic; + +#[proc_macro_derive(ToDynamic, attributes(dynamic))] +pub fn derive_todynamic(input: TokenStream) -> TokenStream { + todynamic::derive(parse_macro_input!(input as DeriveInput)) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +#[proc_macro_derive(FromDynamic, attributes(dynamic))] +pub fn derive_fromdynamic(input: TokenStream) -> TokenStream { + fromdynamic::derive(parse_macro_input!(input as DeriveInput)) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/wezterm-dynamic/derive/src/todynamic.rs b/wezterm-dynamic/derive/src/todynamic.rs new file mode 100644 index 000000000..676aac63b --- /dev/null +++ b/wezterm-dynamic/derive/src/todynamic.rs @@ -0,0 +1,231 @@ +use crate::{attr, bound}; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{ + parse_quote, Data, DataEnum, DataStruct, DeriveInput, Error, Fields, FieldsNamed, Ident, Result, +}; + +pub fn derive(input: DeriveInput) -> Result { + match &input.data { + Data::Struct(DataStruct { + fields: Fields::Named(fields), + .. + }) => derive_struct(&input, fields), + Data::Struct(_) => Err(Error::new( + Span::call_site(), + "currently only structs with named fields are supported", + )), + Data::Enum(enumeration) => derive_enum(&input, enumeration), + Data::Union(_) => Err(Error::new( + Span::call_site(), + "currently only structs and enums are supported by this derive", + )), + } +} + +fn derive_struct(input: &DeriveInput, fields: &FieldsNamed) -> Result { + let ident = &input.ident; + let info = attr::container_info(&input.attrs)?; + let (impl_generics, ty_generics, _where_clause) = input.generics.split_for_impl(); + let dummy = Ident::new( + &format!("_IMPL_PLACEDYNAMIC_FOR_{}", ident), + Span::call_site(), + ); + + let placements = fields + .named + .iter() + .map(attr::field_info) + .collect::>>()?; + let placements = placements + .into_iter() + .map(|f| f.to_dynamic()) + .collect::>(); + + let bound = parse_quote!(wezterm_dynamic::PlaceDynamic); + let bounded_where_clause = bound::where_clause_with_bound(&input.generics, bound); + + let tokens = match info.into { + Some(into) => { + quote!( + + #[allow(non_upper_case_globals)] + const #dummy: () = { + + impl #impl_generics wezterm_dynamic::ToDynamic for #ident #ty_generics #bounded_where_clause { + fn to_dynamic(&self) -> wezterm_dynamic::Value { + let target: #into = self.into(); + target.to_dynamic() + } + } + }; + ) + } + None => { + quote!( + #[allow(non_upper_case_globals)] + const #dummy: () = { + impl #impl_generics wezterm_dynamic::PlaceDynamic for #ident #ty_generics #bounded_where_clause { + fn place_dynamic(&self, place: &mut wezterm_dynamic::Object) { + #( + #placements + )* + } + } + + impl #impl_generics wezterm_dynamic::ToDynamic for #ident #ty_generics #bounded_where_clause { + fn to_dynamic(&self) -> wezterm_dynamic::Value { + use wezterm_dynamic::PlaceDynamic; + + let mut object = wezterm_dynamic::Object::default(); + self.place_dynamic(&mut object); + wezterm_dynamic::Value::Object(object) + } + } + }; + ) + } + }; + + if info.debug { + eprintln!("{}", tokens); + } + Ok(tokens) +} + +fn derive_enum(input: &DeriveInput, enumeration: &DataEnum) -> Result { + if input.generics.lt_token.is_some() || input.generics.where_clause.is_some() { + return Err(Error::new( + Span::call_site(), + "Enums with generics are not supported", + )); + } + + let ident = &input.ident; + let dummy = Ident::new(&format!("_IMPL_TODYNAMIC_FOR_{}", ident), Span::call_site()); + let info = attr::container_info(&input.attrs)?; + + let tokens = match info.into { + Some(into) => { + quote! { + #[allow(non_upper_case_globals)] + const #dummy: () = { + impl wezterm_dynamic::ToDynamic for #ident { + fn to_dynamic(&self) -> wezterm_dynamic::Value { + let target : #into = self.into(); + target.to_dynamic() + } + } + }; + } + } + None => { + let variants = enumeration.variants + .iter() + .map(|variant| { + let ident = &variant.ident; + let literal = ident.to_string(); + match &variant.fields { + Fields::Unit => Ok(quote!( + Self::#ident => Value::String(#literal.to_string()), + )), + Fields::Named(fields) => { + let var_fields = fields + .named + .iter() + .map(|f| f.ident.as_ref().unwrap()) + .collect::>(); + let placements = fields + .named + .iter() + .map(|f| { + let ident = f.ident.as_ref().unwrap(); + let name = ident.to_string(); + quote!( + place.insert(#name.to_dynamic(), #ident.to_dynamic()); + ) + }) + .collect::>(); + + Ok(quote!( + Self::#ident { #( #var_fields, )* } => { + let mut place = wezterm_dynamic::Object::default(); + + #( #placements )* + + let mut obj = wezterm_dynamic::Object::default(); + obj.insert(#literal.to_dynamic(), Value::Object(place)); + Value::Object(obj) + } + )) + } + Fields::Unnamed(fields) => { + let var_fields = fields + .unnamed + .iter() + .enumerate() + .map(|(idx, _f)| Ident::new(&format!("f{}", idx), Span::call_site())) + .collect::>(); + + let hint = var_fields.len(); + + if hint == 1 { + Ok(quote!( + Self::#ident(f) => { + let mut obj = wezterm_dynamic::Object::default(); + obj.insert(#literal.to_dynamic(), f.to_dynamic()); + Value::Object(obj) + } + )) + } else { + let placements = fields + .unnamed + .iter() + .zip(var_fields.iter()) + .map(|(_f, ident)| { + quote!( + place.push(#ident.to_dynamic()); + ) + }) + .collect::>(); + + Ok(quote!( + Self::#ident ( #( #var_fields, )* ) => { + let mut place = Vec::with_capacity(#hint); + + #( #placements )* + + let mut obj = wezterm_dynamic::Object::default(); + obj.insert(#literal.to_dynamic(), Value::Array(place.into())); + Value::Object(obj) + } + )) + } + } + } + }) + .collect::>>()?; + + quote! { + #[allow(non_upper_case_globals)] + const #dummy: () = { + impl wezterm_dynamic::ToDynamic for #ident { + fn to_dynamic(&self) -> wezterm_dynamic::Value { + use wezterm_dynamic::Value; + match self { + #( + #variants + )* + } + } + } + }; + } + } + }; + + if info.debug { + eprintln!("{}", tokens); + } + Ok(tokens) +} diff --git a/wezterm-dynamic/src/array.rs b/wezterm-dynamic/src/array.rs new file mode 100644 index 000000000..a6c490186 --- /dev/null +++ b/wezterm-dynamic/src/array.rs @@ -0,0 +1,98 @@ +use crate::Value; +use core::iter::FromIterator; +use core::ops::{Deref, DerefMut}; +use std::cmp::Ordering; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct Array { + inner: Vec, +} + +impl Ord for Array { + fn cmp(&self, other: &Self) -> Ordering { + let self_ptr = self as *const Self; + let other_ptr = other as *const Self; + self_ptr.cmp(&other_ptr) + } +} + +impl PartialOrd for Array { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From> for Array { + fn from(inner: Vec) -> Self { + Self { inner } + } +} + +impl Drop for Array { + fn drop(&mut self) { + self.inner.drain(..).for_each(crate::drop::safely); + } +} + +fn take(array: Array) -> Vec { + let array = core::mem::ManuallyDrop::new(array); + unsafe { core::ptr::read(&array.inner) } +} + +impl Array { + pub fn new() -> Self { + Array { inner: Vec::new() } + } +} + +impl Deref for Array { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Array { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl IntoIterator for Array { + type Item = Value; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + take(self).into_iter() + } +} + +impl<'a> IntoIterator for &'a Array { + type Item = &'a Value; + type IntoIter = <&'a Vec as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut Array { + type Item = &'a mut Value; + type IntoIter = <&'a mut Vec as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl FromIterator for Array { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + Array { + inner: Vec::from_iter(iter), + } + } +} diff --git a/wezterm-dynamic/src/drop.rs b/wezterm-dynamic/src/drop.rs new file mode 100644 index 000000000..f68464f0a --- /dev/null +++ b/wezterm-dynamic/src/drop.rs @@ -0,0 +1,30 @@ +use crate::Value; + +/// Non-recursive drop implementation. +/// This is taken from dtolnay's miniserde library +/// and is reproduced here under the terms of its +/// MIT license +pub fn safely(value: Value) { + match value { + Value::Array(_) | Value::Object(_) => {} + _ => return, + } + + let mut stack = Vec::new(); + stack.push(value); + while let Some(value) = stack.pop() { + match value { + Value::Array(vec) => { + for child in vec { + stack.push(child); + } + } + Value::Object(map) => { + for (_, child) in map { + stack.push(child); + } + } + _ => {} + } + } +} diff --git a/wezterm-dynamic/src/error.rs b/wezterm-dynamic/src/error.rs new file mode 100644 index 000000000..a82f03dfa --- /dev/null +++ b/wezterm-dynamic/src/error.rs @@ -0,0 +1,181 @@ +use crate::fromdynamic::{FromDynamicOptions, UnknownFieldAction}; +use crate::value::Value; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("`{}` is not a valid {} variant. {}", .variant_name, .type_name, Self::possible_matches(.variant_name, &.possible))] + InvalidVariantForType { + variant_name: String, + type_name: &'static str, + possible: &'static [&'static str], + }, + #[error("`{}` is not a valid {} field. {}", .field_name, .type_name, Self::possible_matches(.field_name, &.possible))] + UnknownFieldForStruct { + field_name: String, + type_name: &'static str, + possible: &'static [&'static str], + }, + #[error("{}", .0)] + Message(String), + #[error("Cannot coerce vec of size {} to array of size {}", .vec_size, .array_size)] + ArraySizeMismatch { vec_size: usize, array_size: usize }, + #[error("Cannot convert `{}` to `{}`", .source_type, .dest_type)] + NoConversion { + source_type: String, + dest_type: &'static str, + }, + #[error("Expected char to be a string with a single character")] + CharFromWrongSizedString, + #[error("Expected a valid `{}` variant name as single key in object, but there are {} keys", .type_name, .num_keys)] + IncorrectNumberOfEnumKeys { + type_name: &'static str, + num_keys: usize, + }, + #[error("Error in {}::{}: {:#}", .type_name, .field_name, .error)] + ErrorInField { + type_name: &'static str, + field_name: &'static str, + error: String, + }, + #[error("`{}` is not a valid type to use as a field name in `{}`", .key_type, .type_name)] + InvalidFieldType { + type_name: &'static str, + key_type: String, + }, +} + +impl Error { + fn compute_unknown_fields( + type_name: &'static str, + object: &crate::Object, + possible: &'static [&'static str], + ) -> Vec { + let mut errors = vec![]; + + for key in object.keys() { + match key { + Value::String(s) => { + if !possible.contains(&s.as_str()) { + errors.push(Self::UnknownFieldForStruct { + field_name: s.to_string(), + type_name, + possible: possible.clone(), + }); + } + } + other => { + errors.push(Self::InvalidFieldType { + type_name, + key_type: other.variant_name().to_string(), + }); + } + } + } + + errors + } + + pub fn raise_unknown_fields( + options: FromDynamicOptions, + type_name: &'static str, + object: &crate::Object, + possible: &'static [&'static str], + ) -> Result<(), Self> { + if options.unknown_fields == UnknownFieldAction::Ignore { + return Ok(()); + } + + let errors = Self::compute_unknown_fields(type_name, object, possible); + if errors.is_empty() { + return Ok(()); + } + + let show_warning = options.unknown_fields == UnknownFieldAction::Warn || errors.len() > 1; + + if show_warning { + for err in &errors { + log::warn!("{:#}", err); + } + } + + if options.unknown_fields == UnknownFieldAction::Deny { + for err in errors { + return Err(err); + } + } + + Ok(()) + } + + fn possible_matches(used: &str, possible: &'static [&'static str]) -> String { + // Produce similar field name list + let mut candidates: Vec<(f64, &str)> = possible + .iter() + .map(|&name| (strsim::jaro_winkler(used, name), name)) + .filter(|(confidence, _)| *confidence > 0.8) + .collect(); + candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); + let suggestions: Vec<&str> = candidates.into_iter().map(|(_, name)| name).collect(); + + // Filter the suggestions out of the allowed field names + // and sort what remains. + let mut fields: Vec<&str> = possible + .iter() + .filter(|&name| !suggestions.iter().any(|candidate| candidate == name)) + .copied() + .collect(); + fields.sort_unstable(); + + let mut message = String::new(); + + match suggestions.len() { + 0 => {} + 1 => message.push_str(&format!("Did you mean `{}`?", suggestions[0])), + _ => { + message.push_str("Did you mean one of "); + for (idx, candidate) in suggestions.iter().enumerate() { + if idx > 0 { + message.push_str(", "); + } + message.push('`'); + message.push_str(candidate); + message.push('`'); + } + message.push('?'); + } + } + if !fields.is_empty() { + if suggestions.is_empty() { + message.push_str("Possible items are "); + } else { + message.push_str(" Other possible items are "); + } + let limit = 5; + for (idx, candidate) in fields.iter().enumerate() { + if idx > 0 { + message.push_str(", "); + } + message.push('`'); + message.push_str(candidate); + message.push('`'); + + if idx > limit { + break; + } + } + if fields.len() > limit { + message.push_str(&format!(" and {} others", fields.len() - limit)); + } + message.push('.'); + } + + message + } +} + +impl From for Error { + fn from(s: String) -> Error { + Error::Message(s) + } +} diff --git a/wezterm-dynamic/src/fromdynamic.rs b/wezterm-dynamic/src/fromdynamic.rs new file mode 100644 index 000000000..c8b31589e --- /dev/null +++ b/wezterm-dynamic/src/fromdynamic.rs @@ -0,0 +1,259 @@ +use crate::error::Error; +use crate::value::Value; +use ordered_float::OrderedFloat; +use std::collections::HashMap; +use std::convert::TryInto; +use std::hash::Hash; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum UnknownFieldAction { + Ignore, + Warn, + Deny, +} + +impl Default for UnknownFieldAction { + fn default() -> UnknownFieldAction { + UnknownFieldAction::Warn + } +} + +#[derive(Copy, Clone, Debug, Default)] +pub struct FromDynamicOptions { + pub unknown_fields: UnknownFieldAction, +} + +impl FromDynamicOptions { + pub fn flatten(self) -> Self { + Self { + unknown_fields: UnknownFieldAction::Ignore, + ..self + } + } +} + +pub trait FromDynamic { + fn from_dynamic(value: &Value, options: FromDynamicOptions) -> Result + where + Self: Sized; +} + +impl FromDynamic for Value { + fn from_dynamic(value: &Value, _options: FromDynamicOptions) -> Result { + Ok(value.clone()) + } +} + +impl FromDynamic for ordered_float::NotNan { + fn from_dynamic(value: &Value, options: FromDynamicOptions) -> Result { + let f = f64::from_dynamic(value, options)?; + Ok(ordered_float::NotNan::new(f).map_err(|e| Error::Message(e.to_string()))?) + } +} + +impl FromDynamic for std::time::Duration { + fn from_dynamic(value: &Value, options: FromDynamicOptions) -> Result { + let f = f64::from_dynamic(value, options)?; + Ok(std::time::Duration::from_secs_f64(f)) + } +} + +impl FromDynamic for Box { + fn from_dynamic(value: &Value, options: FromDynamicOptions) -> Result { + let value = T::from_dynamic(value, options)?; + Ok(Box::new(value)) + } +} + +impl FromDynamic for std::sync::Arc { + fn from_dynamic(value: &Value, options: FromDynamicOptions) -> Result { + let value = T::from_dynamic(value, options)?; + Ok(std::sync::Arc::new(value)) + } +} + +impl FromDynamic for Option { + fn from_dynamic(value: &Value, options: FromDynamicOptions) -> Result { + match value { + Value::Null => Ok(None), + value => Ok(Some(T::from_dynamic(value, options)?)), + } + } +} + +impl FromDynamic for [T; N] { + fn from_dynamic(value: &Value, options: FromDynamicOptions) -> Result { + match value { + Value::Array(arr) => { + let v = arr + .iter() + .map(|v| T::from_dynamic(v, options)) + .collect::, Error>>()?; + v.try_into().map_err(|v: Vec| Error::ArraySizeMismatch { + vec_size: v.len(), + array_size: N, + }) + } + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "array", + }), + } + } +} + +impl FromDynamic for HashMap { + fn from_dynamic(value: &Value, options: FromDynamicOptions) -> Result { + match value { + Value::Object(obj) => { + let mut map = HashMap::with_capacity(obj.len()); + for (k, v) in obj.iter() { + map.insert(K::from_dynamic(k, options)?, T::from_dynamic(v, options)?); + } + Ok(map) + } + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "HashMap", + }), + } + } +} + +impl FromDynamic for Vec { + fn from_dynamic(value: &Value, options: FromDynamicOptions) -> Result { + match value { + Value::Array(arr) => Ok(arr + .iter() + .map(|v| T::from_dynamic(v, options)) + .collect::, Error>>()?), + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "Vec", + }), + } + } +} + +impl FromDynamic for () { + fn from_dynamic(value: &Value, _options: FromDynamicOptions) -> Result { + match value { + Value::Null => Ok(()), + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "()", + }), + } + } +} + +impl FromDynamic for bool { + fn from_dynamic(value: &Value, _options: FromDynamicOptions) -> Result { + match value { + Value::Bool(b) => Ok(*b), + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "bool", + }), + } + } +} + +impl FromDynamic for std::path::PathBuf { + fn from_dynamic(value: &Value, _options: FromDynamicOptions) -> Result { + match value { + Value::String(s) => Ok(s.into()), + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "PathBuf", + }), + } + } +} + +impl FromDynamic for char { + fn from_dynamic(value: &Value, _options: FromDynamicOptions) -> Result { + match value { + Value::String(s) => { + let mut iter = s.chars(); + let c = iter.next().ok_or_else(|| Error::CharFromWrongSizedString)?; + if iter.next().is_some() { + Err(Error::CharFromWrongSizedString) + } else { + Ok(c) + } + } + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "char", + }), + } + } +} + +impl FromDynamic for String { + fn from_dynamic(value: &Value, _options: FromDynamicOptions) -> Result { + match value { + Value::String(s) => Ok(s.to_string()), + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "String", + }), + } + } +} + +macro_rules! int { + ($($ty:ty),* $(,)?) => { + $( +impl FromDynamic for $ty { + fn from_dynamic(value: &Value, _options: FromDynamicOptions) -> Result { + match value { + Value::I64(n) => match (*n).try_into() { + Ok(n) => Ok(n), + Err(err) => Err(Error::Message(err.to_string())), + }, + Value::U64(n) => match (*n).try_into() { + Ok(n) => Ok(n), + Err(err) => Err(Error::Message(err.to_string())), + }, + other => Err(Error::NoConversion{ + source_type:other.variant_name().to_string(), + dest_type: stringify!($ty), + }) + } + } +} + )* + } +} + +int!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize); + +impl FromDynamic for f32 { + fn from_dynamic(value: &Value, _options: FromDynamicOptions) -> Result { + match value { + Value::F64(OrderedFloat(n)) => Ok((*n) as f32), + Value::I64(n) => Ok((*n) as f32), + Value::U64(n) => Ok((*n) as f32), + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "f32", + }), + } + } +} + +impl FromDynamic for f64 { + fn from_dynamic(value: &Value, _options: FromDynamicOptions) -> Result { + match value { + Value::F64(OrderedFloat(n)) => Ok(*n), + Value::I64(n) => Ok((*n) as f64), + Value::U64(n) => Ok((*n) as f64), + other => Err(Error::NoConversion { + source_type: other.variant_name().to_string(), + dest_type: "f64", + }), + } + } +} diff --git a/wezterm-dynamic/src/lib.rs b/wezterm-dynamic/src/lib.rs new file mode 100644 index 000000000..9e6876661 --- /dev/null +++ b/wezterm-dynamic/src/lib.rs @@ -0,0 +1,18 @@ +//! Types for representing Rust types in a more dynamic form +//! that is similar to JSON or Lua values. + +mod array; +mod drop; +mod error; +mod fromdynamic; +mod object; +mod todynamic; +mod value; + +pub use array::Array; +pub use error::Error; +pub use fromdynamic::{FromDynamic, FromDynamicOptions, UnknownFieldAction}; +pub use object::{BorrowedKey, Object, ObjectKeyTrait}; +pub use todynamic::{PlaceDynamic, ToDynamic}; +pub use value::Value; +pub use wezterm_dynamic_derive::{FromDynamic, ToDynamic}; diff --git a/wezterm-dynamic/src/map.rs b/wezterm-dynamic/src/map.rs new file mode 100644 index 000000000..4e0a1a6c9 --- /dev/null +++ b/wezterm-dynamic/src/map.rs @@ -0,0 +1,94 @@ +use crate::Value; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct Map { + inner: BTreeMap, +} + +impl Ord for Map { + fn cmp(&self, other: &Self) -> Ordering { + let self_ptr = self as *const Self; + let other_ptr = other as *const Self; + self_ptr.cmp(&other_ptr) + } +} + +impl PartialOrd for Map { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Drop for Map { + fn drop(&mut self) { + for (_, child) in std::mem::replace(&mut self.inner, BTreeMap::new()) { + crate::drop::safely(child); + } + } +} + +impl From> for Map { + fn from(inner: BTreeMap) -> Self { + Self { inner } + } +} + +impl Deref for Map { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Map { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +fn take(object: Map) -> BTreeMap { + let object = core::mem::ManuallyDrop::new(object); + unsafe { core::ptr::read(&object.inner) } +} + +impl IntoIterator for Map { + type Item = (Value, Value); + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + take(self).into_iter() + } +} + +impl<'a> IntoIterator for &'a Map { + type Item = (&'a Value, &'a Value); + type IntoIter = <&'a BTreeMap as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut Map { + type Item = (&'a Value, &'a mut Value); + type IntoIter = <&'a mut BTreeMap as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl FromIterator<(Value, Value)> for Map { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + Map { + inner: BTreeMap::from_iter(iter), + } + } +} diff --git a/wezterm-dynamic/src/object.rs b/wezterm-dynamic/src/object.rs new file mode 100644 index 000000000..798c978f8 --- /dev/null +++ b/wezterm-dynamic/src/object.rs @@ -0,0 +1,163 @@ +use crate::Value; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::ops::{Deref, DerefMut}; + +/// We'd like to avoid allocating when resolving struct fields, +/// so this is the borrowed version of Value. +/// It's a bit involved to make this work; more details can be +/// found in the excellent guide here: +/// +#[derive(Copy, Clone, Debug, PartialEq, Hash, Eq, Ord, PartialOrd)] +pub enum BorrowedKey<'a> { + Value(&'a Value), + Str(&'a str), +} + +pub trait ObjectKeyTrait { + fn key<'k>(&'k self) -> BorrowedKey<'k>; +} + +impl ObjectKeyTrait for Value { + fn key<'k>(&'k self) -> BorrowedKey<'k> { + match self { + Value::String(s) => BorrowedKey::Str(s.as_str()), + v => BorrowedKey::Value(v), + } + } +} + +impl<'a> ObjectKeyTrait for BorrowedKey<'a> { + fn key<'k>(&'k self) -> BorrowedKey<'k> { + *self + } +} + +impl<'a> std::borrow::Borrow for Value { + fn borrow(&self) -> &(dyn ObjectKeyTrait + 'a) { + self + } +} + +impl<'a> PartialEq for (dyn ObjectKeyTrait + 'a) { + fn eq(&self, other: &Self) -> bool { + self.key().eq(&other.key()) + } +} + +impl<'a> Eq for (dyn ObjectKeyTrait + 'a) {} + +impl<'a> PartialOrd for (dyn ObjectKeyTrait + 'a) { + fn partial_cmp(&self, other: &Self) -> Option { + self.key().partial_cmp(&other.key()) + } +} + +impl<'a> Ord for (dyn ObjectKeyTrait + 'a) { + fn cmp(&self, other: &Self) -> Ordering { + self.key().cmp(&other.key()) + } +} + +impl<'a> std::hash::Hash for (dyn ObjectKeyTrait + 'a) { + fn hash(&self, state: &mut H) { + self.key().hash(state) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct Object { + inner: BTreeMap, +} + +impl Object { + pub fn get_by_str(&self, field_name: &str) -> Option<&Value> { + self.inner + .get(&BorrowedKey::Str(field_name) as &dyn ObjectKeyTrait) + } +} + +impl Ord for Object { + fn cmp(&self, other: &Self) -> Ordering { + let self_ptr = self as *const Self; + let other_ptr = other as *const Self; + self_ptr.cmp(&other_ptr) + } +} + +impl PartialOrd for Object { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Drop for Object { + fn drop(&mut self) { + for (_, child) in std::mem::replace(&mut self.inner, BTreeMap::new()) { + crate::drop::safely(child); + } + } +} + +impl From> for Object { + fn from(inner: BTreeMap) -> Self { + Self { inner } + } +} + +impl Deref for Object { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Object { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +fn take(object: Object) -> BTreeMap { + let object = core::mem::ManuallyDrop::new(object); + unsafe { core::ptr::read(&object.inner) } +} + +impl IntoIterator for Object { + type Item = (Value, Value); + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + take(self).into_iter() + } +} + +impl<'a> IntoIterator for &'a Object { + type Item = (&'a Value, &'a Value); + type IntoIter = <&'a BTreeMap as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut Object { + type Item = (&'a Value, &'a mut Value); + type IntoIter = <&'a mut BTreeMap as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl FromIterator<(Value, Value)> for Object { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + Object { + inner: BTreeMap::from_iter(iter), + } + } +} diff --git a/wezterm-dynamic/src/todynamic.rs b/wezterm-dynamic/src/todynamic.rs new file mode 100644 index 000000000..c40836ab2 --- /dev/null +++ b/wezterm-dynamic/src/todynamic.rs @@ -0,0 +1,192 @@ +use crate::object::Object; +use crate::value::Value; +use ordered_float::OrderedFloat; +use std::collections::{BTreeMap, HashMap}; + +pub trait ToDynamic { + fn to_dynamic(&self) -> Value; +} + +pub trait PlaceDynamic { + fn place_dynamic(&self, place: &mut Object); +} + +impl ToDynamic for Value { + fn to_dynamic(&self) -> Value { + self.clone() + } +} + +impl ToDynamic for ordered_float::NotNan { + fn to_dynamic(&self) -> Value { + Value::F64(OrderedFloat::from(**self)) + } +} + +impl ToDynamic for std::time::Duration { + fn to_dynamic(&self) -> Value { + Value::F64(OrderedFloat(self.as_secs_f64())) + } +} + +impl ToDynamic for HashMap { + fn to_dynamic(&self) -> Value { + Value::Object( + self.iter() + .map(|(k, v)| (k.to_dynamic(), v.to_dynamic())) + .collect::>() + .into(), + ) + } +} + +impl ToDynamic for std::sync::Arc { + fn to_dynamic(&self) -> Value { + self.as_ref().to_dynamic() + } +} + +impl ToDynamic for Box { + fn to_dynamic(&self) -> Value { + self.as_ref().to_dynamic() + } +} + +impl ToDynamic for Option { + fn to_dynamic(&self) -> Value { + match self { + None => Value::Null, + Some(t) => t.to_dynamic(), + } + } +} + +impl ToDynamic for [T; N] { + fn to_dynamic(&self) -> Value { + Value::Array( + self.iter() + .map(T::to_dynamic) + .collect::>() + .into(), + ) + } +} + +impl ToDynamic for Vec { + fn to_dynamic(&self) -> Value { + Value::Array( + self.iter() + .map(T::to_dynamic) + .collect::>() + .into(), + ) + } +} + +impl ToDynamic for () { + fn to_dynamic(&self) -> Value { + Value::Null + } +} + +impl ToDynamic for bool { + fn to_dynamic(&self) -> Value { + Value::Bool(*self) + } +} + +impl ToDynamic for str { + fn to_dynamic(&self) -> Value { + Value::String(self.to_string()) + } +} + +impl ToDynamic for std::path::PathBuf { + fn to_dynamic(&self) -> Value { + Value::String(self.to_string_lossy().to_string()) + } +} + +impl ToDynamic for String { + fn to_dynamic(&self) -> Value { + Value::String(self.to_string()) + } +} + +impl ToDynamic for char { + fn to_dynamic(&self) -> Value { + Value::String(self.to_string()) + } +} + +impl ToDynamic for isize { + fn to_dynamic(&self) -> Value { + Value::I64((*self).try_into().unwrap()) + } +} + +impl ToDynamic for i8 { + fn to_dynamic(&self) -> Value { + Value::I64((*self).into()) + } +} + +impl ToDynamic for i16 { + fn to_dynamic(&self) -> Value { + Value::I64((*self).into()) + } +} + +impl ToDynamic for i32 { + fn to_dynamic(&self) -> Value { + Value::I64((*self).into()) + } +} + +impl ToDynamic for i64 { + fn to_dynamic(&self) -> Value { + Value::I64(*self) + } +} + +impl ToDynamic for usize { + fn to_dynamic(&self) -> Value { + Value::U64((*self).try_into().unwrap()) + } +} + +impl ToDynamic for u8 { + fn to_dynamic(&self) -> Value { + Value::U64((*self).into()) + } +} + +impl ToDynamic for u16 { + fn to_dynamic(&self) -> Value { + Value::U64((*self).into()) + } +} + +impl ToDynamic for u32 { + fn to_dynamic(&self) -> Value { + Value::U64((*self).into()) + } +} + +impl ToDynamic for u64 { + fn to_dynamic(&self) -> Value { + Value::U64(*self) + } +} + +impl ToDynamic for f64 { + fn to_dynamic(&self) -> Value { + Value::F64(OrderedFloat(*self)) + } +} + +impl ToDynamic for f32 { + fn to_dynamic(&self) -> Value { + Value::F64(OrderedFloat((*self).into())) + } +} diff --git a/wezterm-dynamic/src/value.rs b/wezterm-dynamic/src/value.rs new file mode 100644 index 000000000..3c1a6a4a4 --- /dev/null +++ b/wezterm-dynamic/src/value.rs @@ -0,0 +1,36 @@ +use crate::array::Array; +use crate::object::Object; +use ordered_float::OrderedFloat; + +#[derive(Clone, Debug, PartialEq, Hash, Eq, Ord, PartialOrd)] +pub enum Value { + Null, + Bool(bool), + String(String), + Array(Array), + Object(Object), + U64(u64), + I64(i64), + F64(OrderedFloat), +} + +impl Default for Value { + fn default() -> Self { + Self::Null + } +} + +impl Value { + pub fn variant_name(&self) -> &str { + match self { + Self::Null => "Null", + Self::Bool(_) => "Bool", + Self::String(_) => "String", + Self::Array(_) => "Array", + Self::Object(_) => "Object", + Self::U64(_) => "U64", + Self::I64(_) => "I64", + Self::F64(_) => "F64", + } + } +} diff --git a/wezterm-dynamic/tests/fromdynamic.rs b/wezterm-dynamic/tests/fromdynamic.rs new file mode 100644 index 000000000..a09a9ae5a --- /dev/null +++ b/wezterm-dynamic/tests/fromdynamic.rs @@ -0,0 +1,290 @@ +use maplit::btreemap; +use ordered_float::OrderedFloat; +use wezterm_dynamic::{FromDynamic, Object, ToDynamic, Value}; + +#[derive(FromDynamic, Debug, PartialEq)] +struct SimpleStruct { + age: u8, +} + +#[test] +fn simple_struct() { + let s = SimpleStruct::from_dynamic( + &Value::Object( + btreemap!( + "age".to_dynamic() => Value::U64(42)) + .into(), + ), + Default::default(), + ) + .unwrap(); + assert_eq!(s, SimpleStruct { age: 42 }); +} + +#[derive(FromDynamic, Debug, PartialEq, Default)] +struct StructWithSkippedField { + #[dynamic(skip)] + admin: bool, + age: u8, +} + +#[test] +fn skipped_field() { + let s = StructWithSkippedField::from_dynamic( + &Value::Object( + btreemap!( + "age".to_dynamic() => Value::U64(42)) + .into(), + ), + Default::default(), + ) + .unwrap(); + assert_eq!( + s, + StructWithSkippedField { + age: 42, + admin: false + } + ); +} + +#[derive(FromDynamic, Debug, PartialEq)] +struct StructWithFlattenedStruct { + top: bool, + #[dynamic(flatten)] + simple: SimpleStruct, +} + +#[test] +fn flattened() { + let s = StructWithFlattenedStruct::from_dynamic( + &Value::Object( + btreemap!( + "top".to_dynamic() =>Value::Bool(true), + "age".to_dynamic() => Value::U64(42)) + .into(), + ), + Default::default(), + ) + .unwrap(); + assert_eq!( + s, + StructWithFlattenedStruct { + top: true, + simple: SimpleStruct { age: 42 }, + } + ); +} + +#[derive(FromDynamic, Debug, PartialEq)] +enum Units { + A, +} + +#[test] +fn unit_variants() { + assert_eq!( + Units::A, + Units::from_dynamic(&Value::String("A".to_string()), Default::default()).unwrap() + ); +} + +#[derive(FromDynamic, Debug, PartialEq)] +enum Named { + A { foo: bool, bar: bool }, + B { bar: bool }, +} + +#[test] +fn named_variants() { + assert_eq!( + Named::A { + foo: true, + bar: false + }, + Named::from_dynamic( + &Value::Object( + btreemap!( + "A".to_dynamic() => Value::Object( + btreemap!( + "foo".to_dynamic() => Value::Bool(true), + "bar".to_dynamic() => Value::Bool(false), + ).into()) + ) + .into() + ), + Default::default() + ) + .unwrap() + ); + assert_eq!( + Named::B { bar: true }, + Named::from_dynamic( + &Value::Object( + btreemap!( + "B".to_dynamic() => Value::Object( + btreemap!( + "bar".to_dynamic() => Value::Bool(true), + ).into()) + ) + .into() + ), + Default::default() + ) + .unwrap() + ); +} + +#[derive(FromDynamic, Debug, PartialEq)] +enum UnNamed { + A(f32, f32, f32, f32), + Single(bool), +} + +#[test] +fn unnamed_variants() { + assert_eq!( + UnNamed::A(0., 1., 2., 3.), + UnNamed::from_dynamic( + &Value::Object( + btreemap!( + "A".to_dynamic() => Value::Array(vec![ + Value::F64(OrderedFloat(0.)), + Value::F64(OrderedFloat(1.)), + Value::F64(OrderedFloat(2.)), + Value::F64(OrderedFloat(3.)), + ].into()), + ) + .into() + ), + Default::default() + ) + .unwrap() + ); + + assert_eq!( + UnNamed::Single(true), + UnNamed::from_dynamic( + &Value::Object( + btreemap!( + "Single".to_dynamic() => Value::Bool(true), + ) + .into() + ), + Default::default() + ) + .unwrap() + ); +} + +#[derive(FromDynamic, Debug, PartialEq)] +struct OptField { + foo: Option, +} + +#[test] +fn optional() { + assert_eq!( + OptField { foo: None }, + OptField::from_dynamic(&Value::Object(Object::default()), Default::default()).unwrap(), + ); + + assert_eq!( + OptField { foo: Some(true) }, + OptField::from_dynamic( + &Value::Object( + btreemap! { + "foo".to_dynamic() => Value::Bool(true), + } + .into() + ), + Default::default() + ) + .unwrap(), + ); +} + +#[derive(FromDynamic, Debug, PartialEq)] +struct Defaults { + #[dynamic(default)] + s: String, + #[dynamic(default = "woot_string")] + w: String, +} + +fn woot_string() -> String { + "woot".to_string() +} + +#[test] +fn defaults() { + assert_eq!( + Defaults { + s: "".to_string(), + w: "woot".to_string() + }, + Defaults::from_dynamic(&Value::Object(Object::default()), Default::default()).unwrap(), + ); +} + +#[derive(FromDynamic, Debug, PartialEq)] +#[dynamic(try_from = "String")] +struct StructInto { + age: u8, +} + +impl TryFrom for StructInto { + type Error = String; + + fn try_from(s: String) -> Result { + if let [label, value] = &s.split(':').collect::>()[..] { + if *label == "age" { + return Ok(StructInto { + age: value + .parse() + .map_err(|e: std::num::ParseIntError| e.to_string())?, + }); + } + } + Err("bad".to_string()) + } +} + +#[test] +fn struct_into() { + assert_eq!( + StructInto { age: 42 }, + StructInto::from_dynamic(&Value::String("age:42".to_string()), Default::default()).unwrap() + ); +} + +#[derive(FromDynamic, Debug, PartialEq)] +#[dynamic(try_from = "String")] +enum EnumInto { + Age(u8), +} + +impl TryFrom for EnumInto { + type Error = String; + + fn try_from(s: String) -> Result { + if let [label, value] = &s.split(':').collect::>()[..] { + if *label == "age" { + return Ok(EnumInto::Age( + value + .parse() + .map_err(|e: std::num::ParseIntError| e.to_string())?, + )); + } + } + Err("bad".to_string()) + } +} + +#[test] +fn enum_into() { + assert_eq!( + EnumInto::Age(42), + EnumInto::from_dynamic(&Value::String("age:42".to_string()), Default::default()).unwrap() + ); +} diff --git a/wezterm-dynamic/tests/todynamic.rs b/wezterm-dynamic/tests/todynamic.rs new file mode 100644 index 000000000..c3124d25d --- /dev/null +++ b/wezterm-dynamic/tests/todynamic.rs @@ -0,0 +1,221 @@ +use maplit::btreemap; +use ordered_float::OrderedFloat; +use wezterm_dynamic::{ToDynamic, Value}; + +#[test] +fn intrinsics() { + assert_eq!(23u8.to_dynamic(), Value::U64(23)); + assert_eq!(23i8.to_dynamic(), Value::I64(23)); + assert_eq!(23f32.to_dynamic(), Value::F64(OrderedFloat(23.))); + assert_eq!("hello".to_dynamic(), Value::String("hello".to_string())); + assert_eq!(false.to_dynamic(), Value::Bool(false)); +} + +#[derive(ToDynamic, Debug, PartialEq)] +struct SimpleStruct { + age: u8, +} + +#[test] +fn simple_struct() { + assert_eq!( + SimpleStruct { age: 42 }.to_dynamic(), + Value::Object( + btreemap!( + "age".to_dynamic() => Value::U64(42)) + .into() + ) + ); +} + +#[derive(ToDynamic, Debug, PartialEq)] +struct SimpleStructWithRenamedField { + #[dynamic(rename = "how_old")] + age: u8, +} + +#[test] +fn simple_struct_with_renamed_field() { + assert_eq!( + SimpleStructWithRenamedField { age: 42 }.to_dynamic(), + Value::Object( + btreemap!( + "how_old".to_dynamic() => Value::U64(42)) + .into() + ) + ); +} + +#[derive(ToDynamic, Debug, PartialEq)] +struct StructWithSkippedField { + #[dynamic(skip)] + admin: bool, + age: u8, +} + +#[test] +fn skipped_field() { + assert_eq!( + StructWithSkippedField { + admin: true, + age: 42 + } + .to_dynamic(), + Value::Object( + btreemap!( + "age".to_dynamic() => Value::U64(42)) + .into() + ) + ); +} + +#[derive(ToDynamic, Debug, PartialEq)] +struct StructWithFlattenedStruct { + top: bool, + #[dynamic(flatten)] + simple: SimpleStruct, +} + +#[test] +fn flattened() { + assert_eq!( + StructWithFlattenedStruct { + top: true, + simple: SimpleStruct { age: 42 } + } + .to_dynamic(), + Value::Object( + btreemap!( + "top".to_dynamic() => Value::Bool(true), + "age".to_dynamic() => Value::U64(42)) + .into() + ) + ); +} + +#[derive(ToDynamic, Debug, PartialEq)] +enum Units { + A, + B, +} + +#[test] +fn unit_variants() { + assert_eq!(Units::A.to_dynamic(), Value::String("A".to_string())); + assert_eq!(Units::B.to_dynamic(), Value::String("B".to_string())); +} + +#[derive(ToDynamic, Debug, PartialEq)] +enum Named { + A { foo: bool, bar: bool }, + B { bar: bool }, +} + +#[test] +fn named_variants() { + assert_eq!( + Named::A { + foo: true, + bar: false + } + .to_dynamic(), + Value::Object( + btreemap!( + "A".to_dynamic() => Value::Object( + btreemap!( + "foo".to_dynamic() => Value::Bool(true), + "bar".to_dynamic() => Value::Bool(false), + ).into()) + ) + .into() + ) + ); + assert_eq!( + Named::B { bar: true }.to_dynamic(), + Value::Object( + btreemap!( + "B".to_dynamic() => Value::Object( + btreemap!( + "bar".to_dynamic() => Value::Bool(true), + ).into()) + ) + .into() + ) + ); +} + +#[derive(ToDynamic, Debug, PartialEq)] +enum UnNamed { + A(f32, f32, f32, f32), + Single(bool), +} + +#[test] +fn unnamed_variants() { + assert_eq!( + UnNamed::A(0., 1., 2., 3.).to_dynamic(), + Value::Object( + btreemap!( + "A".to_dynamic() => Value::Array(vec![ + Value::F64(OrderedFloat(0.)), + Value::F64(OrderedFloat(1.)), + Value::F64(OrderedFloat(2.)), + Value::F64(OrderedFloat(3.)), + ].into()), + ) + .into() + ) + ); + + assert_eq!( + UnNamed::Single(true).to_dynamic(), + Value::Object( + btreemap!( + "Single".to_dynamic() => Value::Bool(true), + ) + .into() + ) + ); +} + +#[derive(ToDynamic, Debug, PartialEq)] +#[dynamic(into = "String")] +struct StructInto { + age: u8, +} + +impl Into for &StructInto { + fn into(self) -> String { + format!("age:{}", self.age) + } +} + +#[test] +fn struct_into() { + assert_eq!( + StructInto { age: 42 }.to_dynamic(), + Value::String("age:42".to_string()) + ); +} + +#[derive(ToDynamic, Debug, PartialEq)] +#[dynamic(into = "String")] +enum EnumInto { + Age(u8), +} + +impl Into for &EnumInto { + fn into(self) -> String { + match self { + EnumInto::Age(age) => format!("age:{}", age), + } + } +} + +#[test] +fn enum_into() { + assert_eq!( + EnumInto::Age(42).to_dynamic(), + Value::String("age:42".to_string()) + ); +}