1 /**
2  * Auxiliary utilities for dealing with URLs, besides the main matching logic.
3  */
4 module path_matcher.url_util;
5 
6 import std.range.primitives : isRandomAccessRange, hasLength, ElementType;
7 
8 @safe:
9 
10 /**
11  * Extracts segments from a slash-separated URL path and stores them in a given
12  * `store` array which has been pre-allocated.
13  * Params:
14  *   path = The path to parse.
15  *   store = The array to store segments in. Consider allocating this on the
16  *           stack for performance improvements.
17  * Returns: The number of segments that were parsed, or -1 if the given store
18  * is too small to fit all of them.
19  */
20 int toSegments(string path, scope string[] store) @nogc {
21     uint i = 0;
22     uint storeIdx = 0;
23     while (i < path.length) {
24         while (i < path.length && path[i] == '/') i++;
25         if (i >= path.length || path[i] == '?') return storeIdx;
26         immutable uint segmentStart = i;
27         uint segmentEnd = i + 1;
28         while (segmentEnd < path.length && path[segmentEnd] != '/' && path[segmentEnd] != '?') segmentEnd++;
29         if (storeIdx == store.length) return -1;
30         store[storeIdx++] = path[segmentStart .. segmentEnd];
31         i = segmentEnd++;
32     }
33     return storeIdx;
34 }
35 
36 unittest {
37     void doTest(string path, string[] expectedSegments) {
38         import std.format : format;
39         string[32] segments;
40         int count = toSegments(path, segments);
41         assert(count >= 0);
42         assert(
43             segments[0..count] == expectedSegments,
44             format!"Expected segments %s for path %s, but got %s."(
45                 expectedSegments,
46                 path,
47                 segments
48             )
49         );
50     }
51 
52     doTest("/test", ["test"]);
53     doTest("/test/", ["test"]);
54     doTest("test/", ["test"]);
55     doTest("/test?query=hello", ["test"]);
56     doTest("/test/one", ["test", "one"]);
57     doTest("/test/one", ["test", "one"]);
58     doTest("/abc/123/test/yes", ["abc", "123", "test", "yes"]);
59     doTest("a", ["a"]);
60     doTest("a/b", ["a", "b"]);
61     doTest("/a/b?c=d", ["a", "b"]);
62     doTest("", []);
63     doTest("/", []);
64     doTest("///", []);
65 }
66 
67 /**
68  * Helper function to pop a single segment from an array of segments, and
69  * increment a referenced index variable.
70  * Params:
71  *   segments = The list of segments to pop from.
72  *   idx = The referenced index to increment.
73  * Returns: The segment that was popped, or null if we've reached the end of
74  * the segments list.
75  */
76 string popSegment(I)(I segments, ref uint idx) if (
77     isRandomAccessRange!I &&
78     hasLength!I &&
79     is(ElementType!I : string)
80 ) {
81     if (idx >= segments.length) return null;
82     return segments[idx++];
83 }
84 
85 unittest {
86     string[] a = ["a", "b", "c"];
87     uint idx = 0;
88     assert(popSegment(a, idx) == "a");
89     assert(idx == 1);
90 }