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;