Skip to content

Commit 6fed45d

Browse files
committed
Merge pull request #2783 from microsoft/fix/key-location-encoding
fix/key location encoding
1 parent f7e17e5 commit 6fed45d

File tree

5 files changed

+129
-25
lines changed

5 files changed

+129
-25
lines changed

src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Collections.ObjectModel;
67
using System.Linq;
78
using System.Net.Http;
89
using System.Text.Json.Nodes;
@@ -27,7 +28,16 @@ public abstract class OpenApiVisitorBase
2728
/// <param name="segment">Identifier for context</param>
2829
public virtual void Enter(string segment)
2930
{
30-
this._path.Push(segment);
31+
if (string.IsNullOrEmpty(segment))
32+
{
33+
this._path.Push(string.Empty);
34+
return;
35+
}
36+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER
37+
this._path.Push(segment.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.OrdinalIgnoreCase));
38+
#else
39+
this._path.Push(segment.Replace("~", "~0").Replace("/", "~1"));
40+
#endif
3141
}
3242

3343
/// <summary>
@@ -41,7 +51,7 @@ public virtual void Exit()
4151
/// <summary>
4252
/// Pointer to source of validation error in document
4353
/// </summary>
44-
public string PathString { get => "#/" + String.Join("/", _path.Reverse()); }
54+
public string PathString { get => "#/" + string.Join("/", _path.Reverse()); }
4555

4656
/// <summary>
4757
/// Visits <see cref="OpenApiDocument"/>

src/Microsoft.OpenApi/Services/OpenApiWalker.cs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,15 +1276,6 @@ internal void Walk(IOpenApiElement element)
12761276
}
12771277
}
12781278

