git.delta.rocks / jrsonnet / refs/commits / e94fcf5be983

difftreelog

fix(macros) derive(FromUntyped) type check

rqwlztywYaroslav Bolyukin2026-05-05parent: #b42af5c.patch.diff
in: master

1 file changed

modifiedcrates/jrsonnet-macros/src/typed.rsdiffbeforeafterboth
before · crates/jrsonnet-macros/src/typed.rs
1use proc_macro2::TokenStream;2use quote::quote;3use syn::{4	DeriveInput, Error, Ident, LitStr, Result, Token, Type, parenthesized,5	parse::{Parse, ParseStream},6	spanned::Spanned as _,7	token,8};910use crate::{extract_type_from_option, kw, names::Names, parse_attr, type_is_path};1112#[derive(Default)]13#[allow(clippy::struct_excessive_bools)]14struct TypedAttr {15	rename: Option<String>,16	aliases: Vec<String>,17	flatten: bool,18	/// flatten(ok) strategy for flattened optionals19	/// field would be None in case of any parsing error (as in serde)20	flatten_ok: bool,21	// Should it be `field+:` instead of `field:`22	add: bool,23	// Should it be `field::` instead of `field:`24	hide: bool,25	// Builtin value26	method: bool,27}28impl Parse for TypedAttr {29	fn parse(input: ParseStream) -> syn::Result<Self> {30		let mut out = Self::default();31		loop {32			let lookahead = input.lookahead1();33			if lookahead.peek(kw::rename) {34				input.parse::<kw::rename>()?;35				input.parse::<Token![=]>()?;36				let name = input.parse::<LitStr>()?;37				if out.rename.is_some() {38					return Err(Error::new(39						name.span(),40						"rename attribute may only be specified once",41					));42				}43				out.rename = Some(name.value());44			} else if lookahead.peek(kw::alias) {45				input.parse::<kw::alias>()?;46				input.parse::<Token![=]>()?;47				let alias = input.parse::<LitStr>()?;48				out.aliases.push(alias.value());49			} else if lookahead.peek(kw::flatten) {50				input.parse::<kw::flatten>()?;51				out.flatten = true;52				if input.peek(token::Paren) {53					let content;54					parenthesized!(content in input);55					let lookahead = content.lookahead1();56					if lookahead.peek(kw::ok) {57						content.parse::<kw::ok>()?;58						out.flatten_ok = true;59					} else {60						return Err(lookahead.error());61					}62				}63			} else if lookahead.peek(kw::add) {64				input.parse::<kw::add>()?;65				out.add = true;66			} else if lookahead.peek(kw::hide) {67				input.parse::<kw::hide>()?;68				out.hide = true;69			} else if lookahead.peek(kw::method) {70				input.parse::<kw::method>()?;71				out.method = true;72			} else if input.is_empty() {73				break;74			} else {75				return Err(lookahead.error());76			}77			if input.peek(Token![,]) {78				input.parse::<Token![,]>()?;79			} else {80				break;81			}82		}83		Ok(out)84	}85}86struct TypedField {87	attr: TypedAttr,88	ident: Ident,89	ty: Type,90	is_option: bool,91	is_lazy: bool,92}93impl TypedField {94	fn parse(field: &syn::Field) -> Result<Self> {95		let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();96		let Some(ident) = field.ident.clone() else {97			return Err(Error::new(98				field.span(),99				"this field should appear in output object, but it has no visible name",100			));101		};102		let (is_option, ty) = extract_type_from_option(&field.ty)?103			.map_or_else(|| (false, field.ty.clone()), |ty| (true, ty.clone()));104		if is_option && attr.flatten {105			if !attr.flatten_ok {106				return Err(Error::new(107					field.span(),108					"strategy should be set when flattening Option",109				));110			}111		} else if attr.flatten_ok {112			return Err(Error::new(113				field.span(),114				"flatten(ok) is only useable on optional fields",115			));116		}117118		let is_lazy = type_is_path(&ty, "Thunk").is_some();119120		Ok(Self {121			attr,122			ident,123			ty,124			is_option,125			is_lazy,126		})127	}128	/// None if this field is flattened in jsonnet output129	fn name(&self) -> Option<String> {130		if self.attr.flatten {131			return None;132		}133		Some(134			self.attr135				.rename136				.clone()137				.unwrap_or_else(|| self.ident.to_string()),138		)139	}140141	fn expand_field(&self) -> Option<TokenStream> {142		if self.is_option || self.attr.method {143			return None;144		}145		let name = self.name()?;146		let ty = &self.ty;147		Some(quote! {148			(#name, <#ty as Typed>::TYPE)149		})150	}151152	fn expand_parse(&self, names: &mut Names) -> TokenStream {153		if self.is_option {154			self.expand_parse_optional(names)155		} else {156			self.expand_parse_mandatory(names)157		}158	}159160	fn expand_parse_optional(&self, names: &mut Names) -> TokenStream {161		let ident = &self.ident;162		let ty = &self.ty;163164		// optional flatten is handled in same way as serde165		if self.attr.flatten {166			return quote! {167				#ident: <#ty as ParseTypedObj>::parse(&obj).ok(),168			};169		}170171		let name = names.intern(self.name().unwrap());172		let aliases = self173			.attr174			.aliases175			.iter()176			.map(|name| names.intern(name))177			.collect::<Vec<_>>();178179		quote! {180			#ident: {181				let __value = if let Some(__v) = obj.get(__names[#name].clone())? {182					Some(__v)183				} #(else if let Some(__v) = obj.get(__names[#aliases].clone())? {184					Some(__v)185				})* else {186					None187				};188189				__value.map(<#ty as FromUntyped>::from_untyped).transpose()?190			},191		}192	}193194	fn expand_parse_mandatory(&self, names: &mut Names) -> TokenStream {195		let ident = &self.ident;196		let ty = &self.ty;197198		// optional flatten is handled in same way as serde199		if self.attr.flatten {200			return quote! {201				#ident: <#ty as ParseTypedObj>::parse(&obj)?,202			};203		}204205		let name = self.name().unwrap();206		let aliases = &self.attr.aliases;207208		let error_text = if aliases.is_empty() {209			// clippy does not understand name variable usage in quote! macro210			#[allow(clippy::redundant_clone)]211			name.clone()212		} else {213			format!("{name} (alias {})", aliases.join(", "))214		};215216		let error_text = names.intern(error_text);217		let name = names.intern(name);218		let aliases = aliases.iter().map(|alias| names.intern(alias));219220		quote! {221			#ident: {222				let __value = if let Some(__v) = obj.get(__names[#name].clone())? {223					__v224				} #(else if let Some(__v) = obj.get(__names[#aliases].clone())? {225					__v226				})* else {227					return Err(ErrorKind::NoSuchField(__names[#error_text].clone(), vec![]).into());228				};229230				<#ty as FromUntyped>::from_untyped(__value)?231			},232		}233	}234235	fn expand_serialize(&self, names: &mut Names) -> TokenStream {236		let ident = &self.ident;237		let ty = &self.ty;238		self.name().map_or_else(239			|| {240				if self.is_option {241					quote! {242						if let Some(value) = self.#ident {243							<#ty as SerializeTypedObj>::serialize(value, out)?;244						}245					}246				} else {247					quote! {248						<#ty as SerializeTypedObj>::serialize(self.#ident, out)?;249					}250				}251			},252			|name| {253				let name = names.intern(name);254				let hide = if self.attr.hide {255					quote! {.hide()}256				} else {257					quote! {}258				};259				let add = if self.attr.add {260					quote! {.add()}261				} else {262					quote! {}263				};264				let value = if self.attr.method {265					quote! {266						out.method(__names[#name].clone(), value);267					}268				} else if self.is_lazy {269					quote! {270						out.field(__names[#name].clone())271							#hide272							#add273							.try_thunk(<#ty as IntoUntyped>::into_lazy_untyped(value))?;274					}275				} else {276					quote! {277						out.field(__names[#name].clone())278							#hide279							#add280							.try_value(<#ty as IntoUntyped>::into_untyped(value)?)?;281					}282				};283				if self.is_option {284					quote! {285						if let Some(value) = self.#ident {286							#value287						}288					}289				} else {290					quote! {291						{292							let value = self.#ident;293							#value294						}295					}296				}297			},298		)299	}300}301302pub fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {303	let syn::Data::Struct(data) = &input.data else {304		return Err(Error::new(input.span(), "only structs supported"));305	};306307	let ident = &input.ident;308	let fields = data309		.fields310		.iter()311		.map(TypedField::parse)312		.collect::<Result<Vec<_>>>()?;313314	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();315316	let fields = fields317		.iter()318		.filter_map(TypedField::expand_field)319		.collect::<Vec<_>>();320	Ok(quote! {321		const _: () = {322			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;323324			impl #impl_generics Typed for #ident #ty_generics #where_clause {325				const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&[326					#(#fields,)*327				]);328			}329		};330	})331}332pub fn derive_into_untyped_inner(input: DeriveInput) -> Result<TokenStream> {333	let syn::Data::Struct(data) = &input.data else {334		return Err(Error::new(input.span(), "only structs supported"));335	};336337	let ident = &input.ident;338	let fields = data339		.fields340		.iter()341		.map(TypedField::parse)342		.collect::<Result<Vec<_>>>()?;343344	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();345346	let capacity = fields.len();347348	let mut names = Names::default();349350	let fields_serialize = fields351		.iter()352		.map(|f| f.expand_serialize(&mut names))353		.collect::<Vec<_>>();354355	let names_expanded = names.expand();356	Ok(quote! {357		const _: () = {358			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;359360			impl #impl_generics IntoUntyped for #ident #ty_generics #where_clause {361				fn into_untyped(value: Self) -> JrResult<Val> {362					let mut out = ObjValueBuilder::with_capacity(#capacity);363					value.serialize(&mut out)?;364					Ok(Val::Obj(out.build()))365				}366			}367368			#names_expanded369370			impl #impl_generics SerializeTypedObj for #ident #ty_generics #where_clause {371				fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {372					NAMES.with(|__names| {373						#(#fields_serialize)*374375						Ok(())376					})377				}378			}379		};380	})381}382pub fn derive_from_untyped_inner(input: DeriveInput) -> Result<TokenStream> {383	let syn::Data::Struct(data) = &input.data else {384		return Err(Error::new(input.span(), "only structs supported"));385	};386387	let ident = &input.ident;388	let fields = data389		.fields390		.iter()391		.map(TypedField::parse)392		.collect::<Result<Vec<_>>>()?;393394	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();395396	let mut names = Names::default();397398	let fields_parse = fields399		.iter()400		.map(|f| f.expand_parse(&mut names))401		.collect::<Vec<_>>();402403	let names_expanded = names.expand();404	Ok(quote! {405		const _: () = {406			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;407408			impl #impl_generics FromUntyped for #ident #ty_generics #where_clause {409				fn from_untyped(value: Val) -> JrResult<Self> {410					let obj = value.as_obj().expect("shape is correct");411					Self::parse(&obj)412				}413			}414415			#names_expanded416417			impl #impl_generics ParseTypedObj for #ident #ty_generics #where_clause {418				fn parse(obj: &ObjValue) -> JrResult<Self> {419					NAMES.with(|__names| Ok(Self {420						#(#fields_parse)*421					}))422				}423			}424		};425	})426}
after · crates/jrsonnet-macros/src/typed.rs
1use proc_macro2::TokenStream;2use quote::quote;3use syn::{4	DeriveInput, Error, Ident, LitStr, Result, Token, Type, parenthesized,5	parse::{Parse, ParseStream},6	spanned::Spanned as _,7	token,8};910use crate::{extract_type_from_option, kw, names::Names, parse_attr, type_is_path};1112#[derive(Default)]13#[allow(clippy::struct_excessive_bools)]14struct TypedAttr {15	rename: Option<String>,16	aliases: Vec<String>,17	flatten: bool,18	/// flatten(ok) strategy for flattened optionals19	/// field would be None in case of any parsing error (as in serde)20	flatten_ok: bool,21	// Should it be `field+:` instead of `field:`22	add: bool,23	// Should it be `field::` instead of `field:`24	hide: bool,25	// Builtin value26	method: bool,27}28impl Parse for TypedAttr {29	fn parse(input: ParseStream) -> syn::Result<Self> {30		let mut out = Self::default();31		loop {32			let lookahead = input.lookahead1();33			if lookahead.peek(kw::rename) {34				input.parse::<kw::rename>()?;35				input.parse::<Token![=]>()?;36				let name = input.parse::<LitStr>()?;37				if out.rename.is_some() {38					return Err(Error::new(39						name.span(),40						"rename attribute may only be specified once",41					));42				}43				out.rename = Some(name.value());44			} else if lookahead.peek(kw::alias) {45				input.parse::<kw::alias>()?;46				input.parse::<Token![=]>()?;47				let alias = input.parse::<LitStr>()?;48				out.aliases.push(alias.value());49			} else if lookahead.peek(kw::flatten) {50				input.parse::<kw::flatten>()?;51				out.flatten = true;52				if input.peek(token::Paren) {53					let content;54					parenthesized!(content in input);55					let lookahead = content.lookahead1();56					if lookahead.peek(kw::ok) {57						content.parse::<kw::ok>()?;58						out.flatten_ok = true;59					} else {60						return Err(lookahead.error());61					}62				}63			} else if lookahead.peek(kw::add) {64				input.parse::<kw::add>()?;65				out.add = true;66			} else if lookahead.peek(kw::hide) {67				input.parse::<kw::hide>()?;68				out.hide = true;69			} else if lookahead.peek(kw::method) {70				input.parse::<kw::method>()?;71				out.method = true;72			} else if input.is_empty() {73				break;74			} else {75				return Err(lookahead.error());76			}77			if input.peek(Token![,]) {78				input.parse::<Token![,]>()?;79			} else {80				break;81			}82		}83		Ok(out)84	}85}86struct TypedField {87	attr: TypedAttr,88	ident: Ident,89	ty: Type,90	is_option: bool,91	is_lazy: bool,92}93impl TypedField {94	fn parse(field: &syn::Field) -> Result<Self> {95		let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();96		let Some(ident) = field.ident.clone() else {97			return Err(Error::new(98				field.span(),99				"this field should appear in output object, but it has no visible name",100			));101		};102		let (is_option, ty) = extract_type_from_option(&field.ty)?103			.map_or_else(|| (false, field.ty.clone()), |ty| (true, ty.clone()));104		if is_option && attr.flatten {105			if !attr.flatten_ok {106				return Err(Error::new(107					field.span(),108					"strategy should be set when flattening Option",109				));110			}111		} else if attr.flatten_ok {112			return Err(Error::new(113				field.span(),114				"flatten(ok) is only useable on optional fields",115			));116		}117118		let is_lazy = type_is_path(&ty, "Thunk").is_some();119120		Ok(Self {121			attr,122			ident,123			ty,124			is_option,125			is_lazy,126		})127	}128	/// None if this field is flattened in jsonnet output129	fn name(&self) -> Option<String> {130		if self.attr.flatten {131			return None;132		}133		Some(134			self.attr135				.rename136				.clone()137				.unwrap_or_else(|| self.ident.to_string()),138		)139	}140141	fn expand_field(&self) -> Option<TokenStream> {142		if self.is_option || self.attr.method {143			return None;144		}145		let name = self.name()?;146		let ty = &self.ty;147		Some(quote! {148			(#name, <#ty as Typed>::TYPE)149		})150	}151152	fn expand_parse(&self, names: &mut Names) -> TokenStream {153		if self.is_option {154			self.expand_parse_optional(names)155		} else {156			self.expand_parse_mandatory(names)157		}158	}159160	fn expand_parse_optional(&self, names: &mut Names) -> TokenStream {161		let ident = &self.ident;162		let ty = &self.ty;163164		// optional flatten is handled in same way as serde165		if self.attr.flatten {166			return quote! {167				#ident: <#ty as ParseTypedObj>::parse(&obj).ok(),168			};169		}170171		let name = names.intern(self.name().unwrap());172		let aliases = self173			.attr174			.aliases175			.iter()176			.map(|name| names.intern(name))177			.collect::<Vec<_>>();178179		quote! {180			#ident: {181				let __value = if let Some(__v) = obj.get(__names[#name].clone())? {182					Some(__v)183				} #(else if let Some(__v) = obj.get(__names[#aliases].clone())? {184					Some(__v)185				})* else {186					None187				};188189				__value.map(<#ty as FromUntyped>::from_untyped).transpose()?190			},191		}192	}193194	fn expand_parse_mandatory(&self, names: &mut Names) -> TokenStream {195		let ident = &self.ident;196		let ty = &self.ty;197198		// optional flatten is handled in same way as serde199		if self.attr.flatten {200			return quote! {201				#ident: <#ty as ParseTypedObj>::parse(&obj)?,202			};203		}204205		let name = self.name().unwrap();206		let aliases = &self.attr.aliases;207208		let error_text = if aliases.is_empty() {209			// clippy does not understand name variable usage in quote! macro210			#[allow(clippy::redundant_clone)]211			name.clone()212		} else {213			format!("{name} (alias {})", aliases.join(", "))214		};215216		let error_text = names.intern(error_text);217		let name = names.intern(name);218		let aliases = aliases.iter().map(|alias| names.intern(alias));219220		quote! {221			#ident: {222				let __value = if let Some(__v) = obj.get(__names[#name].clone())? {223					__v224				} #(else if let Some(__v) = obj.get(__names[#aliases].clone())? {225					__v226				})* else {227					return Err(ErrorKind::NoSuchField(__names[#error_text].clone(), vec![]).into());228				};229230				<#ty as FromUntyped>::from_untyped(__value)?231			},232		}233	}234235	fn expand_serialize(&self, names: &mut Names) -> TokenStream {236		let ident = &self.ident;237		let ty = &self.ty;238		self.name().map_or_else(239			|| {240				if self.is_option {241					quote! {242						if let Some(value) = self.#ident {243							<#ty as SerializeTypedObj>::serialize(value, out)?;244						}245					}246				} else {247					quote! {248						<#ty as SerializeTypedObj>::serialize(self.#ident, out)?;249					}250				}251			},252			|name| {253				let name = names.intern(name);254				let hide = if self.attr.hide {255					quote! {.hide()}256				} else {257					quote! {}258				};259				let add = if self.attr.add {260					quote! {.add()}261				} else {262					quote! {}263				};264				let value = if self.attr.method {265					quote! {266						out.method(__names[#name].clone(), value);267					}268				} else if self.is_lazy {269					quote! {270						out.field(__names[#name].clone())271							#hide272							#add273							.try_thunk(<#ty as IntoUntyped>::into_lazy_untyped(value))?;274					}275				} else {276					quote! {277						out.field(__names[#name].clone())278							#hide279							#add280							.try_value(<#ty as IntoUntyped>::into_untyped(value)?)?;281					}282				};283				if self.is_option {284					quote! {285						if let Some(value) = self.#ident {286							#value287						}288					}289				} else {290					quote! {291						{292							let value = self.#ident;293							#value294						}295					}296				}297			},298		)299	}300}301302pub fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {303	let syn::Data::Struct(data) = &input.data else {304		return Err(Error::new(input.span(), "only structs supported"));305	};306307	let ident = &input.ident;308	let fields = data309		.fields310		.iter()311		.map(TypedField::parse)312		.collect::<Result<Vec<_>>>()?;313314	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();315316	let fields = fields317		.iter()318		.filter_map(TypedField::expand_field)319		.collect::<Vec<_>>();320	Ok(quote! {321		const _: () = {322			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;323324			impl #impl_generics Typed for #ident #ty_generics #where_clause {325				const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&[326					#(#fields,)*327				]);328			}329		};330	})331}332pub fn derive_into_untyped_inner(input: DeriveInput) -> Result<TokenStream> {333	let syn::Data::Struct(data) = &input.data else {334		return Err(Error::new(input.span(), "only structs supported"));335	};336337	let ident = &input.ident;338	let fields = data339		.fields340		.iter()341		.map(TypedField::parse)342		.collect::<Result<Vec<_>>>()?;343344	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();345346	let capacity = fields.len();347348	let mut names = Names::default();349350	let fields_serialize = fields351		.iter()352		.map(|f| f.expand_serialize(&mut names))353		.collect::<Vec<_>>();354355	let names_expanded = names.expand();356	Ok(quote! {357		const _: () = {358			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;359360			impl #impl_generics IntoUntyped for #ident #ty_generics #where_clause {361				fn into_untyped(value: Self) -> JrResult<Val> {362					let mut out = ObjValueBuilder::with_capacity(#capacity);363					value.serialize(&mut out)?;364					Ok(Val::Obj(out.build()))365				}366			}367368			#names_expanded369370			impl #impl_generics SerializeTypedObj for #ident #ty_generics #where_clause {371				fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {372					NAMES.with(|__names| {373						#(#fields_serialize)*374375						Ok(())376					})377				}378			}379		};380	})381}382pub fn derive_from_untyped_inner(input: DeriveInput) -> Result<TokenStream> {383	let syn::Data::Struct(data) = &input.data else {384		return Err(Error::new(input.span(), "only structs supported"));385	};386387	let ident = &input.ident;388	let fields = data389		.fields390		.iter()391		.map(TypedField::parse)392		.collect::<Result<Vec<_>>>()?;393394	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();395396	let mut names = Names::default();397398	let fields_parse = fields399		.iter()400		.map(|f| f.expand_parse(&mut names))401		.collect::<Vec<_>>();402403	let names_expanded = names.expand();404	Ok(quote! {405		const _: () = {406			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;407408			impl #impl_generics FromUntyped for #ident #ty_generics #where_clause {409				fn from_untyped(value: Val) -> JrResult<Self> {410					<Self as Typed>::TYPE.check(&value)?;411					let obj = value.as_obj().expect("shape is correct");412					Self::parse(&obj)413				}414			}415416			#names_expanded417418			impl #impl_generics ParseTypedObj for #ident #ty_generics #where_clause {419				fn parse(obj: &ObjValue) -> JrResult<Self> {420					NAMES.with(|__names| Ok(Self {421						#(#fields_parse)*422					}))423				}424			}425		};426	})427}