method to match urls and template urls

This commit is contained in:
Tom Gardham-Pallister 2016-07-08 19:33:22 +01:00
parent 7332da7230
commit 4f2d94ceba
21 changed files with 718 additions and 96 deletions

View File

@ -1,10 +0,0 @@
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router
{
public interface IRouterService
{
Response AddRoute(string apiKey, string upstreamApiBaseUrl);
Response<Route> GetRoute(string apiKey);
}
}

View File

@ -1,41 +0,0 @@
using System;
using System.Collections.Generic;
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router
{
public class InMemoryRouterService : IRouterService
{
private readonly Dictionary<string, string> _routes;
public InMemoryRouterService()
{
_routes = new Dictionary<string,string>();
}
public Response AddRoute(string apiKey, string upstreamApiBaseUrl)
{
if(_routes.ContainsKey(apiKey))
{
return new ErrorResponse(new List<Error>(){new RouteKeyAlreadyExists("This key has already been used")});
}
_routes.Add(apiKey, upstreamApiBaseUrl);
return new OkResponse();
}
public Response<Route> GetRoute(string apiKey)
{
Console.WriteLine("looking for {0}", apiKey);
string upstreamApiBaseUrl = null;
if(_routes.TryGetValue(apiKey, out upstreamApiBaseUrl))
{
return new OkResponse<Route>(new Route(apiKey, upstreamApiBaseUrl));
}
Console.WriteLine("Couldnt find it");
return new ErrorResponse<Route>(new List<Error>(){new RouteKeyDoesNotExist("This key does not exist")});
}
}
}

View File

@ -1,14 +0,0 @@
namespace Ocelot.Library.Infrastructure.Router
{
public class Route
{
public Route(string apiKey, string upstreamRoute)
{
ApiKey = apiKey;
UpstreamRoute = upstreamRoute;
}
public string ApiKey {get;private set;}
public string UpstreamRoute {get;private set;}
}
}

View File

@ -1,12 +0,0 @@
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router
{
public class RouteKeyAlreadyExists : Error
{
public RouteKeyAlreadyExists(string message)
: base(message)
{
}
}
}

View File

@ -1,12 +0,0 @@
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router
{
public class RouteKeyDoesNotExist : Error
{
public RouteKeyDoesNotExist(string message)
: base(message)
{
}
}
}

View File

