-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
substitute_one_step: stop after first variable has been substituted #11
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -55,6 +55,7 @@ | |||||||||||||||||||||||||
#![warn(missing_docs, missing_debug_implementations)] | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
pub mod error; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
pub use error::Error; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
mod map; | ||||||||||||||||||||||||||
|
@@ -63,6 +64,24 @@ pub use map::*; | |||||||||||||||||||||||||
#[cfg(feature = "yaml")] | ||||||||||||||||||||||||||
pub mod yaml; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/// When using substitute_one_step, this reports about the | ||||||||||||||||||||||||||
/// type of substitution. | ||||||||||||||||||||||||||
#[derive(Debug, PartialEq, Eq)] | ||||||||||||||||||||||||||
pub enum SubstitutionType { | ||||||||||||||||||||||||||
/// a escape sequence was replace. E.g. "\$" | ||||||||||||||||||||||||||
UnescapeOne, | ||||||||||||||||||||||||||
/// a variable was expanded. E.g. ${VAR} | ||||||||||||||||||||||||||
Variable, | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/// result of single step substitution (one at a time) | ||||||||||||||||||||||||||
#[derive(Debug, PartialEq, Eq)] | ||||||||||||||||||||||||||
pub struct SubstituteOneStepResult { | ||||||||||||||||||||||||||
slice_before_ends: usize, | ||||||||||||||||||||||||||
slice_after_starts: usize, | ||||||||||||||||||||||||||
subst_type: SubstitutionType, | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/// Substitute variables in a string. | ||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||
/// Variables have the form `$NAME`, `${NAME}` or `${NAME:default}`. | ||||||||||||||||||||||||||
|
@@ -86,6 +105,34 @@ where | |||||||||||||||||||||||||
unsafe { Ok(String::from_utf8_unchecked(output)) } | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/// Does one sub-step of substitute | ||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||
/// Returns Some((replacement_string, next_position_in_source_str_after_variable_name)) when succeeded. | ||||||||||||||||||||||||||
/// Returns None if no substitution was possible. | ||||||||||||||||||||||||||
pub fn substitute_one_step<'a, M>( | ||||||||||||||||||||||||||
source: &str, | ||||||||||||||||||||||||||
variables: &'a M, | ||||||||||||||||||||||||||
) -> Result<Option<(String, SubstituteOneStepResult)>, Error> | ||||||||||||||||||||||||||
where | ||||||||||||||||||||||||||
M: VariableMap<'a> + ?Sized, | ||||||||||||||||||||||||||
M::Value: AsRef<str>, | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
let result_option = substitute_impl_one_step(0, source.as_bytes(), &(0..source.len()), variables, &|x| { | ||||||||||||||||||||||||||
x.as_ref().as_bytes() | ||||||||||||||||||||||||||
})?; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
if result_option.is_none() { | ||||||||||||||||||||||||||
return Ok(None); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
let result = result_option.unwrap(); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
// SAFETY: Both source and all variable values are valid UTF-8, so substitation result is also valid UTF-8. | ||||||||||||||||||||||||||
let output_str = unsafe { String::from_utf8_unchecked(result.0) }; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Ok(Some((output_str, result.1))) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/// Substitute variables in a byte string. | ||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||
/// Variables have the form `$NAME`, `${NAME}` or `${NAME:default}`. | ||||||||||||||||||||||||||
|
@@ -107,6 +154,56 @@ where | |||||||||||||||||||||||||
Ok(output) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
fn substitute_impl_one_step<'a, M, F>( | ||||||||||||||||||||||||||
finger: usize, | ||||||||||||||||||||||||||
source: &[u8], | ||||||||||||||||||||||||||
range: &std::ops::Range<usize>, | ||||||||||||||||||||||||||
variables: &'a M, | ||||||||||||||||||||||||||
to_bytes: &F, | ||||||||||||||||||||||||||
) -> Result<Option<(Vec<u8>, SubstituteOneStepResult)>, Error> | ||||||||||||||||||||||||||
where | ||||||||||||||||||||||||||
M: VariableMap<'a> + ?Sized, | ||||||||||||||||||||||||||
F: Fn(&M::Value) -> &[u8], | ||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
let next = match memchr::memchr2(b'$', b'\\', &source[finger..range.end]) { | ||||||||||||||||||||||||||
Some(x) => finger + x, | ||||||||||||||||||||||||||
None => return Ok(None), | ||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
let mut output = Vec::new(); | ||||||||||||||||||||||||||
if source[next] == b'\\' { | ||||||||||||||||||||||||||
output.push(unescape_one(source, next)?); | ||||||||||||||||||||||||||
Ok(Some((output, SubstituteOneStepResult { | ||||||||||||||||||||||||||
slice_before_ends: next, | ||||||||||||||||||||||||||
slice_after_starts: next + 2, | ||||||||||||||||||||||||||
subst_type: SubstitutionType::UnescapeOne, | ||||||||||||||||||||||||||
}))) | ||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||
let variable = parse_variable(source, next)?; | ||||||||||||||||||||||||||
let value = variables.get(variable.name); | ||||||||||||||||||||||||||
match (&value, &variable.default) { | ||||||||||||||||||||||||||
(None, None) => { | ||||||||||||||||||||||||||
return Err(error::NoSuchVariable { | ||||||||||||||||||||||||||
position: variable.name_start, | ||||||||||||||||||||||||||
name: variable.name.to_owned(), | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
.into()) | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
(Some(value), _) => { | ||||||||||||||||||||||||||
output.extend_from_slice(to_bytes(value)); | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
(None, Some(default)) => { | ||||||||||||||||||||||||||
substitute_impl(&mut output, source, default.clone(), variables, to_bytes)?; | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||
Ok(Some((output, SubstituteOneStepResult { | ||||||||||||||||||||||||||
slice_before_ends: next, | ||||||||||||||||||||||||||
slice_after_starts: variable.end_position, | ||||||||||||||||||||||||||
subst_type: SubstitutionType::Variable, | ||||||||||||||||||||||||||
}))) | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
/// Substitute variables in a byte string. | ||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||
/// This is the real implementation used by both [`substitute`] and [`substitute_bytes`]. | ||||||||||||||||||||||||||
|
@@ -124,34 +221,13 @@ where | |||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||
let mut finger = range.start; | ||||||||||||||||||||||||||
while finger < range.end { | ||||||||||||||||||||||||||
let next = match memchr::memchr2(b'$', b'\\', &source[finger..range.end]) { | ||||||||||||||||||||||||||
Some(x) => finger + x, | ||||||||||||||||||||||||||
None => break, | ||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
output.extend_from_slice(&source[finger..next]); | ||||||||||||||||||||||||||
if source[next] == b'\\' { | ||||||||||||||||||||||||||
output.push(unescape_one(source, next)?); | ||||||||||||||||||||||||||
finger = next + 2; | ||||||||||||||||||||||||||
let new_finger_option = substitute_impl_one_step(finger, source, &range, variables, to_bytes)?; | ||||||||||||||||||||||||||
if let Some((mut expanded_value, metadata)) = new_finger_option { | ||||||||||||||||||||||||||
output.extend_from_slice(source.get(range.start..metadata.slice_before_ends).unwrap()); | ||||||||||||||||||||||||||
output.append(&mut expanded_value); | ||||||||||||||||||||||||||
finger = metadata.slice_after_starts; | ||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||
let variable = parse_variable(source, next)?; | ||||||||||||||||||||||||||
let value = variables.get(variable.name); | ||||||||||||||||||||||||||
match (&value, &variable.default) { | ||||||||||||||||||||||||||
(None, None) => { | ||||||||||||||||||||||||||
return Err(error::NoSuchVariable { | ||||||||||||||||||||||||||
position: variable.name_start, | ||||||||||||||||||||||||||
name: variable.name.to_owned(), | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
.into()) | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
(Some(value), _) => { | ||||||||||||||||||||||||||
output.extend_from_slice(to_bytes(value)); | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
(None, Some(default)) => { | ||||||||||||||||||||||||||
substitute_impl(output, source, default.clone(), variables, to_bytes)?; | ||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||
finger = variable.end_position; | ||||||||||||||||||||||||||
break; | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
|
@@ -418,7 +494,10 @@ mod test { | |||||||||||||||||||||||||
fn substitution_in_default_value() { | ||||||||||||||||||||||||||
let mut map: BTreeMap<String, String> = BTreeMap::new(); | ||||||||||||||||||||||||||
map.insert("name".into(), "world".into()); | ||||||||||||||||||||||||||
check!(let Ok("Hello cruel world!") = substitute("Hello ${not_name:cruel $name}!", &map).as_deref()); | ||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||
Ok("Hello cruel world!"), | ||||||||||||||||||||||||||
substitute("Hello ${not_name:cruel $name}!", &map).as_deref() | ||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||
|
@@ -591,7 +670,7 @@ mod test { | |||||||||||||||||||||||||
let variables: &dyn VariableMap<Value = &String> = &variables; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
let_assert!(Ok(expanded) = substitute("one ${aap}", variables)); | ||||||||||||||||||||||||||
assert!(expanded == "one noot"); | ||||||||||||||||||||||||||
assert_eq!(expanded, "one noot"); | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, don't change this. |
||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||
|
@@ -607,4 +686,34 @@ mod test { | |||||||||||||||||||||||||
r" ^^^", "\n", | ||||||||||||||||||||||||||
)); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||
fn test_substitute_one_step_variable_and_escape_sequence() { | ||||||||||||||||||||||||||
let mut variables = BTreeMap::new(); | ||||||||||||||||||||||||||
variables.insert(String::from("NAME"), String::from("subst")); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
let source = r"hello $NAME. Nice\$to meet you $NAME."; | ||||||||||||||||||||||||||
let result = substitute_one_step(source, &variables).unwrap().unwrap(); | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
assert_eq!(result.0, "subst"); | ||||||||||||||||||||||||||
assert_eq!(result.1.slice_before_ends, 6); | ||||||||||||||||||||||||||
assert_eq!(result.1.slice_after_starts, 11); | ||||||||||||||||||||||||||
assert_eq!(result.1.subst_type, SubstitutionType::Variable); | ||||||||||||||||||||||||||
Comment on lines
+697
to
+700
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
let result = substitute_one_step(source.get(result.1.slice_after_starts..).unwrap(), &variables) | ||||||||||||||||||||||||||
.unwrap() | ||||||||||||||||||||||||||
.unwrap(); | ||||||||||||||||||||||||||
assert_eq!(result.0, "$"); | ||||||||||||||||||||||||||
assert_eq!(result.1.slice_before_ends, 6); | ||||||||||||||||||||||||||
assert_eq!(result.1.slice_after_starts, 8); | ||||||||||||||||||||||||||
assert_eq!(result.1.subst_type, SubstitutionType::UnescapeOne); | ||||||||||||||||||||||||||
Comment on lines
+702
to
+708
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||
fn test_substitute_one_step_no_substitution() { | ||||||||||||||||||||||||||
let variables: BTreeMap<String, String> = BTreeMap::new(); | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
let source = r"hello world"; | ||||||||||||||||||||||||||
let result = substitute_one_step(source, &variables).unwrap(); | ||||||||||||||||||||||||||
assert!(result.is_none()); | ||||||||||||||||||||||||||
Comment on lines
+716
to
+717
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't replace these. The
check!()
andassert!()
macros fromassert2
give far better error messages thanassert_eq!()
.