git.delta.rocks / jrsonnet / refs/commits / 590966465ed7

difftreelog

test basic interop checks

Yaroslav Bolyukin2022-04-22parent: #321e7ee.patch.diff
in: master

7 files changed

modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -1,4 +1,5 @@
 use std::{
+	fmt::Debug,
 	path::{Path, PathBuf},
 	rc::Rc,
 };
@@ -166,7 +167,7 @@
 #[derive(Debug, Clone, Trace)]
 pub struct StackTrace(pub Vec<StackTraceElement>);
 
-#[derive(Debug, Clone, Trace)]
+#[derive(Clone, Trace)]
 pub struct LocError(Box<(Error, StackTrace)>);
 impl LocError {
 	pub fn new(e: Error) -> Self {
@@ -186,6 +187,15 @@
 		&mut (self.0).1
 	}
 }
+impl Debug for LocError {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		writeln!(f, "{}", self.0 .0)?;
+		for el in self.0 .1 .0.iter() {
+			writeln!(f, "\t{:?}", el)?;
+		}
+		Ok(())
+	}
+}
 
 pub type Result<V, E = LocError> = std::result::Result<V, E>;
 
modifiedcrates/jrsonnet-evaluator/src/function.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function.rs
+++ b/crates/jrsonnet-evaluator/src/function.rs
@@ -45,7 +45,6 @@
 		evaluate_named(s, self.ctx.unwrap(), &self.value, self.name)
 	}
 }
-
 pub trait ArgLike {
 	fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result<LazyVal>;
 }
@@ -169,6 +168,34 @@
 	}
 }
 
