URL router with single-segment wildcards
I was asked this
No reports yet. Sign in to say where you were asked it.
Build a small URL router.
class Router:
def __init__(self) -> None:
pass
def add_route(self, pattern: str, handler: str) -> None:
pass
def route(self, path: str) -> str:
return ""
- A pattern and a path are split on
/into segments. Empty segments are ignored, so/a/b/is the same as/a/b, and/is the empty path. - A pattern segment is either a literal, which must equal the path segment, or
*, which matches exactly one segment of any value. A pattern matches a path only if they have the same number of segments. -
add_routeregistershandlerforpattern. Registering the same pattern again replaces its handler. -
routereturns the handler of the most specific matching pattern, or""if no pattern matches.- The pattern with the fewest
*segments wins. - If two matching patterns have the same number of
*, compare them left to right: at the first segment where they differ, the one with the literal wins.
- The pattern with the fewest
Aim for lookups that do not scan every registered route.
Constraints: up to 10^4 routes; up to 20 segments per path; segments are non-empty strings without /.
Example
add_route("/shop/*/items", "shop_items")
add_route("/shop/main/items", "main_items")
add_route("/*/*/items", "any_items")
add_route("/x/y/z", "xyz")
add_route("/*/y/w", "any_y_w")
route("/shop/main/items") -> "main_items" # exact match
route("/shop/9/items") -> "shop_items" # 1 wildcard beats 2
route("/blog/9/items") -> "any_items"
route("/x/y/w") -> "any_y_w" # the /x/... branch dead-ends
route("/shop/items") -> "" # wrong number of segments
With /x/* and /*/y both registered, route("/x/y") returns the handler of /x/*, because its first segment is a literal.
def example_router():
r = Router()
r.add_route("/shop/*/items", "shop_items")
r.add_route("/shop/main/items", "main_items")
r.add_route("/*/*/items", "any_items")
r.add_route("/x/y/z", "xyz")
r.add_route("/*/y/w", "any_y_w")
return r
def test_exact_match():
assert example_router().route("/shop/main/items") == "main_items"
def test_one_wildcard_beats_two():
assert example_router().route("/shop/9/items") == "shop_items"
def test_two_wildcards():
assert example_router().route("/blog/9/items") == "any_items"
def test_backs_out_of_a_dead_end():
assert example_router().route("/x/y/w") == "any_y_w"
def test_wrong_number_of_segments():
assert example_router().route("/shop/items") == ""
def test_earlier_literal_wins_a_tie():
r = Router()
r.add_route("/x/*", "x_star")
r.add_route("/*/y", "star_y")
assert r.route("/x/y") == "x_star"
def test_empty_router():
assert Router().route("/anything") == ""
def test_empty_segments_are_ignored():
r = Router()
r.add_route("/a/b/", "ab")
assert r.route("/a/b") == "ab"
assert r.route("a/b") == "ab"
assert r.route("//a//b//") == "ab"
def test_root_is_the_empty_path():
r = Router()
r.add_route("/", "root")
assert r.route("/") == "root"
assert r.route("") == "root"
assert r.route("/a") == ""
def test_star_matches_exactly_one_segment():
r = Router()
r.add_route("/a/*", "a_star")
assert r.route("/a/x") == "a_star"
assert r.route("/a") == ""
assert r.route("/a/x/y") == ""
def test_registering_again_replaces_the_handler():
r = Router()
r.add_route("/a/*", "old")
r.add_route("/a/*", "new")
assert r.route("/a/zzz") == "new"
def test_fewer_wildcards_beat_an_earlier_literal():
r = Router()
r.add_route("/a/*/*", "two_stars")
r.add_route("/*/b/c", "one_star")
assert r.route("/a/b/c") == "one_star"
def test_a_tie_is_decided_at_the_first_difference():
r = Router()
r.add_route("/*/b/c", "star_first")
r.add_route("/a/*/c", "literal_first")
assert r.route("/a/b/c") == "literal_first"
def test_backs_out_from_deep_down():
r = Router()
r.add_route("/a/b/c/d", "literal")
r.add_route("/*/*/*/e", "stars")
assert r.route("/a/b/c/e") == "stars"
assert r.route("/a/b/c/d") == "literal"
def test_ten_thousand_routes():
r = Router()
for i in range(10_000):
r.add_route(f"/api/v{i % 10}/svc{i}/*", f"h{i}")
r.add_route("/api/*/*/health", "health")
for i in range(0, 10_000, 7):
assert r.route(f"/api/v{i % 10}/svc{i}/x") == f"h{i}"
assert r.route("/api/v3/svc3/health") == "h3"
assert r.route("/api/v3/svc4/health") == "health"
assert r.route("/api/v3/svc3") == ""
def test_twenty_segments():
segs = [f"s{i}" for i in range(20)]
r = Router()
r.add_route("/" + "/".join(segs[:-1] + ["other"]), "almost")
r.add_route("/" + "/".join(["*"] * 20), "all_stars")
r.add_route("/" + "/".join(["*"] + segs[1:]), "one_star")
assert r.route("/" + "/".join(segs)) == "one_star"
assert r.route("/" + "/".join(["z"] * 20)) == "all_stars"
assert r.route("/" + "/".join(segs[:-1] + ["other"])) == "almost"
def test_the_latest_star_wins_among_equals():
# Every pattern has one * at a different place; the path matches them all.
n = 16
r = Router()
for i in range(n - 1):
r.add_route("/" + "/".join(["a"] * i + ["*"] + ["a"] * (n - i - 2) + ["x"]), f"p{i}")
r.add_route("/" + "/".join(["*"] * (n - 1) + ["y"]), "y")
assert r.route("/" + "/".join(["a"] * (n - 1) + ["x"])) == f"p{n - 2}"
assert r.route("/" + "/".join(["a"] * (n - 1) + ["y"])) == "y"
Reconstructed from the reported question; details may differ from what interviewers ask.
Sign in to see the worked answer
The full answer, chapter by chapter, with what interviewers expect at each level. Free with an account: one full answer per company.
Sign in to see the worked answer
The full answer, chapter by chapter, with what interviewers expect at each level. Free with an account: one full answer per company.
Sign in to submit your code; every attempt is kept here.

