Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes configuration array as dictionary #779 #780

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/CHANGELOG-v1.md
Expand Up @@ -10,6 +10,11 @@ See [upgrade notes][upgrade-notes] for helpful information when upgrading from p

## Unreleased

What's changed since pre-release v1.7.0-B2108016:

- Bug fixes:
- Fixed configuration array deserializes as dictionary from YAML options. [#779](https://github.com/microsoft/PSRule/issues/779)

## v1.7.0-B2108016 (pre-release)

What's changed since v1.6.0:
Expand Down
21 changes: 16 additions & 5 deletions src/PSRule/Common/YamlConverters.cs
Expand Up @@ -145,16 +145,15 @@ public bool Accepts(Type type)
public object ReadYaml(IParser parser, Type type)
{
// Handle empty objects
if (parser.TryConsume<Scalar>(out _))
if (parser.TryConsume(out Scalar scalar))
{
parser.TryConsume<Scalar>(out _);
return null;
return PSObject.AsPSObject(scalar.Value);
}

var result = new PSObject();
if (parser.TryConsume<MappingStart>(out _))
{
while (parser.TryConsume(out Scalar scalar))
while (parser.TryConsume(out scalar))
{
var name = scalar.Value;
var property = ReadNoteProperty(parser, name);
Expand Down Expand Up @@ -218,7 +217,6 @@ public bool Resolve(NodeEvent nodeEvent, ref Type currentType)
currentType = typeof(PSObject[]);
return true;
}

else if (currentType == typeof(Dictionary<object, object>) || nodeEvent is MappingStart)
{
currentType = typeof(PSObject);
Expand All @@ -228,6 +226,19 @@ public bool Resolve(NodeEvent nodeEvent, ref Type currentType)
}
}

internal sealed class PSOptionYamlTypeResolver : INodeTypeResolver
{
public bool Resolve(NodeEvent nodeEvent, ref Type currentType)
{
if (currentType == typeof(object) && nodeEvent is SequenceStart)
{
currentType = typeof(PSObject[]);
return true;
}
return false;
}
}

/// <summary>
/// A YAML type inspector to read fields and properties from a type for serialization.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/PSRule/Configuration/PSRuleOption.cs
Expand Up @@ -261,6 +261,8 @@ private static PSRuleOption FromYaml(string path, string yaml)
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeConverter(new FieldMapYamlTypeConverter())
.WithTypeConverter(new SuppressionRuleYamlTypeConverter())
.WithTypeConverter(new PSObjectYamlTypeConverter())
.WithNodeTypeResolver(new PSOptionYamlTypeResolver())
.Build();

var option = d.Deserialize<PSRuleOption>(yaml) ?? new PSRuleOption();
Expand Down
2 changes: 2 additions & 0 deletions src/PSRule/Host/HostHelper.cs
Expand Up @@ -222,6 +222,8 @@ private static ILanguageBlock[] ReadYamlObjects(Source[] sources, RunspaceContex
.IgnoreUnmatchedProperties()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeConverter(new FieldMapYamlTypeConverter())
.WithTypeConverter(new PSObjectYamlTypeConverter())
.WithNodeTypeResolver(new PSOptionYamlTypeResolver())
.WithNodeDeserializer(
inner => new LanguageBlockDeserializer(new LanguageExpressionDeserializer(inner)),
s => s.InsteadOf<ObjectNodeDeserializer>())
Expand Down
3 changes: 3 additions & 0 deletions tests/PSRule.Tests/Baseline.Rule.yaml
Expand Up @@ -29,6 +29,9 @@ spec:
- 'WithBaseline'
configuration:
key1: value1
key2:
- value1: abc
- value2: def

---
# Synopsis: This is an example baseline
Expand Down
10 changes: 10 additions & 0 deletions tests/PSRule.Tests/BaselineTests.cs
Expand Up @@ -8,6 +8,7 @@
using System;
using System.IO;
using System.Linq;
using System.Management.Automation;
using Xunit;
using Assert = Xunit.Assert;

Expand All @@ -29,6 +30,15 @@ public void ReadBaseline()
Assert.Equal("value", baseline[0].Metadata.Annotations["key"]);
Assert.False(baseline[0].Obsolete);
Assert.False(baseline[0].GetApiVersionIssue());

var config = baseline[0].Spec.Configuration["key2"] as Array;
Assert.NotNull(config);
Assert.Equal(2, config.Length);
Assert.IsType<PSObject>(config.GetValue(0));
var pso = config.GetValue(0) as PSObject;
Assert.Equal("abc", pso.PropertyValue<string>("value1"));
pso = config.GetValue(1) as PSObject;
Assert.Equal("def", pso.PropertyValue<string>("value2"));

// TestBaseline5
Assert.Equal("TestBaseline5", baseline[4].Name);
Expand Down
37 changes: 37 additions & 0 deletions tests/PSRule.Tests/ConfigurationTests.cs
Expand Up @@ -5,6 +5,7 @@
using PSRule.Pipeline;
using System;
using System.IO;
using System.Management.Automation;
using Xunit;

namespace PSRule
Expand Down Expand Up @@ -41,6 +42,34 @@ public void GetStringValues()
Assert.Equal(new string[] { "123", "456" }, configuration.GetStringValues("key3"));
}

[Fact]
public void GetStringValuesFromYaml()
{
var option = GetOption();
var actual = option.Configuration["option5"] as Array;
Assert.NotNull(actual);
Assert.Equal(2, actual.Length);
Assert.IsType<PSObject>(actual.GetValue(0));
var pso = actual.GetValue(0) as PSObject;
Assert.Equal("option5a", pso.BaseObject);

BuildPipeline(option);
var configuration = GetConfigurationHelper();
Assert.Equal(new string[] { "option5a", "option5b" }, configuration.GetStringValues("option5"));
}

[Fact]
public void GetObjectArrayFromYaml()
{
var option = GetOption();
var actual = option.Configuration["option4"] as Array;
Assert.NotNull(actual);
Assert.Equal(2, actual.Length);
Assert.IsType<PSObject>(actual.GetValue(0));
var pso = actual.GetValue(0) as PSObject;
Assert.Equal("East US", pso.PropertyValue<string>("location"));
}

private static void BuildPipeline(PSRuleOption option)
{
var builder = PipelineBuilder.Invoke(GetSource(), option, null, null);
Expand All @@ -59,6 +88,14 @@ private static Source[] GetSource()
return builder.Build();
}

private static PSRuleOption GetOption()
{
var loaded = PSRuleOption.FromFile(GetSourcePath("PSRule.Tests.yml"));
var option = new PSRuleOption();
option.Configuration = loaded.Configuration;
return option;
}

private static string GetSourcePath(string fileName)
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
Expand Down
4 changes: 4 additions & 0 deletions tests/PSRule.Tests/PSRule.Options.Tests.ps1
Expand Up @@ -193,6 +193,10 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' {
$option.Configuration.option1 | Should -Be 'option';
$option.Configuration.option2 | Should -Be 2;
$option.Configuration.option3 | Should -BeIn 'option3a', 'option3b';
$option.Configuration.option4.Length | Should -Be 2;
$option.Configuration.option4[0].location | Should -Be 'East US';
$option.Configuration.option4[0].zones | Should -BeIn '1', '2', '3';
$option.Configuration.option5 | Should -BeIn 'option5a', 'option5b';
}

It 'from Environment' {
Expand Down
8 changes: 8 additions & 0 deletions tests/PSRule.Tests/PSRule.Tests.yml
Expand Up @@ -14,6 +14,14 @@ configuration:
option1: option
option2: 2
option3: [ 'option3a', 'option3b' ]
option4:
- location: 'East US'
zones: [ "1", "2", "3" ]
- location: 'Australia South East'
zones: [ ]
option5:
- option5a
- option5b

# Configure conventions
convention:
Expand Down