From 799b30668fec6ebd9a8dba52625f4aa93b912d2d Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 6 Apr 2024 22:28:16 +0200 Subject: [PATCH] Allow deriving Arbitrary for String types --- CHANGELOG.md | 1 + Cargo.lock | 27 +- Cargo.toml | 1 + dummy/Cargo.toml | 1 + examples/string_arbitrary/Cargo.toml | 11 + examples/string_arbitrary/src/main.rs | 81 ++++++ nutype_macros/src/any/gen/traits/mod.rs | 22 +- nutype_macros/src/common/models.rs | 22 +- nutype_macros/src/string/gen/mod.rs | 7 +- .../src/string/gen/traits/arbitrary.rs | 246 ++++++++++++++++++ .../string/gen/{traits.rs => traits/mod.rs} | 58 +++-- nutype_macros/src/string/models.rs | 2 +- nutype_macros/src/string/validate.rs | 6 +- 13 files changed, 438 insertions(+), 47 deletions(-) create mode 100644 examples/string_arbitrary/Cargo.toml create mode 100644 examples/string_arbitrary/src/main.rs create mode 100644 nutype_macros/src/string/gen/traits/arbitrary.rs rename nutype_macros/src/string/gen/{traits.rs => traits/mod.rs} (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 389c4f8..8c72a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Support integration with [`arbitrary`](https://crates.io/crates/arbitrary) crate (see `arbitrary` feature). * Support `Arbitrary` for integer types * Support `Arbitrary` for float types + * Support `Arbitrary` for string inner types * Support `Arbitrary` for any inner types * Ability to specify boundaries (`greater`, `greater_or_equal`, `less`, `less_or_equal`, `len_char_min`, `len_char_max`) with expressions or named constants. * Add `#[inline]` attribute to trivial functions diff --git a/Cargo.lock b/Cargo.lock index 0e8377d..a634956 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ name = "any_arbitrary" version = "0.1.0" dependencies = [ "arbitrary", - "arbtest", + "arbtest 0.2.0", "nutype", ] @@ -38,6 +38,15 @@ dependencies = [ "arbitrary", ] +[[package]] +name = "arbtest" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23909d5fb517fac2a8a4c887e847dbe41dd22ec46914586f5727980d0a193fdc" +dependencies = [ + "arbitrary", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -110,6 +119,7 @@ checksum = "210ec60ae7d710bed8683e333e9d2855a8a56a3e9892b38bad3bb0d4d29b0d5e" name = "dummy" version = "0.1.0" dependencies = [ + "arbitrary", "lazy_static", "nutype", "once_cell", @@ -131,7 +141,7 @@ name = "float_arbitrary" version = "0.1.0" dependencies = [ "arbitrary", - "arbtest", + "arbtest 0.2.0", "nutype", ] @@ -153,7 +163,7 @@ name = "integer_arbitrary" version = "0.1.0" dependencies = [ "arbitrary", - "arbtest", + "arbtest 0.2.0", "nutype", ] @@ -413,6 +423,15 @@ dependencies = [ "serde", ] +[[package]] +name = "string_arbitrary" +version = "0.1.0" +dependencies = [ + "arbitrary", + "arbtest 0.3.1", + "nutype", +] + [[package]] name = "string_bounded_len" version = "0.1.0" @@ -465,7 +484,7 @@ name = "test_suite" version = "0.1.0" dependencies = [ "arbitrary", - "arbtest", + "arbtest 0.2.0", "lazy_static", "nutype", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 35cab2b..11ea57d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,5 @@ members = [ "examples/serde_complex", "examples/string_bounded_len", "examples/string_regex_email", + "examples/string_arbitrary", ] diff --git a/dummy/Cargo.toml b/dummy/Cargo.toml index c13e58d..6599e15 100644 --- a/dummy/Cargo.toml +++ b/dummy/Cargo.toml @@ -14,3 +14,4 @@ regex = "*" once_cell = "*" lazy_static = "*" ron = "0.8.1" +arbitrary = "1.3.2" diff --git a/examples/string_arbitrary/Cargo.toml b/examples/string_arbitrary/Cargo.toml new file mode 100644 index 0000000..92a3017 --- /dev/null +++ b/examples/string_arbitrary/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "string_arbitrary" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +arbitrary = "1.3.2" +arbtest = "0.3.1" +nutype = { path = "../../nutype", features = ["arbitrary"] } diff --git a/examples/string_arbitrary/src/main.rs b/examples/string_arbitrary/src/main.rs new file mode 100644 index 0000000..d1b6245 --- /dev/null +++ b/examples/string_arbitrary/src/main.rs @@ -0,0 +1,81 @@ +use arbitrary::Arbitrary; +use arbtest::arbtest; +use nutype::nutype; + +fn main() { + should_generate_arbitrary_string_without_validation_with_respect_to_sanitizers(); + should_generate_arbitrary_string_with_trim_and_min_len_validation(); + should_respect_not_empty_validation_with_trim(); + should_respect_not_empty_validation_without_trim(); + should_respect_len_char_max(); + should_respec_both_len_boundaries(); +} + +fn should_generate_arbitrary_string_without_validation_with_respect_to_sanitizers() { + #[nutype(sanitize(lowercase), derive(Arbitrary, Debug))] + struct LowercaseString(String); + + arbtest(|u| { + let s = LowercaseString::arbitrary(u)?.into_inner(); + assert_eq!(s.to_lowercase(), s); + Ok(()) + }); +} + +fn should_generate_arbitrary_string_with_trim_and_min_len_validation() { + #[nutype(sanitize(trim), validate(len_char_min = 3), derive(Arbitrary, Debug))] + struct Name(String); + + arbtest(|u| { + let s = Name::arbitrary(u)?.into_inner(); + assert_eq!(s.trim(), s); + assert!(s.chars().count() >= 3); + Ok(()) + }); +} + +fn should_respect_not_empty_validation_with_trim() { + #[nutype(sanitize(trim), validate(not_empty), derive(Arbitrary, Debug))] + struct Title(String); + + arbtest(|u| { + let s = Title::arbitrary(u)?.into_inner(); + assert_eq!(s.trim(), s); + assert!(!s.is_empty()); + Ok(()) + }); +} + +fn should_respect_not_empty_validation_without_trim() { + #[nutype(validate(not_empty), derive(Arbitrary, Debug))] + struct Description(String); + + arbtest(|u| { + let s = Description::arbitrary(u)?.into_inner(); + assert!(!s.is_empty()); + Ok(()) + }); +} + +fn should_respect_len_char_max() { + #[nutype(validate(len_char_max = 7), derive(Arbitrary, Debug))] + struct Text(String); + + arbtest(|u| { + let s = Text::arbitrary(u)?.into_inner(); + assert!(s.chars().count() <= 7); + Ok(()) + }); +} + +fn should_respec_both_len_boundaries() { + #[nutype(validate(len_char_min = 3, len_char_max = 5), derive(Arbitrary, Debug))] + struct Text(String); + + arbtest(|u| { + let s = Text::arbitrary(u)?.into_inner(); + assert!(s.chars().count() >= 3); + assert!(s.chars().count() <= 5); + Ok(()) + }); +} diff --git a/nutype_macros/src/any/gen/traits/mod.rs b/nutype_macros/src/any/gen/traits/mod.rs index 79f6c32..71e9e05 100644 --- a/nutype_macros/src/any/gen/traits/mod.rs +++ b/nutype_macros/src/any/gen/traits/mod.rs @@ -162,19 +162,17 @@ fn gen_implemented_traits( AnyIrregularTrait::TryFrom => Ok( gen_impl_trait_try_from(type_name, inner_type, maybe_error_type_name.as_ref()) ), - AnyIrregularTrait::Default => Ok( - match maybe_default_value { - Some(ref default_value) => { - let has_validation = maybe_error_type_name.is_some(); - gen_impl_trait_default(type_name, default_value, has_validation) - } - None => { - panic!( - "Default trait is derived for type {type_name}, but `default = ` is missing" - ); - } + AnyIrregularTrait::Default => match maybe_default_value { + Some(ref default_value) => { + let has_validation = maybe_error_type_name.is_some(); + Ok(gen_impl_trait_default(type_name, default_value, has_validation)) } - ), + None => { + let span = proc_macro2::Span::call_site(); + let msg = format!("Trait `Default` is derived for type {type_name}, but `default = ` parameter is missing in #[nutype] macro"); + Err(syn::Error::new(span, msg)) + } + }, AnyIrregularTrait::SerdeSerialize => Ok( gen_impl_trait_serde_serialize(type_name) ), diff --git a/nutype_macros/src/common/models.rs b/nutype_macros/src/common/models.rs index 5ac89ce..be8b53f 100644 --- a/nutype_macros/src/common/models.rs +++ b/nutype_macros/src/common/models.rs @@ -1,4 +1,5 @@ use kinded::Kinded; +use std::ops::Add; use std::{collections::HashSet, fmt::Debug}; use proc_macro2::{Span, TokenStream}; @@ -219,7 +220,7 @@ pub struct Attributes { /// Represents a value known at compile time or an expression. /// Knowing value at compile time allows to run some extra validations to prevent potential errors. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum ValueOrExpr { Value(T), Expr(syn::Expr), @@ -238,6 +239,25 @@ impl ToTokens for ValueOrExpr { } } +impl Add for ValueOrExpr +where + T: Add + ToTokens, +{ + type Output = ValueOrExpr; + + fn add(self, rhs: T) -> Self::Output { + match self { + Self::Value(lhs) => Self::Value(lhs + rhs), + Self::Expr(lhs) => { + let token_stream = quote!(#lhs + #rhs); + let expr = syn::parse2(token_stream) + .expect("Failed to parse token stream in ValueOrExpr::add"); + Self::Expr(expr) + } + } + } +} + impl Guard { pub fn has_validation(&self) -> bool { match self { diff --git a/nutype_macros/src/string/gen/mod.rs b/nutype_macros/src/string/gen/mod.rs index b32fa97..c5305c8 100644 --- a/nutype_macros/src/string/gen/mod.rs +++ b/nutype_macros/src/string/gen/mod.rs @@ -179,14 +179,15 @@ impl GenerateNewtype for StringNewtype { maybe_error_type_name: Option, traits: HashSet, maybe_default_value: Option, - _guard: &StringGuard, + guard: &StringGuard, ) -> Result { - Ok(gen_traits( + gen_traits( type_name, maybe_error_type_name, traits, maybe_default_value, - )) + guard, + ) } fn gen_tests( diff --git a/nutype_macros/src/string/gen/traits/arbitrary.rs b/nutype_macros/src/string/gen/traits/arbitrary.rs new file mode 100644 index 0000000..5cacc84 --- /dev/null +++ b/nutype_macros/src/string/gen/traits/arbitrary.rs @@ -0,0 +1,246 @@ +use kinded::Kinded; +use proc_macro2::{Span, TokenStream}; +use quote::quote; + +use crate::{ + common::models::{TypeName, ValueOrExpr}, + string::models::{StringGuard, StringSanitizer, StringValidator}, + utils::issue_reporter::{build_github_link_with_issue, Issue}, +}; + +pub fn gen_impl_trait_arbitrary( + type_name: &TypeName, + guard: &StringGuard, +) -> Result { + let construct_value = if guard.has_validation() { + // If by some reason we generate an invalid value, make it very easy for the user to report + let report_issue_msg = + build_github_link_with_issue(&Issue::ArbitraryGeneratedInvalidValue { + inner_type: "String".to_string(), + }); + let type_name = type_name.to_string(); + quote!( + Self::new(inner_value.clone()).unwrap_or_else(|err| { + // Panic with the maximum details about what went wrong + panic!("\nArbitrary generated an invalid value for {}.\nInvalid inner value: {:?}\nValidation error: {:?}\n\n{}", #type_name, inner_value, err, #report_issue_msg); + }) + ) + } else { + quote!(Self::new(inner_value)) + }; + + let maybe_spec = build_specification(guard)?; + let generate_inner_value = gen_generate_valid_inner_value(&maybe_spec); + let size_hint = gen_size_hint(&maybe_spec); + + Ok(quote!( + impl ::arbitrary::Arbitrary<'_> for #type_name { + fn arbitrary(u: &mut ::arbitrary::Unstructured<'_>) -> ::arbitrary::Result { + let inner_value: String = { + #generate_inner_value + }; + Ok(#construct_value) + } + + #[inline] + fn size_hint(depth: usize) -> (usize, Option) { + #size_hint + } + } + )) +} + +fn gen_size_hint(maybe_spec: &Option) -> TokenStream { + match maybe_spec { + Some(spec) => { + let Specification { + min_len, max_len, .. + } = spec; + quote!( + let min_size_hint = #min_len * core::mem::size_of::(); + let max_size_hint = #max_len * core::mem::size_of::(); + (min_size_hint, Some(max_size_hint)) + ) + } + None => { + // That corresponds to `quote!(u.arbitrary()?)` implementation + quote!(>::size_hint(depth)) + } + } +} + +fn gen_generate_valid_inner_value(maybe_spec: &Option) -> TokenStream { + match maybe_spec { + Some(spec) => gen_generate_valid_inner_value_with_validators(spec), + None => { + // When there is no validation, then we can just simply delegate to the arbitrary + // crate, and the job is done. + quote!(u.arbitrary()?) + } + } +} + +/// Subset of StringSanitizer, which is is possible to handle and is relevant for generating +/// arbitrary strings. +#[derive(Kinded)] +enum RelevantSanitizer { + Trim, +} + +/// Subset of StringValidator, which is is possible to handle and is relevant for generating +/// arbitrary strings. +#[derive(Kinded)] +enum RelevantValidator { + LenCharMin(ValueOrExpr), + LenCharMax(ValueOrExpr), +} + +/// Final specification to generate an arbitrary valid string +struct Specification { + has_trim: bool, + min_len: ValueOrExpr, + max_len: ValueOrExpr, +} + +/// If max length is not specified, then sum of min_len + this offset will be used. +const DEFAULT_LEN_OFFSET: usize = 16; + +fn build_specification(guard: &StringGuard) -> Result, syn::Error> { + match guard { + StringGuard::WithoutValidation { .. } => Ok(None), + StringGuard::WithValidation { + sanitizers, + validators, + } => { + let relevant_sanitizers = filter_sanitizers(sanitizers)?; + let relevant_validators = filter_validators(validators)?; + + let has_trim = relevant_sanitizers + .iter() + .any(|s| matches!(s, RelevantSanitizer::Trim)); + let min_len = relevant_validators + .iter() + .find_map(|v| { + if let RelevantValidator::LenCharMin(value) = v { + Some(value.clone()) + } else { + None + } + }) + .unwrap_or_else(|| ValueOrExpr::Value(0)); + let max_len = relevant_validators + .iter() + .find_map(|v| { + if let RelevantValidator::LenCharMax(value) = v { + Some(value.clone()) + } else { + None + } + }) + .unwrap_or_else(|| min_len.clone() + DEFAULT_LEN_OFFSET); + + let spec = Specification { + has_trim, + min_len, + max_len, + }; + Ok(Some(spec)) + } + } +} + +fn filter_validators(validators: &[StringValidator]) -> Result, syn::Error> { + validators.iter().map(|v| { + match v { + StringValidator::LenCharMin(value) => Ok(RelevantValidator::LenCharMin(value.clone())), + StringValidator::LenCharMax(value) => Ok(RelevantValidator::LenCharMax(value.clone())), + // In context of generating an arbitrary string NotEmpty is the same as LenCharMin(1) + StringValidator::NotEmpty => Ok(RelevantValidator::LenCharMin(ValueOrExpr::Value(1))), + StringValidator::Predicate(_) => { + let msg = "It's not possible to derive `Arbitrary` trait for a type with `predicate` validator.\nYou have to implement `Arbitrary` trait on you own."; + Err(syn::Error::new(Span::call_site(), msg)) + } + StringValidator::Regex(_) => { + let msg = "It's not possible to derive `Arbitrary` trait for a type with `regex` validator.\nYou have to implement `Arbitrary` trait on you own."; + Err(syn::Error::new(Span::call_site(), msg)) + } + } + }).collect() +} + +fn filter_sanitizers(sanitizers: &[StringSanitizer]) -> Result, syn::Error> { + sanitizers.iter().filter_map(|s| { + match s { + // Trim is relevant, because trimming a space can decrease string length and cause + // violation of len_char_min validation. + StringSanitizer::Trim => Some(Ok(RelevantSanitizer::Trim)), + // lowercase and uppercase sanitizers do not overlap with any of the validation rules, + // so we can ignore them + StringSanitizer::Lowercase => None, + StringSanitizer::Uppercase => None, + StringSanitizer::With(_) => { + let msg = "It's not possible to derive `Arbitrary` trait for a type with `with` sanitizer.\nYou have to implement `Arbitrary` trait on you own."; + Some(Err(syn::Error::new(Span::call_site(), msg))) + } + } + }).collect() +} + +fn gen_generate_valid_inner_value_with_validators(spec: &Specification) -> TokenStream { + let Specification { + has_trim, + min_len, + max_len, + } = spec; + + if *has_trim { + quote!( + // Pick randomly a target length + let target_len = u.int_in_range((#min_len)..=(#max_len))?; + // Generate string `output` that matches the target_len + let mut output = String::with_capacity(target_len * 2); + for _ in 0..target_len { + let ch: char = u.arbitrary()?; + output.push(ch); + } + // Make sure that the generated string matches the target_len + // after trimming the spaces. + loop { + let count = output.trim().chars().count(); + + match count.cmp(&target_len) { + core::cmp::Ordering::Equal => { + break; + } + core::cmp::Ordering::Less => { + // Try luck one more time: trim the spaces and add another char. + // NOTE: This is inefficient, but it's not expected to happen often. + output = output.trim().to_string(); + let new_char: char = u.arbitrary()?; + output.push(new_char); + } + core::cmp::Ordering::Greater => { + unreachable!( + "This should never happened. Generated string is longer then target_len." + ); + } + } + } + // Return the output string + output + ) + } else { + quote!( + // Pick randomly a target length + let target_len = u.int_in_range((#min_len)..=(#max_len))?; + // Generate string `output` that matches the target_len + let mut output = String::with_capacity(target_len * 2); + for _ in 0..target_len { + let ch: char = u.arbitrary()?; + output.push(ch); + } + // Return the output string + output + ) + } +} diff --git a/nutype_macros/src/string/gen/traits.rs b/nutype_macros/src/string/gen/traits/mod.rs similarity index 80% rename from nutype_macros/src/string/gen/traits.rs rename to nutype_macros/src/string/gen/traits/mod.rs index 792a2a7..42ad88b 100644 --- a/nutype_macros/src/string/gen/traits.rs +++ b/nutype_macros/src/string/gen/traits/mod.rs @@ -1,3 +1,5 @@ +pub mod arbitrary; + use std::collections::HashSet; use proc_macro2::TokenStream; @@ -14,7 +16,7 @@ use crate::{ }, models::{ErrorTypeName, TypeName}, }, - string::models::{StringDeriveTrait, StringInnerType}, + string::models::{StringDeriveTrait, StringGuard, StringInnerType}, }; type StringGeneratableTrait = GeneratableTrait; @@ -47,6 +49,7 @@ enum StringIrregularTrait { Default, SerdeSerialize, SerdeDeserialize, + ArbitraryArbitrary, } impl From for StringGeneratableTrait { @@ -109,6 +112,9 @@ impl From for StringGeneratableTrait { StringDeriveTrait::SchemarsJsonSchema => { StringGeneratableTrait::Transparent(StringTransparentTrait::SchemarsJsonSchema) } + StringDeriveTrait::ArbitraryArbitrary => { + StringGeneratableTrait::Irregular(StringIrregularTrait::ArbitraryArbitrary) + } } } } @@ -134,7 +140,8 @@ pub fn gen_traits( maybe_error_type_name: Option, traits: HashSet, maybe_default_value: Option, -) -> GeneratedTraits { + guard: &StringGuard, +) -> Result { let GeneratableTraits { transparent_traits, irregular_traits, @@ -151,12 +158,13 @@ pub fn gen_traits( maybe_error_type_name, maybe_default_value, irregular_traits, - ); + guard, + )?; - GeneratedTraits { + Ok(GeneratedTraits { derive_transparent_traits, implement_traits, - } + }) } fn gen_implemented_traits( @@ -164,41 +172,49 @@ fn gen_implemented_traits( maybe_error_type_name: Option, maybe_default_value: Option, impl_traits: Vec, -) -> TokenStream { + guard: &StringGuard, +) -> Result { let inner_type = StringInnerType; impl_traits .iter() .map(|t| match t { - StringIrregularTrait::AsRef => gen_impl_trait_as_ref(type_name, quote!(str)), - StringIrregularTrait::Deref => gen_impl_trait_deref(type_name, quote!(String)), + StringIrregularTrait::AsRef => Ok(gen_impl_trait_as_ref(type_name, quote!(str))), + StringIrregularTrait::Deref => Ok(gen_impl_trait_deref(type_name, quote!(String))), StringIrregularTrait::FromStr => { - gen_impl_from_str(type_name, maybe_error_type_name.as_ref()) + Ok(gen_impl_from_str(type_name, maybe_error_type_name.as_ref())) } - StringIrregularTrait::From => gen_impl_from_str_and_string(type_name), - StringIrregularTrait::Into => gen_impl_trait_into(type_name, inner_type), + StringIrregularTrait::From => Ok(gen_impl_from_str_and_string(type_name)), + StringIrregularTrait::Into => Ok(gen_impl_trait_into(type_name, inner_type)), StringIrregularTrait::TryFrom => { - gen_impl_try_from(type_name, maybe_error_type_name.as_ref()) + Ok(gen_impl_try_from(type_name, maybe_error_type_name.as_ref())) } - StringIrregularTrait::Borrow => gen_impl_borrow_str_and_string(type_name), - StringIrregularTrait::Display => gen_impl_trait_display(type_name), + StringIrregularTrait::Borrow => Ok(gen_impl_borrow_str_and_string(type_name)), + StringIrregularTrait::Display => Ok(gen_impl_trait_display(type_name)), StringIrregularTrait::Default => match maybe_default_value { Some(ref default_value) => { let has_validation = maybe_error_type_name.is_some(); - gen_impl_trait_default(type_name, default_value, has_validation) + Ok(gen_impl_trait_default( + type_name, + default_value, + has_validation, + )) } None => { - panic!( - "Default trait is derived for type {type_name}, but `default = ` is missing" - ); + let span = proc_macro2::Span::call_site(); + let msg = format!("Trait `Default` is derived for type {type_name}, but `default = ` parameter is missing in #[nutype] macro"); + Err(syn::Error::new(span, msg)) } }, - StringIrregularTrait::SerdeSerialize => gen_impl_trait_serde_serialize(type_name), - StringIrregularTrait::SerdeDeserialize => gen_impl_trait_serde_deserialize( + StringIrregularTrait::SerdeSerialize => Ok(gen_impl_trait_serde_serialize(type_name)), + StringIrregularTrait::SerdeDeserialize => Ok(gen_impl_trait_serde_deserialize( type_name, inner_type, maybe_error_type_name.as_ref(), - ), + )), + StringIrregularTrait::ArbitraryArbitrary => { + arbitrary::gen_impl_trait_arbitrary(type_name, guard) + } }) .collect() } diff --git a/nutype_macros/src/string/models.rs b/nutype_macros/src/string/models.rs index 46d2389..a0b3f42 100644 --- a/nutype_macros/src/string/models.rs +++ b/nutype_macros/src/string/models.rs @@ -74,7 +74,7 @@ pub enum StringDeriveTrait { SerdeSerialize, SerdeDeserialize, SchemarsJsonSchema, - // Arbitrary, + ArbitraryArbitrary, } impl TypeTrait for StringDeriveTrait { diff --git a/nutype_macros/src/string/validate.rs b/nutype_macros/src/string/validate.rs index 0126cb6..4dde52f 100644 --- a/nutype_macros/src/string/validate.rs +++ b/nutype_macros/src/string/validate.rs @@ -167,11 +167,7 @@ fn to_string_derive_trait( } } DeriveTrait::TryFrom => Ok(StringDeriveTrait::TryFrom), - DeriveTrait::ArbitraryArbitrary => { - // TODO: Implement deriving Arbitrary - let msg = "Deriving Arbitrary trait for string types is not yet implemented"; - Err(syn::Error::new(span, msg)) - } + DeriveTrait::ArbitraryArbitrary => Ok(StringDeriveTrait::ArbitraryArbitrary), } }