@ -0,0 +1,10 @@
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router.UpstreamRouter
{
public interface IUpstreamRouter
{
Response AddRoute(string downstreamUrl, string upstreamUrl);
Response<Route> GetRoute(string downstreamUrl);
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router.UpstreamRouter
{
public class InMemoryUpstreamRouter : IUpstreamRouter
{
private readonly Dictionary<string, string> _routes;
public InMemoryUpstreamRouter()
{
_routes = new Dictionary<string,string>();
}
public Response AddRoute(string downstreamUrl, string upstreamUrl)
{
if(_routes.ContainsKey(downstreamUrl))
{
return new ErrorResponse(new List<Error>(){new RouteKeyAlreadyExists()});
}
_routes.Add(downstreamUrl, upstreamUrl);
return new OkResponse();
}
public Response<Route> GetRoute(string downstreamUrl)
{
string upstreamUrl = null;
if(_routes.TryGetValue(downstreamUrl, out upstreamUrl))
{
return new OkResponse<Route>(new Route(downstreamUrl, upstreamUrl));
}
return new ErrorResponse<Route>(new List<Error>(){new RouteKeyDoesNotExist()});
}
}
}

View File

@ -0,0 +1,14 @@
namespace Ocelot.Library.Infrastructure.Router.UpstreamRouter
{
public class Route
{
public Route(string downstreamUrl, string upstreamUrl)
{
DownstreamUrl = downstreamUrl;
UpstreamUrl = upstreamUrl;
}
public string DownstreamUrl {get;private set;}
public string UpstreamUrl {get;private set;}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router.UpstreamRouter
{
public class RouteKeyAlreadyExists : Error
{
public RouteKeyAlreadyExists()
: base("This key has already been used")
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router.UpstreamRouter
{
public class RouteKeyDoesNotExist : Error
{
public RouteKeyDoesNotExist()
: base("This key does not exist")
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router.UrlPathRouter
{
public class DownstreamUrlPathTemplateAlreadyExists : Error
{
public DownstreamUrlPathTemplateAlreadyExists()
: base("This key has already been used")
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router.UrlPathRouter
{
public class DownstreamUrlPathTemplateDoesNotExist : Error
{
public DownstreamUrlPathTemplateDoesNotExist()
: base("This key does not exist")
{
}
}
}

View File

@ -0,0 +1,10 @@
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router.UrlPathRouter
{
public interface IUrlPathRouter
{
Response AddRoute(string downstreamUrlPathTemplate, string upstreamUrlPathTemplate);
Response<UrlPath> GetRoute(string downstreamUrlPathTemplate);
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using Ocelot.Library.Infrastructure.Responses;
namespace Ocelot.Library.Infrastructure.Router.UrlPathRouter
{
public class InMemoryUrlPathRouter : IUrlPathRouter
{
private readonly Dictionary<string, string> _routes;
public InMemoryUrlPathRouter()
{
_routes = new Dictionary<string,string>();
}
public Response AddRoute(string downstreamUrlPathTemplate, string upstreamUrlPathTemplate)
{
if(_routes.ContainsKey(downstreamUrlPathTemplate))
{
return new ErrorResponse(new List<Error>(){new DownstreamUrlPathTemplateAlreadyExists()});
}
_routes.Add(downstreamUrlPathTemplate, upstreamUrlPathTemplate);
return new OkResponse();
}
public Response<UrlPath> GetRoute(string downstreamUrlPathTemplate)
{
string upstreamUrlPathTemplate = null;
if(_routes.TryGetValue(downstreamUrlPathTemplate, out upstreamUrlPathTemplate))
{
return new OkResponse<UrlPath>(new UrlPath(downstreamUrlPathTemplate, upstreamUrlPathTemplate));
}
return new ErrorResponse<UrlPath>(new List<Error>(){new DownstreamUrlPathTemplateDoesNotExist()});
}
}
}

View File

@ -0,0 +1,14 @@
namespace Ocelot.Library.Infrastructure.Router.UrlPathRouter
{
public class UrlPath
{
public UrlPath(string downstreamUrlPathTemplate, string upstreamUrlPathTemplate)
{
DownstreamUrlPathTemplate = downstreamUrlPathTemplate;
UpstreamUrlPathTemplate = upstreamUrlPathTemplate;
}
public string DownstreamUrlPathTemplate {get;private set;}
public string UpstreamUrlPathTemplate {get;private set;}
}
}

234
test/Ocelot.AcceptanceTests/.gitignore vendored Normal file
View File

@ -0,0 +1,234 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Microsoft Azure ApplicationInsights config file
ApplicationInsights.config
# Windows Store app package directory
AppPackages/
BundleArtifacts/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
# FAKE - F# Make
.fake/

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Ocelot;
using Xunit;
namespace Ocelot.AcceptanceTests
{
public class RouterTests
{
public RouterTests()
{
}
[Fact]
public void should_route_request()
{
}
}
}

View File

@ -0,0 +1,37 @@
{
"version": "1.0.0-*",
"testRunner": "xunit",
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.0",
"type": "platform"
},
"Microsoft.AspNetCore.Mvc": "1.0.0",
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.Extensions.Logging.Debug": "1.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
"Microsoft.AspNetCore.Http": "1.0.0",
"Ocelot.Library": "1.0.0-*",
"Ocelot": "1.0.0-*",
"xunit": "2.1.0",
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"Shouldly": "2.8.0"
},
"frameworks": {
"netcoreapp1.0": {
"imports": [
"dotnet5.6",
"portable-net45+win8"
]
}
}
}

View File

@ -1,5 +1,5 @@
using Ocelot.Library.Infrastructure.Responses; using Ocelot.Library.Infrastructure.Responses;
using Ocelot.Library.Infrastructure.Router; using Ocelot.Library.Infrastructure.Router.UpstreamRouter;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
@ -9,13 +9,13 @@ namespace Ocelot.UnitTests
{ {
private string _upstreamApiUrl; private string _upstreamApiUrl;
private string _apiKey; private string _apiKey;
private IRouterService _router; private IUpstreamRouter _router;
private Response _response; private Response _response;
private Response<Route> _getRouteResponse; private Response<Route> _getRouteResponse;
public RouterTests() public RouterTests()
{ {
_router = new InMemoryRouterService(); _router = new InMemoryUpstreamRouter();
} }
[Fact] [Fact]
@ -77,8 +77,8 @@ namespace Ocelot.UnitTests
private void ThenTheRouteIsReturned() private void ThenTheRouteIsReturned()
{ {
_getRouteResponse.Data.ApiKey.ShouldBe(_apiKey); _getRouteResponse.Data.DownstreamUrl.ShouldBe(_apiKey);
_getRouteResponse.Data.UpstreamRoute.ShouldBe(_upstreamApiUrl); _getRouteResponse.Data.UpstreamUrl.ShouldBe(_upstreamApiUrl);
} }
private void GivenIHaveSetUpAnApiKeyAndUpstreamUrl(string apiKey, string upstreamUrl) private void GivenIHaveSetUpAnApiKeyAndUpstreamUrl(string apiKey, string upstreamUrl)

View File

@ -0,0 +1,133 @@
using System;
using Ocelot.Library.Infrastructure.Responses;
using Ocelot.Library.Infrastructure.Router.UpstreamRouter;
using Shouldly;
using Xunit;
namespace Ocelot.UnitTests
{
public class UrlMapperTests
{
private UrlToUrlTemplateMatcher _urlMapper;
public UrlMapperTests()
{
_urlMapper = new UrlToUrlTemplateMatcher();
}
[Fact]
public void can_match_down_stream_url_with_downstream_template_with_one_query_string_parameter()
{
var downstreamUrl = "api/product/products/?soldout=false";
var downstreamTemplate = "api/product/products/";
var result = _urlMapper.Match(downstreamUrl, downstreamTemplate);
result.ShouldBeTrue();
}
[Fact]
public void can_match_down_stream_url_with_downstream_template_with_one_query_string_parameter_and_one_template()
{
var downstreamUrl = "api/product/products/1/variants/?soldout=false";
var downstreamTemplate = "api/product/products/{productId}/variants/";
var result = _urlMapper.Match(downstreamUrl, downstreamTemplate);
result.ShouldBeTrue();
}
[Fact]
public void can_match_down_stream_url_with_downstream_template_with_one_place_holder()
{
var downstreamUrl = "api/product/products/1";
var downstreamTemplate = "api/product/products/{productId}";
var result = _urlMapper.Match(downstreamUrl, downstreamTemplate);
result.ShouldBeTrue();
}
[Fact]
public void can_match_down_stream_url_with_downstream_template_with_two_place_holders()
{
var downstreamUrl = "api/product/products/1/2";
var downstreamTemplate = "api/product/products/{productId}/{categoryId}";
var result = _urlMapper.Match(downstreamUrl, downstreamTemplate);
result.ShouldBeTrue();
}
[Fact]
public void can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something()
{
var downstreamUrl = "api/product/products/1/categories/2";
var downstreamTemplate = "api/product/products/{productId}/categories/{categoryId}";
var result = _urlMapper.Match(downstreamUrl, downstreamTemplate);
result.ShouldBeTrue();
}
[Fact]
public void can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something()
{
var downstreamUrl = "api/product/products/1/categories/2/variant/123";
var downstreamTemplate = "api/product/products/{productId}/categories/{categoryId}/variant/{variantId}";
var result = _urlMapper.Match(downstreamUrl, downstreamTemplate);
result.ShouldBeTrue();
}
[Fact]
public void can_match_down_stream_url_with_downstream_template_with_three_place_holders()
{
var downstreamUrl = "api/product/products/1/categories/2/variant/";
var downstreamTemplate = "api/product/products/{productId}/categories/{categoryId}/variant/";
var result = _urlMapper.Match(downstreamUrl, downstreamTemplate);
result.ShouldBeTrue();
}
}
public class UrlToUrlTemplateMatcher
{
public bool Match(string url, string urlTemplate)
{
url = url.ToLower();
urlTemplate = urlTemplate.ToLower();
int counterForUrl = 0;
for (int counterForTemplate = 0; counterForTemplate < urlTemplate.Length; counterForTemplate++)
{
if (CharactersDontMatch(urlTemplate[counterForTemplate], url[counterForUrl]) && ContinueScanningUrl(counterForUrl,url.Length))
{
if (IsPlaceholder(urlTemplate[counterForTemplate]))
{
counterForTemplate = GetNextCounterPosition(urlTemplate, counterForTemplate, '}');
counterForUrl = GetNextCounterPosition(url, counterForUrl, '/');
continue;
}
else
{
return false;
}
}
counterForUrl++;
}
return true;
}
private int GetNextCounterPosition(string urlTemplate, int counterForTemplate, char delimiter)
{
var closingPlaceHolderPositionOnTemplate = urlTemplate.IndexOf(delimiter, counterForTemplate);
return closingPlaceHolderPositionOnTemplate + 1;
}
private bool CharactersDontMatch(char characterOne, char characterTwo)
{
return characterOne != characterTwo;
}
private bool ContinueScanningUrl(int counterForUrl, int urlLength)
{
return counterForUrl < urlLength;
}
private bool IsPlaceholder(char character)
{
return character == '{';
}
}
}

View File

@ -0,0 +1,111 @@
using Ocelot.Library.Infrastructure.Responses;
using Ocelot.Library.Infrastructure.Router.UrlPathRouter;
using Shouldly;
using Xunit;
namespace Ocelot.UnitTests
{
public class UrlPathRouterTests
{
private string _upstreamUrlPath;
private string _downstreamUrlPath;
private IUrlPathRouter _router;
private Response _response;
private Response<UrlPath> _getResponse;
public UrlPathRouterTests()
{
_router = new InMemoryUrlPathRouter();
}
[Fact]
public void can_add_url_path()
{
GivenIHaveAnUpstreamUrlPath("api/products/products/{productId}");
GivenIWantToRouteRequestsToMyUpstreamUrlPath("api/products/{productId}");
WhenIAddTheConfiguration();
ThenTheResponseIsSuccesful();
}
[Fact]
public void can_get_url_path()
{
GivenIHaveSetUpADownstreamUrlPathAndAnUpstreamUrlPath("api2", "http://www.someapi.com/api2");
WhenIRetrieveTheUrlPathByDownstreamUrl();
ThenTheUrlPathIsReturned();
}
[Fact]
public void should_return_error_response_when_url_path_already_used()
{
GivenIHaveSetUpADownstreamUrlPathAndAnUpstreamUrlPath("api2", "http://www.someapi.com/api2");
WhenITryToUseTheSameDownstreamUrl();
ThenTheDownstreamUrlAlreadyBeenUsed();
}
[Fact]
public void should_return_error_response_if_key_doesnt_exist()
{
GivenIWantToRouteRequestsToMyUpstreamUrlPath("api");
WhenIRetrieveTheUrlPathByDownstreamUrl();
ThenTheKeyDoesNotExist();
}
private void WhenITryToUseTheSameDownstreamUrl()
{
WhenIAddTheConfiguration();
}
private void ThenTheDownstreamUrlAlreadyBeenUsed()
{
_response.ShouldNotBeNull();
_response.ShouldBeOfType<ErrorResponse>();
_response.Errors[0].Message.ShouldBe("This key has already been used");
}
private void ThenTheKeyDoesNotExist()
{
_getResponse.ShouldNotBeNull();
_getResponse.ShouldBeOfType<ErrorResponse<UrlPath>>();
_getResponse.Errors[0].Message.ShouldBe("This key does not exist");
}
private void WhenIRetrieveTheUrlPathByDownstreamUrl()
{
_getResponse = _router.GetRoute(_downstreamUrlPath);
}
private void ThenTheUrlPathIsReturned()
{
_getResponse.Data.DownstreamUrlPathTemplate.ShouldBe(_downstreamUrlPath);
_getResponse.Data.UpstreamUrlPathTemplate.ShouldBe(_upstreamUrlPath);
}
private void GivenIHaveSetUpADownstreamUrlPathAndAnUpstreamUrlPath(string downstream, string upstreamApiUrl)
{
GivenIHaveAnUpstreamUrlPath(upstreamApiUrl);
GivenIWantToRouteRequestsToMyUpstreamUrlPath(downstream);
WhenIAddTheConfiguration();
}
private void GivenIHaveAnUpstreamUrlPath(string upstreamApiUrl)
{
_upstreamUrlPath = upstreamApiUrl;
}
private void GivenIWantToRouteRequestsToMyUpstreamUrlPath(string apiKey)
{
_downstreamUrlPath = apiKey;
}
private void WhenIAddTheConfiguration()
{
_response = _router.AddRoute(_downstreamUrlPath, _upstreamUrlPath);
}
private void ThenTheResponseIsSuccesful()
{
_response.ShouldBeOfType<OkResponse>();
}
}
}