difftreelog
feat lazily evaluate source field in mergePatch
in: master
5 files changed
crates/jrsonnet-evaluator/src/obj.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/obj.rs
+++ b/crates/jrsonnet-evaluator/src/obj.rs
@@ -1183,6 +1183,12 @@
let entry = receiver.0.new.this_entries.entry(name);
entry.insert_entry(member);
}
+ /// Inserts thunk, replacing if it is already defined
+ pub fn thunk(self, value: impl Into<Thunk<Val>>) {
+ let (receiver, name, member) = self.build_member(MaybeUnbound::Bound(value.into()));
+ let entry = receiver.0.new.this_entries.entry(name);
+ entry.insert_entry(member);
+ }
/// Tries to insert value, returns an error if it was already defined
pub fn try_value(self, value: impl Into<Val>) -> Result<()> {
crates/jrsonnet-formatter/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-formatter/src/lib.rs
+++ b/crates/jrsonnet-formatter/src/lib.rs
@@ -5,8 +5,7 @@
condition_helpers::is_multiple_lines,
condition_resolvers::true_resolver,
ir_helpers::{new_line_group, with_indent},
- ConditionResolver, ConditionResolverContext, LineNumber, PrintItemPath, PrintItems,
- PrintOptions, Signal,
+ ConditionResolver, ConditionResolverContext, LineNumber, PrintItems, PrintOptions,
};
use hi_doc::{Formatting, SnippetBuilder};
use jrsonnet_rowan_parser::{
@@ -14,7 +13,7 @@
Arg, ArgsDesc, Assertion, BinaryOperator, Bind, CompSpec, Destruct, DestructArrayPart,
DestructRest, Expr, ExprBase, FieldName, ForSpec, IfSpec, ImportKind, Literal, Member,
Name, Number, ObjBody, ObjLocal, ParamsDesc, SliceDesc, SourceFile, Stmt, Suffix, Text,
- Trivia, TriviaKind, UnaryOperator, Visibility,
+ UnaryOperator, Visibility,
},
AstNode, AstToken as _, SyntaxToken,
};
crates/jrsonnet-stdlib/src/misc.rsdiffbeforeafterboth1use std::{cell::RefCell, collections::BTreeSet};23use jrsonnet_evaluator::{4 bail,5 error::{ErrorKind::*, Result},6 function::{builtin, CallLocation, FuncVal},7 manifest::JsonFormat,8 typed::{Either2, Either4},9 val::{equals, ArrValue},10 Either, IStr, ObjValue, ObjValueBuilder, ResultExt, Thunk, Val,11};12use jrsonnet_gcmodule::Cc;1314use crate::Settings;1516#[builtin]17pub fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> usize {18 use Either4::*;19 match x {20 A(x) => x.chars().count(),21 B(x) => x.len(),22 C(x) => x.len(),23 D(f) => f.params_len(),24 }25}2627#[builtin]28pub fn builtin_get(29 o: ObjValue,30 f: IStr,31 default: Option<Thunk<Val>>,32 #[default(true)] inc_hidden: bool,33) -> Result<Val> {34 let do_default = move || {35 let Some(default) = default else {36 return Ok(Val::Null);37 };38 default.evaluate()39 };40 // Happy path for invisible fields41 if !inc_hidden && !o.has_field_ex(f.clone(), false) {42 return do_default();43 }44 let Some(v) = o.get(f)? else {45 return do_default();46 };47 Ok(v)48}4950#[builtin(fields(51 settings: Cc<RefCell<Settings>>,52))]53pub fn builtin_ext_var(this: &builtin_ext_var, x: IStr) -> Result<Val> {54 this.settings55 .borrow()56 .ext_vars57 .get(&x)58 .cloned()59 .ok_or_else(|| UndefinedExternalVariable(x))?60 .evaluate_tailstrict()61}6263#[builtin(fields(64 settings: Cc<RefCell<Settings>>,65))]66pub fn builtin_native(this: &builtin_native, x: IStr) -> Val {67 this.settings68 .borrow()69 .ext_natives70 .get(&x)71 .cloned()72 .map_or(Val::Null, Val::Func)73}7475#[builtin(fields(76 settings: Cc<RefCell<Settings>>,77))]78pub fn builtin_trace(79 this: &builtin_trace,80 loc: CallLocation,81 str: Val,82 rest: Option<Thunk<Val>>,83) -> Result<Val> {84 this.settings.borrow().trace_printer.print_trace(85 loc,86 match &str {87 Val::Str(s) => s.clone().into_flat(),88 Val::Func(f) => format!("{f:?}").into(),89 v => v.manifest(JsonFormat::debug())?.into(),90 },91 );92 rest.map_or_else(|| Ok(str), |rest| rest.evaluate())93}9495#[allow(clippy::comparison_chain)]96#[builtin]97pub fn builtin_starts_with(a: Either![IStr, ArrValue], b: Either![IStr, ArrValue]) -> Result<bool> {98 Ok(match (a, b) {99 (Either2::A(a), Either2::A(b)) => a.starts_with(b.as_str()),100 (Either2::B(a), Either2::B(b)) => {101 if b.len() > a.len() {102 return Ok(false);103 } else if b.len() == a.len() {104 return equals(&Val::Arr(a), &Val::Arr(b));105 }106 for (a, b) in a.iter().take(b.len()).zip(b.iter()) {107 let a = a?;108 let b = b?;109 if !equals(&a, &b)? {110 return Ok(false);111 }112 }113 true114 }115 _ => bail!("both arguments should be of the same type"),116 })117}118119#[allow(clippy::comparison_chain)]120#[builtin]121pub fn builtin_ends_with(a: Either![IStr, ArrValue], b: Either![IStr, ArrValue]) -> Result<bool> {122 Ok(match (a, b) {123 (Either2::A(a), Either2::A(b)) => a.ends_with(b.as_str()),124 (Either2::B(a), Either2::B(b)) => {125 if b.len() > a.len() {126 return Ok(false);127 } else if b.len() == a.len() {128 return equals(&Val::Arr(a), &Val::Arr(b));129 }130 let a_len = a.len();131 for (a, b) in a.iter().skip(a_len - b.len()).zip(b.iter()) {132 let a = a?;133 let b = b?;134 if !equals(&a, &b)? {135 return Ok(false);136 }137 }138 true139 }140 _ => bail!("both arguments should be of the same type"),141 })142}143144#[builtin]145pub fn builtin_assert_equal(a: Val, b: Val) -> Result<bool> {146 if equals(&a, &b)? {147 return Ok(true);148 }149 // TODO: Use debug output format150 let format = JsonFormat::std_to_json(151 " ".to_owned(),152 "\n",153 ": ",154 #[cfg(feature = "exp-preserve-order")]155 true,156 );157 let a = if let Some(a) = a.as_str() {158 format!("<A>\n{a}\n</A>")159 } else {160 a.manifest(&format).description("<a> manifestification")?161 };162 let b = if let Some(b) = b.as_str() {163 format!("<B>\n{b}\n</B>")164 } else {165 b.manifest(&format).description("<b> manifestification")?166 };167 bail!("assertion failed: A != B\nA: {a}\nB: {b}")168}169170#[builtin]171pub fn builtin_merge_patch(target: Val, patch: Val) -> Result<Val> {172 let Some(patch) = patch.as_obj() else {173 return Ok(patch);174 };175 let target = target.as_obj().unwrap_or_else(|| ObjValue::empty());176 let target_fields = target177 .fields(178 // FIXME: Makes no sense to preserve order for BTreeSet, it would be better to use IndexSet here?179 // But IndexSet won't allow fast ordered union...180 // // Makes sense to preserve source ordering where possible.181 // // May affect evaluation order, but it is not specified by jsonnet spec.182 // #[cfg(feature = "exp-preserve-order")]183 // true,184 #[cfg(feature = "exp-preserve-order")]185 false,186 )187 .into_iter()188 .collect::<BTreeSet<IStr>>();189 let patch_fields = patch190 .fields(191 // No need to look at the patch field order, I think?192 // New fields (that will be appended at the end) will be alphabeticaly-ordered,193 // but it is fine for jsonpatch, I don't think people write jsonpatch in jsonnet,194 // when they can use mixins.195 #[cfg(feature = "exp-preserve-order")]196 false,197 )198 .into_iter()199 .collect::<BTreeSet<IStr>>();200201 let mut out = ObjValueBuilder::new();202 for field in target_fields.union(&patch_fields) {203 let Some(field_patch) = patch.get(field.clone())? else {204 out.field(field.clone()).value(target.get(field.clone())?.expect("we're iterating over fields union, if field is missing in patch - it exists in target"));205 continue;206 };207 if matches!(field_patch, Val::Null) {208 continue;209 }210 let field_target = target.get(field.clone())?.unwrap_or(Val::Null);211 out.field(field.clone())212 .value(builtin_merge_patch(field_target, field_patch)?);213 }214 Ok(out.build().into())215}1use std::{cell::RefCell, collections::BTreeSet};23use jrsonnet_evaluator::{4 bail,5 error::{ErrorKind::*, Result},6 function::{builtin, CallLocation, FuncVal},7 manifest::JsonFormat,8 typed::{Either2, Either4},9 val::{equals, ArrValue},10 Either, IStr, ObjValue, ObjValueBuilder, ResultExt, Thunk, Val,11};12use jrsonnet_gcmodule::Cc;1314use crate::Settings;1516#[builtin]17pub fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> usize {18 use Either4::*;19 match x {20 A(x) => x.chars().count(),21 B(x) => x.len(),22 C(x) => x.len(),23 D(f) => f.params_len(),24 }25}2627#[builtin]28pub fn builtin_get(29 o: ObjValue,30 f: IStr,31 default: Option<Thunk<Val>>,32 #[default(true)] inc_hidden: bool,33) -> Result<Val> {34 let do_default = move || {35 let Some(default) = default else {36 return Ok(Val::Null);37 };38 default.evaluate()39 };40 // Happy path for invisible fields41 if !inc_hidden && !o.has_field_ex(f.clone(), false) {42 return do_default();43 }44 let Some(v) = o.get(f)? else {45 return do_default();46 };47 Ok(v)48}4950#[builtin(fields(51 settings: Cc<RefCell<Settings>>,52))]53pub fn builtin_ext_var(this: &builtin_ext_var, x: IStr) -> Result<Val> {54 this.settings55 .borrow()56 .ext_vars57 .get(&x)58 .cloned()59 .ok_or_else(|| UndefinedExternalVariable(x))?60 .evaluate_tailstrict()61}6263#[builtin(fields(64 settings: Cc<RefCell<Settings>>,65))]66pub fn builtin_native(this: &builtin_native, x: IStr) -> Val {67 this.settings68 .borrow()69 .ext_natives70 .get(&x)71 .cloned()72 .map_or(Val::Null, Val::Func)73}7475#[builtin(fields(76 settings: Cc<RefCell<Settings>>,77))]78pub fn builtin_trace(79 this: &builtin_trace,80 loc: CallLocation,81 str: Val,82 rest: Option<Thunk<Val>>,83) -> Result<Val> {84 this.settings.borrow().trace_printer.print_trace(85 loc,86 match &str {87 Val::Str(s) => s.clone().into_flat(),88 Val::Func(f) => format!("{f:?}").into(),89 v => v.manifest(JsonFormat::debug())?.into(),90 },91 );92 rest.map_or_else(|| Ok(str), |rest| rest.evaluate())93}9495#[allow(clippy::comparison_chain)]96#[builtin]97pub fn builtin_starts_with(a: Either![IStr, ArrValue], b: Either![IStr, ArrValue]) -> Result<bool> {98 Ok(match (a, b) {99 (Either2::A(a), Either2::A(b)) => a.starts_with(b.as_str()),100 (Either2::B(a), Either2::B(b)) => {101 if b.len() > a.len() {102 return Ok(false);103 } else if b.len() == a.len() {104 return equals(&Val::Arr(a), &Val::Arr(b));105 }106 for (a, b) in a.iter().take(b.len()).zip(b.iter()) {107 let a = a?;108 let b = b?;109 if !equals(&a, &b)? {110 return Ok(false);111 }112 }113 true114 }115 _ => bail!("both arguments should be of the same type"),116 })117}118119#[allow(clippy::comparison_chain)]120#[builtin]121pub fn builtin_ends_with(a: Either![IStr, ArrValue], b: Either![IStr, ArrValue]) -> Result<bool> {122 Ok(match (a, b) {123 (Either2::A(a), Either2::A(b)) => a.ends_with(b.as_str()),124 (Either2::B(a), Either2::B(b)) => {125 if b.len() > a.len() {126 return Ok(false);127 } else if b.len() == a.len() {128 return equals(&Val::Arr(a), &Val::Arr(b));129 }130 let a_len = a.len();131 for (a, b) in a.iter().skip(a_len - b.len()).zip(b.iter()) {132 let a = a?;133 let b = b?;134 if !equals(&a, &b)? {135 return Ok(false);136 }137 }138 true139 }140 _ => bail!("both arguments should be of the same type"),141 })142}143144#[builtin]145pub fn builtin_assert_equal(a: Val, b: Val) -> Result<bool> {146 if equals(&a, &b)? {147 return Ok(true);148 }149 // TODO: Use debug output format150 let format = JsonFormat::std_to_json(151 " ".to_owned(),152 "\n",153 ": ",154 #[cfg(feature = "exp-preserve-order")]155 true,156 );157 let a = if let Some(a) = a.as_str() {158 format!("<A>\n{a}\n</A>")159 } else {160 a.manifest(&format).description("<a> manifestification")?161 };162 let b = if let Some(b) = b.as_str() {163 format!("<B>\n{b}\n</B>")164 } else {165 b.manifest(&format).description("<b> manifestification")?166 };167 bail!("assertion failed: A != B\nA: {a}\nB: {b}")168}169170#[builtin]171pub fn builtin_merge_patch(target: Val, patch: Val) -> Result<Val> {172 let Some(patch) = patch.as_obj() else {173 return Ok(patch);174 };175 let target = target.as_obj().unwrap_or_else(|| ObjValue::empty());176 let target_fields = target177 .fields(178 // FIXME: Makes no sense to preserve order for BTreeSet, it would be better to use IndexSet here?179 // But IndexSet won't allow fast ordered union...180 // // Makes sense to preserve source ordering where possible.181 // // May affect evaluation order, but it is not specified by jsonnet spec.182 // #[cfg(feature = "exp-preserve-order")]183 // true,184 #[cfg(feature = "exp-preserve-order")]185 false,186 )187 .into_iter()188 .collect::<BTreeSet<IStr>>();189 let patch_fields = patch190 .fields(191 // No need to look at the patch field order, I think?192 // New fields (that will be appended at the end) will be alphabeticaly-ordered,193 // but it is fine for jsonpatch, I don't think people write jsonpatch in jsonnet,194 // when they can use mixins.195 #[cfg(feature = "exp-preserve-order")]196 false,197 )198 .into_iter()199 .collect::<BTreeSet<IStr>>();200201 let mut out = ObjValueBuilder::new();202 for field in target_fields.union(&patch_fields) {203 let Some(field_patch) = patch.get(field.clone())? else {204 // All lazy fields might be unified into a single filtered object core instead of creating a thunk per, but this implementation is good enough.205 let target_field = target.get_lazy(field.clone()).expect("we're iterating over fields union, if field is missing in patch - it exists in target");206 out.field(field.clone()).thunk(target_field);207 continue;208 };209 if matches!(field_patch, Val::Null) {210 continue;211 }212 let field_target = target.get(field.clone())?.unwrap_or(Val::Null);213 out.field(field.clone())214 .value(builtin_merge_patch(field_target, field_patch)?);215 }216 Ok(out.build().into())217}tests/golden/issue188.jsonnetdiffbeforeafterboth--- /dev/null
+++ b/tests/golden/issue188.jsonnet
@@ -0,0 +1 @@
+std.mergePatch({ val: error 'should not error' }, {}) + { val+:: {} }
tests/golden/issue188.jsonnet.goldendiffbeforeafterboth--- /dev/null
+++ b/tests/golden/issue188.jsonnet.golden
@@ -0,0 +1 @@
+{ }
\ No newline at end of file