implement random url generator

This commit is contained in:
2025-09-01 19:46:17 +09:00
parent f441abd5eb
commit 1b661f9848
8 changed files with 214 additions and 2 deletions

View File

@@ -9,4 +9,7 @@ tower = { workspace = true }
reqwest = {workspace = true}
url = {workspace = true}
rand = {workspace = true}
bytes = {workspace = true}
thiserror = {workspace = true}
compact_str = {workspace = true}
bytes = {workspace = true}
regex = "1.11"

View File

@@ -0,0 +1,4 @@
pub mod simple;
pub mod random;
pub use simple::{SimpleHttpRequest, SimpleHttpSward};

View File

@@ -0,0 +1,107 @@
use compact_str::CompactString;
use rand::seq::IteratorRandom;
use regex::Regex;
#[derive(Clone)]
pub struct RandomUrlGenerator<Rng: rand::Rng> {
random_engine: Rng,
random_parts: Box<[RegexPart]>,
}
impl<Rng: rand::Rng> RandomUrlGenerator<Rng> {
pub fn new(random_engine: Rng, template: &str) -> Result<Self, RandomUrlGeneratorBuildError> {
let re = Regex::new(r"\[([^\]]+)\](?:\{(\d+)\})?")?;
let mut parts: Vec<RegexPart> = Vec::new();
let mut last_end = 0;
for caps in re.captures_iter(template) {
let m = caps
.get(0)
.ok_or(RandomUrlGeneratorBuildError::SyntaxError)?;
if m.start() > last_end {
parts.push(RegexPart::Literal(template[last_end..m.start()].into()));
}
let expr = caps
.get(1)
.ok_or(RandomUrlGeneratorBuildError::SyntaxError)?
.as_str();
let count = caps
.get(2)
.map(|m| {
m.as_str()
.parse::<usize>()
.map_err(|_| RandomUrlGeneratorBuildError::SyntaxError)
})
.transpose()?
.unwrap_or(1);
let mut chars = Vec::new();
let mut chars_iter = expr.chars().peekable();
while let Some(c) = chars_iter.next() {
if let Some(&dash) = chars_iter.peek()
&& dash == '-'
{
chars_iter.next();
if let Some(end) = chars_iter.next() {
for ch in c..=end {
chars.push(ch);
}
continue;
}
}
chars.push(c);
}
let chars = chars.into_boxed_slice();
parts.push(RegexPart::RandomChars { chars, count });
last_end = m.end();
}
if last_end < template.len() {
parts.push(RegexPart::Literal(template[last_end..].into()));
}
Ok(Self {
random_engine,
random_parts: parts.into_boxed_slice(),
})
}
fn expected_length(&self) -> usize {
self.random_parts.iter().fold(0, |acc, part| match part {
RegexPart::Literal(s) => acc + s.len(),
RegexPart::RandomChars { count, .. } => acc + *count,
})
}
pub fn generate(&mut self) -> CompactString {
self.random_parts.iter().fold(
CompactString::with_capacity(self.expected_length()),
|mut acc, part| {
match &part {
RegexPart::Literal(s) => {
acc.push_str(s);
}
RegexPart::RandomChars { chars, count } => {
let next = (0..*count)
.filter_map(|_| chars.iter().choose(&mut self.random_engine))
.collect::<CompactString>();
acc.push_str(&next);
}
};
acc
},
)
}
}
#[derive(Debug, thiserror::Error)]
pub enum RandomUrlGeneratorBuildError {
#[error("Invalid regex: {0}")]
InvalidRegex(#[from] regex::Error),
#[error("Syntax error in template.")]
SyntaxError,
}
#[derive(Clone)]
enum RegexPart {
Literal(CompactString),
RandomChars { chars: Box<[char]>, count: usize },
}

View File

@@ -7,6 +7,7 @@ use tower::Service;
use url::Url;
#[derive(Clone)]
/// A simple http sward that sends one request per call.
pub struct SimpleHttpSward {
client: Client,
method: Method,

View File

@@ -1 +1,6 @@
pub mod http;
#![deny(clippy::unwrap_used)]
#![deny(clippy::panic)]
#![deny(clippy::expect_used)]
pub mod http;
pub mod utils;

View File