From 590966465ed7f56dd5df6ae842749ec451237365 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Fri, 22 Apr 2022 18:51:08 +0000 Subject: [PATCH] test: basic interop checks --- --- 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); -#[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 = std::result::Result; --- 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; } @@ -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 ArgsLike for [(IStr, A)] { fn unnamed_len(&self) -> usize { 0 --- 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; - fn into_object(self) -> Result { + fn serialize(self, s: State, out: &mut ObjValueBuilder) -> Result<()>; + fn parse(obj: &ObjValue, s: State) -> Result; + fn into_object(self, s: State) -> Result { let mut builder = ObjValueBuilder::new(); - self.serialize(&mut builder)?; + self.serialize(s, &mut builder)?; Ok(builder.build()) } } --- /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 { + 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 { + 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 { + Ok(this.a + b) +} + +#[builtin] +fn curry_add(a: u32) -> Result { + 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(()) +} --- /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)?, + ) + } + }}; +} --- /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(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, + 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, + 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(()) +} --- 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 { + fn parse(name: &str, arg: &FnArg) -> Result { 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::>>()?; 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, 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 { @@ -399,6 +407,17 @@ } else if lookahead.peek(kw::flatten) { input.parse::()?; 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::()?; + 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 { +struct TypedField { + attr: TypedAttr, + ident: Ident, + ty: Type, + is_option: bool, +} +impl TypedField { + fn parse(field: &syn::Field) -> Result { let attr = parse_attr::(&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 { - 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 { - 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 { + 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::>>()?; let typed = { @@ -566,12 +605,12 @@ fn from_untyped(value: Val, s: State) -> Result { let obj = value.as_obj().expect("shape is correct"); - Self::parse(&obj) + Self::parse(&obj, s) } fn into_untyped(value: Self, s: State) -> Result { 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::>>()?; 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 { + fn parse(obj: &ObjValue, s: State) -> Result { Ok(Self { #(#fields_parse)* }) -- gitstuff