Skip to content

Commit 2f27228

Browse files
committed
added basic placeholder system
1 parent e42a62d commit 2f27228

4 files changed

Lines changed: 138 additions & 18 deletions

File tree

LinkRouter/App/Configuration/Config.cs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using LinkRouter.App.Models;
1+
using System.Text;
2+
using System.Text.Json.Serialization;
3+
using System.Text.RegularExpressions;
4+
using LinkRouter.App.Models;
25

36
namespace LinkRouter.App.Configuration;
47

@@ -26,4 +29,83 @@ public class NotFoundBehaviorConfig
2629
public bool RedirectOn404 { get; set; } = false;
2730
public string RedirectUrl { get; set; } = "https://example.com/404";
2831
}
32+
33+
[JsonIgnore]
34+
public static Regex PlaceholderPattern => new(@"\\\{(\d|\w+)\}");
35+
36+
[JsonIgnore]
37+
public CompiledRoute[]? CompiledRoutes { get; set; }
38+
39+
public void CompileRoutes()
40+
{
41+
var compiledRoutes = new List<CompiledRoute>();
42+
43+
foreach (var route in Routes)
44+
{
45+
if (!route.Route.StartsWith("/"))
46+
route.Route = "/" + route.Route;
47+
48+
if (!route.Route.EndsWith("/"))
49+
route.Route += "/";
50+
51+
var compiled = new CompiledRoute
52+
{
53+
Route = route.Route,
54+
RedirectUrl = route.RedirectUrl
55+
};
56+
57+
var replacements = new List<(int Index, int Length, string NewText)>();
58+
59+
var escaped = Regex.Escape(route.Route);
60+
61+
var matches = PlaceholderPattern.Matches(escaped);
62+
63+
64+
foreach (var match in matches.Select(x => x))
65+
{
66+
// Check if the placeholder is immediately followed by another placeholder
67+
68+
Console.WriteLine("matchlenght: "+ (match.Index + match.Length + 2) + " route lenght" + escaped.Length);
69+
70+
if (escaped.Length >= match.Index + match.Length + 2 && escaped.Substring(match.Index + match.Length, 2) == "\\{")
71+
throw new InvalidOperationException(
72+
$"Placeholder {match.Groups[1].Value} cannot be immediately followed by another placeholder. " +
73+
$"Please add a string literal as separator.");
74+
75+
replacements.Add((match.Index, match.Length, "(.+)"));
76+
}
77+
78+
var compiledRouteBuilder = new StringBuilder(escaped);
79+
80+
foreach (var replacement in replacements.OrderByDescending(r => r.Index))
81+
{
82+
compiledRouteBuilder.Remove(replacement.Index, replacement.Length);
83+
compiledRouteBuilder.Insert(replacement.Index, replacement.NewText);
84+
}
85+
86+
compiled.CompiledPattern = new Regex(compiledRouteBuilder.ToString(), RegexOptions.Compiled);
87+
88+
Console.WriteLine(compiled.CompiledPattern.ToString());
89+
90+
var duplicate = matches
91+
.Select((m, i) => m.Groups[1].Value)
92+
.GroupBy(x => x)
93+
.FirstOrDefault(x => x.Count() > 1);
94+
95+
if (duplicate != null)
96+
throw new InvalidOperationException("Cannot use a placeholder twice in the route: " + duplicate.Key);
97+
98+
compiled.Placeholders = matches
99+
.Select((m, i) => m.Groups[1].Value)
100+
.Distinct()
101+
.Select((name, i) => (name, i))
102+
.ToDictionary(x => x.name, x => x.i + 1);
103+
104+
compiledRoutes.Add(compiled);
105+
}
106+
107+
CompiledRoutes = compiledRoutes
108+
.ToArray();
109+
}
110+
29111
}

LinkRouter/App/Http/Controllers/RedirectController.cs

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,38 +37,54 @@ public RedirectController(Config config)
3737
}
3838

