|
@@ -4,13 +4,17 @@ using Invercargill.DataStructures;
|
|
|
namespace Astralis {
|
|
namespace Astralis {
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// HTTP router that maps request paths to handlers with support for named segments.
|
|
|
|
|
|
|
+ /// HTTP router that maps request paths to handlers with support for named segments and wildcards.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
/// <example>
|
|
/// <example>
|
|
|
/// var router = new Router();
|
|
/// var router = new Router();
|
|
|
/// router.map("/users", users_handler);
|
|
/// router.map("/users", users_handler);
|
|
|
/// router.map("/users/{id}", user_detail_handler);
|
|
/// router.map("/users/{id}", user_detail_handler);
|
|
|
/// router.map("/files/{filename}/details", file_details_handler);
|
|
/// router.map("/files/{filename}/details", file_details_handler);
|
|
|
|
|
+ /// router.map("/files/*.jpg", jpg_handler); // Single wildcard with suffix
|
|
|
|
|
+ /// router.map("/files/*", file_handler); // Single wildcard (one segment)
|
|
|
|
|
+ /// router.map("/downloads/**", download_handler); // Greedy wildcard (any depth)
|
|
|
|
|
+ /// router.map("/images/**.png", png_handler); // Greedy wildcard with suffix
|
|
|
/// router.not_found_handler = new NotFoundHandler();
|
|
/// router.not_found_handler = new NotFoundHandler();
|
|
|
/// router.error_handler = new ErrorHandler();
|
|
/// router.error_handler = new ErrorHandler();
|
|
|
/// </example>
|
|
/// </example>
|
|
@@ -30,8 +34,16 @@ namespace Astralis {
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Registers a route handler for the specified path pattern.
|
|
/// Registers a route handler for the specified path pattern.
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// Pattern syntax:
|
|
|
|
|
+ /// - Literal segments: "/users" matches exactly "/users"
|
|
|
|
|
+ /// - Named segments: "/users/{id}" captures the segment value as "id"
|
|
|
|
|
+ /// - Single wildcard (*): "/files/*" matches exactly one segment (e.g., "/files/test")
|
|
|
|
|
+ /// - Single wildcard with suffix (*.ext): "/files/*.jpg" matches one segment ending in .jpg
|
|
|
|
|
+ /// - Greedy wildcard (**): "/files/**" matches zero or more segments (e.g., "/files/a/b/c")
|
|
|
|
|
+ /// - Greedy wildcard with suffix (**.ext): "/files/**.jpg" matches paths ending in .jpg
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
- /// <param name="pattern">The path pattern to match. Supports named segments like "/users/{id}"</param>
|
|
|
|
|
|
|
+ /// <param name="pattern">The path pattern to match</param>
|
|
|
/// <param name="handler">The handler to invoke when the pattern matches</param>
|
|
/// <param name="handler">The handler to invoke when the pattern matches</param>
|
|
|
/// <param name="methods">Optional HTTP methods to restrict this route to (e.g., "GET", "POST")</param>
|
|
/// <param name="methods">Optional HTTP methods to restrict this route to (e.g., "GET", "POST")</param>
|
|
|
public void map(string pattern, owned RouteHandler handler, string[]? methods = null) {
|
|
public void map(string pattern, owned RouteHandler handler, string[]? methods = null) {
|
|
@@ -117,42 +129,108 @@ namespace Astralis {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Represents the type of a route segment.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ internal enum SegmentType {
|
|
|
|
|
+ /// <summary>Exact literal match required (e.g., "users")</summary>
|
|
|
|
|
+ LITERAL,
|
|
|
|
|
+ /// <summary>Named segment that captures its value (e.g., "{id}")</summary>
|
|
|
|
|
+ NAMED,
|
|
|
|
|
+ /// <summary>Single wildcard matching exactly one segment (e.g., "*")</summary>
|
|
|
|
|
+ SINGLE_WILDCARD,
|
|
|
|
|
+ /// <summary>Single wildcard with suffix requirement (e.g., "*.jpg")</summary>
|
|
|
|
|
+ SINGLE_WILDCARD_SUFFIX,
|
|
|
|
|
+ /// <summary>Greedy wildcard matching zero or more segments (e.g., "**")</summary>
|
|
|
|
|
+ GREEDY_WILDCARD,
|
|
|
|
|
+ /// <summary>Greedy wildcard with suffix requirement (e.g., "**.jpg")</summary>
|
|
|
|
|
+ GREEDY_WILDCARD_SUFFIX
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Represents a parsed route segment with its type and metadata.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ internal class ParsedSegment : Object {
|
|
|
|
|
+ public SegmentType segment_type;
|
|
|
|
|
+ public string value;
|
|
|
|
|
+ public string suffix;
|
|
|
|
|
+
|
|
|
|
|
+ public ParsedSegment(SegmentType type, string value, string suffix = "") {
|
|
|
|
|
+ this.segment_type = type;
|
|
|
|
|
+ this.value = value;
|
|
|
|
|
+ this.suffix = suffix;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Represents a single route with its pattern, handler, and allowed methods.
|
|
/// Represents a single route with its pattern, handler, and allowed methods.
|
|
|
|
|
+ /// Supports wildcards: "*" for single segment, "**" for greedy multi-segment matching.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
internal class Route : Object {
|
|
internal class Route : Object {
|
|
|
|
|
|
|
|
private string _pattern;
|
|
private string _pattern;
|
|
|
- private string[] _pattern_segments;
|
|
|
|
|
- private string[] _named_segment_names;
|
|
|
|
|
|
|
+ private ParsedSegment[] _parsed_segments;
|
|
|
public RouteHandler handler { get; private set; }
|
|
public RouteHandler handler { get; private set; }
|
|
|
private string[]? _allowed_methods;
|
|
private string[]? _allowed_methods;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>True if this route contains a greedy wildcard (**)</summary>
|
|
|
|
|
+ private bool _has_greedy_wildcard;
|
|
|
|
|
+ /// <summary>The suffix for greedy wildcard, if any (e.g., ".jpg" for "**.jpg")</summary>
|
|
|
|
|
+ private string _greedy_suffix = "";
|
|
|
|
|
|
|
|
public Route(string pattern, owned RouteHandler handler, string[]? methods = null) {
|
|
public Route(string pattern, owned RouteHandler handler, string[]? methods = null) {
|
|
|
_pattern = pattern;
|
|
_pattern = pattern;
|
|
|
_handler = handler;
|
|
_handler = handler;
|
|
|
_allowed_methods = methods != null && methods.length > 0 ? methods : null;
|
|
_allowed_methods = methods != null && methods.length > 0 ? methods : null;
|
|
|
|
|
+ _has_greedy_wildcard = false;
|
|
|
|
|
|
|
|
// Parse pattern into segments
|
|
// Parse pattern into segments
|
|
|
var segments = pattern.split("/");
|
|
var segments = pattern.split("/");
|
|
|
- var pattern_segments = new string[0];
|
|
|
|
|
- var named_segments = new string[0];
|
|
|
|
|
|
|
+ _parsed_segments = new ParsedSegment[0];
|
|
|
|
|
|
|
|
foreach (var segment in segments) {
|
|
foreach (var segment in segments) {
|
|
|
if (segment != "") {
|
|
if (segment != "") {
|
|
|
- pattern_segments += segment;
|
|
|
|
|
|
|
+ var parsed = parse_segment(segment);
|
|
|
|
|
+ _parsed_segments += parsed;
|
|
|
|
|
|
|
|
- // Check if this is a named segment {name}
|
|
|
|
|
- if (segment.length >= 3 && segment[0] == '{' && segment[segment.length - 1] == '}') {
|
|
|
|
|
- named_segments += segment.substring(1, segment.length - 2);
|
|
|
|
|
- } else {
|
|
|
|
|
- named_segments += ""; // Empty string indicates literal segment
|
|
|
|
|
|
|
+ if (parsed.segment_type == SegmentType.GREEDY_WILDCARD ||
|
|
|
|
|
+ parsed.segment_type == SegmentType.GREEDY_WILDCARD_SUFFIX) {
|
|
|
|
|
+ _has_greedy_wildcard = true;
|
|
|
|
|
+ _greedy_suffix = parsed.suffix;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Parses a single segment into its type and components.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private ParsedSegment parse_segment(string segment) {
|
|
|
|
|
+ // Check for named segment {name}
|
|
|
|
|
+ if (segment.length >= 3 && segment[0] == '{' && segment[segment.length - 1] == '}') {
|
|
|
|
|
+ return new ParsedSegment(SegmentType.NAMED, segment.substring(1, segment.length - 2));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check for greedy wildcard ** or **.suffix
|
|
|
|
|
+ if (segment.has_prefix("**")) {
|
|
|
|
|
+ string suffix = segment.substring(2);
|
|
|
|
|
+ if (suffix.length > 0) {
|
|
|
|
|
+ return new ParsedSegment(SegmentType.GREEDY_WILDCARD_SUFFIX, "**", suffix);
|
|
|
|
|
+ }
|
|
|
|
|
+ return new ParsedSegment(SegmentType.GREEDY_WILDCARD, "**");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- _pattern_segments = pattern_segments;
|
|
|
|
|
- _named_segment_names = named_segments;
|
|
|
|
|
|
|
+ // Check for single wildcard * or *.suffix
|
|
|
|
|
+ if (segment.has_prefix("*")) {
|
|
|
|
|
+ string suffix = segment.substring(1);
|
|
|
|
|
+ if (suffix.length > 0) {
|
|
|
|
|
+ return new ParsedSegment(SegmentType.SINGLE_WILDCARD_SUFFIX, "*", suffix);
|
|
|
|
|
+ }
|
|
|
|
|
+ return new ParsedSegment(SegmentType.SINGLE_WILDCARD, "*");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Literal segment
|
|
|
|
|
+ return new ParsedSegment(SegmentType.LITERAL, segment);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -183,32 +261,166 @@ namespace Astralis {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Check segment count
|
|
|
|
|
- if (path_segs.length != _pattern_segments.length) {
|
|
|
|
|
|
|
+ // Use different matching strategy based on whether we have greedy wildcards
|
|
|
|
|
+ if (_has_greedy_wildcard) {
|
|
|
|
|
+ return match_with_greedy(path_segs);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return match_simple(path_segs);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Simple matching for routes without greedy wildcards.
|
|
|
|
|
+ /// Requires exact segment count match.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private RouteContext? match_simple(string[] path_segs) {
|
|
|
|
|
+ // Check segment count - must match exactly
|
|
|
|
|
+ if (path_segs.length != _parsed_segments.length) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Match each segment
|
|
// Match each segment
|
|
|
var named_segments = new Dictionary<string, string>();
|
|
var named_segments = new Dictionary<string, string>();
|
|
|
|
|
+ var wildcard_captures = new Vector<string>();
|
|
|
|
|
|
|
|
- for (int i = 0; i < _pattern_segments.length; i++) {
|
|
|
|
|
- var pattern_seg = _pattern_segments[i];
|
|
|
|
|
|
|
+ for (int i = 0; i < _parsed_segments.length; i++) {
|
|
|
|
|
+ var parsed = _parsed_segments[i];
|
|
|
var path_seg = path_segs[i];
|
|
var path_seg = path_segs[i];
|
|
|
|
|
|
|
|
- if (_named_segment_names[i] != "") {
|
|
|
|
|
- // This is a named segment - capture the value
|
|
|
|
|
- named_segments[_named_segment_names[i]] = path_seg;
|
|
|
|
|
- } else if (pattern_seg != path_seg) {
|
|
|
|
|
- // Literal segment doesn't match
|
|
|
|
|
|
|
+ if (!match_segment(parsed, path_seg, named_segments, wildcard_captures)) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Build route context
|
|
// Build route context
|
|
|
|
|
+ return build_context(path_segs, named_segments, wildcard_captures);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Matching for routes with greedy wildcards (**).
|
|
|
|
|
+ /// Allows variable segment count.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private RouteContext? match_with_greedy(string[] path_segs) {
|
|
|
|
|
+ var named_segments = new Dictionary<string, string>();
|
|
|
|
|
+ var wildcard_captures = new Vector<string>();
|
|
|
|
|
+
|
|
|
|
|
+ int path_index = 0;
|
|
|
|
|
+ int pattern_index = 0;
|
|
|
|
|
+
|
|
|
|
|
+ while (pattern_index < _parsed_segments.length && path_index <= path_segs.length) {
|
|
|
|
|
+ var parsed = _parsed_segments[pattern_index];
|
|
|
|
|
+
|
|
|
|
|
+ if (parsed.segment_type == SegmentType.GREEDY_WILDCARD ||
|
|
|
|
|
+ parsed.segment_type == SegmentType.GREEDY_WILDCARD_SUFFIX) {
|
|
|
|
|
+ // Find how many segments this greedy wildcard should consume
|
|
|
|
|
+ int remaining_patterns = _parsed_segments.length - pattern_index - 1;
|
|
|
|
|
+ int min_path_needed = remaining_patterns; // Minimum segments needed for remaining patterns
|
|
|
|
|
+
|
|
|
|
|
+ // Check if we have enough path segments
|
|
|
|
|
+ if (path_segs.length - path_index < min_path_needed) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate how many segments the greedy wildcard consumes
|
|
|
|
|
+ int greedy_count = path_segs.length - path_index - min_path_needed;
|
|
|
|
|
+
|
|
|
|
|
+ // For greedy with suffix, the last consumed segment must match the suffix
|
|
|
|
|
+ if (parsed.segment_type == SegmentType.GREEDY_WILDCARD_SUFFIX) {
|
|
|
|
|
+ if (greedy_count == 0) {
|
|
|
|
|
+ // Need at least one segment to match the suffix
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ int last_greedy_index = path_index + greedy_count - 1;
|
|
|
|
|
+ if (last_greedy_index >= path_segs.length) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ string last_seg = path_segs[last_greedy_index];
|
|
|
|
|
+ if (!last_seg.has_suffix(parsed.suffix)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Capture all segments consumed by greedy wildcard
|
|
|
|
|
+ var greedy_segments = new string[0];
|
|
|
|
|
+ for (int i = 0; i < greedy_count; i++) {
|
|
|
|
|
+ string seg = path_segs[path_index + i];
|
|
|
|
|
+ greedy_segments += seg;
|
|
|
|
|
+ wildcard_captures.add(seg);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Store the greedy capture as a single string with "/" separator
|
|
|
|
|
+ if (greedy_segments.length > 0) {
|
|
|
|
|
+ named_segments["*"] = string.joinv("/", greedy_segments);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ named_segments["*"] = "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ path_index += greedy_count;
|
|
|
|
|
+ pattern_index++;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Non-greedy segment - match normally
|
|
|
|
|
+ if (path_index >= path_segs.length) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!match_segment(parsed, path_segs[path_index], named_segments, wildcard_captures)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ path_index++;
|
|
|
|
|
+ pattern_index++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if we've consumed all path segments and all patterns
|
|
|
|
|
+ if (path_index != path_segs.length || pattern_index != _parsed_segments.length) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return build_context(path_segs, named_segments, wildcard_captures);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Matches a single path segment against a parsed pattern segment.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private bool match_segment(ParsedSegment parsed, string path_seg,
|
|
|
|
|
+ Dictionary<string, string> named_segments,
|
|
|
|
|
+ Vector<string> wildcard_captures) {
|
|
|
|
|
+ switch (parsed.segment_type) {
|
|
|
|
|
+ case SegmentType.LITERAL:
|
|
|
|
|
+ return path_seg == parsed.value;
|
|
|
|
|
+
|
|
|
|
|
+ case SegmentType.NAMED:
|
|
|
|
|
+ named_segments[parsed.value] = path_seg;
|
|
|
|
|
+ return true;
|
|
|
|
|
+
|
|
|
|
|
+ case SegmentType.SINGLE_WILDCARD:
|
|
|
|
|
+ // Matches any non-empty segment
|
|
|
|
|
+ wildcard_captures.add(path_seg);
|
|
|
|
|
+ return true;
|
|
|
|
|
+
|
|
|
|
|
+ case SegmentType.SINGLE_WILDCARD_SUFFIX:
|
|
|
|
|
+ // Must end with the suffix
|
|
|
|
|
+ if (path_seg.has_suffix(parsed.suffix)) {
|
|
|
|
|
+ wildcard_captures.add(path_seg);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Builds a RouteContext from the match results.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private RouteContext build_context(string[] path_segs,
|
|
|
|
|
+ Dictionary<string, string> named_segments,
|
|
|
|
|
+ Vector<string> wildcard_captures) {
|
|
|
var context = new RouteContext();
|
|
var context = new RouteContext();
|
|
|
context.segments = Wrap.array<string>(path_segs).to_series();
|
|
context.segments = Wrap.array<string>(path_segs).to_series();
|
|
|
context.named_segments = named_segments;
|
|
context.named_segments = named_segments;
|
|
|
-
|
|
|
|
|
|
|
+ context.wildcard_captures = wildcard_captures.to_series();
|
|
|
return context;
|
|
return context;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -241,7 +453,7 @@ namespace Astralis {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// Context information for a matched route, containing path segments and named captures.
|
|
|
|
|
|
|
+ /// Context information for a matched route, containing path segments, named captures, and wildcard captures.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public class RouteContext : Object {
|
|
public class RouteContext : Object {
|
|
|
|
|
|
|
@@ -254,9 +466,25 @@ namespace Astralis {
|
|
|
/// Named segment values captured from the route pattern.
|
|
/// Named segment values captured from the route pattern.
|
|
|
/// For pattern "/users/{id}/posts/{post_id}" matching "/users/123/posts/456",
|
|
/// For pattern "/users/{id}/posts/{post_id}" matching "/users/123/posts/456",
|
|
|
/// this would contain {"id": "123", "post_id": "456"}.
|
|
/// this would contain {"id": "123", "post_id": "456"}.
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// For greedy wildcards (**), the key "*" contains the matched path segments
|
|
|
|
|
+ /// joined with "/" (e.g., for "/files/**" matching "/files/a/b/c", this would
|
|
|
|
|
+ /// contain {"*": "a/b/c"}).
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public Dictionary<string, string> named_segments { get; set; }
|
|
public Dictionary<string, string> named_segments { get; set; }
|
|
|
|
|
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// All values captured by wildcard segments (* or **).
|
|
|
|
|
+ /// Each wildcard match adds its captured value(s) to this series.
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// For single wildcards (*), each matching segment is added individually.
|
|
|
|
|
+ /// For greedy wildcards (**), all consumed segments are added individually.
|
|
|
|
|
+ ///
|
|
|
|
|
+ /// Example: Pattern "/files/*/*.jpg" matching "/files/images/photo.jpg"
|
|
|
|
|
+ /// would have wildcard_captures = ["images", "photo.jpg"]
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public Series<string> wildcard_captures { get; set; }
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Gets a named segment value by name.
|
|
/// Gets a named segment value by name.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -296,6 +524,15 @@ namespace Astralis {
|
|
|
public bool has_segment(string name) {
|
|
public bool has_segment(string name) {
|
|
|
return named_segments.has(name);
|
|
return named_segments.has(name);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets the greedy wildcard capture value (from ** patterns).
|
|
|
|
|
+ /// This is a convenience method for getting the "*" key from named_segments.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <returns>The greedy wildcard capture as a path string, or null if not present</returns>
|
|
|
|
|
+ public string? get_greedy_capture() {
|
|
|
|
|
+ return get_segment("*");
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|