+impl ArgsLike for [(); 0] {
+	fn unnamed_len(&self) -> usize {
+		0
+	}
+
+	fn unnamed_iter(
+		&self,
+		_s: State,
+		_ctx: Context,
+		_tailstrict: bool,
+		_handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>,
+	) -> Result<()> {
+		Ok(())
+	}
+
+	fn named_iter(
+		&self,
+		_s: State,
+		_ctx: Context,
+		_tailstrict: bool,
+		_handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>,
+	) -> Result<()> {
+		Ok(())
+	}
+
+	fn named_names(&self, _handler: &mut dyn FnMut(&IStr)) {}
+}
+
 impl<A: ArgLike> ArgsLike for [(IStr, A)] {
 	fn unnamed_len(&self) -> usize {
 		0
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -14,11 +14,11 @@
 };
 
 pub trait TypedObj: Typed {
-	fn serialize(self, out: &mut ObjValueBuilder) -> Result<()>;
-	fn parse(obj: &ObjValue) -> Result<Self>;
-	fn into_object(self) -> Result<ObjValue> {
+	fn serialize(self, s: State, out: &mut ObjValueBuilder) -> Result<()>;
+	fn parse(obj: &ObjValue, s: State) -> Result<Self>;
+	fn into_object(self, s: State) -> Result<ObjValue> {
 		let mut builder = ObjValueBuilder::new();
-		self.serialize(&mut builder)?;
+		self.serialize(s, &mut builder)?;
 		Ok(builder.build())
 	}
 }
addedcrates/jrsonnet-evaluator/tests/builtin.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-evaluator/tests/builtin.rs
@@ -0,0 +1,105 @@
+mod common;
+
+use std::path::PathBuf;
+
+use gcmodule::Cc;
+use jrsonnet_evaluator::{
+	error::Result,
+	function::{builtin, Builtin, CallLocation},
+	gc::TraceBox,
+	typed::Typed,
+	val::FuncVal,
+	State, Val,
+};
+
+#[builtin]
+fn a() -> Result<u32> {
+	Ok(1)
+}
+
+#[test]
+fn basic_function() -> Result<()> {
+	let s = State::default();
+	let a: a = a {};
+	let v = u32::from_untyped(
+		a.call(
+			s.clone(),
+			s.create_default_context(),
+			CallLocation::native(),
+			&[],
+		)?,
+		s.clone(),
+	)?;
+
+	ensure_eq!(v, 1);
+	Ok(())
+}
+
+#[builtin]
+fn native_add(a: u32, b: u32) -> Result<u32> {
+	Ok(a + b)
+}
+
+#[test]
+fn call_from_code() -> Result<()> {
+	let s = State::default();
+	s.with_stdlib();
+	s.settings_mut().globals.insert(
+		"nativeAdd".into(),
+		Val::Func(FuncVal::StaticBuiltin(native_add::INST)),
+	);
+
+	let v = s.evaluate_snippet_raw(
+		PathBuf::new().into(),
+		"
+            assert nativeAdd(1, 2) == 3;
+            assert nativeAdd(100, 200) == 300;
+            null
+        "
+		.into(),
+	)?;
+	ensure_val_eq!(s.clone(), v, Val::Null);
+	Ok(())
+}
+
+#[builtin(fields(
+    a: u32
+))]
+fn curried_add(this: &curried_add, b: u32) -> Result<u32> {
+	Ok(this.a + b)
+}
+
+#[builtin]
+fn curry_add(a: u32) -> Result<FuncVal> {
+	Ok(FuncVal::Builtin(Cc::new(TraceBox(Box::new(curried_add {
+		a,
+	})))))
+}
+
+#[test]
+fn nonstatic_builtin() -> Result<()> {
+	let s = State::default();
+	s.with_stdlib();
+	s.settings_mut().globals.insert(
+		"curryAdd".into(),
+		Val::Func(FuncVal::StaticBuiltin(curry_add::INST)),
+	);
+
+	let v = s.evaluate_snippet_raw(
+		PathBuf::new().into(),
+		"
+            local a = curryAdd(1);
+            local b = curryAdd(4);
+
+            assert a(2) == 3;
+            assert a(200) == 201;
+
+            assert b(2) == 6;
+            assert b(200) == 204;
+            null
+        "
+		.into(),
+	)?;
+	ensure_val_eq!(s.clone(), v, Val::Null);
+	Ok(())
+}
addedcrates/jrsonnet-evaluator/tests/common.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-evaluator/tests/common.rs
@@ -0,0 +1,25 @@
+#[macro_export]
+macro_rules! ensure_eq {
+	($a:expr, $b:expr $(,)?) => {{
+		if $a != $b {
+			::jrsonnet_evaluator::throw_runtime!(
+				"assertion failed: a != b\na={:#?}\nb={:#?}",
+				$a,
+				$b,
+			)
+		}
+	}};
+}
+
+#[macro_export]
+macro_rules! ensure_val_eq {
+	($s:expr, $a:expr, $b:expr) => {{
+		if !::jrsonnet_evaluator::val::equals($s.clone(), &$a.clone(), &$b.clone())? {
+			::jrsonnet_evaluator::throw_runtime!(
+				"assertion failed: a != b\na={:#?}\nb={:#?}",
+				$a.to_json($s.clone(), 2)?,
+				$b.to_json($s.clone(), 2)?,
+			)
+		}
+	}};
+}
addedcrates/jrsonnet-evaluator/tests/typed_obj.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-evaluator/tests/typed_obj.rs
@@ -0,0 +1,194 @@
+mod common;
+
+use std::{fmt::Debug, path::PathBuf};
+
+use jrsonnet_evaluator::{error::Result, typed::Typed, State};
+
+#[derive(Clone, Typed, PartialEq, Debug)]
+struct A {
+	a: u32,
+	b: u16,
+}
+
+fn test_roundtrip<T: Typed + PartialEq + Debug + Clone>(value: T, s: State) -> Result<()> {
+	let untyped = T::into_untyped(value.clone(), s.clone())?;
+	let value2 = T::from_untyped(untyped.clone(), s.clone())?;
+	ensure_eq!(value, value2);
+	let untyped2 = T::into_untyped(value2, s.clone())?;
+	ensure_val_eq!(s, untyped, untyped2);
+
+	Ok(())
+}
+
+#[test]
+fn simple_object() -> Result<()> {
+	let s = State::default();
+	s.with_stdlib();
+	let a = A::from_untyped(
+		s.evaluate_snippet_raw(PathBuf::new().into(), "{a: 1, b: 2}".into())?,
+		s.clone(),
+	)?;
+	ensure_eq!(a, A { a: 1, b: 2 });
+	test_roundtrip(a.clone(), s.clone())?;
+	Ok(())
+}
+
+#[derive(Clone, Typed, PartialEq, Debug)]
+struct B {
+	a: u32,
+	#[typed(rename = "c")]
+	b: u16,
+}
+
+#[test]
+fn renamed_field() -> Result<()> {
+	let s = State::default();
+	s.with_stdlib();
+	let b = B::from_untyped(
+		s.evaluate_snippet_raw(PathBuf::new().into(), "{a: 1, c: 2}".into())?,
+		s.clone(),
+	)?;
+	ensure_eq!(b, B { a: 1, b: 2 });
+	ensure_eq!(
+		&B::into_untyped(b.clone(), s.clone())?.to_string(s.clone())? as &str,
+		"{a: 1, c: 2}",
+	);
+	test_roundtrip(b.clone(), s.clone())?;
+	Ok(())
+}
+
+#[derive(Clone, Typed, PartialEq, Debug)]
+struct ObjectKind {
+	#[typed(rename = "apiVersion")]
+	api_version: String,
+	#[typed(rename = "kind")]
+	kind: String,
+}
+
+#[derive(Clone, Typed, PartialEq, Debug)]
+struct Object {
+	#[typed(flatten)]
+	kind: ObjectKind,
+	b: u16,
+}
+
+#[test]
+fn flattened_object() -> Result<()> {
+	let s = State::default();
+	s.with_stdlib();
+	let obj = Object::from_untyped(
+		s.evaluate_snippet_raw(
+			PathBuf::new().into(),
+			"{apiVersion: 'ver', kind: 'kind', b: 2}".into(),
+		)?,
+		s.clone(),
+	)?;
+	ensure_eq!(
+		obj,
+		Object {
+			kind: ObjectKind {
+				api_version: "ver".into(),
+				kind: "kind".into(),
+			},
+			b: 2
+		}
+	);
+	ensure_eq!(
+		&Object::into_untyped(obj.clone(), s.clone())?.to_string(s.clone())? as &str,
+		r#"{"apiVersion": "ver", "b": 2, "kind": "kind"}"#,
+	);
+	test_roundtrip(obj.clone(), s.clone())?;
+	Ok(())
+}
+
+#[derive(Clone, Typed, PartialEq, Debug)]
+struct C {
+	a: Option<u32>,
+	b: u16,
+}
+
+#[test]
+fn optional_field_some() -> Result<()> {
+	let s = State::default();
+	s.with_stdlib();
+	let c = C::from_untyped(
+		s.evaluate_snippet_raw(PathBuf::new().into(), "{a: 1, b: 2}".into())?,
+		s.clone(),
+	)?;
+	ensure_eq!(c, C { a: Some(1), b: 2 });
+	ensure_eq!(
+		&C::into_untyped(c.clone(), s.clone())?.to_string(s.clone())? as &str,
+		r#"{"a": 1, "b": 2}"#,
+	);
+	test_roundtrip(c.clone(), s.clone())?;
+	Ok(())
+}
+
+#[test]
+fn optional_field_none() -> Result<()> {
+	let s = State::default();
+	s.with_stdlib();
+	let c = C::from_untyped(
+		s.evaluate_snippet_raw(PathBuf::new().into(), "{b: 2}".into())?,
+		s.clone(),
+	)?;
+	ensure_eq!(c, C { a: None, b: 2 });
+	ensure_eq!(
+		&C::into_untyped(c.clone(), s.clone())?.to_string(s.clone())? as &str,
+		r#"{"b": 2}"#,
+	);
+	test_roundtrip(c.clone(), s.clone())?;
+	Ok(())
+}
+
+#[derive(Clone, Typed, PartialEq, Debug)]
+struct D {
+	#[typed(flatten(ok))]
+	e: Option<E>,
+	b: u16,
+}
+
+#[derive(Clone, Typed, PartialEq, Debug)]
+struct E {
+	v: u32,
+}
+
+#[test]
+fn flatten_optional_some() -> Result<()> {
+	let s = State::default();
+	s.with_stdlib();
+	let d = D::from_untyped(
+		s.evaluate_snippet_raw(PathBuf::new().into(), "{b: 2, v:1}".into())?,
+		s.clone(),
+	)?;
+	ensure_eq!(
+		d,
+		D {
+			e: Some(E { v: 1 }),
+			b: 2
+		}
+	);
+	ensure_eq!(
+		&D::into_untyped(d.clone(), s.clone())?.to_string(s.clone())? as &str,
+		r#"{"b": 2, "v": 1}"#,
+	);
+	test_roundtrip(d.clone(), s.clone())?;
+	Ok(())
+}
+
+#[test]
+fn flatten_optional_none() -> Result<()> {
+	let s = State::default();
+	s.with_stdlib();
+	let d = D::from_untyped(
+		s.evaluate_snippet_raw(PathBuf::new().into(), "{b: 2, v: '1'}".into())?,
+		s.clone(),
+	)?;
+	ensure_eq!(d, D { e: None, b: 2 });
+	ensure_eq!(
+		&D::into_untyped(d.clone(), s.clone())?.to_string(s.clone())? as &str,
+		r#"{"b": 2}"#,
+	);
+	test_roundtrip(d.clone(), s.clone())?;
+	Ok(())
+}
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
before · crates/jrsonnet-macros/src/lib.rs
1use proc_macro2::TokenStream;2use quote::quote;3use syn::{4	parenthesized,5	parse::{Parse, ParseStream},6	parse_macro_input,7	punctuated::Punctuated,8	spanned::Spanned,9	token::Comma,10	Attribute, DeriveInput, Error, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path,11	PathArguments, Result, ReturnType, Token, Type,12};1314fn parse_attr<A: Parse, I>(attrs: &[Attribute], ident: I) -> Result<Option<A>>15where16	Ident: PartialEq<I>,17{18	let attrs = attrs19		.iter()20		.filter(|a| a.path.is_ident(&ident))21		.collect::<Vec<_>>();22	if attrs.len() > 1 {23		return Err(Error::new(24			attrs[1].span(),25			"this attribute may be specified only once",26		));27	} else if attrs.is_empty() {28		return Ok(None);29	}30	let attr = attrs[0];31	let attr = attr.parse_args::<A>()?;3233	Ok(Some(attr))34}3536fn path_is(path: &Path, needed: &str) -> bool {37	path.leading_colon.is_none()38		&& !path.segments.is_empty()39		&& path.segments.iter().last().unwrap().ident == needed40}4142fn type_is_path<'ty>(ty: &'ty Type, needed: &str) -> Option<&'ty PathArguments> {43	match ty {44		Type::Path(path) if path.qself.is_none() && path_is(&path.path, needed) => {45			let args = &path.path.segments.iter().last().unwrap().arguments;46			Some(args)47		}48		_ => None,49	}50}5152fn extract_type_from_option(ty: &Type) -> Result<Option<&Type>> {53	Ok(if let Some(args) = type_is_path(ty, "Option") {54		// It should have only on angle-bracketed param ("<String>"):55		let generic_arg = match args {56			PathArguments::AngleBracketed(params) => params.args.iter().next().unwrap(),57			_ => return Err(Error::new(args.span(), "missing option generic")),58		};59		// This argument must be a type:60		match generic_arg {61			GenericArgument::Type(ty) => Some(ty),62			_ => {63				return Err(Error::new(64					generic_arg.span(),65					"option generic should be a type",66				))67			}68		}69	} else {70		None71	})72}7374struct Field {75	name: Ident,76	_colon: Token![:],77	ty: Type,78}79impl Parse for Field {80	fn parse(input: ParseStream) -> syn::Result<Self> {81		Ok(Self {82			name: input.parse()?,83			_colon: input.parse()?,84			ty: input.parse()?,85		})86	}87}8889mod kw {90	syn::custom_keyword!(fields);91	syn::custom_keyword!(rename);92	syn::custom_keyword!(flatten);93}9495struct EmptyAttr;96impl Parse for EmptyAttr {97	fn parse(_input: ParseStream) -> Result<Self> {98		Ok(Self)99	}100}101102struct BuiltinAttrs {103	fields: Vec<Field>,104}105impl Parse for BuiltinAttrs {106	fn parse(input: ParseStream) -> syn::Result<Self> {107		if input.is_empty() {108			return Ok(Self { fields: Vec::new() });109		}110		input.parse::<kw::fields>()?;111		let fields;112		parenthesized!(fields in input);113		let p = Punctuated::<Field, Comma>::parse_terminated(&fields)?;114		Ok(Self {115			fields: p.into_iter().collect(),116		})117	}118}119120enum ArgInfo {121	Normal {122		ty: Box<Type>,123		is_option: bool,124		name: String,125		cfg_attrs: Vec<Attribute>,126		// ident: Ident,127	},128	Lazy {129		is_option: bool,130		name: String,131	},132	State,133	Location,134	This,135}136137impl ArgInfo {138	fn parse(arg: &FnArg) -> Result<Self> {139		let arg = match arg {140			FnArg::Receiver(_) => unreachable!(),141			FnArg::Typed(a) => a,142		};143		let ident = match &arg.pat as &Pat {144			Pat::Ident(i) => i.ident.clone(),145			_ => return Err(Error::new(arg.pat.span(), "arg should be plain identifier")),146		};147		let ty = &arg.ty;148		if type_is_path(ty, "State").is_some() {149			return Ok(Self::State);150		} else if type_is_path(ty, "CallLocation").is_some() {151			return Ok(Self::Location);152		} else if type_is_path(ty, "Self").is_some() {153			return Ok(Self::This);154		} else if type_is_path(ty, "LazyVal").is_some() {155			return Ok(Self::Lazy {156				is_option: false,157				name: ident.to_string(),158			});159		}160161		let (is_option, ty) = if let Some(ty) = extract_type_from_option(ty)? {162			if type_is_path(ty, "LazyVal").is_some() {163				return Ok(Self::Lazy {164					is_option: true,165					name: ident.to_string(),166				});167			}168169			(true, Box::new(ty.clone()))170		} else {171			(false, ty.clone())172		};173174		let cfg_attrs = arg175			.attrs176			.iter()177			.filter(|a| a.path.is_ident("cfg"))178			.cloned()179			.collect();180181		Ok(Self::Normal {182			ty,183			is_option,184			name: ident.to_string(),185			cfg_attrs,186		})187	}188}189190#[proc_macro_attribute]191pub fn builtin(192	attr: proc_macro::TokenStream,193	item: proc_macro::TokenStream,194) -> proc_macro::TokenStream {195	let attr = parse_macro_input!(attr as BuiltinAttrs);196	let item: ItemFn = parse_macro_input!(item);197198	match builtin_inner(attr, item) {199		Ok(v) => v.into(),200		Err(e) => e.into_compile_error().into(),201	}202}203204fn builtin_inner(attr: BuiltinAttrs, fun: ItemFn) -> syn::Result<TokenStream> {205	let result = match fun.sig.output {206		ReturnType::Default => {207			return Err(Error::new(208				fun.sig.span(),209				"builtin should return something",210			))211		}212		ReturnType::Type(_, ref ty) => ty.clone(),213	};214	let result_inner = if let Some(args) = type_is_path(&result, "Result") {215		let generic_arg = match args {216			PathArguments::AngleBracketed(params) => params.args.iter().next().unwrap(),217			_ => return Err(Error::new(args.span(), "missing result generic")),218		};219		// This argument must be a type:220		match generic_arg {221			GenericArgument::Type(ty) => ty,222			_ => {223				return Err(Error::new(224					generic_arg.span(),225					"option generic should be a type",226				))227			}228		}229	} else {230		return Err(Error::new(result.span(), "return value should be result"));231	};232233	let args = fun234		.sig235		.inputs236		.iter()237		.map(ArgInfo::parse)238		.collect::<Result<Vec<_>>>()?;239240	let params_desc = args.iter().flat_map(|a| match a {241		ArgInfo::Normal {242			is_option,243			name,244			cfg_attrs,245			..246		} => Some(quote! {247			#(#cfg_attrs)*248			BuiltinParam {249				name: std::borrow::Cow::Borrowed(#name),250				has_default: #is_option,251			},252		}),253		ArgInfo::Lazy { is_option, name } => Some(quote! {254			BuiltinParam {255				name: std::borrow::Cow::Borrowed(#name),256				has_default: #is_option,257			},258		}),259		ArgInfo::State => None,260		ArgInfo::Location => None,261		ArgInfo::This => None,262	});263264	let pass = args.iter().map(|a| match a {265		ArgInfo::Normal {266			ty,267			is_option,268			name,269			cfg_attrs,270		} => {271			let eval = quote! {s.push_description(272				|| format!("argument <{}> evaluation", #name),273				|| <#ty>::from_untyped(value.evaluate(s.clone())?, s.clone()),274			)?};275			let value = if *is_option {276				quote! {if let Some(value) = parsed.get(#name) {277					Some(#eval)278				} else {279					None280				},}281			} else {282				quote! {{283					let value = parsed.get(#name).expect("args shape is checked");284					#eval285				},}286			};287			quote! {288				#(#cfg_attrs)*289				#value290			}291		}292		ArgInfo::Lazy { is_option, name } => {293			if *is_option {294				quote! {if let Some(value) = parsed.get(#name) {295					Some(value.clone())296				} else {297					None298				}}299			} else {300				quote! {301					parsed.get(#name).expect("args shape is correct").clone(),302				}303			}304		}305		ArgInfo::State => quote! {s.clone(),},306		ArgInfo::Location => quote! {location,},307		ArgInfo::This => quote! {self,},308	});309310	let fields = attr.fields.iter().map(|field| {311		let name = &field.name;312		let ty = &field.ty;313		quote! {314			pub #name: #ty,315		}316	});317318	let name = &fun.sig.ident;319	let vis = &fun.vis;320	let static_ext = if attr.fields.is_empty() {321		quote! {322			impl #name {323				pub const INST: &'static dyn StaticBuiltin = &#name {};324			}325			impl StaticBuiltin for #name {}326		}327	} else {328		quote! {}329	};330	let static_derive_copy = if attr.fields.is_empty() {331		quote! {, Copy}332	} else {333		quote! {}334	};335336	Ok(quote! {337		#fun338		#[doc(hidden)]339		#[allow(non_camel_case_types)]340		#[derive(Clone, gcmodule::Trace #static_derive_copy)]341		#vis struct #name {342			#(#fields)*343		}344		const _: () = {345			use ::jrsonnet_evaluator::{346				State,347				function::{Builtin, CallLocation, StaticBuiltin, BuiltinParam, ArgsLike, parse_builtin_call},348				error::Result, Context,349				parser::ExprLocation,350			};351			const PARAMS: &'static [BuiltinParam] = &[352				#(#params_desc)*353			];354355			#static_ext356			impl Builtin for #name357			where358				Self: 'static359			{360				fn name(&self) -> &str {361					stringify!(#name)362				}363				fn params(&self) -> &[BuiltinParam] {364					PARAMS365				}366				fn call(&self, s: State, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result<Val> {367					let parsed = parse_builtin_call(s.clone(), ctx, &PARAMS, args, false)?;368369					let result: #result = #name(#(#pass)*);370					let result = result?;371					<#result_inner>::into_untyped(result, s)372				}373			}374		};375	})376}377378#[derive(Default)]379struct TypedAttr {380	rename: Option<String>,381	flatten: bool,382}383impl Parse for TypedAttr {384	fn parse(input: ParseStream) -> syn::Result<Self> {385		let mut out = Self::default();386		loop {387			let lookahead = input.lookahead1();388			if lookahead.peek(kw::rename) {389				input.parse::<kw::rename>()?;390				input.parse::<Token![=]>()?;391				let name = input.parse::<LitStr>()?;392				if out.rename.is_some() {393					return Err(Error::new(394						name.span(),395						"rename attribute may only be specified once",396					));397				}398				out.rename = Some(name.value());399			} else if lookahead.peek(kw::flatten) {400				input.parse::<kw::flatten>()?;401				out.flatten = true;402			} else if input.is_empty() {403				break;404			} else {405				return Err(lookahead.error());406			}407			if input.peek(Token![,]) {408				input.parse::<Token![,]>()?;409			} else {410				break;411			}412		}413		// input.parse::<kw::rename>()?;414		// input.parse::<Token![=]>()?;415		// let rename = input.parse::<LitStr>()?.value();416		Ok(out)417	}418}419420struct TypedField<'f>(&'f syn::Field, TypedAttr);421impl<'f> TypedField<'f> {422	fn try_new(field: &'f syn::Field) -> Result<Self> {423		let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();424		if field.ident.is_none() {425			return Err(Error::new(426				field.span(),427				"this field should appear in output object, but it has no visible name",428			));429		}430		Ok(Self(field, attr))431	}432	fn ident(&self) -> Ident {433		self.0434			.ident435			.clone()436			.expect("constructor disallows fields without name")437	}438	/// None if this field is flattened in jsonnet output439	fn name(&self) -> Option<String> {440		if self.1.flatten {441			return None;442		}443		Some(444			self.1445				.rename446				.clone()447				.unwrap_or_else(|| self.ident().to_string()),448		)449	}450451	fn expand_field(&self) -> Option<TokenStream> {452		if self.is_option() {453			return None;454		}455		let name = self.name()?;456		let ty = &self.0.ty;457		Some(quote! {458			(#name, <#ty>::TYPE)459		})460	}461	fn expand_parse(&self) -> TokenStream {462		let ident = self.ident();463		let ty = &self.0.ty;464		if self.1.flatten {465			// optional flatten is handled in same way as serde466			return if self.is_option() {467				quote! {468					#ident: <#ty>::parse(&obj).ok(),469				}470			} else {471				quote! {472					#ident: <#ty>::parse(&obj)?,473				}474			};475		};476477		let name = self.name().unwrap();478		let value = if let Some(ty) = self.as_option() {479			quote! {480				if let Some(value) = obj.get(#name.into())? {481					Some(<#ty>::try_from(vakue)?)482				} else {483					None484				}485			}486		} else {487			quote! {488				<#ty>::try_from(obj.get(#name.into())?.ok_or_else(|| Error::NoSuchField(#name.into()))?)?489			}490		};491492		quote! {493			#ident: #value,494		}495	}496	fn expand_serialize(&self) -> TokenStream {497		let ident = self.ident();498		if let Some(name) = self.name() {499			if self.is_option() {500				quote! {501					if let Some(value) = self.#ident {502						out.member(#name.into()).value(value.try_into()?)?;503					}504				}505			} else {506				quote! {507					out.member(#name.into()).value(self.#ident.try_into()?)?;508				}509			}510		} else if self.is_option() {511			quote! {512				if let Some(value) = self.#ident {513					value.serialize(out)?;514				}515			}516		} else {517			quote! {518				self.#ident.serialize(out)?;519			}520		}521	}522523	fn as_option(&self) -> Option<&Type> {524		extract_type_from_option(&self.0.ty).unwrap()525	}526	fn is_option(&self) -> bool {527		self.as_option().is_some()528	}529}530531#[proc_macro_derive(Typed, attributes(typed))]532pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {533	let input = parse_macro_input!(item as DeriveInput);534535	match derive_typed_inner(input) {536		Ok(v) => v.into(),537		Err(e) => e.to_compile_error().into(),538	}539}540541fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {542	let data = match &input.data {543		syn::Data::Struct(s) => s,544		_ => return Err(Error::new(input.span(), "only structs supported")),545	};546547	let ident = &input.ident;548	let fields = data549		.fields550		.iter()551		.map(TypedField::try_new)552		.collect::<Result<Vec<_>>>()?;553554	let typed = {555		let fields = fields556			.iter()557			.flat_map(TypedField::expand_field)558			.collect::<Vec<_>>();559		let len = fields.len();560		quote! {561			const ITEMS: [(&'static str, &'static ComplexValType); #len] = [562				#(#fields,)*563			];564			impl Typed for #ident {565				const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&ITEMS);566567				fn from_untyped(value: Val, s: State) -> Result<Self> {568					let obj = value.as_obj().expect("shape is correct");569					Self::parse(&obj)570				}571572				fn into_untyped(value: Self, s: State) -> Result<Val> {573					let mut out = ObjValueBuilder::new();574					value.serialize(&mut out)?;575					Ok(Val::Obj(out.build()))576				}577578			}579		}580	};581582	let fields_parse = fields.iter().map(TypedField::expand_parse);583	let fields_serialize = fields.iter().map(TypedField::expand_serialize);584585	Ok(quote! {586		const _: () = {587			use ::jrsonnet_evaluator::{588				typed::{ComplexValType, Typed, TypedObj, CheckType},589				Val,590				error::{LocError, Error},591				ObjValueBuilder, ObjValue,592			};593594			#typed595596			impl #ident {597				fn serialize(self, out: &mut ObjValueBuilder) -> Result<(), LocError> {598					#(#fields_serialize)*599600					Ok(())601				}602				fn parse(obj: &ObjValue) -> Result<Self, LocError> {603					Ok(Self {604						#(#fields_parse)*605					})606				}607			}608		};609	})610}
after · crates/jrsonnet-macros/src/lib.rs
1use proc_macro2::TokenStream;2use quote::quote;3use syn::{4	parenthesized,5	parse::{Parse, ParseStream},6	parse_macro_input,7	punctuated::Punctuated,8	spanned::Spanned,9	token::{self, Comma},10	Attribute, DeriveInput, Error, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path,11	PathArguments, Result, ReturnType, Token, Type,12};1314fn parse_attr<A: Parse, I>(attrs: &[Attribute], ident: I) -> Result<Option<A>>15where16	Ident: PartialEq<I>,17{18	let attrs = attrs19		.iter()20		.filter(|a| a.path.is_ident(&ident))21		.collect::<Vec<_>>();22	if attrs.len() > 1 {23		return Err(Error::new(24			attrs[1].span(),25			"this attribute may be specified only once",26		));27	} else if attrs.is_empty() {28		return Ok(None);29	}30	let attr = attrs[0];31	let attr = attr.parse_args::<A>()?;3233	Ok(Some(attr))34}3536fn path_is(path: &Path, needed: &str) -> bool {37	path.leading_colon.is_none()38		&& !path.segments.is_empty()39		&& path.segments.iter().last().unwrap().ident == needed40}4142fn type_is_path<'ty>(ty: &'ty Type, needed: &str) -> Option<&'ty PathArguments> {43	match ty {44		Type::Path(path) if path.qself.is_none() && path_is(&path.path, needed) => {45			let args = &path.path.segments.iter().last().unwrap().arguments;46			Some(args)47		}48		_ => None,49	}50}5152fn extract_type_from_option(ty: &Type) -> Result<Option<&Type>> {53	Ok(if let Some(args) = type_is_path(ty, "Option") {54		// It should have only on angle-bracketed param ("<String>"):55		let generic_arg = match args {56			PathArguments::AngleBracketed(params) => params.args.iter().next().unwrap(),57			_ => return Err(Error::new(args.span(), "missing option generic")),58		};59		// This argument must be a type:60		match generic_arg {61			GenericArgument::Type(ty) => Some(ty),62			_ => {63				return Err(Error::new(64					generic_arg.span(),65					"option generic should be a type",66				))67			}68		}69	} else {70		None71	})72}7374struct Field {75	name: Ident,76	_colon: Token![:],77	ty: Type,78}79impl Parse for Field {80	fn parse(input: ParseStream) -> syn::Result<Self> {81		Ok(Self {82			name: input.parse()?,83			_colon: input.parse()?,84			ty: input.parse()?,85		})86	}87}8889mod kw {90	syn::custom_keyword!(fields);91	syn::custom_keyword!(rename);92	syn::custom_keyword!(flatten);93	syn::custom_keyword!(ok);94}9596struct EmptyAttr;97impl Parse for EmptyAttr {98	fn parse(_input: ParseStream) -> Result<Self> {99		Ok(Self)100	}101}102103struct BuiltinAttrs {104	fields: Vec<Field>,105}106impl Parse for BuiltinAttrs {107	fn parse(input: ParseStream) -> syn::Result<Self> {108		if input.is_empty() {109			return Ok(Self { fields: Vec::new() });110		}111		input.parse::<kw::fields>()?;112		let fields;113		parenthesized!(fields in input);114		let p = Punctuated::<Field, Comma>::parse_terminated(&fields)?;115		Ok(Self {116			fields: p.into_iter().collect(),117		})118	}119}120121enum ArgInfo {122	Normal {123		ty: Box<Type>,124		is_option: bool,125		name: String,126		cfg_attrs: Vec<Attribute>,127		// ident: Ident,128	},129	Lazy {130		is_option: bool,131		name: String,132	},133	State,134	Location,135	This,136}137138impl ArgInfo {139	fn parse(name: &str, arg: &FnArg) -> Result<Self> {140		let arg = match arg {141			FnArg::Receiver(_) => unreachable!(),142			FnArg::Typed(a) => a,143		};144		let ident = match &arg.pat as &Pat {145			Pat::Ident(i) => i.ident.clone(),146			_ => return Err(Error::new(arg.pat.span(), "arg should be plain identifier")),147		};148		let ty = &arg.ty;149		if type_is_path(ty, "State").is_some() {150			return Ok(Self::State);151		} else if type_is_path(ty, "CallLocation").is_some() {152			return Ok(Self::Location);153		} else if type_is_path(ty, "LazyVal").is_some() {154			return Ok(Self::Lazy {155				is_option: false,156				name: ident.to_string(),157			});158		}159160		match &ty as &Type {161			Type::Reference(r) if type_is_path(&r.elem, &name).is_some() => return Ok(Self::This),162			_ => {}163		}164165		let (is_option, ty) = if let Some(ty) = extract_type_from_option(ty)? {166			if type_is_path(ty, "LazyVal").is_some() {167				return Ok(Self::Lazy {168					is_option: true,169					name: ident.to_string(),170				});171			}172173			(true, Box::new(ty.clone()))174		} else {175			(false, ty.clone())176		};177178		let cfg_attrs = arg179			.attrs180			.iter()181			.filter(|a| a.path.is_ident("cfg"))182			.cloned()183			.collect();184185		Ok(Self::Normal {186			ty,187			is_option,188			name: ident.to_string(),189			cfg_attrs,190		})191	}192}193194#[proc_macro_attribute]195pub fn builtin(196	attr: proc_macro::TokenStream,197	item: proc_macro::TokenStream,198) -> proc_macro::TokenStream {199	let attr = parse_macro_input!(attr as BuiltinAttrs);200	let item: ItemFn = parse_macro_input!(item);201202	match builtin_inner(attr, item) {203		Ok(v) => v.into(),204		Err(e) => e.into_compile_error().into(),205	}206}207208fn builtin_inner(attr: BuiltinAttrs, fun: ItemFn) -> syn::Result<TokenStream> {209	let result = match fun.sig.output {210		ReturnType::Default => {211			return Err(Error::new(212				fun.sig.span(),213				"builtin should return something",214			))215		}216		ReturnType::Type(_, ref ty) => ty.clone(),217	};218	let result_inner = if let Some(args) = type_is_path(&result, "Result") {219		let generic_arg = match args {220			PathArguments::AngleBracketed(params) => params.args.iter().next().unwrap(),221			_ => return Err(Error::new(args.span(), "missing result generic")),222		};223		// This argument must be a type:224		match generic_arg {225			GenericArgument::Type(ty) => ty,226			_ => {227				return Err(Error::new(228					generic_arg.span(),229					"option generic should be a type",230				))231			}232		}233	} else {234		return Err(Error::new(result.span(), "return value should be result"));235	};236237	let name = fun.sig.ident.to_string();238	let args = fun239		.sig240		.inputs241		.iter()242		.map(|arg| ArgInfo::parse(&name, arg))243		.collect::<Result<Vec<_>>>()?;244245	let params_desc = args.iter().flat_map(|a| match a {246		ArgInfo::Normal {247			is_option,248			name,249			cfg_attrs,250			..251		} => Some(quote! {252			#(#cfg_attrs)*253			BuiltinParam {254				name: std::borrow::Cow::Borrowed(#name),255				has_default: #is_option,256			},257		}),258		ArgInfo::Lazy { is_option, name } => Some(quote! {259			BuiltinParam {260				name: std::borrow::Cow::Borrowed(#name),261				has_default: #is_option,262			},263		}),264		ArgInfo::State => None,265		ArgInfo::Location => None,266		ArgInfo::This => None,267	});268269	let pass = args.iter().map(|a| match a {270		ArgInfo::Normal {271			ty,272			is_option,273			name,274			cfg_attrs,275		} => {276			let eval = quote! {s.push_description(277				|| format!("argument <{}> evaluation", #name),278				|| <#ty>::from_untyped(value.evaluate(s.clone())?, s.clone()),279			)?};280			let value = if *is_option {281				quote! {if let Some(value) = parsed.get(#name) {282					Some(#eval)283				} else {284					None285				},}286			} else {287				quote! {{288					let value = parsed.get(#name).expect("args shape is checked");289					#eval290				},}291			};292			quote! {293				#(#cfg_attrs)*294				#value295			}296		}297		ArgInfo::Lazy { is_option, name } => {298			if *is_option {299				quote! {if let Some(value) = parsed.get(#name) {300					Some(value.clone())301				} else {302					None303				}}304			} else {305				quote! {306					parsed.get(#name).expect("args shape is correct").clone(),307				}308			}309		}310		ArgInfo::State => quote! {s.clone(),},311		ArgInfo::Location => quote! {location,},312		ArgInfo::This => quote! {self,},313	});314315	let fields = attr.fields.iter().map(|field| {316		let name = &field.name;317		let ty = &field.ty;318		quote! {319			pub #name: #ty,320		}321	});322323	let name = &fun.sig.ident;324	let vis = &fun.vis;325	let static_ext = if attr.fields.is_empty() {326		quote! {327			impl #name {328				pub const INST: &'static dyn StaticBuiltin = &#name {};329			}330			impl StaticBuiltin for #name {}331		}332	} else {333		quote! {}334	};335	let static_derive_copy = if attr.fields.is_empty() {336		quote! {, Copy}337	} else {338		quote! {}339	};340341	Ok(quote! {342		#fun343		#[doc(hidden)]344		#[allow(non_camel_case_types)]345		#[derive(Clone, gcmodule::Trace #static_derive_copy)]346		#vis struct #name {347			#(#fields)*348		}349		const _: () = {350			use ::jrsonnet_evaluator::{351				State, Val,352				function::{Builtin, CallLocation, StaticBuiltin, BuiltinParam, ArgsLike, parse_builtin_call},353				error::Result, Context, typed::Typed,354				parser::ExprLocation,355			};356			const PARAMS: &'static [BuiltinParam] = &[357				#(#params_desc)*358			];359360			#static_ext361			impl Builtin for #name362			where363				Self: 'static364			{365				fn name(&self) -> &str {366					stringify!(#name)367				}368				fn params(&self) -> &[BuiltinParam] {369					PARAMS370				}371				fn call(&self, s: State, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result<Val> {372					let parsed = parse_builtin_call(s.clone(), ctx, &PARAMS, args, false)?;373374					let result: #result = #name(#(#pass)*);375					let result = result?;376					<#result_inner>::into_untyped(result, s)377				}378			}379		};380	})381}382383#[derive(Default)]384struct TypedAttr {385	rename: Option<String>,386	flatten: bool,387	/// flatten(ok) strategy for flattened optionals388	/// field would be None in case of any parsing error (as in serde)389	flatten_ok: bool,390}391impl Parse for TypedAttr {392	fn parse(input: ParseStream) -> syn::Result<Self> {393		let mut out = Self::default();394		loop {395			let lookahead = input.lookahead1();396			if lookahead.peek(kw::rename) {397				input.parse::<kw::rename>()?;398				input.parse::<Token![=]>()?;399				let name = input.parse::<LitStr>()?;400				if out.rename.is_some() {401					return Err(Error::new(402						name.span(),403						"rename attribute may only be specified once",404					));405				}406				out.rename = Some(name.value());407			} else if lookahead.peek(kw::flatten) {408				input.parse::<kw::flatten>()?;409				out.flatten = true;410				if input.peek(token::Paren) {411					let content;412					parenthesized!(content in input);413					let lookahead = content.lookahead1();414					if lookahead.peek(kw::ok) {415						content.parse::<kw::ok>()?;416						out.flatten_ok = true;417					} else {418						return Err(lookahead.error());419					}420				}421			} else if input.is_empty() {422				break;423			} else {424				return Err(lookahead.error());425			}426			if input.peek(Token![,]) {427				input.parse::<Token![,]>()?;428			} else {429				break;430			}431		}432		// input.parse::<kw::rename>()?;433		// input.parse::<Token![=]>()?;434		// let rename = input.parse::<LitStr>()?.value();435		Ok(out)436	}437}438439struct TypedField {440	attr: TypedAttr,441	ident: Ident,442	ty: Type,443	is_option: bool,444}445impl TypedField {446	fn parse(field: &syn::Field) -> Result<Self> {447		let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();448		let ident = if let Some(ident) = field.ident.clone() {449			ident450		} else {451			return Err(Error::new(452				field.span(),453				"this field should appear in output object, but it has no visible name",454			));455		};456		let (is_option, ty) = if let Some(ty) = extract_type_from_option(&field.ty)? {457			(true, ty.clone())458		} else {459			(false, field.ty.clone())460		};461		if is_option && attr.flatten {462			if !attr.flatten_ok {463				return Err(Error::new(464					field.span(),465					"strategy should be set when flattening Option",466				));467			}468		} else {469			if attr.flatten_ok {470				return Err(Error::new(471					field.span(),472					"flatten(ok) is only useable on optional fields",473				));474			}475		}476		Ok(Self {477			attr,478			ident,479			ty,480			is_option,481		})482	}483	/// None if this field is flattened in jsonnet output484	fn name(&self) -> Option<String> {485		if self.attr.flatten {486			return None;487		}488		Some(489			self.attr490				.rename491				.clone()492				.unwrap_or_else(|| self.ident.to_string()),493		)494	}495496	fn expand_field(&self) -> Option<TokenStream> {497		if self.is_option {498			return None;499		}500		let name = self.name()?;501		let ty = &self.ty;502		Some(quote! {503			(#name, <#ty>::TYPE)504		})505	}506	fn expand_parse(&self) -> TokenStream {507		let ident = &self.ident;508		let ty = &self.ty;509		if self.attr.flatten {510			// optional flatten is handled in same way as serde511			return if self.is_option {512				quote! {513					#ident: <#ty>::parse(&obj, s.clone()).ok(),514				}515			} else {516				quote! {517					#ident: <#ty>::parse(&obj, s.clone())?,518				}519			};520		};521522		let name = self.name().unwrap();523		let value = if self.is_option {524			quote! {525				if let Some(value) = obj.get(s.clone(), #name.into())? {526					Some(<#ty>::from_untyped(value, s.clone())?)527				} else {528					None529				}530			}531		} else {532			quote! {533				<#ty>::from_untyped(obj.get(s.clone(), #name.into())?.ok_or_else(|| Error::NoSuchField(#name.into()))?, s.clone())?534			}535		};536537		quote! {538			#ident: #value,539		}540	}541	fn expand_serialize(&self) -> Result<TokenStream> {542		let ident = &self.ident;543		let ty = &self.ty;544		Ok(if let Some(name) = self.name() {545			if self.is_option {546				quote! {547					if let Some(value) = self.#ident {548						out.member(#name.into()).value(s.clone(), <#ty>::into_untyped(value, s.clone())?)?;549					}550				}551			} else {552				quote! {553					out.member(#name.into()).value(s.clone(), <#ty>::into_untyped(self.#ident, s.clone())?)?;554				}555			}556		} else if self.is_option {557			quote! {558				if let Some(value) = self.#ident {559					value.serialize(s.clone(), out)?;560				}561			}562		} else {563			quote! {564				self.#ident.serialize(s.clone(), out)?;565			}566		})567	}568}569570#[proc_macro_derive(Typed, attributes(typed))]571pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {572	let input = parse_macro_input!(item as DeriveInput);573574	match derive_typed_inner(input) {575		Ok(v) => v.into(),576		Err(e) => e.to_compile_error().into(),577	}578}579580fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {581	let data = match &input.data {582		syn::Data::Struct(s) => s,583		_ => return Err(Error::new(input.span(), "only structs supported")),584	};585586	let ident = &input.ident;587	let fields = data588		.fields589		.iter()590		.map(TypedField::parse)591		.collect::<Result<Vec<_>>>()?;592593	let typed = {594		let fields = fields595			.iter()596			.flat_map(TypedField::expand_field)597			.collect::<Vec<_>>();598		let len = fields.len();599		quote! {600			const ITEMS: [(&'static str, &'static ComplexValType); #len] = [601				#(#fields,)*602			];603			impl Typed for #ident {604				const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&ITEMS);605606				fn from_untyped(value: Val, s: State) -> Result<Self> {607					let obj = value.as_obj().expect("shape is correct");608					Self::parse(&obj, s)609				}610611				fn into_untyped(value: Self, s: State) -> Result<Val> {612					let mut out = ObjValueBuilder::new();613					value.serialize(s, &mut out)?;614					Ok(Val::Obj(out.build()))615				}616617			}618		}619	};620621	let fields_parse = fields.iter().map(TypedField::expand_parse);622	let fields_serialize = fields623		.iter()624		.map(TypedField::expand_serialize)625		.collect::<Result<Vec<_>>>()?;626627	Ok(quote! {628		const _: () = {629			use ::jrsonnet_evaluator::{630				typed::{ComplexValType, Typed, TypedObj, CheckType},631				Val, State,632				error::{LocError, Error, Result},633				ObjValueBuilder, ObjValue,634			};635636			#typed637638			impl TypedObj for #ident {639				fn serialize(self, s: State, out: &mut ObjValueBuilder) -> Result<(), LocError> {640					#(#fields_serialize)*641642					Ok(())643				}644				fn parse(obj: &ObjValue, s: State) -> Result<Self, LocError> {645					Ok(Self {646						#(#fields_parse)*647					})648				}649			}650		};651	})652}