Skip to content

Commit 38f3641

Browse files
Allows adding multiple instances of the same plugin. Closes #554 (#557)
1 parent 1746534 commit 38f3641

File tree

11 files changed

+107
-73
lines changed

11 files changed

+107
-73
lines changed

dev-proxy-abstractions/BaseProxyPlugin.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.CommandLine;
45
using Microsoft.Extensions.Configuration;
56

67
namespace Microsoft.DevProxy.Abstractions;
@@ -11,6 +12,9 @@ public abstract class BaseProxyPlugin : IProxyPlugin
1112
protected ILogger? _logger;
1213

1314
public virtual string Name => throw new NotImplementedException();
15+
16+
public virtual Option[] GetOptions() => Array.Empty<Option>();
17+
1418
public virtual void Register(IPluginEvents pluginEvents,
1519
IProxyContext context,
1620
ISet<UrlToWatch> urlsToWatch,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.CommandLine;
5+
using System.CommandLine.Parsing;
6+
7+
namespace Microsoft.DevProxy.Abstractions;
8+
9+
public static class CommandLineExtensions
10+
{
11+
public static T? GetValueForOption<T>(this ParseResult parseResult, string optionName, Option[] options)
12+
{
13+
// we need to remove the leading - because CommandLine stores the option
14+
// name without them
15+
var option = options
16+
.FirstOrDefault(o => o.Name == optionName.TrimStart('-'))
17+
as Option<T>;
18+
if (option is null)
19+
{
20+
throw new InvalidOperationException($"Could not find option with name {optionName} and value type {typeof(T).Name}");
21+
}
22+
23+
return parseResult.GetValueForOption(option);
24+
}
25+
}

dev-proxy-abstractions/IProxyPlugin.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.CommandLine;
45
using Microsoft.Extensions.Configuration;
56

67
namespace Microsoft.DevProxy.Abstractions;
78

89
public interface IProxyPlugin
910
{
1011
string Name { get; }
12+
Option[] GetOptions();
1113
void Register(IPluginEvents pluginEvents,
1214
IProxyContext context,
1315
ISet<UrlToWatch> urlsToWatch,

dev-proxy-abstractions/PluginEvents.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public ThrottlingInfo(int throttleForSeconds, string retryAfterHeaderName)
5757
}
5858

5959
public class ProxyHttpEventArgsBase
60-
{
60+
{
6161
internal ProxyHttpEventArgsBase(SessionEventArgs session)
6262
{
6363
Session = session ?? throw new ArgumentNullException(nameof(session));
@@ -98,21 +98,21 @@ public ProxyResponseArgs(SessionEventArgs session, ResponseState responseState)
9898

9999
public class InitArgs
100100
{
101-
public InitArgs(RootCommand rootCommand)
101+
public InitArgs()
102102
{
103-
RootCommand = rootCommand ?? throw new ArgumentNullException(nameof(rootCommand));
104103
}
105-
public RootCommand RootCommand { get; set; }
106-
107104
}
108105

109106
public class OptionsLoadedArgs
110107
{
111-
public OptionsLoadedArgs(InvocationContext context)
108+
public InvocationContext Context { get; set; }
109+
public Option[] Options { get; set; }
110+
111+
public OptionsLoadedArgs(InvocationContext context, Option[] options)
112112
{
113113
Context = context ?? throw new ArgumentNullException(nameof(context));
114+
Options = options ?? throw new ArgumentNullException(nameof(options));
114115
}
115-
public InvocationContext Context { get; set; }
116116
}
117117

118118
public class RequestLog

dev-proxy-plugins/MockResponses/MockResponsePlugin.cs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,30 @@ public class MockResponsePlugin : BaseProxyPlugin
3535
{
3636
protected MockResponseConfiguration _configuration = new();
3737
private MockResponsesLoader? _loader = null;
38-
private readonly Option<bool?> _noMocks;
39-
private readonly Option<string?> _mocksFile;
38+
private static readonly string _noMocksOptionName = "--no-mocks";
39+
private static readonly string _mocksFileOptionName = "--mocks-file";
4040
public override string Name => nameof(MockResponsePlugin);
4141
private IProxyConfiguration? _proxyConfiguration;
4242
// tracks the number of times a mock has been applied
4343
// used in combination with mocks that have an Nth property
4444
private Dictionary<string, int> _appliedMocks = new();
4545

46-
public MockResponsePlugin()
46+
public override Option[] GetOptions()
4747
{
48-
_noMocks = new Option<bool?>("--no-mocks", "Disable loading mock requests");
48+
var _noMocks = new Option<bool?>(_noMocksOptionName, "Disable loading mock requests")
49+
{
50+
ArgumentHelpName = "no mocks"
51+
};
4952
_noMocks.AddAlias("-n");
50-
_noMocks.ArgumentHelpName = "no mocks";
5153

52-
_mocksFile = new Option<string?>("--mocks-file", "Provide a file populated with mock responses")
54+
var _mocksFile = new Option<string?>(_mocksFileOptionName, "Provide a file populated with mock responses")
5355
{
5456
ArgumentHelpName = "mocks file"
5557
};
56-
}
5758

59+
return [_noMocks, _mocksFile];
60+
}
61+
5862
public override void Register(IPluginEvents pluginEvents,
5963
IProxyContext context,
6064
ISet<UrlToWatch> urlsToWatch,
@@ -65,25 +69,18 @@ public override void Register(IPluginEvents pluginEvents,
6569
configSection?.Bind(_configuration);
6670
_loader = new MockResponsesLoader(_logger!, _configuration);
6771

68-
pluginEvents.Init += OnInit;
6972
pluginEvents.OptionsLoaded += OnOptionsLoaded;
7073
pluginEvents.BeforeRequest += OnRequest;
7174

7275
_proxyConfiguration = context.Configuration;
7376
}
7477

75-
private void OnInit(object? sender, InitArgs e)
76-
{
77-
e.RootCommand.AddOption(_noMocks);
78-
e.RootCommand.AddOption(_mocksFile);
79-
}
80-
8178
private void OnOptionsLoaded(object? sender, OptionsLoadedArgs e)
8279
{
8380
InvocationContext context = e.Context;
84-
81+
8582
// allow disabling of mocks as a command line option
86-
var noMocks = context.ParseResult.GetValueForOption(_noMocks);
83+
var noMocks = context.ParseResult.GetValueForOption<bool?>(_noMocksOptionName, e.Options);
8784
if (noMocks.HasValue)
8885
{
8986
_configuration.NoMocks = noMocks.Value;
@@ -95,7 +92,7 @@ private void OnOptionsLoaded(object? sender, OptionsLoadedArgs e)
9592
}
9693

9794
// update the name of the mocks file to load from if supplied
98-
string? mocksFile = context.ParseResult.GetValueForOption(_mocksFile);
95+
var mocksFile = context.ParseResult.GetValueForOption<string?>(_mocksFileOptionName, e.Options);
9996
if (mocksFile is not null)
10097
{
10198
_configuration.MocksFile = Path.GetFullPath(ProxyUtils.ReplacePathTokens(mocksFile));

dev-proxy-plugins/RandomErrors/GraphRandomErrorPlugin.cs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class GraphRandomErrorConfiguration
2828

2929
public class GraphRandomErrorPlugin : BaseProxyPlugin
3030
{
31-
private readonly Option<IEnumerable<int>> _allowedErrors;
31+
private static readonly string _allowedErrorsOptionName = "--allowed-errors";
3232
private readonly GraphRandomErrorConfiguration _configuration = new();
3333
private IProxyConfiguration? _proxyConfiguration;
3434

@@ -90,11 +90,6 @@ public class GraphRandomErrorPlugin : BaseProxyPlugin
9090

9191
public GraphRandomErrorPlugin()
9292
{
93-
_allowedErrors = new Option<IEnumerable<int>>("--allowed-errors", "List of errors that Dev Proxy may produce");
94-
_allowedErrors.AddAlias("-a");
95-
_allowedErrors.ArgumentHelpName = "allowed errors";
96-
_allowedErrors.AllowMultipleArgumentsPerToken = true;
97-
9893
_random = new Random();
9994
}
10095

@@ -225,6 +220,18 @@ private void UpdateProxyBatchResponse(ProxyRequestArgs ev, GraphBatchResponsePay
225220

226221
private static string BuildApiErrorMessage(Request r) => $"Some error was generated by the proxy. {(ProxyUtils.IsGraphRequest(r) ? ProxyUtils.IsSdkRequest(r) ? "" : String.Join(' ', MessageUtils.BuildUseSdkForErrorsMessage(r)) : "")}";
227222

223+
public override Option[] GetOptions()
224+
{
225+
var _allowedErrors = new Option<IEnumerable<int>>(_allowedErrorsOptionName, "List of errors that Dev Proxy may produce")
226+
{
227+
ArgumentHelpName = "allowed errors",
228+
AllowMultipleArgumentsPerToken = true
229+
};
230+
_allowedErrors.AddAlias("-a");
231+
232+
return [_allowedErrors];
233+
}
234+
228235
public override void Register(IPluginEvents pluginEvents,
229236
IProxyContext context,
230237
ISet<UrlToWatch> urlsToWatch,
@@ -233,7 +240,6 @@ public override void Register(IPluginEvents pluginEvents,
233240
base.Register(pluginEvents, context, urlsToWatch, configSection);
234241

235242
configSection?.Bind(_configuration);
236-
pluginEvents.Init += OnInit;
237243
pluginEvents.OptionsLoaded += OnOptionsLoaded;
238244
pluginEvents.BeforeRequest += OnRequest;
239245

@@ -244,17 +250,12 @@ public override void Register(IPluginEvents pluginEvents,
244250
_proxyConfiguration = context.Configuration;
245251
}
246252

247-
private void OnInit(object? sender, InitArgs e)
248-
{
249-
e.RootCommand.AddOption(_allowedErrors);
250-
}
251-
252253
private void OnOptionsLoaded(object? sender, OptionsLoadedArgs e)
253254
{
254255
InvocationContext context = e.Context;
255256

256257
// Configure the allowed errors
257-
IEnumerable<int>? allowedErrors = context.ParseResult.GetValueForOption(_allowedErrors);
258+
var allowedErrors = context.ParseResult.GetValueForOption<IEnumerable<int>?>(_allowedErrorsOptionName, e.Options);
258259
if (allowedErrors?.Any() ?? false)
259260
_configuration.AllowedErrors = allowedErrors.ToList();
260261

dev-proxy-plugins/RequestLogs/ExecutionSummaryPlugin.cs

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@ public class ExecutionSummaryPlugin : BaseProxyPlugin
2727
{
2828
public override string Name => nameof(ExecutionSummaryPlugin);
2929
private ExecutionSummaryPluginConfiguration _configuration = new();
30-
private readonly Option<string?> _filePath;
31-
private readonly Option<SummaryGroupBy?> _groupBy;
30+
private static readonly string _filePathOptionName = "--summary-file-path";
31+
private static readonly string _groupByOptionName = "--summary-group-by";
3232
private const string _requestsInterceptedMessage = "Requests intercepted";
3333
private const string _requestsPassedThroughMessage = "Requests passed through";
3434

35-
public ExecutionSummaryPlugin()
35+
public override Option[] GetOptions()
3636
{
37-
_filePath = new Option<string?>("--summary-file-path", "Path to the file where the summary should be saved. If not specified, the summary will be printed to the console. Path can be absolute or relative to the current working directory.");
38-
_filePath.ArgumentHelpName = "summary-file-path";
39-
_filePath.AddValidator(input =>
37+
var filePath = new Option<string?>(_filePathOptionName, "Path to the file where the summary should be saved. If not specified, the summary will be printed to the console. Path can be absolute or relative to the current working directory.")
38+
{
39+
ArgumentHelpName = "summary-file-path"
40+
};
41+
filePath.AddValidator(input =>
4042
{
4143
var outputFilePath = input.Tokens.First().Value;
4244
if (string.IsNullOrEmpty(outputFilePath))
@@ -58,15 +60,19 @@ public ExecutionSummaryPlugin()
5860
}
5961
});
6062

61-
_groupBy = new Option<SummaryGroupBy?>("--summary-group-by", "Specifies how the information should be grouped in the summary. Available options: `url` (default), `messageType`.");
62-
_groupBy.ArgumentHelpName = "summary-group-by";
63-
_groupBy.AddValidator(input =>
63+
var groupBy = new Option<SummaryGroupBy?>(_groupByOptionName, "Specifies how the information should be grouped in the summary. Available options: `url` (default), `messageType`.")
64+
{
65+
ArgumentHelpName = "summary-group-by"
66+
};
67+
groupBy.AddValidator(input =>
6468
{
6569
if (!Enum.TryParse<SummaryGroupBy>(input.Tokens.First().Value, true, out var groupBy))
6670
{
6771
input.ErrorMessage = $"{input.Tokens.First().Value} is not a valid option to group by. Allowed values are: {string.Join(", ", Enum.GetNames(typeof(SummaryGroupBy)))}";
6872
}
6973
});
74+
75+
return [filePath, groupBy];
7076
}
7177

7278
public override void Register(IPluginEvents pluginEvents,
@@ -78,28 +84,21 @@ public override void Register(IPluginEvents pluginEvents,
7884

7985
configSection?.Bind(_configuration);
8086

81-
pluginEvents.Init += OnInit;
8287
pluginEvents.OptionsLoaded += OnOptionsLoaded;
8388
pluginEvents.AfterRecordingStop += AfterRecordingStop;
8489
}
8590

86-
private void OnInit(object? sender, InitArgs e)
87-
{
88-
e.RootCommand.AddOption(_filePath);
89-
e.RootCommand.AddOption(_groupBy);
90-
}
91-
9291
private void OnOptionsLoaded(object? sender, OptionsLoadedArgs e)
9392
{
9493
InvocationContext context = e.Context;
9594

96-
var filePath = context.ParseResult.GetValueForOption(_filePath);
95+
var filePath = context.ParseResult.GetValueForOption<string?>(_filePathOptionName, e.Options);
9796
if (filePath is not null)
9897
{
9998
_configuration.FilePath = filePath;
10099
}
101100

102-
var groupBy = context.ParseResult.GetValueForOption(_groupBy);
101+
var groupBy = context.ParseResult.GetValueForOption<SummaryGroupBy?>(_groupByOptionName, e.Options);
103102
if (groupBy is not null)
104103
{
105104
_configuration.GroupBy = groupBy.Value;

dev-proxy-plugins/RequestLogs/MinimalPermissionsGuidancePlugin.cs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ public class MinimalPermissionsGuidancePlugin : BaseProxyPlugin
4242
{
4343
public override string Name => nameof(MinimalPermissionsGuidancePlugin);
4444
private MinimalPermissionsGuidancePluginConfiguration _configuration = new();
45-
private readonly Option<string?> _filePath;
45+
private static readonly string _filePathOptionName = "--minimal-permissions-summary-file-path";
4646

47-
public MinimalPermissionsGuidancePlugin()
47+
public override Option[] GetOptions()
4848
{
49-
_filePath = new Option<string?>("--minimal-permissions-summary-file-path", "Path to the file where the permissions summary should be saved. If not specified, the summary will be printed to the console. Path can be absolute or relative to the current working directory.")
49+
var filePath = new Option<string?>(_filePathOptionName, "Path to the file where the permissions summary should be saved. If not specified, the summary will be printed to the console. Path can be absolute or relative to the current working directory.")
5050
{
5151
ArgumentHelpName = "minimal-permissions-summary-file-path"
5252
};
53-
_filePath.AddValidator(input =>
53+
filePath.AddValidator(input =>
5454
{
5555
var outputFilePath = input.Tokens.First().Value;
5656
if (string.IsNullOrEmpty(outputFilePath))
@@ -71,6 +71,8 @@ public MinimalPermissionsGuidancePlugin()
7171
input.ErrorMessage = $"The directory {outputDir} does not exist.";
7272
}
7373
});
74+
75+
return [filePath];
7476
}
7577

7678
public override void Register(IPluginEvents pluginEvents,
@@ -82,7 +84,6 @@ public override void Register(IPluginEvents pluginEvents,
8284

8385
configSection?.Bind(_configuration);
8486

85-
pluginEvents.Init += Init;
8687
pluginEvents.OptionsLoaded += OptionsLoaded;
8788
pluginEvents.AfterRecordingStop += AfterRecordingStop;
8889
}
@@ -91,18 +92,13 @@ private void OptionsLoaded(object? sender, OptionsLoadedArgs e)
9192
{
9293
InvocationContext context = e.Context;
9394

94-
var filePath = context.ParseResult.GetValueForOption(_filePath);
95+
var filePath = context.ParseResult.GetValueForOption<string?>(_filePathOptionName, e.Options);
9596
if (filePath is not null)
9697
{
9798
_configuration.FilePath = filePath;
9899
}
99100
}
100101

101-
private void Init(object? sender, InitArgs e)
102-
{
103-
e.RootCommand.AddOption(_filePath);
104-
}
105-
106102
private async Task AfterRecordingStop(object? sender, RecordingArgs e)
107103
{
108104
if (!e.RequestLogs.Any())

dev-proxy/Program.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,18 @@
3939
}
4040

4141
PluginLoaderResult loaderResults = new PluginLoader(logger).LoadPlugins(pluginEvents, context);
42-
43-
// have all the plugins init and provide any command line options
44-
pluginEvents.RaiseInit(new InitArgs(rootCommand));
45-
46-
rootCommand.Handler = proxyHost.GetCommandHandler(pluginEvents, loaderResults.UrlsToWatch, logger);
42+
// have all the plugins init
43+
pluginEvents.RaiseInit(new InitArgs());
44+
45+
var options = loaderResults.ProxyPlugins
46+
.SelectMany(p => p.GetOptions())
47+
// remove duplicates by comparing the option names
48+
.GroupBy(o => o.Name)
49+
.Select(g => g.First())
50+
.ToList();
51+
options.ForEach(rootCommand.AddOption);
52+
53+
rootCommand.Handler = proxyHost.GetCommandHandler(pluginEvents, options.ToArray(), loaderResults.UrlsToWatch, logger);
4754

4855
// filter args to retrieve options
4956
var incomingOptions = args.Where(arg => arg.StartsWith("-")).ToArray();

0 commit comments

Comments
 (0)