Skip to content

Commit

Permalink
[dotnet/auto] allow deserializing complex stack config values
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaid-Ajaj committed Oct 25, 2022
1 parent 9922d06 commit 365a2e6
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,47 @@ public void CanDeserializeSecureString()
}

[Fact]
public void CannotDeserializeObject()
public void DeserializeObjectWorks()
{
const string json = @"
{
""config"": {
""value"": {
""test"": ""test"",
""nested"": {
""one"": 1,
""two"": true,
""three"": ""three""
}
""hello"": ""world""
}
}
}
";

Assert.Throws<NotSupportedException>(
() => _serializer.DeserializeJson<StackSettings>(json));
var settings = _serializer.DeserializeJson<StackSettings>(json);
Assert.NotNull(settings.Config);
Assert.True(settings!.Config!.ContainsKey("value"));

var value = settings.Config["value"];
Assert.NotNull(value);
Assert.Equal("{\"hello\":\"world\"}", value.Value);
Assert.False(value.IsSecure);
}

[Fact]
public void DeserializeArrayWorks()
{
const string json = @"
{
""config"": {
""value"": [1,2,3,4,5]
}
}
";

var settings = _serializer.DeserializeJson<StackSettings>(json);
Assert.NotNull(settings.Config);
Assert.True(settings!.Config!.ContainsKey("value"));

var value = settings.Config["value"];
Assert.NotNull(value);
Assert.Equal("[1,2,3,4,5]", value.Value);
Assert.False(value.IsSecure);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,22 @@ public void CanDeserializeSecureString()
}

[Fact]
public void CannotDeserializeObject()
public void CanDeserializeObject()
{
const string yaml = @"
config:
value:
test: test
nested:
one: 1
two: true
three: three
hello: world
";

Assert.Throws<YamlException>(
() => _serializer.DeserializeYaml<StackSettings>(yaml));
var settings = _serializer.DeserializeYaml<StackSettings>(yaml);
Assert.NotNull(settings.Config);
Assert.True(settings!.Config!.ContainsKey("value"));

var value = settings.Config["value"];
Assert.NotNull(value);
Assert.Equal("{\"hello\":\"world\"}", value.Value);
Assert.False(value.IsSecure);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,28 @@ public override StackSettingsConfigValue Read(ref Utf8JsonReader reader, Type ty
return new StackSettingsConfigValue(value, false);
}

// confirm object
if (element.ValueKind != JsonValueKind.Object)
throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}]. Expecting object if not plain string.");

// check if secure string
var securePropertyName = options.PropertyNamingPolicy?.ConvertName("Secure") ?? "Secure";
if (element.TryGetProperty(securePropertyName, out var secureProperty))
// check if the element is an object,
// if it has a single property called "secure" then it is a secret value
// otherwise, serialize the whole object as JSON into the stack settings value
if (element.ValueKind == JsonValueKind.Object)
{
if (secureProperty.ValueKind != JsonValueKind.String)
throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}] as a secure string. Expecting a string secret.");

var secret = secureProperty.GetString();
return new StackSettingsConfigValue(secret, true);
foreach(var property in element.EnumerateObject())
{
if (string.Equals("Secure", property.Name, StringComparison.OrdinalIgnoreCase))
{
var secureValue = property.Value;
if (secureValue.ValueKind != JsonValueKind.String)
{
throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}] as a secure string. Expecting a string secret.");
}

return new StackSettingsConfigValue(secureValue.GetString(), true);
}
}
}

throw new NotSupportedException("Automation API does not currently support deserializing complex objects from stack settings.");
var serializedElement = JsonSerializer.Serialize(element);
return new StackSettingsConfigValue(serializedElement, false);
}

public override void Write(Utf8JsonWriter writer, StackSettingsConfigValue value, JsonSerializerOptions options)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2016-2021, Pulumi Corporation

using System;
using System.Collections.Generic;
using System.Text.Json;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
Expand All @@ -22,27 +24,44 @@ public object ReadYaml(IParser parser, Type type)
return new StackSettingsConfigValue(stringValue.Value, false);
}

// confirm it is an object
if (!parser.TryConsume<MappingStart>(out _))
throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting object if not plain string.");
var deserializer = new Deserializer();

// get first property name
if (!parser.TryConsume<Scalar>(out var firstPropertyName))
throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting first property name inside object.");

// check if secure string
if (string.Equals("Secure", firstPropertyName.Value, StringComparison.OrdinalIgnoreCase))
// check whether it is an object with a single property called "secure"
// this means we have a secret value serialized into the value
if (parser.TryConsume<MappingStart>(out var _))
{
// secure string
if (!parser.TryConsume<Scalar>(out var securePropertyValue))
throw new YamlException($"Unable to deserialize [{type.FullName}] as a secure string. Expecting a string secret.");
var dictionaryFromYaml = new Dictionary<string, object?>();

// in which case, check whether it is a secure value
while (parser.TryConsume<Scalar>(out var firstPropertyName))
{
if (string.Equals("Secure", firstPropertyName.Value, StringComparison.OrdinalIgnoreCase))
{
// secure string
if (!parser.TryConsume<Scalar>(out var securePropertyValue))
throw new YamlException($"Unable to deserialize [{type.FullName}] as a secure string. Expecting a string secret.");

// needs to be 1 mapping end and then return
parser.Require<MappingEnd>();
parser.MoveNext();
return new StackSettingsConfigValue(securePropertyValue.Value, true);
}

// not a secure string, so we need to add first value to the dictionary
dictionaryFromYaml.Add(firstPropertyName.Value, deserializer.Deserialize<object?>(parser));
}

// needs to be 1 mapping end and then return
parser.Require<MappingEnd>();
parser.MoveNext();
return new StackSettingsConfigValue(securePropertyValue.Value, true);
// serialize the dictionary back into the value as JSON
var serializedDictionary = JsonSerializer.Serialize(dictionaryFromYaml);
return new StackSettingsConfigValue(serializedDictionary, false);
}
throw new NotSupportedException("Automation API does not currently support deserializing complex objects from stack settings.");

// for anything else, i.e. arrays, parse the contents as is and serialize it a JSON string
var deserializedFromYaml = deserializer.Deserialize<object?>(parser);
var serializedToJson = JsonSerializer.Serialize(deserializedFromYaml);
return new StackSettingsConfigValue(serializedToJson, false);
}

public void WriteYaml(IEmitter emitter, object? value, Type type)
Expand Down

0 comments on commit 365a2e6

Please sign in to comment.