#![forbid(unsafe_code)]
#![warn(missing_docs)]
use proc_macro::{Delimiter, Spacing, TokenStream, TokenTree};
#[proc_macro_derive(CanonicalEncode)]
pub fn derive_canonical_encode(input: TokenStream) -> TokenStream {
match Parsed::from_input(input) {
Ok(parsed) => parsed.canonical_encode_impl(),
Err(message) => compile_error(&message),
}
}
#[proc_macro_derive(CanonicalDecode)]
pub fn derive_canonical_decode(input: TokenStream) -> TokenStream {
match Parsed::from_input(input) {
Ok(parsed) => parsed.canonical_decode_impl(),
Err(message) => compile_error(&message),
}
}
#[proc_macro_derive(JsonEncode)]
pub fn derive_json_encode(input: TokenStream) -> TokenStream {
match Parsed::from_input(input).and_then(|parsed| parsed.json_encode_impl()) {
Ok(tokens) => tokens,
Err(message) => compile_error(&message),
}
}
#[proc_macro_derive(JsonDecode)]
pub fn derive_json_decode(input: TokenStream) -> TokenStream {
match Parsed::from_input(input).and_then(|parsed| parsed.json_decode_impl()) {
Ok(tokens) => tokens,
Err(message) => compile_error(&message),
}
}
#[proc_macro_derive(CsvEncode)]
pub fn derive_csv_encode(input: TokenStream) -> TokenStream {
match Parsed::from_input(input).and_then(|parsed| parsed.csv_encode_impl()) {
Ok(tokens) => tokens,
Err(message) => compile_error(&message),
}
}
#[proc_macro_derive(CsvDecode)]
pub fn derive_csv_decode(input: TokenStream) -> TokenStream {
match Parsed::from_input(input).and_then(|parsed| parsed.csv_decode_impl()) {
Ok(tokens) => tokens,
Err(message) => compile_error(&message),
}
}
enum Kind {
Struct,
Enum,
Union,
}
enum Shape {
Named(Vec<String>),
Tuple(usize),
Unit,
}
struct Variant {
name: String,
shape: Shape,
}
enum Body {
Struct(Shape),
Enum(Vec<Variant>),
}
struct Parsed {
name: String,
body: Body,
}
struct RawVariant {
name: String,
shape: Result<Shape, String>,
has_discriminant: bool,
}
enum RawBody {
Struct(Shape),
Enum(Vec<RawVariant>),
Union,
}
struct Raw {
name: String,
has_generics: bool,
saw_repr: bool,
body: RawBody,
}
impl Parsed {
fn from_input(input: TokenStream) -> Result<Self, String> {
validate(classify(input)?)
}
fn canonical_encode_impl(&self) -> TokenStream {
let statements = match &self.body {
Body::Struct(shape) => struct_encode_statements(shape),
Body::Enum(variants) => enum_encode_statements(variants),
};
format!(
"impl ::reliakit_codec::CanonicalEncode for {name} {{\n\
fn encode<__W: ::reliakit_codec::EncodeSink + ?Sized>(&self, __writer: &mut __W) \
-> ::core::result::Result<(), ::reliakit_codec::CodecError> {{\n\
{statements}\n\
::core::result::Result::Ok(())\n\
}}\n\
}}",
name = self.name,
)
.parse()
.expect("reliakit-derive generated invalid CanonicalEncode tokens")
}
fn canonical_decode_impl(&self) -> TokenStream {
let value = match &self.body {
Body::Struct(shape) => struct_decode_value(shape),
Body::Enum(variants) => enum_decode_value(&self.name, variants),
};
format!(
"impl ::reliakit_codec::CanonicalDecode for {name} {{\n\
fn decode<__R: ::reliakit_codec::DecodeSource + ?Sized>(__reader: &mut __R) \
-> ::core::result::Result<Self, ::reliakit_codec::CodecError> {{\n\
{value}\n\
}}\n\
}}",
name = self.name,
)
.parse()
.expect("reliakit-derive generated invalid CanonicalDecode tokens")
}
fn json_encode_impl(&self) -> Result<TokenStream, String> {
let value = match &self.body {
Body::Struct(shape) => json_encode_value(shape),
Body::Enum(_) => {
return Err("reliakit-derive: JsonEncode does not support enums yet".into())
}
};
Ok(format!(
"impl ::reliakit_json::JsonEncode for {name} {{\n\
fn to_json_value(&self) -> ::reliakit_json::JsonValue {{\n\
{value}\n\
}}\n\
}}",
name = self.name,
)
.parse()
.expect("reliakit-derive generated invalid JsonEncode tokens"))
}
fn json_decode_impl(&self) -> Result<TokenStream, String> {
let body = match &self.body {
Body::Struct(shape) => json_decode_body(shape),
Body::Enum(_) => {
return Err("reliakit-derive: JsonDecode does not support enums yet".into())
}
};
Ok(format!(
"impl ::reliakit_json::JsonDecode for {name} {{\n\
fn from_json_value(__value: &::reliakit_json::JsonValue) \
-> ::core::result::Result<Self, ::reliakit_json::JsonDecodeError> {{\n\
{body}\n\
}}\n\
}}",
name = self.name,
)
.parse()
.expect("reliakit-derive generated invalid JsonDecode tokens"))
}
fn csv_encode_impl(&self) -> Result<TokenStream, String> {
let fields = csv_named_fields(&self.body, "CsvEncode")?;
let methods = csv_encode_methods(fields);
Ok(format!(
"impl ::reliakit_csv::CsvEncode for {name} {{\n{methods}\n}}",
name = self.name,
)
.parse()
.expect("reliakit-derive generated invalid CsvEncode tokens"))
}
fn csv_decode_impl(&self) -> Result<TokenStream, String> {
let fields = csv_named_fields(&self.body, "CsvDecode")?;
let method = csv_decode_method(fields);
Ok(format!(
"impl ::reliakit_csv::CsvDecode for {name} {{\n{method}\n}}",
name = self.name,
)
.parse()
.expect("reliakit-derive generated invalid CsvDecode tokens"))
}
}
fn json_key(field: &str) -> &str {
field.strip_prefix("r#").unwrap_or(field)
}
fn json_encode_value(shape: &Shape) -> String {
match shape {
Shape::Named(fields) => {
let mut inserts = String::new();
for field in fields {
let key = json_key(field);
inserts.push_str(&format!(
"__object.insert({key:?}.into(), \
::reliakit_json::JsonEncode::to_json_value(&self.{field}));",
));
}
format!(
"let mut __object = ::reliakit_json::JsonObject::new();\n\
{inserts}\n\
::reliakit_json::JsonValue::Object(__object)"
)
}
Shape::Tuple(count) => {
let mut items = String::new();
for index in 0..*count {
items.push_str(&format!(
"::reliakit_json::JsonEncode::to_json_value(&self.{index}),"
));
}
format!("::reliakit_json::JsonValue::array([{items}])")
}
Shape::Unit => "::reliakit_json::JsonValue::Null".to_string(),
}
}
fn json_decode_body(shape: &Shape) -> String {
match shape {
Shape::Named(fields) => {
let mut inner = String::new();
for field in fields {
let key = json_key(field);
let missing = format!("missing field `{key}`");
inner.push_str(&format!(
"{field}: ::reliakit_json::JsonDecode::from_json_value(\
__object.get({key:?}).ok_or_else(|| \
::reliakit_json::JsonDecodeError::missing_field({missing:?}))?)?,",
));
}
format!(
"let __object = __value.as_object().ok_or_else(|| \
::reliakit_json::JsonDecodeError::unexpected_type(\"expected a JSON object\"))?;\n\
::core::result::Result::Ok(Self {{ {inner} }})"
)
}
Shape::Tuple(count) => {
let mut inner = String::new();
for index in 0..*count {
inner.push_str(&format!(
"::reliakit_json::JsonDecode::from_json_value(&__array[{index}])?,"
));
}
format!(
"let __array = __value.as_array().ok_or_else(|| \
::reliakit_json::JsonDecodeError::unexpected_type(\"expected a JSON array\"))?;\n\
if __array.len() != {count} {{ return ::core::result::Result::Err(\
::reliakit_json::JsonDecodeError::unexpected_type(\
\"JSON array has the wrong number of elements\")); }}\n\
::core::result::Result::Ok(Self({inner}))"
)
}
Shape::Unit => "if !__value.is_null() {\n\
return ::core::result::Result::Err(\
::reliakit_json::JsonDecodeError::unexpected_type(\
\"expected JSON null for a unit struct\"));\n\
}\n\
::core::result::Result::Ok(Self)"
.to_string(),
}
}
fn csv_column(field: &str) -> &str {
field.strip_prefix("r#").unwrap_or(field)
}
fn csv_named_fields<'a>(body: &'a Body, trait_name: &str) -> Result<&'a [String], String> {
match body {
Body::Struct(Shape::Named(fields)) => Ok(fields),
Body::Struct(_) => Err(format!(
"reliakit-derive: {trait_name} requires a struct with named fields \
(CSV columns need names)"
)),
Body::Enum(_) => Err(format!(
"reliakit-derive: {trait_name} does not support enums"
)),
}
}
fn csv_encode_methods(fields: &[String]) -> String {
let mut header = String::new();
let mut pushes = String::new();
for field in fields {
let column = csv_column(field);
header.push_str(&format!("__header.push({column:?});"));
pushes.push_str(&format!(
"__out.push(::reliakit_csv::CsvField::encode_field(&self.{field}));"
));
}
format!(
"fn header() -> ::reliakit_csv::__private::Vec<&'static str> {{\n\
let mut __header = ::reliakit_csv::__private::Vec::new();\n\
{header}\n\
__header\n\
}}\n\
fn encode_fields(&self, __out: &mut ::reliakit_csv::__private::Vec<\
::reliakit_csv::__private::String>) {{\n\
{pushes}\n\
}}"
)
}
fn csv_decode_method(fields: &[String]) -> String {
let count = fields.len();
let mut inner = String::new();
for (index, field) in fields.iter().enumerate() {
inner.push_str(&format!(
"{field}: ::reliakit_csv::CsvField::decode_field(__fields[{index}])\
.map_err(|__e| __e.at_field({index}))?,"
));
}
format!(
"fn decode_fields(__fields: &[&str]) \
-> ::core::result::Result<Self, ::reliakit_csv::CsvDecodeError> {{\n\
if __fields.len() != {count} {{ return ::core::result::Result::Err(\
::reliakit_csv::CsvDecodeError::field_count()); }}\n\
::core::result::Result::Ok(Self {{ {inner} }})\n\
}}"
)
}
fn validate(raw: Raw) -> Result<Parsed, String> {
match raw.body {
RawBody::Union => Err("reliakit-derive does not support unions".into()),
RawBody::Struct(shape) => {
if raw.has_generics {
return Err("reliakit-derive does not support generic types yet".into());
}
Ok(Parsed {
name: raw.name,
body: Body::Struct(shape),
})
}
RawBody::Enum(raw_variants) => {
if raw.has_generics {
return Err("reliakit-derive does not support generic types yet".into());
}
if raw.saw_repr {
return Err("reliakit-derive does not support `#[repr(...)]` on enums; \
variant tags are always the u32 declaration index"
.into());
}
let mut variants = Vec::new();
for raw_variant in raw_variants {
if raw_variant.has_discriminant {
return Err(format!(
"reliakit-derive does not support explicit enum discriminants \
(`{} = ...`); variant tags are the u32 declaration index",
raw_variant.name
));
}
match raw_variant.shape {
Ok(shape) => variants.push(Variant {
name: raw_variant.name,
shape,
}),
Err(message) => return Err(message),
}
}
if variants.is_empty() {
return Err("reliakit-derive cannot derive for an empty enum \
(there is no variant to encode or decode)"
.into());
}
Ok(Parsed {
name: raw.name,
body: Body::Enum(variants),
})
}
}
}
fn classify(input: TokenStream) -> Result<Raw, String> {
let tokens: Vec<TokenTree> = input.into_iter().collect();
let mut idx = 0;
let mut saw_repr = false;
let kind = loop {
match tokens.get(idx) {
Some(TokenTree::Ident(ident)) => match ident.to_string().as_str() {
"struct" => break Kind::Struct,
"enum" => break Kind::Enum,
"union" => break Kind::Union,
_ => idx += 1,
},
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => {
if attr_is_repr(group.stream()) {
saw_repr = true;
}
idx += 1;
}
Some(_) => idx += 1,
None => return Err("reliakit-derive: expected a struct, enum, or union".into()),
}
};
idx += 1;
let name = match tokens.get(idx) {
Some(TokenTree::Ident(ident)) => ident.to_string(),
_ => return Err("reliakit-derive: expected a type name after the item keyword".into()),
};
idx += 1;
let has_generics =
matches!(tokens.get(idx), Some(TokenTree::Punct(punct)) if punct.as_char() == '<');
let body = if has_generics {
match kind {
Kind::Struct => RawBody::Struct(Shape::Unit),
Kind::Enum => RawBody::Enum(Vec::new()),
Kind::Union => RawBody::Union,
}
} else {
match kind {
Kind::Union => RawBody::Union,
Kind::Struct => match tokens.get(idx) {
Some(TokenTree::Group(group)) => match group.delimiter() {
Delimiter::Brace => RawBody::Struct(Shape::Named(named_fields(group.stream()))),
Delimiter::Parenthesis => {
RawBody::Struct(Shape::Tuple(count_fields(group.stream())))
}
_ => return Err("reliakit-derive: unexpected struct body".into()),
},
Some(TokenTree::Punct(punct)) if punct.as_char() == ';' => {
RawBody::Struct(Shape::Unit)
}
_ => return Err("reliakit-derive: unexpected struct body".into()),
},
Kind::Enum => match tokens.get(idx) {
Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Brace => {
RawBody::Enum(raw_variants(group.stream()))
}
_ => return Err("reliakit-derive: expected a braced enum body".into()),
},
}
};
Ok(Raw {
name,
has_generics,
saw_repr,
body,
})
}
fn struct_encode_statements(shape: &Shape) -> String {
let mut body = String::new();
match shape {
Shape::Named(fields) => {
for field in fields {
body.push_str(&format!(
"::reliakit_codec::CanonicalEncode::encode(&self.{field}, __writer)?;",
));
}
}
Shape::Tuple(count) => {
for index in 0..*count {
body.push_str(&format!(
"::reliakit_codec::CanonicalEncode::encode(&self.{index}, __writer)?;",
));
}
}
Shape::Unit => {}
}
body
}
fn struct_decode_value(shape: &Shape) -> String {
let construct = match shape {
Shape::Named(fields) => {
let mut inner = String::new();
for field in fields {
inner.push_str(&format!(
"{field}: ::reliakit_codec::CanonicalDecode::decode(__reader)?,",
));
}
format!("Self {{ {inner} }}")
}
Shape::Tuple(count) => {
let mut inner = String::new();
for _ in 0..*count {
inner.push_str("::reliakit_codec::CanonicalDecode::decode(__reader)?,");
}
format!("Self({inner})")
}
Shape::Unit => "Self".to_string(),
};
format!("::core::result::Result::Ok({construct})")
}
fn enum_encode_statements(variants: &[Variant]) -> String {
let mut arms = String::new();
for (index, variant) in variants.iter().enumerate() {
let tag = index as u32;
let name = &variant.name;
let tag_encode =
format!("::reliakit_codec::CanonicalEncode::encode(&{tag}u32, __writer)?;");
match &variant.shape {
Shape::Unit => {
arms.push_str(&format!("Self::{name} => {{ {tag_encode} }},"));
}
Shape::Tuple(count) => {
let mut pattern = String::new();
let mut encodes = String::new();
for i in 0..*count {
if i > 0 {
pattern.push_str(", ");
}
pattern.push_str(&format!("__f{i}"));
encodes.push_str(&format!(
"::reliakit_codec::CanonicalEncode::encode(__f{i}, __writer)?;",
));
}
arms.push_str(&format!(
"Self::{name}({pattern}) => {{ {tag_encode} {encodes} }},"
));
}
Shape::Named(fields) => {
let mut pattern = String::new();
let mut encodes = String::new();
for (i, field) in fields.iter().enumerate() {
if i > 0 {
pattern.push_str(", ");
}
pattern.push_str(&format!("{field}: __f{i}"));
encodes.push_str(&format!(
"::reliakit_codec::CanonicalEncode::encode(__f{i}, __writer)?;",
));
}
arms.push_str(&format!(
"Self::{name} {{ {pattern} }} => {{ {tag_encode} {encodes} }},"
));
}
}
}
format!("match self {{ {arms} }}")
}
fn enum_decode_value(name: &str, variants: &[Variant]) -> String {
let mut arms = String::new();
for (index, variant) in variants.iter().enumerate() {
let tag = index as u32;
let vname = &variant.name;
let construct = match &variant.shape {
Shape::Unit => format!("Self::{vname}"),
Shape::Tuple(count) => {
let mut inner = String::new();
for _ in 0..*count {
inner.push_str("::reliakit_codec::CanonicalDecode::decode(__reader)?,");
}
format!("Self::{vname}({inner})")
}
Shape::Named(fields) => {
let mut inner = String::new();
for field in fields {
inner.push_str(&format!(
"{field}: ::reliakit_codec::CanonicalDecode::decode(__reader)?,",
));
}
format!("Self::{vname} {{ {inner} }}")
}
};
arms.push_str(&format!("{tag}u32 => {construct},"));
}
let message = format!("reliakit-derive: unknown variant tag for {name}");
format!(
"let __tag: u32 = ::reliakit_codec::CanonicalDecode::decode(__reader)?;\n\
::core::result::Result::Ok(match __tag {{\n\
{arms}\n\
_ => return ::core::result::Result::Err(\
::reliakit_codec::CodecError::invalid_value({message:?})),\n\
}})"
)
}
fn raw_variants(stream: TokenStream) -> Vec<RawVariant> {
let mut variants = Vec::new();
for segment in top_level_segments(stream) {
if segment.is_empty() {
continue;
}
let name_idx = match segment
.iter()
.position(|t| matches!(t, TokenTree::Ident(_)))
{
Some(i) => i,
None => {
variants.push(RawVariant {
name: String::new(),
shape: Err("reliakit-derive: expected an enum variant name".into()),
has_discriminant: false,
});
continue;
}
};
let name = match &segment[name_idx] {
TokenTree::Ident(ident) => ident.to_string(),
_ => unreachable!("position matched an ident"),
};
let mut has_discriminant = false;
let shape = match segment.get(name_idx + 1) {
None => Ok(Shape::Unit),
Some(TokenTree::Group(group)) => match group.delimiter() {
Delimiter::Parenthesis => Ok(Shape::Tuple(count_fields(group.stream()))),
Delimiter::Brace => Ok(Shape::Named(named_fields(group.stream()))),
_ => Err(format!(
"reliakit-derive: unsupported syntax in enum variant `{name}`"
)),
},
Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => {
has_discriminant = true;
Ok(Shape::Unit)
}
Some(_) => Err(format!(
"reliakit-derive: unsupported syntax in enum variant `{name}`"
)),
};
variants.push(RawVariant {
name,
shape,
has_discriminant,
});
}
variants
}
fn attr_is_repr(stream: TokenStream) -> bool {
matches!(stream.into_iter().next(), Some(TokenTree::Ident(ident)) if ident.to_string() == "repr")
}
fn named_fields(stream: TokenStream) -> Vec<String> {
let mut fields = Vec::new();
for segment in top_level_segments(stream) {
for window in segment.windows(2) {
if let (TokenTree::Ident(ident), TokenTree::Punct(punct)) = (&window[0], &window[1]) {
if punct.as_char() == ':' && punct.spacing() == Spacing::Alone {
fields.push(ident.to_string());
break;
}
}
}
}
fields
}
fn count_fields(stream: TokenStream) -> usize {
top_level_segments(stream)
.into_iter()
.filter(|segment| !segment.is_empty())
.count()
}
fn top_level_segments(stream: TokenStream) -> Vec<Vec<TokenTree>> {
let mut segments = Vec::new();
let mut current = Vec::new();
for token in stream {
match &token {
TokenTree::Punct(punct) if punct.as_char() == ',' => {
segments.push(core::mem::take(&mut current));
}
_ => current.push(token),
}
}
if !current.is_empty() {
segments.push(current);
}
segments
}
fn compile_error(message: &str) -> TokenStream {
format!("::core::compile_error!({message:?});")
.parse()
.expect("compile_error message produced invalid tokens")
}
#[cfg(test)]
mod tests {
use super::*;
fn enum_raw(variants: Vec<RawVariant>, saw_repr: bool, has_generics: bool) -> Raw {
Raw {
name: "E".to_string(),
has_generics,
saw_repr,
body: RawBody::Enum(variants),
}
}
fn unit_variant(name: &str) -> RawVariant {
RawVariant {
name: name.to_string(),
shape: Ok(Shape::Unit),
has_discriminant: false,
}
}
fn err_of(raw: Raw) -> String {
match validate(raw) {
Err(message) => message,
Ok(_) => panic!("expected validation to reject the item"),
}
}
fn ok_of(raw: Raw) -> Parsed {
match validate(raw) {
Ok(parsed) => parsed,
Err(message) => panic!("unexpected validation error: {message}"),
}
}
#[test]
fn rejects_union() {
let raw = Raw {
name: "U".to_string(),
has_generics: false,
saw_repr: false,
body: RawBody::Union,
};
assert!(err_of(raw).contains("does not support unions"));
}
#[test]
fn rejects_generic_struct() {
let raw = Raw {
name: "S".to_string(),
has_generics: true,
saw_repr: false,
body: RawBody::Struct(Shape::Unit),
};
assert!(err_of(raw).contains("does not support generic types yet"));
}
#[test]
fn rejects_generic_enum() {
let raw = enum_raw(vec![unit_variant("A")], false, true);
assert!(err_of(raw).contains("does not support generic types yet"));
}
#[test]
fn rejects_repr_enum() {
let raw = enum_raw(vec![unit_variant("A")], true, false);
assert!(err_of(raw).contains("does not support `#[repr(...)]` on enums"));
}
#[test]
fn rejects_explicit_discriminant() {
let raw = enum_raw(
vec![RawVariant {
name: "A".to_string(),
shape: Ok(Shape::Unit),
has_discriminant: true,
}],
false,
false,
);
let err = err_of(raw);
assert!(err.contains("does not support explicit enum discriminants"));
assert!(err.contains("`A = ...`"));
}
#[test]
fn rejects_empty_enum() {
let raw = enum_raw(vec![], false, false);
assert!(err_of(raw).contains("cannot derive for an empty enum"));
}
#[test]
fn rejects_unsupported_variant_syntax() {
let raw = enum_raw(
vec![RawVariant {
name: "A".to_string(),
shape: Err("reliakit-derive: unsupported syntax in enum variant `A`".to_string()),
has_discriminant: false,
}],
false,
false,
);
assert!(err_of(raw).contains("unsupported syntax"));
}
#[test]
fn accepts_struct() {
let raw = Raw {
name: "S".to_string(),
has_generics: false,
saw_repr: false,
body: RawBody::Struct(Shape::Named(vec!["x".to_string()])),
};
let parsed = ok_of(raw);
assert_eq!(parsed.name, "S");
assert!(matches!(parsed.body, Body::Struct(Shape::Named(_))));
}
#[test]
fn accepts_enum_preserving_variant_order() {
let raw = enum_raw(
vec![
unit_variant("A"),
RawVariant {
name: "B".to_string(),
shape: Ok(Shape::Tuple(1)),
has_discriminant: false,
},
RawVariant {
name: "C".to_string(),
shape: Ok(Shape::Named(vec!["id".to_string()])),
has_discriminant: false,
},
],
false,
false,
);
match ok_of(raw).body {
Body::Enum(variants) => {
let names: Vec<&str> = variants.iter().map(|v| v.name.as_str()).collect();
assert_eq!(names, ["A", "B", "C"]);
}
Body::Struct(_) => panic!("expected an enum body"),
}
}
#[test]
fn csv_rejects_non_named_structs_and_enums() {
assert!(
csv_named_fields(&Body::Struct(Shape::Tuple(2)), "CsvEncode")
.unwrap_err()
.contains("requires a struct with named fields")
);
assert!(csv_named_fields(&Body::Struct(Shape::Unit), "CsvDecode")
.unwrap_err()
.contains("named fields"));
let enum_body = Body::Enum(vec![Variant {
name: "A".to_string(),
shape: Shape::Unit,
}]);
assert!(csv_named_fields(&enum_body, "CsvEncode")
.unwrap_err()
.contains("does not support enums"));
}
#[test]
fn csv_named_struct_builds_methods() {
let body = Body::Struct(Shape::Named(vec!["id".to_string(), "r#type".to_string()]));
let fields = csv_named_fields(&body, "CsvEncode").expect("named struct accepted");
let enc = csv_encode_methods(fields);
assert!(enc.contains("__header.push(\"id\")"));
assert!(enc.contains("__header.push(\"type\")"));
assert!(enc.contains("encode_field(&self.id)"));
assert!(enc.contains("encode_field(&self.r#type)"));
let dec = csv_decode_method(fields);
assert!(dec.contains("__fields.len() != 2"));
assert!(dec.contains("__fields[0]"));
assert!(dec.contains("at_field(1)"));
}
}