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

no changes

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
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -6,7 +6,7 @@
 	parse_macro_input,
 	punctuated::Punctuated,
 	spanned::Spanned,
-	token::Comma,
+	token::{self, Comma},
 	Attribute, DeriveInput, Error, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path,
 	PathArguments, Result, ReturnType, Token, Type,
 };
@@ -90,6 +90,7 @@
 	syn::custom_keyword!(fields);
 	syn::custom_keyword!(rename);
 	syn::custom_keyword!(flatten);
+	syn::custom_keyword!(ok);
 }
 
 struct EmptyAttr;
@@ -135,7 +136,7 @@
 }
 
 impl ArgInfo {
-	fn parse(arg: &FnArg) -> Result<Self> {
+	fn parse(name: &str, arg: &FnArg) -> Result<Self> {
 		let arg = match arg {
 			FnArg::Receiver(_) => unreachable!(),
 			FnArg::Typed(a) => a,
@@ -149,8 +150,6 @@
 			return Ok(Self::State);
 		} else if type_is_path(ty, "CallLocation").is_some() {
 			return Ok(Self::Location);
-		} else if type_is_path(ty, "Self").is_some() {
-			return Ok(Self::This);
 		} else if type_is_path(ty, "LazyVal").is_some() {
 			return Ok(Self::Lazy {
 				is_option: false,
@@ -158,6 +157,11 @@
 			});
 		}
 
+		match &ty as &Type {
+			Type::Reference(r) if type_is_path(&r.elem, &name).is_some() => return Ok(Self::This),
+			_ => {}
+		}
+
 		let (is_option, ty) = if let Some(ty) = extract_type_from_option(ty)? {
 			if type_is_path(ty, "LazyVal").is_some() {
 				return Ok(Self::Lazy {
@@ -230,11 +234,12 @@
 		return Err(Error::new(result.span(), "return value should be result"));
 	};
 
+	let name = fun.sig.ident.to_string();
 	let args = fun
 		.sig
 		.inputs
 		.iter()
-		.map(ArgInfo::parse)
+		.map(|arg| ArgInfo::parse(&name, arg))
 		.collect::<Result<Vec<_>>>()?;
 
 	let params_desc = args.iter().flat_map(|a| match a {
@@ -343,9 +348,9 @@
 		}
 		const _: () = {
 			use ::jrsonnet_evaluator::{
-				State,
+				State, Val,
 				function::{Builtin, CallLocation, StaticBuiltin, BuiltinParam, ArgsLike, parse_builtin_call},
-				error::Result, Context,
+				error::Result, Context, typed::Typed,
 				parser::ExprLocation,
 			};
 			const PARAMS: &'static [BuiltinParam] = &[
@@ -379,6 +384,9 @@
 struct TypedAttr {
 	rename: Option<String>,
 	flatten: bool,
+	/// flatten(ok) strategy for flattened optionals
+	/// field would be None in case of any parsing error (as in serde)
+	flatten_ok: bool,
 }
 impl Parse for TypedAttr {
 	fn parse(input: ParseStream) -> syn::Result<Self> {
@@ -399,6 +407,17 @@
 			} else if lookahead.peek(kw::flatten) {
 				input.parse::<kw::flatten>()?;
 				out.flatten = true;
+				if input.peek(token::Paren) {
+					let content;
+					parenthesized!(content in input);
+					let lookahead = content.lookahead1();
+					if lookahead.peek(kw::ok) {
+						content.parse::<kw::ok>()?;
+						out.flatten_ok = true;
+					} else {
+						return Err(lookahead.error());
+					}
+				}
 			} else if input.is_empty() {
 				break;
 			} else {
@@ -417,75 +436,101 @@
 	}
 }
 
-struct TypedField<'f>(&'f syn::Field, TypedAttr);
-impl<'f> TypedField<'f> {
-	fn try_new(field: &'f syn::Field) -> Result<Self> {
+struct TypedField {
+	attr: TypedAttr,
+	ident: Ident,
+	ty: Type,
+	is_option: bool,
+}
+impl TypedField {
+	fn parse(field: &syn::Field) -> Result<Self> {
 		let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();
-		if field.ident.is_none() {
+		let ident = if let Some(ident) = field.ident.clone() {
+			ident
+		} else {
 			return Err(Error::new(
 				field.span(),
 				"this field should appear in output object, but it has no visible name",
 			));
+		};
+		let (is_option, ty) = if let Some(ty) = extract_type_from_option(&field.ty)? {
+			(true, ty.clone())
+		} else {
+			(false, field.ty.clone())
+		};
+		if is_option && attr.flatten {
+			if !attr.flatten_ok {
+				return Err(Error::new(
+					field.span(),
+					"strategy should be set when flattening Option",
+				));
+			}
+		} else {
+			if attr.flatten_ok {
+				return Err(Error::new(
+					field.span(),
+					"flatten(ok) is only useable on optional fields",
+				));
+			}
 		}
-		Ok(Self(field, attr))
-	}
-	fn ident(&self) -> Ident {
-		self.0
-			.ident
-			.clone()
-			.expect("constructor disallows fields without name")
+		Ok(Self {
+			attr,
+			ident,
+			ty,
+			is_option,
+		})
 	}
 	/// None if this field is flattened in jsonnet output
 	fn name(&self) -> Option<String> {
-		if self.1.flatten {
+		if self.attr.flatten {
 			return None;
 		}
 		Some(
-			self.1
+			self.attr
 				.rename
 				.clone()
-				.unwrap_or_else(|| self.ident().to_string()),
+				.unwrap_or_else(|| self.ident.to_string()),
 		)
 	}
 
 	fn expand_field(&self) -> Option<TokenStream> {
-		if self.is_option() {
+		if self.is_option {
 			return None;
 		}
 		let name = self.name()?;
-		let ty = &self.0.ty;
+		let ty = &self.ty;
 		Some(quote! {
 			(#name, <#ty>::TYPE)
 		})
 	}
 	fn expand_parse(&self) -> TokenStream {
-		let ident = self.ident();
-		let ty = &self.0.ty;
-		if self.1.flatten {
+		let ident = &self.ident;
+		let ty = &self.ty;
+		if self.attr.flatten {
 			// optional flatten is handled in same way as serde
-			return if self.is_option() {
+			return if self.is_option {
 				quote! {
-					#ident: <#ty>::parse(&obj).ok(),
+					#ident: <#ty>::parse(&obj, s.clone()).ok(),
 				}
 			} else {
 				quote! {
-					#ident: <#ty>::parse(&obj)?,
+					#ident: <#ty>::parse(&obj, s.clone())?,
 				}
 			};
 		};
 
 		let name = self.name().unwrap();
-		let value = if let Some(ty) = self.as_option() {
+		let value = if self.is_option {
 			quote! {
-				if let Some(value) = obj.get(#name.into())? {
-					Some(<#ty>::try_from(vakue)?)
+				if let Some(value) = obj.get(s.clone(), #name.into())? {
+					Some(<#ty>::from_untyped(value, s.clone())?)
 				} else {
 					None
 				}
 			}
 		} else {
 			quote! {
-				<#ty>::try_from(obj.get(#name.into())?.ok_or_else(|| Error::NoSuchField(#name.into()))?)?
+				<#ty>::from_untyped(obj.get(s.clone(), #name.into())?.ok_or_else(|| Error::NoSuchField(#name.into()))?, s.clone())?
 			}
 		};
 
@@ -493,39 +538,33 @@
 			#ident: #value,
 		}
 	}
-	fn expand_serialize(&self) -> TokenStream {
-		let ident = self.ident();
-		if let Some(name) = self.name() {
-			if self.is_option() {
+	fn expand_serialize(&self) -> Result<TokenStream> {
+		let ident = &self.ident;
+		let ty = &self.ty;
+		Ok(if let Some(name) = self.name() {
+			if self.is_option {
 				quote! {
 					if let Some(value) = self.#ident {
-						out.member(#name.into()).value(value.try_into()?)?;
+						out.member(#name.into()).value(s.clone(), <#ty>::into_untyped(value, s.clone())?)?;
 					}
 				}
 			} else {
 				quote! {
-					out.member(#name.into()).value(self.#ident.try_into()?)?;
+					out.member(#name.into()).value(s.clone(), <#ty>::into_untyped(self.#ident, s.clone())?)?;
 				}
 			}
-		} else if self.is_option() {
+		} else if self.is_option {
 			quote! {
 				if let Some(value) = self.#ident {
-					value.serialize(out)?;
+					value.serialize(s.clone(), out)?;
 				}
 			}
 		} else {
 			quote! {
-				self.#ident.serialize(out)?;
+				self.#ident.serialize(s.clone(), out)?;
 			}
-		}
-	}
-
-	fn as_option(&self) -> Option<&Type> {
-		extract_type_from_option(&self.0.ty).unwrap()
+		})
 	}
-	fn is_option(&self) -> bool {
-		self.as_option().is_some()
-	}
 }
 
 #[proc_macro_derive(Typed, attributes(typed))]
@@ -548,7 +587,7 @@
 	let fields = data
 		.fields
 		.iter()
-		.map(TypedField::try_new)
+		.map(TypedField::parse)
 		.collect::<Result<Vec<_>>>()?;
 
 	let typed = {
@@ -566,12 +605,12 @@
 
 				fn from_untyped(value: Val, s: State) -> Result<Self> {
 					let obj = value.as_obj().expect("shape is correct");
-					Self::parse(&obj)
+					Self::parse(&obj, s)
 				}
 
 				fn into_untyped(value: Self, s: State) -> Result<Val> {
 					let mut out = ObjValueBuilder::new();
-					value.serialize(&mut out)?;
+					value.serialize(s, &mut out)?;
 					Ok(Val::Obj(out.build()))
 				}
 
@@ -580,26 +619,29 @@
 	};
 
 	let fields_parse = fields.iter().map(TypedField::expand_parse);
-	let fields_serialize = fields.iter().map(TypedField::expand_serialize);
+	let fields_serialize = fields
+		.iter()
+		.map(TypedField::expand_serialize)
+		.collect::<Result<Vec<_>>>()?;
 
 	Ok(quote! {
 		const _: () = {
 			use ::jrsonnet_evaluator::{
 				typed::{ComplexValType, Typed, TypedObj, CheckType},
-				Val,
-				error::{LocError, Error},
+				Val, State,
+				error::{LocError, Error, Result},
 				ObjValueBuilder, ObjValue,
 			};
 
 			#typed
 
-			impl #ident {
-				fn serialize(self, out: &mut ObjValueBuilder) -> Result<(), LocError> {
+			impl TypedObj for #ident {
+				fn serialize(self, s: State, out: &mut ObjValueBuilder) -> Result<(), LocError> {
 					#(#fields_serialize)*
 
 					Ok(())
 				}
-				fn parse(obj: &ObjValue) -> Result<Self, LocError> {
+				fn parse(obj: &ObjValue, s: State) -> Result<Self, LocError> {
 					Ok(Self {
 						#(#fields_parse)*
 					})