///|
/// Split a URL path into segments
/// "/hello/world" => ["hello", "world"]
/// "/" => []
pub fn split_path(path : String) -> Array[String] {
let paths : Array[String] = []
let parts = path.split("/")
for part in parts {
let s = part.to_owned()
if s != "" {
paths.push(s)
}
}
paths
}
///|
/// Split a routing path with group marker preservation
/// This handles regex patterns in braces like :id{[0-9]+}
pub fn split_routing_path(route_path : String) -> Array[String] {
let (groups, processed_path) = extract_groups_from_path(route_path)
let paths = split_path(processed_path)
replace_group_marks(paths, groups)
}
///|
/// Extract regex groups from path and replace them with markers
/// :id{[0-9]+} becomes :id@N where N is the index
fn extract_groups_from_path(path : String) -> (Array[(String, String)], String) {
let groups : Array[(String, String)] = []
let result = StringBuilder::new()
let mut i = 0
let len = path.length()
while i < len {
let c = path[i]
if c == '{'.to_int().to_uint16() {
let start = i
let mut depth = 1
i += 1
while i < len && depth > 0 {
let c2 = path[i]
if c2 == '{'.to_int().to_uint16() {
depth += 1
} else if c2 == '}'.to_int().to_uint16() {
depth -= 1
}
i += 1
}
let group_content = path.view(start_offset=start, end_offset=i).to_owned()
let mark = "@\{start}"
groups.push((mark, group_content))
result.write_string(mark)
} else {
result.write_char(c.to_int().unsafe_to_char())
i += 1
}
}
(groups, result.to_string())
}
///|
/// Replace group markers back with their original content
fn replace_group_marks(
paths : Array[String],
groups : Array[(String, String)],
) -> Array[String] {
let result : Array[String] = []
for path in paths {
let mut current = path
for j = groups.length() - 1; j >= 0; j = j - 1 {
let (mark, content) = groups[j]
current = current.replace(old=mark, new=content)
}
result.push(current)
}
result
}
///|
/// Pattern types for URL matching
pub(all) enum Pattern {
/// Wildcard pattern (*)
Wildcard
/// Parameter pattern (:name)
Param(String)
/// Parameter with regex pattern (:name{regex})
ParamWithRegex(String, String)
}
///|
pub impl Show for Pattern with fn output(self, logger) {
match self {
Wildcard => logger.write_string("Wildcard")
Param(name) => {
logger.write_string("Param(")
logger.write_string(name)
logger.write_string(")")
}
ParamWithRegex(name, regex) => {
logger.write_string("ParamWithRegex(")
logger.write_string(name)
logger.write_string(", ")
logger.write_string(regex)
logger.write_string(")")
}
}
}
///|
pub impl Eq for Pattern with fn equal(self, other) {
match (self, other) {
(Wildcard, Wildcard) => true
(Param(a), Param(b)) => a == b
(ParamWithRegex(a1, a2), ParamWithRegex(b1, b2)) => a1 == b1 && a2 == b2
_ => false
}
}
///|
/// Find index of substring in string
fn index_of(s : String, needle : String) -> Int {
let s_len = s.length()
let n_len = needle.length()
if n_len == 0 {
return 0
}
if n_len > s_len {
return -1
}
for i = 0; i <= s_len - n_len; i = i + 1 {
let mut found = true
for j = 0; j < n_len; j = j + 1 {
if s[i + j] != needle[j] {
found = false
break
}
}
if found {
return i
}
}
-1
}
///|
/// Get pattern from a path segment
/// Returns None for static segments
/// "*" => Wildcard
/// ":id" => Param("id")
/// ":id{[0-9]+}" => ParamWithRegex("id", "[0-9]+")
pub fn get_pattern(label : String) -> Pattern? {
if label == "*" {
return Some(Wildcard)
}
if label.length() > 0 && label[0] == ':'.to_int().to_uint16() {
let rest = label.view(start_offset=1).to_owned()
// Check for regex pattern in braces
let brace_start = index_of(rest, "{")
if brace_start >= 0 {
let name = rest.view(end_offset=brace_start).to_owned()
// Extract regex (without braces)
let regex_start = brace_start + 1
let regex_end = rest.length() - 1 // Remove closing brace
if regex_end > regex_start {
let regex = rest
.view(start_offset=regex_start, end_offset=regex_end)
.to_owned()
return Some(ParamWithRegex(name, regex))
}
}
return Some(Param(rest))
}
None
}
///|
/// Check for optional parameters in path
/// "/api/animals/:type?" => ["/api/animals", "/api/animals/:type"]
/// Returns None if path doesn't end with optional parameter
pub fn check_optional_parameter(path : String) -> Array[String]? {
let len = path.length()
if len == 0 ||
path[len - 1] != '?'.to_int().to_uint16() ||
!path.contains(":") {
return None
}
let segments = path.split("/")
let results : Array[String] = []
let base_path = StringBuilder::new()
for segment in segments {
let seg = segment.to_owned()
if seg == "" {
continue
}
if !seg.contains(":") {
base_path.write_char('/')
base_path.write_string(seg)
} else if seg.contains(":") {
if seg.contains("?") {
// Optional parameter found
let current_base = base_path.to_string()
if results.is_empty() && current_base == "" {
results.push("/")
} else {
results.push(current_base)
}
// Remove trailing ? from segment
let optional_segment = seg.view(end_offset=seg.length() - 1).to_owned()
base_path.write_char('/')
base_path.write_string(optional_segment)
results.push(base_path.to_string())
} else {
base_path.write_char('/')
base_path.write_string(seg)
}
}
}
// Remove duplicates while preserving order
let unique : Array[String] = []
for r in results {
if !unique.contains(r) {
unique.push(r)
}
}
if unique.is_empty() {
None
} else {
Some(unique)
}
}