Skip to content

Commit fab840b

Browse files
committed
Initial support for format() function
1 parent 8dc0655 commit fab840b

File tree

6 files changed

+260
-22
lines changed

6 files changed

+260
-22
lines changed

dsc/Cargo.lock

Lines changed: 56 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dsc_lib/Cargo.lock

Lines changed: 21 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dsc_lib/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ chrono = "0.4"
1616
clap = { version = "4.5", features = ["derive"] }
1717
derive_builder ="0.20"
1818
indicatif = "0.17"
19-
jsonschema = { version = "0.29", default-features = false }
19+
jsonschema = { version = "0.30", default-features = false }
2020
linked-hash-map = "0.5"
2121
num-traits = "0.2"
2222
regex = "1.11"
23+
rt-format = "0.3"
2324
rust-i18n = { version = "3.1" }
2425
# reqwest = { version = "0.12.8", features = ["native-tls"], default-features = false }
2526
schemars = { version = "0.8", features = ["preserve_order"] }
@@ -29,13 +30,13 @@ serde_yaml = { version = "0.9" }
2930
thiserror = "2.0"
3031
security_context_lib = { path = "../security_context_lib" }
3132
semver = "1.0"
32-
tokio = { version = "1.43", features = ["full"] }
33+
tokio = { version = "1.44", features = ["full"] }
3334
tracing = "0.1"
3435
tracing-indicatif = { version = "0.3" }
3536
tree-sitter = "0.25"
3637
tree-sitter-rust = "0.24"
3738
tree-sitter-dscexpression = { path = "../tree-sitter-dscexpression" }
38-
uuid = { version = "1.15", features = ["v4"] }
39+
uuid = { version = "1.16", features = ["v4"] }
3940
which = "7.0"
4041

4142
[dev-dependencies]

