difftreelog
feat `*stripChars` builtins
in: master
12 files changed
.editorconfigdiffbeforeafterboth--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[tests/golden/*.jsonnet.golden]
+generated_code = true
+indent_style = space
+indent_size = 4
+insert_final_newline = false
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -90,9 +90,9 @@
[[package]]
name = "anyhow"
-version = "1.0.83"
+version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "autocfg"
@@ -102,9 +102,9 @@
[[package]]
name = "base64"
-version = "0.21.7"
+version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "beef"
@@ -194,7 +194,7 @@
"heck",
"proc-macro2",
"quote",
- "syn 2.0.61",
+ "syn 2.0.64",
]
[[package]]
@@ -258,6 +258,12 @@
]
[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -289,9 +295,9 @@
[[package]]
name = "either"
-version = "1.11.0"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
+checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
[[package]]
name = "encode_unicode"
@@ -412,9 +418,9 @@
[[package]]
name = "insta"
-version = "1.38.0"
+version = "1.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc"
+checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5"
dependencies = [
"console",
"lazy_static",
@@ -430,9 +436,9 @@
[[package]]
name = "itertools"
-version = "0.12.1"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
@@ -547,7 +553,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.61",
+ "syn 2.0.64",
]
[[package]]
@@ -608,6 +614,17 @@
]
[[package]]
+name = "json-structural-diff"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25c7940d3c84d2079306c176c7b2b37622b6bc5e43fbd1541b1e4a4e1fd02045"
+dependencies = [
+ "difflib",
+ "regex",
+ "serde_json",
+]
+
+[[package]]
name = "keccak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -624,9 +641,9 @@
[[package]]
name = "libc"
-version = "0.2.154"
+version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libjsonnet"
@@ -646,9 +663,9 @@
[[package]]
name = "linux-raw-sys"
-version = "0.4.13"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lock_api"
@@ -681,7 +698,7 @@
"proc-macro2",
"quote",
"regex-syntax",
- "syn 2.0.61",
+ "syn 2.0.64",
]
[[package]]
@@ -989,22 +1006,22 @@
[[package]]
name = "serde"
-version = "1.0.201"
+version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
+checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.201"
+version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
+checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.61",
+ "syn 2.0.64",
]
[[package]]
@@ -1121,9 +1138,9 @@
[[package]]
name = "syn"
-version = "2.0.61"
+version = "2.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
+checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
dependencies = [
"proc-macro2",
"quote",
@@ -1149,7 +1166,9 @@
"jrsonnet-evaluator",
"jrsonnet-gcmodule",
"jrsonnet-stdlib",
+ "json-structural-diff",
"serde",
+ "serde_json",
]
[[package]]
@@ -1160,22 +1179,22 @@
[[package]]
name = "thiserror"
-version = "1.0.60"
+version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.60"
+version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.61",
+ "syn 2.0.64",
]
[[package]]
@@ -1347,5 +1366,5 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.61",
+ "syn 2.0.64",
]
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,8 +44,8 @@
serde_yaml_with_quirks = "0.8.24"
# Error handling
-anyhow = "1.0.80"
-thiserror = "1.0"
+anyhow = "1.0.83"
+thiserror = "1.0.60"
# Code formatting
dprint-core = "0.65.0"
@@ -63,20 +63,20 @@
# Source code parsing.
# Jrsonnet has two parsers for jsonnet - one is for execution, and another is for better parsing diagnostics/lints/LSP.
# First (and fast one) is based on peg, second is based on rowan.
-peg = "0.8.2"
+peg = "0.8.3"
logos = "0.14.0"
ungrammar = "1.16.1"
-rowan = "0.15"
+rowan = "0.15.15"
mimallocator = "0.1.3"
indoc = "2.0"
-insta = "1.35"
+insta = "1.39"
tempfile = "3.10"
pathdiff = "0.2.1"
-hashbrown = "0.14.3"
+hashbrown = "0.14.5"
static_assertions = "1.1"
rustc-hash = "1.1"
-num-bigint = "0.4.4"
+num-bigint = "0.4.5"
derivative = "2.2.0"
strsim = "0.11.0"
structdump = "0.2.0"
@@ -84,16 +84,18 @@
quote = "1.0"
syn = "2.0"
drop_bomb = "0.1.5"
-base64 = "0.21.7"
+base64 = "0.22.1"
indexmap = "2.2.3"
-itertools = "0.12.1"
-xshell = "0.2.5"
+itertools = "0.13.0"
+xshell = "0.2.6"
lsp-server = "0.7.6"
-lsp-types = "0.95.0"
+lsp-types = "0.96.0"
-regex = "1.10.3"
-lru = "0.12.2"
+regex = "1.10"
+lru = "0.12.3"
+
+json-structural-diff = "0.1.0"
[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "deny"
crates/jrsonnet-evaluator/src/arr/mod.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/arr/mod.rs
+++ b/crates/jrsonnet-evaluator/src/arr/mod.rs
@@ -11,7 +11,7 @@
/// Represents a Jsonnet array value.
#[derive(Debug, Clone, Trace)]
-// may contrain other ArrValue
+// may contain other ArrValue
#[trace(tracking(force))]
pub struct ArrValue(Cc<TraceBox<dyn ArrayLike>>);
crates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth1use std::{2 cell::RefCell,3 fmt::{self, Debug, Display},4 mem::replace,5 num::NonZeroU32,6 rc::Rc,7};89use jrsonnet_gcmodule::{Cc, Trace};10use jrsonnet_interner::IStr;11use jrsonnet_types::ValType;1213pub use crate::arr::{ArrValue, ArrayLike};14use crate::{15 bail,16 error::{Error, ErrorKind::*},17 function::FuncVal,18 gc::{GcHashMap, TraceBox},19 manifest::{ManifestFormat, ToStringFormat},20 tb,21 typed::BoundedUsize,22 ObjValue, Result, Unbound, WeakObjValue,23};2425pub trait ThunkValue: Trace {26 type Output;27 fn get(self: Box<Self>) -> Result<Self::Output>;28}2930#[derive(Trace)]31enum ThunkInner<T: Trace> {32 Computed(T),33 Errored(Error),34 Waiting(TraceBox<dyn ThunkValue<Output = T>>),35 Pending,36}3738/// Lazily evaluated value39#[allow(clippy::module_name_repetitions)]40#[derive(Clone, Trace)]41pub struct Thunk<T: Trace>(Cc<RefCell<ThunkInner<T>>>);4243impl<T: Trace> Thunk<T> {44 pub fn evaluated(val: T) -> Self {45 Self(Cc::new(RefCell::new(ThunkInner::Computed(val))))46 }47 pub fn new(f: impl ThunkValue<Output = T> + 'static) -> Self {48 Self(Cc::new(RefCell::new(ThunkInner::Waiting(tb!(f)))))49 }50 pub fn errored(e: Error) -> Self {51 Self(Cc::new(RefCell::new(ThunkInner::Errored(e))))52 }53 pub fn result(res: Result<T, Error>) -> Self {54 match res {55 Ok(o) => Self::evaluated(o),56 Err(e) => Self::errored(e),57 }58 }59}6061impl<T> Thunk<T>62where63 T: Clone + Trace,64{65 pub fn force(&self) -> Result<()> {66 self.evaluate()?;67 Ok(())68 }6970 /// Evaluate thunk, or return cached value71 ///72 /// # Errors73 ///74 /// - Lazy value evaluation returned error75 /// - This method was called during inner value evaluation76 pub fn evaluate(&self) -> Result<T> {77 match &*self.0.borrow() {78 ThunkInner::Computed(v) => return Ok(v.clone()),79 ThunkInner::Errored(e) => return Err(e.clone()),80 ThunkInner::Pending => return Err(InfiniteRecursionDetected.into()),81 ThunkInner::Waiting(..) => (),82 };83 let ThunkInner::Waiting(value) = replace(&mut *self.0.borrow_mut(), ThunkInner::Pending)84 else {85 unreachable!();86 };87 let new_value = match value.0.get() {88 Ok(v) => v,89 Err(e) => {90 *self.0.borrow_mut() = ThunkInner::Errored(e.clone());91 return Err(e);92 }93 };94 *self.0.borrow_mut() = ThunkInner::Computed(new_value.clone());95 Ok(new_value)96 }97}9899pub trait ThunkMapper<Input>: Trace {100 type Output;101 fn map(self, from: Input) -> Result<Self::Output>;102}103impl<Input> Thunk<Input>104where105 Input: Trace + Clone,106{107 pub fn map<M>(self, mapper: M) -> Thunk<M::Output>108 where109 M: ThunkMapper<Input>,110 M::Output: Trace,111 {112 #[derive(Trace)]113 struct Mapped<Input: Trace, Mapper: Trace> {114 inner: Thunk<Input>,115 mapper: Mapper,116 }117 impl<Input, Mapper> ThunkValue for Mapped<Input, Mapper>118 where119 Input: Trace + Clone,120 Mapper: ThunkMapper<Input>,121 {122 type Output = Mapper::Output;123124 fn get(self: Box<Self>) -> Result<Self::Output> {125 let value = self.inner.evaluate()?;126 let mapped = self.mapper.map(value)?;127 Ok(mapped)128 }129 }130131 Thunk::new(Mapped::<Input, M> {132 inner: self,133 mapper,134 })135 }136}137138impl<T: Trace> From<Result<T>> for Thunk<T> {139 fn from(value: Result<T>) -> Self {140 match value {141 Ok(o) => Self::evaluated(o),142 Err(e) => Self::errored(e),143 }144 }145}146impl<T, V: Trace> From<T> for Thunk<V>147where148 T: ThunkValue<Output = V>,149{150 fn from(value: T) -> Self {151 Self::new(value)152 }153}154155impl<T: Trace + Default> Default for Thunk<T> {156 fn default() -> Self {157 Self::evaluated(T::default())158 }159}160161type CacheKey = (Option<WeakObjValue>, Option<WeakObjValue>);162163#[derive(Trace, Clone)]164pub struct CachedUnbound<I, T>165where166 I: Unbound<Bound = T>,167 T: Trace,168{169 cache: Cc<RefCell<GcHashMap<CacheKey, T>>>,170 value: I,171}172impl<I: Unbound<Bound = T>, T: Trace> CachedUnbound<I, T> {173 pub fn new(value: I) -> Self {174 Self {175 cache: Cc::new(RefCell::new(GcHashMap::new())),176 value,177 }178 }179}180impl<I: Unbound<Bound = T>, T: Clone + Trace> Unbound for CachedUnbound<I, T> {181 type Bound = T;182 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<T> {183 let cache_key = (184 sup.as_ref().map(|s| s.clone().downgrade()),185 this.as_ref().map(|t| t.clone().downgrade()),186 );187 {188 if let Some(t) = self.cache.borrow().get(&cache_key) {189 return Ok(t.clone());190 }191 }192 let bound = self.value.bind(sup, this)?;193194 {195 let mut cache = self.cache.borrow_mut();196 cache.insert(cache_key, bound.clone());197 }198199 Ok(bound)200 }201}202203impl<T: Debug + Trace> Debug for Thunk<T> {204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {205 write!(f, "Lazy")206 }207}208impl<T: Trace> PartialEq for Thunk<T> {209 fn eq(&self, other: &Self) -> bool {210 Cc::ptr_eq(&self.0, &other.0)211 }212}213214/// Represents a Jsonnet value, which can be sliced or indexed (string or array).215#[allow(clippy::module_name_repetitions)]216pub enum IndexableVal {217 /// String.218 Str(IStr),219 /// Array.220 Arr(ArrValue),221}222impl IndexableVal {223 pub fn is_empty(&self) -> bool {224 match self {225 Self::Str(s) => s.is_empty(),226 Self::Arr(s) => s.is_empty(),227 }228 }229230 pub fn to_array(self) -> ArrValue {231 match self {232 Self::Str(s) => ArrValue::chars(s.chars()),233 Self::Arr(arr) => arr,234 }235 }236 /// Slice the value.237 ///238 /// # Implementation239 ///240 /// For strings, will create a copy of specified interval.241 ///242 /// For arrays, nothing will be copied on this call, instead [`ArrValue::Slice`] view will be returned.243 pub fn slice(244 self,245 index: Option<i32>,246 end: Option<i32>,247 step: Option<BoundedUsize<1, { i32::MAX as usize }>>,248 ) -> Result<Self> {249 match &self {250 Self::Str(s) => {251 let mut computed_len = None;252 let mut get_len = || {253 computed_len.map_or_else(254 || {255 let len = s.chars().count();256 let _ = computed_len.insert(len);257 len258 },259 |len| len,260 )261 };262 let mut get_idx = |pos: Option<i32>, default| {263 match pos {264 Some(v) if v < 0 => get_len().saturating_sub((-v) as usize),265 // No need to clamp, as iterator interface is used266 Some(v) => v as usize,267 None => default,268 }269 };270271 let index = get_idx(index, 0);272 let end = get_idx(end, usize::MAX);273 let step = step.as_deref().copied().unwrap_or(1);274275 if index >= end {276 return Ok(Self::Str("".into()));277 }278279 Ok(Self::Str(280 (s.chars()281 .skip(index)282 .take(end - index)283 .step_by(step)284 .collect::<String>())285 .into(),286 ))287 }288 Self::Arr(arr) => Ok(Self::Arr(arr.clone().slice(289 index,290 end,291 step.map(|v| NonZeroU32::new(v.value() as u32).expect("bounded != 0")),292 ))),293 }294 }295}296297#[derive(Debug, Clone, Trace)]298pub enum StrValue {299 Flat(IStr),300 Tree(Rc<(StrValue, StrValue, usize)>),301}302impl StrValue {303 pub fn concat(a: Self, b: Self) -> Self {304 // TODO: benchmark for an optimal value, currently just a arbitrary choice305 const STRING_EXTEND_THRESHOLD: usize = 100;306307 if a.is_empty() {308 b309 } else if b.is_empty() {310 a311 } else if a.len() + b.len() < STRING_EXTEND_THRESHOLD {312 Self::Flat(format!("{a}{b}").into())313 } else {314 let len = a.len() + b.len();315 Self::Tree(Rc::new((a, b, len)))316 }317 }318 pub fn into_flat(self) -> IStr {319 #[cold]320 fn write_buf(s: &StrValue, out: &mut String) {321 match s {322 StrValue::Flat(f) => out.push_str(f),323 StrValue::Tree(t) => {324 write_buf(&t.0, out);325 write_buf(&t.1, out);326 }327 }328 }329 match self {330 Self::Flat(f) => f,331 Self::Tree(_) => {332 let mut buf = String::with_capacity(self.len());333 write_buf(&self, &mut buf);334 buf.into()335 }336 }337 }338 pub fn len(&self) -> usize {339 match self {340 Self::Flat(v) => v.len(),341 Self::Tree(t) => t.2,342 }343 }344 pub fn is_empty(&self) -> bool {345 match self {346 Self::Flat(v) => v.is_empty(),347 // Can't create non-flat empty string348 Self::Tree(_) => false,349 }350 }351}352impl<T> From<T> for StrValue353where354 IStr: From<T>,355{356 fn from(value: T) -> Self {357 Self::Flat(IStr::from(value))358 }359}360impl Display for StrValue {361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {362 match self {363 Self::Flat(v) => write!(f, "{v}"),364 Self::Tree(t) => {365 write!(f, "{}", t.0)?;366 write!(f, "{}", t.1)367 }368 }369 }370}371impl PartialEq for StrValue {372 // False positive, into_flat returns not StrValue, but IStr, thus no infinite recursion here.373 #[allow(clippy::unconditional_recursion)]374 fn eq(&self, other: &Self) -> bool {375 let a = self.clone().into_flat();376 let b = other.clone().into_flat();377 a == b378 }379}380impl Eq for StrValue {}381impl PartialOrd for StrValue {382 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {383 Some(self.cmp(other))384 }385}386impl Ord for StrValue {387 fn cmp(&self, other: &Self) -> std::cmp::Ordering {388 let a = self.clone().into_flat();389 let b = other.clone().into_flat();390 a.cmp(&b)391 }392}393394/// Represents any valid Jsonnet value.395#[derive(Debug, Clone, Trace, Default)]396pub enum Val {397 /// Represents a Jsonnet boolean.398 Bool(bool),399 /// Represents a Jsonnet null value.400 #[default]401 Null,402 /// Represents a Jsonnet string.403 Str(StrValue),404 /// Represents a Jsonnet number.405 /// Should be finite, and not NaN406 /// This restriction isn't enforced by enum, as enum field can't be marked as private407 Num(f64),408 /// Experimental bigint409 #[cfg(feature = "exp-bigint")]410 BigInt(#[trace(skip)] Box<num_bigint::BigInt>),411 /// Represents a Jsonnet array.412 Arr(ArrValue),413 /// Represents a Jsonnet object.414 Obj(ObjValue),415 /// Represents a Jsonnet function.416 Func(FuncVal),417}418419#[cfg(target_pointer_width = "64")]420static_assertions::assert_eq_size!(Val, [u8; 24]);421422impl From<IndexableVal> for Val {423 fn from(v: IndexableVal) -> Self {424 match v {425 IndexableVal::Str(s) => Self::string(s),426 IndexableVal::Arr(a) => Self::Arr(a),427 }428 }429}430431impl Val {432 pub const fn as_bool(&self) -> Option<bool> {433 match self {434 Self::Bool(v) => Some(*v),435 _ => None,436 }437 }438 pub const fn as_null(&self) -> Option<()> {439 match self {440 Self::Null => Some(()),441 _ => None,442 }443 }444 pub fn as_str(&self) -> Option<IStr> {445 match self {446 Self::Str(s) => Some(s.clone().into_flat()),447 _ => None,448 }449 }450 pub const fn as_num(&self) -> Option<f64> {451 match self {452 Self::Num(n) => Some(*n),453 _ => None,454 }455 }456 pub fn as_arr(&self) -> Option<ArrValue> {457 match self {458 Self::Arr(a) => Some(a.clone()),459 _ => None,460 }461 }462 pub fn as_obj(&self) -> Option<ObjValue> {463 match self {464 Self::Obj(o) => Some(o.clone()),465 _ => None,466 }467 }468 pub fn as_func(&self) -> Option<FuncVal> {469 match self {470 Self::Func(f) => Some(f.clone()),471 _ => None,472 }473 }474475 /// Creates `Val::Num` after checking for numeric overflow.476 /// As numbers are `f64`, we can just check for their finity.477 pub fn new_checked_num(num: f64) -> Result<Self> {478 if num.is_finite() {479 Ok(Self::Num(num))480 } else {481 bail!("overflow")482 }483 }484485 pub const fn value_type(&self) -> ValType {486 match self {487 Self::Str(..) => ValType::Str,488 Self::Num(..) => ValType::Num,489 #[cfg(feature = "exp-bigint")]490 Self::BigInt(..) => ValType::BigInt,491 Self::Arr(..) => ValType::Arr,492 Self::Obj(..) => ValType::Obj,493 Self::Bool(_) => ValType::Bool,494 Self::Null => ValType::Null,495 Self::Func(..) => ValType::Func,496 }497 }498499 pub fn manifest(&self, format: impl ManifestFormat) -> Result<String> {500 fn manifest_dyn(val: &Val, manifest: &dyn ManifestFormat) -> Result<String> {501 manifest.manifest(val.clone())502 }503 manifest_dyn(self, &format)504 }505506 pub fn to_string(&self) -> Result<IStr> {507 Ok(match self {508 Self::Bool(true) => "true".into(),509 Self::Bool(false) => "false".into(),510 Self::Null => "null".into(),511 Self::Str(s) => s.clone().into_flat(),512 _ => self.manifest(ToStringFormat).map(IStr::from)?,513 })514 }515516 pub fn into_indexable(self) -> Result<IndexableVal> {517 Ok(match self {518 Self::Str(s) => IndexableVal::Str(s.into_flat()),519 Self::Arr(arr) => IndexableVal::Arr(arr),520 _ => bail!(ValueIsNotIndexable(self.value_type())),521 })522 }523524 pub fn function(function: impl Into<FuncVal>) -> Self {525 Self::Func(function.into())526 }527 pub fn string(string: impl Into<StrValue>) -> Self {528 Self::Str(string.into())529 }530}531532impl From<IStr> for Val {533 fn from(value: IStr) -> Self {534 Self::string(value)535 }536}537impl From<String> for Val {538 fn from(value: String) -> Self {539 Self::string(value)540 }541}542impl From<&str> for Val {543 fn from(value: &str) -> Self {544 Self::string(value)545 }546}547impl From<ObjValue> for Val {548 fn from(value: ObjValue) -> Self {549 Self::Obj(value)550 }551}552553const fn is_function_like(val: &Val) -> bool {554 matches!(val, Val::Func(_))555}556557/// Native implementation of `std.primitiveEquals`558pub fn primitive_equals(val_a: &Val, val_b: &Val) -> Result<bool> {559 Ok(match (val_a, val_b) {560 (Val::Bool(a), Val::Bool(b)) => a == b,561 (Val::Null, Val::Null) => true,562 (Val::Str(a), Val::Str(b)) => a == b,563 (Val::Num(a), Val::Num(b)) => (a - b).abs() <= f64::EPSILON,564 #[cfg(feature = "exp-bigint")]565 (Val::BigInt(a), Val::BigInt(b)) => a == b,566 (Val::Arr(_), Val::Arr(_)) => {567 bail!("primitiveEquals operates on primitive types, got array")568 }569 (Val::Obj(_), Val::Obj(_)) => {570 bail!("primitiveEquals operates on primitive types, got object")571 }572 (a, b) if is_function_like(a) && is_function_like(b) => {573 bail!("cannot test equality of functions")574 }575 (_, _) => false,576 })577}578579/// Native implementation of `std.equals`580pub fn equals(val_a: &Val, val_b: &Val) -> Result<bool> {581 if val_a.value_type() != val_b.value_type() {582 return Ok(false);583 }584 match (val_a, val_b) {585 (Val::Arr(a), Val::Arr(b)) => {586 if ArrValue::ptr_eq(a, b) {587 return Ok(true);588 }589 if a.len() != b.len() {590 return Ok(false);591 }592 for (a, b) in a.iter().zip(b.iter()) {593 if !equals(&a?, &b?)? {594 return Ok(false);595 }596 }597 Ok(true)598 }599 (Val::Obj(a), Val::Obj(b)) => {600 if ObjValue::ptr_eq(a, b) {601 return Ok(true);602 }603 let fields = a.fields(604 #[cfg(feature = "exp-preserve-order")]605 false,606 );607 if fields608 != b.fields(609 #[cfg(feature = "exp-preserve-order")]610 false,611 ) {612 return Ok(false);613 }614 for field in fields {615 if !equals(616 &a.get(field.clone())?.expect("field exists"),617 &b.get(field)?.expect("field exists"),618 )? {619 return Ok(false);620 }621 }622 Ok(true)623 }624 (a, b) => Ok(primitive_equals(a, b)?),625 }626}crates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -201,6 +201,9 @@
("parseOctal", builtin_parse_octal::INST),
("parseHex", builtin_parse_hex::INST),
("stringChars", builtin_string_chars::INST),
+ ("lstripChars", builtin_lstrip_chars::INST),
+ ("rstripChars", builtin_rstrip_chars::INST),
+ ("stripChars", builtin_strip_chars::INST),
// Misc
("length", builtin_length::INST),
("get", builtin_get::INST),
crates/jrsonnet-stdlib/src/std.jsonnetdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/std.jsonnet
+++ b/crates/jrsonnet-stdlib/src/std.jsonnet
@@ -3,22 +3,6 @@
thisFile:: error 'std.thisFile is deprecated, to enable its support in jrsonnet - recompile it with "legacy-this-file" support.\nThis will slow down stdlib caching a bit, though',
- lstripChars(str, chars)::
- if std.length(str) > 0 && std.member(chars, str[0]) then
- std.lstripChars(str[1:], chars)
- else
- str,
-
- rstripChars(str, chars)::
- local len = std.length(str);
- if len > 0 && std.member(chars, str[len - 1]) then
- std.rstripChars(str[:len - 1], chars)
- else
- str,
-
- stripChars(str, chars)::
- std.lstripChars(std.rstripChars(str, chars), chars),
-
mapWithIndex(func, arr)::
if !std.isFunction(func) then
error ('std.mapWithIndex first param must be function, got ' + std.type(func))
crates/jrsonnet-stdlib/src/strings.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/strings.rs
+++ b/crates/jrsonnet-stdlib/src/strings.rs
@@ -1,9 +1,11 @@
+use std::collections::BTreeSet;
+
use jrsonnet_evaluator::{
bail,
error::{ErrorKind::*, Result},
function::builtin,
- typed::{Either2, M1},
- val::ArrValue,
+ typed::{Either2, Typed, M1},
+ val::{ArrValue, IndexableVal},
Either, IStr, Val,
};
@@ -215,6 +217,53 @@
})
}
+#[builtin]
+pub fn builtin_string_chars(str: IStr) -> ArrValue {
+ ArrValue::chars(str.chars())
+}
+
+#[builtin]
+pub fn builtin_lstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {
+ if str.is_empty() || chars.is_empty() {
+ return Ok(str);
+ }
+
+ let pattern = new_trim_pattern(chars)?;
+ Ok(str.as_str().trim_start_matches(pattern).into())
+}
+
+#[builtin]
+pub fn builtin_rstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {
+ if str.is_empty() || chars.is_empty() {
+ return Ok(str);
+ }
+
+ let pattern = new_trim_pattern(chars)?;
+ Ok(str.as_str().trim_end_matches(pattern).into())
+}
+
+#[builtin]
+pub fn builtin_strip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {
+ if str.is_empty() || chars.is_empty() {
+ return Ok(str);
+ }
+
+ let pattern = new_trim_pattern(chars)?;
+ Ok(str.as_str().trim_matches(pattern).into())
+}
+
+fn new_trim_pattern(chars: IndexableVal) -> Result<impl Fn(char) -> bool> {
+ let chars: BTreeSet<char> = match chars {
+ IndexableVal::Str(chars) => chars.chars().collect(),
+ IndexableVal::Arr(chars) => chars
+ .iter()
+ .filter_map(|it| it.map(|it| char::from_untyped(it).ok()).transpose())
+ .collect::<Result<_, _>>()?,
+ };
+
+ Ok(move |char| chars.contains(&char))
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -242,9 +291,4 @@
assert_eq!(parse_nat::<16>("a9").unwrap(), 0xA9 as f64);
assert_eq!(parse_nat::<16>("BbC").unwrap(), 0xBBC as f64);
}
-}
-
-#[builtin]
-pub fn builtin_string_chars(str: IStr) -> ArrValue {
- ArrValue::chars(str.chars())
}
tests/Cargo.tomldiffbeforeafterboth--- a/tests/Cargo.toml
+++ b/tests/Cargo.toml
@@ -12,3 +12,5 @@
jrsonnet-gcmodule.workspace = true
jrsonnet-stdlib.workspace = true
serde.workspace = true
+json-structural-diff.workspace = true
+serde_json.workspace = true
tests/golden/builtin_strings_string.jsonnetdiffbeforeafterboth--- /dev/null
+++ b/tests/golden/builtin_strings_string.jsonnet
@@ -0,0 +1,21 @@
+{
+ lstripChars_singleChar: std.lstripChars("aaabcdef", "a"),
+ lstripChars_multipleChars: std.lstripChars("klmn", "kql"),
+ lstripChars_array: std.lstripChars("forward", [1, "f", [], "o", "d", "for"]),
+
+ rstripChars_singleChar: std.rstripChars("nice_boy", "y"),
+ rstripChars_multipleChars: std.rstripChars("amoguass", "sa"),
+ rstripChars_array: std.rstripChars("cool just cool", ["o", "l", 12.2323443]),
+
+ stripChars_singleCharL: std.stripChars("feefoofaa", "f"),
+ stripChars_singleCharR: std.stripChars("lolkekw", "w"),
+ stripChars_singleChar: std.stripChars("joper jej", "j"),
+
+ stripChars_multipleCharsL: std.stripChars("abcdefg", "cab"),
+ stripChars_multipleCharsR: std.stripChars("still breathing", "gthin"),
+ stripChars_multipleChars: std.stripChars("sus sus sus", "us"),
+
+ stripChars_arrayL: std.stripChars("chel medvedo svin", ["c", 3204990, {"svin": {}}, "vi"]),
+ stripChars_arrayR: std.stripChars("lach-vs-miri", ["r", "i", "craft", "is", "mine"]),
+ stripChars_array: std.stripChars("UwU Lel Stosh", ["h", "U", "s", {}, [], null, "w", [1, 2, 3]]),
+}
tests/golden/builtin_strings_string.jsonnet.goldendiffbeforeafterboth--- /dev/null
+++ b/tests/golden/builtin_strings_string.jsonnet.golden
@@ -0,0 +1,17 @@
+{
+ "lstripChars_array": "rward",
+ "lstripChars_multipleChars": "mn",
+ "lstripChars_singleChar": "bcdef",
+ "rstripChars_array": "cool just c",
+ "rstripChars_multipleChars": "amogu",
+ "rstripChars_singleChar": "nice_bo",
+ "stripChars_array": " Lel Sto",
+ "stripChars_arrayL": "hel medvedo svin",
+ "stripChars_arrayR": "lach-vs-m",
+ "stripChars_multipleChars": " sus ",
+ "stripChars_multipleCharsL": "defg",
+ "stripChars_multipleCharsR": "still brea",
+ "stripChars_singleChar": "oper je",
+ "stripChars_singleCharL": "eefoofaa",
+ "stripChars_singleCharR": "lolkek"
+}
\ No newline at end of file
tests/tests/golden.rsdiffbeforeafterboth--- a/tests/tests/golden.rs
+++ b/tests/tests/golden.rs
@@ -9,7 +9,6 @@
FileImportResolver, State,
};
use jrsonnet_stdlib::StateExt;
-
mod common;
fn run(file: &Path) -> String {
@@ -35,6 +34,8 @@
#[test]
fn test() -> io::Result<()> {
+ use json_structural_diff::JsonDiff;
+
let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
root.push("golden");
@@ -54,6 +55,35 @@
} else {
let golden = fs::read_to_string(golden_path)?;
+ match (serde_json::from_str(&result), serde_json::from_str(&golden)) {
+ (Err(_), Ok(_)) => assert_eq!(
+ result,
+ golden,
+ "unexpected error for golden {}",
+ entry.path().display()
+ ),
+ (Ok(_), Err(_)) => assert_eq!(
+ result,
+ golden,
+ "expected error for golden {}",
+ entry.path().display()
+ ),
+ (Ok(result), Ok(golden)) => {
+ // Show diff relative to golden`.
+ let diff = JsonDiff::diff_string(&golden, &result, false);
+ if let Some(diff) = diff {
+ panic!(
+ "Result \n{result:#}\n\
+ and golden \n{golden:#}\n\
+ did not match structurally:\n{diff:#}\n\
+ for golden {}",
+ entry.path().display()
+ );
+ }
+ }
+ (Err(_), Err(_)) => {}
+ };
+
assert_eq!(
result,
golden,