Skip to content

Commit b537260

Browse files
authored
Add more preferences for configuring openapi search paths (#424)
* Add more preferences for configuring openapi search paths * Seal a couple classes, move a null check
1 parent 62713b0 commit b537260

File tree

6 files changed

+283
-34
lines changed

6 files changed

+283
-34
lines changed

src/Microsoft.HttpRepl/ApiConnection.cs

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,9 @@ namespace Microsoft.HttpRepl
1717
{
1818
internal class ApiConnection
1919
{
20-
// OpenAPI description search paths are appended to the base url to
21-
// attempt to find the description document. A search path is a
22-
// relative url that is appended to the base url using Uri.TryCreate,
23-
// so the semantics of relative urls matter here.
24-
// Example: Base path https://localhost/v1/ and search path openapi.json
25-
// will result in https://localhost/v1/openapi.json being tested.
26-
// Example: Base path https://localhost/v1/ and search path /openapi.json
27-
// will result in https://localhost/openapi.json being tested.
28-
private static readonly string[] OpenApiDescriptionSearchPaths = new[] {
29-
"swagger.json",
30-
"/swagger.json",
31-
"swagger/v1/swagger.json",
32-
"/swagger/v1/swagger.json",
33-
"openapi.json",
34-
"/openapi.json",
35-
};
36-
37-
private readonly IPreferences _preferences;
3820
private readonly IWritable _logger;
3921
private readonly bool _logVerboseMessages;
22+
private readonly IOpenApiSearchPathsProvider _searchPaths;
4023

4124
public Uri? RootUri { get; set; }
4225
public bool HasRootUri => RootUri is object;
@@ -48,11 +31,11 @@ internal class ApiConnection
4831
public bool HasSwaggerDocument => SwaggerDocument is object;
4932
public bool AllowBaseOverrideBySwagger { get; set; }
5033

51-
public ApiConnection(IPreferences preferences, IWritable logger, bool logVerboseMessages)
34+
public ApiConnection(IPreferences preferences, IWritable logger, bool logVerboseMessages, IOpenApiSearchPathsProvider? openApiSearchPaths = null)
5235
{
53-
_preferences = preferences ?? throw new ArgumentNullException(nameof(preferences));
5436
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
5537
_logVerboseMessages = logVerboseMessages;
38+
_searchPaths = openApiSearchPaths ?? new OpenApiSearchPathsProvider(preferences);
5639
}
5740

5841
private async Task FindSwaggerDoc(HttpClient client, IEnumerable<string> swaggerSearchPaths, CancellationToken cancellationToken)
@@ -157,7 +140,7 @@ public async Task SetupHttpState(HttpState httpState, bool performAutoDetect, Ca
157140
}
158141
else if (performAutoDetect)
159142
{
160-
await FindSwaggerDoc(httpState.Client, GetSwaggerSearchPaths(), cancellationToken);
143+
await FindSwaggerDoc(httpState.Client, _searchPaths.GetOpenApiSearchPaths(), cancellationToken);
161144
}
162145

163146
if (HasSwaggerDocument)
@@ -177,20 +160,7 @@ public async Task SetupHttpState(HttpState httpState, bool performAutoDetect, Ca
177160
}
178161
}
179162

180-
private IEnumerable<string> GetSwaggerSearchPaths()
181-
{
182-
string rawValue = _preferences.GetValue(WellKnownPreference.SwaggerSearchPaths);
183163

184-
if (rawValue is null)
185-
{
186-
return OpenApiDescriptionSearchPaths;
187-
}
188-
else
189-
{
190-
string[] paths = rawValue.Split('|', StringSplitOptions.RemoveEmptyEntries);
191-
return paths;
192-
}
193-
}
194164

195165
private void WriteVerbose(string s)
196166
{
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#nullable enable
5+
6+
using System.Collections.Generic;
7+
8+
namespace Microsoft.HttpRepl.OpenApi
9+
{
10+
internal interface IOpenApiSearchPathsProvider
11+
{
12+
IEnumerable<string> GetOpenApiSearchPaths();
13+
}
14+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#nullable enable
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using Microsoft.HttpRepl.OpenApi;
10+
11+
namespace Microsoft.HttpRepl.Preferences
12+
{
13+
internal sealed class OpenApiSearchPathsProvider : IOpenApiSearchPathsProvider
14+
{
15+
// OpenAPI description search paths are appended to the base url to
16+
// attempt to find the description document. A search path is a
17+
// relative url that is appended to the base url using Uri.TryCreate,
18+
// so the semantics of relative urls matter here.
19+
// Example: Base path https://localhost/v1/ and search path openapi.json
20+
// will result in https://localhost/v1/openapi.json being tested.
21+
// Example: Base path https://localhost/v1/ and search path /openapi.json
22+
// will result in https://localhost/openapi.json being tested.
23+
internal static IEnumerable<string> DefaultSearchPaths { get; } = new[] {
24+
"swagger.json",
25+
"/swagger.json",
26+
"swagger/v1/swagger.json",
27+
"/swagger/v1/swagger.json",
28+
"openapi.json",
29+
"/openapi.json",
30+
};
31+
32+
private readonly IPreferences _preferences;
33+
public OpenApiSearchPathsProvider(IPreferences preferences)
34+
{
35+
_preferences = preferences ?? throw new ArgumentNullException(nameof(preferences));
36+
}
37+
38+
public IEnumerable<string> GetOpenApiSearchPaths()
39+
{
40+
string[] configSearchPaths = Split(_preferences.GetValue(WellKnownPreference.SwaggerSearchPaths));
41+
42+
if (configSearchPaths.Length > 0)
43+
{
44+
return configSearchPaths;
45+
}
46+
47+
string[] addToSearchPaths = Split(_preferences.GetValue(WellKnownPreference.SwaggerAddToSearchPaths));
48+
string[] removeFromSearchPaths = Split(_preferences.GetValue(WellKnownPreference.SwaggerRemoveFromSearchPaths));
49+
50+
return DefaultSearchPaths.Union(addToSearchPaths).Except(removeFromSearchPaths);
51+
}
52+
53+
private static string[] Split(string searchPaths)
54+
{
55+
if (string.IsNullOrWhiteSpace(searchPaths))
56+
{
57+
return Array.Empty<string>();
58+
}
59+
else
60+
{
61+
return searchPaths.Split('|', StringSplitOptions.RemoveEmptyEntries);
62+
}
63+
}
64+
}
65+
}

src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ public static IReadOnlyList<string> Names
174174

175175
public static string SwaggerUIEndpoint { get; } = "swagger.uiEndpoint";
176176

177+
public static string SwaggerRemoveFromSearchPaths => "swagger.removeFromSearchPaths";
178+
179+
public static string SwaggerAddToSearchPaths => "swagger.addToSearchPaths";
180+
177181
public static string UseDefaultCredentials { get; } = "httpClient.useDefaultCredentials";
178182

179183
public static string HttpClientUserAgent { get; } = "httpClient.userAgent";
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.HttpRepl.Preferences;
7+
using Microsoft.Repl.ConsoleHandling;
8+
9+
namespace Microsoft.HttpRepl.Fakes
10+
{
11+
public sealed class FakePreferences : IPreferences
12+
{
13+
private readonly Dictionary<string, string> _currentPreferences;
14+
15+
public FakePreferences()
16+
{
17+
DefaultPreferences = new Dictionary<string, string>();
18+
_currentPreferences = new();
19+
}
20+
21+
public IReadOnlyDictionary<string, string> DefaultPreferences { get; }
22+
public IReadOnlyDictionary<string, string> CurrentPreferences => _currentPreferences;
23+
24+
public bool GetBoolValue(string preference, bool defaultValue = false)
25+
{
26+
if (CurrentPreferences.TryGetValue(preference, out string value) && bool.TryParse(value, out bool result))
27+
{
28+
return result;
29+
}
30+
31+
return defaultValue;
32+
}
33+
34+
public AllowedColors GetColorValue(string preference, AllowedColors defaultValue = AllowedColors.None)
35+
{
36+
if (CurrentPreferences.TryGetValue(preference, out string value) && Enum.TryParse(value, true, out AllowedColors result))
37+
{
38+
return result;
39+
}
40+
41+
return defaultValue;
42+
}
43+
44+
public int GetIntValue(string preference, int defaultValue = 0)
45+
{
46+
if (CurrentPreferences.TryGetValue(preference, out string value) && int.TryParse(value, out int result))
47+
{
48+
return result;
49+
}
50+
51+
return defaultValue;
52+
}
53+
54+
public string GetValue(string preference, string defaultValue = null)
55+
{
56+
if (CurrentPreferences.TryGetValue(preference, out string value))
57+
{
58+
return value;
59+
}
60+
61+
return defaultValue;
62+
}
63+
64+
public bool SetValue(string preference, string value)
65+
{
66+
_currentPreferences[preference] = value;
67+
return true;
68+
}
69+
70+
public bool TryGetValue(string preference, out string value) => CurrentPreferences.TryGetValue(preference, out value);
71+
}
72+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Microsoft.HttpRepl.Fakes;
8+
using Microsoft.HttpRepl.Preferences;
9+
using Xunit;
10+
11+
namespace Microsoft.HttpRepl.Tests.Preferences
12+
{
13+
public class OpenApiSearchPathsProviderTests
14+
{
15+
[Fact]
16+
public void WithNoOverrides_ReturnsDefault()
17+
{
18+
// Arrange
19+
NullPreferences preferences = new();
20+
OpenApiSearchPathsProvider provider = new(preferences);
21+
IEnumerable<string> expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths;
22+
23+
// Act
24+
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();
25+
26+
// Assert
27+
AssertPathLists(expectedPaths, paths);
28+
}
29+
30+
[Fact]
31+
public void WithFullOverride_ReturnsConfiguredOverride()
32+
{
33+
// Arrange
34+
string searchPathOverrides = "/red|/green|/blue";
35+
FakePreferences preferences = new();
36+
preferences.SetValue(WellKnownPreference.SwaggerSearchPaths, searchPathOverrides);
37+
OpenApiSearchPathsProvider provider = new(preferences);
38+
string[] expectedPaths = searchPathOverrides.Split('|');
39+
40+
// Act
41+
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();
42+
43+
// Assert
44+
AssertPathLists(expectedPaths, paths);
45+
}
46+
47+
[Fact]
48+
public void WithAdditions_ReturnsDefaultPlusAdditions()
49+
{
50+
// Arrange
51+
string[] searchPathAdditions = new[] { "/red", "/green", "/blue" };
52+
FakePreferences preferences = new();
53+
preferences.SetValue(WellKnownPreference.SwaggerAddToSearchPaths, string.Join('|', searchPathAdditions));
54+
OpenApiSearchPathsProvider provider = new(preferences);
55+
IEnumerable<string> expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths.Union(searchPathAdditions);
56+
57+
// Act
58+
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();
59+
60+
// Assert
61+
AssertPathLists(expectedPaths, paths);
62+
}
63+
64+
[Fact]
65+
public void WithRemovals_ReturnsDefaultMinusRemovals()
66+
{
67+
// Arrange
68+
string[] searchPathRemovals = new[] { "swagger.json", "/swagger.json", "swagger/v1/swagger.json", "/swagger/v1/swagger.json" };
69+
FakePreferences preferences = new();
70+
preferences.SetValue(WellKnownPreference.SwaggerRemoveFromSearchPaths, string.Join('|', searchPathRemovals));
71+
OpenApiSearchPathsProvider provider = new(preferences);
72+
IEnumerable<string> expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths.Except(searchPathRemovals);
73+
74+
// Act
75+
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();
76+
77+
// Assert
78+
AssertPathLists(expectedPaths, paths);
79+
}
80+
81+
[Fact]
82+
public void WithAdditionsAndRemovals_ReturnsCorrectSet()
83+
{
84+
// Arrange
85+
string[] searchPathAdditions = new[] { "/red", "/green", "/blue" };
86+
string[] searchPathRemovals = new[] { "swagger.json", "/swagger.json", "swagger/v1/swagger.json", "/swagger/v1/swagger.json" };
87+
FakePreferences preferences = new();
88+
preferences.SetValue(WellKnownPreference.SwaggerAddToSearchPaths, string.Join('|', searchPathAdditions));
89+
preferences.SetValue(WellKnownPreference.SwaggerRemoveFromSearchPaths, string.Join('|', searchPathRemovals));
90+
OpenApiSearchPathsProvider provider = new(preferences);
91+
IEnumerable<string> expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths.Union(searchPathAdditions).Except(searchPathRemovals);
92+
93+
// Act
94+
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();
95+
96+
// Assert
97+
AssertPathLists(expectedPaths, paths);
98+
}
99+
100+
private static void AssertPathLists(IEnumerable<string> expectedPaths, IEnumerable<string> paths)
101+
{
102+
Assert.NotNull(expectedPaths);
103+
Assert.NotNull(paths);
104+
105+
IEnumerator<string> expectedPathsEnumerator = expectedPaths.GetEnumerator();
106+
IEnumerator<string> pathsEnumerator = paths.GetEnumerator();
107+
108+
while (expectedPathsEnumerator.MoveNext())
109+
{
110+
Assert.True(pathsEnumerator.MoveNext(), $"Missing path \"{expectedPathsEnumerator.Current}\"");
111+
Assert.Equal(expectedPathsEnumerator.Current, pathsEnumerator.Current, StringComparer.Ordinal);
112+
}
113+
114+
if (pathsEnumerator.MoveNext())
115+
{
116+
// We can't do a one-liner here like the Missing path version above because
117+
// the order the second parameter is evaluated regardless of the result of the
118+
// evaluation of the first parameter. Also xUnit doesn't have an Assert.Fail,
119+
// so we have to use Assert.True(false) per their comparison chart.
120+
Assert.True(false, $"Extra path \"{pathsEnumerator.Current}\"");
121+
}
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)