Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Docfx.Common/EntityMergers/ReflectionEntityMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public void Merge(ref object source, object overrides, IMergeContext context)
}

var type = o.GetType();
if (type.IsValueType)
if (type.IsValueType && Nullable.GetUnderlyingType(prop.Prop.PropertyType) == null)
{
var defaultValue = Activator.CreateInstance(type);
if (object.Equals(defaultValue, o))
Expand Down Expand Up @@ -184,7 +184,7 @@ public void Merge(ref object source, object overrides, IMergeContext context)
}

var type = o.GetType();
if (type.IsValueType)
if (type.IsValueType && Nullable.GetUnderlyingType(prop.Prop.PropertyType) == null)
{
var defaultValue = Activator.CreateInstance(type);
if (object.Equals(defaultValue, o))
Expand Down
37 changes: 37 additions & 0 deletions test/Docfx.Build.RestApi.Tests/SwaggerCompatibilityTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,43 @@ namespace Docfx.Build.RestApi.Tests;
[Trait("Category", "SwaggerCompatibility")]
public class SwaggerCompatibilityTest : TestBase
{
[Fact]
public void PreservesSwagger2InfoMetadataValues()
{
var file = CreateFile("swagger.json", """
{
"swagger": "2.0",
"info": {
"title": "Compatibility", "version": "1",
"description": { "en": "Hello" },
"contact": "custom contact",
"custom": { "enabled": false, "values": [0, "", null] }
},
"tags": [{ "name": "items", "info": "tag metadata" }],
"paths": { "/items": { "get": {
"operationId": "getItems", "info": ["operation metadata", false]
} } }
}
""", GetRandomFolder());

// These values are accepted by the Swagger 2.0 parser, even outside the specification.
var swagger = SwaggerJsonParser.Parse(file);
AssertJson("""
{
"description": { "en": "Hello" },
"contact": "custom contact",
"custom": { "enabled": false, "values": [0, "", null] }
}
""", JToken.FromObject(swagger.Info.PatternedObjects));

var model = SwaggerModelConverter.FromSwaggerModel(swagger);
Assert.Equal("Compatibility", model.Name);
Assert.Equal("Compatibility/1", model.Uid);
Assert.Equal("tag metadata", Assert.Single(model.Tags).Metadata["info"]);
AssertJson("""["operation metadata", false]""",
Assert.IsType<JArray>(Assert.Single(model.Children).Metadata["info"]));
}

[Fact]
public void AllSevenSwaggerMethodsPreserveDocumentOrderAndOperationIdentity()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,122 @@ public class SwaggerOutputCompatibilityTest : TestBase
private const string RootUid = "api.example.test/v1/Compatibility API/1.0";
private const string RootHtmlId = "api_example_test_v1_Compatibility_API_1_0";

[Theory]
[InlineData(false, false, false)]
[InlineData(true, false, false)]
[InlineData(false, true, false)]
[InlineData(true, true, false)]
[InlineData(false, false, true)]
[InlineData(true, false, true)]
[InlineData(false, true, true)]
[InlineData(true, true, true)]
public void PreservesFreeMetadataThroughSplittingAndOverwrite(bool splitTags, bool splitOperations, bool overwrite)
{
var input = GetRandomFolder();
var output = GetRandomFolder();
var file = CreateFile("service.json", """
{
"swagger": "2.0",
"info": { "title": "Metadata", "version": "1.0", "description": { "en": "Hello" } },
"custom": { "owner": "root", "keep": false },
"inherited": { "values": [0, "", null] },
"tags": [{
"name": "items", "info": "tag metadata",
"custom": { "owner": "tag", "keep": false },
"tagOnly": { "enabled": false }
}],
"paths": { "/items": { "get": {
"operationId": "getItems", "tags": ["items"],
"info": ["operation metadata", false],
"custom": { "owner": "operation", "keep": false },
"responses": { "200": { "description": "OK" } }
} } }
}
""", input);
var files = new FileCollection(Directory.GetCurrentDirectory());
files.Add(DocumentType.Article, [file], input);
if (overwrite)
{
var overwriteFile = CreateFile("overwrite.md", """
---
uid: Metadata/1.0
info: root overwrite
custom:
owner: root overwrite
---

---
uid: Metadata/1.0/tag/items
info: tag overwrite
custom:
owner: tag overwrite
---

---
uid: Metadata/1.0/getItems
info: operation overwrite
custom:
owner: operation overwrite
---
""", input);
files.Add(DocumentType.Overwrite, [overwriteFile], input);
}

using var builder = new DocumentBuilder(GetAssemblies(splitTags, splitOperations), []);
builder.Build(new DocumentBuildParameters
{
Files = files,
OutputBaseDir = output,
ApplyTemplateSettings = new ApplyTemplateSettings(input, output)
{
TransformDocument = false,
RawModelExportSettings = { Export = true }
}
});

var root = ReadModel(output, "service.raw.json");
Assert.Equal(overwrite ? "root overwrite" : null, (string)root["info"]);
Assert.Equal(overwrite ? "root overwrite" : "root", (string)root["custom"]["owner"]);
Assert.False((bool)root["custom"]["keep"]);
Assert.True(JToken.DeepEquals(JObject.Parse("""{"values":[0,"",null]}"""), root["inherited"]));

var tag = splitTags ? ReadModel(output, "service/items.raw.json")
: splitOperations ? null : Assert.Single(root["tags"]);
if (tag != null)
{
Assert.Equal(overwrite ? "tag overwrite" : "tag metadata", (string)tag["info"]);
Assert.Equal(overwrite ? "tag overwrite" : "tag", (string)tag["custom"]["owner"]);
Assert.False((bool)tag["custom"]["keep"]);
Assert.False((bool)tag["tagOnly"]["enabled"]);
if (splitTags)
{
Assert.True(JToken.DeepEquals(root["inherited"], tag["inherited"]));
}
}

var operation = splitOperations
? ReadModel(output, splitTags ? "service/items/getItems.raw.json" : "service/getItems.raw.json")
: Assert.Single((splitTags ? tag : root)["children"]);
if (overwrite)
{
Assert.Equal("operation overwrite", (string)operation["info"]);
}
else
{
Assert.True(JToken.DeepEquals(JArray.Parse("""["operation metadata", false]"""), operation["info"]));
}
Assert.Equal(overwrite ? "operation overwrite" : "operation", (string)operation["custom"]["owner"]);
Assert.False((bool)operation["custom"]["keep"]);
if (splitOperations)
{
Assert.True(JToken.DeepEquals(root["inherited"], operation["inherited"]));
if (splitTags)
{
Assert.True(JToken.DeepEquals(tag["tagOnly"], operation["tagOnly"]));
}
}
}

[Theory]
[InlineData("trace")]
[InlineData("custom")]
Expand Down
20 changes: 20 additions & 0 deletions test/Docfx.Common.Tests/ReflectionEntityMergerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ public void TestReflectionEntityMergerWithBasicScenarios()
Assert.Same(overrides.Nested.Nested, sample.Nested.Nested);
}

[Fact]
public void NullableDefaultsAreExplicitOverwriteValues()
{
var sample = new NullableDefaults { Required = true, Count = 1 };
var merger = new MergerFacade(new ReflectionEntityMerger());
merger.Merge(ref sample, new NullableDefaults());
Assert.True(sample.Required);
Assert.Equal(1, sample.Count);
merger.Merge(ref sample, new NullableDefaults { Required = false, Count = 0 });
Assert.False(sample.Required);
Assert.Equal(0, sample.Count);
}

public class NullableDefaults
{
public bool? Required { get; set; }
[MergeOption(MergeOption.Replace)]
public int? Count { get; set; }
}

public class BasicSample
{
public int IntValue { get; set; }
Expand Down
Loading