Skip to content

Commit

Permalink
Enums and newtypes support in AutoJsJson
Browse files Browse the repository at this point in the history
  • Loading branch information
sfisol committed Mar 20, 2024
1 parent 0c1f56f commit b413ae4
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
200 changes: 200 additions & 0 deletions crates/vertigo-macro/src/jsjson/enums.rs
Original file line number Diff line number Diff line change
@@ -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<TokenStream, String> {
// 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::<Vec<_>>();
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::<Vec<_>>();

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::<Vec<_>>();

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::<Vec<_>>();

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<Self, vertigo::JsJsonContext> {
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())
}
17 changes: 17 additions & 0 deletions crates/vertigo-macro/src/jsjson/mod.rs
Original file line number Diff line number Diff line change
@@ -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<TokenStream, String> {
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()),
}
}
76 changes: 76 additions & 0 deletions crates/vertigo-macro/src/jsjson/newtypes.rs
Original file line number Diff line number Diff line change
@@ -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<TokenStream, String> {
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::<Vec<_>>();
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<Self, vertigo::JsJsonContext> {
#(#decodes)*
}
}
};

Ok(result.into())
}
Original file line number Diff line number Diff line change
@@ -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<TokenStream, String> {
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<TokenStream, String> {
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);
Expand All @@ -36,15 +28,15 @@ pub(crate) fn impl_js_json_derive(ast: &syn::DeriveInput) -> Result<TokenStream,
}

let result = quote! {
impl vertigo::JsJsonSerialize for #structure_name {
impl vertigo::JsJsonSerialize for #name {
fn to_json(self) -> vertigo::JsJson {
vertigo::JsJson::Object(::std::collections::HashMap::from([
#(#list_to_json)*
]))
}
}

impl vertigo::JsJsonDeserialize for #structure_name {
impl vertigo::JsJsonDeserialize for #name {
fn from_json(context: vertigo::JsJsonContext, mut json: vertigo::JsJson) -> Result<Self, vertigo::JsJsonContext> {
Ok(Self {
#(#list_from_json)*
Expand Down
35 changes: 35 additions & 0 deletions crates/vertigo-macro/src/jsjson/tuple_fields.rs
Original file line number Diff line number Diff line change
@@ -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<Ident>, Vec<TokenStream>) {
let field_idents = fields_iter
.enumerate()
.map(|(n, field)| Ident::new(&format!("f_{}", n), field.span()))
.collect::<Vec<_>>();

let field_encodes = field_idents
.iter()
.map(|field_ident| {
quote! {
#field_ident.to_json(),
}
})
.collect::<Vec<_>>();

(field_idents, field_encodes)
}

/// Takes tuple fields and returns generated encodes
pub(super) fn get_decodes(field_idents: Vec<Ident>) -> Vec<TokenStream> {
field_idents
.iter()
.map(|_| {
quote! {
vertigo::JsJsonDeserialize::from_json(ctx.clone(), fields_rev.pop().unwrap())?,
}
})
.collect::<Vec<_>>()
}
Loading

0 comments on commit b413ae4

Please sign in to comment.