Skip to content

Commit 60869f6

Browse files
authored
Merge pull request #520 from simon-pearson/feature/openapi-workspace-fragments
OpenAPI Workspace Document Fragments
2 parents 46df7d2 + 62b7e63 commit 60869f6

File tree

6 files changed

+282
-15
lines changed

6 files changed

+282
-15
lines changed

src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic)
4545
/// <param name="version">Version of the OpenAPI specification that the fragment conforms to.</param>
4646
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
4747
/// <returns>Instance of newly created OpenApiDocument</returns>
48-
public T ReadFragment<T>(Stream input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic) where T : IOpenApiElement
48+
public T ReadFragment<T>(Stream input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic) where T : IOpenApiReferenceable
4949
{
5050
using (var reader = new StreamReader(input))
5151
{
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using Microsoft.OpenApi.Exceptions;
7+
using Microsoft.OpenApi.Interfaces;
8+
using Microsoft.OpenApi.Models;
9+
using Microsoft.OpenApi.Properties;
10+
11+
namespace Microsoft.OpenApi.Extensions
12+
{
13+
/// <summary>
14+
/// Extension methods for resolving references on <see cref="IOpenApiReferenceable"/> elements.
15+
/// </summary>
16+
public static class OpenApiReferencableExtensions
17+
{
18+
/// <summary>
19+
/// Resolves a JSON Pointer with respect to an element, returning the referenced element.
20+
/// </summary>
21+
/// <param name="element">The referencable Open API element on which to apply the JSON pointer</param>
22+
/// <param name="pointer">a JSON Pointer [RFC 6901](https://tools.ietf.org/html/rfc6901).</param>
23+
/// <returns>The element pointed to by the JSON pointer.</returns>
24+
public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable element, JsonPointer pointer)
25+
{
26+
if (!pointer.Tokens.Any())
27+
{
28+
return element;
29+
}
30+
var propertyName = pointer.Tokens.FirstOrDefault();
31+
var mapKey = pointer.Tokens.ElementAtOrDefault(1);
32+
try
33+
{
34+
if (element.GetType() == typeof(OpenApiHeader))
35+
{
36+
return ResolveReferenceOnHeaderElement((OpenApiHeader)element, propertyName, mapKey, pointer);
37+
}
38+
if (element.GetType() == typeof(OpenApiParameter))
39+
{
40+
return ResolveReferenceOnParameterElement((OpenApiParameter)element, propertyName, mapKey, pointer);
41+
}
42+
if (element.GetType() == typeof(OpenApiResponse))
43+
{
44+
return ResolveReferenceOnResponseElement((OpenApiResponse)element, propertyName, mapKey, pointer);
45+
}
46+
}
47+
catch (KeyNotFoundException)
48+
{
49+
throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer));
50+
}
51+
throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer));
52+
}
53+
54+
private static IOpenApiReferenceable ResolveReferenceOnHeaderElement(
55+
OpenApiHeader headerElement,
56+
string propertyName,
57+
string mapKey,
58+
JsonPointer pointer)
59+
{
60+
switch (propertyName)
61+
{
62+
case OpenApiConstants.Schema:
63+
return headerElement.Schema;
64+
case OpenApiConstants.Examples when mapKey != null:
65+
return headerElement.Examples[mapKey];
66+
default:
67+
throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer));
68+
}
69+
}
70+
71+
private static IOpenApiReferenceable ResolveReferenceOnParameterElement(
72+
OpenApiParameter parameterElement,
73+
string propertyName,
74+
string mapKey,
75+
JsonPointer pointer)
76+
{
77+
switch (propertyName)
78+
{
79+
case OpenApiConstants.Schema:
80+
return parameterElement.Schema;
81+
case OpenApiConstants.Examples when mapKey != null:
82+
return parameterElement.Examples[mapKey];
83+
default:
84+
throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer));
85+
}
86+
}
87+
88+
private static IOpenApiReferenceable ResolveReferenceOnResponseElement(
89+
OpenApiResponse responseElement,
90+
string propertyName,
91+
string mapKey,
92+
JsonPointer pointer)
93+
{
94+
switch (propertyName)
95+
{
96+
case OpenApiConstants.Headers when mapKey != null:
97+
return responseElement.Headers[mapKey];
98+
case OpenApiConstants.Links when mapKey != null:
99+
return responseElement.Links[mapKey];
100+
default:
101+
throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer));
102+
}
103+
}
104+
}
105+
}

src/Microsoft.OpenApi/JsonPointer.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public class JsonPointer
1717
/// <param name="pointer">Pointer as string.</param>
1818
public JsonPointer(string pointer)
1919
{
20-
Tokens = pointer.Split('/').Skip(1).Select(Decode).ToArray();
20+
Tokens = string.IsNullOrEmpty(pointer) || pointer == "/"
21+
? new string[0]
22+
: pointer.Split('/').Skip(1).Select(Decode).ToArray();
2123
}
2224

2325
/// <summary>

