difftreelog
feat sync jsonnet stdlib changes
in: master
8 files changed
crates/jrsonnet-evaluator/src/manifest.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/manifest.rs
+++ b/crates/jrsonnet-evaluator/src/manifest.rs
@@ -413,18 +413,19 @@
val.value_type()
)
};
- if !arr.is_empty() {
- for (i, v) in arr.iter().enumerate() {
- let v = v.with_description(|| format!("elem <{i}> evaluation"))?;
- out.push_str("---\n");
- in_description_frame(
- || format!("elem <{i}> manifestification"),
- || self.inner.manifest_buf(v, out),
- )?;
+ for (i, v) in arr.iter().enumerate() {
+ if i != 0 {
out.push('\n');
}
+ let v = v.with_description(|| format!("elem <{i}> evaluation"))?;
+ out.push_str("---\n");
+ in_description_frame(
+ || format!("elem <{i}> manifestification"),
+ || self.inner.manifest_buf(v, out),
+ )?;
}
if self.c_document_end {
+ out.push('\n');
out.push_str("...");
}
if self.end_newline {
crates/jrsonnet-parser/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-parser/src/lib.rs
+++ b/crates/jrsonnet-parser/src/lib.rs
@@ -237,7 +237,11 @@
Expr::ArrComp(expr, specs)
}
pub rule number_expr(s: &ParserSettings) -> Expr
- = n:number() { expr::Expr::Num(n) }
+ = n:number() {? if n.is_finite() {
+ Ok(expr::Expr::Num(n))
+ } else {
+ Err("!!!numbers are finite")
+ }}
pub rule var_expr(s: &ParserSettings) -> Expr
= n:id() { expr::Expr::Var(n) }
pub rule id_loc(s: &ParserSettings) -> LocExpr
crates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -3,6 +3,7 @@
use std::{
cell::{Ref, RefCell, RefMut},
collections::HashMap,
+ f64,
rc::Rc,
};
@@ -14,6 +15,7 @@
error::{ErrorKind::*, Result},
function::{CallLocation, FuncVal, TlaArg},
trace::PathResolver,
+ val::NumValue,
ContextBuilder, IStr, ObjValue, ObjValueBuilder, Thunk, Val,
};
use jrsonnet_gcmodule::{Acyclic, Cc, Trace};
@@ -63,6 +65,7 @@
("isObject", builtin_is_object::INST),
("isArray", builtin_is_array::INST),
("isFunction", builtin_is_function::INST),
+ ("isNull", builtin_is_null::INST),
// Arrays
("makeArray", builtin_make_array::INST),
("repeat", builtin_repeat::INST),
@@ -104,6 +107,8 @@
("floor", builtin_floor::INST),
("ceil", builtin_ceil::INST),
("log", builtin_log::INST),
+ ("log2", builtin_log2::INST),
+ ("log10", builtin_log10::INST),
("pow", builtin_pow::INST),
("sqrt", builtin_sqrt::INST),
("sin", builtin_sin::INST),
@@ -121,6 +126,9 @@
("isOdd", builtin_is_odd::INST),
("isInteger", builtin_is_integer::INST),
("isDecimal", builtin_is_decimal::INST),
+ ("deg2rad", builtin_deg2rad::INST),
+ ("rad2deg", builtin_rad2deg::INST),
+ ("hypot", builtin_hypot::INST),
// Operator
("mod", builtin_mod::INST),
("primitiveEquals", builtin_primitive_equals::INST),
@@ -201,6 +209,7 @@
("lstripChars", builtin_lstrip_chars::INST),
("rstripChars", builtin_rstrip_chars::INST),
("stripChars", builtin_strip_chars::INST),
+ ("trim", builtin_trim::INST),
// Misc
("length", builtin_length::INST),
("get", builtin_get::INST),
@@ -248,6 +257,10 @@
builder.method("trace", builtin_trace { settings });
builder.method("id", FuncVal::Id);
+ builder.field("pi").hide().value(Val::Num(
+ NumValue::new(f64::consts::PI).expect("pi is finite"),
+ ));
+
#[cfg(feature = "exp-regex")]
{
// Regex
crates/jrsonnet-stdlib/src/math.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/math.rs
+++ b/crates/jrsonnet-stdlib/src/math.rs
@@ -1,3 +1,5 @@
+use std::f64;
+
use jrsonnet_evaluator::{function::builtin, typed::PositiveF64};
#[builtin]
@@ -56,6 +58,16 @@
}
#[builtin]
+pub fn builtin_log2(x: f64) -> f64 {
+ x.log2()
+}
+
+#[builtin]
+pub fn builtin_log10(x: f64) -> f64 {
+ x.log10()
+}
+
+#[builtin]
pub fn builtin_pow(x: f64, n: f64) -> f64 {
x.powf(n)
}
@@ -153,3 +165,18 @@
pub fn builtin_is_decimal(x: f64) -> bool {
builtin_round(x) != x
}
+
+#[builtin]
+pub fn builtin_deg2rad(x: f64) -> f64 {
+ x * f64::consts::PI / 180.0
+}
+
+#[builtin]
+pub fn builtin_rad2deg(x: f64) -> f64 {
+ x * 180.0 / f64::consts::PI
+}
+
+#[builtin]
+pub fn builtin_hypot(x: f64, y: f64) -> f64 {
+ x.hypot(y)
+}
crates/jrsonnet-stdlib/src/misc.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/misc.rs
+++ b/crates/jrsonnet-stdlib/src/misc.rs
@@ -156,8 +156,16 @@
#[cfg(feature = "exp-preserve-order")]
true,
);
- let a = a.manifest(&format).description("<a> manifestification")?;
- let b = b.manifest(&format).description("<b> manifestification")?;
+ let a = if let Some(a) = a.as_str() {
+ format!("<A>\n{a}\n</A>")
+ } else {
+ a.manifest(&format).description("<a> manifestification")?
+ };
+ let b = if let Some(b) = b.as_str() {
+ format!("<B>\n{b}\n</B>")
+ } else {
+ b.manifest(&format).description("<b> manifestification")?
+ };
bail!("assertion failed: A != B\nA: {a}\nB: {b}")
}
@@ -166,9 +174,7 @@
let Some(patch) = patch.as_obj() else {
return Ok(patch);
};
- let Some(target) = target.as_obj() else {
- return Ok(Val::Obj(patch));
- };
+ let target = target.as_obj().unwrap_or_else(|| ObjValue::new_empty());
let target_fields = target
.fields(
// FIXME: Makes no sense to preserve order for BTreeSet, it would be better to use IndexSet here?
@@ -203,10 +209,7 @@
if matches!(field_patch, Val::Null) {
continue;
}
- let Some(field_target) = target.get(field.clone())? else {
- out.field(field.clone()).value(field_patch);
- continue;
- };
+ let field_target = target.get(field.clone())?.unwrap_or(Val::Null);
out.field(field.clone())
.value(builtin_merge_patch(field_target, field_patch)?);
}
crates/jrsonnet-stdlib/src/objects.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/objects.rs
+++ b/crates/jrsonnet-stdlib/src/objects.rs
@@ -1,7 +1,8 @@
use jrsonnet_evaluator::{
function::builtin,
+ rustc_hash::FxHashSet,
val::{ArrValue, Val},
- IStr, ObjValue, ObjValueBuilder,
+ IStr, MaybeUnbound, ObjValue, ObjValueBuilder, Thunk,
};
#[builtin]
@@ -166,14 +167,31 @@
preserve_order: bool,
) -> ObjValue {
let mut new_obj = ObjValueBuilder::with_capacity(obj.len() - 1);
- for (k, v) in obj.iter(
+ let all_fields = obj.fields_ex(
+ true,
#[cfg(feature = "exp-preserve-order")]
preserve_order,
- ) {
- if k == key {
+ );
+ let visible_fields = obj
+ .fields_ex(
+ false,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
+ .into_iter()
+ .collect::<FxHashSet<_>>();
+
+ for field in &all_fields {
+ if *field == key {
continue;
}
- new_obj.field(k).value(v.unwrap());
+ let mut b = new_obj.field(field.clone());
+ if !visible_fields.contains(&field) {
+ b = b.hide();
+ }
+ let _ = b.binding(MaybeUnbound::Bound(Thunk::result(
+ obj.get(field.clone()).transpose().expect("field exists"),
+ )));
}
new_obj.build()
crates/jrsonnet-stdlib/src/strings.rsdiffbeforeafterboth1use std::collections::BTreeSet;23use jrsonnet_evaluator::{4 bail,5 error::{ErrorKind::*, Result},6 function::builtin,7 typed::{Either2, Typed, M1},8 val::{ArrValue, IndexableVal},9 Either, IStr, Val,10};1112#[builtin]13pub const fn builtin_codepoint(str: char) -> u32 {14 str as u3215}1617#[builtin]18pub fn builtin_substr(str: IStr, from: usize, len: usize) -> String {19 str.chars().skip(from).take(len).collect()20}2122#[builtin]23pub fn builtin_char(n: u32) -> Result<char> {24 Ok(std::char::from_u32(n).ok_or_else(|| InvalidUnicodeCodepointGot(n))?)25}2627#[builtin]28pub fn builtin_str_replace(str: String, from: IStr, to: IStr) -> String {29 str.replace(&from as &str, &to as &str)30}3132#[builtin]33pub fn builtin_escape_string_bash(str_: String) -> String {34 const QUOTE: char = '\'';35 let mut out = str_.replace(QUOTE, "'\"'\"'");36 out.insert(0, QUOTE);37 out.push(QUOTE);38 out39}4041#[builtin]42pub fn builtin_escape_string_dollars(str_: String) -> String {43 str_.replace('$', "$$")44}4546#[builtin]47pub fn builtin_is_empty(str: String) -> bool {48 str.is_empty()49}5051#[builtin]52pub fn builtin_equals_ignore_case(str1: String, str2: String) -> bool {53 str1.to_ascii_lowercase() == str2.to_ascii_lowercase()54}5556#[builtin]57pub fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {58 use Either2::*;59 match maxsplits {60 A(n) => str.splitn(n + 1, &c as &str).map(Val::string).collect(),61 B(_) => str.split(&c as &str).map(Val::string).collect(),62 }63}6465#[builtin]66pub fn builtin_splitlimitr(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {67 use Either2::*;68 match maxsplits {69 A(n) =>70 // rsplitn does not implement DoubleEndedIterator so collect into71 // a temporary vec72 {73 str.rsplitn(n + 1, &c as &str)74 .map(Val::string)75 .collect::<Vec<_>>()76 .into_iter()77 .rev()78 .collect()79 }80 B(_) => str.split(&c as &str).map(Val::string).collect(),81 }82}8384#[builtin]85pub fn builtin_split(str: IStr, c: IStr) -> ArrValue {86 use Either2::*;87 builtin_splitlimit(str, c, B(M1))88}8990#[builtin]91pub fn builtin_ascii_upper(str: IStr) -> String {92 str.to_ascii_uppercase()93}9495#[builtin]96pub fn builtin_ascii_lower(str: IStr) -> String {97 str.to_ascii_lowercase()98}99100#[builtin]101pub fn builtin_find_substr(pat: IStr, str: IStr) -> ArrValue {102 if pat.is_empty() || str.is_empty() || pat.len() > str.len() {103 return ArrValue::empty();104 }105106 let str = str.as_str();107 let pat = pat.as_bytes();108 let strb = str.as_bytes();109110 let max_pos = str.len() - pat.len();111112 let mut out: Vec<Val> = Vec::new();113 for (ch_idx, (i, _)) in str114 .char_indices()115 .take_while(|(i, _)| i <= &max_pos)116 .enumerate()117 {118 if &strb[i..i + pat.len()] == pat {119 out.push(Val::Num(120 ch_idx.try_into().expect("unrealisticly long string"),121 ));122 }123 }124 out.into()125}126127#[builtin]128pub fn builtin_parse_int(str: IStr) -> Result<f64> {129 if let Some(raw) = str.strip_prefix('-') {130 if raw.is_empty() {131 bail!("integer only consists of a minus")132 }133134 parse_nat::<10>(raw).map(|value| -value)135 } else {136 if str.is_empty() {137 bail!("empty integer")138 }139140 parse_nat::<10>(str.as_str())141 }142}143144#[builtin]145pub fn builtin_parse_octal(str: IStr) -> Result<f64> {146 if str.is_empty() {147 bail!("empty octal integer");148 }149150 parse_nat::<8>(str.as_str())151}152153#[builtin]154pub fn builtin_parse_hex(str: IStr) -> Result<f64> {155 if str.is_empty() {156 bail!("empty hexadecimal integer");157 }158159 parse_nat::<16>(str.as_str())160}161162fn parse_nat<const BASE: u32>(raw: &str) -> Result<f64> {163 const ZERO_CODE: u32 = '0' as u32;164 const UPPER_A_CODE: u32 = 'A' as u32;165 const LOWER_A_CODE: u32 = 'a' as u32;166167 #[inline]168 fn checked_sub_if(condition: bool, lhs: u32, rhs: u32) -> Option<u32> {169 if condition {170 lhs.checked_sub(rhs)171 } else {172 None173 }174 }175176 debug_assert!(177 1 <= BASE && BASE <= 16,178 "integer base should be between 1 and 16"179 );180181 let base = f64::from(BASE);182183 raw.chars().try_fold(0f64, |aggregate, digit| {184 let digit = digit as u32;185 // if-let-else looks better here than Option combinators186 #[allow(clippy::option_if_let_else)]187 let digit = if let Some(digit) = checked_sub_if(BASE > 10, digit, LOWER_A_CODE) {188 digit + 10189 } else if let Some(digit) = checked_sub_if(BASE > 10, digit, UPPER_A_CODE) {190 digit + 10191 } else {192 digit.checked_sub(ZERO_CODE).unwrap_or(BASE)193 };194195 if digit < BASE {196 Ok(base.mul_add(aggregate, f64::from(digit)))197 } else {198 bail!("{raw:?} is not a base {BASE} integer");199 }200 })201}202203#[cfg(feature = "exp-bigint")]204#[builtin]205pub fn builtin_bigint(v: Either![f64, IStr]) -> Result<Val> {206 use jrsonnet_evaluator::runtime_error;207 use Either2::*;208 Ok(match v {209 A(a) => {210 Val::BigInt(Box::new(a.to_string().parse().map_err(|e| {211 runtime_error!("number is not convertible to bigint: {e}")212 })?))213 }214 B(b) => Val::BigInt(Box::new(215 b.as_str()216 .parse()217 .map_err(|e| runtime_error!("bad bigint: {e}"))?,218 )),219 })220}221222#[builtin]223pub fn builtin_string_chars(str: IStr) -> ArrValue {224 ArrValue::chars(str.chars())225}226227#[builtin]228pub fn builtin_lstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {229 if str.is_empty() || chars.is_empty() {230 return Ok(str);231 }232233 let pattern = new_trim_pattern(chars)?;234 Ok(str.as_str().trim_start_matches(pattern).into())235}236237#[builtin]238pub fn builtin_rstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {239 if str.is_empty() || chars.is_empty() {240 return Ok(str);241 }242243 let pattern = new_trim_pattern(chars)?;244 Ok(str.as_str().trim_end_matches(pattern).into())245}246247#[builtin]248pub fn builtin_strip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {249 if str.is_empty() || chars.is_empty() {250 return Ok(str);251 }252253 let pattern = new_trim_pattern(chars)?;254 Ok(str.as_str().trim_matches(pattern).into())255}256257fn new_trim_pattern(chars: IndexableVal) -> Result<impl Fn(char) -> bool> {258 let chars: BTreeSet<char> = match chars {259 IndexableVal::Str(chars) => chars.chars().collect(),260 IndexableVal::Arr(chars) => chars261 .iter()262 .filter_map(|it| it.map(|it| char::from_untyped(it).ok()).transpose())263 .collect::<Result<_, _>>()?,264 };265266 Ok(move |char| chars.contains(&char))267}268269#[cfg(test)]270#[allow(clippy::float_cmp)]271mod tests {272 use super::*;273274 #[test]275 fn parse_nat_base_8() {276 assert_eq!(parse_nat::<8>("0").unwrap(), 0.);277 assert_eq!(parse_nat::<8>("5").unwrap(), 5.);278 assert_eq!(parse_nat::<8>("32").unwrap(), f64::from(0o32));279 assert_eq!(parse_nat::<8>("761").unwrap(), f64::from(0o761));280 }281282 #[test]283 fn parse_nat_base_10() {284 assert_eq!(parse_nat::<10>("0").unwrap(), 0.);285 assert_eq!(parse_nat::<10>("3").unwrap(), 3.);286 assert_eq!(parse_nat::<10>("27").unwrap(), 27.);287 assert_eq!(parse_nat::<10>("123").unwrap(), 123.);288 }289290 #[test]291 fn parse_nat_base_16() {292 assert_eq!(parse_nat::<16>("0").unwrap(), 0.);293 assert_eq!(parse_nat::<16>("A").unwrap(), 10.);294 assert_eq!(parse_nat::<16>("a9").unwrap(), f64::from(0xA9));295 assert_eq!(parse_nat::<16>("BbC").unwrap(), f64::from(0xBBC));296 }297}1use std::collections::BTreeSet;23use jrsonnet_evaluator::{4 bail,5 error::{ErrorKind::*, Result},6 function::builtin,7 typed::{Either2, Typed, M1},8 val::{ArrValue, IndexableVal},9 Either, IStr, Val,10};1112#[builtin]13pub const fn builtin_codepoint(str: char) -> u32 {14 str as u3215}1617#[builtin]18pub fn builtin_substr(str: IStr, from: usize, len: usize) -> String {19 str.chars().skip(from).take(len).collect()20}2122#[builtin]23pub fn builtin_char(n: u32) -> Result<char> {24 Ok(std::char::from_u32(n).ok_or_else(|| InvalidUnicodeCodepointGot(n))?)25}2627#[builtin]28pub fn builtin_str_replace(str: String, from: IStr, to: IStr) -> String {29 str.replace(&from as &str, &to as &str)30}3132#[builtin]33pub fn builtin_escape_string_bash(str_: String) -> String {34 const QUOTE: char = '\'';35 let mut out = str_.replace(QUOTE, "'\"'\"'");36 out.insert(0, QUOTE);37 out.push(QUOTE);38 out39}4041#[builtin]42pub fn builtin_escape_string_dollars(str_: String) -> String {43 str_.replace('$', "$$")44}4546#[builtin]47pub fn builtin_is_empty(str: String) -> bool {48 str.is_empty()49}5051#[builtin]52pub fn builtin_equals_ignore_case(str1: String, str2: String) -> bool {53 str1.to_ascii_lowercase() == str2.to_ascii_lowercase()54}5556#[builtin]57pub fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {58 use Either2::*;59 match maxsplits {60 A(n) => str.splitn(n + 1, &c as &str).map(Val::string).collect(),61 B(_) => str.split(&c as &str).map(Val::string).collect(),62 }63}6465#[builtin]66pub fn builtin_splitlimitr(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {67 use Either2::*;68 match maxsplits {69 A(n) =>70 // rsplitn does not implement DoubleEndedIterator so collect into71 // a temporary vec72 {73 str.rsplitn(n + 1, &c as &str)74 .map(Val::string)75 .collect::<Vec<_>>()76 .into_iter()77 .rev()78 .collect()79 }80 B(_) => str.split(&c as &str).map(Val::string).collect(),81 }82}8384#[builtin]85pub fn builtin_split(str: IStr, c: IStr) -> ArrValue {86 use Either2::*;87 builtin_splitlimit(str, c, B(M1))88}8990#[builtin]91pub fn builtin_ascii_upper(str: IStr) -> String {92 str.to_ascii_uppercase()93}9495#[builtin]96pub fn builtin_ascii_lower(str: IStr) -> String {97 str.to_ascii_lowercase()98}99100#[builtin]101pub fn builtin_find_substr(pat: IStr, str: IStr) -> ArrValue {102 if pat.is_empty() || str.is_empty() || pat.len() > str.len() {103 return ArrValue::empty();104 }105106 let str = str.as_str();107 let pat = pat.as_bytes();108 let strb = str.as_bytes();109110 let max_pos = str.len() - pat.len();111112 let mut out: Vec<Val> = Vec::new();113 for (ch_idx, (i, _)) in str114 .char_indices()115 .take_while(|(i, _)| i <= &max_pos)116 .enumerate()117 {118 if &strb[i..i + pat.len()] == pat {119 out.push(Val::Num(120 ch_idx.try_into().expect("unrealisticly long string"),121 ));122 }123 }124 out.into()125}126127#[builtin]128pub fn builtin_parse_int(str: IStr) -> Result<f64> {129 if let Some(raw) = str.strip_prefix('-') {130 if raw.is_empty() {131 bail!("integer only consists of a minus")132 }133134 parse_nat::<10>(raw).map(|value| -value)135 } else {136 if str.is_empty() {137 bail!("empty integer")138 }139140 parse_nat::<10>(str.as_str())141 }142}143144#[builtin]145pub fn builtin_parse_octal(str: IStr) -> Result<f64> {146 if str.is_empty() {147 bail!("empty octal integer");148 }149150 parse_nat::<8>(str.as_str())151}152153#[builtin]154pub fn builtin_parse_hex(str: IStr) -> Result<f64> {155 if str.is_empty() {156 bail!("empty hexadecimal integer");157 }158159 parse_nat::<16>(str.as_str())160}161162fn parse_nat<const BASE: u32>(raw: &str) -> Result<f64> {163 const ZERO_CODE: u32 = '0' as u32;164 const UPPER_A_CODE: u32 = 'A' as u32;165 const LOWER_A_CODE: u32 = 'a' as u32;166167 #[inline]168 fn checked_sub_if(condition: bool, lhs: u32, rhs: u32) -> Option<u32> {169 if condition {170 lhs.checked_sub(rhs)171 } else {172 None173 }174 }175176 debug_assert!(177 1 <= BASE && BASE <= 16,178 "integer base should be between 1 and 16"179 );180181 let base = f64::from(BASE);182183 raw.chars().try_fold(0f64, |aggregate, digit| {184 let digit = digit as u32;185 // if-let-else looks better here than Option combinators186 #[allow(clippy::option_if_let_else)]187 let digit = if let Some(digit) = checked_sub_if(BASE > 10, digit, LOWER_A_CODE) {188 digit + 10189 } else if let Some(digit) = checked_sub_if(BASE > 10, digit, UPPER_A_CODE) {190 digit + 10191 } else {192 digit.checked_sub(ZERO_CODE).unwrap_or(BASE)193 };194195 if digit < BASE {196 Ok(base.mul_add(aggregate, f64::from(digit)))197 } else {198 bail!("{raw:?} is not a base {BASE} integer");199 }200 })201}202203#[cfg(feature = "exp-bigint")]204#[builtin]205pub fn builtin_bigint(v: Either![f64, IStr]) -> Result<Val> {206 use jrsonnet_evaluator::runtime_error;207 use Either2::*;208 Ok(match v {209 A(a) => {210 Val::BigInt(Box::new(a.to_string().parse().map_err(|e| {211 runtime_error!("number is not convertible to bigint: {e}")212 })?))213 }214 B(b) => Val::BigInt(Box::new(215 b.as_str()216 .parse()217 .map_err(|e| runtime_error!("bad bigint: {e}"))?,218 )),219 })220}221222#[builtin]223pub fn builtin_string_chars(str: IStr) -> ArrValue {224 ArrValue::chars(str.chars())225}226227#[builtin]228pub fn builtin_lstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {229 if str.is_empty() || chars.is_empty() {230 return Ok(str);231 }232233 let pattern = new_trim_pattern(chars)?;234 Ok(str.as_str().trim_start_matches(pattern).into())235}236237#[builtin]238pub fn builtin_rstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {239 if str.is_empty() || chars.is_empty() {240 return Ok(str);241 }242243 let pattern = new_trim_pattern(chars)?;244 Ok(str.as_str().trim_end_matches(pattern).into())245}246247#[builtin]248pub fn builtin_strip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {249 if str.is_empty() || chars.is_empty() {250 return Ok(str);251 }252253 let pattern = new_trim_pattern(chars)?;254 Ok(str.as_str().trim_matches(pattern).into())255}256257#[builtin]258pub fn builtin_trim(str: IStr) -> String {259 let filter =260 |v: char| {261 v == ' '262 || v == '\t' || v == '\n'263 || v == '\u{000c}'264 || v == '\r' || v == '\u{0085}'265 || v == '\u{00a0}'266 };267 str.as_str().trim_matches(filter).to_string()268}269270fn new_trim_pattern(chars: IndexableVal) -> Result<impl Fn(char) -> bool> {271 let chars: BTreeSet<char> = match chars {272 IndexableVal::Str(chars) => chars.chars().collect(),273 IndexableVal::Arr(chars) => chars274 .iter()275 .filter_map(|it| it.map(|it| char::from_untyped(it).ok()).transpose())276 .collect::<Result<_, _>>()?,277 };278279 Ok(move |char| chars.contains(&char))280}281282#[cfg(test)]283#[allow(clippy::float_cmp)]284mod tests {285 use super::*;286287 #[test]288 fn parse_nat_base_8() {289 assert_eq!(parse_nat::<8>("0").unwrap(), 0.);290 assert_eq!(parse_nat::<8>("5").unwrap(), 5.);291 assert_eq!(parse_nat::<8>("32").unwrap(), f64::from(0o32));292 assert_eq!(parse_nat::<8>("761").unwrap(), f64::from(0o761));293 }294295 #[test]296 fn parse_nat_base_10() {297 assert_eq!(parse_nat::<10>("0").unwrap(), 0.);298 assert_eq!(parse_nat::<10>("3").unwrap(), 3.);299 assert_eq!(parse_nat::<10>("27").unwrap(), 27.);300 assert_eq!(parse_nat::<10>("123").unwrap(), 123.);301 }302303 #[test]304 fn parse_nat_base_16() {305 assert_eq!(parse_nat::<16>("0").unwrap(), 0.);306 assert_eq!(parse_nat::<16>("A").unwrap(), 10.);307 assert_eq!(parse_nat::<16>("a9").unwrap(), f64::from(0xA9));308 assert_eq!(parse_nat::<16>("BbC").unwrap(), f64::from(0xBBC));309 }310}crates/jrsonnet-stdlib/src/types.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/types.rs
+++ b/crates/jrsonnet-stdlib/src/types.rs
@@ -29,3 +29,7 @@
pub fn builtin_is_function(v: Val) -> bool {
matches!(v, Val::Func(_))
}
+#[builtin]
+pub fn builtin_is_null(v: Val) -> bool {
+ matches!(v, Val::Null)
+}