mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Derive Cast
for enums
This commit is contained in:
parent
cb3c263c4a
commit
880b1847bd
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1269,6 +1269,7 @@ dependencies = [
|
|||||||
name = "typst-macros"
|
name = "typst-macros"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
@ -193,30 +193,15 @@ impl Resolve for HorizontalAlign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// How to determine line breaks in a paragraph.
|
/// How to determine line breaks in a paragraph.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
pub enum Linebreaks {
|
pub enum Linebreaks {
|
||||||
/// Determine the line breaks in a simple first-fit style.
|
/// Determine the line breaks in a simple first-fit style.
|
||||||
Simple,
|
Simple,
|
||||||
/// Optimize the line breaks for the whole paragraph.
|
/// Optimize the line breaks for the whole paragraph.
|
||||||
Optimized,
|
|
||||||
}
|
|
||||||
|
|
||||||
cast_from_value! {
|
|
||||||
Linebreaks,
|
|
||||||
/// Determine the line breaks in a simple first-fit style.
|
|
||||||
"simple" => Self::Simple,
|
|
||||||
/// Optimize the line breaks for the whole paragraph.
|
|
||||||
///
|
///
|
||||||
/// Typst will try to produce more evenly filled lines of text by
|
/// Typst will try to produce more evenly filled lines of text by
|
||||||
/// considering the whole paragraph when calculating line breaks.
|
/// considering the whole paragraph when calculating line breaks.
|
||||||
"optimized" => Self::Optimized,
|
Optimized,
|
||||||
}
|
|
||||||
|
|
||||||
cast_to_value! {
|
|
||||||
v: Linebreaks => Value::from(match v {
|
|
||||||
Linebreaks::Simple => "simple",
|
|
||||||
Linebreaks::Optimized => "optimized",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A paragraph break.
|
/// A paragraph break.
|
||||||
|
@ -169,12 +169,22 @@ impl LayoutMath for CasesNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A vector / matrix delimiter.
|
/// A vector / matrix delimiter.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
pub enum Delimiter {
|
pub enum Delimiter {
|
||||||
|
/// Delimit with parentheses.
|
||||||
|
#[string("(")]
|
||||||
Paren,
|
Paren,
|
||||||
|
/// Delimit with brackets.
|
||||||
|
#[string("[")]
|
||||||
Bracket,
|
Bracket,
|
||||||
|
/// Delimit with curly braces.
|
||||||
|
#[string("{")]
|
||||||
Brace,
|
Brace,
|
||||||
|
/// Delimit with vertical bars.
|
||||||
|
#[string("|")]
|
||||||
Bar,
|
Bar,
|
||||||
|
/// Delimit with double vertical bars.
|
||||||
|
#[string("||")]
|
||||||
DoubleBar,
|
DoubleBar,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,30 +212,6 @@ impl Delimiter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cast_from_value! {
|
|
||||||
Delimiter,
|
|
||||||
/// Delimit with parentheses.
|
|
||||||
"(" => Self::Paren,
|
|
||||||
/// Delimit with brackets.
|
|
||||||
"[" => Self::Bracket,
|
|
||||||
/// Delimit with curly braces.
|
|
||||||
"{" => Self::Brace,
|
|
||||||
/// Delimit with vertical bars.
|
|
||||||
"|" => Self::Bar,
|
|
||||||
/// Delimit with double vertical bars.
|
|
||||||
"||" => Self::DoubleBar,
|
|
||||||
}
|
|
||||||
|
|
||||||
cast_to_value! {
|
|
||||||
v: Delimiter => Value::from(match v {
|
|
||||||
Delimiter::Paren => "(",
|
|
||||||
Delimiter::Bracket => "[",
|
|
||||||
Delimiter::Brace => "{",
|
|
||||||
Delimiter::Bar => "|",
|
|
||||||
Delimiter::DoubleBar => "||",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout the inner contents of a vector.
|
/// Layout the inner contents of a vector.
|
||||||
fn layout_vec_body(
|
fn layout_vec_body(
|
||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
|
@ -246,7 +246,7 @@ cast_from_value! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A case transformation on text.
|
/// A case transformation on text.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
pub enum Case {
|
pub enum Case {
|
||||||
/// Everything is lowercased.
|
/// Everything is lowercased.
|
||||||
Lower,
|
Lower,
|
||||||
@ -264,19 +264,6 @@ impl Case {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cast_from_value! {
|
|
||||||
Case,
|
|
||||||
"lower" => Self::Lower,
|
|
||||||
"upper" => Self::Upper,
|
|
||||||
}
|
|
||||||
|
|
||||||
cast_to_value! {
|
|
||||||
v: Case => Value::from(match v {
|
|
||||||
Case::Lower => "lower",
|
|
||||||
Case::Upper => "upper",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display text in small capitals.
|
/// Display text in small capitals.
|
||||||
///
|
///
|
||||||
/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts
|
/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts
|
||||||
|
@ -686,53 +686,23 @@ cast_to_value! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Which kind of numbers / figures to select.
|
/// Which kind of numbers / figures to select.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
pub enum NumberType {
|
pub enum NumberType {
|
||||||
/// Numbers that fit well with capital text. ("lnum")
|
/// Numbers that fit well with capital text (the OpenType `lnum`
|
||||||
|
/// font feature).
|
||||||
Lining,
|
Lining,
|
||||||
/// Numbers that fit well into a flow of upper- and lowercase text. ("onum")
|
/// Numbers that fit well into a flow of upper- and lowercase text (the
|
||||||
|
/// OpenType `onum` font feature).
|
||||||
OldStyle,
|
OldStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
cast_from_value! {
|
|
||||||
NumberType,
|
|
||||||
/// Numbers that fit well with capital text (the OpenType `lnum`
|
|
||||||
/// font feature).
|
|
||||||
"lining" => Self::Lining,
|
|
||||||
// Numbers that fit well into a flow of upper- and lowercase text (the
|
|
||||||
/// OpenType `onum` font feature).
|
|
||||||
"old-style" => Self::OldStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
cast_to_value! {
|
|
||||||
v: NumberType => Value::from(match v {
|
|
||||||
NumberType::Lining => "lining",
|
|
||||||
NumberType::OldStyle => "old-style",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The width of numbers / figures.
|
/// The width of numbers / figures.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
pub enum NumberWidth {
|
pub enum NumberWidth {
|
||||||
/// Number widths are glyph specific. ("pnum")
|
|
||||||
Proportional,
|
|
||||||
/// All numbers are of equal width / monospaced. ("tnum")
|
|
||||||
Tabular,
|
|
||||||
}
|
|
||||||
|
|
||||||
cast_from_value! {
|
|
||||||
NumberWidth,
|
|
||||||
/// Numbers with glyph-specific widths (the OpenType `pnum` font feature).
|
/// Numbers with glyph-specific widths (the OpenType `pnum` font feature).
|
||||||
"proportional" => Self::Proportional,
|
Proportional,
|
||||||
/// Numbers of equal width (the OpenType `tnum` font feature).
|
/// Numbers of equal width (the OpenType `tnum` font feature).
|
||||||
"tabular" => Self::Tabular,
|
Tabular,
|
||||||
}
|
|
||||||
|
|
||||||
cast_to_value! {
|
|
||||||
v: NumberWidth => Value::from(match v {
|
|
||||||
NumberWidth::Proportional => "proportional",
|
|
||||||
NumberWidth::Tabular => "tabular",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OpenType font features settings.
|
/// OpenType font features settings.
|
||||||
|
@ -113,33 +113,15 @@ impl Layout for ImageNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// How an image should adjust itself to a given area.
|
/// How an image should adjust itself to a given area.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
pub enum ImageFit {
|
pub enum ImageFit {
|
||||||
/// The image should completely cover the area.
|
/// The image should completely cover the area. This is the default.
|
||||||
Cover,
|
Cover,
|
||||||
/// The image should be fully contained in the area.
|
/// The image should be fully contained in the area.
|
||||||
Contain,
|
Contain,
|
||||||
/// The image should be stretched so that it exactly fills the area.
|
|
||||||
Stretch,
|
|
||||||
}
|
|
||||||
|
|
||||||
cast_from_value! {
|
|
||||||
ImageFit,
|
|
||||||
/// The image should completely cover the area. This is the default.
|
|
||||||
"cover" => Self::Cover,
|
|
||||||
/// The image should be fully contained in the area.
|
|
||||||
"contain" => Self::Contain,
|
|
||||||
/// The image should be stretched so that it exactly fills the area, even if
|
/// The image should be stretched so that it exactly fills the area, even if
|
||||||
/// this means that the image will be distorted.
|
/// this means that the image will be distorted.
|
||||||
"stretch" => Self::Stretch,
|
Stretch,
|
||||||
}
|
|
||||||
|
|
||||||
cast_to_value! {
|
|
||||||
fit: ImageFit => Value::from(match fit {
|
|
||||||
ImageFit::Cover => "cover",
|
|
||||||
ImageFit::Contain => "contain",
|
|
||||||
ImageFit::Stretch => "stretch",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load an image from a path.
|
/// Load an image from a path.
|
||||||
|
@ -15,3 +15,4 @@ proc-macro2 = "1"
|
|||||||
quote = "1"
|
quote = "1"
|
||||||
syn = { version = "1", features = ["full", "extra-traits"] }
|
syn = { version = "1", features = ["full", "extra-traits"] }
|
||||||
unscanny = "0.1"
|
unscanny = "0.1"
|
||||||
|
heck = "0.4"
|
||||||
|
@ -1,5 +1,67 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Expand the `#[derive(Cast)]` macro.
|
||||||
|
pub fn cast(item: DeriveInput) -> Result<TokenStream> {
|
||||||
|
let ty = &item.ident;
|
||||||
|
|
||||||
|
let syn::Data::Enum(data) = &item.data else {
|
||||||
|
bail!(item, "only enums are supported");
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut variants = vec![];
|
||||||
|
for variant in &data.variants {
|
||||||
|
if let Some((_, expr)) = &variant.discriminant {
|
||||||
|
bail!(expr, "explicit discriminant is not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let string = if let Some(attr) =
|
||||||
|
variant.attrs.iter().find(|attr| attr.path.is_ident("string"))
|
||||||
|
{
|
||||||
|
attr.parse_args::<syn::LitStr>()?.value()
|
||||||
|
} else {
|
||||||
|
kebab_case(&variant.ident)
|
||||||
|
};
|
||||||
|
|
||||||
|
variants.push(Variant {
|
||||||
|
ident: variant.ident.clone(),
|
||||||
|
string,
|
||||||
|
docs: documentation(&variant.attrs),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let strs_to_variants = variants.iter().map(|Variant { ident, string, docs }| {
|
||||||
|
quote! {
|
||||||
|
#[doc = #docs]
|
||||||
|
#string => Self::#ident
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let variants_to_strs = variants.iter().map(|Variant { ident, string, .. }| {
|
||||||
|
quote! {
|
||||||
|
#ty::#ident => #string
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
::typst::eval::cast_from_value! {
|
||||||
|
#ty,
|
||||||
|
#(#strs_to_variants),*
|
||||||
|
}
|
||||||
|
|
||||||
|
::typst::eval::cast_to_value! {
|
||||||
|
v: #ty => ::typst::eval::Value::from(match v {
|
||||||
|
#(#variants_to_strs),*
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Variant {
|
||||||
|
ident: Ident,
|
||||||
|
string: String,
|
||||||
|
docs: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Expand the `cast_from_value!` macro.
|
/// Expand the `cast_from_value!` macro.
|
||||||
pub fn cast_from_value(stream: TokenStream) -> Result<TokenStream> {
|
pub fn cast_from_value(stream: TokenStream) -> Result<TokenStream> {
|
||||||
let castable: Castable = syn::parse2(stream)?;
|
let castable: Castable = syn::parse2(stream)?;
|
||||||
|
@ -15,7 +15,7 @@ use quote::quote;
|
|||||||
use syn::ext::IdentExt;
|
use syn::ext::IdentExt;
|
||||||
use syn::parse::{Parse, ParseStream, Parser};
|
use syn::parse::{Parse, ParseStream, Parser};
|
||||||
use syn::punctuated::Punctuated;
|
use syn::punctuated::Punctuated;
|
||||||
use syn::{parse_quote, Ident, Result, Token};
|
use syn::{parse_quote, DeriveInput, Ident, Result, Token};
|
||||||
|
|
||||||
use self::util::*;
|
use self::util::*;
|
||||||
|
|
||||||
@ -35,6 +35,15 @@ pub fn node(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implement `Cast` for an enum.
|
||||||
|
#[proc_macro_derive(Cast, attributes(string))]
|
||||||
|
pub fn cast(item: BoundaryStream) -> BoundaryStream {
|
||||||
|
let item = syn::parse_macro_input!(item as DeriveInput);
|
||||||
|
castable::cast(item)
|
||||||
|
.unwrap_or_else(|err| err.to_compile_error())
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
/// Implement `Cast` and optionally `Type` for a type.
|
/// Implement `Cast` and optionally `Type` for a type.
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn cast_from_value(stream: BoundaryStream) -> BoundaryStream {
|
pub fn cast_from_value(stream: BoundaryStream) -> BoundaryStream {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use heck::ToKebabCase;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Return an error at the given item.
|
/// Return an error at the given item.
|
||||||
@ -55,7 +57,7 @@ pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> {
|
|||||||
|
|
||||||
/// Convert an identifier to a kebab-case string.
|
/// Convert an identifier to a kebab-case string.
|
||||||
pub fn kebab_case(name: &Ident) -> String {
|
pub fn kebab_case(name: &Ident) -> String {
|
||||||
name.to_string().to_lowercase().replace('_', "-")
|
name.to_string().to_kebab_case()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract documentation comments from an attribute list.
|
/// Extract documentation comments from an attribute list.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
pub use typst_macros::{cast_from_value, cast_to_value};
|
pub use typst_macros::{cast_from_value, cast_to_value, Cast};
|
||||||
|
|
||||||
use std::num::{NonZeroI64, NonZeroUsize};
|
use std::num::{NonZeroI64, NonZeroUsize};
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
|
@ -12,7 +12,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use ttf_parser::GlyphId;
|
use ttf_parser::GlyphId;
|
||||||
|
|
||||||
use crate::eval::{cast_from_value, cast_to_value, Value};
|
use crate::eval::Cast;
|
||||||
use crate::geom::Em;
|
use crate::geom::Em;
|
||||||
use crate::util::Buffer;
|
use crate::util::Buffer;
|
||||||
|
|
||||||
@ -231,12 +231,9 @@ pub struct LineMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Identifies a vertical metric of a font.
|
/// Identifies a vertical metric of a font.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
pub enum VerticalFontMetric {
|
pub enum VerticalFontMetric {
|
||||||
/// The typographic ascender.
|
/// The font's ascender, which typically exceeds the height of all glyphs.
|
||||||
///
|
|
||||||
/// Corresponds to the typographic ascender from the `OS/2` table if present
|
|
||||||
/// and falls back to the ascender from the `hhea` table otherwise.
|
|
||||||
Ascender,
|
Ascender,
|
||||||
/// The approximate height of uppercase letters.
|
/// The approximate height of uppercase letters.
|
||||||
CapHeight,
|
CapHeight,
|
||||||
@ -244,33 +241,6 @@ pub enum VerticalFontMetric {
|
|||||||
XHeight,
|
XHeight,
|
||||||
/// The baseline on which the letters rest.
|
/// The baseline on which the letters rest.
|
||||||
Baseline,
|
Baseline,
|
||||||
/// The typographic descender.
|
/// The font's ascender, which typically exceeds the depth of all glyphs.
|
||||||
///
|
|
||||||
/// Corresponds to the typographic descender from the `OS/2` table if
|
|
||||||
/// present and falls back to the descender from the `hhea` table otherwise.
|
|
||||||
Descender,
|
Descender,
|
||||||
}
|
}
|
||||||
|
|
||||||
cast_from_value! {
|
|
||||||
VerticalFontMetric,
|
|
||||||
/// The font's ascender, which typically exceeds the height of all glyphs.
|
|
||||||
"ascender" => Self::Ascender,
|
|
||||||
/// The approximate height of uppercase letters.
|
|
||||||
"cap-height" => Self::CapHeight,
|
|
||||||
/// The approximate height of non-ascending lowercase letters.
|
|
||||||
"x-height" => Self::XHeight,
|
|
||||||
/// The baseline on which the letters rest.
|
|
||||||
"baseline" => Self::Baseline,
|
|
||||||
/// The font's ascender, which typically exceeds the depth of all glyphs.
|
|
||||||
"descender" => Self::Descender,
|
|
||||||
}
|
|
||||||
|
|
||||||
cast_to_value! {
|
|
||||||
v: VerticalFontMetric => Value::from(match v {
|
|
||||||
VerticalFontMetric::Ascender => "ascender",
|
|
||||||
VerticalFontMetric::CapHeight => "cap-height",
|
|
||||||
VerticalFontMetric::XHeight => "x-height",
|
|
||||||
VerticalFontMetric::Baseline => "baseline" ,
|
|
||||||
VerticalFontMetric::Descender => "descender",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter};
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::eval::{cast_from_value, cast_to_value, Value};
|
use crate::eval::{cast_from_value, cast_to_value, Cast, Value};
|
||||||
use crate::geom::Ratio;
|
use crate::geom::Ratio;
|
||||||
|
|
||||||
/// Properties that distinguish a font from other fonts in the same family.
|
/// Properties that distinguish a font from other fonts in the same family.
|
||||||
@ -32,7 +32,7 @@ impl Debug for FontVariant {
|
|||||||
|
|
||||||
/// The style of a font.
|
/// The style of a font.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Cast)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum FontStyle {
|
pub enum FontStyle {
|
||||||
/// The default, typically upright style.
|
/// The default, typically upright style.
|
||||||
@ -62,24 +62,6 @@ impl Default for FontStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cast_from_value! {
|
|
||||||
FontStyle,
|
|
||||||
/// The default, typically upright style.
|
|
||||||
"normal" => Self::Normal,
|
|
||||||
/// A cursive style with custom letterform.
|
|
||||||
"italic" => Self::Italic,
|
|
||||||
/// Just a slanted version of the normal style.
|
|
||||||
"oblique" => Self::Oblique,
|
|
||||||
}
|
|
||||||
|
|
||||||
cast_to_value! {
|
|
||||||
v: FontStyle => Value::from(match v {
|
|
||||||
FontStyle::Normal => "normal",
|
|
||||||
FontStyle::Italic => "italic",
|
|
||||||
FontStyle::Oblique => "oblique",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The weight of a font.
|
/// The weight of a font.
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user