Skip to content

Commit b1f7f2a

Browse files
committed
feat(rad): new RadonString operators
1 parent 241c2ef commit b1f7f2a

5 files changed

Lines changed: 233 additions & 87 deletions

File tree

Cargo.lock

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

rad/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ tokio = "1.44.1"
1414

1515
[dependencies]
1616
anyhow = "1.0.98"
17+
base64 = "0.22.1"
1718
cbor-codec = { git = "https://github.com/witnet/cbor-codec.git", branch = "feat/ldexpf-shim" }
1819
futures = "0.3.31"
1920
hex = "0.4.1"
@@ -26,10 +27,12 @@ minidom = { git = "https://github.com/witnet/xmpp-rs", rev = "bc8a33ff5da95ee403
2627
num_enum = "0.7.3"
2728
ordered-float = "3.9.2"
2829
rand = "0.7.3"
30+
regex = "1.4.2"
2931
reqwest = { version = "0.12.15", features = ["socks"] }
3032
serde = "1.0.111"
3133
serde_cbor = "0.11.2"
3234
serde_json = "1.0.96"
35+
slicestring = "0.3.2"
3336
thiserror = "2.0.12"
3437
# the url crate is used to perform additional validations before passing arguments to the surf http client
3538
# the version of url must be kept in sync with the version used by surf in the `witnet_net` crate

rad/src/operators/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ pub enum RadonOpCodes {
100100
///////////////////////////////////////////////////////////////////////
101101
// String operator codes (start at 0x70)
102102
StringAsBoolean = 0x70,
103-
// StringAsBytes = 0x71,
103+
StringAsBytes = 0x71,
104104
StringAsFloat = 0x72,
105105
StringAsInteger = 0x73,
106106
StringLength = 0x74,
@@ -110,6 +110,9 @@ pub enum RadonOpCodes {
110110
StringParseXMLMap = 0x78,
111111
StringToLowerCase = 0x79,
112112
StringToUpperCase = 0x7A,
113+
StringReplace = 0x7B,
114+
StringSlice = 0x7C,
115+
StringSplit = 0x7D,
113116
}
114117

115118
impl fmt::Display for RadonOpCodes {

rad/src/operators/string.rs

Lines changed: 191 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,128 @@ use std::{
44
str::FromStr,
55
};
66

7+
use base64::Engine;
8+
use regex::Regex;
79
use serde_cbor::value::{Value, from_value};
810
use serde_json::Value as JsonValue;
11+
use slicestring::Slice;
912

1013
use crate::{
1114
error::RadError,
1215
hash_functions::{self, RadonHashFunctions},
1316
types::{
14-
RadonType, RadonTypes, array::RadonArray, boolean::RadonBoolean, bytes::RadonBytes,
15-
float::RadonFloat, integer::RadonInteger, map::RadonMap, string::RadonString,
17+
RadonType, RadonTypes,
18+
array::RadonArray,
19+
boolean::RadonBoolean,
20+
bytes::{RadonBytes, RadonBytesEncoding},
21+
float::RadonFloat,
22+
integer::RadonInteger,
23+
map::RadonMap,
24+
string::RadonString,
1625
},
1726
};
1827

1928
const MAX_DEPTH: u8 = 20;
2029
const DEFAULT_THOUSANDS_SEPARATOR: &str = ",";
2130
const DEFAULT_DECIMAL_SEPARATOR: &str = ".";
2231

32+
pub fn as_bool(input: &RadonString) -> Result<RadonBoolean, RadError> {
33+
let str_value = radon_trim(input);
34+
bool::from_str(&str_value)
35+
.map(RadonBoolean::from)
36+
.map_err(Into::into)
37+
}
38+
39+
pub fn as_bytes(input: &RadonString, args: &Option<Vec<Value>>) -> Result<RadonBytes, RadError> {
40+
let wrong_args = || RadError::WrongArguments {
41+
input_type: RadonString::radon_type_name(),
42+
operator: "AsBytes".to_string(),
43+
args: args.to_owned().unwrap_or(Vec::<Value>::default()).to_vec(),
44+
};
45+
let mut input_string = input.value();
46+
if input_string.starts_with("0x") {
47+
input_string = input_string.slice(2..);
48+
}
49+
if input_string.len() % 2 != 0 {
50+
input_string.insert(0, '0');
51+
}
52+
let mut bytes_encoding = RadonBytesEncoding::Hex;
53+
match args {
54+
Some(args) => {
55+
if args.len() > 0 {
56+
let arg = args.first().ok_or_else(wrong_args)?.to_owned();
57+
let bytes_encoding_u8 = from_value::<u8>(arg).map_err(|_| wrong_args())?;
58+
bytes_encoding =
59+
RadonBytesEncoding::try_from(bytes_encoding_u8).map_err(|_| wrong_args())?;
60+
}
61+
}
62+
_ => (),
63+
}
64+
match bytes_encoding {
65+
RadonBytesEncoding::Hex => Ok(RadonBytes::from(
66+
hex::decode(input_string.as_str()).map_err(|_err| RadError::Decode {
67+
from: "RadonString",
68+
to: "RadonBytes",
69+
})?,
70+
)),
71+
RadonBytesEncoding::Base64 => Ok(RadonBytes::from(
72+
base64::engine::general_purpose::STANDARD
73+
.decode(input.value())
74+
.map_err(|_err| RadError::Decode {
75+
from: "RadonString",
76+
to: "RadonBytes",
77+
})?,
78+
)),
79+
RadonBytesEncoding::Utf8 => Ok(RadonBytes::from(input.value().as_bytes().to_vec())),
80+
}
81+
}
82+
83+
/// Converts a `RadonString` into a `RadonFloat`, provided that the input string actually represents
84+
/// a valid floating point number.
85+
pub fn as_float(input: &RadonString, args: &Option<Vec<Value>>) -> Result<RadonFloat, RadError> {
86+
f64::from_str(&to_numeric_string(
87+
input,
88+
args.as_deref().unwrap_or_default(),
89+
))
90+
.map(RadonFloat::from)
91+
.map_err(Into::into)
92+
}
93+
94+
/// Converts a `RadonString` into a `RadonFloat`, provided that the input string actually represents
95+
/// a valid integer number.
96+
pub fn as_integer(
97+
input: &RadonString,
98+
args: &Option<Vec<Value>>,
99+
) -> Result<RadonInteger, RadError> {
100+
i128::from_str(&to_numeric_string(
101+
input,
102+
args.as_deref().unwrap_or_default(),
103+
))
104+
.map(RadonInteger::from)
105+
.map_err(Into::into)
106+
}
107+
108+
pub fn hash(input: &RadonString, args: &[Value]) -> Result<RadonString, RadError> {
109+
let wrong_args = || RadError::WrongArguments {
110+
input_type: RadonString::radon_type_name(),
111+
operator: "Hash".to_string(),
112+
args: args.to_vec(),
113+
};
114+
115+
let input_string = input.value();
116+
let input_bytes = input_string.as_bytes();
117+
118+
let arg = args.first().ok_or_else(wrong_args)?.to_owned();
119+
let hash_function_integer = from_value::<u8>(arg).map_err(|_| wrong_args())?;
120+
let hash_function_code =
121+
RadonHashFunctions::try_from(hash_function_integer).map_err(|_| wrong_args())?;
122+
123+
let digest = hash_functions::hash(input_bytes, hash_function_code)?;
124+
let hex_string = hex::encode(digest);
125+
126+
Ok(RadonString::from(hex_string))
127+
}
128+
23129
/// Parse `RadonTypes` from a JSON-encoded `RadonString`.
24130
pub fn parse_json(input: &RadonString) -> Result<RadonTypes, RadError> {
25131
let json_value: JsonValue =
@@ -142,6 +248,10 @@ pub fn parse_xml_map(input: &RadonString) -> Result<RadonMap, RadError> {
142248
}
143249
}
144250

251+
pub fn length(input: &RadonString) -> RadonInteger {
252+
RadonInteger::from(input.value().len() as i128)
253+
}
254+
145255
pub fn radon_trim(input: &RadonString) -> String {
146256
if input.value().ends_with('\n') {
147257
input.value()[..input.value().len() - 1].to_string()
@@ -150,84 +260,10 @@ pub fn radon_trim(input: &RadonString) -> String {
150260
}
151261
}
152262

153-
pub fn to_bool(input: &RadonString) -> Result<RadonBoolean, RadError> {
154-
let str_value = radon_trim(input);
155-
bool::from_str(&str_value)
156-
.map(RadonBoolean::from)
157-
.map_err(Into::into)
158-
}
159-
160-
/// Converts a `RadonString` into a `RadonFloat`, provided that the input string actually represents
161-
/// a valid floating point number.
162-
pub fn as_float(input: &RadonString, args: &Option<Vec<Value>>) -> Result<RadonFloat, RadError> {
163-
f64::from_str(&as_numeric_string(
164-
input,
165-
args.as_deref().unwrap_or_default(),
166-
))
167-
.map(RadonFloat::from)
168-
.map_err(Into::into)
169-
}
170-
171-
/// Converts a `RadonString` into a `RadonFloat`, provided that the input string actually represents
172-
/// a valid integer number.
173-
pub fn as_integer(
174-
input: &RadonString,
175-
args: &Option<Vec<Value>>,
176-
) -> Result<RadonInteger, RadError> {
177-
i128::from_str(&as_numeric_string(
178-
input,
179-
args.as_deref().unwrap_or_default(),
180-
))
181-
.map(RadonInteger::from)
182-
.map_err(Into::into)
183-
}
184-
185-
/// Converts a `RadonString` into a `String` containing a numeric value, provided that the input
186-
/// string actually represents a valid number.
187-
pub fn as_numeric_string(input: &RadonString, args: &[Value]) -> String {
188-
let str_value = radon_trim(input);
189-
let (thousands_separator, decimal_separator) = read_separators_from_args(args);
190-
191-
replace_separators(str_value, thousands_separator, decimal_separator)
192-
}
193-
194-
pub fn length(input: &RadonString) -> RadonInteger {
195-
RadonInteger::from(input.value().len() as i128)
196-
}
197-
198-
pub fn to_lowercase(input: &RadonString) -> RadonString {
199-
RadonString::from(input.value().as_str().to_lowercase())
200-
}
201-
202-
pub fn to_uppercase(input: &RadonString) -> RadonString {
203-
RadonString::from(input.value().as_str().to_uppercase())
204-
}
205-
206-
pub fn hash(input: &RadonString, args: &[Value]) -> Result<RadonString, RadError> {
207-
let wrong_args = || RadError::WrongArguments {
208-
input_type: RadonString::radon_type_name(),
209-
operator: "Hash".to_string(),
210-
args: args.to_vec(),
211-
};
212-
213-
let input_string = input.value();
214-
let input_bytes = input_string.as_bytes();
215-
216-
let arg = args.first().ok_or_else(wrong_args)?.to_owned();
217-
let hash_function_integer = from_value::<u8>(arg).map_err(|_| wrong_args())?;
218-
let hash_function_code =
219-
RadonHashFunctions::try_from(hash_function_integer).map_err(|_| wrong_args())?;
220-
221-
let digest = hash_functions::hash(input_bytes, hash_function_code)?;
222-
let hex_string = hex::encode(digest);
223-
224-
Ok(RadonString::from(hex_string))
225-
}
226-
227263
pub fn string_match(input: &RadonString, args: &[Value]) -> Result<RadonTypes, RadError> {
228264
let wrong_args = || RadError::WrongArguments {
229265
input_type: RadonString::radon_type_name(),
230-
operator: "String match".to_string(),
266+
operator: "StringMatch".to_string(),
231267
args: args.to_vec(),
232268
};
233269

@@ -281,6 +317,66 @@ pub fn string_match(input: &RadonString, args: &[Value]) -> Result<RadonTypes, R
281317
.unwrap_or(Ok(temp_def))
282318
}
283319

320+
pub fn string_replace(input: &RadonString, args: &[Value]) -> Result<RadonString, RadError> {
321+
let wrong_args = || RadError::WrongArguments {
322+
input_type: RadonString::radon_type_name(),
323+
operator: "StringReplace".to_string(),
324+
args: args.to_vec(),
325+
};
326+
let regex = RadonString::try_from(args.first().ok_or_else(wrong_args)?.to_owned())?;
327+
let replacement = RadonString::try_from(args.get(1).ok_or_else(wrong_args)?.to_owned())?;
328+
Ok(RadonString::from(input.value().as_str().replace(
329+
regex.value().as_str(),
330+
replacement.value().as_str(),
331+
)))
332+
}
333+
334+
pub fn string_slice(input: &RadonString, args: &[Value]) -> Result<RadonString, RadError> {
335+
let wrong_args = || RadError::WrongArguments {
336+
input_type: RadonString::radon_type_name(),
337+
operator: "StringSlice".to_string(),
338+
args: args.to_vec(),
339+
};
340+
let mut end_index: usize = input.value().len();
341+
match args.len() {
342+
2 => {
343+
let start_index = from_value::<i64>(args[0].clone())
344+
.unwrap_or_default()
345+
.rem_euclid(end_index as i64) as usize;
346+
end_index = from_value::<i64>(args[1].clone())
347+
.unwrap_or_default()
348+
.rem_euclid(end_index as i64) as usize;
349+
Ok(RadonString::from(
350+
input.value().as_str().slice(start_index..end_index),
351+
))
352+
}
353+
1 => {
354+
let start_index = from_value::<i64>(args[0].clone())
355+
.unwrap_or_default()
356+
.rem_euclid(end_index as i64) as usize;
357+
Ok(RadonString::from(
358+
input.value().as_str().slice(start_index..end_index),
359+
))
360+
}
361+
_ => Err(wrong_args()),
362+
}
363+
}
364+
365+
pub fn string_split(input: &RadonString, args: &[Value]) -> Result<RadonArray, RadError> {
366+
let wrong_args = || RadError::WrongArguments {
367+
input_type: RadonString::radon_type_name(),
368+
operator: "StringSplit".to_string(),
369+
args: args.to_vec(),
370+
};
371+
let pattern = RadonString::try_from(args.first().ok_or_else(wrong_args)?.to_owned())?;
372+
let parts: Vec<RadonTypes> = Regex::new(pattern.value().as_str())
373+
.unwrap()
374+
.split(input.value().as_str())
375+
.map(|part| RadonTypes::from(RadonString::from(part)))
376+
.collect();
377+
Ok(RadonArray::from(parts))
378+
}
379+
284380
/// Replace thousands and decimals separators in a `String`.
285381
#[inline]
286382
pub fn replace_separators(
@@ -321,6 +417,23 @@ fn default_decimal_separator<T>(_: T) -> String {
321417
String::from(DEFAULT_DECIMAL_SEPARATOR)
322418
}
323419

420+
pub fn to_lowercase(input: &RadonString) -> RadonString {
421+
RadonString::from(input.value().as_str().to_lowercase())
422+
}
423+
424+
/// Converts a `RadonString` into a `String` containing a numeric value, provided that the input
425+
/// string actually represents a valid number.
426+
pub fn to_numeric_string(input: &RadonString, args: &[Value]) -> String {
427+
let str_value = radon_trim(input);
428+
let (thousands_separator, decimal_separator) = read_separators_from_args(args);
429+
430+
replace_separators(str_value, thousands_separator, decimal_separator)
431+
}
432+
433+
pub fn to_uppercase(input: &RadonString) -> RadonString {
434+
RadonString::from(input.value().as_str().to_uppercase())
435+
}
436+
324437
/// This module was introduced for encapsulating the interim legacy logic before WIP-0024 is
325438
/// introduced, for the sake of maintainability.
326439
///
@@ -731,7 +844,7 @@ mod tests {
731844
let rad_float = RadonBoolean::from(false);
732845
let rad_string: RadonString = RadonString::from("false");
733846

734-
assert_eq!(to_bool(&rad_string).unwrap(), rad_float);
847+
assert_eq!(as_bool(&rad_string).unwrap(), rad_float);
735848
}
736849

737850
#[test]

0 commit comments

Comments
 (0)