mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 12:52:27 +00:00
xml: Add support for tuple structs
This commit is contained in:
@@ -5,6 +5,8 @@ use super::{
|
|||||||
use darling::FromField;
|
use darling::FromField;
|
||||||
use heck::ToKebabCase;
|
use heck::ToKebabCase;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
use quote::ToTokens;
|
||||||
|
use syn::spanned::Spanned;
|
||||||
|
|
||||||
fn wrap_option_if_no_default(
|
fn wrap_option_if_no_default(
|
||||||
value: proc_macro2::TokenStream,
|
value: proc_macro2::TokenStream,
|
||||||
@@ -19,29 +21,34 @@ fn wrap_option_if_no_default(
|
|||||||
|
|
||||||
pub struct Field {
|
pub struct Field {
|
||||||
pub field: syn::Field,
|
pub field: syn::Field,
|
||||||
|
pub field_num: usize,
|
||||||
pub attrs: FieldAttrs,
|
pub attrs: FieldAttrs,
|
||||||
pub container_attrs: ContainerAttrs,
|
pub container_attrs: ContainerAttrs,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Field {
|
impl Field {
|
||||||
pub fn from_syn_field(field: syn::Field, container_attrs: ContainerAttrs) -> Self {
|
pub fn from_syn_field(
|
||||||
|
field: syn::Field,
|
||||||
|
field_num: usize,
|
||||||
|
container_attrs: ContainerAttrs,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
attrs: FieldAttrs::from_field(&field).unwrap(),
|
attrs: FieldAttrs::from_field(&field).unwrap(),
|
||||||
field,
|
field,
|
||||||
|
field_num,
|
||||||
container_attrs,
|
container_attrs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Field name in XML
|
/// Field name in XML
|
||||||
pub fn xml_name(&self) -> syn::LitByteStr {
|
pub fn xml_name(&self) -> syn::LitByteStr {
|
||||||
self.attrs
|
self.attrs.common.rename.to_owned().unwrap_or({
|
||||||
.common
|
let ident = self
|
||||||
.rename
|
.field_ident()
|
||||||
.to_owned()
|
.as_ref()
|
||||||
.unwrap_or(syn::LitByteStr::new(
|
.expect("unnamed tag fields need a rename attribute");
|
||||||
self.field_ident().to_string().to_kebab_case().as_bytes(),
|
syn::LitByteStr::new(ident.to_string().to_kebab_case().as_bytes(), ident.span())
|
||||||
self.field_ident().span(),
|
})
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to enforce the correct XML namespace
|
/// Whether to enforce the correct XML namespace
|
||||||
@@ -50,11 +57,23 @@ impl Field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Field identifier
|
/// Field identifier
|
||||||
pub fn field_ident(&self) -> &syn::Ident {
|
pub fn field_ident(&self) -> &Option<syn::Ident> {
|
||||||
self.field
|
&self.field.ident
|
||||||
.ident
|
}
|
||||||
|
|
||||||
|
/// Builder field identifier, unnamed fields also get an identifier
|
||||||
|
pub fn builder_field_ident(&self) -> syn::Ident {
|
||||||
|
self.field_ident().to_owned().unwrap_or(syn::Ident::new(
|
||||||
|
&format!("_{i}", i = self.field_num),
|
||||||
|
self.field.span(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn target_field_index(&self) -> proc_macro2::TokenStream {
|
||||||
|
self.field_ident()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("tuple structs not supported")
|
.map(syn::Ident::to_token_stream)
|
||||||
|
.unwrap_or(syn::Index::from(self.field_num).to_token_stream())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_optional(&self) -> bool {
|
fn is_optional(&self) -> bool {
|
||||||
@@ -85,7 +104,7 @@ impl Field {
|
|||||||
|
|
||||||
/// Field in the builder struct for the deserializer
|
/// Field in the builder struct for the deserializer
|
||||||
pub fn builder_field(&self) -> proc_macro2::TokenStream {
|
pub fn builder_field(&self) -> proc_macro2::TokenStream {
|
||||||
let field_ident = self.field_ident();
|
let builder_field_ident = self.builder_field_ident();
|
||||||
let ty = self.deserializer_type();
|
let ty = self.deserializer_type();
|
||||||
|
|
||||||
let builder_field_type = match (
|
let builder_field_type = match (
|
||||||
@@ -100,12 +119,12 @@ impl Field {
|
|||||||
(false, None, _) => quote! { Option<#ty> },
|
(false, None, _) => quote! { Option<#ty> },
|
||||||
};
|
};
|
||||||
|
|
||||||
quote! { #field_ident: #builder_field_type }
|
quote! { #builder_field_ident: #builder_field_type }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Field initialiser in the builder struct for the deserializer
|
/// Field initialiser in the builder struct for the deserializer
|
||||||
pub fn builder_field_init(&self) -> proc_macro2::TokenStream {
|
pub fn builder_field_init(&self) -> proc_macro2::TokenStream {
|
||||||
let field_ident = self.field_ident();
|
let builder_field_ident = self.builder_field_ident();
|
||||||
let builder_field_initialiser = match (
|
let builder_field_initialiser = match (
|
||||||
self.attrs.flatten.is_present(),
|
self.attrs.flatten.is_present(),
|
||||||
&self.attrs.default,
|
&self.attrs.default,
|
||||||
@@ -117,12 +136,14 @@ impl Field {
|
|||||||
(true, None, false) => quote! { vec![] },
|
(true, None, false) => quote! { vec![] },
|
||||||
(false, None, _) => quote! { None },
|
(false, None, _) => quote! { None },
|
||||||
};
|
};
|
||||||
quote! { #field_ident: #builder_field_initialiser }
|
quote! { #builder_field_ident: #builder_field_initialiser }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map builder field to target field
|
/// Map builder field to target field
|
||||||
pub fn builder_field_build(&self) -> proc_macro2::TokenStream {
|
pub fn builder_field_build(&self) -> proc_macro2::TokenStream {
|
||||||
let field_ident = self.field_ident();
|
// If named: use field_ident, if unnamed: use field_num
|
||||||
|
let target_field_index = self.target_field_index();
|
||||||
|
let builder_field_ident = self.builder_field_ident();
|
||||||
let builder_value = match (
|
let builder_value = match (
|
||||||
self.attrs.flatten.is_present(),
|
self.attrs.flatten.is_present(),
|
||||||
self.attrs.default.is_some(),
|
self.attrs.default.is_some(),
|
||||||
@@ -130,16 +151,16 @@ impl Field {
|
|||||||
) {
|
) {
|
||||||
(true, _, true) => unreachable!(),
|
(true, _, true) => unreachable!(),
|
||||||
(true, _, false) => {
|
(true, _, false) => {
|
||||||
quote! { FromIterator::from_iter(builder.#field_ident.into_iter()) }
|
quote! { FromIterator::from_iter(builder.#builder_field_ident.into_iter()) }
|
||||||
}
|
}
|
||||||
(false, true, true) => unreachable!(),
|
(false, true, true) => unreachable!(),
|
||||||
(false, true, false) => quote! { builder.#field_ident },
|
(false, true, false) => quote! { builder.#builder_field_ident },
|
||||||
(false, false, true) => quote! { builder.#field_ident },
|
(false, false, true) => quote! { builder.#builder_field_ident },
|
||||||
(false, false, false) => {
|
(false, false, false) => {
|
||||||
quote! { builder.#field_ident.expect("todo: handle missing field") }
|
quote! { builder.#builder_field_ident.expect("todo: handle missing field") }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
quote! { #field_ident: #builder_value }
|
quote! { #target_field_index: #builder_value }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn named_branch(&self) -> Option<proc_macro2::TokenStream> {
|
pub fn named_branch(&self) -> Option<proc_macro2::TokenStream> {
|
||||||
@@ -168,15 +189,15 @@ impl Field {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let field_name = self.xml_name();
|
let field_name = self.xml_name();
|
||||||
let field_ident = self.field_ident();
|
let builder_field_ident = self.builder_field_ident();
|
||||||
let deserializer = self.deserializer_type();
|
let deserializer = self.deserializer_type();
|
||||||
let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? };
|
let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? };
|
||||||
let assignment = match (self.attrs.flatten.is_present(), &self.attrs.default) {
|
let assignment = match (self.attrs.flatten.is_present(), &self.attrs.default) {
|
||||||
(true, _) => {
|
(true, _) => {
|
||||||
quote! { builder.#field_ident.push(#value); }
|
quote! { builder.#builder_field_ident.push(#value); }
|
||||||
}
|
}
|
||||||
(false, Some(_default)) => quote! { builder.#field_ident = #value; },
|
(false, Some(_default)) => quote! { builder.#builder_field_ident = #value; },
|
||||||
(false, None) => quote! { builder.#field_ident = Some(#value); },
|
(false, None) => quote! { builder.#builder_field_ident = Some(#value); },
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
@@ -188,17 +209,17 @@ impl Field {
|
|||||||
if self.attrs.xml_ty != FieldType::Untagged {
|
if self.attrs.xml_ty != FieldType::Untagged {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let field_ident = self.field_ident();
|
let builder_field_ident = self.builder_field_ident();
|
||||||
let deserializer = self.deserializer_type();
|
let deserializer = self.deserializer_type();
|
||||||
let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? };
|
let value = quote! { <#deserializer as rustical_xml::XmlDeserialize>::deserialize(reader, &start, empty)? };
|
||||||
|
|
||||||
Some(if self.attrs.flatten.is_present() {
|
Some(if self.attrs.flatten.is_present() {
|
||||||
quote! {
|
quote! {
|
||||||
_ => { builder.#field_ident.push(#value); }
|
_ => { builder.#builder_field_ident.push(#value); }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
quote! {
|
quote! {
|
||||||
_ => { builder.#field_ident = Some(#value); }
|
_ => { builder.#builder_field_ident = Some(#value); }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -207,7 +228,7 @@ impl Field {
|
|||||||
if self.attrs.xml_ty != FieldType::Text {
|
if self.attrs.xml_ty != FieldType::Text {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let field_ident = self.field_ident();
|
let builder_field_ident = self.builder_field_ident();
|
||||||
let value = wrap_option_if_no_default(
|
let value = wrap_option_if_no_default(
|
||||||
quote! {
|
quote! {
|
||||||
rustical_xml::Value::deserialize(text.as_ref())?
|
rustical_xml::Value::deserialize(text.as_ref())?
|
||||||
@@ -215,7 +236,7 @@ impl Field {
|
|||||||
self.attrs.default.is_some(),
|
self.attrs.default.is_some(),
|
||||||
);
|
);
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
builder.#field_ident = #value;
|
builder.#builder_field_ident = #value;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +244,7 @@ impl Field {
|
|||||||
if self.attrs.xml_ty != FieldType::Attr {
|
if self.attrs.xml_ty != FieldType::Attr {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let field_ident = self.field_ident();
|
let builder_field_ident = self.builder_field_ident();
|
||||||
let field_name = self.xml_name();
|
let field_name = self.xml_name();
|
||||||
|
|
||||||
let value = wrap_option_if_no_default(
|
let value = wrap_option_if_no_default(
|
||||||
@@ -235,7 +256,7 @@ impl Field {
|
|||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
#field_name => {
|
#field_name => {
|
||||||
builder.#field_ident = #value;
|
builder.#builder_field_ident = #value;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -244,7 +265,7 @@ impl Field {
|
|||||||
if self.attrs.xml_ty != FieldType::TagName {
|
if self.attrs.xml_ty != FieldType::TagName {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let field_ident = self.field_ident();
|
let builder_field_ident = self.builder_field_ident();
|
||||||
|
|
||||||
let value = wrap_option_if_no_default(
|
let value = wrap_option_if_no_default(
|
||||||
quote! {
|
quote! {
|
||||||
@@ -254,13 +275,12 @@ impl Field {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
builder.#field_ident = #value;
|
builder.#builder_field_ident = #value;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tag_writer(&self) -> Option<proc_macro2::TokenStream> {
|
pub fn tag_writer(&self) -> Option<proc_macro2::TokenStream> {
|
||||||
let field_ident = self.field_ident();
|
let target_field_index = self.target_field_index();
|
||||||
let field_name = self.xml_name();
|
|
||||||
let serializer = if let Some(serialize_with) = &self.attrs.serialize_with {
|
let serializer = if let Some(serialize_with) = &self.attrs.serialize_with {
|
||||||
quote! { #serialize_with }
|
quote! { #serialize_with }
|
||||||
} else {
|
} else {
|
||||||
@@ -274,28 +294,34 @@ impl Field {
|
|||||||
match (&self.attrs.xml_ty, self.attrs.flatten.is_present()) {
|
match (&self.attrs.xml_ty, self.attrs.flatten.is_present()) {
|
||||||
(FieldType::Attr, _) => None,
|
(FieldType::Attr, _) => None,
|
||||||
(FieldType::Text, true) => Some(quote! {
|
(FieldType::Text, true) => Some(quote! {
|
||||||
for item in self.#field_ident.iter() {
|
for item in self.#target_field_index.iter() {
|
||||||
writer.write_event(Event::Text(BytesText::new(item)))?;
|
writer.write_event(Event::Text(BytesText::new(item)))?;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
(FieldType::Text, false) => Some(quote! {
|
(FieldType::Text, false) => Some(quote! {
|
||||||
writer.write_event(Event::Text(BytesText::new(&self.#field_ident)))?;
|
writer.write_event(Event::Text(BytesText::new(&self.#target_field_index)))?;
|
||||||
}),
|
}),
|
||||||
(FieldType::Tag, true) => Some(quote! {
|
(FieldType::Tag, true) => {
|
||||||
for item in self.#field_ident.iter() {
|
let field_name = self.xml_name();
|
||||||
|
Some(quote! {
|
||||||
|
for item in self.#target_field_index.iter() {
|
||||||
#serializer(item, #ns, Some(#field_name), namespaces, writer)?;
|
#serializer(item, #ns, Some(#field_name), namespaces, writer)?;
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
(FieldType::Tag, false) => Some(quote! {
|
}
|
||||||
#serializer(&self.#field_ident, #ns, Some(#field_name), namespaces, writer)?;
|
(FieldType::Tag, false) => {
|
||||||
}),
|
let field_name = self.xml_name();
|
||||||
|
Some(quote! {
|
||||||
|
#serializer(&self.#target_field_index, #ns, Some(#field_name), namespaces, writer)?;
|
||||||
|
})
|
||||||
|
}
|
||||||
(FieldType::Untagged, true) => Some(quote! {
|
(FieldType::Untagged, true) => Some(quote! {
|
||||||
for item in self.#field_ident.iter() {
|
for item in self.#target_field_index.iter() {
|
||||||
#serializer(item, None, None, namespaces, writer)?;
|
#serializer(item, None, None, namespaces, writer)?;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
(FieldType::Untagged, false) => Some(quote! {
|
(FieldType::Untagged, false) => Some(quote! {
|
||||||
#serializer(&self.#field_ident, None, None, namespaces, writer)?;
|
#serializer(&self.#target_field_index, None, None, namespaces, writer)?;
|
||||||
}),
|
}),
|
||||||
// TODO: Think about what to do here
|
// TODO: Think about what to do here
|
||||||
(FieldType::TagName | FieldType::Namespace, _) => None,
|
(FieldType::TagName | FieldType::Namespace, _) => None,
|
||||||
|
|||||||
@@ -24,13 +24,29 @@ impl NamedStruct {
|
|||||||
fields: named
|
fields: named
|
||||||
.named
|
.named
|
||||||
.iter()
|
.iter()
|
||||||
.map(|field| Field::from_syn_field(field.to_owned(), attrs.container.clone()))
|
.enumerate()
|
||||||
|
.map(|(i, field)| {
|
||||||
|
Field::from_syn_field(field.to_owned(), i, attrs.container.clone())
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
attrs,
|
attrs,
|
||||||
ident: input.ident.to_owned(),
|
ident: input.ident.to_owned(),
|
||||||
generics: input.generics.to_owned(),
|
generics: input.generics.to_owned(),
|
||||||
},
|
},
|
||||||
syn::Fields::Unnamed(_) => panic!("not implemented for tuple struct"),
|
syn::Fields::Unnamed(unnamed) => NamedStruct {
|
||||||
|
fields: unnamed
|
||||||
|
.unnamed
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, field)| {
|
||||||
|
Field::from_syn_field(field.to_owned(), i, attrs.container.clone())
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
attrs,
|
||||||
|
ident: input.ident.to_owned(),
|
||||||
|
generics: input.generics.to_owned(),
|
||||||
|
},
|
||||||
|
|
||||||
syn::Fields::Unit => NamedStruct {
|
syn::Fields::Unit => NamedStruct {
|
||||||
fields: vec![],
|
fields: vec![],
|
||||||
attrs,
|
attrs,
|
||||||
@@ -219,11 +235,11 @@ impl NamedStruct {
|
|||||||
.filter(|field| field.attrs.xml_ty == FieldType::Attr)
|
.filter(|field| field.attrs.xml_ty == FieldType::Attr)
|
||||||
.map(|field| {
|
.map(|field| {
|
||||||
let field_name = field.xml_name();
|
let field_name = field.xml_name();
|
||||||
let field_ident = field.field_ident();
|
let field_index = field.target_field_index();
|
||||||
quote! {
|
quote! {
|
||||||
::quick_xml::events::attributes::Attribute {
|
::quick_xml::events::attributes::Attribute {
|
||||||
key: ::quick_xml::name::QName(#field_name),
|
key: ::quick_xml::name::QName(#field_name),
|
||||||
value: ::std::borrow::Cow::from(::rustical_xml::Value::serialize(&self.#field_ident).into_bytes())
|
value: ::std::borrow::Cow::from(::rustical_xml::Value::serialize(&self.#field_index).into_bytes())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -233,9 +249,9 @@ impl NamedStruct {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|field| field.attrs.xml_ty == FieldType::TagName)
|
.find(|field| field.attrs.xml_ty == FieldType::TagName)
|
||||||
.map(|field| {
|
.map(|field| {
|
||||||
let field_ident = field.field_ident();
|
let field_index = field.target_field_index();
|
||||||
quote! {
|
quote! {
|
||||||
let tag_str = self.#field_ident.to_string();
|
let tag_str = self.#field_index.to_string();
|
||||||
let tag = Some(tag.unwrap_or(tag_str.as_bytes()));
|
let tag = Some(tag.unwrap_or(tag_str.as_bytes()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -245,9 +261,9 @@ impl NamedStruct {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|field| field.attrs.xml_ty == FieldType::Namespace)
|
.find(|field| field.attrs.xml_ty == FieldType::Namespace)
|
||||||
.map(|field| {
|
.map(|field| {
|
||||||
let field_ident = field.field_ident();
|
let field_index = field.target_field_index();
|
||||||
quote! {
|
quote! {
|
||||||
let ns = Some(self.#field_ident);
|
let ns = Some(self.#field_index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -286,3 +286,28 @@ fn test_struct_xml_decl() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_struct_tuple() {
|
||||||
|
#[derive(Debug, XmlDeserialize, XmlRootTag, PartialEq)]
|
||||||
|
#[xml(root = b"document")]
|
||||||
|
struct Document {
|
||||||
|
child: Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, XmlDeserialize, PartialEq, Default)]
|
||||||
|
struct Child(#[xml(ty = "tag_name")] String, #[xml(ty = "text")] String);
|
||||||
|
|
||||||
|
let doc = Document::parse_str(
|
||||||
|
r#"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<document><child>Hello!</child></document>"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
doc,
|
||||||
|
Document {
|
||||||
|
child: Child("child".to_owned(), "Hello!".to_owned())
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -216,3 +216,26 @@ fn test_struct_ns() {
|
|||||||
let out = String::from_utf8(buf).unwrap();
|
let out = String::from_utf8(buf).unwrap();
|
||||||
dbg!(out);
|
dbg!(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_struct_tuple() {
|
||||||
|
#[derive(Debug, XmlRootTag, XmlSerialize, PartialEq)]
|
||||||
|
#[xml(root = b"document")]
|
||||||
|
struct Document {
|
||||||
|
child: Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, XmlSerialize, PartialEq, Default)]
|
||||||
|
struct Child(#[xml(ty = "tag_name")] String, #[xml(ty = "text")] String);
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut writer = quick_xml::Writer::new(&mut buf);
|
||||||
|
|
||||||
|
Document {
|
||||||
|
child: Child("child".to_owned(), "Hello!".to_owned()),
|
||||||
|
}
|
||||||
|
.serialize_root(&mut writer)
|
||||||
|
.unwrap();
|
||||||
|
let out = String::from_utf8(buf).unwrap();
|
||||||
|
dbg!(out);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user