diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs
index 397062aca..45a85d3ab 100644
--- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs
+++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Text.Json.Nodes;
@@ -27,7 +28,16 @@ public abstract class OpenApiVisitorBase
/// Identifier for context
public virtual void Enter(string segment)
{
- this._path.Push(segment);
+ if (string.IsNullOrEmpty(segment))
+ {
+ this._path.Push(string.Empty);
+ return;
+ }
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER
+ this._path.Push(segment.Replace("~", "~0", StringComparison.Ordinal).Replace("/", "~1", StringComparison.OrdinalIgnoreCase));
+#else
+ this._path.Push(segment.Replace("~", "~0").Replace("/", "~1"));
+#endif
}
///
@@ -41,7 +51,7 @@ public virtual void Exit()
///
/// Pointer to source of validation error in document
///
- public string PathString { get => "#/" + String.Join("/", _path.Reverse()); }
+ public string PathString { get => "#/" + string.Join("/", _path.Reverse()); }
///
/// Visits
diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs
index c8e5d72d7..01172cdd1 100644
--- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs
+++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs
@@ -1297,15 +1297,6 @@ internal void Walk(IOpenApiElement element)
}
}
- private static string ReplaceSlashes(string value)
- {
-#if NET8_0_OR_GREATER
- return value.Replace("/", "~1", StringComparison.Ordinal);
-#else
- return value.Replace("/", "~1");
-#endif
- }
-
///
/// Adds a segment to the context path to enable pointing to the current location in the document
///
@@ -1315,7 +1306,7 @@ private static string ReplaceSlashes(string value)
/// An action that walks objects within the context.
private void WalkItem(string context, T state, Action walk)
{
- _visitor.Enter(ReplaceSlashes(context));
+ _visitor.Enter(context);
walk(this, state);
_visitor.Exit();
}
@@ -1330,7 +1321,7 @@ private void WalkItem(string context, T state, Action walk)
/// An action that walks objects within the context.
private void WalkItem(string context, T state, Action walk, bool isComponent)
{
- _visitor.Enter(ReplaceSlashes(context));
+ _visitor.Enter(context);
walk(this, state, isComponent);
_visitor.Exit();
}
@@ -1351,7 +1342,7 @@ private void WalkDictionary(
{
if (state != null && state.Count > 0)
{
- _visitor.Enter(ReplaceSlashes(context));
+ _visitor.Enter(context);
foreach (var item in state)
{
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs
index a1f166ee3..38767035f 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs
@@ -2,6 +2,8 @@
// Licensed under the MIT license.
using System;
+using System.Collections.Generic;
+using System.Linq;
namespace Microsoft.OpenApi
{
@@ -74,28 +76,49 @@ private void ValidateSchemaReference(OpenApiSchemaReference reference)
{
if (reference.RecursiveTarget is null)
{
+ var segments = GetSegments().ToArray();
+ EnterSegments(segments);
// The reference was not followed to a valid schema somewhere in the document
- context.Enter(GetSegment());
context.CreateWarning(ruleName, string.Format(SRResource.Validation_SchemaReferenceDoesNotExist, reference.Reference.ReferenceV3));
- context.Exit();
+ ExitSegments(segments.Length);
}
}
catch (InvalidOperationException ex)
{
- context.Enter(GetSegment());
+ var segments = GetSegments().ToArray();
+ EnterSegments(segments);
context.CreateWarning(ruleName, ex.Message);
- context.Exit();
+ ExitSegments(segments.Length);
}
- string GetSegment()
+ void ExitSegments(int length)
{
- // Trim off the leading "#/" as the context is already at the root of the document
- return
-#if NET8_0_OR_GREATER
- $"{PathString[2..]}/$ref";
+ for (var i = 0; i < length; i++)
+ {
+ context.Exit();
+ }
+ }
+
+ void EnterSegments(string[] segments)
+ {
+ foreach (var segment in segments)
+ {
+ context.Enter(segment);
+ }
+ }
+
+ IEnumerable GetSegments()
+ {
+ foreach (var segment in this.PathString.Substring(2).Split('/'))
+ {
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER
+ yield return segment.Replace("~1", "/", StringComparison.OrdinalIgnoreCase).Replace("~0", "~", StringComparison.OrdinalIgnoreCase);
#else
- PathString.Substring(2) + "/$ref";
+ yield return segment.Replace("~1", "/").Replace("~0", "~");
#endif
+ }
+ yield return "$ref";
+ // Trim off the leading "#/" as the context is already at the root of the document
}
}
}
diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs
new file mode 100644
index 000000000..ce92819f3
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiVisitorBaseTests.cs
@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using Xunit;
+
+namespace Microsoft.OpenApi.Tests.Services;
+
+public class OpenApiVisitorBaseTests
+{
+ [Fact]
+ public void EncodesReservedCharacters()
+ {
+ // Given
+ var openApiDocument = new OpenApiDocument
+ {
+ Info = new()
+ {
+ Title = "foo",
+ Version = "1.2.2"
+ },
+ Paths = new()
+ {
+ },
+ Components = new()
+ {
+ Schemas = new Dictionary()
+ {
+ ["Pet~"] = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.Object
+ },
+ ["Pet/"] = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.Object
+ },
+ }
+ }
+ };
+ var visitor = new LocatorVisitor();
+
+ // When
+ visitor.Visit(openApiDocument);
+
+ // Then
+ Assert.Equivalent(
+ new List
+ {
+ "#/components/schemas/Pet~0",
+ "#/components/schemas/Pet~1"
+ }, visitor.Locations);
+ }
+
+ private class LocatorVisitor : OpenApiVisitorBase
+ {
+ public List Locations { get; } = new List();
+
+ public override void Visit(IOpenApiSchema openApiSchema)
+ {
+ Locations.Add(this.PathString);
+ }
+ public override void Visit(OpenApiComponents components)
+ {
+ Enter("schemas");
+ if (components.Schemas != null)
+ {
+ foreach (var schemaKvp in components.Schemas)
+ {
+ Enter(schemaKvp.Key);
+ this.Visit(schemaKvp.Value);
+ Exit();
+ }
+ }
+ Exit();
+ }
+ public override void Visit(OpenApiDocument doc)
+ {
+ Enter("components");
+ Visit(doc.Components);
+ Exit();
+ }
+ }
+}
diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs
index c5aa3533d..13d710909 100644
--- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiRecommendedRulesTests.cs
@@ -200,7 +200,7 @@ public static void GetOperationWithRequestBodyIsInvalid()
Assert.NotNull(warnings);
var warning = Assert.Single(warnings);
Assert.Equal("GET operations should not have a request body.", warning.Message);
- Assert.Equal("#/paths//people/get/requestBody", warning.Pointer);
+ Assert.Equal("#/paths/~1people/get/requestBody", warning.Pointer);
}
[Fact]