diff --git a/Swashbuckle.TestApp/App_Start/WebApiConfig.cs b/Swashbuckle.TestApp/App_Start/WebApiConfig.cs index 301c6545b..9bfe3f7ba 100644 --- a/Swashbuckle.TestApp/App_Start/WebApiConfig.cs +++ b/Swashbuckle.TestApp/App_Start/WebApiConfig.cs @@ -9,23 +9,29 @@ public static class WebApiConfig public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( - name: "Orders_default", + name: "Orders_route", routeTemplate: "api/orders/{id}", defaults: new {controller = "Orders", id = RouteParameter.Optional} ); config.Routes.MapHttpRoute( - name: "OrderItems_default", + name: "OrderItems_route", routeTemplate: "api/orders/{orderId}/items/{id}", defaults: new {controller = "OrderItems", id = RouteParameter.Optional} ); config.Routes.MapHttpRoute( - name: "Customers_default", + name: "Customers_route", routeTemplate: "api/customers/{id}", defaults: new {controller = "Customers", id = RouteParameter.Optional} ); + config.Routes.MapHttpRoute( + name: "Products_route", + routeTemplate: "api/products", + defaults: new {controller = "Products"} + ); + // Uncomment below to support documentation from Xml Comments // try diff --git a/Swashbuckle.TestApp/Controllers/ProductsController.cs b/Swashbuckle.TestApp/Controllers/ProductsController.cs new file mode 100644 index 000000000..828059923 --- /dev/null +++ b/Swashbuckle.TestApp/Controllers/ProductsController.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Web.Http; +using Swashbuckle.TestApp.Models; + +namespace Swashbuckle.TestApp.Controllers +{ + public class ProductsController : ApiController + { + public IEnumerable GetAll() + { + throw new NotImplementedException(); + } + } +} diff --git a/Swashbuckle.TestApp/Models/Product.cs b/Swashbuckle.TestApp/Models/Product.cs new file mode 100644 index 000000000..5d1433b0c --- /dev/null +++ b/Swashbuckle.TestApp/Models/Product.cs @@ -0,0 +1,32 @@ +namespace Swashbuckle.TestApp.Models +{ + public abstract class Product + { + public int Id { get; set; } + + public decimal Price { get; set; } + } + + public class Book : Product + { + public string Title { get; set; } + + public string Author { get; set; } + } + + public class Album : Product + { + public string Name { get; set; } + + public string Artist { get; set; } + } + + public abstract class Service : Product + {} + + public class Shipping : Service + {} + + public class Packaging : Service + {} +} \ No newline at end of file diff --git a/Swashbuckle.TestApp/Swashbuckle.TestApp.csproj b/Swashbuckle.TestApp/Swashbuckle.TestApp.csproj index 248fa0e72..64c967be1 100644 --- a/Swashbuckle.TestApp/Swashbuckle.TestApp.csproj +++ b/Swashbuckle.TestApp/Swashbuckle.TestApp.csproj @@ -112,6 +112,7 @@ + @@ -122,6 +123,7 @@ Global.asax + diff --git a/Swashbuckle.Tests/SwaggerGeneratorTests.cs b/Swashbuckle.Tests/SwaggerGeneratorTests.cs index 494bc3cfc..eecdea799 100644 --- a/Swashbuckle.Tests/SwaggerGeneratorTests.cs +++ b/Swashbuckle.Tests/SwaggerGeneratorTests.cs @@ -38,7 +38,7 @@ public void It_should_generate_a_listing_according_to_provided_strategy() var resourceListing = _swaggerSpec.Listing; Assert.AreEqual("1.0", resourceListing.ApiVersion); Assert.AreEqual("1.2", resourceListing.SwaggerVersion); - Assert.AreEqual(3, resourceListing.Apis.Count()); + Assert.AreEqual(4, resourceListing.Apis.Count()); Assert.IsTrue(resourceListing.Apis.Any(a => a.Path == "/Orders"), "Orders declaration not listed"); @@ -46,6 +46,8 @@ public void It_should_generate_a_listing_according_to_provided_strategy() "OrderItems declaration not listed"); Assert.IsTrue(resourceListing.Apis.Any(a => a.Path == "/Customers"), "Customers declaration not listed"); + Assert.IsTrue(resourceListing.Apis.Any(a => a.Path == "/Products"), + "Products declaration not listed"); } [Test] @@ -413,7 +415,7 @@ public void It_should_generate_a_parameter_spec_for_each_parameter_in_a_given_op } [Test] - public void It_should_generate_a_model_spec_for_all_complex_types_in_a_declaration() + public void It_should_generate_model_specs_for_all_complex_types_in_a_declaration_including_sub_types() { ApiDeclaration("/Orders", dec => { @@ -546,6 +548,92 @@ public void It_should_generate_a_model_spec_for_all_complex_types_in_a_declarati }); }); + + ApiDeclaration("/Products", dec => + { + Assert.AreEqual(6, dec.Models.Count); + + Model(dec, "Product", model => + { + ModelProperty(model, "Id", property => + { + Assert.AreEqual("integer", property.Type); + Assert.AreEqual("int32", property.Format); + Assert.IsNull(property.Items); + Assert.IsNull(property.Enum); + }); + + ModelProperty(model, "Price", property => + { + Assert.AreEqual("number", property.Type); + Assert.AreEqual("double", property.Format); + Assert.IsNull(property.Items); + Assert.IsNull(property.Enum); + }); + + CollectionAssert.AreEqual(new[] { "Book", "Album", "Service" }, model.SubTypes); + }); + + Model(dec, "Book", model => + { + ModelProperty(model, "Title", property => + { + Assert.AreEqual("string", property.Type); + Assert.IsNull(property.Format); + Assert.IsNull(property.Items); + Assert.IsNull(property.Enum); + }); + + ModelProperty(model, "Author", property => + { + Assert.AreEqual("string", property.Type); + Assert.IsNull(property.Format); + Assert.IsNull(property.Items); + Assert.IsNull(property.Enum); + }); + + CollectionAssert.IsEmpty(model.SubTypes); + }); + + Model(dec, "Album", model => + { + ModelProperty(model, "Name", property => + { + Assert.AreEqual("string", property.Type); + Assert.IsNull(property.Format); + Assert.IsNull(property.Items); + Assert.IsNull(property.Enum); + }); + + ModelProperty(model, "Artist", property => + { + Assert.AreEqual("string", property.Type); + Assert.IsNull(property.Format); + Assert.IsNull(property.Items); + Assert.IsNull(property.Enum); + }); + + CollectionAssert.IsEmpty(model.SubTypes); + }); + + Model(dec, "Service", model => + { + CollectionAssert.IsEmpty(model.Properties); + CollectionAssert.AreEqual(new[] { "Shipping", "Packaging" }, model.SubTypes); + }); + + Model(dec, "Shipping", model => + { + CollectionAssert.IsEmpty(model.Properties); + CollectionAssert.IsEmpty(model.SubTypes); + }); + + Model(dec, "Packaging", model => + { + CollectionAssert.IsEmpty(model.Properties); + CollectionAssert.IsEmpty(model.SubTypes); + }); + }); } [Test] diff --git a/Swashbuckle/Models/ModelSpecGenerator.cs b/Swashbuckle/Models/ModelSpecGenerator.cs index 04fa1528a..4c11b1532 100644 --- a/Swashbuckle/Models/ModelSpecGenerator.cs +++ b/Swashbuckle/Models/ModelSpecGenerator.cs @@ -98,7 +98,7 @@ private ModelSpec CreateSpecFor(Type type, bool deferIfComplex, IDictionary complexTypes) { - var propInfos = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + var propInfos = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly); var propSpecs = propInfos .ToDictionary(propInfo => propInfo.Name, propInfo => CreateSpecFor(propInfo.PropertyType, true, complexTypes)); @@ -107,12 +107,18 @@ private ModelSpec CreateComplexSpecFor(Type type, IDictionary c .Select(propInfo => propInfo.Name) .ToList(); + var subTypes = type.DirectSubTypes() + .Select(subType => CreateSpecFor(subType, true, complexTypes)) + .Select(subTypeSpec => subTypeSpec.Ref) + .ToList(); + return new ModelSpec { Id = UniqueIdFor(type), Type = "object", Properties = propSpecs, - Required = required + Required = required, + SubTypes = subTypes }; } diff --git a/Swashbuckle/Models/SwaggerSpec.cs b/Swashbuckle/Models/SwaggerSpec.cs index 29dd1285a..6e0848ec7 100644 --- a/Swashbuckle/Models/SwaggerSpec.cs +++ b/Swashbuckle/Models/SwaggerSpec.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.Web.Http.Description; using Newtonsoft.Json; @@ -170,5 +171,8 @@ public class ModelSpec [JsonProperty("required")] public IList Required { get; set; } + + [JsonProperty("subTypes")] + public IList SubTypes { get; set; } } } diff --git a/Swashbuckle/Models/TypeExtensions.cs b/Swashbuckle/Models/TypeExtensions.cs index 9ba20c1d1..54376181f 100644 --- a/Swashbuckle/Models/TypeExtensions.cs +++ b/Swashbuckle/Models/TypeExtensions.cs @@ -32,5 +32,10 @@ public static bool IsEnumerable(this Type type, out Type enumerableTypeArgument) return enumerableType != null; } + + public static IEnumerable DirectSubTypes(this Type baseType) + { + return baseType.Assembly.GetTypes().Where(type => type.BaseType == baseType); + } } } \ No newline at end of file