diff --git a/samples/IslandGateway.Sample/appsettings.json b/samples/IslandGateway.Sample/appsettings.json
index 15312c135..f9e562809 100644
--- a/samples/IslandGateway.Sample/appsettings.json
+++ b/samples/IslandGateway.Sample/appsettings.json
@@ -34,7 +34,11 @@
{
"RouteId": "backend1/route1",
"BackendId": "backend1",
- "Rule": "Host('localhost') && Path('/{**catchall}')"
+ "Match": {
+ "Methods": [ "GET", "POST" ],
+ "Host": "localhost",
+ "Path": "/{**catchall}"
+ }
}
]
}
diff --git a/src/IslandGateway.Core/Abstractions/RouteDiscovery/Contract/GatewayMatch.cs b/src/IslandGateway.Core/Abstractions/RouteDiscovery/Contract/GatewayMatch.cs
new file mode 100644
index 000000000..15efacb6d
--- /dev/null
+++ b/src/IslandGateway.Core/Abstractions/RouteDiscovery/Contract/GatewayMatch.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Linq;
+
+namespace IslandGateway.Core.Abstractions
+{
+ ///
+ /// Describes the matching criteria for a route.
+ ///
+ public class GatewayMatch : IDeepCloneable
+ {
+ ///
+ /// Only match requests that use these optional HTTP methods. E.g. GET, POST.
+ ///
+ public IReadOnlyList Methods { get; set; }
+
+ ///
+ /// Only match requests with the given Host header.
+ ///
+ public string Host { get; set; }
+
+ ///
+ /// Only match requests with the given Path pattern.
+ ///
+ public string Path { get; set; }
+
+ // TODO:
+ ///
+ /// Only match requests that contain all of these query parameters.
+ ///
+ // public ICollection> QueryParameters { get; set; }
+
+ // TODO:
+ ///
+ /// Only match requests that contain all of these request headers.
+ ///
+ // public ICollection> Headers { get; set; }
+
+ GatewayMatch IDeepCloneable.DeepClone()
+ {
+ return new GatewayMatch()
+ {
+ Methods = Methods?.ToArray(),
+ Host = Host,
+ Path = Path,
+ // Headers = Headers.DeepClone(); // TODO:
+ };
+ }
+ }
+}
diff --git a/src/IslandGateway.Core/Abstractions/RouteDiscovery/Contract/GatewayRoute.cs b/src/IslandGateway.Core/Abstractions/RouteDiscovery/Contract/GatewayRoute.cs
index 3a5d9ce6b..3eaac9254 100644
--- a/src/IslandGateway.Core/Abstractions/RouteDiscovery/Contract/GatewayRoute.cs
+++ b/src/IslandGateway.Core/Abstractions/RouteDiscovery/Contract/GatewayRoute.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
@@ -7,7 +7,7 @@
namespace IslandGateway.Core.Abstractions
{
///
- /// Describes a route that matches incoming requests based on a
+ /// Describes a route that matches incoming requests based on a the criteria
/// and proxies matching requests to the backend identified by its .
///
public sealed class GatewayRoute : IDeepCloneable
@@ -17,10 +17,7 @@ public sealed class GatewayRoute : IDeepCloneable
///
public string RouteId { get; set; }
- ///
- /// Rule that incoming requests must match for this route to apply. E.g. Host('example.com').
- ///
- public string Rule { get; set; }
+ public GatewayMatch Match { get; private set; } = new GatewayMatch();
///
/// Optionally, a priority value for this route. Routes with higher numbers take precedence over lower numbers.
@@ -44,7 +41,7 @@ GatewayRoute IDeepCloneable.DeepClone()
return new GatewayRoute
{
RouteId = RouteId,
- Rule = Rule,
+ Match = Match.DeepClone(),
Priority = Priority,
BackendId = BackendId,
Metadata = Metadata?.DeepClone(StringComparer.Ordinal),
diff --git a/src/IslandGateway.Core/Configuration/DependencyInjection/BuilderExtensions/Core.cs b/src/IslandGateway.Core/Configuration/DependencyInjection/BuilderExtensions/Core.cs
index 16e57c161..c3b7bf3c3 100644
--- a/src/IslandGateway.Core/Configuration/DependencyInjection/BuilderExtensions/Core.cs
+++ b/src/IslandGateway.Core/Configuration/DependencyInjection/BuilderExtensions/Core.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using IslandGateway.Common.Abstractions.Telemetry;
@@ -43,8 +43,6 @@ public static IIslandGatewayBuilder AddInMemoryRepos(this IIslandGatewayBuilder
public static IIslandGatewayBuilder AddConfigBuilder(this IIslandGatewayBuilder builder)
{
builder.Services.AddSingleton();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
return builder;
diff --git a/src/IslandGateway.Core/IslandGateway.Core.csproj b/src/IslandGateway.Core/IslandGateway.Core.csproj
index 4af24a10c..8ed1eb153 100644
--- a/src/IslandGateway.Core/IslandGateway.Core.csproj
+++ b/src/IslandGateway.Core/IslandGateway.Core.csproj
@@ -10,10 +10,6 @@
-
-
-
-
diff --git a/src/IslandGateway.Core/Service/Config/ConfigErrors.cs b/src/IslandGateway.Core/Service/Config/ConfigErrors.cs
index e87e53b8a..186bb9af5 100644
--- a/src/IslandGateway.Core/Service/Config/ConfigErrors.cs
+++ b/src/IslandGateway.Core/Service/Config/ConfigErrors.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace IslandGateway.Core.Service
@@ -14,7 +14,6 @@ internal static class ConfigErrors
internal const string RouteUnknownBackend = "Route_UnknownBackend";
internal const string RouteNoBackends = "Route_NoBackends";
internal const string RouteUnsupportedAction = "Route_UnsupportedAction";
- internal const string RouteBadRule = "Route_BadRule";
internal const string ParsedRouteMissingId = "ParsedRoute_MissingId";
internal const string ParsedRouteRuleHasNoMatchers = "ParsedRoute_RuleHasNoMatchers";
diff --git a/src/IslandGateway.Core/Service/Config/DynamicConfigBuilder.cs b/src/IslandGateway.Core/Service/Config/DynamicConfigBuilder.cs
index 5fd2fc718..dd08cb92d 100644
--- a/src/IslandGateway.Core/Service/Config/DynamicConfigBuilder.cs
+++ b/src/IslandGateway.Core/Service/Config/DynamicConfigBuilder.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
@@ -16,26 +16,22 @@ internal class DynamicConfigBuilder : IDynamicConfigBuilder
private readonly IBackendsRepo _backendsRepo;
private readonly IBackendEndpointsRepo _endpointsRepo;
private readonly IRoutesRepo _routesRepo;
- private readonly IRouteParser _routeParser;
private readonly IRouteValidator _parsedRouteValidator;
public DynamicConfigBuilder(
IBackendsRepo backendsRepo,
IBackendEndpointsRepo endpointsRepo,
IRoutesRepo routesRepo,
- IRouteParser routeParser,
IRouteValidator parsedRouteValidator)
{
Contracts.CheckValue(backendsRepo, nameof(backendsRepo));
Contracts.CheckValue(endpointsRepo, nameof(endpointsRepo));
Contracts.CheckValue(routesRepo, nameof(routesRepo));
- Contracts.CheckValue(routeParser, nameof(routeParser));
Contracts.CheckValue(parsedRouteValidator, nameof(parsedRouteValidator));
_backendsRepo = backendsRepo;
_endpointsRepo = endpointsRepo;
_routesRepo = routesRepo;
- _routeParser = routeParser;
_parsedRouteValidator = parsedRouteValidator;
}
@@ -118,14 +114,16 @@ private async Task> GetRoutesAsync(IConfigErrorReporter error
continue;
}
- var parsedResult = _routeParser.ParseRoute(route, errorReporter);
- if (!parsedResult.IsSuccess)
- {
- // routeParser already reported error message
- continue;
- }
+ var parsedRoute = new ParsedRoute {
+ RouteId = route.RouteId,
+ Methods = route.Match.Methods,
+ Host = route.Match.Host,
+ Path = route.Match.Path,
+ Priority = route.Priority,
+ BackendId = route.BackendId,
+ Metadata = route.Metadata,
+ };
- var parsedRoute = parsedResult.Value;
if (!_parsedRouteValidator.ValidateRoute(parsedRoute, errorReporter))
{
// parsedRouteValidator already reported error message
diff --git a/src/IslandGateway.Core/Service/Config/IRouteParser.cs b/src/IslandGateway.Core/Service/Config/IRouteParser.cs
deleted file mode 100644
index 69656bcb4..000000000
--- a/src/IslandGateway.Core/Service/Config/IRouteParser.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using IslandGateway.Core.Abstractions;
-using IslandGateway.Core.ConfigModel;
-
-namespace IslandGateway.Core.Service
-{
- ///
- /// Provides a method to parse a route into a format convenient for internal use in Island Gateway.
- ///
- internal interface IRouteParser
- {
- ///
- /// Parses a route into a format convenient for internal use in Island Gateway.
- ///
- Result ParseRoute(GatewayRoute route, IConfigErrorReporter errorReporter);
- }
-}
diff --git a/src/IslandGateway.Core/Service/Config/RouteParser.cs b/src/IslandGateway.Core/Service/Config/RouteParser.cs
deleted file mode 100644
index 07f03fb55..000000000
--- a/src/IslandGateway.Core/Service/Config/RouteParser.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using IslandGateway.Core.Abstractions;
-using IslandGateway.Core.ConfigModel;
-using IslandGateway.Utilities;
-
-namespace IslandGateway.Core.Service
-{
- internal class RouteParser : IRouteParser
- {
- private readonly IRuleParser _ruleParser;
-
- public RouteParser(IRuleParser ruleParser)
- {
- Contracts.CheckValue(ruleParser, nameof(ruleParser));
- _ruleParser = ruleParser;
- }
-
- public Result ParseRoute(GatewayRoute route, IConfigErrorReporter errorReporter)
- {
- if (route.Rule == null)
- {
- errorReporter.ReportError(ConfigErrors.RouteBadRule, route.RouteId, $"Route '{route.RouteId}' did not specify a rule");
- return Result.Failure();
- }
-
- var parsedRule = _ruleParser.Parse(route.Rule);
- if (!parsedRule.IsSuccess)
- {
- errorReporter.ReportError(ConfigErrors.RouteBadRule, route.RouteId, $"Route '{route.RouteId}' has an invalid rule: {parsedRule.Error} (rule: {route.Rule}");
- return Result.Failure();
- }
-
- var parsedRoute = new ParsedRoute
- {
- RouteId = route.RouteId,
- Rule = route.Rule,
- Matchers = parsedRule.Value,
- Priority = route.Priority,
- BackendId = route.BackendId,
- Metadata = route.Metadata,
- };
-
- return Result.Success(parsedRoute);
- }
- }
-}
diff --git a/src/IslandGateway.Core/Service/Config/RuleParsing/IRuleParser.cs b/src/IslandGateway.Core/Service/Config/RuleParsing/IRuleParser.cs
deleted file mode 100644
index d22d04a13..000000000
--- a/src/IslandGateway.Core/Service/Config/RuleParsing/IRuleParser.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System.Collections.Generic;
-using IslandGateway.Core.Abstractions;
-
-namespace IslandGateway.Core.Service
-{
-
- ///
- /// Interface for a class that parses Core Gateway rules
- /// such as HostName('abc.example.com') && PathPrefix('/a/b')
- /// and produces the corresponding AST.
- ///
- internal interface IRuleParser
- {
- ///
- /// Parses a rule and produces the corresponding AST or an error value.
- ///
- Result, string> Parse(string rule);
- }
-}
diff --git a/src/IslandGateway.Core/Service/Config/RuleParsing/Matchers/HostMatcher.cs b/src/IslandGateway.Core/Service/Config/RuleParsing/Matchers/HostMatcher.cs
deleted file mode 100644
index 4f280fad9..000000000
--- a/src/IslandGateway.Core/Service/Config/RuleParsing/Matchers/HostMatcher.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using IslandGateway.Utilities;
-
-namespace IslandGateway.Core.Service
-{
- internal sealed class HostMatcher : RuleMatcherBase
- {
- public HostMatcher(string name, string[] args)
- : base(name, args)
- {
- Contracts.Check(args.Length == 1, $"Expected 1 argument, found {args.Length}.");
- Contracts.CheckNonEmpty(args[0], $"{nameof(args)}[0]");
- }
-
- public string Host => Args[0];
- }
-}
diff --git a/src/IslandGateway.Core/Service/Config/RuleParsing/Matchers/MethodMatcher.cs b/src/IslandGateway.Core/Service/Config/RuleParsing/Matchers/MethodMatcher.cs
deleted file mode 100644
index 95e6f7d62..000000000
--- a/src/IslandGateway.Core/Service/Config/RuleParsing/Matchers/MethodMatcher.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using IslandGateway.Utilities;
-
-namespace IslandGateway.Core.Service
-{
- internal sealed class MethodMatcher : RuleMatcherBase
- {
- public MethodMatcher(string name, string[] args)
- : base(name, args)
- {
- Contracts.Check(args.Length >= 1, $"Expected at least 1 argument, found {args.Length}.");
- }
-
- public string[] Methods => Args;
- }
-}
diff --git a/src/IslandGateway.Core/Service/Config/RuleParsing/Matchers/PathMatcher.cs b/src/IslandGateway.Core/Service/Config/RuleParsing/Matchers/PathMatcher.cs
deleted file mode 100644
index fd86e11ab..000000000
--- a/src/IslandGateway.Core/Service/Config/RuleParsing/Matchers/PathMatcher.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using IslandGateway.Utilities;
-
-namespace IslandGateway.Core.Service
-{
- internal sealed class PathMatcher : RuleMatcherBase
- {
- public PathMatcher(string name, string[] args)
- : base(name, args)
- {
- Contracts.Check(args.Length == 1, $"Expected 1 argument, found {args.Length}.");
- Contracts.CheckNonEmpty(args[0], $"{nameof(args)}[0]");
- }
-
- public string Pattern => Args[0];
- }
-}
diff --git a/src/IslandGateway.Core/Service/Config/RuleParsing/RouteValidator.cs b/src/IslandGateway.Core/Service/Config/RuleParsing/RouteValidator.cs
index 6a03c00a8..f2f391f99 100644
--- a/src/IslandGateway.Core/Service/Config/RuleParsing/RouteValidator.cs
+++ b/src/IslandGateway.Core/Service/Config/RuleParsing/RouteValidator.cs
@@ -1,9 +1,8 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Text.RegularExpressions;
using IslandGateway.Core.Abstractions;
using IslandGateway.Core.ConfigModel;
@@ -14,6 +13,7 @@ namespace IslandGateway.Core.Service
{
internal class RouteValidator : IRouteValidator
{
+ // TODO: IDN support. How strictly do we need to validate this anyways? This is app config, not external input.
///
/// Regex explanation:
/// Either:
@@ -42,6 +42,7 @@ internal class RouteValidator : IRouteValidator
"HEAD", "OPTIONS", "GET", "PUT", "POST", "PATCH", "DELETE", "TRACE",
};
+ // Note this performs all validation steps without short circuiting in order to report all possible errors.
public bool ValidateRoute(ParsedRoute route, IConfigErrorReporter errorReporter)
{
Contracts.CheckValue(route, nameof(route));
@@ -54,121 +55,76 @@ public bool ValidateRoute(ParsedRoute route, IConfigErrorReporter errorReporter)
success = false;
}
- if ((route.Matchers?.Count ?? 0) == 0)
- {
- errorReporter.ReportError(ConfigErrors.ParsedRouteRuleHasNoMatchers, route.RouteId, $"Route '{route.RouteId}' rule has no matchers.");
- success = false;
- }
-
- if (route.Matchers != null && !route.Matchers.Any(m => m is HostMatcher))
- {
- errorReporter.ReportError(ConfigErrors.ParsedRouteRuleMissingHostMatcher, route.RouteId, $"Route '{route.RouteId}' rule is missing required matcher 'Host()'.");
- success = false;
- }
-
- if (route.Matchers != null && route.Matchers.Count(m => m is HostMatcher) > 1)
- {
- errorReporter.ReportError(ConfigErrors.ParsedRouteRuleMultipleHostMatchers, route.RouteId, $"Route '{route.RouteId}' rule has more than one 'Host()' matcher.");
- success = false;
- }
-
- if (route.Matchers != null && route.Matchers.Count(m => m is PathMatcher) > 1)
- {
- errorReporter.ReportError(ConfigErrors.ParsedRouteRuleMultiplePathMatchers, route.RouteId, $"Route '{route.RouteId}' rule has more than one 'Path()' matchers.");
- success = false;
- }
-
- if (route.Matchers != null && !ValidateAllMatchers(route.RouteId, route.Matchers, errorReporter))
- {
- success = false;
- }
+ success &= ValidateHost(route.Host, route.RouteId, errorReporter);
+ success &= ValidatePath(route.Path, route.RouteId, errorReporter);
+ success &= ValidateMethods(route.Methods, route.RouteId, errorReporter);
return success;
}
- private static bool ValidateAllMatchers(string routeId, IList matchers, IConfigErrorReporter errorReporter)
+ private static bool ValidateHost(string host, string routeId, IConfigErrorReporter errorReporter)
{
- var success = true;
-
- foreach (var matcher in matchers)
+ // TODO: Why is Host required? I'd only expect Host OR Path to be required, with Path being the more common usage.
+ if (string.IsNullOrEmpty(host))
{
- bool roundSuccess;
- string errorMessage;
-
- switch (matcher)
- {
- case HostMatcher hostMatcher:
- roundSuccess = ValidateHostMatcher(hostMatcher, out errorMessage);
- break;
- case PathMatcher pathMatcher:
- roundSuccess = ValidatePathMatcher(pathMatcher, out errorMessage);
- break;
- case MethodMatcher methodMatcher:
- roundSuccess = ValidateMethodMatcher(methodMatcher, out errorMessage);
- break;
- default:
- roundSuccess = false;
- errorMessage = "Unknown matcher";
- break;
- }
-
- if (!roundSuccess)
- {
- errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Invalid matcher '{matcher}'. {errorMessage}");
- success = false;
- }
+ errorReporter.ReportError(ConfigErrors.ParsedRouteRuleMissingHostMatcher, routeId, $"Route '{routeId}' is missing required field 'Host'.");
+ return false;
}
- return success;
- }
-
- private static bool ValidateHostMatcher(HostMatcher hostMatcher, out string errorMessage)
- {
- if (!_hostNameRegex.IsMatch(hostMatcher.Host))
+ if (!_hostNameRegex.IsMatch(host))
{
- errorMessage = $"Invalid host name '{hostMatcher.Host}'";
+ errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Invalid host name '{host}'");
return false;
}
- errorMessage = null;
return true;
}
- private static bool ValidatePathMatcher(PathMatcher pathMatcher, out string errorMessage)
+ private static bool ValidatePath(string path, string routeId, IConfigErrorReporter errorReporter)
{
+ // Path is optional
+ if (string.IsNullOrEmpty(path))
+ {
+ return true;
+ }
+
try
{
- RoutePatternFactory.Parse(pathMatcher.Pattern);
+ RoutePatternFactory.Parse(path);
}
catch (RoutePatternException ex)
{
- errorMessage = $"Invalid path pattern '{pathMatcher.Pattern}': {ex.Message}";
+ errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Invalid path pattern '{path}': {ex.Message}");
return false;
}
- errorMessage = null;
return true;
}
- private static bool ValidateMethodMatcher(MethodMatcher methodMatcher, out string errorMessage)
+ private static bool ValidateMethods(IReadOnlyList methods, string routeId, IConfigErrorReporter errorReporter)
{
+ // Methods are optional
+ if (methods == null)
+ {
+ return true;
+ }
+
var seenMethods = new HashSet(StringComparer.OrdinalIgnoreCase);
- foreach (var method in methodMatcher.Methods)
+ foreach (var method in methods)
{
if (!seenMethods.Add(method))
{
- errorMessage = $"Duplicate verb '{method}'";
+ errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Duplicate verb '{method}'");
return false;
}
if (!_validMethods.Contains(method))
{
- errorMessage = $"Unsupported verb '{method}'";
+ errorReporter.ReportError(ConfigErrors.ParsedRouteRuleInvalidMatcher, routeId, $"Unsupported verb '{method}'");
return false;
}
}
- errorMessage = null;
return true;
}
}
diff --git a/src/IslandGateway.Core/Service/Config/RuleParsing/RuleMatcherBase.cs b/src/IslandGateway.Core/Service/Config/RuleParsing/RuleMatcherBase.cs
deleted file mode 100644
index 2a419cfbd..000000000
--- a/src/IslandGateway.Core/Service/Config/RuleParsing/RuleMatcherBase.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System.Linq;
-using IslandGateway.Utilities;
-
-namespace IslandGateway.Core.Service
-{
- internal abstract class RuleMatcherBase
- {
- protected RuleMatcherBase(string name, string[] args)
- {
- Contracts.CheckNonEmpty(name, nameof(name));
- Contracts.CheckValue(args, nameof(args));
- Name = name;
- Args = args;
- }
-
- internal string Name { get; }
- internal string[] Args { get; }
-
- public override string ToString()
- {
- return $"{Name}({FormatArgs()})";
-
- string FormatArgs()
- {
- return string.Join(", ", Args.Select(FormatArg));
-
- static string FormatArg(string arg)
- {
- return $"'{arg.Replace("'", "''")}'";
- }
- }
- }
- }
-}
diff --git a/src/IslandGateway.Core/Service/Config/RuleParsing/RuleParser.cs b/src/IslandGateway.Core/Service/Config/RuleParsing/RuleParser.cs
deleted file mode 100644
index 40317e2e4..000000000
--- a/src/IslandGateway.Core/Service/Config/RuleParsing/RuleParser.cs
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System.Collections.Generic;
-using IslandGateway.Core.Abstractions;
-using IslandGateway.Utilities;
-using Superpower;
-using Superpower.Display;
-using Superpower.Model;
-using Superpower.Parsers;
-using Superpower.Tokenizers;
-
-namespace IslandGateway.Core.Service
-{
- ///
- /// Interface for a class that parses Core Gateway rules
- /// such as HostName('abc.example.com') && PathPrefix('/a/b')
- /// and produces the corresponding AST.
- ///
- internal class RuleParser : IRuleParser
- {
- internal enum RuleToken
- {
- None,
-
- [Token(Category = "identifier", Example = "Host, PathPrefix, Path, ...")]
- Identifier,
-
- [Token(Category = "parentheses", Example = "(")]
- LParen,
-
- [Token(Category = "parentheses", Example = ")")]
- RParen,
-
- [Token(Category = "string literal", Example = "'example ''with'' quotes'")]
- StringLiteral,
-
- [Token(Category = "comma", Example = ",")]
- Comma,
-
- [Token(Category = "operator", Example = "&&")]
- LogicalAnd,
-
- [Token(Category = "operator", Example = "||")]
- LogicalOr,
- }
-
- public Result, string> Parse(string rule)
- {
- Contracts.CheckValue(rule, nameof(rule));
-
- MatcherNode[] parsedNodes;
- try
- {
- var tokens = Lexer.Tokenize(rule);
- parsedNodes = Parser.Parse(tokens);
- }
- catch (ParseException ex)
- {
- return Result, string>.Failure($"Parse error: {ex.Message}");
- }
-
- var results = new List(parsedNodes.Length);
- foreach (var node in parsedNodes)
- {
- RuleMatcherBase matcher;
- switch (node.MatcherName.ToUpperInvariant())
- {
- case "HOST":
- if (node.Arguments.Length != 1)
- {
- return Result, string>.Failure($"'Host' matcher requires one argument, found {node.Arguments.Length}");
- }
- matcher = new HostMatcher("Host", node.Arguments);
- break;
- case "PATH":
- if (node.Arguments.Length != 1)
- {
- return Result, string>.Failure($"'Path' matcher requires one argument, found {node.Arguments.Length}");
- }
- matcher = new PathMatcher("Path", node.Arguments);
- break;
- case "METHOD":
- if (node.Arguments.Length < 1)
- {
- return Result, string>.Failure($"'Method' matcher requires at least one argument, found {node.Arguments.Length}");
- }
- matcher = new MethodMatcher("Method", node.Arguments);
- break;
- case "QUERY":
- case "HEADER":
- default:
- return Result, string>.Failure($"Unsupported matcher '{node.MatcherName}'");
- }
-
- results.Add(matcher);
- }
-
- return Result, string>.Success(results);
- }
-
- internal static class Lexer
- {
- private static readonly Tokenizer Tokenizer = new TokenizerBuilder()
- .Ignore(Span.WhiteSpace)
- .Match(Character.EqualTo('('), RuleToken.LParen)
- .Match(Character.EqualTo(')'), RuleToken.RParen)
- .Match(Character.EqualTo(','), RuleToken.Comma)
- .Match(Span.EqualTo("&&"), RuleToken.LogicalAnd)
- .Match(Span.EqualTo("||"), RuleToken.LogicalOr)
- .Match(Identifier.CStyle, RuleToken.Identifier)
- .Match(QuotedString.SqlStyle, RuleToken.StringLiteral)
- .Build();
-
- public static TokenList Tokenize(string input)
- {
- return Tokenizer.Tokenize(input);
- }
- }
-
- internal static class Parser
- {
- private static readonly TokenListParser SingleArgument =
- from stringLiteral in Token.EqualTo(RuleToken.StringLiteral).Apply(QuotedString.SqlStyle)
- select stringLiteral;
-
- private static readonly TokenListParser MatcherInvocation =
- from identifier in Token.EqualTo(RuleToken.Identifier)
- from open in Token.EqualTo(RuleToken.LParen)
- from arg in SingleArgument.ManyDelimitedBy(Token.EqualTo(RuleToken.Comma))
- from close in Token.EqualTo(RuleToken.RParen)
- select new MatcherNode { MatcherName = identifier.ToStringValue(), Arguments = arg };
-
- private static readonly TokenListParser MatcherInvocations =
- from matchers in MatcherInvocation.ManyDelimitedBy(Token.EqualTo(RuleToken.LogicalAnd)).AtEnd()
- select matchers;
-
- public static MatcherNode[] Parse(TokenList tokens)
- {
- return MatcherInvocations.Parse(tokens);
- }
- }
-
- internal class MatcherNode
- {
- public string MatcherName { get; set; }
- public string[] Arguments { get; set; }
- }
- }
-}
diff --git a/src/IslandGateway.Core/Service/ConfigModel/ParsedRoute.cs b/src/IslandGateway.Core/Service/ConfigModel/ParsedRoute.cs
index 3213d568b..99788b683 100644
--- a/src/IslandGateway.Core/Service/ConfigModel/ParsedRoute.cs
+++ b/src/IslandGateway.Core/Service/ConfigModel/ParsedRoute.cs
@@ -1,11 +1,13 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
+using System.Text;
using IslandGateway.Core.Service;
namespace IslandGateway.Core.ConfigModel
{
+ // TODO: Do we even need the ParsedRoute? It now matches the GatewayRoute 1:1
internal class ParsedRoute
{
///
@@ -14,15 +16,31 @@ internal class ParsedRoute
public string RouteId { get; set; }
///
- /// Gets or sets the original rule expression for this route.
+ /// Only match requests that use these optional HTTP methods. E.g. GET, POST.
///
- public string Rule { get; set; }
+ public IReadOnlyList Methods { get; set; }
///
- /// Gets or sets the parsed matchers for this route. This is computed
- /// from the original route's rule.
+ /// Only match requests with the given Host header.
///
- public IList Matchers { get; set; }
+ public string Host { get; set; }
+
+ ///
+ /// Only match requests with the given Path pattern.
+ ///
+ public string Path { get; set; }
+
+ // TODO:
+ ///
+ /// Only match requests that contain all of these query parameters.
+ ///
+ // public ICollection> QueryParameters { get; set; }
+
+ // TODO:
+ ///
+ /// Only match requests that contain all of these request headers.
+ ///
+ // public ICollection> Headers { get; set; }
///
/// Gets or sets the priority of this route.
@@ -40,5 +58,29 @@ internal class ParsedRoute
/// Arbitrary key-value pairs that further describe this route.
///
public IDictionary Metadata { get; set; }
+
+ internal string GetMatcherSummary()
+ {
+ var builder = new StringBuilder();
+
+ if (!string.IsNullOrEmpty(Host))
+ {
+ builder.AppendFormat("Host({0});", Host);
+ }
+
+ if (!string.IsNullOrEmpty(Path))
+ {
+ builder.AppendFormat("Path({0});", Path);
+ }
+
+ if (Methods != null && Methods.Count > 0)
+ {
+ builder.Append("Methods(");
+ builder.AppendJoin(',', Methods);
+ builder.Append(");");
+ }
+
+ return builder.ToString();
+ }
}
}
diff --git a/src/IslandGateway.Core/Service/DynamicEndpoints/RuntimeRouteBuilder.cs b/src/IslandGateway.Core/Service/DynamicEndpoints/RuntimeRouteBuilder.cs
index 77d0dbf83..0b89213b4 100644
--- a/src/IslandGateway.Core/Service/DynamicEndpoints/RuntimeRouteBuilder.cs
+++ b/src/IslandGateway.Core/Service/DynamicEndpoints/RuntimeRouteBuilder.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
@@ -39,23 +39,14 @@ public RouteConfig Build(ParsedRoute source, BackendInfo backendOrNull, RouteInf
var aspNetCoreEndpoints = new List(1);
var newRouteConfig = new RouteConfig(
route: runtimeRoute,
- rule: source.Rule,
+ matcherSummary: source.GetMatcherSummary(),
priority: source.Priority,
backendOrNull: backendOrNull,
aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly());
// TODO: Handle arbitrary AST's properly
- string pathPattern;
- var pathMatcher = (PathMatcher)source.Matchers?.FirstOrDefault(m => m is PathMatcher);
- if (pathMatcher != null)
- {
- pathPattern = pathMatcher.Pattern;
- }
- else
- {
- // Catch-all pattern when no matcher was specified
- pathPattern = "/{**catchall}";
- }
+ // Catch-all pattern when no path was specified
+ var pathPattern = string.IsNullOrEmpty(source.Path) ? "/{**catchall}" : source.Path;
// TODO: Propagate route priority
var endpointBuilder = new AspNetCore.Routing.RouteEndpointBuilder(
@@ -65,10 +56,14 @@ public RouteConfig Build(ParsedRoute source, BackendInfo backendOrNull, RouteInf
endpointBuilder.DisplayName = source.RouteId;
endpointBuilder.Metadata.Add(newRouteConfig);
- var hostMatcher = source.Matchers?.FirstOrDefault(m => m is HostMatcher) as HostMatcher;
- if (hostMatcher != null)
+ if (source.Host != null)
+ {
+ endpointBuilder.Metadata.Add(new AspNetCore.Routing.HostAttribute(source.Host));
+ }
+
+ if (source.Methods != null && source.Methods.Count > 0)
{
- endpointBuilder.Metadata.Add(new AspNetCore.Routing.HostAttribute(hostMatcher.Host));
+ endpointBuilder.Metadata.Add(new AspNetCore.Routing.HttpMethodMetadata(source.Methods));
}
var endpoint = endpointBuilder.Build();
diff --git a/src/IslandGateway.Core/Service/Management/IslandGatewayConfigManager.cs b/src/IslandGateway.Core/Service/Management/IslandGatewayConfigManager.cs
index 132bcf344..4ab0f180d 100644
--- a/src/IslandGateway.Core/Service/Management/IslandGatewayConfigManager.cs
+++ b/src/IslandGateway.Core/Service/Management/IslandGatewayConfigManager.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
@@ -177,9 +177,7 @@ private void UpdateRuntimeRoutes(DynamicConfigRoot config)
{
var currentRouteConfig = route.Config.Value;
if (currentRouteConfig == null ||
- currentRouteConfig.Rule != configRoute.Rule ||
- currentRouteConfig.Priority != configRoute.Priority ||
- currentRouteConfig.BackendOrNull != backendOrNull)
+ currentRouteConfig.HasConfigChanged(configRoute, backendOrNull))
{
// Config changed, so update runtime route
changed = true;
diff --git a/src/IslandGateway.Core/Service/RuntimeModel/RouteConfig.cs b/src/IslandGateway.Core/Service/RuntimeModel/RouteConfig.cs
index 817bd66a2..42cc83bab 100644
--- a/src/IslandGateway.Core/Service/RuntimeModel/RouteConfig.cs
+++ b/src/IslandGateway.Core/Service/RuntimeModel/RouteConfig.cs
@@ -1,7 +1,9 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
+using IslandGateway.Core.ConfigModel;
+using IslandGateway.Core.Service;
using IslandGateway.Utilities;
using AspNetCore = Microsoft.AspNetCore;
@@ -15,13 +17,13 @@ namespace IslandGateway.Core.RuntimeModel
///
/// All members must remain immutable to avoid thread safety issues.
/// Instead, instances of are replaced
- /// in ther entirety when values need to change.
+ /// in their entirety when values need to change.
///
internal sealed class RouteConfig
{
public RouteConfig(
RouteInfo route,
- string rule,
+ string matcherSummary,
int? priority,
BackendInfo backendOrNull,
IReadOnlyList aspNetCoreEndpoints)
@@ -30,7 +32,7 @@ public RouteConfig(
Contracts.CheckValue(aspNetCoreEndpoints, nameof(aspNetCoreEndpoints));
Route = route;
- Rule = rule;
+ MatcherSummary = matcherSummary;
Priority = priority;
BackendOrNull = backendOrNull;
AspNetCoreEndpoints = aspNetCoreEndpoints;
@@ -38,12 +40,19 @@ public RouteConfig(
public RouteInfo Route { get; }
- public string Rule { get; }
+ internal string MatcherSummary{ get; }
public int? Priority { get; }
public BackendInfo BackendOrNull { get; }
public IReadOnlyList AspNetCoreEndpoints { get; }
+
+ public bool HasConfigChanged(ParsedRoute newConfig, BackendInfo backendOrNull)
+ {
+ return Priority != newConfig.Priority
+ || BackendOrNull != backendOrNull
+ || !MatcherSummary.Equals(newConfig.GetMatcherSummary());
+ }
}
}
diff --git a/test/IslandGateway.Core.Tests/Abstractions/RouteDiscovery/Contract/GatewayRouteTests.cs b/test/IslandGateway.Core.Tests/Abstractions/RouteDiscovery/Contract/GatewayRouteTests.cs
index c3299afaf..35f643da1 100644
--- a/test/IslandGateway.Core.Tests/Abstractions/RouteDiscovery/Contract/GatewayRouteTests.cs
+++ b/test/IslandGateway.Core.Tests/Abstractions/RouteDiscovery/Contract/GatewayRouteTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
@@ -22,7 +22,12 @@ public void DeepClone_Works()
var sut = new GatewayRoute
{
RouteId = "route1",
- Rule = "Host('example.com')",
+ Match =
+ {
+ Methods = new[] { "GET", "POST" },
+ Host = "example.com",
+ Path = "/",
+ },
Priority = 2,
BackendId = "backend1",
Metadata = new Dictionary
@@ -37,7 +42,11 @@ public void DeepClone_Works()
// Assert
clone.Should().NotBeSameAs(sut);
clone.RouteId.Should().Be(sut.RouteId);
- clone.Rule.Should().Be(sut.Rule);
+ clone.Match.Should().NotBeSameAs(sut.Match);
+ clone.Match.Methods.Should().NotBeSameAs(sut.Match.Methods);
+ clone.Match.Methods.Should().BeEquivalentTo(sut.Match.Methods);
+ clone.Match.Host.Should().Be(sut.Match.Host);
+ clone.Match.Path.Should().Be(sut.Match.Path);
clone.Priority.Should().Be(sut.Priority);
clone.BackendId.Should().Be(sut.BackendId);
clone.Metadata.Should().NotBeNull();
@@ -57,7 +66,9 @@ public void DeepClone_Nulls_Works()
// Assert
clone.Should().NotBeSameAs(sut);
clone.RouteId.Should().BeNull();
- clone.Rule.Should().BeNull();
+ clone.Match.Methods.Should().BeNull();
+ clone.Match.Host.Should().BeNull();
+ clone.Match.Path.Should().BeNull();
clone.Priority.Should().BeNull();
clone.BackendId.Should().BeNull();
clone.Metadata.Should().BeNull();
diff --git a/test/IslandGateway.Core.Tests/Service/Config/DynamicConfigBuilderTests.cs b/test/IslandGateway.Core.Tests/Service/Config/DynamicConfigBuilderTests.cs
index 637936d47..ad787a65a 100644
--- a/test/IslandGateway.Core.Tests/Service/Config/DynamicConfigBuilderTests.cs
+++ b/test/IslandGateway.Core.Tests/Service/Config/DynamicConfigBuilderTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
@@ -107,18 +107,13 @@ public async Task BuildConfigAsync_ValidRoute_Works()
.Setup(r => r.GetBackendsAsync(It.IsAny()))
.ReturnsAsync(new List());
- var route1 = new GatewayRoute { RouteId = "route1", Rule = "Host('example.com')", Priority = 1, BackendId = "backend1" };
+ var route1 = new GatewayRoute { RouteId = "route1", Match = { Host = "example.com" }, Priority = 1, BackendId = "backend1" };
Mock()
.Setup(r => r.GetRoutesAsync(It.IsAny()))
.ReturnsAsync(new[] { route1 });
- var parsedRoute1 = new ParsedRoute();
- Mock()
- .Setup(r => r.ParseRoute(route1, errorReporter))
- .Returns(Result.Success(parsedRoute1));
-
Mock()
- .Setup(r => r.ValidateRoute(parsedRoute1, errorReporter))
+ .Setup(r => r.ValidateRoute(It.IsAny(), errorReporter))
.Returns(true);
// Act
@@ -131,7 +126,7 @@ public async Task BuildConfigAsync_ValidRoute_Works()
result.Value.Should().NotBeNull();
result.Value.Backends.Should().BeEmpty();
result.Value.Routes.Should().HaveCount(1);
- result.Value.Routes[0].Should().BeSameAs(parsedRoute1);
+ result.Value.Routes[0].RouteId.Should().BeSameAs(route1.RouteId);
}
[Fact]
@@ -143,15 +138,12 @@ public async Task BuildConfigAsync_RouteParseError_SkipsRoute()
.Setup(r => r.GetBackendsAsync(It.IsAny()))
.ReturnsAsync(new List());
- var route1 = new GatewayRoute { RouteId = "route1", Rule = "Host('example.com')", Priority = 1, BackendId = "backend1" };
+ var route1 = new GatewayRoute { RouteId = "route1", Match = { Host = "example.com" }, Priority = 1, BackendId = "backend1" };
Mock()
.Setup(r => r.GetRoutesAsync(It.IsAny()))
.ReturnsAsync(new[] { route1 });
var parsedRoute1 = new ParsedRoute();
- Mock()
- .Setup(r => r.ParseRoute(route1, errorReporter))
- .Returns(Result.Failure());
// Act
var configManager = Create();
@@ -173,15 +165,12 @@ public async Task BuildConfigAsync_RouteValidationError_SkipsRoute()
.Setup(r => r.GetBackendsAsync(It.IsAny()))
.ReturnsAsync(new List());
- var route1 = new GatewayRoute { RouteId = "route1", Rule = "Host('example.com')", Priority = 1, BackendId = "backend1" };
+ var route1 = new GatewayRoute { RouteId = "route1", Match = { Host = "example.com" }, Priority = 1, BackendId = "backend1" };
Mock()
.Setup(r => r.GetRoutesAsync(It.IsAny()))
.ReturnsAsync(new[] { route1 });
var parsedRoute1 = new ParsedRoute();
- Mock()
- .Setup(r => r.ParseRoute(route1, errorReporter))
- .Returns(Result.Success(parsedRoute1));
Mock()
.Setup(r => r.ValidateRoute(parsedRoute1, errorReporter))
diff --git a/test/IslandGateway.Core.Tests/Service/Config/RouteParserTests.cs b/test/IslandGateway.Core.Tests/Service/Config/RouteParserTests.cs
deleted file mode 100644
index e072921cc..000000000
--- a/test/IslandGateway.Core.Tests/Service/Config/RouteParserTests.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System.Collections.Generic;
-using FluentAssertions;
-using IslandGateway.Core.Abstractions;
-using Tests.Common;
-using Xunit;
-
-namespace IslandGateway.Core.Service.Tests
-{
- public class RouteParserTests : TestAutoMockBase
- {
- [Fact]
- public void Constructor_Works()
- {
- Create();
- }
-
- [Fact]
- public void ParseRoute_ValidRoute_Works()
- {
- // Arrange
- const string TestRouteId = "route1";
- const string TestBackendId = "backend1";
- const string TestRule = "Host('example.com')";
- const int TestPriority = 2;
-
- var matchers = new[] { new HostMatcher("Host", new[] { "example.com" }) };
- var ruleParseResult = Result, string>.Success(matchers);
- Mock()
- .Setup(r => r.Parse(TestRule))
- .Returns(ruleParseResult);
- var routeParser = Create();
- var route = new GatewayRoute
- {
- RouteId = TestRouteId,
- Rule = TestRule,
- Priority = TestPriority,
- BackendId = TestBackendId,
- Metadata = new Dictionary
- {
- { "key", "value" },
- },
- };
- var errorReporter = new TestConfigErrorReporter();
-
- // Act
- var result = routeParser.ParseRoute(route, errorReporter);
-
- // Assert
- result.IsSuccess.Should().BeTrue();
- result.Value.RouteId.Should().Be(TestRouteId);
- result.Value.Rule.Should().Be(TestRule);
- result.Value.Priority.Should().Be(TestPriority);
- result.Value.Matchers.Should().BeSameAs(matchers);
- result.Value.BackendId.Should().Be(TestBackendId);
- result.Value.Metadata["key"].Should().Be("value");
- errorReporter.Errors.Should().BeEmpty();
- }
-
- [Fact]
- public void ParseRoute_NullRule_ReportsError()
- {
- // Arrange
- var route = new GatewayRoute { RouteId = null };
- var errorReporter = new TestConfigErrorReporter();
- var routeParser = Create();
-
- // Act
- var result = routeParser.ParseRoute(route, errorReporter);
-
- // Assert
- result.IsSuccess.Should().BeFalse();
- errorReporter.Errors.Should().Contain(err => err.ErrorCode == ConfigErrors.RouteBadRule);
- }
-
- [Fact]
- public void ParseRoute_ParseError_ReportsError()
- {
- // Arrange
- const string TestRule = "bad rule";
- const string TestParserErrorMessage = "parser error message";
-
- var ruleParseResult = Result, string>.Failure(TestParserErrorMessage);
- Mock()
- .Setup(r => r.Parse(TestRule))
- .Returns(ruleParseResult);
- var route = new GatewayRoute
- {
- RouteId = null,
- Rule = TestRule,
- };
- var errorReporter = new TestConfigErrorReporter();
- var routeParser = Create();
-
- // Act
- var result = routeParser.ParseRoute(route, errorReporter);
-
- // Assert
- result.IsSuccess.Should().BeFalse();
- errorReporter.Errors.Should().Contain(err => err.ErrorCode == ConfigErrors.RouteBadRule && err.Message.Contains(TestParserErrorMessage));
- }
- }
-}
diff --git a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/Matchers/HostMatcherTests.cs b/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/Matchers/HostMatcherTests.cs
deleted file mode 100644
index c0a033f30..000000000
--- a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/Matchers/HostMatcherTests.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System;
-using FluentAssertions;
-using Xunit;
-
-namespace IslandGateway.Core.Service.Tests
-{
- public class HostMatcherTests
- {
- [Fact]
- public void Constructor_Works()
- {
- // arrange
- const string TestHost = "example.com";
-
- // Act
- var matcher = new HostMatcher("Host", new[] { TestHost });
-
- // Assert
- matcher.Host.Should().Be(TestHost);
- }
-
- [Fact]
- public void Constructor_InvalidArgCount_Throws()
- {
- // Arrange
- Action action1 = () => new HostMatcher("Host", new string[0]);
- Action action2 = () => new HostMatcher("Host", new[] { "a", "b" });
-
- // Act & Assert
- action1.Should().ThrowExactly();
- action2.Should().ThrowExactly();
- }
-
- [Theory]
- [InlineData(null)]
- [InlineData("")]
- public void Constructor_EmptyHostName_Throws(string hostName)
- {
- // Arrange
- Action action = () => new HostMatcher("Host", new[] { hostName });
-
- // Act & Assert
- action.Should().ThrowExactly();
- }
- }
-}
diff --git a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/Matchers/MethodMatcherTests.cs b/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/Matchers/MethodMatcherTests.cs
deleted file mode 100644
index eb8e6bd94..000000000
--- a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/Matchers/MethodMatcherTests.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System;
-using FluentAssertions;
-using Xunit;
-
-namespace IslandGateway.Core.Service.Tests
-{
- public class MethodMatcherTests
- {
- [Fact]
- public void Constructor_Works()
- {
- // arrange
- var methods = new[] { "GET" };
-
- // Act
- var matcher = new MethodMatcher("Method", methods);
-
- // Assert
- matcher.Methods.Should().BeSameAs(methods);
- }
-
- [Fact]
- public void Constructor_InvalidArgCount_Throws()
- {
- // Arrange
- Action action = () => new MethodMatcher("Method", new string[0]);
-
- // Act & Assert
- action.Should().ThrowExactly();
- }
- }
-}
diff --git a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/Matchers/PathMatcherTests.cs b/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/Matchers/PathMatcherTests.cs
deleted file mode 100644
index edd01fc8f..000000000
--- a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/Matchers/PathMatcherTests.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System;
-using FluentAssertions;
-using Xunit;
-
-namespace IslandGateway.Core.Service.Tests
-{
- public class PathMatcherTests
- {
- [Fact]
- public void Constructor_Works()
- {
- // arrange
- const string TestPattern = "/a";
-
- // Act
- var matcher = new PathMatcher("Path", new[] { TestPattern });
-
- // Assert
- matcher.Pattern.Should().Be(TestPattern);
- }
-
- [Fact]
- public void Constructor_InvalidArgCount_Throws()
- {
- // Arrange
- Action action1 = () => new PathMatcher("Path", new string[0]);
- Action action2 = () => new PathMatcher("Path", new[] { "a", "b" });
-
- // Act & Assert
- action1.Should().ThrowExactly();
- action2.Should().ThrowExactly();
- }
-
- [Theory]
- [InlineData(null)]
- [InlineData("")]
- public void Constructor_EmptyHostName_Throws(string hostName)
- {
- // Arrange
- Action action = () => new PathMatcher("Path", new[] { hostName });
-
- // Act & Assert
- action.Should().ThrowExactly();
- }
- }
-}
diff --git a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/RouteValidatorTests.cs b/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/RouteValidatorTests.cs
index a1a76224d..17f88e8ee 100644
--- a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/RouteValidatorTests.cs
+++ b/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/RouteValidatorTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using FluentAssertions;
@@ -11,14 +11,6 @@ namespace IslandGateway.Core.Service.Tests
{
public class RouteValidatorTests : TestAutoMockBase
{
- private readonly RouteParser _routeParser;
-
- public RouteValidatorTests()
- {
- Provide();
- _routeParser = Create();
- }
-
[Fact]
public void Constructor_Works()
{
@@ -26,23 +18,24 @@ public void Constructor_Works()
}
[Theory]
- [InlineData("Host('example.com') && Path('/a/')")]
- [InlineData("Host('example.com') && Path('/a/**')")]
- [InlineData("Path('/a/**') && Host('example.com')")]
- [InlineData("Path('/a/**') && Host('example.com') && Method('GET')")]
- [InlineData("Host('example.com') && Method('get')")]
- [InlineData("Host('example.com') && Method('gEt', 'put')")]
- [InlineData("Host('example.com') && Method('gEt', 'put', 'POST', 'traCE', 'PATCH', 'DELETE', 'HEAd')")]
- [InlineData("Host('*.example.com')")]
- [InlineData("Host('a-b.example.com')")]
- [InlineData("Host('a-b.b-c.example.com')")]
- public void Accepts_ValidRules(string rule)
+ [InlineData("example.com", "/a/", null)]
+ [InlineData("example.com", "/a/**", null)]
+ [InlineData("example.com", "/a/**", "GET")]
+ [InlineData("example.com", null, "get")]
+ [InlineData("example.com", null, "gEt,put")]
+ [InlineData("example.com", null, "gEt,put,POST,traCE,PATCH,DELETE,Head")]
+ [InlineData("*.example.com", null, null)]
+ [InlineData("a-b.example.com", null, null)]
+ [InlineData("a-b.b-c.example.com", null, null)]
+ public void Accepts_ValidRules(string host, string path, string methods)
{
// Arrange
- var route = new GatewayRoute
+ var route = new ParsedRoute
{
RouteId = "route1",
- Rule = rule,
+ Host = host,
+ Path = path,
+ Methods = methods?.Split(","),
BackendId = "be1",
};
@@ -73,15 +66,15 @@ public void Rejects_MissingRouteId(string routeId)
}
[Theory]
- [InlineData("")]
- [InlineData("Method('GET')")]
- public void Rejects_MissingHost(string rule)
+ [InlineData(null)]
+ [InlineData("/")]
+ public void Rejects_MissingHost(string path)
{
// Arrange
- var route = new GatewayRoute
+ var route = new ParsedRoute
{
RouteId = "route1",
- Rule = rule,
+ Path = path,
BackendId = "be1",
};
@@ -94,24 +87,24 @@ public void Rejects_MissingHost(string rule)
}
[Theory]
- [InlineData("Host('.example.com')")]
- [InlineData("Host('example*.com')")]
- [InlineData("Host('example.*.com')")]
- [InlineData("Host('example.*a.com')")]
- [InlineData("Host('*example.com')")]
- [InlineData("Host('-example.com')")]
- [InlineData("Host('example-.com')")]
- [InlineData("Host('-example-.com')")]
- [InlineData("Host('a.-example.com')")]
- [InlineData("Host('a.example-.com')")]
- [InlineData("Host('a.-example-.com')")]
- public void Rejects_InvalidHost(string rule)
+ [InlineData(".example.com")]
+ [InlineData("example*.com")]
+ [InlineData("example.*.com")]
+ [InlineData("example.*a.com")]
+ [InlineData("*example.com")]
+ [InlineData("-example.com")]
+ [InlineData("example-.com")]
+ [InlineData("-example-.com")]
+ [InlineData("a.-example.com")]
+ [InlineData("a.example-.com")]
+ [InlineData("a.-example-.com")]
+ public void Rejects_InvalidHost(string host)
{
// Arrange
- var route = new GatewayRoute
+ var route = new ParsedRoute
{
RouteId = "route1",
- Rule = rule,
+ Host = host,
BackendId = "be1",
};
@@ -124,17 +117,18 @@ public void Rejects_InvalidHost(string rule)
}
[Theory]
- [InlineData("Host('example.com') && Path('/{***a}')")]
- [InlineData("Host('example.com') && Path('/{')")]
- [InlineData("Host('example.com') && Path('/}')")]
- [InlineData("Host('example.com') && Path('/{ab/c}')")]
- public void Rejects_InvalidPath(string rule)
+ [InlineData("/{***a}")]
+ [InlineData("/{")]
+ [InlineData("/}")]
+ [InlineData("/{ab/c}")]
+ public void Rejects_InvalidPath(string path)
{
// Arrange
- var route = new GatewayRoute
+ var route = new ParsedRoute
{
RouteId = "route1",
- Rule = rule,
+ Host = "example.com",
+ Path = path,
BackendId = "be1",
};
@@ -147,16 +141,17 @@ public void Rejects_InvalidPath(string rule)
}
[Theory]
- [InlineData("Host('example.com') && Method('')")]
- [InlineData("Host('example.com') && Method('gett')")]
- [InlineData("Host('example.com') && Method('get', 'post', 'get')")]
- public void Rejects_InvalidMethod(string rule)
+ [InlineData("")]
+ [InlineData("gett")]
+ [InlineData("get,post,get")]
+ public void Rejects_InvalidMethod(string methods)
{
// Arrange
- var route = new GatewayRoute
+ var route = new ParsedRoute
{
RouteId = "route1",
- Rule = rule,
+ Host = "example.com",
+ Methods = methods.Split(","),
BackendId = "be1",
};
@@ -168,50 +163,9 @@ public void Rejects_InvalidMethod(string rule)
result.ErrorReporter.Errors.Should().Contain(err => err.ErrorCode == ConfigErrors.ParsedRouteRuleInvalidMatcher && err.Message.Contains("verb"));
}
- [Fact]
- public void Rejects_MultipleHosts()
- {
- // Arrange
- var route = new GatewayRoute
- {
- RouteId = "route1",
- Rule = "Host('example.com') && Host('example.com')",
- BackendId = "be1",
- };
-
- // Act
- var result = RunScenario(route);
-
- // Assert
- result.IsSuccess.Should().BeFalse();
- result.ErrorReporter.Errors.Should().Contain(err => err.ErrorCode == ConfigErrors.ParsedRouteRuleMultipleHostMatchers);
- }
-
- [Fact]
- public void Rejects_MultiplePaths()
- {
- // Arrange
- var route = new GatewayRoute
- {
- RouteId = "route1",
- Rule = "Path('/a') && Path('/a')",
- BackendId = "be1",
- };
-
- // Act
- var result = RunScenario(route);
-
- // Assert
- result.IsSuccess.Should().BeFalse();
- result.ErrorReporter.Errors.Should().Contain(err => err.ErrorCode == ConfigErrors.ParsedRouteRuleMultiplePathMatchers);
- }
-
- private (bool IsSuccess, TestConfigErrorReporter ErrorReporter) RunScenario(GatewayRoute route)
+ private (bool IsSuccess, TestConfigErrorReporter ErrorReporter) RunScenario(ParsedRoute parsedRoute)
{
var errorReporter = new TestConfigErrorReporter();
- var parseResult = _routeParser.ParseRoute(route, errorReporter);
- parseResult.IsSuccess.Should().BeTrue();
- var parsedRoute = parseResult.Value;
var validator = Create();
var isSuccess = validator.ValidateRoute(parsedRoute, errorReporter);
diff --git a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/RuleParserTests.cs b/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/RuleParserTests.cs
deleted file mode 100644
index ee031b3f9..000000000
--- a/test/IslandGateway.Core.Tests/Service/Config/RuleParsing/RuleParserTests.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System;
-using System.Collections.Generic;
-using FluentAssertions;
-using IslandGateway.Core.Abstractions;
-using Tests.Common;
-using Xunit;
-
-namespace IslandGateway.Core.Service.Tests
-{
- public class RuleParserTests : TestAutoMockBase
- {
- [Fact]
- public void Constructor_Works()
- {
- Create();
- }
-
- [Fact]
- public void NullInput_ThrowsArgumentNullException()
- {
- // Arrange
- var parser = Create();
-
- // Act & Assert
- Action action = () => parser.Parse(null);
- action.Should().ThrowExactly();
- }
-
- [Theory]
- [InlineData("Host('example.com')")]
- [InlineData("Host('example.com') ", "Host('example.com')")]
- [InlineData(" Host('example.com')", "Host('example.com')")]
- [InlineData("Host ( 'example.com' )", "Host('example.com')")]
- [InlineData("Host('example.com') && Path('/a')")]
- [InlineData("Host('example.com') && Method('get')")]
- [InlineData("Host('example.com') && Method('get', 'post')")]
- [InlineData("Host('example.com')&&Path('/a') ", "Host('example.com') && Path('/a')")]
- [InlineData("Host('*.example.com') && Path('/a''')")]
- public void ValidRule_Works(string input, string expectedOutputIfDifferent = null)
- {
- // Arrange
- var parser = Create();
-
- // Act
- var output = parser.Parse(input);
-
- // Assert
- output.IsSuccess.Should().BeTrue();
- Serialize(output).Should().Be(expectedOutputIfDifferent ?? input);
- }
-
- [Theory]
- [InlineData("Host(\"example.com\")", "Parse error: Syntax error (line 1, column 6): unexpected `\"`.")]
- [InlineData("Host(@'example.com')", "Parse error: Syntax error (line 1, column 6): unexpected `@`.")]
- [InlineData("&& Host('example.com')", "Parse error: Syntax error (line 1, column 1): unexpected operator `&&`.")]
- [InlineData("Host('example.com')&&", "Parse error: Syntax error: unexpected end of input, expected `Host, PathPrefix, Path, ...`.")]
- [InlineData("Host(a)", "Parse error: Syntax error (line 1, column 6): unexpected identifier `a`, expected `)`.")]
- [InlineData("Host(example.com)", "Parse error: Syntax error (line 1, column 13): unexpected `.`.")]
- [InlineData("Host('example.com' && PathPrefix('/a'))", "Parse error: Syntax error (line 1, column 20): unexpected operator `&&`, expected `)`.")]
- public void InvalidRuleSyntax_ProducesGoodErrorMessage(string input, string expectedOutput)
- {
- // Arrange
- var parser = Create();
-
- // Act
- var output = parser.Parse(input);
-
- // Assert
- output.IsSuccess.Should().BeFalse();
- Serialize(output).Should().Be(expectedOutput ?? input);
- }
-
- [Theory]
- [InlineData("Host()", "'Host' matcher requires one argument, found 0")]
- [InlineData("Path()", "'Path' matcher requires one argument, found 0")]
- [InlineData("Method()", "'Method' matcher requires at least one argument, found 0")]
- [InlineData("Host('a','b')", "'Host' matcher requires one argument, found 2")]
- [InlineData("Path('a','b')", "'Path' matcher requires one argument, found 2")]
- [InlineData("Host('a','b', 'c' )", "'Host' matcher requires one argument, found 3")]
- public void InvalidRuleSemantics_ProducesGoodErrorMessage(string input, string expectedOutput)
- {
- // Arrange
- var parser = Create();
-
- // Act
- var output = parser.Parse(input);
-
- // Assert
- output.IsSuccess.Should().BeFalse();
- Serialize(output).Should().Be(expectedOutput ?? input);
- }
-
- private static string Serialize(Result, string> parsed)
- {
- if (!parsed.IsSuccess)
- {
- return parsed.Error;
- }
-
- return string.Join(" && ", parsed.Value);
- }
- }
-}
diff --git a/test/IslandGateway.Core.Tests/Service/DynamicEndpoints/RuntimeRouteBuilderTests.cs b/test/IslandGateway.Core.Tests/Service/DynamicEndpoints/RuntimeRouteBuilderTests.cs
index c768e3510..a74c94c58 100644
--- a/test/IslandGateway.Core.Tests/Service/DynamicEndpoints/RuntimeRouteBuilderTests.cs
+++ b/test/IslandGateway.Core.Tests/Service/DynamicEndpoints/RuntimeRouteBuilderTests.cs
@@ -1,9 +1,10 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using FluentAssertions;
+using FluentAssertions.Common;
using IslandGateway.Core.ConfigModel;
using IslandGateway.Core.RuntimeModel;
using IslandGateway.Core.Service.Management;
@@ -31,12 +32,8 @@ public void BuildEndpoints_HostAndPath_Works()
var parsedRoute = new ParsedRoute
{
RouteId = "route1",
- Rule = "Host('example.com') && Path('/a')",
- Matchers = new List
- {
- new HostMatcher("Host", new[] { "example.com" }),
- new PathMatcher("Path", new[] { "/a" }),
- },
+ Host = "example.com",
+ Path = "/a",
Priority = 12,
};
var backend = new BackendInfo("backend1", new EndpointManager(), new Mock().Object);
@@ -48,7 +45,7 @@ public void BuildEndpoints_HostAndPath_Works()
// Assert
config.BackendOrNull.Should().BeSameAs(backend);
config.Priority.Should().Be(12);
- config.Rule.Should().Be("Host('example.com') && Path('/a')");
+ config.MatcherSummary.Should().Be(parsedRoute.GetMatcherSummary());
config.AspNetCoreEndpoints.Should().HaveCount(1);
var routeEndpoint = config.AspNetCoreEndpoints[0] as AspNetCore.Routing.RouteEndpoint;
routeEndpoint.DisplayName.Should().Be("route1");
@@ -68,11 +65,7 @@ public void BuildEndpoints_JustHost_Works()
var parsedRoute = new ParsedRoute
{
RouteId = "route1",
- Rule = "Host('example.com')",
- Matchers = new List
- {
- new HostMatcher("Host", new[] { "example.com" }),
- },
+ Host = "example.com",
Priority = 12,
};
var backend = new BackendInfo("backend1", new EndpointManager(), new Mock().Object);
@@ -84,7 +77,7 @@ public void BuildEndpoints_JustHost_Works()
// Assert
config.BackendOrNull.Should().BeSameAs(backend);
config.Priority.Should().Be(12);
- config.Rule.Should().Be("Host('example.com')");
+ config.MatcherSummary.Should().Be(parsedRoute.GetMatcherSummary());
config.AspNetCoreEndpoints.Should().HaveCount(1);
var routeEndpoint = config.AspNetCoreEndpoints[0] as AspNetCore.Routing.RouteEndpoint;
routeEndpoint.DisplayName.Should().Be("route1");
@@ -104,11 +97,7 @@ public void BuildEndpoints_JustHostWithWildcard_Works()
var parsedRoute = new ParsedRoute
{
RouteId = "route1",
- Rule = "Host('*.example.com')",
- Matchers = new List
- {
- new HostMatcher("Host", new[] { "*.example.com" }),
- },
+ Host = "*.example.com",
Priority = 12,
};
var backend = new BackendInfo("backend1", new EndpointManager(), new Mock().Object);
@@ -120,7 +109,7 @@ public void BuildEndpoints_JustHostWithWildcard_Works()
// Assert
config.BackendOrNull.Should().BeSameAs(backend);
config.Priority.Should().Be(12);
- config.Rule.Should().Be("Host('*.example.com')");
+ config.MatcherSummary.Should().Be(parsedRoute.GetMatcherSummary());
config.AspNetCoreEndpoints.Should().HaveCount(1);
var routeEndpoint = config.AspNetCoreEndpoints[0] as AspNetCore.Routing.RouteEndpoint;
routeEndpoint.DisplayName.Should().Be("route1");
@@ -140,11 +129,7 @@ public void BuildEndpoints_JustPath_Works()
var parsedRoute = new ParsedRoute
{
RouteId = "route1",
- Rule = "Path('/a')",
- Matchers = new List
- {
- new PathMatcher("Path", new[] { "/a" }),
- },
+ Path = "/a",
Priority = 12,
};
var backend = new BackendInfo("backend1", new EndpointManager(), new Mock().Object);
@@ -156,7 +141,7 @@ public void BuildEndpoints_JustPath_Works()
// Assert
config.BackendOrNull.Should().BeSameAs(backend);
config.Priority.Should().Be(12);
- config.Rule.Should().Be("Path('/a')");
+ config.MatcherSummary.Should().Be(parsedRoute.GetMatcherSummary());
config.AspNetCoreEndpoints.Should().HaveCount(1);
var routeEndpoint = config.AspNetCoreEndpoints[0] as AspNetCore.Routing.RouteEndpoint;
routeEndpoint.DisplayName.Should().Be("route1");
@@ -175,7 +160,6 @@ public void BuildEndpoints_NullMatchers_Works()
var parsedRoute = new ParsedRoute
{
RouteId = "route1",
- Rule = "Host('example.com')",
Priority = 12,
};
var backend = new BackendInfo("backend1", new EndpointManager(), new Mock().Object);
@@ -187,7 +171,7 @@ public void BuildEndpoints_NullMatchers_Works()
// Assert
config.BackendOrNull.Should().BeSameAs(backend);
config.Priority.Should().Be(12);
- config.Rule.Should().Be("Host('example.com')");
+ config.MatcherSummary.Should().Be("");
config.AspNetCoreEndpoints.Should().HaveCount(1);
var routeEndpoint = config.AspNetCoreEndpoints[0] as AspNetCore.Routing.RouteEndpoint;
routeEndpoint.DisplayName.Should().Be("route1");
@@ -206,11 +190,7 @@ public void BuildEndpoints_InvalidPath_BubblesOutException()
var parsedRoute = new ParsedRoute
{
RouteId = "route1",
- Rule = "Path('/{invalid')",
- Matchers = new List
- {
- new PathMatcher("Path", new[] { "/{invalid" }),
- },
+ Path = "/{invalid",
Priority = 12,
};
var backend = new BackendInfo("backend1", new EndpointManager(), new Mock().Object);
diff --git a/test/IslandGateway.Core.Tests/Service/Proxy/ProxyInvokerTests.cs b/test/IslandGateway.Core.Tests/Service/Proxy/ProxyInvokerTests.cs
index 27b5fb210..11b4bea8f 100644
--- a/test/IslandGateway.Core.Tests/Service/Proxy/ProxyInvokerTests.cs
+++ b/test/IslandGateway.Core.Tests/Service/Proxy/ProxyInvokerTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
@@ -62,7 +62,7 @@ public async Task InvokeAsync_Works()
var aspNetCoreEndpoints = new List();
var routeConfig = new RouteConfig(
route: new RouteInfo("route1"),
- rule: "Host('example.com') && Path('/')",
+ matcherSummary: null,
priority: null,
backendOrNull: backend1,
aspNetCoreEndpoints: aspNetCoreEndpoints.AsReadOnly());