3939
[HttpGet("/{*path}")]
40-
public IActionResult RedirectToExternalUrl(string path)
40+
public async Task<ActionResult> RedirectToExternalUrl(string path)
4141
{
42-
var redirectRoute = Config.Routes.FirstOrDefault(x => x.Route == path || x.Route == path + "/" || x.Route == "/" + path);
43-
44-
if (redirectRoute != null)
42+
43+
if (!path.EndsWith("/"))
44+
path += "/";
45+
46+
path = "/" + path;
47+
48+
Console.WriteLine(path);
49+
50+
var redirectRoute = Config.CompiledRoutes?.FirstOrDefault(x => x.CompiledPattern.IsMatch(path));
51+
52+
if (redirectRoute == null)
4553
{
46-
RouteCounter
47-
.WithLabels(redirectRoute.Route)
54+
NotFoundCounter
55+
.WithLabels(path)
4856
.Inc();
4957

50-
return Redirect(redirectRoute.RedirectUrl);
51-
}
58+
if (Config.NotFoundBehavior.RedirectOn404)
59+
return Redirect(Config.NotFoundBehavior.RedirectUrl);
5260

53-
NotFoundCounter
54-
.WithLabels("/" + path)
55-
.Inc();
61+
return NotFound();
62+
}
63+
64+
var match = redirectRoute.CompiledPattern.Match(path);
5665

57-
if (Config.NotFoundBehavior.RedirectOn404)
58-
return Redirect(Config.NotFoundBehavior.RedirectUrl);
66+
Console.WriteLine(redirectRoute.CompiledPattern);
5967

60-
return NotFound();
68+
string redirectUrl = redirectRoute.RedirectUrl;
69+
70+
foreach (var placeholder in redirectRoute.Placeholders)
71+
{
72+
var value = match.Groups[placeholder.Value].Value;
73+
redirectUrl = redirectUrl.Replace("{" + placeholder.Key + "}", value);
74+
}
75+
76+
return Redirect(redirectUrl);
6177
}
62-
78+
6379
[HttpGet("/")]
6480
public IActionResult GetRootRoute()
6581
{
6682
RouteCounter
6783
.WithLabels("/")
6884
.Inc();
69-
85+
7086
string url = Config.RootRoute;
71-
87+
7288
return Redirect(url);
7389
}
7490
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace LinkRouter.App.Models;
4+
5+
public class CompiledRoute : RedirectRoute
6+
{
7+
public Regex CompiledPattern { get; set; }
8+
9+
public Dictionary<string, int> Placeholders { get; set; } = new();
10+
}

LinkRouter/App/Services/ConfigWatcher.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public ConfigWatcher(ILogger<ConfigWatcher> logger, Config config)
1818

1919
protected override Task ExecuteAsync(CancellationToken stoppingToken)
2020
{
21+
2122
if (!File.Exists(ConfigPath))
2223
{
2324
Logger.LogWarning("Watched file does not exist: {FilePath}", ConfigPath);
@@ -29,6 +30,8 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken)
2930
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.CreationTime
3031
};
3132

33+
OnChanged(Watcher, new FileSystemEventArgs(WatcherChangeTypes.Created, string.Empty, string.Empty));
34+
3235
Watcher.Changed += OnChanged;
3336

3437
Watcher.EnableRaisingEvents = true;
@@ -48,6 +51,15 @@ private void OnChanged(object sender, FileSystemEventArgs e)
4851
Config.RootRoute = config?.RootRoute ?? "https://example.com";
4952

5053
Logger.LogInformation("Config file changed.");
54+
55+
try
56+
{
57+
Config.CompileRoutes();
58+
} catch (InvalidOperationException ex)
59+
{
60+
Logger.LogError("Failed to compile routes: " + ex.Message);
61+
Environment.Exit(1);
62+
}
5163
}
5264
catch (IOException ex)
5365
{

0 commit comments

Comments
 (0)