dsc_lib/src/functions/format.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::DscError;
5+
use crate::configure::context::Context;
6+
use crate::functions::{AcceptedArgKind, Function};
7+
use rt_format::{Format, FormatArgument, ParsedFormat, Specifier};
8+
use rust_i18n::t;
9+
use serde_json::Value;
10+
use std::fmt::{Error, Formatter, Write};
11+
12+
impl FormatArgument for Value {
13+
fn supports_format(&self, spec: &Specifier) -> bool {
14+
match self {
15+
Value::Boolean(_) | Value::String(_) | Value::Number(_) => true,
16+
_ => false,
17+
}
18+
}
19+
20+
fn fmt_display(&self, f: &mut Formatter) -> std::fmt::Result {
21+
match self {
22+
Value::Boolean(b) => write!(f, "{}", b),
23+
Value::String(s) => write!(f, "{}", s),
24+
Value::Number(n) => write!(f, "{}", n),
25+
_ => Err(fmt::Error),
26+
}
27+
}
28+
29+
fn fmt_lower_hex(&self, f: &mut Formatter) -> std::fmt::Result {
30+
match self {
31+
Value::Number(n) => write!(f, "{:x}", n.as_i64().unwrap_or_default()),
32+
_ => Err(fmt::Error),
33+
}
34+
}
35+
36+
fn fmt_upper_hex(&self, f: &mut Formatter) -> std::fmt::Result {
37+
match self {
38+
Value::Number(n) => write!(f, "{:X}", n.as_i64().unwrap_or_default()),
39+
_ => Err(fmt::Error),
40+
}
41+
}
42+
43+
fn fmt_binary(&self, f: &mut Formatter) -> std::fmt::Result {
44+
match self {
45+
Value::Number(n) => write!(f, "{:b}", n.as_i64().unwrap_or_default()),
46+
_ => Err(fmt::Error),
47+
}
48+
}
49+
50+
fn fmt_octal(&self, f: &mut Formatter) -> std::fmt::Result {
51+
match self {
52+
Value::Number(n) => write!(f, "{:o}", n.as_i64().unwrap_or_default()),
53+
_ => Err(fmt::Error),
54+
}
55+
}
56+
}
57+
58+
#[derive(Debug, Default)]
59+
pub struct Format {}
60+
61+
impl Function for Format {
62+
fn min_args(&self) -> usize {
63+
2
64+
}
65+
66+
fn max_args(&self) -> usize {
67+
usize::MAX
68+
}
69+
70+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
71+
vec![AcceptedArgKind::Boolean, AcceptedArgKind::String, AcceptedArgKind::Number]
72+
}
73+
74+
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
75+
let mut string_result = String::new();
76+
let Ok(format_string) = args[0].as_str() else {
77+
return Err(DscError::Parser("First `format()` argument must be a string".to_string()));
78+
};
79+
for value in &args[1..] {
80+
if let Some(parsed_format) = ParsedFormat::parse(format_string) {
81+
let mut formatted_string = String::new();
82+
for specifier in parsed_format.specifiers() {
83+
if let Some(arg) = args.get(specifier.index()) {
84+
formatted_string.push_str(&arg.to_string());
85+
} else {
86+
return Err(DscError::Parser("Invalid format specifier".to_string()));
87+
}
88+
}
89+
string_result.push_str(&formatted_string);
90+
} else {
91+
return Err(DscError::Parser("Invalid format string".to_string()));
92+
}
93+
}
94+
Ok(Value::String(string_result))
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use crate::configure::context::Context;
101+
use crate::parser::Statement;
102+
103+
#[test]
104+
fn position() {
105+
let mut parser = Statement::new().unwrap();
106+
let result = parser.parse_and_execute("[format('{0} - {1}', 'hello', 2)]", &Context::new()).unwrap();
107+
assert_eq!(result, "hello - 2");
108+
}
109+
110+
#[test]
111+
fn strings_with_spaces() {
112+
let mut parser = Statement::new().unwrap();
113+
let result = parser.parse_and_execute("[concat('a ', ' ', ' b')]", &Context::new()).unwrap();
114+
assert_eq!(result, "a b");
115+
}
116+
117+
#[test]
118+
fn arrays() {
119+
let mut parser = Statement::new().unwrap();
120+
let result = parser.parse_and_execute("[concat(createArray('a','b'), createArray('c','d'))]", &Context::new()).unwrap();
121+
assert_eq!(result.to_string(), r#"["a","b","c","d"]"#);
122+
}
123+
124+
#[test]
125+
fn string_and_numbers() {
126+
let mut parser = Statement::new().unwrap();
127+
let result = parser.parse_and_execute("[concat('a', 1)]", &Context::new());
128+
assert!(result.is_err());
129+
}
130+
131+
#[test]
132+
fn nested() {
133+
let mut parser = Statement::new().unwrap();
134+
let result = parser.parse_and_execute("[concat('a', concat('b', 'c'), 'd')]", &Context::new()).unwrap();
135+
assert_eq!(result, "abcd");
136+
}
137+
138+
#[test]
139+
fn invalid_one_parameter() {
140+
let mut parser = Statement::new().unwrap();
141+
let result = parser.parse_and_execute("[concat('a')]", &Context::new());
142+
assert!(result.is_err());
143+
}
144+
145+
#[test]
146+
fn string_and_array() {
147+
let mut parser = Statement::new().unwrap();
148+
let result = parser.parse_and_execute("[concat('a', createArray('b','c'))]", &Context::new());
149+
assert!(result.is_err());
150+
}
151+
152+
#[test]
153+
fn array_and_string() {
154+
let mut parser = Statement::new().unwrap();
155+
let result = parser.parse_and_execute("[concat(createArray('a','b'), 'c')]", &Context::new());
156+
assert!(result.is_err());
157+
}
158+
}

dsc_lib/src/functions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod div;
1616
pub mod envvar;
1717
pub mod equals;
1818
pub mod r#if;
19+
pub mod format;
1920
pub mod int;
2021
pub mod max;
2122
pub mod min;
@@ -77,6 +78,7 @@ impl FunctionDispatcher {
7778
functions.insert("envvar".to_string(), Box::new(envvar::Envvar{}));
7879
functions.insert("equals".to_string(), Box::new(equals::Equals{}));
7980
functions.insert("if".to_string(), Box::new(r#if::If{}));
81+
functions.insert("format".to_string(), Box::new(format::Format{}));
8082
functions.insert("int".to_string(), Box::new(int::Int{}));
8183
functions.insert("max".to_string(), Box::new(max::Max{}));
8284
functions.insert("min".to_string(), Box::new(min::Min{}));

0 commit comments

Comments
 (0)