diff --git a/CHANGES.md b/CHANGES.md index 5a43fd48..0e098e7d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ * `Driver::plains` method to allow responding with plaintext pages * In `css!` macro there is now possibility to reference a class created by another `css!` using `[]` brackets +* Enums nad newtypes support in `AutoJsJson` * Components now accept value without attribute name if the names matches (`color={color}` → `{color}`) ### Changed diff --git a/crates/vertigo-macro/src/jsjson/enums.rs b/crates/vertigo-macro/src/jsjson/enums.rs new file mode 100644 index 00000000..a006913c --- /dev/null +++ b/crates/vertigo-macro/src/jsjson/enums.rs @@ -0,0 +1,200 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{ext::IdentExt, DataEnum, Fields, Ident}; + +// { +// "Somestring": "foobar" +// } +// +// { +// "Point": { "x": 10, "y": "one" } +// } +// +// { +// "Tuple": ["two", 20] +// } +// +// "Nothing" +pub(super) fn impl_js_json_enum(name: &Ident, data: &DataEnum) -> Result { + // Encoding code for every variant + let mut variant_encodes = vec![]; + + // Encoding code for every simple variant (data-less) + let mut variant_string_decodes = vec![]; + + // Envoding code for every compound variant (with data) + let mut variant_object_decodes = vec![]; + + for variant in &data.variants { + let variant_ident = &variant.ident; + let variant_name = &variant.ident.unraw().to_string(); + match &variant.fields { + // Simple variant + // Enum::Variant <-> "Variant" + Fields::Unit => { + variant_encodes.push(quote! { Self::#variant_ident => #variant_name.to_json(), }); + variant_string_decodes.push(quote! { #variant_name => Ok(Self::#variant_ident), }); + } + + // Compound variant with unnamed field(s) (tuple) + // Enum::Variant(...) <-> "Variant": ... + Fields::Unnamed(fields) => { + // Enum::Variant(T) <-> "Variant": T + if fields.unnamed.len() == 1 { + // Encode + variant_encodes.push(quote! { + Self::#variant_ident(value) => { + vertigo::JsJson::Object(::std::collections::HashMap::from([ + ( + #variant_name.to_string(), + value.to_json(), + ), + ])) + } + }); + + // Decode + variant_object_decodes.push(quote! { + if let Some(value) = compound_variant.get_mut(#variant_name) { + return Ok(Self::#variant_ident( + vertigo::JsJsonDeserialize::from_json(ctx.clone(), value.to_owned())? + )) + } + }); + + // Enum::Variant(T1, T2...) <-> "Variant": [T1, T2, ...] + } else { + // Encode + let (field_idents, field_encodes) = + super::tuple_fields::get_encodes(fields.unnamed.iter()); + + variant_encodes.push(quote! { + Self::#variant_ident(#(#field_idents,)*) => { + vertigo::JsJson::Object(::std::collections::HashMap::from([ + ( + #variant_name.to_string(), + vertigo::JsJson::List(vec![ + #(#field_encodes)* + ]) + ), + ])) + } + }); + + // Decode + let fields_number = field_idents.len(); + let field_decodes = super::tuple_fields::get_decodes(field_idents); + + variant_object_decodes.push(quote! { + if let Some(value) = compound_variant.get_mut(#variant_name) { + match value.to_owned() { + vertigo::JsJson::List(fields) => { + if fields.len() != #fields_number { + return Err(ctx.add( + format!("Wrong unmber of fields in tuple for variant {}. Expected {}, got {}", #variant_name, #fields_number, fields.len()) + )); + } + let mut fields_rev = fields.into_iter().rev().collect::>(); + return Ok(Self::#variant_ident ( + #(#field_decodes)* + )) + }, + x => return Err(ctx.add( + format!("Invalid type {} while decoding enum tuple, expected list", x.typename()) + )), + } + } + }); + } + } + + // Compound variant with named field(s) (anonymous struct) + // Enum::Variant { x: X, y: Y, ...) <-> "Variant": { x: X, y: Y, ... } + Fields::Named(fields) => { + // Encode + let field_idents = fields + .named + .iter() + .filter_map(|field| field.ident.clone()) + .collect::>(); + + let field_encodes = field_idents + .iter() + .map(|field_ident| { + let field_name = field_ident.unraw().to_string(); + quote! { + (#field_name.to_string(), #field_ident.to_json()), + } + }) + .collect::>(); + + variant_encodes.push(quote! { + Self::#variant_ident {#(#field_idents,)*} => { + vertigo::JsJson::Object(::std::collections::HashMap::from([ + ( + #variant_name.to_string(), + vertigo::JsJson::Object(::std::collections::HashMap::from([ + #(#field_encodes)* + ])) + ), + ])) + } + }); + + // Decode + let field_decodes = field_idents + .iter() + .map(|field_ident| { + let field_name = field_ident.unraw().to_string(); + quote! { + #field_ident: value.get_property(&ctx, #field_name)?, + } + }) + .collect::>(); + + variant_object_decodes.push(quote! { + if let Some(value) = compound_variant.get_mut(#variant_name) { + return Ok(Self::#variant_ident { + #(#field_decodes)* + }) + } + }); + } + } + } + + let result = quote! { + impl vertigo::JsJsonSerialize for #name { + fn to_json(self) -> vertigo::JsJson { + match self { + #(#variant_encodes)* + } + } + } + + impl vertigo::JsJsonDeserialize for #name { + fn from_json( + ctx: vertigo::JsJsonContext, + json: vertigo::JsJson, + ) -> Result { + match json { + vertigo::JsJson::String(simple_variant) => { + match simple_variant.as_str() { + #(#variant_string_decodes)* + x => Err(ctx.add(format!("Invalid simple variant {x}"))), + } + } + vertigo::JsJson::Object(mut compound_variant) => { + #(#variant_object_decodes)* + Err(ctx.add("Value not matched with any variant".to_string())) + } + x => Err(ctx.add( + format!("Invalid type {} while decoding enum, expected string or object", x.typename()) + )), + } + } + } + }; + + Ok(result.into()) +} diff --git a/crates/vertigo-macro/src/jsjson/mod.rs b/crates/vertigo-macro/src/jsjson/mod.rs new file mode 100644 index 00000000..44b99369 --- /dev/null +++ b/crates/vertigo-macro/src/jsjson/mod.rs @@ -0,0 +1,17 @@ +use proc_macro::TokenStream; +use syn::{Data, DeriveInput}; + +mod enums; +mod newtypes; +mod structs; +mod tuple_fields; + +pub(crate) fn impl_js_json_derive(ast: &DeriveInput) -> Result { + let name = &ast.ident; + + match ast.data { + Data::Struct(ref data) => structs::impl_js_json_struct(name, data), + Data::Enum(ref data) => enums::impl_js_json_enum(name, data), + Data::Union(ref _data) => Err("Unions not supported yet".to_string()), + } +} diff --git a/crates/vertigo-macro/src/jsjson/newtypes.rs b/crates/vertigo-macro/src/jsjson/newtypes.rs new file mode 100644 index 00000000..9b312615 --- /dev/null +++ b/crates/vertigo-macro/src/jsjson/newtypes.rs @@ -0,0 +1,76 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{DataStruct, Ident}; + +pub(super) fn impl_js_json_newtype(name: &Ident, data: &DataStruct) -> Result { + let mut encodes = Vec::new(); + let mut decodes = Vec::new(); + + // Struct(T) <-> T + if data.fields.len() == 1 { + // Encode + encodes.push(quote! { + self.0.to_json() + }); + + // Decode + decodes.push(quote! { + vertigo::JsJsonDeserialize::from_json(ctx, json).map(Self) + }); + + // Struct(T1, T2...) <-> [T1, T2, ...] + } else { + // Encode + let (field_idents, field_encodes) = super::tuple_fields::get_encodes(data.fields.iter()); + + encodes.push(quote! { + let #name (#(#field_idents,)*) = self; + vertigo::JsJson::List(vec![ + #(#field_encodes)* + ]) + }); + + // Decode + let fields_number = field_idents.len(); + let field_decodes = super::tuple_fields::get_decodes(field_idents); + let name_str = name.to_string(); + + decodes.push(quote! { + match json { + vertigo::JsJson::List(fields) => { + if fields.len() != #fields_number { + return Err(ctx.add( + format!("Wrong number of fields in tuple for newtype {}. Expected {}, got {}", #name_str, #fields_number, fields.len()) + )); + } + let mut fields_rev = fields.into_iter().rev().collect::>(); + return Ok(#name( + #(#field_decodes)* + )) + } + x => return Err(ctx.add( + format!("Invalid type {} while decoding newtype tuple, expected list", x.typename()) + )), + } + }); + } + + let result = quote! { + impl vertigo::JsJsonSerialize for #name { + fn to_json(self) -> vertigo::JsJson { + #(#encodes)* + } + } + + impl vertigo::JsJsonDeserialize for #name { + fn from_json( + ctx: vertigo::JsJsonContext, + json: vertigo::JsJson, + ) -> Result { + #(#decodes)* + } + } + }; + + Ok(result.into()) +} diff --git a/crates/vertigo-macro/src/js_json_derive.rs b/crates/vertigo-macro/src/jsjson/structs.rs similarity index 66% rename from crates/vertigo-macro/src/js_json_derive.rs rename to crates/vertigo-macro/src/jsjson/structs.rs index d7b70b68..eac388c4 100644 --- a/crates/vertigo-macro/src/js_json_derive.rs +++ b/crates/vertigo-macro/src/jsjson/structs.rs @@ -1,21 +1,13 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{ext::IdentExt, Data}; - -pub(crate) fn impl_js_json_derive(ast: &syn::DeriveInput) -> Result { - let structure_name = &ast.ident; - - let Data::Struct(ref data) = ast.data else { - return Err(String::from( - "This macro can only be used for the structure", - )); - }; +use syn::{ext::IdentExt, DataStruct, Ident}; +pub(super) fn impl_js_json_struct(name: &Ident, data: &DataStruct) -> Result { let mut field_list = Vec::new(); - for field in data.fields.iter() { + for field in &data.fields { let Some(field_name) = &field.ident else { - return Err(String::from("Problem with specifying the field name")); + return super::newtypes::impl_js_json_newtype(name, data); }; field_list.push(field_name); @@ -36,7 +28,7 @@ pub(crate) fn impl_js_json_derive(ast: &syn::DeriveInput) -> Result vertigo::JsJson { vertigo::JsJson::Object(::std::collections::HashMap::from([ #(#list_to_json)* @@ -44,7 +36,7 @@ pub(crate) fn impl_js_json_derive(ast: &syn::DeriveInput) -> Result Result { Ok(Self { #(#list_from_json)* diff --git a/crates/vertigo-macro/src/jsjson/tuple_fields.rs b/crates/vertigo-macro/src/jsjson/tuple_fields.rs new file mode 100644 index 00000000..69dc1e96 --- /dev/null +++ b/crates/vertigo-macro/src/jsjson/tuple_fields.rs @@ -0,0 +1,35 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{punctuated::Iter, Field, Ident}; + +/// Takes tuple fields and returns (generated fields' names, generated encodes) +pub(super) fn get_encodes(fields_iter: Iter<'_, Field>) -> (Vec, Vec) { + let field_idents = fields_iter + .enumerate() + .map(|(n, field)| Ident::new(&format!("f_{}", n), field.span())) + .collect::>(); + + let field_encodes = field_idents + .iter() + .map(|field_ident| { + quote! { + #field_ident.to_json(), + } + }) + .collect::>(); + + (field_idents, field_encodes) +} + +/// Takes tuple fields and returns generated encodes +pub(super) fn get_decodes(field_idents: Vec) -> Vec { + field_idents + .iter() + .map(|_| { + quote! { + vertigo::JsJsonDeserialize::from_json(ctx.clone(), fields_rev.pop().unwrap())?, + } + }) + .collect::>() +} diff --git a/crates/vertigo-macro/src/lib.rs b/crates/vertigo-macro/src/lib.rs index 85744f0b..58e953d3 100644 --- a/crates/vertigo-macro/src/lib.rs +++ b/crates/vertigo-macro/src/lib.rs @@ -10,7 +10,7 @@ mod component; mod css_parser; mod html_parser; mod include_static; -mod js_json_derive; +mod jsjson; mod main_wrap; mod wasm_path; @@ -80,7 +80,7 @@ pub fn css(input: TokenStream) -> TokenStream { pub fn auto_js_json(input: TokenStream) -> TokenStream { let ast = syn::parse(input).unwrap(); - match js_json_derive::impl_js_json_derive(&ast) { + match jsjson::impl_js_json_derive(&ast) { Ok(result) => result, Err(message) => { emit_error!(Span::call_site(), "{}", message); diff --git a/crates/vertigo/src/driver_module/js_value/serialize.rs b/crates/vertigo/src/driver_module/js_value/serialize.rs index 1c5a847b..c2c138fb 100644 --- a/crates/vertigo/src/driver_module/js_value/serialize.rs +++ b/crates/vertigo/src/driver_module/js_value/serialize.rs @@ -70,7 +70,7 @@ impl JsJsonDeserialize for String { match json { JsJson::String(value) => Ok(value), other => { - let message = ["string expected, received", other.typename()].concat(); + let message = ["string expected, received ", other.typename()].concat(); Err(context.add(message)) } } @@ -88,7 +88,7 @@ impl JsJsonDeserialize for u64 { match json { JsJson::Number(value) => Ok(value as u64), other => { - let message = ["number(u64) expected, received", other.typename()].concat(); + let message = ["number(u64) expected, received ", other.typename()].concat(); Err(context.add(message)) } } @@ -106,7 +106,7 @@ impl JsJsonDeserialize for i64 { match json { JsJson::Number(value) => Ok(value as i64), other => { - let message = ["number(i64) expected, received", other.typename()].concat(); + let message = ["number(i64) expected, received ", other.typename()].concat(); Err(context.add(message)) } } @@ -124,7 +124,7 @@ impl JsJsonDeserialize for u32 { match json { JsJson::Number(value) => Ok(value as u32), other => { - let message = ["number(u32) expected, received", other.typename()].concat(); + let message = ["number(u32) expected, received ", other.typename()].concat(); Err(context.add(message)) } } @@ -142,7 +142,7 @@ impl JsJsonDeserialize for i32 { match json { JsJson::Number(value) => Ok(value as i32), other => { - let message = ["number(i32) expected, received", other.typename()].concat(); + let message = ["number(i32) expected, received ", other.typename()].concat(); Err(context.add(message)) } } @@ -164,7 +164,7 @@ impl JsJsonDeserialize for bool { JsJson::False => Ok(false), JsJson::True => Ok(true), other => { - let message = ["bool expected, received", other.typename()].concat(); + let message = ["bool expected, received ", other.typename()].concat(); Err(context.add(message)) } } @@ -194,7 +194,7 @@ impl JsJsonDeserialize for Vec { let mut list = Vec::new(); let JsJson::List(inner) = json else { - let message = ["List expected, received", json.typename()].concat(); + let message = ["List expected, received ", json.typename()].concat(); return Err(context.add(message)); }; diff --git a/crates/vertigo/src/lib.rs b/crates/vertigo/src/lib.rs index e79e89b0..760b9602 100644 --- a/crates/vertigo/src/lib.rs +++ b/crates/vertigo/src/lib.rs @@ -188,6 +188,8 @@ pub use vertigo_macro::bind_spawn; /// /// Used for fetching and sending objects over the network. /// +/// Enums representation is compatible with serde's "external tagging" which is the default. +/// /// ```rust /// #[derive(vertigo::AutoJsJson)] /// pub struct Post { diff --git a/crates/vertigo/src/tests/autojsjson.rs b/crates/vertigo/src/tests/autojsjson.rs index b548790c..2213e7e1 100644 --- a/crates/vertigo/src/tests/autojsjson.rs +++ b/crates/vertigo/src/tests/autojsjson.rs @@ -1,8 +1,8 @@ use crate::{self as vertigo, JsJsonContext}; -use crate::{AutoJsJson, JsJsonSerialize}; +use crate::{AutoJsJson, JsJsonDeserialize, JsJsonSerialize}; #[test] -fn raw_field_name() { +fn test_raw_field_name() { #[derive(AutoJsJson)] pub struct Test { pub r#type: String, @@ -24,3 +24,103 @@ fn raw_field_name() { assert!(hash_map.contains_key("type")); assert!(hash_map.contains_key("name")); } + +#[test] +fn test_simple_enum() { + #[derive(AutoJsJson, Clone, Debug, PartialEq)] + pub enum Side { + Left, + Right, + } + + let left = Side::Left; + let right = Side::Right; + + let left_js = left.clone().to_json(); + let right_js = right.clone().to_json(); + + let again_left = Side::from_json(JsJsonContext::new(""), left_js).unwrap_or_else(|_| panic!()); + let again_right = + Side::from_json(JsJsonContext::new(""), right_js).unwrap_or_else(|_| panic!()); + + assert_eq!(again_left, left); + assert_eq!(again_right, right); +} + +#[test] +fn test_compound_enum() { + #[derive(AutoJsJson, Clone, Debug, PartialEq)] + pub enum TestType { + Somestring(String), + Point { x: u32, y: String }, + Tuple(String, u32), + Number(u32), + EmptyTuple(), + EmptyStruct{}, + Nothing + } + + let somestring = TestType::Somestring("asdf".to_string()); + let point = TestType::Point { x: 10, y: "raz".to_string() }; + let tuple = TestType::Tuple ( "raz".to_string(), 10 ); + let number = TestType::Number(50); + let nothing = TestType::Nothing; + + let somestring_json = somestring.clone().to_json(); + let point_json = point.clone().to_json(); + let tuple_json = tuple.clone().to_json(); + let number_json = number.clone().to_json(); + let nothing_json = nothing.clone().to_json(); + + use vertigo::JsJsonDeserialize; + + let again_somestring = TestType::from_json(JsJsonContext::new(""), somestring_json).unwrap_or_else(|err| panic!("1. {}", err.convert_to_string())); + let again_point = TestType::from_json(JsJsonContext::new(""), point_json).unwrap_or_else(|err| panic!("2. {}", err.convert_to_string())); + let again_tuple = TestType::from_json(JsJsonContext::new(""), tuple_json).unwrap_or_else(|err| panic!("3. {}", err.convert_to_string())); + let again_number = TestType::from_json(JsJsonContext::new(""), number_json).unwrap_or_else(|err| panic!("4. {}", err.convert_to_string())); + let again_nothing = TestType::from_json(JsJsonContext::new(""), nothing_json).unwrap_or_else(|err| panic!("4. {}", err.convert_to_string())); + + assert_eq!(somestring, again_somestring); + assert_eq!(point, again_point); + assert_eq!(tuple, again_tuple); + assert_eq!(number, again_number); + assert_eq!(nothing, again_nothing); +} + +#[test] +fn test_newtype() { + #[derive(AutoJsJson, Clone, Debug, PartialEq)] + pub struct MyNumber(pub i32); + #[derive(AutoJsJson, Clone, Debug, PartialEq)] + pub struct MyString(pub String); + + let my_number = MyNumber(3); + let my_string = MyString("test".to_string()); + + let my_number_js = my_number.clone().to_json(); + let my_string_js = my_string.clone().to_json(); + + use vertigo::JsJsonDeserialize; + + let again_my_number = MyNumber::from_json(JsJsonContext::new(""), my_number_js).unwrap_or_else(|_| panic!()); + let again_my_string = MyString::from_json(JsJsonContext::new(""), my_string_js).unwrap_or_else(|_| panic!()); + + assert_eq!(again_my_number, my_number); + assert_eq!(my_string, again_my_string); +} + +#[test] +fn test_newtype_tuple() { + #[derive(AutoJsJson, Clone, Debug, PartialEq)] + pub struct MyType(pub i32, String); + + let my_type = MyType(3, "test".to_string()); + + let my_type_js = my_type.clone().to_json(); + + use vertigo::JsJsonDeserialize; + + let again_my_type = MyType::from_json(JsJsonContext::new(""), my_type_js).unwrap_or_else(|_| panic!()); + + assert_eq!(again_my_type, my_type); +} diff --git a/crates/vertigo/src/tests/mod.rs b/crates/vertigo/src/tests/mod.rs index aef5050f..28602dac 100644 --- a/crates/vertigo/src/tests/mod.rs +++ b/crates/vertigo/src/tests/mod.rs @@ -1,5 +1,5 @@ -// Tests that can't be places directly alongside the code, -// for example macros +// Tests that can't be placed directly alongside the code, +// for example tests of macros. mod autojsjson; mod component;