///|
/// Token in a topic pattern.
pub(all) enum TopicToken {
ExactSegment(String)
OneSegment
TailSegments
} derive(Eq, @debug.Debug)
///|
/// Parsed topic pattern. Higher specificity wins when routes are sorted.
pub(all) struct TopicPattern {
raw : String
tokens : Array[TopicToken]
specificity : Int
} derive(Eq, @debug.Debug)
///|
pub(all) struct MatchReport {
pattern : String
topic : String
matched : Bool
specificity : Int
topic_segments : Int
} derive(Eq, @debug.Debug)
///|
pub fn topic_pattern(
pattern : StringView,
) -> Result[TopicPattern, EventRailError] {
let raw = pattern.to_owned()
guard raw != "" else { return Err(EmptyPattern) }
let segments = raw.split(".").to_array()
let tokens : Array[TopicToken] = Array::new(capacity=segments.length())
let mut specificity = 0
for idx, seg_view in segments {
let seg = seg_view.to_owned()
guard seg != "" else { return Err(EmptySegment(raw)) }
if seg == "*" {
tokens.push(OneSegment)
specificity += 1
} else if seg == "**" {
guard idx == segments.length() - 1 else {
return Err(InvalidPattern(raw))
}
tokens.push(TailSegments)
} else if seg.contains("*") {
return Err(InvalidPattern(raw))
} else {
tokens.push(ExactSegment(seg))
specificity += 4
}
}
Ok({ raw, tokens, specificity })
}
///|
pub fn topic_segments(
topic : StringView,
) -> Result[Array[String], EventRailError] {
let raw = topic.to_owned()
guard raw != "" else { return Err(EmptyTopic) }
guard !raw.contains("*") else { return Err(InvalidPattern(raw)) }
let parts = raw.split(".").to_array()
let out : Array[String] = Array::new(capacity=parts.length())
for part in parts {
let owned = part.to_owned()
guard owned != "" else { return Err(EmptySegment(raw)) }
out.push(owned)
}
Ok(out)
}
///|
pub fn TopicPattern::matches_topic(
self : TopicPattern,
topic : StringView,
) -> Result[Bool, EventRailError] {
match topic_segments(topic) {
Err(err) => Err(err)
Ok(segments) => Ok(match_token_at(self.tokens, segments, 0, 0))
}
}
///|
pub fn TopicPattern::report(
self : TopicPattern,
topic : StringView,
) -> Result[MatchReport, EventRailError] {
match topic_segments(topic) {
Err(err) => Err(err)
Ok(segments) =>
Ok({
pattern: self.raw,
topic: topic.to_owned(),
matched: match_token_at(self.tokens, segments, 0, 0),
specificity: self.specificity,
topic_segments: segments.length(),
})
}
}
///|
pub fn TopicPattern::is_catch_all(self : TopicPattern) -> Bool {
self.tokens.length() == 1 && self.tokens[0] is TailSegments
}
///|
pub fn TopicPattern::prefix(self : TopicPattern) -> String {
let parts : Array[String] = []
for token in self.tokens {
match token {
ExactSegment(value) => parts.push(value)
OneSegment | TailSegments => break
}
}
parts.join(".")
}
///|
fn match_token_at(
tokens : Array[TopicToken],
segments : Array[String],
token_idx : Int,
segment_idx : Int,
) -> Bool {
if token_idx == tokens.length() {
return segment_idx == segments.length()
}
match tokens[token_idx] {
ExactSegment(expected) =>
segment_idx < segments.length() &&
segments[segment_idx] == expected &&
match_token_at(tokens, segments, token_idx + 1, segment_idx + 1)
OneSegment =>
segment_idx < segments.length() &&
match_token_at(tokens, segments, token_idx + 1, segment_idx + 1)
TailSegments => true
}
}
///|
pub fn compare_pattern_priority(
left : TopicPattern,
right : TopicPattern,
) -> Int {
let score = right.specificity - left.specificity
if score != 0 {
score
} else {
left.raw.compare(right.raw)
}
}