diff --git a/src/Ocelot.Library/Infrastructure/Configuration/Configuration.cs b/src/Ocelot.Library/Infrastructure/Configuration/Configuration.cs new file mode 100644 index 00000000..f70712ed --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Configuration/Configuration.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Ocelot.Library.Infrastructure.Configuration +{ + public class Configuration + { + public Configuration() + { + Routes = new List(); + } + public Configuration(List routes) + { + Routes = routes; + } + + public List Routes { get; private set; } + } +} diff --git a/src/Ocelot.Library/Infrastructure/Configuration/ConfigurationReader.cs b/src/Ocelot.Library/Infrastructure/Configuration/ConfigurationReader.cs new file mode 100644 index 00000000..139cff46 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Configuration/ConfigurationReader.cs @@ -0,0 +1,22 @@ +namespace Ocelot.Library.Infrastructure.Configuration +{ + using System.IO; + using YamlDotNet.Serialization; + using YamlDotNet.Serialization.NamingConventions; + + public class ConfigurationReader : IConfigurationReader + { + public Configuration Read(string configurationFilePath) + { + var contents = File.ReadAllText(configurationFilePath); + + var input = new StringReader(contents); + + var deserializer = new Deserializer(namingConvention: new CamelCaseNamingConvention()); + + var configuration = deserializer.Deserialize(input); + + return configuration;; + } + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/Configuration/IConfigurationReader.cs b/src/Ocelot.Library/Infrastructure/Configuration/IConfigurationReader.cs new file mode 100644 index 00000000..d53b0526 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Configuration/IConfigurationReader.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Library.Infrastructure.Configuration +{ + public interface IConfigurationReader + { + Configuration Read(string configurationFilePath); + } +} diff --git a/src/Ocelot.Library/Infrastructure/Configuration/Route.cs b/src/Ocelot.Library/Infrastructure/Configuration/Route.cs new file mode 100644 index 00000000..aa551658 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Configuration/Route.cs @@ -0,0 +1,18 @@ +namespace Ocelot.Library.Infrastructure.Configuration +{ + public class Route + { + public Route() + { + + } + public Route(string downstream, string upstream) + { + Downstream = downstream; + Upstream = upstream; + } + + public string Downstream { get; private set; } + public string Upstream { get; private set; } + } +} diff --git a/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationExtensions.cs b/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationExtensions.cs new file mode 100644 index 00000000..802016c8 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationExtensions.cs @@ -0,0 +1,42 @@ +using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; + +namespace Ocelot.Library.Infrastructure.Configuration +{ + public static class YamlConfigurationExtensions + { + public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path) + { + return AddYamlFile(builder, provider: null, path: path, optional: false, reloadOnChange: false); + } + + public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, bool optional) + { + return AddYamlFile(builder, provider: null, path: path, optional: optional, reloadOnChange: false); + } + + public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange) + { + return AddYamlFile(builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange); + } + + public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange) + { + if (provider == null && Path.IsPathRooted(path)) + { + provider = new PhysicalFileProvider(Path.GetDirectoryName(path)); + path = Path.GetFileName(path); + } + var source = new YamlConfigurationSource + { + FileProvider = provider, + Path = path, + Optional = optional, + ReloadOnChange = reloadOnChange + }; + builder.Add(source); + return builder; + } + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationFileParser.cs b/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationFileParser.cs new file mode 100644 index 00000000..5e6d7c34 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationFileParser.cs @@ -0,0 +1,121 @@ +namespace Ocelot.Library.Infrastructure.Configuration +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Microsoft.Extensions.Configuration; + using YamlDotNet.RepresentationModel; + + internal class YamlConfigurationFileParser + { + private readonly IDictionary _data = new SortedDictionary(StringComparer.OrdinalIgnoreCase); + private readonly Stack _context = new Stack(); + private string _currentPath; + + public IDictionary Parse(Stream input) + { + _data.Clear(); + _context.Clear(); + + // https://dotnetfiddle.net/rrR2Bb + var yaml = new YamlStream(); + yaml.Load(new StreamReader(input)); + + if (yaml.Documents.Any()) + { + var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; + + // The document node is a mapping node + VisitYamlMappingNode(mapping); + } + + return _data; + } + + private void VisitYamlNodePair(KeyValuePair yamlNodePair) + { + var context = ((YamlScalarNode)yamlNodePair.Key).Value; + VisitYamlNode(context, yamlNodePair.Value); + } + + private void VisitYamlNode(string context, YamlNode node) + { + if (node is YamlScalarNode) + { + VisitYamlScalarNode(context, (YamlScalarNode)node); + } + if (node is YamlMappingNode) + { + VisitYamlMappingNode(context, (YamlMappingNode)node); + } + if (node is YamlSequenceNode) + { + VisitYamlSequenceNode(context, (YamlSequenceNode)node); + } + } + + private void VisitYamlScalarNode(string context, YamlScalarNode yamlValue) + { + //a node with a single 1-1 mapping + EnterContext(context); + var currentKey = _currentPath; + + if (_data.ContainsKey(currentKey)) + { + throw new FormatException("Key is duplicate ${currentKey}"); + } + + _data[currentKey] = yamlValue.Value; + ExitContext(); + } + + private void VisitYamlMappingNode(YamlMappingNode node) + { + foreach (var yamlNodePair in node.Children) + { + VisitYamlNodePair(yamlNodePair); + } + } + + private void VisitYamlMappingNode(string context, YamlMappingNode yamlValue) + { + //a node with an associated sub-document + EnterContext(context); + + VisitYamlMappingNode(yamlValue); + + ExitContext(); + } + + private void VisitYamlSequenceNode(string context, YamlSequenceNode yamlValue) + { + //a node with an associated list + EnterContext(context); + + VisitYamlSequenceNode(yamlValue); + + ExitContext(); + } + + private void VisitYamlSequenceNode(YamlSequenceNode node) + { + for (int i = 0; i < node.Children.Count; i++) + { + VisitYamlNode(i.ToString(), node.Children[i]); + } + } + + private void EnterContext(string context) + { + _context.Push(context); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); + } + + private void ExitContext() + { + _context.Pop(); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); + } + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationProvider.cs b/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationProvider.cs new file mode 100644 index 00000000..fc81e102 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationProvider.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Library.Infrastructure.Configuration +{ + using System.IO; + using Microsoft.Extensions.Configuration; + + public class YamlConfigurationProvider : FileConfigurationProvider + { + public YamlConfigurationProvider(YamlConfigurationSource source) : base(source) { } + + public override void Load(Stream stream) + { + var parser = new YamlConfigurationFileParser(); + + Data = parser.Parse(stream); + } + } +} diff --git a/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationSource.cs b/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationSource.cs new file mode 100644 index 00000000..d1684517 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Configuration/YamlConfigurationSource.cs @@ -0,0 +1,13 @@ +namespace Ocelot.Library.Infrastructure.Configuration +{ + using Microsoft.Extensions.Configuration; + + public class YamlConfigurationSource : FileConfigurationSource + { + public override IConfigurationProvider Build(IConfigurationBuilder builder) + { + FileProvider = FileProvider ?? builder.GetFileProvider(); + return new YamlConfigurationProvider(this); + } + } +} diff --git a/src/Ocelot.Library/Middleware/ProxyMiddleware.cs b/src/Ocelot.Library/Middleware/ProxyMiddleware.cs index 077bfae9..0acf6c25 100644 --- a/src/Ocelot.Library/Middleware/ProxyMiddleware.cs +++ b/src/Ocelot.Library/Middleware/ProxyMiddleware.cs @@ -7,6 +7,8 @@ using Ocelot.Library.Infrastructure.UrlTemplateReplacer; namespace Ocelot.Library.Middleware { using System.Net; + using Infrastructure.Configuration; + using Microsoft.Extensions.Options; public class ProxyMiddleware { @@ -14,19 +16,23 @@ namespace Ocelot.Library.Middleware private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; private readonly IUrlTemplateMapRepository _urlTemplateMapRepository; private readonly IDownstreamUrlTemplateVariableReplacer _urlReplacer; + private readonly IOptions _optionsAccessor; + public ProxyMiddleware(RequestDelegate next, IUrlPathToUrlTemplateMatcher urlMatcher, IUrlTemplateMapRepository urlPathRepository, - IDownstreamUrlTemplateVariableReplacer urlReplacer) + IDownstreamUrlTemplateVariableReplacer urlReplacer, IOptions optionsAccessor) { _next = next; _urlMatcher = urlMatcher; _urlTemplateMapRepository = urlPathRepository; _urlReplacer = urlReplacer; + _optionsAccessor = optionsAccessor; } public async Task Invoke(HttpContext context) { + var downstreamUrlPath = context.Request.Path.ToString(); var upstreamUrlTemplates = _urlTemplateMapRepository.All; diff --git a/src/Ocelot.Library/project.json b/src/Ocelot.Library/project.json index ca884c95..7037f0a7 100644 --- a/src/Ocelot.Library/project.json +++ b/src/Ocelot.Library/project.json @@ -1,23 +1,24 @@ { "version": "1.0.0-*", - "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" + "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", + "YamlDotNet": "3.9.0" + }, "frameworks": { "netcoreapp1.0": { diff --git a/src/Ocelot/Startup.cs b/src/Ocelot/Startup.cs index c585c1a5..779a0e74 100644 --- a/src/Ocelot/Startup.cs +++ b/src/Ocelot/Startup.cs @@ -8,6 +8,7 @@ using Ocelot.Library.Middleware; namespace Ocelot { + using Library.Infrastructure.Configuration; using Library.Infrastructure.UrlMatcher; using Library.Infrastructure.UrlTemplateReplacer; using Library.Infrastructure.UrlTemplateRepository; @@ -20,6 +21,7 @@ namespace Ocelot .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddYamlFile("configuration.yaml") .AddEnvironmentVariables(); Configuration = builder.Build(); } @@ -29,6 +31,10 @@ namespace Ocelot // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddOptions(); + + services.Configure(Configuration); + // Add framework services. services.AddSingleton(); services.AddSingleton(); diff --git a/test/Ocelot.AcceptanceTests/ConfigurationReaderTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationReaderTests.cs new file mode 100644 index 00000000..14878829 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ConfigurationReaderTests.cs @@ -0,0 +1,54 @@ +namespace Ocelot.AcceptanceTests +{ + using System.Collections.Generic; + using Library.Infrastructure.Configuration; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + + public class ConfigurationReaderTests + { + private readonly IConfigurationReader _configurationReader; + private string _configPath; + private Configuration _result; + + public ConfigurationReaderTests() + { + _configurationReader = new ConfigurationReader(); + } + + [Fact] + public void can_read_configuration() + { + const string path = "./ConfigurationReaderTests.can_read_configuration.yaml"; + + var expected = + new Configuration(new List + { + new Route("productservice/category/{categoryId}/products/{productId}/variants/{variantId}", + "https://www.moonpig.com/api/products/{categoryId}/{productId}/{variantId}") + }); + + this.Given(x => x.GivenAConfigPathOf(path)) + .When(x => x.WhenICallTheConfigurationReader()) + .Then(x => x.ThenTheFollowingConfigurationIsReturned(expected)) + .BDDfy(); + } + + private void GivenAConfigPathOf(string configPath) + { + _configPath = configPath; + } + + private void WhenICallTheConfigurationReader() + { + _result = _configurationReader.Read(_configPath); + } + + private void ThenTheFollowingConfigurationIsReturned(Configuration expected) + { + _result.Routes[0].Downstream.ShouldBe(expected.Routes[0].Downstream); + _result.Routes[0].Upstream.ShouldBe(expected.Routes[0].Upstream); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/OcelotTests.cs b/test/Ocelot.AcceptanceTests/OcelotTests.cs index 6f767a64..08a2734c 100644 --- a/test/Ocelot.AcceptanceTests/OcelotTests.cs +++ b/test/Ocelot.AcceptanceTests/OcelotTests.cs @@ -36,7 +36,7 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } - [Fact] + [Fact] public void should_return_response_200() { this.When(x => x.WhenIRequestTheUrl("/")) diff --git a/test/Ocelot.AcceptanceTests/configuration.yaml b/test/Ocelot.AcceptanceTests/configuration.yaml new file mode 100644 index 00000000..98c05a41 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/configuration.yaml @@ -0,0 +1,3 @@ +routes: + - downstream: "productservice/category/{categoryId}/products/{productId}/variants/{variantId}" + upstream: "https://www.moonpig.com/api/products/{categoryId}/{productId}/{variantId}" diff --git a/test/Ocelot.AcceptanceTests/project.json b/test/Ocelot.AcceptanceTests/project.json index c350a62b..c672997c 100644 --- a/test/Ocelot.AcceptanceTests/project.json +++ b/test/Ocelot.AcceptanceTests/project.json @@ -1,39 +1,48 @@ { - "version": "1.0.0-*", + "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-*", - "xunit": "2.1.0", - "dotnet-test-xunit": "2.2.0-preview2-build1029", - "Shouldly": "2.8.0", - "Ocelot": "1.0.0-*", - "Microsoft.AspNetCore.TestHost": "1.0.0", - "TestStack.BDDfy": "4.3.1" - }, - - "frameworks": { - "netcoreapp1.0": { - "imports": [ - "dotnet5.6", - "portable-net45+win8" - ] - } + "buildOptions": { + "copyToOutput": { + "include": [ + "configuration.yaml" + ] } + }, + + "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-*", + "xunit": "2.1.0", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Shouldly": "2.8.0", + "Ocelot": "1.0.0-*", + "Microsoft.AspNetCore.TestHost": "1.0.0", + "TestStack.BDDfy": "4.3.1", + "YamlDotNet": "3.9.0" + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": [ + "dotnet5.6", + "portable-net45+win8" + ] + } + } } diff --git a/test/Ocelot.UnitTests/project.json b/test/Ocelot.UnitTests/project.json index f415f589..f8c0cf9d 100644 --- a/test/Ocelot.UnitTests/project.json +++ b/test/Ocelot.UnitTests/project.json @@ -3,28 +3,29 @@ "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-*", - "xunit": "2.1.0", - "dotnet-test-xunit": "2.2.0-preview2-build1029", - "Shouldly": "2.8.0", - "TestStack.BDDfy": "4.3.1" + "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-*", + "xunit": "2.1.0", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Shouldly": "2.8.0", + "TestStack.BDDfy": "4.3.1", + "YamlDotNet": "3.9.0" + }, "frameworks": { "netcoreapp1.0": {