-
Notifications
You must be signed in to change notification settings - Fork 390
/
Copy pathValidationSubsystem.cs
179 lines (154 loc) · 6.65 KB
/
ValidationSubsystem.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.CommandLine.Parsing;
using System.CommandLine.Subsystems;
using System.CommandLine.Validation;
namespace System.CommandLine;
// TODO: Add support for terminating validator. This is needed at least for required because it would be annoying to get an error that you forgot to enter something, and also all the validation errors for the default value Probably other uses, so generalize to termintating.
public sealed class ValidationSubsystem : CliSubsystem
{
// The type here is the ValueCondition type
private Dictionary<Type, ValueValidator> valueValidators = [];
private Dictionary<Type, CommandValidator> commandValidators = [];
private ValidationSubsystem()
: base("", SubsystemKind.Validation)
{ }
public static ValidationSubsystem Create()
{
var newValidationSubsystem = new ValidationSubsystem();
newValidationSubsystem.AddValidator(new RangeValidator());
newValidationSubsystem.AddValidator(new InclusiveGroupValidator());
return newValidationSubsystem;
}
public static ValidationSubsystem CreateEmpty()
=> new ValidationSubsystem();
public void AddValidator(ValueValidator validator)
{
foreach (var type in validator.ValueConditionTypes)
{
valueValidators[type] = validator;
}
}
public void AddValidator(CommandValidator validator)
{
foreach (var type in validator.ValueConditionTypes)
{
commandValidators[type] = validator;
}
}
protected internal override bool GetIsActivated(ParseResult? parseResult) => true;
public override void Execute(PipelineResult pipelineResult)
{
if (pipelineResult.ParseResult is null)
{
return;
}
var validationContext = new ValidationContext(pipelineResult, this);
var commandResults = CommandAndAncestors(pipelineResult.ParseResult.CommandResult);
var valueSymbols = GetValueSymbols(commandResults);
foreach (var symbol in valueSymbols)
{
ValidateValue(symbol, validationContext);
}
foreach (var commandResult in commandResults)
{
ValidateCommand(commandResult, validationContext);
}
}
private void ValidateValue(CliValueSymbol valueSymbol, ValidationContext validationContext)
{
var valueConditions = valueSymbol.EnumerateValueConditions();
// manually implement the foreach so we can efficiently skip getting the
// value if there are no conditions
var enumerator = valueConditions.GetEnumerator();
if (!enumerator.MoveNext())
{
return;
}
var value = validationContext.GetValue(valueSymbol);
var valueResult = validationContext.GetValueResult(valueSymbol);
do
{
ValidateValueCondition(value, valueSymbol, valueResult, enumerator.Current, validationContext);
} while (enumerator.MoveNext());
}
private void ValidateCommand(CliCommandResult commandResult, ValidationContext validationContext)
{
var valueConditions = commandResult.Command.EnumerateCommandConditions();
// unlike ValidateValue we do not need to manually implement the foreach
// to skip unnecessary work, as there is no additional work to be before
// calling command validators
foreach (var condition in valueConditions)
{
ValidateCommandCondition(commandResult, condition, validationContext);
}
}
private static List<CliValueSymbol> GetValueSymbols(IEnumerable<CliCommandResult> commandResults)
=> commandResults
.SelectMany(commandResult => commandResult.ValueResults.Select(valueResult => valueResult.ValueSymbol))
.Distinct()
.ToList();
// Consider moving to CliCommandResult
private static IEnumerable<CliCommandResult> CommandAndAncestors(CliCommandResult commandResult)
=> commandResult.Parent is not null
? [commandResult, .. CommandAndAncestors(commandResult.Parent)]
: [commandResult];
private void ValidateValueCondition(object? value, CliValueSymbol valueSymbol, CliValueResult? valueResult, ValueCondition condition, ValidationContext validationContext)
{
if (condition is IValueValidator conditionValidator)
{
conditionValidator.Validate(value, valueSymbol, valueResult, condition, validationContext);
return;
}
ValueValidator? validator = GetValidator(condition);
if (validator == null)
{
if (condition.MustHaveValidator)
{
validationContext.AddError(new CliDiagnostic(new("", "", "{valueSymbol} must have {condition} validator.", severity: CliDiagnosticSeverity.Error, null), [valueSymbol.Name, condition.Name], cliSymbolResult: valueResult));
}
return;
}
validator.Validate(value, valueSymbol, valueResult, condition, validationContext);
}
private ValueValidator? GetValidator(ValueCondition condition)
{
if (!valueValidators.TryGetValue(condition.GetType(), out var validator) || validator is null)
{
if (condition.MustHaveValidator)
{
// Output missing validator error
}
}
return validator;
}
private CommandValidator? GetValidator(CommandCondition condition)
{
if (!commandValidators.TryGetValue(condition.GetType(), out var validator) || validator is null)
{
if (condition.MustHaveValidator)
{
// Output missing validator error
}
}
return validator;
}
private void ValidateCommandCondition(CliCommandResult commandResult, CommandCondition condition, ValidationContext validationContext)
{
if (condition is ICommandValidator conditionValidator)
{
conditionValidator.Validate(commandResult, condition, validationContext);
return;
}
CommandValidator? validator = GetValidator(condition);
if (validator == null)
{
if (condition.MustHaveValidator)
{
validationContext.AddError(new CliDiagnostic(new("", "", "{commandResult} must have {condition} validator.", severity: CliDiagnosticSeverity.Error, null), [commandResult.Command.Name, condition.Name]));
}
return;
}
validator.Validate(commandResult, condition, validationContext);
}
}