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

difftreelog

feat(macros) #[typed(method)]

tzummrkzYaroslav Bolyukin2026-04-25parent: #9fe9fbb.patch.diff
in: master

2 files changed

modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -127,6 +127,7 @@
 	syn::custom_keyword!(flatten);
 	syn::custom_keyword!(add);
 	syn::custom_keyword!(hide);
+	syn::custom_keyword!(method);
 	syn::custom_keyword!(ok);
 }
 
@@ -381,18 +382,8 @@
 
 	let name = &fun.sig.ident;
 	let vis = &fun.vis;
-	let static_ext = if attr.fields.is_empty() {
-		quote! {
-			impl #name {
-				pub const INST: &'static dyn StaticBuiltin = &#name {};
-			}
-			impl StaticBuiltin for #name {}
-		}
-	} else {
-		quote! {}
-	};
 	let static_derive_copy = if attr.fields.is_empty() {
-		quote! {, Copy}
+		quote! {, Copy, Default}
 	} else {
 		quote! {}
 	};
@@ -409,7 +400,7 @@
 		const _: () = {
 			use ::jrsonnet_evaluator::{
 				State, Val,
-				function::{builtin::{Builtin, StaticBuiltin}, FunctionSignature, ParamParse, ParamName, ParamDefault, CallLocation},
+				function::{builtin::Builtin, FunctionSignature, ParamParse, ParamName, ParamDefault, CallLocation},
 				Result, Context, typed::{Typed, FromUntyped, IntoUntypedResult},
 				parser::Span, params, Thunk,
 			};
@@ -417,7 +408,6 @@
 				#(#params_desc)*
 			);
 
