Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more preferences for configuring openapi search paths #424

Merged
merged 2 commits into from
Nov 2, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
39 changes: 5 additions & 34 deletions src/Microsoft.HttpRepl/ApiConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,9 @@ namespace Microsoft.HttpRepl
{
internal class ApiConnection
{
// OpenAPI description search paths are appended to the base url to
// attempt to find the description document. A search path is a
// relative url that is appended to the base url using Uri.TryCreate,
// so the semantics of relative urls matter here.
// Example: Base path https://localhost/v1/ and search path openapi.json
// will result in https://localhost/v1/openapi.json being tested.
// Example: Base path https://localhost/v1/ and search path /openapi.json
// will result in https://localhost/openapi.json being tested.
private static readonly string[] OpenApiDescriptionSearchPaths = new[] {
"swagger.json",
"/swagger.json",
"swagger/v1/swagger.json",
"/swagger/v1/swagger.json",
"openapi.json",
"/openapi.json",
};

private readonly IPreferences _preferences;
private readonly IWritable _logger;
private readonly bool _logVerboseMessages;
private readonly IOpenApiSearchPathsProvider _searchPaths;

public Uri? RootUri { get; set; }
public bool HasRootUri => RootUri is object;
Expand All @@ -48,11 +31,12 @@ internal class ApiConnection
public bool HasSwaggerDocument => SwaggerDocument is object;
public bool AllowBaseOverrideBySwagger { get; set; }

public ApiConnection(IPreferences preferences, IWritable logger, bool logVerboseMessages)
public ApiConnection(IPreferences preferences, IWritable logger, bool logVerboseMessages, IOpenApiSearchPathsProvider? openApiSearchPaths = null)
{
_preferences = preferences ?? throw new ArgumentNullException(nameof(preferences));
_ = preferences ?? throw new ArgumentNullException(nameof(preferences));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a little funky, though I understand the intent. Maybe put the ArgumentNullException check in the ctor of OpenApiSearchPathsProvider instead where it's actually used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming the "looks a little funky" was in reference to using the discard. According to Jimmy, it saves a few ops!

But yeah, since I also changed it to not store the value anymore and just pass it, moving it makes sense. Done.

_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_logVerboseMessages = logVerboseMessages;
_searchPaths = openApiSearchPaths ?? new OpenApiSearchPathsProvider(preferences);
}

private async Task FindSwaggerDoc(HttpClient client, IEnumerable<string> swaggerSearchPaths, CancellationToken cancellationToken)
Expand Down Expand Up @@ -157,7 +141,7 @@ public async Task SetupHttpState(HttpState httpState, bool performAutoDetect, Ca
}
else if (performAutoDetect)
{
await FindSwaggerDoc(httpState.Client, GetSwaggerSearchPaths(), cancellationToken);
await FindSwaggerDoc(httpState.Client, _searchPaths.GetOpenApiSearchPaths(), cancellationToken);
}

if (HasSwaggerDocument)
Expand All @@ -177,20 +161,7 @@ public async Task SetupHttpState(HttpState httpState, bool performAutoDetect, Ca
}
}

private IEnumerable<string> GetSwaggerSearchPaths()
{
string rawValue = _preferences.GetValue(WellKnownPreference.SwaggerSearchPaths);

if (rawValue is null)
{
return OpenApiDescriptionSearchPaths;
}
else
{
string[] paths = rawValue.Split('|', StringSplitOptions.RemoveEmptyEntries);
return paths;
}
}

private void WriteVerbose(string s)
{
Expand Down
14 changes: 14 additions & 0 deletions src/Microsoft.HttpRepl/OpenApi/IOpenApiSearchPathsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using System.Collections.Generic;

namespace Microsoft.HttpRepl.OpenApi
{
internal interface IOpenApiSearchPathsProvider
{
IEnumerable<string> GetOpenApiSearchPaths();
}
}
65 changes: 65 additions & 0 deletions src/Microsoft.HttpRepl/Preferences/OpenApiSearchPathsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.HttpRepl.OpenApi;

namespace Microsoft.HttpRepl.Preferences
{
internal class OpenApiSearchPathsProvider : IOpenApiSearchPathsProvider
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: sealed?

{
// OpenAPI description search paths are appended to the base url to
// attempt to find the description document. A search path is a
// relative url that is appended to the base url using Uri.TryCreate,
// so the semantics of relative urls matter here.
// Example: Base path https://localhost/v1/ and search path openapi.json
// will result in https://localhost/v1/openapi.json being tested.
// Example: Base path https://localhost/v1/ and search path /openapi.json
// will result in https://localhost/openapi.json being tested.
internal static IEnumerable<string> DefaultSearchPaths { get; } = new[] {
"swagger.json",
"/swagger.json",
"swagger/v1/swagger.json",
"/swagger/v1/swagger.json",
"openapi.json",
"/openapi.json",
};

private readonly IPreferences _preferences;
public OpenApiSearchPathsProvider(IPreferences preferences)
{
_preferences = preferences;
}

public IEnumerable<string> GetOpenApiSearchPaths()
{
string[] configSearchPaths = Split(_preferences.GetValue(WellKnownPreference.SwaggerSearchPaths));

if (configSearchPaths.Length > 0)
{
return configSearchPaths;
}

string[] addToSearchPaths = Split(_preferences.GetValue(WellKnownPreference.SwaggerAddToSearchPaths));
string[] removeFromSearchPaths = Split(_preferences.GetValue(WellKnownPreference.SwaggerRemoveFromSearchPaths));

return DefaultSearchPaths.Union(addToSearchPaths).Except(removeFromSearchPaths);
}

private static string[] Split(string searchPaths)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function for exclusive use with search path preferences, or could it be useful for current other preferences or potential future ones down the line?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a huge fan of pipe- (or frankly anything-) delimited strings, so I'm hoping there won't be too many of those before we get to a better settings system. I believe this is the only use case right now.

{
if (string.IsNullOrWhiteSpace(searchPaths))
{
return Array.Empty<string>();
}
else
{
return searchPaths.Split('|', StringSplitOptions.RemoveEmptyEntries);
}
}
}
}
4 changes: 4 additions & 0 deletions src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ public static IReadOnlyList<string> Names

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

public static string SwaggerRemoveFromSearchPaths => "swagger.removeFromSearchPaths";

public static string SwaggerAddToSearchPaths => "swagger.addToSearchPaths";

public static string UseDefaultCredentials { get; } = "httpClient.useDefaultCredentials";

public static string HttpClientUserAgent { get; } = "httpClient.userAgent";
Expand Down
72 changes: 72 additions & 0 deletions test/Microsoft.HttpRepl.Fakes/FakePreferences.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using Microsoft.HttpRepl.Preferences;
using Microsoft.Repl.ConsoleHandling;

namespace Microsoft.HttpRepl.Fakes
{
public class FakePreferences : IPreferences
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: sealed? :P

{
private readonly Dictionary<string, string> _currentPreferences;

public FakePreferences()
{
DefaultPreferences = new Dictionary<string, string>();
_currentPreferences = new();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this madness...Can you link me to this C# feature?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

public IReadOnlyDictionary<string, string> DefaultPreferences { get; }
public IReadOnlyDictionary<string, string> CurrentPreferences => _currentPreferences;

public bool GetBoolValue(string preference, bool defaultValue = false)
{
if (CurrentPreferences.TryGetValue(preference, out string value) && bool.TryParse(value, out bool result))
{
return result;
}

return defaultValue;
}

public AllowedColors GetColorValue(string preference, AllowedColors defaultValue = AllowedColors.None)
{
if (CurrentPreferences.TryGetValue(preference, out string value) && Enum.TryParse(value, true, out AllowedColors result))
{
return result;
}

return defaultValue;
}

public int GetIntValue(string preference, int defaultValue = 0)
{
if (CurrentPreferences.TryGetValue(preference, out string value) && int.TryParse(value, out int result))
{
return result;
}

return defaultValue;
}

public string GetValue(string preference, string defaultValue = null)
{
if (CurrentPreferences.TryGetValue(preference, out string value))
{
return value;
}

return defaultValue;
}

public bool SetValue(string preference, string value)
{
_currentPreferences[preference] = value;
return true;
}

public bool TryGetValue(string preference, out string value) => CurrentPreferences.TryGetValue(preference, out value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.HttpRepl.Fakes;
using Microsoft.HttpRepl.Preferences;
using Xunit;

namespace Microsoft.HttpRepl.Tests.Preferences
{
public class OpenApiSearchPathsProviderTests
{
[Fact]
public void WithNoOverrides_ReturnsDefault()
{
// Arrange
NullPreferences preferences = new();
OpenApiSearchPathsProvider provider = new(preferences);
IEnumerable<string> expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths;

// Act
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();

// Assert
AssertPathLists(expectedPaths, paths);
}

[Fact]
public void WithFullOverride_ReturnsConfiguredOverride()
{
// Arrange
string searchPathOverrides = "/red|/green|/blue";
FakePreferences preferences = new();
preferences.SetValue(WellKnownPreference.SwaggerSearchPaths, searchPathOverrides);
OpenApiSearchPathsProvider provider = new(preferences);
string[] expectedPaths = searchPathOverrides.Split('|');

// Act
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();

// Assert
AssertPathLists(expectedPaths, paths);
}

[Fact]
public void WithAdditions_ReturnsDefaultPlusAdditions()
{
// Arrange
string[] searchPathAdditions = new[] { "/red", "/green", "/blue" };
FakePreferences preferences = new();
preferences.SetValue(WellKnownPreference.SwaggerAddToSearchPaths, string.Join('|', searchPathAdditions));
OpenApiSearchPathsProvider provider = new(preferences);
IEnumerable<string> expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths.Union(searchPathAdditions);

// Act
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();

// Assert
AssertPathLists(expectedPaths, paths);
}

[Fact]
public void WithRemovals_ReturnsDefaultMinusRemovals()
{
// Arrange
string[] searchPathRemovals = new[] { "swagger.json", "/swagger.json", "swagger/v1/swagger.json", "/swagger/v1/swagger.json" };
FakePreferences preferences = new();
preferences.SetValue(WellKnownPreference.SwaggerRemoveFromSearchPaths, string.Join('|', searchPathRemovals));
OpenApiSearchPathsProvider provider = new(preferences);
IEnumerable<string> expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths.Except(searchPathRemovals);

// Act
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();

// Assert
AssertPathLists(expectedPaths, paths);
}

[Fact]
public void WithAdditionsAndRemovals_ReturnsCorrectSet()
{
// Arrange
string[] searchPathAdditions = new[] { "/red", "/green", "/blue" };
string[] searchPathRemovals = new[] { "swagger.json", "/swagger.json", "swagger/v1/swagger.json", "/swagger/v1/swagger.json" };
FakePreferences preferences = new();
preferences.SetValue(WellKnownPreference.SwaggerAddToSearchPaths, string.Join('|', searchPathAdditions));
preferences.SetValue(WellKnownPreference.SwaggerRemoveFromSearchPaths, string.Join('|', searchPathRemovals));
OpenApiSearchPathsProvider provider = new(preferences);
IEnumerable<string> expectedPaths = OpenApiSearchPathsProvider.DefaultSearchPaths.Union(searchPathAdditions).Except(searchPathRemovals);

// Act
IEnumerable<string> paths = provider.GetOpenApiSearchPaths();

// Assert
AssertPathLists(expectedPaths, paths);
}

private static void AssertPathLists(IEnumerable<string> expectedPaths, IEnumerable<string> paths)
{
Assert.NotNull(expectedPaths);
Assert.NotNull(paths);

IEnumerator<string> expectedPathsEnumerator = expectedPaths.GetEnumerator();
IEnumerator<string> pathsEnumerator = paths.GetEnumerator();

while (expectedPathsEnumerator.MoveNext())
{
Assert.True(pathsEnumerator.MoveNext(), $"Missing path \"{expectedPathsEnumerator.Current}\"");
Assert.Equal(expectedPathsEnumerator.Current, pathsEnumerator.Current, StringComparer.Ordinal);
}

if (pathsEnumerator.MoveNext())
{
// We can't do a one-liner here like the Missing path version above because
// the order the second parameter is evaluated regardless of the result of the
// evaluation of the first parameter. Also xUnit doesn't have an Assert.Fail,
// so we have to use Assert.True(false) per their comparison chart.
Assert.True(false, $"Extra path \"{pathsEnumerator.Current}\"");
}
}
}
}