diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md index 8127c0816d..f28156904d 100644 --- a/docs/CHANGELOG-v3.md +++ b/docs/CHANGELOG-v3.md @@ -29,6 +29,9 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since pre-release v3.0.0-B0351: +- General improvements: + - Added support for registering custom emitters by @BernieWhite. + [#2681](https://github.com/microsoft/PSRule/issues/2681) - Engineering: - Migrate samples into PSRule repository by @BernieWhite. [#2614](https://github.com/microsoft/PSRule/issues/2614) diff --git a/docs/concepts/emitters.md b/docs/concepts/emitters.md index 4a0f4ee12d..d9df0e0673 100644 --- a/docs/concepts/emitters.md +++ b/docs/concepts/emitters.md @@ -28,7 +28,11 @@ Name | Default file extensions | Configurable ## Custom emitters -Custom emitters are a planned feature in PSRule v3. +Custom emitters can be created by implementing the `PSRule.Emitters.IEmitter` interface available in `Microsoft.PSRule.Types`. +This custom type implementation will be loaded by PSRule and used to process the input object. + +To use a custom emitter, it must be registered with PSRule as a service. +This can be done by a convention within the `-Initialize` script block. ## Configuring formats diff --git a/src/PSRule.Types/Emitters/IEmitter.cs b/src/PSRule.Types/Emitters/IEmitter.cs index 17a4deb3ef..28a342309b 100644 --- a/src/PSRule.Types/Emitters/IEmitter.cs +++ b/src/PSRule.Types/Emitters/IEmitter.cs @@ -11,7 +11,7 @@ public interface IEmitter : IDisposable /// /// Visit an object and emit any input objects for processing. /// - /// A context object for the emitter. + /// The current context for the emitter. /// The object to visit. /// Returns true when the emitter processed the object and false when it did not. bool Visit(IEmitterContext context, object o); @@ -19,15 +19,8 @@ public interface IEmitter : IDisposable /// /// Determines if the emitter accepts the specified object type. /// - /// - /// - /// + /// The current context for the emitter. + /// The type of object. + /// Returns true if the emitter supports processing the object type and false if it does not. bool Accepts(IEmitterContext context, Type type); - - ///// - ///// Configure the emitter using an options instance. - ///// - ///// - ///// - //bool Configure(PSRuleOption option); } diff --git a/src/PSRule.Types/Runtime/IRuntimeServiceCollection.cs b/src/PSRule.Types/Runtime/IRuntimeServiceCollection.cs new file mode 100644 index 0000000000..40c76d0de3 --- /dev/null +++ b/src/PSRule.Types/Runtime/IRuntimeServiceCollection.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Runtime; + +/// +/// A context for registering scoped runtime services within a factory. +/// +public interface IRuntimeServiceCollection : IDisposable +{ + /// + /// The name of the scope. + /// + string ScopeName { get; } + + /// + /// Access configuration values at runtime. + /// + IConfiguration Configuration { get; } + + /// + /// Add a service. + /// + /// The specified interface type. + /// The concrete type to add. + void AddService() + where TInterface : class + where TService : class, TInterface; + + /// + /// Add a service. + /// + /// A unique name of the service instance. + /// An instance of the service. + void AddService(string instanceName, object instance); +} diff --git a/src/PSRule/Common/JsonConverters.cs b/src/PSRule/Common/JsonConverters.cs index bb3776a5d6..0dd1a55e22 100644 --- a/src/PSRule/Common/JsonConverters.cs +++ b/src/PSRule/Common/JsonConverters.cs @@ -11,8 +11,8 @@ using PSRule.Definitions; using PSRule.Definitions.Baselines; using PSRule.Definitions.Expressions; +using PSRule.Emitters; using PSRule.Pipeline; -using PSRule.Pipeline.Emitters; using PSRule.Resources; using PSRule.Runtime; diff --git a/src/PSRule/Common/YamlConverters.cs b/src/PSRule/Common/YamlConverters.cs index 2bee8a8464..6be2634b89 100644 --- a/src/PSRule/Common/YamlConverters.cs +++ b/src/PSRule/Common/YamlConverters.cs @@ -10,15 +10,16 @@ using PSRule.Data; using PSRule.Definitions; using PSRule.Definitions.Expressions; +using PSRule.Emitters; using PSRule.Host; using PSRule.Pipeline; -using PSRule.Pipeline.Emitters; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; +using IEmitter = YamlDotNet.Core.IEmitter; namespace PSRule; diff --git a/src/PSRule/Pipeline/Emitters/BaseEmitterContext.cs b/src/PSRule/Emitters/BaseEmitterContext.cs similarity index 86% rename from src/PSRule/Pipeline/Emitters/BaseEmitterContext.cs rename to src/PSRule/Emitters/BaseEmitterContext.cs index 412f7748fb..ffe945dcc4 100644 --- a/src/PSRule/Pipeline/Emitters/BaseEmitterContext.cs +++ b/src/PSRule/Emitters/BaseEmitterContext.cs @@ -4,11 +4,11 @@ using System.Collections; using System.Management.Automation; using PSRule.Data; -using PSRule.Emitters; using PSRule.Options; +using PSRule.Pipeline; using PSRule.Runtime; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; /// /// @@ -45,7 +45,7 @@ public void Emit(ITargetObject value) protected abstract void Enqueue(ITargetObject value); - private static IEnumerable ReadObjectPath(ITargetObject targetObject, string objectPath, bool caseSensitive) + private static ITargetObject[] ReadObjectPath(ITargetObject targetObject, string objectPath, bool caseSensitive) { if (!ObjectHelper.GetPath( bindingContext: null, @@ -59,10 +59,10 @@ private static IEnumerable ReadObjectPath(ITargetObject targetObj if (typeof(IEnumerable).IsAssignableFrom(nestedType)) { var result = new List(); - foreach (var item in (nestedObject as IEnumerable)) + foreach (var item in nestedObject as IEnumerable) result.Add(new TargetObject(PSObject.AsPSObject(item))); - return result.ToArray(); + return [.. result]; } else { diff --git a/src/PSRule/Pipeline/Emitters/EmitterBuilder.cs b/src/PSRule/Emitters/EmitterBuilder.cs similarity index 81% rename from src/PSRule/Pipeline/Emitters/EmitterBuilder.cs rename to src/PSRule/Emitters/EmitterBuilder.cs index 0ea72678e9..95b5762408 100644 --- a/src/PSRule/Pipeline/Emitters/EmitterBuilder.cs +++ b/src/PSRule/Emitters/EmitterBuilder.cs @@ -3,11 +3,10 @@ using Microsoft.Extensions.DependencyInjection; using PSRule.Definitions; -using PSRule.Emitters; using PSRule.Options; using PSRule.Runtime; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; #nullable enable @@ -29,6 +28,7 @@ public EmitterBuilder(ILanguageScopeSet? languageScopeSet = default, IFormatOpti _Services = new ServiceCollection(); AddInternalServices(); AddInternalEmitters(); + AddEmittersFromLanguageScope(); } /// @@ -45,6 +45,20 @@ public void AddEmitter(string scope) where T : class, IEmitter _Services.AddScoped(typeof(T)); } + /// + /// Add an implementation class. + /// + /// The scope of the emitter. + /// An emitter type that implements . + /// The parameter must not be a null or empty string. + public void AddEmitter(string scope, Type type) + { + if (string.IsNullOrEmpty(scope)) throw new ArgumentNullException(nameof(scope)); + + _EmitterTypes.Add(new KeyValuePair(scope, type)); + _Services.AddScoped(type); + } + /// /// Add an existing emitter instance that is already configured. /// @@ -118,6 +132,22 @@ private void AddInternalEmitters() AddEmitter(ResourceHelper.STANDALONE_SCOPE_NAME); } + /// + /// Add custom emitters from the language scope. + /// + private void AddEmittersFromLanguageScope() + { + if (_LanguageScopeSet == null) return; + + foreach (var scope in _LanguageScopeSet.Get()) + { + foreach (var emitterType in scope.GetEmitters()) + { + AddEmitter(scope.Name, emitterType); + } + } + } + /// /// Create a configuration for the emitter based on it's scope. /// diff --git a/src/PSRule/Pipeline/Emitters/EmitterChain.cs b/src/PSRule/Emitters/EmitterChain.cs similarity index 67% rename from src/PSRule/Pipeline/Emitters/EmitterChain.cs rename to src/PSRule/Emitters/EmitterChain.cs index a67d65602b..0369118aee 100644 --- a/src/PSRule/Pipeline/Emitters/EmitterChain.cs +++ b/src/PSRule/Emitters/EmitterChain.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Emitters; -namespace PSRule.Pipeline.Emitters; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Emitters; /// /// A chain of emitters. diff --git a/src/PSRule/Pipeline/Emitters/EmitterCollection.cs b/src/PSRule/Emitters/EmitterCollection.cs similarity index 98% rename from src/PSRule/Pipeline/Emitters/EmitterCollection.cs rename to src/PSRule/Emitters/EmitterCollection.cs index a9abd942b4..7fad0c5c3f 100644 --- a/src/PSRule/Pipeline/Emitters/EmitterCollection.cs +++ b/src/PSRule/Emitters/EmitterCollection.cs @@ -4,9 +4,9 @@ using System.Management.Automation; using Microsoft.Extensions.DependencyInjection; using PSRule.Data; -using PSRule.Emitters; +using PSRule.Pipeline; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; #nullable enable diff --git a/src/PSRule/Pipeline/Emitters/EmitterContext.cs b/src/PSRule/Emitters/EmitterContext.cs similarity index 96% rename from src/PSRule/Pipeline/Emitters/EmitterContext.cs rename to src/PSRule/Emitters/EmitterContext.cs index d725f33a1a..5c918e6483 100644 --- a/src/PSRule/Pipeline/Emitters/EmitterContext.cs +++ b/src/PSRule/Emitters/EmitterContext.cs @@ -4,10 +4,10 @@ using System.Collections.Concurrent; using PSRule.Configuration; using PSRule.Data; -using PSRule.Emitters; using PSRule.Options; +using PSRule.Pipeline; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; #nullable enable diff --git a/src/PSRule/Pipeline/Emitters/InternalEmitterConfiguration.cs b/src/PSRule/Emitters/InternalEmitterConfiguration.cs similarity index 95% rename from src/PSRule/Pipeline/Emitters/InternalEmitterConfiguration.cs rename to src/PSRule/Emitters/InternalEmitterConfiguration.cs index fe3ed82001..769dea7a8e 100644 --- a/src/PSRule/Pipeline/Emitters/InternalEmitterConfiguration.cs +++ b/src/PSRule/Emitters/InternalEmitterConfiguration.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Emitters; using PSRule.Options; using PSRule.Runtime; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; #nullable enable diff --git a/src/PSRule/Pipeline/Emitters/InternalFileInfo.cs b/src/PSRule/Emitters/InternalFileInfo.cs similarity index 97% rename from src/PSRule/Pipeline/Emitters/InternalFileInfo.cs rename to src/PSRule/Emitters/InternalFileInfo.cs index 1d80852396..ec385d26b4 100644 --- a/src/PSRule/Pipeline/Emitters/InternalFileInfo.cs +++ b/src/PSRule/Emitters/InternalFileInfo.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using PSRule.Data; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; [DebuggerDisplay("{Path}")] internal sealed class InternalFileInfo : IFileInfo, IDisposable diff --git a/src/PSRule/Pipeline/Emitters/InternalFileStream.cs b/src/PSRule/Emitters/InternalFileStream.cs similarity index 96% rename from src/PSRule/Pipeline/Emitters/InternalFileStream.cs rename to src/PSRule/Emitters/InternalFileStream.cs index cb288b735e..a6a7dd40b0 100644 --- a/src/PSRule/Pipeline/Emitters/InternalFileStream.cs +++ b/src/PSRule/Emitters/InternalFileStream.cs @@ -4,7 +4,7 @@ using System.Text; using PSRule.Data; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; internal sealed class InternalFileStream : IFileStream { diff --git a/src/PSRule/Pipeline/Emitters/JsonEmitter.cs b/src/PSRule/Emitters/JsonEmitter.cs similarity index 98% rename from src/PSRule/Pipeline/Emitters/JsonEmitter.cs rename to src/PSRule/Emitters/JsonEmitter.cs index 5284ec0ddf..85529c9928 100644 --- a/src/PSRule/Pipeline/Emitters/JsonEmitter.cs +++ b/src/PSRule/Emitters/JsonEmitter.cs @@ -5,10 +5,10 @@ using System.Management.Automation; using Newtonsoft.Json; using PSRule.Data; -using PSRule.Emitters; +using PSRule.Pipeline; using PSRule.Runtime; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; /// /// An for processing JSON. diff --git a/src/PSRule/Pipeline/Emitters/JsonEmitterParser.cs b/src/PSRule/Emitters/JsonEmitterParser.cs similarity index 90% rename from src/PSRule/Pipeline/Emitters/JsonEmitterParser.cs rename to src/PSRule/Emitters/JsonEmitterParser.cs index 94ee2c6f53..1c259bb4d8 100644 --- a/src/PSRule/Pipeline/Emitters/JsonEmitterParser.cs +++ b/src/PSRule/Emitters/JsonEmitterParser.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; using PSRule.Data; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; internal sealed class JsonEmitterParser : JsonTextReader { diff --git a/src/PSRule/Pipeline/Emitters/MarkdownEmitter.cs b/src/PSRule/Emitters/MarkdownEmitter.cs similarity index 97% rename from src/PSRule/Pipeline/Emitters/MarkdownEmitter.cs rename to src/PSRule/Emitters/MarkdownEmitter.cs index 21cb1c26db..c52ae43fa3 100644 --- a/src/PSRule/Pipeline/Emitters/MarkdownEmitter.cs +++ b/src/PSRule/Emitters/MarkdownEmitter.cs @@ -4,10 +4,10 @@ using System.Collections.Immutable; using System.Management.Automation; using PSRule.Data; -using PSRule.Emitters; using PSRule.Help; +using PSRule.Pipeline; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; /// /// An for processing Markdown. diff --git a/src/PSRule/Pipeline/Emitters/PowerShellDataEmitter.cs b/src/PSRule/Emitters/PowerShellDataEmitter.cs similarity index 98% rename from src/PSRule/Pipeline/Emitters/PowerShellDataEmitter.cs rename to src/PSRule/Emitters/PowerShellDataEmitter.cs index 81116c6e43..a50787716d 100644 --- a/src/PSRule/Pipeline/Emitters/PowerShellDataEmitter.cs +++ b/src/PSRule/Emitters/PowerShellDataEmitter.cs @@ -4,9 +4,9 @@ using System.Collections.Immutable; using System.Management.Automation; using PSRule.Data; -using PSRule.Emitters; +using PSRule.Pipeline; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; /// /// An for processing PowerShell Data. diff --git a/src/PSRule/Pipeline/Emitters/YamlEmitter.cs b/src/PSRule/Emitters/YamlEmitter.cs similarity index 97% rename from src/PSRule/Pipeline/Emitters/YamlEmitter.cs rename to src/PSRule/Emitters/YamlEmitter.cs index 9bd4d77523..6eb9d7cfbc 100644 --- a/src/PSRule/Pipeline/Emitters/YamlEmitter.cs +++ b/src/PSRule/Emitters/YamlEmitter.cs @@ -4,17 +4,17 @@ using System.Collections.Immutable; using PSRule.Data; using PSRule.Definitions; -using PSRule.Emitters; +using PSRule.Pipeline; using PSRule.Runtime; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NodeDeserializers; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; /// -/// An for processing YAML. +/// An for processing YAML. /// internal sealed class YamlEmitter : FileEmitter { diff --git a/src/PSRule/Pipeline/Emitters/YamlEmitterParser.cs b/src/PSRule/Emitters/YamlEmitterParser.cs similarity index 91% rename from src/PSRule/Pipeline/Emitters/YamlEmitterParser.cs rename to src/PSRule/Emitters/YamlEmitterParser.cs index efd32b9779..ce8017de35 100644 --- a/src/PSRule/Pipeline/Emitters/YamlEmitterParser.cs +++ b/src/PSRule/Emitters/YamlEmitterParser.cs @@ -4,7 +4,7 @@ using PSRule.Data; using YamlDotNet.Core; -namespace PSRule.Pipeline.Emitters; +namespace PSRule.Emitters; /// /// A custom parser that implements source mapping. diff --git a/src/PSRule/Pipeline/AssertPipelineBuilder.cs b/src/PSRule/Pipeline/AssertPipelineBuilder.cs index 4bfe3eb804..9c11355bd3 100644 --- a/src/PSRule/Pipeline/AssertPipelineBuilder.cs +++ b/src/PSRule/Pipeline/AssertPipelineBuilder.cs @@ -201,9 +201,8 @@ public sealed override IPipeline Build(IPipelineWriter writer = null) return !RequireModules() || !RequireSources() ? null : (IPipeline)new InvokeRulePipeline( - context: PrepareContext(PipelineHookActions.Default), + context: PrepareContext(PipelineHookActions.Default, writer: HandleJobSummary(writer ?? PrepareWriter())), source: Source, - writer: HandleJobSummary(writer ?? PrepareWriter()), outcome: RuleOutcome.Processed); } diff --git a/src/PSRule/Pipeline/ExportBaselinePipelineBuilder.cs b/src/PSRule/Pipeline/ExportBaselinePipelineBuilder.cs index 65ed92f87b..12f94f3464 100644 --- a/src/PSRule/Pipeline/ExportBaselinePipelineBuilder.cs +++ b/src/PSRule/Pipeline/ExportBaselinePipelineBuilder.cs @@ -43,10 +43,8 @@ public override IPipeline Build(IPipelineWriter writer = null) { var filter = new BaselineFilter(_Name); return new GetBaselinePipeline( - pipeline: PrepareContext(PipelineHookActions.Empty), + pipeline: PrepareContext(PipelineHookActions.Empty, writer ?? PrepareWriter()), source: Source, - reader: PrepareReader(), - writer: writer ?? PrepareWriter(), filter: filter ); } diff --git a/src/PSRule/Pipeline/GetBaselinePipeline.cs b/src/PSRule/Pipeline/GetBaselinePipeline.cs index 7da5af5a0e..63caf1b9f7 100644 --- a/src/PSRule/Pipeline/GetBaselinePipeline.cs +++ b/src/PSRule/Pipeline/GetBaselinePipeline.cs @@ -14,19 +14,17 @@ internal sealed class GetBaselinePipeline : RulePipeline internal GetBaselinePipeline( PipelineContext pipeline, Source[] source, - PipelineInputStream reader, - IPipelineWriter writer, IResourceFilter filter ) - : base(pipeline, source, reader, writer) + : base(pipeline, source) { _Filter = filter; } public override void End() { - Writer.WriteObject(HostHelper.GetBaseline(Source, Context).Where(Match), true); - Writer.End(Result); + Pipeline.Writer.WriteObject(HostHelper.GetBaseline(Source, Context).Where(Match), true); + Pipeline.Writer.End(Result); } private bool Match(Baseline baseline) diff --git a/src/PSRule/Pipeline/GetBaselinePipelineBuilder.cs b/src/PSRule/Pipeline/GetBaselinePipelineBuilder.cs index 191e2cf3d5..0c2e708c69 100644 --- a/src/PSRule/Pipeline/GetBaselinePipelineBuilder.cs +++ b/src/PSRule/Pipeline/GetBaselinePipelineBuilder.cs @@ -41,10 +41,8 @@ public override IPipeline Build(IPipelineWriter writer = null) { var filter = new BaselineFilter(ResolveBaselineGroup(_Name)); return new GetBaselinePipeline( - pipeline: PrepareContext(PipelineHookActions.Empty), + pipeline: PrepareContext(PipelineHookActions.Empty, writer: writer), source: Source, - reader: PrepareReader(), - writer: writer ?? PrepareWriter(), filter: filter ); } diff --git a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs index 532aa8d979..e1c3b66060 100644 --- a/src/PSRule/Pipeline/GetRuleHelpPipeline.cs +++ b/src/PSRule/Pipeline/GetRuleHelpPipeline.cs @@ -50,10 +50,9 @@ public void Online() public override IPipeline Build(IPipelineWriter writer = null) { return new GetRuleHelpPipeline( - pipeline: PrepareContext(PipelineHookActions.Empty), - source: Source, - reader: PrepareReader(), - writer: writer ?? PrepareWriter()); + pipeline: PrepareContext(PipelineHookActions.Empty, writer: writer ?? PrepareWriter()), + source: Source + ); } private sealed class HelpWriter : PipelineWriter @@ -152,14 +151,14 @@ protected override PipelineWriter PrepareWriter() internal sealed class GetRuleHelpPipeline : RulePipeline, IPipeline { - internal GetRuleHelpPipeline(PipelineContext pipeline, Source[] source, PipelineInputStream reader, IPipelineWriter writer) - : base(pipeline, source, reader, writer) + internal GetRuleHelpPipeline(PipelineContext pipeline, Source[] source) + : base(pipeline, source) { // Do nothing } public override void End() { - Writer.WriteObject(HostHelper.GetRuleHelp(Context), true); + Pipeline.Writer.WriteObject(HostHelper.GetRuleHelp(Context), true); } } diff --git a/src/PSRule/Pipeline/GetRulePipeline.cs b/src/PSRule/Pipeline/GetRulePipeline.cs index eb5429c731..9b5a8a745c 100644 --- a/src/PSRule/Pipeline/GetRulePipeline.cs +++ b/src/PSRule/Pipeline/GetRulePipeline.cs @@ -12,18 +12,16 @@ internal sealed class GetRulePipeline : RulePipeline, IPipeline internal GetRulePipeline( PipelineContext pipeline, Source[] source, - PipelineInputStream reader, - IPipelineWriter writer, bool includeDependencies ) - : base(pipeline, source, reader, writer) + : base(pipeline, source) { _IncludeDependencies = includeDependencies; } public override void End() { - Writer.WriteObject(HostHelper.GetRule(Context, _IncludeDependencies), true); - Writer.End(Result); + Pipeline.Writer.WriteObject(HostHelper.GetRule(Context, _IncludeDependencies), true); + Pipeline.Writer.End(Result); } } diff --git a/src/PSRule/Pipeline/GetRulePipelineBuilder.cs b/src/PSRule/Pipeline/GetRulePipelineBuilder.cs index cc7b423d59..8e832e813f 100644 --- a/src/PSRule/Pipeline/GetRulePipelineBuilder.cs +++ b/src/PSRule/Pipeline/GetRulePipelineBuilder.cs @@ -45,10 +45,8 @@ public override IPipeline Build(IPipelineWriter writer = null) return !RequireModules() || !RequireSources() ? null : (IPipeline)new GetRulePipeline( - pipeline: PrepareContext(PipelineHookActions.Empty), + pipeline: PrepareContext(PipelineHookActions.Empty, writer: writer ?? PrepareWriter()), source: Source, - reader: PrepareReader(), - writer: writer ?? PrepareWriter(), includeDependencies: _IncludeDependencies ); } diff --git a/src/PSRule/Pipeline/GetTargetPipeline.cs b/src/PSRule/Pipeline/GetTargetPipeline.cs index 1a21919482..abcf85ee93 100644 --- a/src/PSRule/Pipeline/GetTargetPipeline.cs +++ b/src/PSRule/Pipeline/GetTargetPipeline.cs @@ -10,19 +10,19 @@ namespace PSRule.Pipeline; /// internal sealed class GetTargetPipeline : RulePipeline { - internal GetTargetPipeline(PipelineContext context, PipelineInputStream reader, IPipelineWriter writer) - : base(context, null, reader, writer) { } + internal GetTargetPipeline(PipelineContext context) + : base(context, null) { } public override void Process(PSObject sourceObject) { try { - Reader.Enqueue(sourceObject); - while (Reader.TryDequeue(out var next)) + Pipeline.Reader.Enqueue(sourceObject); + while (Pipeline.Reader.TryDequeue(out var next)) { // TODO: Temporary workaround to cast interface if (next is TargetObject to) - Writer.WriteObject(to.Value, false); + Pipeline.Writer.WriteObject(to.Value, false); } } catch (Exception) diff --git a/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs b/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs index e1c538b076..db5aa83152 100644 --- a/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs +++ b/src/PSRule/Pipeline/GetTargetPipelineBuilder.cs @@ -56,7 +56,7 @@ public void InputPath(string[] path) /// public override IPipeline Build(IPipelineWriter writer = null) { - return new GetTargetPipeline(PrepareContext(PipelineHookActions.Empty), PrepareReader(), writer ?? PrepareWriter()); + return new GetTargetPipeline(PrepareContext(PipelineHookActions.Empty, writer ?? PrepareWriter())); } /// diff --git a/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs b/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs index d60e099679..ff25f5891b 100644 --- a/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs +++ b/src/PSRule/Pipeline/InvokePipelineBuilderBase.cs @@ -93,7 +93,7 @@ public override IPipeline Build(IPipelineWriter writer = null) Unblock(writer); return !RequireModules() || !RequireSources() ? null - : (IPipeline)new InvokeRulePipeline(PrepareContext(PipelineHookActions.Default, writer), Source, writer, Option.Output.Outcome.Value); + : (IPipeline)new InvokeRulePipeline(PrepareContext(PipelineHookActions.Default, writer), Source, Option.Output.Outcome.Value); } protected void Unblock(IPipelineWriter writer) diff --git a/src/PSRule/Pipeline/InvokeRulePipeline.cs b/src/PSRule/Pipeline/InvokeRulePipeline.cs index a2eddf1eda..40ad347f31 100644 --- a/src/PSRule/Pipeline/InvokeRulePipeline.cs +++ b/src/PSRule/Pipeline/InvokeRulePipeline.cs @@ -25,8 +25,8 @@ internal sealed class InvokeRulePipeline : RulePipeline, IPipeline // Track whether Dispose has been called. private bool _Disposed; - internal InvokeRulePipeline(PipelineContext context, Source[] source, IPipelineWriter writer, RuleOutcome outcome) - : base(context, source, context.Reader, writer) + internal InvokeRulePipeline(PipelineContext context, Source[] source, RuleOutcome outcome) + : base(context, source) { _RuleGraph = HostHelper.GetRuleBlockGraph(Context); RuleCount = _RuleGraph.Count; @@ -58,8 +58,8 @@ public override void Process(PSObject sourceObject) { try { - Reader.Enqueue(sourceObject); - while (Reader.TryDequeue(out var next)) + Pipeline.Reader.Enqueue(sourceObject); + while (Pipeline.Reader.TryDequeue(out var next)) { // TODO: Temporary workaround to cast interface if (next is TargetObject to) @@ -67,7 +67,7 @@ public override void Process(PSObject sourceObject) var result = ProcessTargetObject(to); _Completed.Add(result); - Writer.WriteObject(result, false); + Pipeline.Writer.WriteObject(result, false); } } } @@ -89,9 +89,11 @@ public override void End() } if (_IsSummary) - Writer.WriteObject(_Summary.Values.Where(r => _Outcome == RuleOutcome.All || (r.Outcome & _Outcome) > 0).ToArray(), true); + { + Pipeline.Writer.WriteObject(_Summary.Values.Where(r => _Outcome == RuleOutcome.All || (r.Outcome & _Outcome) > 0).ToArray(), true); + } - Writer.End(Result); + Pipeline.Writer.End(Result); } private InvokeResult ProcessTargetObject(TargetObject targetObject) diff --git a/src/PSRule/Pipeline/PipelineBuilderBase.cs b/src/PSRule/Pipeline/PipelineBuilderBase.cs index 5c4e6816c5..2f040cce09 100644 --- a/src/PSRule/Pipeline/PipelineBuilderBase.cs +++ b/src/PSRule/Pipeline/PipelineBuilderBase.cs @@ -180,14 +180,20 @@ protected PipelineContext PrepareContext((BindTargetMethod bindTargetName, BindT var languageScopeSet = GetLanguageScopeSet(); var resourceCache = GetResourceCache(unresolved, languageScopeSet); + var options = GetOptionBuilder(resourceCache, binding); + + foreach (var scope in languageScopeSet.Get()) + { + scope.Configure(options.Build(scope.Name)); + } return PipelineContext.New( option: Option, hostContext: HostContext, - reader: PrepareReader(), + reader: PrepareReader, writer: writer, languageScope: languageScopeSet, - optionBuilder: GetOptionBuilder(resourceCache, binding), + optionBuilder: options, resourceCache: resourceCache ); } @@ -198,7 +204,7 @@ protected ILanguageScopeSet GetLanguageScopeSet() return _LanguageScopeSet; var builder = new LanguageScopeSetBuilder(); - builder.Init(Option, Source); + builder.Init(Source); return _LanguageScopeSet = builder.Build(); } diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index ad0fa251fc..1277c47ad7 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -59,7 +59,8 @@ internal sealed class PipelineContext : IPipelineContext, IBindingContext internal IList SuppressionGroup; internal readonly IHostContext HostContext; - internal readonly IPipelineReader Reader; + private readonly Func _GetReader; + internal IPipelineReader? Reader { get; private set; } internal readonly string RunId; internal readonly Stopwatch RunTime; @@ -77,7 +78,7 @@ internal sealed class PipelineContext : IPipelineContext, IBindingContext /// public ILanguageScopeSet LanguageScope { get; } - private PipelineContext(PSRuleOption option, IHostContext hostContext, IPipelineReader reader, IPipelineWriter writer, ILanguageScopeSet languageScope, OptionContextBuilder optionBuilder, ResourceCache resourceCache) + private PipelineContext(PSRuleOption option, IHostContext hostContext, Func reader, IPipelineWriter writer, ILanguageScopeSet languageScope, OptionContextBuilder optionBuilder, ResourceCache resourceCache) { Option = option ?? throw new ArgumentNullException(nameof(option)); LanguageScope = languageScope ?? throw new ArgumentNullException(nameof(languageScope)); @@ -86,7 +87,7 @@ private PipelineContext(PSRuleOption option, IHostContext hostContext, IPipeline ResourceCache = resourceCache; HostContext = hostContext; - Reader = reader; + _GetReader = reader; Writer = writer; _LanguageMode = option.Execution.LanguageMode ?? ExecutionOption.Default.LanguageMode!.Value; _PathExpressionCache = []; @@ -103,7 +104,7 @@ private PipelineContext(PSRuleOption option, IHostContext hostContext, IPipeline LanguageScope = languageScope; } - public static PipelineContext New(PSRuleOption option, IHostContext hostContext, IPipelineReader reader, IPipelineWriter writer, ILanguageScopeSet languageScope, OptionContextBuilder optionBuilder, ResourceCache resourceCache) + public static PipelineContext New(PSRuleOption option, IHostContext hostContext, Func reader, IPipelineWriter writer, ILanguageScopeSet languageScope, OptionContextBuilder optionBuilder, ResourceCache resourceCache) { var context = new PipelineContext(option, hostContext, reader, writer, languageScope, optionBuilder, resourceCache); CurrentThread = context; @@ -177,6 +178,8 @@ internal void Initialize(RunspaceContext runspaceContext, Source[] sources) _DefaultOptionContext = _OptionBuilder.Build(null); _OptionBuilder.CheckObsolete(runspaceContext); + + Reader = _GetReader(); } internal void UpdateLanguageScope(ILanguageScope languageScope) @@ -248,6 +251,8 @@ private void Dispose(bool disposing) LocalizedDataCache.Clear(); ExpressionCache.Clear(); ContentCache.Clear(); + // Reader.Dispose(); + Writer.Dispose(); RunTime.Stop(); } _Disposed = true; diff --git a/src/PSRule/Pipeline/PipelineInputStream.cs b/src/PSRule/Pipeline/PipelineInputStream.cs index d6b094ea0b..d4ad5471d1 100644 --- a/src/PSRule/Pipeline/PipelineInputStream.cs +++ b/src/PSRule/Pipeline/PipelineInputStream.cs @@ -5,7 +5,7 @@ using System.Management.Automation; using PSRule.Configuration; using PSRule.Data; -using PSRule.Pipeline.Emitters; +using PSRule.Emitters; using PSRule.Runtime; namespace PSRule.Pipeline; diff --git a/src/PSRule/Pipeline/RulePipeline.cs b/src/PSRule/Pipeline/RulePipeline.cs index f1502b7ef2..39bc41569b 100644 --- a/src/PSRule/Pipeline/RulePipeline.cs +++ b/src/PSRule/Pipeline/RulePipeline.cs @@ -12,21 +12,16 @@ internal abstract class RulePipeline : IPipeline protected readonly PipelineContext Pipeline; internal readonly RunspaceContext Context; protected readonly Source[] Source; - protected readonly IPipelineReader Reader; - protected readonly IPipelineWriter Writer; // Track whether Dispose has been called. private bool _Disposed; - protected RulePipeline(PipelineContext pipelineContext, Source[] source, IPipelineReader reader, IPipelineWriter writer) + protected RulePipeline(PipelineContext pipelineContext, Source[] source) { - Result = new DefaultPipelineResult(writer, pipelineContext.Option.Execution.Break.GetValueOrDefault(ExecutionOption.Default.Break.Value)); + Result = new DefaultPipelineResult(pipelineContext.Writer, pipelineContext.Option.Execution.Break.GetValueOrDefault(ExecutionOption.Default.Break.Value)); Pipeline = pipelineContext; Context = new RunspaceContext(Pipeline); Source = source; - Reader = reader; - Writer = writer; - // Initialize contexts Context.Initialize(source); } @@ -42,9 +37,9 @@ protected RulePipeline(PipelineContext pipelineContext, Source[] source, IPipeli /// public virtual void Begin() { - Writer.Begin(); + Pipeline.Writer.Begin(); Context.Begin(); - Reader.Open(); + Pipeline.Reader.Open(); } /// @@ -56,7 +51,7 @@ public virtual void Process(PSObject sourceObject) /// public virtual void End() { - Writer.End(Result); + Pipeline.Writer.End(Result); } #endregion IPipeline @@ -75,7 +70,6 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - Writer.Dispose(); Context.Dispose(); Pipeline.Dispose(); } diff --git a/src/PSRule/Pipeline/SelectorTargetAnnotation.cs b/src/PSRule/Pipeline/SelectorTargetAnnotation.cs new file mode 100644 index 0000000000..d60dd4981c --- /dev/null +++ b/src/PSRule/Pipeline/SelectorTargetAnnotation.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Definitions.Selectors; + +namespace PSRule.Pipeline; + +internal sealed class SelectorTargetAnnotation : TargetObjectAnnotation +{ + private readonly Dictionary _Results; + + public SelectorTargetAnnotation() + { + _Results = new Dictionary(); + } + + public bool TryGetSelectorResult(SelectorVisitor selector, out bool result) + { + return _Results.TryGetValue(selector.InstanceId, out result); + } + + public void SetSelectorResult(SelectorVisitor selector, bool result) + { + _Results[selector.InstanceId] = result; + } +} diff --git a/src/PSRule/Pipeline/TargetObject.cs b/src/PSRule/Pipeline/TargetObject.cs index ebf2576dfa..5da6ac47c5 100644 --- a/src/PSRule/Pipeline/TargetObject.cs +++ b/src/PSRule/Pipeline/TargetObject.cs @@ -6,35 +6,9 @@ using System.Management.Automation; using Newtonsoft.Json.Linq; using PSRule.Data; -using PSRule.Definitions.Selectors; namespace PSRule.Pipeline; -internal abstract class TargetObjectAnnotation -{ - -} - -internal sealed class SelectorTargetAnnotation : TargetObjectAnnotation -{ - private readonly Dictionary _Results; - - public SelectorTargetAnnotation() - { - _Results = new Dictionary(); - } - - public bool TryGetSelectorResult(SelectorVisitor selector, out bool result) - { - return _Results.TryGetValue(selector.InstanceId, out result); - } - - public void SetSelectorResult(SelectorVisitor selector, bool result) - { - _Results[selector.InstanceId] = result; - } -} - /// /// An object processed by PSRule. /// diff --git a/src/PSRule/Pipeline/TargetObjectAnnotation.cs b/src/PSRule/Pipeline/TargetObjectAnnotation.cs new file mode 100644 index 0000000000..1a3de4cf99 --- /dev/null +++ b/src/PSRule/Pipeline/TargetObjectAnnotation.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Pipeline; + +internal abstract class TargetObjectAnnotation +{ + +} diff --git a/src/PSRule/Runtime/ILanguageScope.cs b/src/PSRule/Runtime/ILanguageScope.cs index 1ed4133359..af82266e4f 100644 --- a/src/PSRule/Runtime/ILanguageScope.cs +++ b/src/PSRule/Runtime/ILanguageScope.cs @@ -50,11 +50,22 @@ internal interface ILanguageScope : IDisposable /// void AddService(string name, object service); + /// + /// Configure a service in the scope. + /// + /// A delegate action to call to configure services. + void ConfigureServices(Action? configure); + /// /// Get a previously added service. /// object? GetService(string name); + /// + /// Get any emitters added to the scope. + /// + IEnumerable GetEmitters(); + ITargetBindingResult? Bind(TargetObject targetObject); ITargetBindingResult? Bind(object targetObject); diff --git a/src/PSRule/Runtime/LanguageScope.cs b/src/PSRule/Runtime/LanguageScope.cs index ff096a90ca..3d63561776 100644 --- a/src/PSRule/Runtime/LanguageScope.cs +++ b/src/PSRule/Runtime/LanguageScope.cs @@ -6,6 +6,7 @@ using PSRule.Data; using PSRule.Definitions; using PSRule.Definitions.Rules; +using PSRule.Emitters; using PSRule.Options; using PSRule.Pipeline; using PSRule.Runtime.Binding; @@ -15,11 +16,12 @@ namespace PSRule.Runtime; #nullable enable [DebuggerDisplay("{Name}")] -internal sealed class LanguageScope : ILanguageScope +internal sealed class LanguageScope : ILanguageScope, IRuntimeServiceCollection { private IDictionary? _Configuration; private WildcardMap? _Override; private readonly Dictionary _Service; + private readonly List _Emitters; private readonly Dictionary _Filter; private ITargetBinder? _TargetBinder; private StringComparer? _BindingComparer; @@ -32,7 +34,7 @@ public LanguageScope(string name) Name = ResourceHelper.NormalizeScope(name); _Filter = []; _Service = []; - _Configuration = new Dictionary(StringComparer.OrdinalIgnoreCase); + _Emitters = []; } /// @@ -43,13 +45,6 @@ public LanguageScope(string name) public StringComparer GetBindingComparer() => _BindingComparer ?? StringComparer.OrdinalIgnoreCase; - ///// - //public void Configure(Dictionary configuration) - //{ - // _Configuration ??= new Dictionary(StringComparer.OrdinalIgnoreCase); - // _Configuration.AddUnique(configuration); - //} - /// public void Configure(OptionContext context) { @@ -75,6 +70,7 @@ public void Configure(OptionContext context) var overrides = option.Level .Where(l => l.Value != SeverityLevel.None) .Select(l => new KeyValuePair(l.Key, new RuleOverride { Level = l.Value })); + return new WildcardMap(overrides); } @@ -89,8 +85,7 @@ public bool TryConfigurationValue(string key, out object? value) public bool TryGetOverride(ResourceId id, out RuleOverride? value) { value = default; - if (_Override == null) - return false; + if (_Override == null) return false; return _Override.TryGetValue(id.Value, out value) || _Override.TryGetValue(id.Name, out value); @@ -111,18 +106,37 @@ public void WithFilter(IResourceFilter resourceFilter) /// public void AddService(string name, object service) { - if (_Service.ContainsKey(name)) + if (string.IsNullOrEmpty(name) || service == null || _Service.ContainsKey(name)) return; _Service.Add(name, service); } + /// + /// Configure services to the scope. + /// + /// An delegate that configures zero or many services in the current scope. + public void ConfigureServices(Action? configure) + { + if (configure == null) + return; + + // Configure services + configure(this); + } + /// public object? GetService(string name) { return _Service.TryGetValue(name, out var service) ? service : null; } + /// + public IEnumerable GetEmitters() + { + return _Emitters; + } + public ITargetBindingResult? Bind(TargetObject targetObject) { return _TargetBinder?.Bind(targetObject); @@ -168,6 +182,8 @@ public IConfiguration ToConfiguration() return new InternalConfiguration(_Configuration ?? new Dictionary(StringComparer.OrdinalIgnoreCase)); } + #region IDisposable + private void Dispose(bool disposing) { if (!_Disposed) @@ -195,6 +211,26 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } + + #endregion IDisposable + + #region IRuntimeServiceCollection + + /// + string IRuntimeServiceCollection.ScopeName => Name; + + /// + IConfiguration IRuntimeServiceCollection.Configuration => ToConfiguration(); + + /// + void IRuntimeServiceCollection.AddService() + { + // Add any emitter. + if (typeof(TInterface) == typeof(IEmitter)) + _Emitters.Add(typeof(TService)); + } + + #endregion IRuntimeServiceCollection } #nullable restore diff --git a/src/PSRule/Runtime/LanguageScopeSetBuilder.cs b/src/PSRule/Runtime/LanguageScopeSetBuilder.cs index af28df3070..99941e2ef3 100644 --- a/src/PSRule/Runtime/LanguageScopeSetBuilder.cs +++ b/src/PSRule/Runtime/LanguageScopeSetBuilder.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Configuration; using PSRule.Definitions; using PSRule.Pipeline; @@ -24,13 +23,10 @@ public LanguageScopeSetBuilder() /// /// Perform initialization of the builder from the environment. /// - public void Init(PSRuleOption? option, Source[]? sources) + public void Init(Source[]? sources) { // Create all the module scopes from known sources. Import(sources); - - // Use options to configure the root scope. - Configure(option); } /// @@ -54,16 +50,6 @@ public ILanguageScopeSet Build() return new LanguageScopeSet(_Scopes); } - /// - /// Configure the root scope with options. - /// - /// - /// - private void Configure(PSRuleOption? option) - { - // Do nothing currently. - } - /// /// Import modules scopes from sources. /// diff --git a/src/PSRule/Runtime/PSRule.cs b/src/PSRule/Runtime/PSRule.cs index 2e4fd42692..c847950bfb 100644 --- a/src/PSRule/Runtime/PSRule.cs +++ b/src/PSRule/Runtime/PSRule.cs @@ -348,6 +348,22 @@ public void AddService(string id, object service) GetContext().AddService(id, service); } + /// + /// Configure services for dependency injection. + /// + /// + /// + /// Thrown when accessing this method outside of a convention initialize block. + /// + public void ConfigureServices(Action? configure) + { + if (configure == null) + return; + + RequireScope(RunspaceScope.ConventionInitialize); + GetContext()?.LanguageScope?.ConfigureServices(configure); + } + /// /// Retrieve a reusable singleton object from the PSRule runtime that has previously been stored with . /// diff --git a/src/PSRule/Runtime/RunspaceScope.cs b/src/PSRule/Runtime/RunspaceScope.cs index 08051d6ea3..77c6b3810b 100644 --- a/src/PSRule/Runtime/RunspaceScope.cs +++ b/src/PSRule/Runtime/RunspaceScope.cs @@ -54,11 +54,6 @@ public enum RunspaceScope /// ConventionInitialize = 128, - /// - /// Initialization. - /// - Initialize = ConventionInitialize, - /// /// When any convention block is executing. /// diff --git a/tests/PSRule.Tests/BaseTests.cs b/tests/PSRule.Tests/BaseTests.cs index d344586d2a..853984a225 100644 --- a/tests/PSRule.Tests/BaseTests.cs +++ b/tests/PSRule.Tests/BaseTests.cs @@ -5,8 +5,8 @@ using System.IO; using System.Management.Automation; using PSRule.Configuration; +using PSRule.Emitters; using PSRule.Pipeline; -using PSRule.Pipeline.Emitters; namespace PSRule; diff --git a/tests/PSRule.Tests/ContextBaseTests.cs b/tests/PSRule.Tests/ContextBaseTests.cs index ea1cbe1989..dbae2f9f0c 100644 --- a/tests/PSRule.Tests/ContextBaseTests.cs +++ b/tests/PSRule.Tests/ContextBaseTests.cs @@ -15,11 +15,11 @@ internal PipelineContext GetPipelineContext(PSRuleOption? option = default, IPip { option ??= GetOption(); writer ??= GetTestWriter(option); - languageScope ??= GetLanguageScopeSet(option, sources); + languageScope ??= GetLanguageScopeSet(sources); return PipelineContext.New( option: option, hostContext: null, - reader: null, + reader: () => new PipelineInputStream(null, null, null, null), writer: writer, languageScope: languageScope, optionBuilder: optionBuilder ?? new OptionContextBuilder(), @@ -27,9 +27,9 @@ internal PipelineContext GetPipelineContext(PSRuleOption? option = default, IPip ); } - internal OptionContextBuilder GetOptionBuilder() + internal OptionContextBuilder GetOptionBuilder(PSRuleOption? option = default) { - return new OptionContextBuilder(option: GetOption(), bindTargetName: PipelineHookActions.BindTargetName, bindTargetType: PipelineHookActions.BindTargetType, bindField: PipelineHookActions.BindField); + return new OptionContextBuilder(option: option ?? GetOption(), bindTargetName: PipelineHookActions.BindTargetName, bindTargetType: PipelineHookActions.BindTargetType, bindField: PipelineHookActions.BindField); } internal ResourceCache GetResourceCache(PSRuleOption? option = default, ILanguageScopeSet? languageScope = default, Source[]? sources = default, IPipelineWriter? writer = default) @@ -37,16 +37,25 @@ internal ResourceCache GetResourceCache(PSRuleOption? option = default, ILanguag return new ResourceCacheBuilder ( writer: writer ?? GetTestWriter(option), - languageScopeSet: languageScope ?? GetLanguageScopeSet(option, sources) + languageScopeSet: languageScope ?? GetLanguageScopeSet(sources) ).Import(sources).Build(unresolved: null); } - internal static ILanguageScopeSet GetLanguageScopeSet(PSRuleOption? option = default, Source[]? sources = default) + internal static ILanguageScopeSet GetLanguageScopeSet(Source[]? sources = default, OptionContextBuilder? optionContextBuilder = default) { var builder = new LanguageScopeSetBuilder(); - builder.Init(option, sources); - - return builder.Build(); + builder.Init(sources); + var languageScopeSet = builder.Build(); + + if (optionContextBuilder != null) + { + foreach (var scope in languageScopeSet.Get()) + { + scope.Configure(optionContextBuilder.Build(scope.Name)); + } + } + + return languageScopeSet; } } diff --git a/tests/PSRule.Tests/Emitters/CustomEmitter.cs b/tests/PSRule.Tests/Emitters/CustomEmitter.cs new file mode 100644 index 0000000000..eea8d994da --- /dev/null +++ b/tests/PSRule.Tests/Emitters/CustomEmitter.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace PSRule.Emitters; + +#nullable enable + +internal sealed class CustomEmitter : IEmitter +{ + public bool Accepts(IEmitterContext context, Type type) + { + return true; + } + + public void Dispose() + { + // Do nothing. Nothing to dispose. + } + + public bool Visit(IEmitterContext context, object o) + { + throw new NotImplementedException(); + } +} + +#nullable restore diff --git a/tests/PSRule.Tests/Emitters/EmitterBuilderTests.cs b/tests/PSRule.Tests/Emitters/EmitterBuilderTests.cs index ba6d0b1d5f..d648f1fdd1 100644 --- a/tests/PSRule.Tests/Emitters/EmitterBuilderTests.cs +++ b/tests/PSRule.Tests/Emitters/EmitterBuilderTests.cs @@ -3,7 +3,7 @@ using System.Linq; using PSRule.Definitions; -using PSRule.Pipeline.Emitters; +using PSRule.Runtime; namespace PSRule.Emitters; @@ -31,8 +31,10 @@ public void Build_WhenEmitterSupportsConfiguration_ShouldInjectConfigurationInst { var option = GetOption(); option.Format.Add("test", new Options.FormatType { Type = [".t"] }); + option.Configuration["custom_flag"] = true; + var optionContextBuilder = GetOptionBuilder(option); - var languageScopeSet = GetLanguageScopeSet(option: option); + var languageScopeSet = GetLanguageScopeSet(optionContextBuilder: optionContextBuilder); var builder = new EmitterBuilder(languageScopeSet, option.Format); builder.AddEmitter(ResourceHelper.STANDALONE_SCOPE_NAME); @@ -42,5 +44,28 @@ public void Build_WhenEmitterSupportsConfiguration_ShouldInjectConfigurationInst Assert.NotNull(actual); Assert.NotNull(actual.Configuration); Assert.Equal([".t"], actual.Configuration.GetFormatTypes("test")); + Assert.True(actual.Configuration.IsEnabled("custom_flag")); + Assert.False(actual.Configuration.IsEnabled("not_set")); + Assert.Equal("default", actual.Configuration.GetValueOrDefault("not_set", "default")); + } + + /// + /// Tests that any emitters that have been registered as services in the language scope are added to the collection. + /// + [Fact] + public void Build_WhenLanguageScopeIncludedEmitter_ShouldAddCustomEmitter() + { + var builder = new LanguageScopeSetBuilder(); + builder.CreateModuleScope("test"); + var languageScopeSet = builder.Build(); + if (languageScopeSet.TryScope("test", out var scope)) + { + scope.ConfigureServices(c => c.AddService()); + } + + var collection = new EmitterBuilder(languageScopeSet).Build(new TestEmitterContext()); + + Assert.NotNull(collection); + Assert.NotNull(collection.Emitters.FirstOrDefault(i => i is CustomEmitter)); } } diff --git a/tests/PSRule.Tests/Emitters/EmitterCollectionTests.cs b/tests/PSRule.Tests/Emitters/EmitterCollectionTests.cs index f95c1fbd25..d8d31aa701 100644 --- a/tests/PSRule.Tests/Emitters/EmitterCollectionTests.cs +++ b/tests/PSRule.Tests/Emitters/EmitterCollectionTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using PSRule.Definitions; -using PSRule.Pipeline.Emitters; namespace PSRule.Emitters; diff --git a/tests/PSRule.Tests/Emitters/EmitterTests.cs b/tests/PSRule.Tests/Emitters/EmitterTests.cs index 418b257267..777d46cae3 100644 --- a/tests/PSRule.Tests/Emitters/EmitterTests.cs +++ b/tests/PSRule.Tests/Emitters/EmitterTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using PSRule.Options; -using PSRule.Pipeline.Emitters; using PSRule.Runtime; namespace PSRule.Emitters; diff --git a/tests/PSRule.Tests/Emitters/JsonEmitterTests.cs b/tests/PSRule.Tests/Emitters/JsonEmitterTests.cs index 8a1ac601f9..4901888350 100644 --- a/tests/PSRule.Tests/Emitters/JsonEmitterTests.cs +++ b/tests/PSRule.Tests/Emitters/JsonEmitterTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Pipeline.Emitters; using PSRule.Runtime; namespace PSRule.Emitters; diff --git a/tests/PSRule.Tests/Emitters/MarkdownEmitterTests.cs b/tests/PSRule.Tests/Emitters/MarkdownEmitterTests.cs index 2fca8c0ef9..45d7881be4 100644 --- a/tests/PSRule.Tests/Emitters/MarkdownEmitterTests.cs +++ b/tests/PSRule.Tests/Emitters/MarkdownEmitterTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Pipeline.Emitters; - namespace PSRule.Emitters; /// diff --git a/tests/PSRule.Tests/Emitters/PowerShellDataTests.cs b/tests/PSRule.Tests/Emitters/PowerShellDataTests.cs index 60deb41ee1..838c2456d7 100644 --- a/tests/PSRule.Tests/Emitters/PowerShellDataTests.cs +++ b/tests/PSRule.Tests/Emitters/PowerShellDataTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Pipeline.Emitters; - namespace PSRule.Emitters; /// diff --git a/tests/PSRule.Tests/Emitters/TestEmitter.cs b/tests/PSRule.Tests/Emitters/TestEmitter.cs index d9fdc01e34..dbe6a9bc54 100644 --- a/tests/PSRule.Tests/Emitters/TestEmitter.cs +++ b/tests/PSRule.Tests/Emitters/TestEmitter.cs @@ -9,17 +9,19 @@ namespace PSRule.Emitters; +#nullable enable + /// /// An emitter for testing. /// public sealed class TestEmitter : FileEmitter { - private readonly ImmutableHashSet _Types; - private readonly Func _VisitFile; + private readonly ImmutableHashSet? _Types; + private readonly Func? _VisitFile; - public TestEmitter(ILogger logger, IEmitterConfiguration emitterConfiguration) + public TestEmitter(ILogger logger, IEmitterConfiguration? emitterConfiguration) { - Logger = logger; + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); Configuration = emitterConfiguration; } @@ -29,9 +31,9 @@ internal TestEmitter(string[] types, Func vi _VisitFile = visitFile; } - public ILogger Logger { get; } + public ILogger? Logger { get; } - public IEmitterConfiguration Configuration { get; } + public IEmitterConfiguration? Configuration { get; } protected override bool AcceptsFilePath(IEmitterContext context, IFileInfo info) { @@ -43,3 +45,5 @@ protected override bool VisitFile(IEmitterContext context, IFileStream stream) return _VisitFile != null && _VisitFile(context, stream); } } + +#nullable restore diff --git a/tests/PSRule.Tests/Emitters/YamlEmitterTests.cs b/tests/PSRule.Tests/Emitters/YamlEmitterTests.cs index 6cec42e99f..7ec324e6ae 100644 --- a/tests/PSRule.Tests/Emitters/YamlEmitterTests.cs +++ b/tests/PSRule.Tests/Emitters/YamlEmitterTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Pipeline.Emitters; using PSRule.Runtime; namespace PSRule.Emitters; diff --git a/tests/PSRule.Tests/MockLanguageScope.cs b/tests/PSRule.Tests/MockLanguageScope.cs index ae8635a30e..e5bf7f5158 100644 --- a/tests/PSRule.Tests/MockLanguageScope.cs +++ b/tests/PSRule.Tests/MockLanguageScope.cs @@ -47,6 +47,11 @@ public void Configure(OptionContext context) throw new NotImplementedException(); } + public void ConfigureServices(Action configure) + { + throw new NotImplementedException(); + } + public void Dispose() { @@ -57,6 +62,11 @@ public StringComparer GetBindingComparer() throw new NotImplementedException(); } + public IEnumerable GetEmitters() + { + throw new NotImplementedException(); + } + public IResourceFilter GetFilter(ResourceKind kind) { throw new NotImplementedException(); diff --git a/tests/PSRule.Tests/Pipeline/PipelineTests.cs b/tests/PSRule.Tests/Pipeline/PipelineTests.cs index a9be1aed20..c7acab7862 100644 --- a/tests/PSRule.Tests/Pipeline/PipelineTests.cs +++ b/tests/PSRule.Tests/Pipeline/PipelineTests.cs @@ -164,10 +164,10 @@ public void PipelineWithInvariantCulture() { var option = GetOption(); var sources = GetSource(); - var writer = GetTestWriter(option); Environment.UseCurrentCulture(CultureInfo.InvariantCulture); + var writer = GetTestWriter(option); var context = GetPipelineContext(option: option, sources: sources, writer: writer); - var pipeline = new GetRulePipeline(context, sources, new PipelineInputStream(null, null, null, null), writer, false); + var pipeline = new GetRulePipeline(context, sources, false); try { pipeline.Begin(); @@ -187,9 +187,9 @@ public void PipelineWithInvariantCultureDisabled() Environment.UseCurrentCulture(CultureInfo.InvariantCulture); var option = GetOption(); option.Execution.InvariantCulture = ExecutionActionPreference.Ignore; - var context = GetPipelineContext(option: option); var writer = GetTestWriter(option); - var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null, null), writer, false); + var context = GetPipelineContext(option: option, writer: writer); + var pipeline = new GetRulePipeline(context, GetSource(), false); try { pipeline.Begin(); diff --git a/tests/PSRule.Tests/TestEmitterContext.cs b/tests/PSRule.Tests/TestEmitterContext.cs index 855045d2cc..1182e44713 100644 --- a/tests/PSRule.Tests/TestEmitterContext.cs +++ b/tests/PSRule.Tests/TestEmitterContext.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using PSRule.Data; +using PSRule.Emitters; using PSRule.Options; -using PSRule.Pipeline.Emitters; namespace PSRule;