-			#static_ext
 			impl Builtin for #name
 			where
 				Self: 'static
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}26impl Parse for TypedAttr {27	fn parse(input: ParseStream) -> syn::Result<Self> {28		let mut out = Self::default();29		loop {30			let lookahead = input.lookahead1();31			if lookahead.peek(kw::rename) {32				input.parse::<kw::rename>()?;33				input.parse::<Token![=]>()?;34				let name = input.parse::<LitStr>()?;35				if out.rename.is_some() {36					return Err(Error::new(37						name.span(),38						"rename attribute may only be specified once",39					));40				}41				out.rename = Some(name.value());42			} else if lookahead.peek(kw::alias) {43				input.parse::<kw::alias>()?;44				input.parse::<Token![=]>()?;45				let alias = input.parse::<LitStr>()?;46				out.aliases.push(alias.value());47			} else if lookahead.peek(kw::flatten) {48				input.parse::<kw::flatten>()?;49				out.flatten = true;50				if input.peek(token::Paren) {51					let content;52					parenthesized!(content in input);53					let lookahead = content.lookahead1();54					if lookahead.peek(kw::ok) {55						content.parse::<kw::ok>()?;56						out.flatten_ok = true;57					} else {58						return Err(lookahead.error());59					}60				}61			} else if lookahead.peek(kw::add) {62				input.parse::<kw::add>()?;63				out.add = true;64			} else if lookahead.peek(kw::hide) {65				input.parse::<kw::hide>()?;66				out.hide = true;67			} else if input.is_empty() {68				break;69			} else {70				return Err(lookahead.error());71			}72			if input.peek(Token![,]) {73				input.parse::<Token![,]>()?;74			} else {75				break;76			}77		}78		Ok(out)79	}80}81struct TypedField {82	attr: TypedAttr,83	ident: Ident,84	ty: Type,85	is_option: bool,86	is_lazy: bool,87}88impl TypedField {89	fn parse(field: &syn::Field) -> Result<Self> {90		let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();91		let Some(ident) = field.ident.clone() else {92			return Err(Error::new(93				field.span(),94				"this field should appear in output object, but it has no visible name",95			));96		};97		let (is_option, ty) = extract_type_from_option(&field.ty)?98			.map_or_else(|| (false, field.ty.clone()), |ty| (true, ty.clone()));99		if is_option && attr.flatten {100			if !attr.flatten_ok {101				return Err(Error::new(102					field.span(),103					"strategy should be set when flattening Option",104				));105			}106		} else if attr.flatten_ok {107			return Err(Error::new(108				field.span(),109				"flatten(ok) is only useable on optional fields",110			));111		}112113		let is_lazy = type_is_path(&ty, "Thunk").is_some();114115		Ok(Self {116			attr,117			ident,118			ty,119			is_option,120			is_lazy,121		})122	}123	/// None if this field is flattened in jsonnet output124	fn name(&self) -> Option<String> {125		if self.attr.flatten {126			return None;127		}128		Some(129			self.attr130				.rename131				.clone()132				.unwrap_or_else(|| self.ident.to_string()),133		)134	}135136	fn expand_field(&self) -> Option<TokenStream> {137		if self.is_option {138			return None;139		}140		let name = self.name()?;141		let ty = &self.ty;142		Some(quote! {143			(#name, <#ty as Typed>::TYPE)144		})145	}146147	fn expand_parse(&self, names: &mut Names) -> TokenStream {148		if self.is_option {149			self.expand_parse_optional(names)150		} else {151			self.expand_parse_mandatory(names)152		}153	}154155	fn expand_parse_optional(&self, names: &mut Names) -> TokenStream {156		let ident = &self.ident;157		let ty = &self.ty;158159		// optional flatten is handled in same way as serde160		if self.attr.flatten {161			return quote! {162				#ident: <#ty as ParseTypedObj>::parse(&obj).ok(),163			};164		}165166		let name = names.intern(self.name().unwrap());167		let aliases = self168			.attr169			.aliases170			.iter()171			.map(|name| names.intern(name))172			.collect::<Vec<_>>();173174		quote! {175			#ident: {176				let __value = if let Some(__v) = obj.get(__names[#name].clone())? {177					Some(__v)178				} #(else if let Some(__v) = obj.get(__names[#aliases].clone())? {179					Some(__v)180				})* else {181					None182				};183184				__value.map(<#ty as FromUntyped>::from_untyped).transpose()?185			},186		}187	}188189	fn expand_parse_mandatory(&self, names: &mut Names) -> TokenStream {190		let ident = &self.ident;191		let ty = &self.ty;192193		// optional flatten is handled in same way as serde194		if self.attr.flatten {195			return quote! {196				#ident: <#ty as ParseTypedObj>::parse(&obj)?,197			};198		}199200		let name = self.name().unwrap();201		let aliases = &self.attr.aliases;202203		let error_text = if aliases.is_empty() {204			// clippy does not understand name variable usage in quote! macro205			#[allow(clippy::redundant_clone)]206			name.clone()207		} else {208			format!("{name} (alias {})", aliases.join(", "))209		};210211		let error_text = names.intern(error_text);212		let name = names.intern(name);213		let aliases = aliases.iter().map(|alias| names.intern(alias));214215		quote! {216			#ident: {217				let __value = if let Some(__v) = obj.get(__names[#name].clone())? {218					__v219				} #(else if let Some(__v) = obj.get(__names[#aliases].clone())? {220					__v221				})* else {222					return Err(ErrorKind::NoSuchField(__names[#error_text].clone(), vec![]).into());223				};224225				<#ty as FromUntyped>::from_untyped(__value)?226			},227		}228	}229230	fn expand_serialize(&self, names: &mut Names) -> TokenStream {231		let ident = &self.ident;232		let ty = &self.ty;233		self.name().map_or_else(234			|| {235				if self.is_option {236					quote! {237						if let Some(value) = self.#ident {238							<#ty as SerializeTypedObj>::serialize(value, out)?;239						}240					}241				} else {242					quote! {243						<#ty as SerializeTypedObj>::serialize(self.#ident, out)?;244					}245				}246			},247			|name| {248				let name = names.intern(name);249				let hide = if self.attr.hide {250					quote! {.hide()}251				} else {252					quote! {}253				};254				let add = if self.attr.add {255					quote! {.add()}256				} else {257					quote! {}258				};259				let value = if self.is_lazy {260					quote! {261						out.field(__names[#name].clone())262							#hide263							#add264							.try_thunk(<#ty as IntoUntyped>::into_lazy_untyped(value))?;265					}266				} else {267					quote! {268						out.field(__names[#name].clone())269							#hide270							#add271							.try_value(<#ty as IntoUntyped>::into_untyped(value)?)?;272					}273				};274				if self.is_option {275					quote! {276						if let Some(value) = self.#ident {277							#value278						}279					}280				} else {281					quote! {282						{283							let value = self.#ident;284							#value285						}286					}287				}288			},289		)290	}291}292293pub fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {294	let syn::Data::Struct(data) = &input.data else {295		return Err(Error::new(input.span(), "only structs supported"));296	};297298	let ident = &input.ident;299	let fields = data300		.fields301		.iter()302		.map(TypedField::parse)303		.collect::<Result<Vec<_>>>()?;304305	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();306307	let fields = fields308		.iter()309		.filter_map(TypedField::expand_field)310		.collect::<Vec<_>>();311	Ok(quote! {312		const _: () = {313			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;314315			impl #impl_generics Typed for #ident #ty_generics #where_clause {316				const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&[317					#(#fields,)*318				]);319			}320		};321	})322}323pub fn derive_into_untyped_inner(input: DeriveInput) -> Result<TokenStream> {324	let syn::Data::Struct(data) = &input.data else {325		return Err(Error::new(input.span(), "only structs supported"));326	};327328	let ident = &input.ident;329	let fields = data330		.fields331		.iter()332		.map(TypedField::parse)333		.collect::<Result<Vec<_>>>()?;334335	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();336337	let capacity = fields.len();338339	let mut names = Names::default();340341	let fields_serialize = fields342		.iter()343		.map(|f| f.expand_serialize(&mut names))344		.collect::<Vec<_>>();345346	let names_expanded = names.expand();347	Ok(quote! {348		const _: () = {349			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;350351			impl #impl_generics IntoUntyped for #ident #ty_generics #where_clause {352				fn into_untyped(value: Self) -> JrResult<Val> {353					let mut out = ObjValueBuilder::with_capacity(#capacity);354					value.serialize(&mut out)?;355					Ok(Val::Obj(out.build()))356				}357			}358359			#names_expanded360361			impl #impl_generics SerializeTypedObj for #ident #ty_generics #where_clause {362				fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {363					NAMES.with(|__names| {364						#(#fields_serialize)*365366						Ok(())367					})368				}369			}370		};371	})372}373pub fn derive_from_untyped_inner(input: DeriveInput) -> Result<TokenStream> {374	let syn::Data::Struct(data) = &input.data else {375		return Err(Error::new(input.span(), "only structs supported"));376	};377378	let ident = &input.ident;379	let fields = data380		.fields381		.iter()382		.map(TypedField::parse)383		.collect::<Result<Vec<_>>>()?;384385	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();386387	let mut names = Names::default();388389	let fields_parse = fields390		.iter()391		.map(|f| f.expand_parse(&mut names))392		.collect::<Vec<_>>();393394	let names_expanded = names.expand();395	Ok(quote! {396		const _: () = {397			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;398399			impl #impl_generics FromUntyped for #ident #ty_generics #where_clause {400				fn from_untyped(value: Val) -> JrResult<Self> {401					let obj = value.as_obj().expect("shape is correct");402					Self::parse(&obj)403				}404			}405406			#names_expanded407408			impl #impl_generics ParseTypedObj for #ident #ty_generics #where_clause {409				fn parse(obj: &ObjValue) -> JrResult<Self> {410					NAMES.with(|__names| Ok(Self {411						#(#fields_parse)*412					}))413				}414			}415		};416	})417}
after · crates/jrsonnet-macros/src/typed.rs
1use proc_macro2::TokenStream;2use quote::quote;3use syn::{4	parenthesized,5	parse::{Parse, ParseStream},6	spanned::Spanned as _,7	token, DeriveInput, Error, Ident, LitStr, Result, Token, Type,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}