diff --git a/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj b/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj index b600711..afbcab2 100644 --- a/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj +++ b/Swashbuckle.OData.Nuget/Swashbuckle.OData.NuGet.nuproj @@ -25,12 +25,12 @@ Richard Beauchamp Extends Swashbuckle with OData v4 support! Extends Swashbuckle with OData v4 support! - Provides support for containment and ODataRoute attributes. + Provide single quotes around swagger path parameters of type string https://github.com/rbeauchamp/Swashbuckle.OData https://github.com/rbeauchamp/Swashbuckle.OData/blob/master/License.txt Copyright 2015 Swashbuckle Swagger SwaggerUi OData Documentation Discovery Help WebApi AspNet AspNetWebApi Docs WebHost IIS - 2.7.0 + 2.7.1 diff --git a/Swashbuckle.OData.Sample/ODataControllers/ProductsController.cs b/Swashbuckle.OData.Sample/ODataControllers/ProductsController.cs index 1aa4d31..077bc12 100644 --- a/Swashbuckle.OData.Sample/ODataControllers/ProductsController.cs +++ b/Swashbuckle.OData.Sample/ODataControllers/ProductsController.cs @@ -27,6 +27,15 @@ static ProductsController() }).ToList().ForEach(p => Data.TryAdd(p.Id, p)); } + /// + /// Query products + /// + [EnableQuery] + public IQueryable GetProducts() + { + return Data.Values.AsQueryable(); + } + /// /// Get the most expensive product. This is a function bound to a collection. /// diff --git a/Swashbuckle.OData.Tests/Fixtures/FunctionTests.cs b/Swashbuckle.OData.Tests/Fixtures/FunctionTests.cs index c4b4ed1..db10600 100644 --- a/Swashbuckle.OData.Tests/Fixtures/FunctionTests.cs +++ b/Swashbuckle.OData.Tests/Fixtures/FunctionTests.cs @@ -94,7 +94,7 @@ public async Task It_supports_a_function_that_accepts_a_string_parameter() // Assert PathItem pathItem; - swaggerDocument.paths.TryGetValue("/odata/v1/Products({Id})/Default.CalculateGeneralSalesTax(state={state})", out pathItem); + swaggerDocument.paths.TryGetValue("/odata/v1/Products({Id})/Default.CalculateGeneralSalesTax(state='{state}')", out pathItem); pathItem.Should().NotBeNull(); pathItem.get.Should().NotBeNull(); } @@ -113,7 +113,7 @@ public async Task It_supports_unbound_functions() // Assert PathItem pathItem; - swaggerDocument.paths.TryGetValue("/odata/v1/GetSalesTaxRate(state={state})", out pathItem); + swaggerDocument.paths.TryGetValue("/odata/v1/GetSalesTaxRate(state='{state}')", out pathItem); pathItem.Should().NotBeNull(); pathItem.get.Should().NotBeNull(); } diff --git a/Swashbuckle.OData.Tests/Fixtures/StringTypeUrlParamTests.cs b/Swashbuckle.OData.Tests/Fixtures/StringTypeUrlParamTests.cs new file mode 100644 index 0000000..8ad60ec --- /dev/null +++ b/Swashbuckle.OData.Tests/Fixtures/StringTypeUrlParamTests.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading.Tasks; +using System.Web.Http.Dispatcher; +using System.Web.OData.Builder; +using System.Web.OData.Extensions; +using FluentAssertions; +using Microsoft.OData.Edm; +using Microsoft.Owin.Hosting; +using NUnit.Framework; +using Owin; +using Swashbuckle.Swagger; +using SwashbuckleODataSample; +using SwashbuckleODataSample.Models; + +namespace Swashbuckle.OData.Tests +{ + [TestFixture] + public class StringTypeUrlParamTests + { + [Test] + public async Task It_wraps_string_type_url_params_with_single_quotes() + { + using (WebApp.Start(HttpClientUtils.BaseAddress, appBuilder => Configuration(appBuilder, typeof(ProductsV1Controller)))) + { + // Arrange + var httpClient = HttpClientUtils.GetHttpClient(HttpClientUtils.BaseAddress, ODataConfig.ODataRoutePrefix); + + // Act + var swaggerDocument = await httpClient.GetJsonAsync("swagger/docs/v1"); + + // Assert + PathItem pathItem; + swaggerDocument.paths.TryGetValue("/odata/v1/Products({Id})/Default.CalculateGeneralSalesTax(state='{state}')", out pathItem); + pathItem.Should().NotBeNull(); + pathItem.get.Should().NotBeNull(); + } + } + + private static void Configuration(IAppBuilder appBuilder, Type targetController) + { + var config = appBuilder.GetStandardHttpConfig(targetController); + + var controllerSelector = new UnitTestODataVersionControllerSelector(config, targetController); + config.Services.Replace(typeof(IHttpControllerSelector), controllerSelector); + + // Define a route to a controller class that contains functions + config.MapODataServiceRoute("FunctionsODataRoute", "odata/v1", GetFunctionsEdmModel()); + controllerSelector.RouteVersionSuffixMapping.Add("FunctionsODataRoute", "V1"); + + config.EnsureInitialized(); + } + + private static IEdmModel GetFunctionsEdmModel() + { + ODataModelBuilder builder = new ODataConventionModelBuilder(); + + builder.EntitySet("Products"); + + var productType = builder.EntityType(); + + // Function bound to a collection + // Returns the most expensive product, a single entity + productType.Collection + .Function("MostExpensive") + .Returns(); + + // Function bound to a collection + // Returns the top 10 product, a collection + productType.Collection + .Function("Top10") + .ReturnsCollectionFromEntitySet("Products"); + + // Function bound to a single entity + // Returns the instance's price rank among all products + productType + .Function("GetPriceRank") + .Returns(); + + // Function bound to a single entity + // Accept a string as parameter and return a double + // This function calculate the general sales tax base on the + // state + productType + .Function("CalculateGeneralSalesTax") + .Returns() + .Parameter("state"); + + // Unbound Function + builder.Function("GetSalesTaxRate") + .Returns() + .Parameter("state"); + + return builder.GetEdmModel(); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj index 67a1b0d..c97a0db 100644 --- a/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj +++ b/Swashbuckle.OData.Tests/Swashbuckle.OData.Tests.csproj @@ -150,6 +150,7 @@ + diff --git a/Swashbuckle.OData/Descriptions/ApiDescriptionExtensions.cs b/Swashbuckle.OData/Descriptions/ApiDescriptionExtensions.cs new file mode 100644 index 0000000..bee9491 --- /dev/null +++ b/Swashbuckle.OData/Descriptions/ApiDescriptionExtensions.cs @@ -0,0 +1,25 @@ +using System.Web.Http.Description; +using Swashbuckle.Swagger; + +namespace Swashbuckle.OData.Descriptions +{ + public static class ApiDescriptionExtensions + { + public static string GetRelativePathWithQuotedStringParams(this ApiDescription apiDescription) + { + var parameters = apiDescription.ParameterDescriptions; + + var newRelativePathSansQueryString = apiDescription.RelativePathSansQueryString(); + + foreach (var parameter in parameters) + { + if (newRelativePathSansQueryString.Contains("{" + parameter.Name + "}") && parameter.ParameterDescriptor.ParameterType == typeof(string)) + { + newRelativePathSansQueryString = newRelativePathSansQueryString.Replace("{" + parameter.Name + "}", "\'{" + parameter.Name + "}\'"); + } + } + + return apiDescription.RelativePath.Replace(apiDescription.RelativePathSansQueryString(), newRelativePathSansQueryString); + } + } +} \ No newline at end of file diff --git a/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapper.cs b/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapper.cs index 7193def..92aa179 100644 --- a/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapper.cs +++ b/Swashbuckle.OData/Descriptions/ODataActionDescriptorMapper.cs @@ -63,6 +63,8 @@ public IEnumerable Map(ODataActionDescriptor oDataActionDescript // Have to set ResponseDescription because it's internal!?? apiDescription.GetType().GetProperty("ResponseDescription").SetValue(apiDescription, responseDescription); + apiDescription.RelativePath = apiDescription.GetRelativePathWithQuotedStringParams(); + apiDescriptions.Add(apiDescription); } return apiDescriptions; diff --git a/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs b/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs index c2e1cfe..23c6f88 100644 --- a/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs +++ b/Swashbuckle.OData/Descriptions/ODataSwaggerUtilities.cs @@ -326,15 +326,7 @@ public static string GetPathForEntity(string routePrefix, IEdmNavigationSource n private static string AppendSingleColumnKeyTemplate(IEdmEntitySet entitySet, string singleEntityPath) { var key = entitySet.EntityType().Key().Single(); - //if (key.Type.Definition.TypeKind == EdmTypeKind.Primitive && ((IEdmPrimitiveType) key.Type.Definition).PrimitiveKind == EdmPrimitiveTypeKind.String) - //{ - // singleEntityPath += "'{" + key.Name + "}', "; - //} - //else - //{ - singleEntityPath += "{" + key.Name + "}, "; - //} return singleEntityPath; } @@ -342,14 +334,7 @@ private static string AppendMultiColumnKeyTemplate(IEdmEntitySet entitySet, stri { foreach (var key in entitySet.EntityType().Key()) { - //if (key.Type.Definition.TypeKind == EdmTypeKind.Primitive && ((IEdmPrimitiveType)key.Type.Definition).PrimitiveKind == EdmPrimitiveTypeKind.String) - //{ - // singleEntityPath += key.Name + "='{" + key.Name + "}', "; - //} - //else - //{ - singleEntityPath += key.Name + "={" + key.Name + "}, "; - //} + singleEntityPath += key.Name + "={" + key.Name + "}, "; } return singleEntityPath; } @@ -403,14 +388,7 @@ public static string GetPathForOperationOfEntitySet(IEdmOperation operation, IEd { foreach (var parameter in operation.Parameters.Skip(1)) { - //if (parameter.Type.Definition.TypeKind == EdmTypeKind.Primitive && ((IEdmPrimitiveType) parameter.Type.Definition).PrimitiveKind == EdmPrimitiveTypeKind.String) - //{ - // swaggerOperationPath += parameter.Name + "=" + "'{" + parameter.Name + "}',"; - //} - //else - //{ - swaggerOperationPath += parameter.Name + "=" + "{" + parameter.Name + "},"; - //} + swaggerOperationPath += parameter.Name + "=" + "{" + parameter.Name + "},"; } } if (swaggerOperationPath.EndsWith(",", StringComparison.Ordinal)) @@ -444,14 +422,7 @@ public static string GetPathForOperationOfEntity(string routePrefix, IEdmOperati { foreach (var parameter in operation.Parameters.Skip(1)) { - //if (parameter.Type.Definition.TypeKind == EdmTypeKind.Primitive && ((IEdmPrimitiveType) parameter.Type.Definition).PrimitiveKind == EdmPrimitiveTypeKind.String) - //{ - // swaggerOperationPath += parameter.Name + "=" + "{" + parameter.Name + "},"; - //} - //else - //{ - swaggerOperationPath += parameter.Name + "=" + "{" + parameter.Name + "},"; - //} + swaggerOperationPath += parameter.Name + "=" + "{" + parameter.Name + "},"; } } if (swaggerOperationPath.EndsWith(",", StringComparison.Ordinal)) diff --git a/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs b/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs index 7eeac6d..f2a5335 100644 --- a/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs +++ b/Swashbuckle.OData/Descriptions/SwaggerOperationMapper.cs @@ -67,6 +67,8 @@ public IEnumerable Map(ODataActionDescriptor oDataActionDescript // Have to set ResponseDescription because it's internal!?? apiDescription.GetType().GetProperty("ResponseDescription").SetValue(apiDescription, responseDescription); + apiDescription.RelativePath = apiDescription.GetRelativePathWithQuotedStringParams(); + apiDescriptions.Add(apiDescription); } diff --git a/Swashbuckle.OData/Properties/AssemblyInfo.cs b/Swashbuckle.OData/Properties/AssemblyInfo.cs index 033618c..a0188dc 100644 --- a/Swashbuckle.OData/Properties/AssemblyInfo.cs +++ b/Swashbuckle.OData/Properties/AssemblyInfo.cs @@ -37,4 +37,4 @@ [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: AssemblyInformationalVersion("2.7.0")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("2.7.1")] \ No newline at end of file diff --git a/Swashbuckle.OData/Swashbuckle.OData.csproj b/Swashbuckle.OData/Swashbuckle.OData.csproj index 123db67..6607388 100644 --- a/Swashbuckle.OData/Swashbuckle.OData.csproj +++ b/Swashbuckle.OData/Swashbuckle.OData.csproj @@ -172,6 +172,7 @@ +