git.delta.rocks / jrsonnet / refs/commits / 0ef9787dd3a6

difftreelog

fix ToStringFormat should not quote top-level string

Yaroslav Bolyukin2024-05-19parent: #12a8bf0.patch.diff
in: master

2 files changed

modifiedcrates/jrsonnet-evaluator/src/manifest.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/manifest.rs
+++ b/crates/jrsonnet-evaluator/src/manifest.rs
@@ -84,8 +84,9 @@
 			debug_truncate_strings: None,
 		}
 	}
-	// Same format as std.toString
-	pub fn std_to_string() -> Self {
+	/// Same format as std.toString, except does not keeps top-level string as-is
+	/// To avoid confusion, the format is private in jrsonnet, use [`ToStringFormat`] instead
+	const fn std_to_string_helper() -> Self {
 		Self {
 			padding: Cow::Borrowed(""),
 			mtype: JsonFormatting::ToString,
@@ -344,10 +345,17 @@
 	}
 }
 
+/// Same as [`JsonFormat`] with pre-set options, but top-level string is serialized as-is,
+/// without quoting.
 pub struct ToStringFormat;
 impl ManifestFormat for ToStringFormat {
 	fn manifest_buf(&self, val: Val, out: &mut String) -> Result<()> {
-		JsonFormat::std_to_string().manifest_buf(val, out)
+		const JSON_TO_STRING: JsonFormat = JsonFormat::std_to_string_helper();
+		if let Some(str) = val.as_str() {
+			out.push_str(&str);
+			return Ok(());
+		}
+		JSON_TO_STRING.manifest_buf(val, out)
 	}
 	fn file_trailing_newline(&self) -> bool {
 		false
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
before · crates/jrsonnet-macros/src/lib.rs
1use std::string::String;23use proc_macro2::TokenStream;4use quote::quote;5use syn::{6	parenthesized,7	parse::{Parse, ParseStream},8	parse_macro_input,9	punctuated::Punctuated,10	spanned::Spanned,11	token::{self, Comma},12	Attribute, DeriveInput, Error, Expr, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path,13	PathArguments, Result, ReturnType, Token, Type,14};1516fn parse_attr<A: Parse, I>(attrs: &[Attribute], ident: I) -> Result<Option<A>>17where18	Ident: PartialEq<I>,19{20	let attrs = attrs21		.iter()22		.filter(|a| a.path().is_ident(&ident))23		.collect::<Vec<_>>();24	if attrs.len() > 1 {25		return Err(Error::new(26			attrs[1].span(),27			"this attribute may be specified only once",28		));29	} else if attrs.is_empty() {30		return Ok(None);31	}32	let attr = attrs[0];33	let attr = attr.parse_args::<A>()?;3435	Ok(Some(attr))36}37fn remove_attr<I>(attrs: &mut Vec<Attribute>, ident: I)38where39	Ident: PartialEq<I>,40{41	attrs.retain(|a| !a.path().is_ident(&ident));42}4344fn path_is(path: &Path, needed: &str) -> bool {45	path.leading_colon.is_none()46		&& !path.segments.is_empty()47		&& path.segments.iter().last().unwrap().ident == needed48}4950fn type_is_path<'ty>(ty: &'ty Type, needed: &str) -> Option<&'ty PathArguments> {51	match ty {52		Type::Path(path) if path.qself.is_none() && path_is(&path.path, needed) => {53			let args = &path.path.segments.iter().last().unwrap().arguments;54			Some(args)55		}56		_ => None,57	}58}5960fn extract_type_from_option(ty: &Type) -> Result<Option<&Type>> {61	let Some(args) = type_is_path(ty, "Option") else {62		return Ok(None);63	};64	// It should have only on angle-bracketed param ("<String>"):65	let PathArguments::AngleBracketed(params) = args else {66		return Err(Error::new(args.span(), "missing option generic"));67	};68	let generic_arg = params.args.iter().next().unwrap();69	// This argument must be a type:70	let GenericArgument::Type(ty) = generic_arg else {71		return Err(Error::new(72			generic_arg.span(),73			"option generic should be a type",74		));75	};76	Ok(Some(ty))77}7879struct Field {80	attrs: Vec<Attribute>,81	name: Ident,82	_colon: Token![:],83	ty: Type,84}85impl Parse for Field {86	fn parse(input: ParseStream) -> syn::Result<Self> {87		Ok(Self {88			attrs: input.call(Attribute::parse_outer)?,89			name: input.parse()?,90			_colon: input.parse()?,91			ty: input.parse()?,92		})93	}94}9596mod kw {97	syn::custom_keyword!(fields);98	syn::custom_keyword!(rename);99	syn::custom_keyword!(flatten);100	syn::custom_keyword!(add);101	syn::custom_keyword!(hide);102	syn::custom_keyword!(ok);103}104105struct EmptyAttr;106impl Parse for EmptyAttr {107	fn parse(_input: ParseStream) -> Result<Self> {108		Ok(Self)109	}110}111112struct BuiltinAttrs {113	fields: Vec<Field>,114}115impl Parse for BuiltinAttrs {116	fn parse(input: ParseStream) -> syn::Result<Self> {117		if input.is_empty() {118			return Ok(Self { fields: Vec::new() });119		}120		input.parse::<kw::fields>()?;121		let fields;122		parenthesized!(fields in input);123		let p = Punctuated::<Field, Comma>::parse_terminated(&fields)?;124		Ok(Self {125			fields: p.into_iter().collect(),126		})127	}128}129130enum Optionality {131	Required,132	Optional,133	Default(Expr),134}135impl Optionality {136	fn is_optional(&self) -> bool {137		!matches!(self, Self::Required)138	}139}140141enum ArgInfo {142	Normal {143		ty: Box<Type>,144		optionality: Optionality,145		name: Option<String>,146		cfg_attrs: Vec<Attribute>,147	},148	Lazy {149		is_option: bool,150		name: Option<String>,151	},152	Context,153	Location,154	This,155}156157impl ArgInfo {158	fn parse(name: &str, arg: &mut FnArg) -> Result<Self> {159		let FnArg::Typed(arg) = arg else {160			unreachable!()161		};162		let ident = match &arg.pat as &Pat {163			Pat::Ident(i) => Some(i.ident.clone()),164			_ => None,165		};166		let ty = &arg.ty;167		if type_is_path(ty, "Context").is_some() {168			return Ok(Self::Context);169		} else if type_is_path(ty, "CallLocation").is_some() {170			return Ok(Self::Location);171		} else if type_is_path(ty, "Thunk").is_some() {172			return Ok(Self::Lazy {173				is_option: false,174				name: ident.map(|v| v.to_string()),175			});176		}177178		match ty as &Type {179			Type::Reference(r) if type_is_path(&r.elem, name).is_some() => return Ok(Self::This),180			_ => {}181		}182183		let (optionality, ty) = if let Some(default) = parse_attr::<_, _>(&arg.attrs, "default")? {184			remove_attr(&mut arg.attrs, "default");185			(Optionality::Default(default), ty.clone())186		} else if let Some(ty) = extract_type_from_option(ty)? {187			if type_is_path(ty, "Thunk").is_some() {188				return Ok(Self::Lazy {189					is_option: true,190					name: ident.map(|v| v.to_string()),191				});192			}193194			(Optionality::Optional, Box::new(ty.clone()))195		} else {196			(Optionality::Required, ty.clone())197		};198199		let cfg_attrs = arg200			.attrs201			.iter()202			.filter(|a| a.path().is_ident("cfg"))203			.cloned()204			.collect();205206		Ok(Self::Normal {207			ty,208			optionality,209			name: ident.map(|v| v.to_string()),210			cfg_attrs,211		})212	}213}214215#[proc_macro_attribute]216pub fn builtin(217	attr: proc_macro::TokenStream,218	item: proc_macro::TokenStream,219) -> proc_macro::TokenStream {220	let attr = parse_macro_input!(attr as BuiltinAttrs);221	let item_fn = parse_macro_input!(item as ItemFn);222223	match builtin_inner(attr, item_fn) {224		Ok(v) => v.into(),225		Err(e) => e.into_compile_error().into(),226	}227}228229#[allow(clippy::too_many_lines)]230fn builtin_inner(attr: BuiltinAttrs, mut fun: ItemFn) -> syn::Result<TokenStream> {231	let ReturnType::Type(_, result) = &fun.sig.output else {232		return Err(Error::new(233			fun.sig.span(),234			"builtin should return something",235		));236	};237238	let name = fun.sig.ident.to_string();239	let args = fun240		.sig241		.inputs242		.iter_mut()243		.map(|arg| ArgInfo::parse(&name, arg))244		.collect::<Result<Vec<_>>>()?;245246	let params_desc = args.iter().filter_map(|a| match a {247		ArgInfo::Normal {248			optionality,249			name,250			cfg_attrs,251			..252		} => {253			let name = name254				.as_ref()255				.map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});256			let default = match optionality {257				Optionality::Required => quote!(ParamDefault::None),258				Optionality::Optional => quote!(ParamDefault::Exists),259				Optionality::Default(e) => quote!(ParamDefault::Literal(stringify!(#e))),260			};261			Some(quote! {262				#(#cfg_attrs)*263				BuiltinParam::new(#name, #default),264			})265		}266		ArgInfo::Lazy { is_option, name } => {267			let name = name268				.as_ref()269				.map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});270			Some(quote! {271				BuiltinParam::new(#name, ParamDefault::exists(#is_option)),272			})273		}274		ArgInfo::Context | ArgInfo::Location | ArgInfo::This => None,275	});276277	let mut id = 0usize;278	let pass = args279		.iter()280		.map(|a| match a {281			ArgInfo::Normal { .. } | ArgInfo::Lazy { .. } => {282				let cid = id;283				id += 1;284				(quote! {#cid}, a)285			}286			ArgInfo::Context | ArgInfo::Location | ArgInfo::This => {287				(quote! {compile_error!("should not use id")}, a)288			}289		})290		.map(|(id, a)| match a {291			ArgInfo::Normal {292				ty,293				optionality,294				name,295				cfg_attrs,296			} => {297				let name = name.as_ref().map_or("<unnamed>", String::as_str);298				let eval = quote! {jrsonnet_evaluator::State::push_description(299					|| format!("argument <{}> evaluation", #name),300					|| <#ty>::from_untyped(value.evaluate()?),301				)?};302				let value = match optionality {303					Optionality::Required => quote! {{304						let value = parsed[#id].as_ref().expect("args shape is checked");305						#eval306					},},307					Optionality::Optional => quote! {if let Some(value) = &parsed[#id] {308						Some(#eval)309					} else {310						None311					},},312					Optionality::Default(expr) => quote! {if let Some(value) = &parsed[#id] {313						#eval314					} else {315						let v: #ty = #expr;316						v317					},},318				};319				quote! {320					#(#cfg_attrs)*321					#value322				}323			}324			ArgInfo::Lazy { is_option, .. } => {325				if *is_option {326					quote! {if let Some(value) = &parsed[#id] {327						Some(value.clone())328					} else {329						None330					},}331				} else {332					quote! {333						parsed[#id].as_ref().expect("args shape is correct").clone(),334					}335				}336			}337			ArgInfo::Context => quote! {ctx.clone(),},338			ArgInfo::Location => quote! {location,},339			ArgInfo::This => quote! {self,},340		});341342	let fields = attr.fields.iter().map(|field| {343		let attrs = &field.attrs;344		let name = &field.name;345		let ty = &field.ty;346		quote! {347			#(#attrs)*348			pub #name: #ty,349		}350	});351352	let name = &fun.sig.ident;353	let vis = &fun.vis;354	let static_ext = if attr.fields.is_empty() {355		quote! {356			impl #name {357				pub const INST: &'static dyn StaticBuiltin = &#name {};358			}359			impl StaticBuiltin for #name {}360		}361	} else {362		quote! {}363	};364	let static_derive_copy = if attr.fields.is_empty() {365		quote! {, Copy}366	} else {367		quote! {}368	};369370	Ok(quote! {371		#fun372373		#[doc(hidden)]374		#[allow(non_camel_case_types)]375		#[derive(Clone, jrsonnet_gcmodule::Trace #static_derive_copy)]376		#vis struct #name {377			#(#fields)*378		}379		const _: () = {380			use ::jrsonnet_evaluator::{381				State, Val,382				function::{builtin::{Builtin, StaticBuiltin, BuiltinParam, ParamName, ParamDefault}, CallLocation, ArgsLike, parse::parse_builtin_call},383				Result, Context, typed::Typed,384				parser::ExprLocation,385			};386			const PARAMS: &'static [BuiltinParam] = &[387				#(#params_desc)*388			];389390			#static_ext391			impl Builtin for #name392			where393				Self: 'static394			{395				fn name(&self) -> &str {396					stringify!(#name)397				}398				fn params(&self) -> &[BuiltinParam] {399					PARAMS400				}401				#[allow(unused_variables)]402				fn call(&self, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result<Val> {403					let parsed = parse_builtin_call(ctx.clone(), &PARAMS, args, false)?;404405					let result: #result = #name(#(#pass)*);406					<_ as Typed>::into_result(result)407				}408				fn as_any(&self) -> &dyn ::std::any::Any {409					self410				}411			}412		};413	})414}415416#[derive(Default)]417#[allow(clippy::struct_excessive_bools)]418struct TypedAttr {419	rename: Option<String>,420	flatten: bool,421	/// flatten(ok) strategy for flattened optionals422	/// field would be None in case of any parsing error (as in serde)423	flatten_ok: bool,424	// Should it be `field+:` instead of `field:`425	add: bool,426	// Should it be `field::` instead of `field:`427	hide: bool,428}429impl Parse for TypedAttr {430	fn parse(input: ParseStream) -> syn::Result<Self> {431		let mut out = Self::default();432		loop {433			let lookahead = input.lookahead1();434			if lookahead.peek(kw::rename) {435				input.parse::<kw::rename>()?;436				input.parse::<Token![=]>()?;437				let name = input.parse::<LitStr>()?;438				if out.rename.is_some() {439					return Err(Error::new(440						name.span(),441						"rename attribute may only be specified once",442					));443				}444				out.rename = Some(name.value());445			} else if lookahead.peek(kw::flatten) {446				input.parse::<kw::flatten>()?;447				out.flatten = true;448				if input.peek(token::Paren) {449					let content;450					parenthesized!(content in input);451					let lookahead = content.lookahead1();452					if lookahead.peek(kw::ok) {453						content.parse::<kw::ok>()?;454						out.flatten_ok = true;455					} else {456						return Err(lookahead.error());457					}458				}459			} else if lookahead.peek(kw::add) {460				input.parse::<kw::add>()?;461				out.add = true;462			} else if lookahead.peek(kw::hide) {463				input.parse::<kw::hide>()?;464				out.hide = true;465			} else if input.is_empty() {466				break;467			} else {468				return Err(lookahead.error());469			}470			if input.peek(Token![,]) {471				input.parse::<Token![,]>()?;472			} else {473				break;474			}475		}476		Ok(out)477	}478}479480struct TypedField {481	attr: TypedAttr,482	ident: Ident,483	ty: Type,484	is_option: bool,485}486impl TypedField {487	fn parse(field: &syn::Field) -> Result<Self> {488		let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();489		let Some(ident) = field.ident.clone() else {490			return Err(Error::new(491				field.span(),492				"this field should appear in output object, but it has no visible name",493			));494		};495		let (is_option, ty) = extract_type_from_option(&field.ty)?496			.map_or_else(|| (false, field.ty.clone()), |ty| (true, ty.clone()));497		if is_option && attr.flatten {498			if !attr.flatten_ok {499				return Err(Error::new(500					field.span(),501					"strategy should be set when flattening Option",502				));503			}504		} else if attr.flatten_ok {505			return Err(Error::new(506				field.span(),507				"flatten(ok) is only useable on optional fields",508			));509		}510511		Ok(Self {512			attr,513			ident,514			ty,515			is_option,516		})517	}518	/// None if this field is flattened in jsonnet output519	fn name(&self) -> Option<String> {520		if self.attr.flatten {521			return None;522		}523		Some(524			self.attr525				.rename526				.clone()527				.unwrap_or_else(|| self.ident.to_string()),528		)529	}530531	fn expand_field(&self) -> Option<TokenStream> {532		if self.is_option {533			return None;534		}535		let name = self.name()?;536		let ty = &self.ty;537		Some(quote! {538			(#name, <#ty as Typed>::TYPE)539		})540	}541	fn expand_parse(&self) -> TokenStream {542		let ident = &self.ident;543		let ty = &self.ty;544		if self.attr.flatten {545			// optional flatten is handled in same way as serde546			return if self.is_option {547				quote! {548					#ident: <#ty as TypedObj>::parse(&obj).ok(),549				}550			} else {551				quote! {552					#ident: <#ty as TypedObj>::parse(&obj)?,553				}554			};555		};556557		let name = self.name().unwrap();558		let value = if self.is_option {559			quote! {560				if let Some(value) = obj.get(#name.into())? {561					Some(<#ty as Typed>::from_untyped(value)?)562				} else {563					None564				}565			}566		} else {567			quote! {568				<#ty as Typed>::from_untyped(obj.get(#name.into())?.ok_or_else(|| ErrorKind::NoSuchField(#name.into(), vec![]))?)?569			}570		};571572		quote! {573			#ident: #value,574		}575	}576	fn expand_serialize(&self) -> TokenStream {577		let ident = &self.ident;578		let ty = &self.ty;579		self.name().map_or_else(580			|| {581				if self.is_option {582					quote! {583						if let Some(value) = self.#ident {584							<#ty as TypedObj>::serialize(value, out)?;585						}586					}587				} else {588					quote! {589						<#ty as TypedObj>::serialize(self.#ident, out)?;590					}591				}592			},593			|name| {594				let hide = if self.attr.hide {595					quote! {.hide()}596				} else {597					quote! {}598				};599				let add = if self.attr.add {600					quote! {.add()}601				} else {602					quote! {}603				};604				if self.is_option {605					quote! {606						if let Some(value) = self.#ident {607							out.field(#name)608								#hide609								#add610								.try_value(<#ty as Typed>::into_untyped(value)?)?;611						}612					}613				} else {614					quote! {615						out.field(#name)616							#hide617							#add618							.try_value(<#ty as Typed>::into_untyped(self.#ident)?)?;619					}620				}621			},622		)623	}624}625626#[proc_macro_derive(Typed, attributes(typed))]627pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {628	let input = parse_macro_input!(item as DeriveInput);629630	match derive_typed_inner(input) {631		Ok(v) => v.into(),632		Err(e) => e.to_compile_error().into(),633	}634}635636fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {637	let syn::Data::Struct(data) = &input.data else {638		return Err(Error::new(input.span(), "only structs supported"));639	};640641	let ident = &input.ident;642	let fields = data643		.fields644		.iter()645		.map(TypedField::parse)646		.collect::<Result<Vec<_>>>()?;647648	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();649650	let typed = {651		let fields = fields652			.iter()653			.filter_map(TypedField::expand_field)654			.collect::<Vec<_>>();655		quote! {656			impl #impl_generics Typed for #ident #ty_generics #where_clause {657				const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&[658					#(#fields,)*659				]);660661				fn from_untyped(value: Val) -> JrResult<Self> {662					let obj = value.as_obj().expect("shape is correct");663					Self::parse(&obj)664				}665666				fn into_untyped(value: Self) -> JrResult<Val> {667					let mut out = ObjValueBuilder::new();668					value.serialize(&mut out)?;669					Ok(Val::Obj(out.build()))670				}671672			}673		}674	};675676	let fields_parse = fields.iter().map(TypedField::expand_parse);677	let fields_serialize = fields678		.iter()679		.map(TypedField::expand_serialize)680		.collect::<Vec<_>>();681682	Ok(quote! {683		const _: () = {684			use ::jrsonnet_evaluator::{685				typed::{ComplexValType, Typed, TypedObj, CheckType},686				Val, State,687				error::{ErrorKind, Result as JrResult},688				ObjValueBuilder, ObjValue,689			};690691			#typed692693			impl #impl_generics TypedObj for #ident #ty_generics #where_clause {694				fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {695					#(#fields_serialize)*696697					Ok(())698				}699				fn parse(obj: &ObjValue) -> JrResult<Self> {700					Ok(Self {701						#(#fields_parse)*702					})703				}704			}705		};706	})707}708709struct FormatInput {710	formatting: LitStr,711	arguments: Vec<Expr>,712}713impl Parse for FormatInput {714	fn parse(input: ParseStream) -> Result<Self> {715		let formatting = input.parse()?;716		let mut arguments = Vec::new();717718		while input.peek(Token![,]) {719			input.parse::<Token![,]>()?;720			if input.is_empty() {721				// Trailing comma722				break;723			}724			let expr = input.parse()?;725			arguments.push(expr);726		}727728		if !input.is_empty() {729			return Err(syn::Error::new(input.span(), "unexpected trailing input"));730		}731732		Ok(Self {733			formatting,734			arguments,735		})736	}737}738fn is_format_str(i: &str) -> bool {739	let mut is_plain = true;740	// -1 = {741	// +1 = }742	let mut is_bracket = 0i8;743	for ele in i.chars() {744		match ele {745			'{' if is_bracket == -1 => {746				is_bracket = 0;747			}748			'}' if is_bracket == -1 => {749				is_plain = false;750				break;751			}752			'}' if is_bracket == 1 => {753				is_bracket = 0;754			}755			'{' if is_bracket == 1 => {756				is_plain = false;757				break;758			}759			'{' => {760				is_bracket = -1;761			}762			'}' => {763				is_bracket = 1;764			}765			_ if is_bracket != 0 => {766				is_plain = false;767				break;768			}769			_ => {}770		}771	}772	!is_plain || is_bracket != 0773}774impl FormatInput {775	fn expand(self) -> TokenStream {776		let format = self.formatting;777		if is_format_str(&format.value()) {778			let args = self.arguments;779			quote! {780				::jrsonnet_evaluator::IStr::from(format!(#format #(, #args)*))781			}782		} else {783			if let Some(first) = self.arguments.first() {784				return syn::Error::new(785					first.span(),786					"string has no formatting codes, it should not have the arguments",787				)788				.into_compile_error();789			}790			quote! {791				::jrsonnet_evaluator::IStr::from(#format)792			}793		}794	}795}796797/// `IStr` formatting helper798///799/// Using `format!("literal with no codes").into()` is slower than just `"literal with no codes".into()`800/// This macro looks for formatting codes in the input string, and uses801/// `format!()` only when necessary802#[proc_macro]803pub fn format_istr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {804	let input = parse_macro_input!(input as FormatInput);805	input.expand().into()806}