1#![allow(
2 missing_docs,
3 reason = "Intentional compatibility, platform, or test-only suppression."
4)]
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{Data, DeriveInput, Fields, parse_macro_input};
8
9#[proc_macro_derive(StringNewtype)]
28pub fn derive_string_newtype(input: TokenStream) -> TokenStream {
29 let input = parse_macro_input!(input as DeriveInput);
30 impl_string_newtype(&input).unwrap_or_else(|err| err.to_compile_error().into())
31}
32
33fn impl_string_newtype(input: &DeriveInput) -> syn::Result<TokenStream> {
34 let name = &input.ident;
35
36 let field_type = match &input.data {
38 Data::Struct(data) => match &data.fields {
39 Fields::Unnamed(fields) => {
40 if fields.unnamed.len() != 1 {
41 return Err(syn::Error::new_spanned(
42 name,
43 "StringNewtype requires a tuple struct with exactly one field",
44 ));
45 }
46 let Some(field) = fields.unnamed.first() else {
47 return Err(syn::Error::new_spanned(
48 name,
49 "StringNewtype requires a tuple struct with exactly one field",
50 ));
51 };
52 &field.ty
53 }
54 _ => {
55 return Err(syn::Error::new_spanned(name, "StringNewtype can only be derived for tuple structs"));
56 }
57 },
58 _ => {
59 return Err(syn::Error::new_spanned(name, "StringNewtype can only be derived for structs"));
60 }
61 };
62
63 if !is_string_type(field_type) {
65 return Err(syn::Error::new_spanned(field_type, "StringNewtype requires the inner type to be String"));
66 }
67
68 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
69
70 let output = quote! {
71 impl #impl_generics #name #ty_generics #where_clause {
72 pub fn new(value: impl Into<String>) -> Self {
74 Self(value.into())
75 }
76
77 pub fn as_str(&self) -> &str {
79 &self.0
80 }
81
82 pub fn into_inner(self) -> String {
84 self.0
85 }
86 }
87
88 impl #impl_generics std::ops::Deref for #name #ty_generics #where_clause {
89 type Target = str;
90
91 fn deref(&self) -> &Self::Target {
92 &self.0
93 }
94 }
95
96 impl #impl_generics std::borrow::Borrow<str> for #name #ty_generics #where_clause {
97 fn borrow(&self) -> &str {
98 &self.0
99 }
100 }
101
102 impl #impl_generics AsRef<str> for #name #ty_generics #where_clause {
103 fn as_ref(&self) -> &str {
104 &self.0
105 }
106 }
107
108 impl #impl_generics std::fmt::Display for #name #ty_generics #where_clause {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 self.0.fmt(f)
111 }
112 }
113
114 impl #impl_generics From<String> for #name #ty_generics #where_clause {
115 fn from(value: String) -> Self {
116 Self(value)
117 }
118 }
119
120 impl #impl_generics From<&str> for #name #ty_generics #where_clause {
121 fn from(value: &str) -> Self {
122 Self(value.to_string())
123 }
124 }
125
126 impl #impl_generics From<#name #ty_generics> for String #where_clause {
127 fn from(value: #name #ty_generics) -> Self {
128 value.0
129 }
130 }
131 };
132
133 Ok(output.into())
134}
135
136fn is_string_type(ty: &syn::Type) -> bool {
137 if let syn::Type::Path(type_path) = ty
138 && type_path.qself.is_none()
139 && type_path.path.segments.len() == 1
140 {
141 return type_path.path.segments.first().is_some_and(|segment| segment.ident == "String");
142 }
143 false
144}