From 6a8d488339835bb871452821edd55a1caf3b045a Mon Sep 17 00:00:00 2001 From: AdmiringWorm Date: Mon, 15 Jan 2024 17:05:20 +0100 Subject: [PATCH] (#3381) Add new rule command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new command called ´rule` with an alias of `rules` to be able to view any Validation Rules that have been implemented by Chocolatey CLI, or the details of a specific view. This allows a user to see what is implemented when they are packaging their Chocolatey CLI package. This is intended to be used in the future to enhance other functionality. --- src/chocolatey.tests/chocolatey.tests.csproj | 1 + .../commands/ChocolateyRuleCommandSpecs.cs | 679 ++++++++++++++++++ src/chocolatey/chocolatey.csproj | 1 + .../commands/ChocolateyRuleCommand.cs | 236 ++++++ .../configuration/ChocolateyConfiguration.cs | 20 + 5 files changed, 937 insertions(+) create mode 100644 src/chocolatey.tests/infrastructure.app/commands/ChocolateyRuleCommandSpecs.cs create mode 100644 src/chocolatey/infrastructure.app/commands/ChocolateyRuleCommand.cs diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj index 75ec5e4ee0..06f30d8af3 100644 --- a/src/chocolatey.tests/chocolatey.tests.csproj +++ b/src/chocolatey.tests/chocolatey.tests.csproj @@ -168,6 +168,7 @@ + diff --git a/src/chocolatey.tests/infrastructure.app/commands/ChocolateyRuleCommandSpecs.cs b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyRuleCommandSpecs.cs new file mode 100644 index 0000000000..cee264479e --- /dev/null +++ b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyRuleCommandSpecs.cs @@ -0,0 +1,679 @@ +namespace chocolatey.tests.infrastructure.app.commands +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using chocolatey.infrastructure.app.attributes; + using chocolatey.infrastructure.app.commands; + using chocolatey.infrastructure.app.configuration; + using chocolatey.infrastructure.commandline; + using chocolatey.infrastructure.rules; + using chocolatey.infrastructure.services; + using FluentAssertions; + using FluentAssertions.Execution; + using Moq; + + public class ChocolateyRuleCommandSpecs + { + [ConcernFor("rule")] + public abstract class ChocolateyRuleCommandSpecsBase : TinySpec + { + protected ChocolateyRuleCommand Command; + protected Mock RuleService = new Mock(); + protected ChocolateyConfiguration Configuration = new ChocolateyConfiguration(); + + public override void Context() + { + Command = new ChocolateyRuleCommand(RuleService.Object); + } + } + + public class When_Implementing_Command_For : ChocolateyRuleCommandSpecsBase + { + private List _results; + + public override void Because() + { + _results = Command.GetType().GetCustomAttributes().ToList(); + } + + [Fact] + public void Should_Have_Expected_Number_Of_Commands() + { + _results.Should().HaveCount(2); + } + + [InlineData("rule")] + [InlineData("rules")] + public void Should_Implement_Expected_Command(string name) + { + _results.Should().ContainSingle(r => r.CommandName == name); + } + + [Fact] + public void Should_Specify_Expected_Version_For_All_Commands() + { + _results.Should().AllSatisfy(r => r.Version.Should().Be("2.3.0")); + } + + [Fact] + public void Should_Mention_Rules_Command_Is_An_Alias() + { + _results.Should().ContainSingle(r => r.CommandName == "rules" && r.Description.Contains("alias for rule")); + } + } + + public class When_Configuring_The_Argument_Parser : ChocolateyRuleCommandSpecsBase + { + private OptionSet _optionSet; + + public override void Context() + { + base.Context(); + _optionSet = new OptionSet(); + } + + public override void Because() + { + Command.ConfigureArgumentParser(_optionSet, Configuration); + } + + [InlineData("n")] + [InlineData("name")] + public void Should_Set_Expected_Arguments_On_Option_Set(string name) + { + _optionSet.Contains(name).Should().BeTrue(); + } + } + + public class When_Handling_Additional_Argument_Parsing : ChocolateyRuleCommandSpecsBase + { + private readonly IList _unparsedArgs = new List(); + private Action _because; + + public override void Because() + { + _because = () => Command.ParseAdditionalArguments(_unparsedArgs, Configuration); + } + + public void Reset() + { + _unparsedArgs.Clear(); + Configuration = new ChocolateyConfiguration(); + } + + [Fact] + public void Should_Set_Default_Sub_Command_On_Empty_Arguments() + { + Reset(); + _because(); + + Configuration.RuleCommand.Command.Should().BeLowerCased().And.Be("list"); + } + + [Fact] + public void Should_Set_Command_to_List_when_Specified() + { + Reset(); + _unparsedArgs.Add("LIST"); + _because(); + + Configuration.RuleCommand.Command.Should().BeLowerCased().And.Be("list"); + } + + [Fact] + public void Should_Set_Command_To_Get_When_Specified() + { + Reset(); + _unparsedArgs.Add("Get"); + _because(); + + Configuration.RuleCommand.Command.Should().BeLowerCased().And.Be("get"); + } + + [Fact] + public void Should_Set_Name_When_Command_When_Not_Already_Set() + { + Reset(); + _unparsedArgs.Add("Get"); + _unparsedArgs.Add("test-name"); + _because(); + + Configuration.RuleCommand.Name.Should().Be("test-name"); + } + + [Fact] + public void Should_Not_Set_Name_When_Already_Set() + { + Reset(); + _unparsedArgs.Add("Get"); + _unparsedArgs.Add("test-name"); + Configuration.RuleCommand.Name = "old-name"; + _because(); + + Configuration.RuleCommand.Name.Should().Be("old-name"); + } + + [Fact] + public void Should_Not_Set_Name_When_Only_Sub_Command_Specified() + { + Reset(); + _unparsedArgs.Add("something"); + _because(); + + Configuration.RuleCommand.Name.Should().BeNullOrEmpty(); + } + } + + public class When_Validating : ChocolateyRuleCommandSpecsBase + { + private Action _because; + + public override void Because() + { + _because = () => Command.Validate(Configuration); + } + + [Fact] + public void Should_Run_Successfully_When_Only_List_Command_Specified() + { + Configuration = new ChocolateyConfiguration(); + Configuration.RuleCommand.Command = "list"; + + _because.Should().NotThrow(); + } + + [Fact] + public void Throws_Expected_Exception_When_Name_Specified_For_List_Command() + { + Configuration = new ChocolateyConfiguration(); + Configuration.RuleCommand.Command = "list"; + Configuration.RuleCommand.Name = "some-name"; + + _because.Should().Throw().WithMessage("A Rule Name can not be specified when listing all validation rules."); + } + + [Fact] + public void Throws_Expected_Exception_When_Name_Is_Not_Specified_For_Get_Command() + { + Configuration = new ChocolateyConfiguration(); + Configuration.RuleCommand.Command = "get"; + + _because.Should().Throw().WithMessage("A Rule Name is required to when not listing all validation rules."); + } + + [Fact] + public void Should_Run_Successfully_When_Get_Command_With_Name_Is_Specified() + { + Configuration = new ChocolateyConfiguration(); + Configuration.RuleCommand.Command = "get"; + Configuration.RuleCommand.Name = "some-name"; + + _because.Should().NotThrow(); + } + + [Fact] + public void Should_Log_Warning_When_Unknown_Sub_Command_Is_Used() + { + Configuration = new ChocolateyConfiguration(); + Configuration.RuleCommand.Command = "something"; + + using (new AssertionScope()) + { + _because.Should().NotThrow(); + MockLogger.Messages.Should().ContainKey(LogLevel.Warn.ToString()) + .WhoseValue.Should().Contain("Unknown command 'something'. Setting to list."); + Configuration.RuleCommand.Command.Should().Be("list"); + } + } + + [Fact] + public void Throws_Expected_Exception_When_Name_Supplied_For_Unknown_Command() + { + Configuration = new ChocolateyConfiguration(); + Configuration.RuleCommand.Command = "something"; + Configuration.RuleCommand.Name = "MyName"; + + using (new AssertionScope()) + { + _because.Should().Throw().WithMessage("A Rule Name can not be specified when listing all validation rules."); + Configuration.RuleCommand.Command.Should().Be("list"); + MockLogger.Messages.Should().ContainKey(LogLevel.Warn.ToString()) + .WhoseValue.Should().Contain("Unknown command 'something'. Setting to list."); + } + } + } + + public class When_Run_Is_Called_Using_List_Command_And_Has_Rules : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.Error, "Error ID", "Some Error Summary", "Help URL"), + new ImmutableRule(RuleType.Warning, "Warning ID", "Some Warning Summary", "Help URL"), + new ImmutableRule(RuleType.Information, "Information ID", "Some Information Summary", "Help URL"), + new ImmutableRule(RuleType.Note, "Note ID", "Some Note Summary", "Help URL"), + new ImmutableRule(RuleType.None, "Disabled ID", "Some Disabled Summary", "Help URL"), + }); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Output_Headers_In_Correct_Order() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInOrder( + "Implemented Package Rules", + "Error/Required Rules", + "Warning/Guideline Rules", + "Information/Suggestion Rules", + "Note Rules", + "Disabled Rules"); + } + + [Fact] + public void Should_Output_Error_Rules_With_Expected_Header() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "Error/Required Rules", + string.Empty, + "Error ID: Some Error Summary"); + } + + [Fact] + public void Should_Output_Warning_Rules_With_Expected_Header() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "Warning/Guideline Rules", + string.Empty, + "Warning ID: Some Warning Summary"); + } + + [Fact] + public void Should_Output_Information_Rules_With_Expected_Header() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "Information/Suggestion Rules", + string.Empty, + "Information ID: Some Information Summary"); + } + + [Fact] + public void Should_Output_Note_Rules_With_Expected_Header() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "Note Rules", + string.Empty, + "Note ID: Some Note Summary"); + } + + [Fact] + public void Should_Output_Disabled_Rules_With_Expected_Header() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "Disabled Rules", + string.Empty, + "Disabled ID: Some Disabled Summary"); + } + } + + public class When_Run_Is_Called_Using_List_Command_And_Has_Rules_With_Limited_Output : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RegularOutput = false; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.Error, "Error ID", "Some Error Summary", "Help URL"), + new ImmutableRule(RuleType.Warning, "Warning ID", "Some Warning Summary", "Help URL"), + new ImmutableRule(RuleType.Information, "Information ID", "Some Information Summary", "Help URL"), + new ImmutableRule(RuleType.Note, "Note ID", "Some Note Summary", "Help URL"), + new ImmutableRule(RuleType.None, "Disabled ID", "Some Disabled Summary", "Help URL"), + }); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Output_Rules_With_In_Order_With_Limited_Output() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "Error|Error ID|Some Error Summary|Help URL", + "Warning|Warning ID|Some Warning Summary|Help URL", + "Information|Information ID|Some Information Summary|Help URL", + "Note|Note ID|Some Note Summary|Help URL", + "None|Disabled ID|Some Disabled Summary|Help URL"); + } + } + + public class When_Run_Is_Called_Using_List_Command_Without_Any_Rules_Available : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(Array.Empty()); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Output_Rules_With_In_Order_With_Limited_Output() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInOrder( + "Implemented Package Rules", + "Error/Required Rules", + "No implemented Error/Required rules available.", + "Warning/Guideline Rules", + "No implemented Warning/Guideline rules available.", + "Information/Suggestion Rules", + "No implemented Information/Suggestion rules available.", + "Note Rules", + "No implemented Note rules available.", + "Disabled Rules", + "No implemented Disabled rules available."); + } + } + + public class When_Run_Is_Called_Using_List_Command_Without_Any_Rules_Available_With_Limited_Output : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RegularOutput = false; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(Array.Empty()); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Not_Output_Any_Rules() + { + MockLogger.Messages.Should().NotContainKey(LogLevel.Info.ToString()); + } + } + + public class When_Run_Is_Called_With_Rule_Name_And_Error_Severity : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RuleCommand.Name = "ErrorID"; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.Error, "ErrorID", "My Rule Summary", "https://rules.info/rule/ErrorID"), + new ImmutableRule(RuleType.Error, "OtherID", "Some Other Rule Summary") + }); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Output_Information_On_Found_Rule() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "ID: ErrorID | Severity: Error", + "Summary: My Rule Summary", + "Help URL: https://rules.info/rule/ErrorID"); + } + } + + public class When_Run_Is_Called_With_Rule_Name_And_Warning_Severity : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RuleCommand.Name = "WarningID"; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.Warning, "WarningID", "My Rule Summary", "https://rules.info/rule/WarningID"), + new ImmutableRule(RuleType.Error, "OtherID", "Some Other Rule Summary") + }); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Output_Information_On_Found_Rule() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "ID: WarningID | Severity: Warning", + "Summary: My Rule Summary", + "Help URL: https://rules.info/rule/WarningID"); + } + } + + public class When_Run_Is_Called_With_Rule_Name_And_Information_Severity : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RuleCommand.Name = "InformationID"; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.Information, "InformationID", "My Rule Summary", "https://rules.info/rule/InformationID"), + new ImmutableRule(RuleType.Error, "OtherID", "Some Other Rule Summary") + }); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Output_Information_On_Found_Rule() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "ID: InformationID | Severity: Information", + "Summary: My Rule Summary", + "Help URL: https://rules.info/rule/InformationID"); + } + } + + public class When_Run_Is_Called_With_Rule_Name_And_Note_Severity : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RuleCommand.Name = "NoteID"; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.Note, "NoteID", "My Rule Summary", "https://rules.info/rule/NoteID"), + new ImmutableRule(RuleType.Error, "OtherID", "Some Other Rule Summary") + }); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Output_Information_On_Found_Rule() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "ID: NoteID | Severity: Note", + "Summary: My Rule Summary", + "Help URL: https://rules.info/rule/NoteID"); + } + } + + public class When_Run_Is_Called_With_Rule_Name_And_Disabled_Severity : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RuleCommand.Name = "DisabledID"; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.None, "DisabledID", "My Rule Summary", "https://rules.info/rule/DisabledID"), + new ImmutableRule(RuleType.Error, "OtherID", "Some Other Rule Summary") + }); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Output_Information_On_Found_Rule() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().ContainInConsecutiveOrder( + "ID: DisabledID | Severity: None", + "Summary: My Rule Summary", + "Help URL: https://rules.info/rule/DisabledID"); + } + } + + public class When_Run_Is_Called_With_Rule_Name_And_Does_Not_Exist : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RuleCommand.Name = "NoneExistingID"; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.Error, "OtherID", "I should not be found") + }); + } + + public override void Because() + { + } + + [Fact] + public void Throws_Expected_Argument_Exception() + { + Action action = () => Command.Run(Configuration); + action.Should().Throw() + .WithMessage("No rule with the identifier NoneExistingID could be found."); + } + } + + public class When_Run_Is_Called_With_Rule_Name_And_Limited_Output : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RuleCommand.Name = "LimitedRule"; + Configuration.RegularOutput = false; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.Warning, "LimitedRule", "I am the summary of a warning.", "https://limited.info/rules/CCCC"), + new ImmutableRule(RuleType.Warning, "OtherID", "I Should Not be Outputted") + }); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Only_Output_Single_Rule_In_Parsable_Output() + { + MockLogger.Messages.Should() + .ContainKey(LogLevel.Info.ToString()) + .WhoseValue.Should().OnlyContain(v => v == "Warning|LimitedRule|I am the summary of a warning.|https://limited.info/rules/CCCC"); + } + } + + public class When_Run_Is_Called_With_Not_Found_Rule_Name_And_Limited_Output : ChocolateyRuleCommandSpecsBase + { + public override void Context() + { + base.Context(); + Configuration.RuleCommand.Name = "LimitedRule"; + Configuration.RegularOutput = false; + + RuleService.Setup(r => r.GetAllAvailableRules()) + .Returns(new[] + { + new ImmutableRule(RuleType.Warning, "OtherID", "I Should Not be Outputted") + }); + } + + public override void Because() + { + Command.Run(Configuration); + } + + [Fact] + public void Should_Not_Output_Any_Rules() + { + MockLogger.Messages.Should().NotContainKey(LogLevel.Info.ToString()); + } + } + } +} diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index a945314f6d..edcee6b9a5 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -204,6 +204,7 @@ + diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyRuleCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyRuleCommand.cs new file mode 100644 index 0000000000..e2bc6f39fa --- /dev/null +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyRuleCommand.cs @@ -0,0 +1,236 @@ +// Copyright © 2017 - 2024 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.commands +{ + using System; + using System.Collections.Generic; + using System.Linq; + using chocolatey.infrastructure.app.attributes; + using chocolatey.infrastructure.app.configuration; + using chocolatey.infrastructure.commandline; + using chocolatey.infrastructure.commands; + using chocolatey.infrastructure.logging; + using chocolatey.infrastructure.rules; + using chocolatey.infrastructure.services; + + [CommandFor("rule", "view or list implemented package rules", Version = "2.3.0")] + [CommandFor("rules", "view or list implemented package rules (alias for rule)", Version = "2.3.0")] + public class ChocolateyRuleCommand : ChocolateyCommandBase, ICommand + { + private readonly IRuleService _ruleService; + + public ChocolateyRuleCommand(IRuleService ruleService) + { + _ruleService = ruleService ?? throw new ArgumentNullException(nameof(ruleService)); + } + + public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfiguration configuration) + { + optionSet + .Add("n=|name=", + "The name or identifier of the rule to show more details about. Required with actions other than list. Defaults to empty.", + option => configuration.RuleCommand.Name = option); + } + + public virtual void DryRun(ChocolateyConfiguration configuration) + { + Run(configuration); + } + + public virtual bool MayRequireAdminAccess() + { + return false; + } + + public virtual void ParseAdditionalArguments(IList unparsedArguments, ChocolateyConfiguration configuration) + { + configuration.Input = string.Join(" ", unparsedArguments); + + var command = unparsedArguments.DefaultIfEmpty().Select(a => a.ToLowerSafe()).FirstOrDefault(); + + if (string.IsNullOrEmpty(command)) + { + command = "list"; + } + + configuration.RuleCommand.Command = command; + + if (string.IsNullOrEmpty(configuration.RuleCommand.Name) && unparsedArguments.Count >= 2) + { + configuration.RuleCommand.Name = unparsedArguments[1]; + } + } + + public virtual void Run(ChocolateyConfiguration config) + { + IEnumerable implementedRules = _ruleService.GetAllAvailableRules().OrderBy(r => r.Id); + + if (!string.IsNullOrEmpty(config.RuleCommand.Name)) + { + var foundRule = implementedRules.FirstOrDefault(i => i.Id.IsEqualTo(config.RuleCommand.Name)); + + // Since the return value is a structure, it will never be null. + // As such we check that the identifier is not empty. + if (config.RegularOutput) + { + if (string.IsNullOrEmpty(foundRule.Id)) + { + throw new ApplicationException("No rule with the identifier {0} could be found.".FormatWith(config.RuleCommand.Name)); + } + + // Not using multiline logging here, as it causes issues + // with unit tests. + this.Log().Info("ID: {0} | Severity: {1}", foundRule.Id, foundRule.Severity); + this.Log().Info("Summary: {0}", foundRule.Summary); + this.Log().Info("Help URL: {0}", foundRule.HelpUrl); + + return; + } + else if (!string.IsNullOrEmpty(foundRule.Id)) + { + implementedRules = new[] { foundRule }; + } + else + { + return; + } + } + + if (config.RegularOutput) + { + this.Log().Info(ChocolateyLoggers.Important, "Implemented Package Rules"); + } + + OutputRules("Error/Required", config, implementedRules.Where(r => r.Severity == RuleType.Error).ToList()); + OutputRules("Warning/Guideline", config, implementedRules.Where(r => r.Severity == RuleType.Warning).ToList()); + OutputRules("Information/Suggestion", config, implementedRules.Where(r => r.Severity == RuleType.Information).ToList()); + OutputRules("Note", config, implementedRules.Where(r => r.Severity == RuleType.Note).ToList()); + OutputRules("Disabled", config, implementedRules.Where(r => r.Severity == RuleType.None).ToList()); + } + + public virtual void Validate(ChocolateyConfiguration configuration) + { + switch (configuration.RuleCommand.Command) + { + case "list": + if (!string.IsNullOrEmpty(configuration.RuleCommand.Name)) + { + throw new ApplicationException("A Rule Name can not be specified when listing all validation rules."); + } + break; + case "get": + if (string.IsNullOrEmpty(configuration.RuleCommand.Name)) + { + throw new ApplicationException("A Rule Name is required to when not listing all validation rules."); + } + break; + + default: + this.Log().Warn("Unknown command '{0}'. Setting to list.", configuration.RuleCommand.Command); + configuration.RuleCommand.Command = "list"; + Validate(configuration); + break; + } + } + + protected override string GetCommandDescription(CommandForAttribute attribute, ChocolateyConfiguration configuration) + { + return @"Get or list information about what rule validations is implemented by Chocolatey CLI +or one of the loaded extensions."; + } + + protected override IEnumerable GetCommandExamples(CommandForAttribute[] attributes, ChocolateyConfiguration configuration) + { + return new[] + { + "choco rules", + "choco rules list", + "choco rule get --name CHCR0002" + }; + } + + protected override IEnumerable GetCommandUsage(CommandForAttribute[] attributes, ChocolateyConfiguration configuration) + { + return new[] + { + "choco rule [list]|get [options/switches]", + "choco rules [list]|get [options/switches]", + }; + } + + protected virtual void OutputRules(string type, ChocolateyConfiguration config, IReadOnlyList rules) + { + if (config.RegularOutput) + { + this.Log().Info(""); + this.Log().Info(ChocolateyLoggers.Important, type + " Rules"); + this.Log().Info(""); + + if (rules.Count == 0) + { + this.Log().Info("No implemented " + type + " rules available."); + + return; + } + } + + foreach (var rule in rules) + { + if (config.RegularOutput) + { + this.Log().Info("{0}: {1}", rule.Id, rule.Summary); + } + else + { + this.Log().Info("{0}|{1}|{2}|{3}", rule.Severity, rule.Id, rule.Summary, rule.HelpUrl); + } + } + } + +#pragma warning disable IDE1006 + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void configure_argument_parser(OptionSet optionSet, ChocolateyConfiguration configuration) + => ConfigureArgumentParser(optionSet, configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void handle_additional_argument_parsing(IList unparsedArguments, ChocolateyConfiguration configuration) + => ParseAdditionalArguments(unparsedArguments, configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void handle_validation(ChocolateyConfiguration configuration) + => Validate(configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void help_message(ChocolateyConfiguration configuration) + => HelpMessage(configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual bool may_require_admin_access() + => MayRequireAdminAccess(); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void noop(ChocolateyConfiguration configuration) + => DryRun(configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void run(ChocolateyConfiguration configuration) + => Run(configuration); + +#pragma warning restore IDE1006 + } +} diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index 5ec4816f43..dc75749afa 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -58,6 +58,7 @@ public ChocolateyConfiguration() ExportCommand = new ExportCommandConfiguration(); TemplateCommand = new TemplateCommandConfiguration(); CacheCommand = new CacheCommandConfiguration(); + RuleCommand = new RuleCommandConfiguration(); #if DEBUG AllowUnofficialBuild = true; #endif @@ -494,6 +495,11 @@ private void AppendOutput(StringBuilder propertyValues, string append) /// public CacheCommandConfiguration CacheCommand { get; set; } + /// + /// Gets or sets the configuration related specifically to the Rule command. + /// + public RuleCommandConfiguration RuleCommand { get; set; } + #pragma warning disable IDE1006 [Obsolete("This overload is deprecated and will be removed in v3.")] public void start_backup() @@ -746,4 +752,18 @@ public sealed class CacheCommandConfiguration /// public bool RemoveExpiredItemsOnly { get; set; } } + + [Serializable] + public sealed class RuleCommandConfiguration + { + /// + /// Gets or sets the name or identifier of a rule the user wants to use for the command. + /// + public string Name { get; set; } + + /// + /// Gets or sets the sub command to execute. + /// + public string Command { get; set; } + } }