1279-
private static string ReplaceSlashes(string value)
1280-
{
1281-
#if NET8_0_OR_GREATER
1282-
return value.Replace("/", "~1", StringComparison.Ordinal);
1283-
#else
1284-
return value.Replace("/", "~1");
1285-
#endif
1286-
}
1287-
12881279
/// <summary>
12891280
/// Adds a segment to the context path to enable pointing to the current location in the document
12901281
/// </summary>
@@ -1294,7 +1285,7 @@ private static string ReplaceSlashes(string value)
12941285
/// <param name="walk">An action that walks objects within the context.</param>
12951286
private void WalkItem<T>(string context, T state, Action<OpenApiWalker, T> walk)
12961287
{
1297-
_visitor.Enter(ReplaceSlashes(context));
1288+
_visitor.Enter(context);
12981289
walk(this, state);
12991290
_visitor.Exit();
13001291
}
@@ -1309,7 +1300,7 @@ private void WalkItem<T>(string context, T state, Action<OpenApiWalker, T> walk)
13091300
/// <param name="walk">An action that walks objects within the context.</param>
13101301
private void WalkItem<T>(string context, T state, Action<OpenApiWalker, T, bool> walk, bool isComponent)
13111302
{
1312-
_visitor.Enter(ReplaceSlashes(context));
1303+
_visitor.Enter(context);
13131304
walk(this, state, isComponent);
13141305
_visitor.Exit();
13151306
}
@@ -1330,7 +1321,7 @@ private void WalkDictionary<T>(
13301321
{
13311322
if (state != null && state.Count > 0)
13321323
{
1333-
_visitor.Enter(ReplaceSlashes(context));
1324+
_visitor.Enter(context);
13341325

13351326
foreach (var item in state)
13361327
{

src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the MIT license.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
57

68
namespace Microsoft.OpenApi
79
{
@@ -74,28 +76,49 @@ private void ValidateSchemaReference(OpenApiSchemaReference reference)
7476
{
7577
if (reference.RecursiveTarget is null)
7678
{
79+
var segments = GetSegments().ToArray();
80+
EnterSegments(segments);
7781
// The reference was not followed to a valid schema somewhere in the document
78-
context.Enter(GetSegment());
7982
context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, reference.Reference.ReferenceV3));
80-
context.Exit();
83+
ExitSegments(segments.Length);
8184
}
8285
}
8386
catch (InvalidOperationException ex)
8487
{
85-
context.Enter(GetSegment());
88+
var segments = GetSegments().ToArray();
89+
EnterSegments(segments);
8690
context.CreateWarning(ruleName, ex.Message);
87-
context.Exit();
91+
ExitSegments(segments.Length);
8892
}
8993

90-
string GetSegment()
94+
void ExitSegments(int length)
9195
{
92-
// Trim off the leading "#/" as the context is already at the root of the document
93-
return
94-
#if NET8_0_OR_GREATER
95-
$"{PathString[2..]}/$ref";
96+
for (var i = 0; i < length; i++)
97+
{
98+
context.Exit();
99+
}
100+
}
101+
102+
void EnterSegments(string[] segments)
103+
{
104+
foreach (var segment in segments)
105+
{
106+
context.Enter(segment);
107+
}
108+
}
109+
110+
IEnumerable<string> GetSegments()
111+
{
112+
foreach (var segment in this.PathString.Substring(2).Split('/'))
113+
{
114+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER
115+
yield return segment.Replace("~1", "/", StringComparison.OrdinalIgnoreCase).Replace("~0", "~", StringComparison.OrdinalIgnoreCase);
96116
#else
97-
PathString.Substring(2) + "/$ref";
117+
yield return segment.Replace("~1", "/").Replace("~0", "~");
98118
#endif
119+
}
120+
yield return "$ref";
121+
// Trim off the leading "#/" as the context is already at the root of the document
99122
}
100123
}
101124
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.Collections.Generic;
2+
using Xunit;
3+
4+
namespace Microsoft.OpenApi.Tests.Services;
5+
6+
public class OpenApiVisitorBaseTests
7+
{
8+
[Fact]
9+
public void EncodesReservedCharacters()
10+
{
11+
// Given
12+
var openApiDocument = new OpenApiDocument
13+
{
14+
Info = new()
15+
{
16+
Title = "foo",
17+
Version = "1.2.2"
18+
},
19+
Paths = new()
20+
{
21+
},
22+
Components = new()
23+
{
24+
Schemas = new Dictionary<string, IOpenApiSchema>()
25+
{
26+
["Pet~"] = new OpenApiSchema()
27+
{
28+
Type = JsonSchemaType.Object
29+
},
30+
["Pet/"] = new OpenApiSchema()
31+
{
32+
Type = JsonSchemaType.Object
33+
},
34+
}
35+
}
36+
};
37+
var visitor = new LocatorVisitor();
38+
39+
// When
40+
visitor.Visit(openApiDocument);
41+
42+
// Then
43+
Assert.Equivalent(
44+
new List<string>
45+
{
46+
"#/components/schemas/Pet~0",
47+
"#/components/schemas/Pet~1"
48+
}, visitor.Locations);
49+
}
50+
51+
private class LocatorVisitor : OpenApiVisitorBase
52+
{
53+
public List<string> Locations { get; } = new List<string>();
54+
55+
public override void Visit(IOpenApiSchema openApiSchema)
56+
{
57+
Locations.Add(this.PathString);
58+
}
59+
public override void Visit(OpenApiComponents components)
60+
{
61+
Enter("schemas");
62+
if (components.Schemas != null)
63+
{
64+
foreach (var schemaKvp in components.Schemas)
65+
{
66+
Enter(schemaKvp.Key);
67+
this.Visit(schemaKvp.Value);
68+
Exit();
69+
}
70+
}
71+
Exit();
72+
}
73+
public override void Visit(OpenApiDocument doc)
74+
{
75+
Enter("components");
76+
Visit(doc.Components);
77+
Exit();
78+
}
79+
}
80+
}

test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public static void GetOperationWithRequestBodyIsInvalid()
200200
Assert.NotNull(warnings);
201201
var warning = Assert.Single(warnings);
202202
Assert.Equal("GET operations should not have a request body.", warning.Message);
203-
Assert.Equal("#/paths//people/get/requestBody", warning.Pointer);
203+
Assert.Equal("#/paths/~1people/get/requestBody", warning.Pointer);
204204
}
205205

206206
[Fact]

0 commit comments

Comments
 (0)