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
before · crates/jrsonnet-evaluator/src/function.rs
1use std::{borrow::Cow, collections::HashMap};23use gcmodule::Trace;4use jrsonnet_interner::IStr;5pub use jrsonnet_macros::builtin;6use jrsonnet_parser::{ArgsDesc, ExprLocation, LocExpr, ParamsDesc};78use crate::{9	error::Error::*, evaluate, evaluate_named, gc::TraceBox, throw, typed::Typed,10	val::LazyValValue, Context, FutureWrapper, GcHashMap, LazyVal, Result, State, Val,11};1213#[derive(Clone, Copy)]14pub struct CallLocation<'l>(pub Option<&'l ExprLocation>);15impl<'l> CallLocation<'l> {16	pub const fn new(loc: &'l ExprLocation) -> Self {17		Self(Some(loc))18	}19}20impl CallLocation<'static> {21	pub const fn native() -> Self {22		Self(None)23	}24}2526#[derive(Trace)]27struct EvaluateLazyVal {28	ctx: Context,29	expr: LocExpr,30}31impl LazyValValue for EvaluateLazyVal {32	fn get(self: Box<Self>, s: State) -> Result<Val> {33		evaluate(s, self.ctx, &self.expr)34	}35}3637#[derive(Trace)]38struct EvaluateNamedLazyVal {39	ctx: FutureWrapper<Context>,40	name: IStr,41	value: LocExpr,42}43impl LazyValValue for EvaluateNamedLazyVal {44	fn get(self: Box<Self>, s: State) -> Result<Val> {45		evaluate_named(s, self.ctx.unwrap(), &self.value, self.name)46	}47}4849pub trait ArgLike {50	fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result<LazyVal>;51}52impl ArgLike for &LocExpr {53	fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result<LazyVal> {54		Ok(if tailstrict {55			LazyVal::new_resolved(evaluate(s, ctx, self)?)56		} else {57			LazyVal::new(TraceBox(Box::new(EvaluateLazyVal {58				ctx,59				expr: (*self).clone(),60			})))61		})62	}63}64impl<T> ArgLike for T65where66	T: Typed + Clone,67{68	fn evaluate_arg(&self, s: State, _ctx: Context, _tailstrict: bool) -> Result<LazyVal> {69		let val = T::into_untyped(self.clone(), s)?;70		Ok(LazyVal::new_resolved(val))71	}72}73pub enum TlaArg {74	String(IStr),75	Code(LocExpr),76	Val(Val),77}78impl ArgLike for TlaArg {79	fn evaluate_arg(&self, s: State, ctx: Context, tailstrict: bool) -> Result<LazyVal> {80		match self {81			TlaArg::String(s) => Ok(LazyVal::new_resolved(Val::Str(s.clone()))),82			TlaArg::Code(code) => Ok(if tailstrict {83				LazyVal::new_resolved(evaluate(s, ctx, code)?)84			} else {85				LazyVal::new(TraceBox(Box::new(EvaluateLazyVal {86					ctx,87					expr: code.clone(),88				})))89			}),90			TlaArg::Val(val) => Ok(LazyVal::new_resolved(val.clone())),91		}92	}93}9495pub trait ArgsLike {96	fn unnamed_len(&self) -> usize;97	fn unnamed_iter(98		&self,99		s: State,100		ctx: Context,101		tailstrict: bool,102		handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>,103	) -> Result<()>;104	fn named_iter(105		&self,106		s: State,107		ctx: Context,108		tailstrict: bool,109		handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>,110	) -> Result<()>;111	fn named_names(&self, handler: &mut dyn FnMut(&IStr));112}113114impl ArgsLike for ArgsDesc {115	fn unnamed_len(&self) -> usize {116		self.unnamed.len()117	}118119	fn unnamed_iter(120		&self,121		s: State,122		ctx: Context,123		tailstrict: bool,124		handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>,125	) -> Result<()> {126		for (id, arg) in self.unnamed.iter().enumerate() {127			handler(128				id,129				if tailstrict {130					LazyVal::new_resolved(evaluate(s.clone(), ctx.clone(), arg)?)131				} else {132					LazyVal::new(TraceBox(Box::new(EvaluateLazyVal {133						ctx: ctx.clone(),134						expr: arg.clone(),135					})))136				},137			)?;138		}139		Ok(())140	}141142	fn named_iter(143		&self,144		s: State,145		ctx: Context,146		tailstrict: bool,147		handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>,148	) -> Result<()> {149		for (name, arg) in self.named.iter() {150			handler(151				name,152				if tailstrict {153					LazyVal::new_resolved(evaluate(s.clone(), ctx.clone(), arg)?)154				} else {155					LazyVal::new(TraceBox(Box::new(EvaluateLazyVal {156						ctx: ctx.clone(),157						expr: arg.clone(),158					})))159				},160			)?;161		}162		Ok(())163	}164165	fn named_names(&self, handler: &mut dyn FnMut(&IStr)) {166		for (name, _) in self.named.iter() {167			handler(name)168		}169	}170}171172impl<A: ArgLike> ArgsLike for [(IStr, A)] {173	fn unnamed_len(&self) -> usize {174		0175	}176177	fn unnamed_iter(178		&self,179		_s: State,180		_ctx: Context,181		_tailstrict: bool,182		_handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>,183	) -> Result<()> {184		Ok(())185	}186187	fn named_iter(188		&self,189		s: State,190		ctx: Context,191		tailstrict: bool,192		handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>,193	) -> Result<()> {194		for (name, val) in self.iter() {195			handler(name, val.evaluate_arg(s.clone(), ctx.clone(), tailstrict)?)?;196		}197		Ok(())198	}199200	fn named_names(&self, handler: &mut dyn FnMut(&IStr)) {201		for (name, _) in self.iter() {202			handler(name);203		}204	}205}206207impl<A: ArgLike> ArgsLike for HashMap<IStr, A> {208	fn unnamed_len(&self) -> usize {209		0210	}211212	fn unnamed_iter(213		&self,214		_s: State,215		_ctx: Context,216		_tailstrict: bool,217		_handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>,218	) -> Result<()> {219		Ok(())220	}221222	fn named_iter(223		&self,224		s: State,225		ctx: Context,226		tailstrict: bool,227		handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>,228	) -> Result<()> {229		for (name, value) in self.iter() {230			handler(231				name,232				value.evaluate_arg(s.clone(), ctx.clone(), tailstrict)?,233			)?;234		}235		Ok(())236	}237238	fn named_names(&self, handler: &mut dyn FnMut(&IStr)) {239		for (name, _) in self.iter() {240			handler(name);241		}242	}243}244245impl<A: ArgLike> ArgsLike for [A] {246	fn unnamed_len(&self) -> usize {247		self.len()248	}249250	fn unnamed_iter(251		&self,252		s: State,253		ctx: Context,254		tailstrict: bool,255		handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>,256	) -> Result<()> {257		for (i, arg) in self.iter().enumerate() {258			handler(i, arg.evaluate_arg(s.clone(), ctx.clone(), tailstrict)?)?;259		}260		Ok(())261	}262263	fn named_iter(264		&self,265		_s: State,266		_ctx: Context,267		_tailstrict: bool,268		_handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>,269	) -> Result<()> {270		Ok(())271	}272273	fn named_names(&self, _handler: &mut dyn FnMut(&IStr)) {}274}275impl<A: ArgLike> ArgsLike for &[A] {276	fn unnamed_len(&self) -> usize {277		(*self).unnamed_len()278	}279280	fn unnamed_iter(281		&self,282		s: State,283		ctx: Context,284		tailstrict: bool,285		handler: &mut dyn FnMut(usize, LazyVal) -> Result<()>,286	) -> Result<()> {287		(*self).unnamed_iter(s, ctx, tailstrict, handler)288	}289290	fn named_iter(291		&self,292		s: State,293		ctx: Context,294		tailstrict: bool,295		handler: &mut dyn FnMut(&IStr, LazyVal) -> Result<()>,296	) -> Result<()> {297		(*self).named_iter(s, ctx, tailstrict, handler)298	}299300	fn named_names(&self, handler: &mut dyn FnMut(&IStr)) {301		(*self).named_names(handler)302	}303}304305/// Creates correct [context](Context) for function body evaluation returning error on invalid call.306///307/// ## Parameters308/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)309/// * `body_ctx`: used for default parameter values' execution and for body execution (if set)310/// * `params`: function parameters' definition311/// * `args`: passed function arguments312/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily313pub fn parse_function_call(314	s: State,315	ctx: Context,316	body_ctx: Context,317	params: &ParamsDesc,318	args: &dyn ArgsLike,319	tailstrict: bool,320) -> Result<Context> {321	let mut passed_args = GcHashMap::with_capacity(params.len());322	if args.unnamed_len() > params.len() {323		throw!(TooManyArgsFunctionHas(params.len()))324	}325326	let mut filled_args = 0;327328	args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| {329		let name = params[id].0.clone();330		passed_args.insert(name, arg);331		filled_args += 1;332		Ok(())333	})?;334335	args.named_iter(s, ctx, tailstrict, &mut |name, value| {336		// FIXME: O(n) for arg existence check337		if !params.iter().any(|p| &p.0 == name) {338			throw!(UnknownFunctionParameter((name as &str).to_owned()));339		}340		if passed_args.insert(name.clone(), value).is_some() {341			throw!(BindingParameterASecondTime(name.clone()));342		}343		filled_args += 1;344		Ok(())345	})?;346347	if filled_args < params.len() {348		// Some args are unset, but maybe we have defaults for them349		// Default values should be created in newly created context350		let fctx = Context::new_future();351		let mut defaults = GcHashMap::with_capacity(params.len() - filled_args);352353		for param in params.iter().filter(|p| p.1.is_some()) {354			if passed_args.contains_key(&param.0.clone()) {355				continue;356			}357			LazyVal::new(TraceBox(Box::new(EvaluateNamedLazyVal {358				ctx: fctx.clone(),359				name: param.0.clone(),360				value: param.1.clone().unwrap(),361			})));362363			defaults.insert(364				param.0.clone(),365				LazyVal::new(TraceBox(Box::new(EvaluateNamedLazyVal {366					ctx: fctx.clone(),367					name: param.0.clone(),368					value: param.1.clone().unwrap(),369				}))),370			);371			filled_args += 1;372		}373374		// Some args still wasn't filled375		if filled_args != params.len() {376			for param in params.iter().skip(args.unnamed_len()) {377				let mut found = false;378				args.named_names(&mut |name| {379					if name == &param.0 {380						found = true;381					}382				});383				if !found {384					throw!(FunctionParameterNotBoundInCall(param.0.clone()));385				}386			}387			unreachable!();388		}389390		Ok(body_ctx391			.extend(passed_args, None, None, None)392			.extend_bound(defaults)393			.into_future(fctx))394	} else {395		let body_ctx = body_ctx.extend(passed_args, None, None, None);396		Ok(body_ctx)397	}398}399400type BuiltinParamName = Cow<'static, str>;401402#[derive(Clone, Trace)]403pub struct BuiltinParam {404	pub name: BuiltinParamName,405	pub has_default: bool,406}407408/// Do not implement it directly, instead use #[builtin] macro409pub trait Builtin: Trace {410	fn name(&self) -> &str;411	fn params(&self) -> &[BuiltinParam];412	fn call(&self, s: State, ctx: Context, loc: CallLocation, args: &dyn ArgsLike) -> Result<Val>;413}414415pub trait StaticBuiltin: Builtin + Send + Sync416where417	Self: 'static,418{419	// In impl, to make it object safe:420	// const INST: &'static Self;421}422423/// You shouldn't probally use this function, use jrsonnet_macros::builtin instead424///425/// ## Parameters426/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)427/// * `params`: function parameters' definition428/// * `args`: passed function arguments429/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily430pub fn parse_builtin_call(431	s: State,432	ctx: Context,433	params: &[BuiltinParam],434	args: &dyn ArgsLike,435	tailstrict: bool,436) -> Result<GcHashMap<BuiltinParamName, LazyVal>> {437	let mut passed_args = GcHashMap::with_capacity(params.len());438	if args.unnamed_len() > params.len() {439		throw!(TooManyArgsFunctionHas(params.len()))440	}441442	let mut filled_args = 0;443444	args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| {445		let name = params[id].name.clone();446		passed_args.insert(name, arg);447		filled_args += 1;448		Ok(())449	})?;450451	args.named_iter(s, ctx, tailstrict, &mut |name, arg| {452		// FIXME: O(n) for arg existence check453		let p = params454			.iter()455			.find(|p| p.name == name as &str)456			.ok_or_else(|| UnknownFunctionParameter((name as &str).to_owned()))?;457		if passed_args.insert(p.name.clone(), arg).is_some() {458			throw!(BindingParameterASecondTime(name.clone()));459		}460		filled_args += 1;461		Ok(())462	})?;463464	if filled_args < params.len() {465		for param in params.iter().filter(|p| p.has_default) {466			if passed_args.contains_key(&param.name) {467				continue;468			}469			filled_args += 1;470		}471472		// Some args still wasn't filled473		if filled_args != params.len() {474			for param in params.iter().skip(args.unnamed_len()) {475				let mut found = false;476				args.named_names(&mut |name| {477					if name as &str == &param.name as &str {478						found = true;479					}480				});481				if !found {482					throw!(FunctionParameterNotBoundInCall(param.name.clone().into()));483				}484			}485			unreachable!();486		}487	}488	Ok(passed_args)489}490491/// Creates Context, which has all argument default values applied492/// and with unbound values causing error to be returned493pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Context {494	let fctx = Context::new_future();495496	let mut bindings = GcHashMap::new();497498	#[derive(Trace)]499	struct DependsOnUnbound(IStr);500	impl LazyValValue for DependsOnUnbound {501		fn get(self: Box<Self>, _: State) -> Result<Val> {502			Err(FunctionParameterNotBoundInCall(self.0.clone()).into())503		}504	}505506	for param in params.iter() {507		if let Some(v) = &param.1 {508			bindings.insert(509				param.0.clone(),510				LazyVal::new(TraceBox(Box::new(EvaluateNamedLazyVal {511					ctx: fctx.clone(),512					name: param.0.clone(),513					value: v.clone(),514				}))),515			);516		} else {517			bindings.insert(518				param.0.clone(),519				LazyVal::new(TraceBox(Box::new(DependsOnUnbound(param.0.clone())))),520			);521		}522	}523524	body_ctx525		.extend(bindings, None, None, None)526		.into_future(fctx)527}
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
--- 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)*
 					})