src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Text;
99
using System.Threading.Tasks;
10+
using Microsoft.OpenApi.Extensions;
1011
using Microsoft.OpenApi.Interfaces;
1112
using Microsoft.OpenApi.Models;
1213

@@ -18,7 +19,7 @@ namespace Microsoft.OpenApi.Services
1819
public class OpenApiWorkspace
1920
{
2021
private Dictionary<Uri, OpenApiDocument> _documents = new Dictionary<Uri, OpenApiDocument>();
21-
private Dictionary<Uri, IOpenApiElement> _fragments = new Dictionary<Uri, IOpenApiElement>();
22+
private Dictionary<Uri, IOpenApiReferenceable> _fragments = new Dictionary<Uri, IOpenApiReferenceable>();
2223
private Dictionary<Uri, Stream> _artifacts = new Dictionary<Uri, Stream>();
2324

2425
/// <summary>
@@ -92,7 +93,7 @@ public void AddDocument(string location, OpenApiDocument document)
9293
/// <remarks>Not sure how this is going to work. Does the reference just point to the fragment as a whole, or do we need to
9394
/// to be able to point into the fragment. Keeping it private until we figure it out.
9495
/// </remarks>
95-
private void AddFragment(string location, IOpenApiElement fragment)
96+
public void AddFragment(string location, IOpenApiReferenceable fragment)
9697
{
9798
_fragments.Add(ToLocationUrl(location), fragment);
9899
}
@@ -114,21 +115,14 @@ public void AddArtifact(string location, Stream artifact)
114115
/// <returns></returns>
115116
public IOpenApiReferenceable ResolveReference(OpenApiReference reference)
116117
{
117-
if (_documents.TryGetValue(new Uri(BaseUrl,reference.ExternalResource),out var doc))
118+
if (_documents.TryGetValue(new Uri(BaseUrl, reference.ExternalResource), out var doc))
118119
{
119-
return doc.ResolveReference(reference, true);
120+
return doc.ResolveReference(reference, true);
120121
}
121122
else if (_fragments.TryGetValue(new Uri(BaseUrl, reference.ExternalResource), out var fragment))
122123
{
123-
var frag = fragment as IOpenApiReferenceable;
124-
if (frag != null)
125-
{
126-
return null; // frag.ResolveReference(reference, true); // IOpenApiElement needs to implement ResolveReference
127-
}
128-
else
129-
{
130-
return null;
131-
}
124+
var jsonPointer = new JsonPointer($"/{reference.Id ?? string.Empty}");
125+
return fragment.ResolveReference(jsonPointer);
132126
}
133127
return null;
134128
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.OpenApi.Exceptions;
7+
using Microsoft.OpenApi.Extensions;
8+
using Microsoft.OpenApi.Interfaces;
9+
using Microsoft.OpenApi.Models;
10+
using Microsoft.OpenApi.Properties;
11+
using Xunit;
12+
13+
namespace Microsoft.OpenApi.Tests.Workspaces
14+
{
15+
16+
public class OpenApiReferencableTests
17+
{
18+
private static readonly OpenApiCallback _callbackFragment = new OpenApiCallback();
19+
private static readonly OpenApiExample _exampleFragment = new OpenApiExample();
20+
private static readonly OpenApiLink _linkFragment = new OpenApiLink();
21+
private static readonly OpenApiHeader _headerFragment = new OpenApiHeader()
22+
{
23+
Schema = new OpenApiSchema(),
24+
Examples = new Dictionary<string, OpenApiExample>
25+
{
26+
{ "example1", new OpenApiExample() }
27+
}
28+
};
29+
private static readonly OpenApiParameter _parameterFragment = new OpenApiParameter
30+
{
31+
Schema = new OpenApiSchema(),
32+
Examples = new Dictionary<string, OpenApiExample>
33+
{
34+
{ "example1", new OpenApiExample() }
35+
}
36+
};
37+
private static readonly OpenApiRequestBody _requestBodyFragment = new OpenApiRequestBody();
38+
private static readonly OpenApiResponse _responseFragment = new OpenApiResponse()
39+
{
40+
Headers = new Dictionary<string, OpenApiHeader>
41+
{
42+
{ "header1", new OpenApiHeader() }
43+
},
44+
Links = new Dictionary<string, OpenApiLink>
45+
{
46+
{ "link1", new OpenApiLink() }
47+
}
48+
};
49+
private static readonly OpenApiSchema _schemaFragment = new OpenApiSchema();
50+
private static readonly OpenApiSecurityScheme _securitySchemeFragment = new OpenApiSecurityScheme();
51+
private static readonly OpenApiTag _tagFragment = new OpenApiTag();
52+
53+
public static IEnumerable<object[]> ResolveReferenceCanResolveValidJsonPointersTestData =>
54+
new List<object[]>
55+
{
56+
new object[] { _callbackFragment, "/", _callbackFragment },
57+
new object[] { _exampleFragment, "/", _exampleFragment },
58+
new object[] { _linkFragment, "/", _linkFragment },
59+
new object[] { _headerFragment, "/", _headerFragment },
60+
new object[] { _headerFragment, "/schema", _headerFragment.Schema },
61+
new object[] { _headerFragment, "/examples/example1", _headerFragment.Examples["example1"] },
62+
new object[] { _parameterFragment, "/", _parameterFragment },
63+
new object[] { _parameterFragment, "/schema", _parameterFragment.Schema },
64+
new object[] { _parameterFragment, "/examples/example1", _parameterFragment.Examples["example1"] },
65+
new object[] { _requestBodyFragment, "/", _requestBodyFragment },
66+
new object[] { _responseFragment, "/", _responseFragment },
67+
new object[] { _responseFragment, "/headers/header1", _responseFragment.Headers["header1"] },
68+
new object[] { _responseFragment, "/links/link1", _responseFragment.Links["link1"] },
69+
new object[] { _schemaFragment, "/", _schemaFragment},
70+
new object[] { _securitySchemeFragment, "/", _securitySchemeFragment},
71+
new object[] { _tagFragment, "/", _tagFragment}
72+
};
73+
74+
[Theory]
75+
[MemberData(nameof(ResolveReferenceCanResolveValidJsonPointersTestData))]
76+
public void ResolveReferenceCanResolveValidJsonPointers(
77+
IOpenApiReferenceable element,
78+
string jsonPointer,
79+
IOpenApiElement expectedResolvedElement)
80+
{
81+
// Act
82+
var actualResolvedElement = element.ResolveReference(new JsonPointer(jsonPointer));
83+
84+
// Assert
85+
Assert.Same(expectedResolvedElement, actualResolvedElement);
86+
}
87+
88+
public static IEnumerable<object[]> ResolveReferenceShouldThrowOnInvalidReferenceIdTestData =>
89+
new List<object[]>
90+
{
91+
new object[] { _callbackFragment, "/a" },
92+
new object[] { _headerFragment, "/a" },
93+
new object[] { _headerFragment, "/examples" },
94+
new object[] { _headerFragment, "/examples/" },
95+
new object[] { _headerFragment, "/examples/a" },
96+
new object[] { _parameterFragment, "/a" },
97+
new object[] { _parameterFragment, "/examples" },
98+
new object[] { _parameterFragment, "/examples/" },
99+
new object[] { _parameterFragment, "/examples/a" },
100+
new object[] { _responseFragment, "/a" },
101+
new object[] { _responseFragment, "/headers" },
102+
new object[] { _responseFragment, "/headers/" },
103+
new object[] { _responseFragment, "/headers/a" },
104+
new object[] { _responseFragment, "/content" },
105+
new object[] { _responseFragment, "/content/" },
106+
new object[] { _responseFragment, "/content/a" }
107+
108+
};
109+
110+
[Theory]
111+
[MemberData(nameof(ResolveReferenceShouldThrowOnInvalidReferenceIdTestData))]
112+
public void ResolveReferenceShouldThrowOnInvalidReferenceId(IOpenApiReferenceable element, string jsonPointer)
113+
{
114+
// Act
115+
Action resolveReference = () => element.ResolveReference(new JsonPointer(jsonPointer));
116+
117+
// Assert
118+
var exception = Assert.Throws<OpenApiException>(resolveReference);
119+
Assert.Equal(string.Format(SRResource.InvalidReferenceId, jsonPointer), exception.Message);
120+
}
121+
}
122+
}

test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,50 @@ public void OpenApiWorkspacesShouldLoadDocumentFragments()
155155
Assert.True(false);
156156
}
157157

158+
[Fact]
159+
public void OpenApiWorkspacesCanResolveReferencesToDocumentFragments()
160+
{
161+
// Arrange
162+
var workspace = new OpenApiWorkspace();
163+
var schemaFragment = new OpenApiSchema { Type = "string", Description = "Schema from a fragment" };
164+
workspace.AddFragment("fragment", schemaFragment);
165+
166+
// Act
167+
var schema = workspace.ResolveReference(new OpenApiReference()
168+
{
169+
ExternalResource = "fragment"
170+
}) as OpenApiSchema;
171+
172+
// Assert
173+
Assert.NotNull(schema);
174+
Assert.Equal("Schema from a fragment", schema.Description);
175+
}
176+
177+
[Fact]
178+
public void OpenApiWorkspacesCanResolveReferencesToDocumentFragmentsWithJsonPointers()
179+
{
180+
// Arrange
181+
var workspace = new OpenApiWorkspace();
182+
var responseFragment = new OpenApiResponse()
183+
{
184+
Headers = new Dictionary<string, OpenApiHeader>
185+
{
186+
{ "header1", new OpenApiHeader() }
187+
}
188+
};
189+
workspace.AddFragment("fragment", responseFragment);
190+
191+
// Act
192+
var resolvedElement = workspace.ResolveReference(new OpenApiReference()
193+
{
194+
Id = "headers/header1",
195+
ExternalResource = "fragment"
196+
});
197+
198+
// Assert
199+
Assert.Same(responseFragment.Headers["header1"], resolvedElement);
200+
}
201+
158202

159203
// Test artifacts
160204

0 commit comments

Comments
 (0)