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

Extending Config class to support passing JsonSerializerOptions #373

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/Improvements-370.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
component: sdk
kind: Improvements
body: Extending Config class to support passing JsonSerializerOptions
time: 2024-11-08T21:31:30+01:00
custom:
PR: "370"
125 changes: 125 additions & 0 deletions sdk/Pulumi.Tests/ConfigTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;

namespace Pulumi.Tests
{
public class ConfigTests
{
private Config target = new Config("cfg");
private Mock<IDeploymentInternal> deploymentInternal = new Mock<IDeploymentInternal>();
private const string PascalCaseJson = "{ \"Value\":\"Value\" }";
private const string CamelCaseJson = "{ \"value\":\"Value\" }";
private static readonly JsonSerializerOptions CamelCaseSerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

public ConfigTests()
{
var runner = new Mock<IRunner>();
deploymentInternal.Setup(x => x.Runner).Returns(runner.Object);
Deployment.Instance = new DeploymentInstance(deploymentInternal.Object);
}


private class TestConfig
{
public string? Value { get; set; }
}

[Fact]
public void GetObjectReturnsObject()
{
deploymentInternal.Setup(x => x.GetConfig("cfg:test")).Returns(PascalCaseJson);

var result = target.GetObject<TestConfig>("test");

Assert.NotNull(result);
Assert.Equal("Value", result?.Value);
}

[Fact]
public void GetObjectReturnsObjectWithCustomJsonSerializerOptions()
{
deploymentInternal.Setup(x => x.GetConfig("cfg:test")).Returns(CamelCaseJson);

var result = target.GetObject<TestConfig>("test", CamelCaseSerializerOptions);

Assert.NotNull(result);
Assert.Equal("Value", result?.Value);
}

[Fact]
public async Task GetSecretObjectReturnsObject()
{
deploymentInternal.Setup(x => x.GetConfig("cfg:test")).Returns(PascalCaseJson);

var output = target.GetSecretObject<TestConfig>("test");
var result = await output.GetValueAsync(new TestConfig());

Assert.Equal("Value", result.Value);
}

[Fact]
public async Task GetSecretObjectReturnsObjectWithCustomJsonSerializerOptions()
{
deploymentInternal.Setup(x => x.GetConfig("cfg:test")).Returns(CamelCaseJson);

var output = target.GetSecretObject<TestConfig>("test", CamelCaseSerializerOptions);
var result = await output.GetValueAsync(new TestConfig());

Assert.Equal("Value", result.Value);
}

[Fact]
public void RequireObjectReturnsObject()
{
deploymentInternal.Setup(x => x.GetConfig("cfg:test")).Returns(PascalCaseJson);

var result = target.RequireObject<TestConfig>("test");

Assert.NotNull(result);
Assert.Equal("Value", result?.Value);
}

[Fact]
public void RequireObjectReturnsObjectWithCustomJsonSerializerOptions()
{
deploymentInternal.Setup(x => x.GetConfig("cfg:test")).Returns(CamelCaseJson);

var result = target.RequireObject<TestConfig>("test", CamelCaseSerializerOptions);

Assert.NotNull(result);
Assert.Equal("Value", result?.Value);
}

[Fact]
public async Task RequireSecretObjectReturnsObject()
{
deploymentInternal.Setup(x => x.GetConfig("cfg:test")).Returns(PascalCaseJson);

var output = target.RequireSecretObject<TestConfig>("test");
var result = await output.GetValueAsync(new TestConfig());

Assert.Equal("Value", result.Value);
}

[Fact]
public async Task RequireSecretObjectReturnsObjectWithCustomJsonSerializerOptions()
{
deploymentInternal.Setup(x => x.GetConfig("cfg:test")).Returns(CamelCaseJson);

var output = target.RequireSecretObject<TestConfig>("test", CamelCaseSerializerOptions);
var result = await output.GetValueAsync(new TestConfig());

Assert.Equal("Value", result.Value);
}
}
}
49 changes: 45 additions & 4 deletions sdk/Pulumi/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,28 @@ private static Output<T> MakeStructSecret<T>(T value) where T : struct
=> MakeStructSecret(GetDoubleImpl(key));

[return: MaybeNull]
private T GetObjectImpl<T>(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
private T GetObjectImpl<T>(string key, string? use = null, [CallerMemberName] string? insteadOf = null, JsonSerializerOptions? options = null)
{
var v = GetImpl(key, use, insteadOf);
try
{
return v == null ? default : JsonSerializer.Deserialize<T>(v);
return v == null ? default : JsonSerializer.Deserialize<T>(v, options);
}
catch (JsonException ex)
{
throw new ConfigTypeException(FullKey(key), v, typeof(T).FullName!, ex);
}
}

/// <summary>
/// Loads an optional configuration value, as an object, by its key, or null if it doesn't
/// exist. This works by taking the value associated with <paramref name="key"/> and passing
/// it to <see cref="JsonSerializer.Deserialize{TValue}(string, JsonSerializerOptions)"/>.
/// </summary>
[return: MaybeNull]
public T GetObject<T>(string key, JsonSerializerOptions? options)
=> GetObjectImpl<T>(key, nameof(GetSecretObject), options: options);

/// <summary>
/// Loads an optional configuration value, as an object, by its key, or null if it doesn't
/// exist. This works by taking the value associated with <paramref name="key"/> and passing
Expand All @@ -177,6 +186,20 @@ private T GetObjectImpl<T>(string key, string? use = null, [CallerMemberName] st
public T GetObject<T>(string key)
=> GetObjectImpl<T>(key, nameof(GetSecretObject));

/// <summary>
/// Loads an optional configuration value, as an object, by its key, marking it as a secret
/// or null if it doesn't exist. This works by taking the value associated with <paramref
/// name="key"/> and passing it to <see cref="JsonSerializer.Deserialize{TValue}(string, JsonSerializerOptions)"/>.
/// </summary>
public Output<T>? GetSecretObject<T>(string key, JsonSerializerOptions options)
{
var v = GetImpl(key);
if (v == null)
return null;

return Output.CreateSecret(GetObjectImpl<T>(key, options: options)!);
}

/// <summary>
/// Loads an optional configuration value, as an object, by its key, marking it as a secret
/// or null if it doesn't exist. This works by taking the value associated with <paramref
Expand Down Expand Up @@ -258,15 +281,24 @@ public double RequireDouble(string key)
public Output<double> RequireSecretDouble(string key)
=> MakeStructSecret(RequireDoubleImpl(key));

private T RequireObjectImpl<T>(string key, string? use = null, [CallerMemberName] string? insteadOf = null)
private T RequireObjectImpl<T>(string key, string? use = null, [CallerMemberName] string? insteadOf = null, JsonSerializerOptions? options = null)
{
var v = GetImpl(key);
if (v == null)
throw new ConfigMissingException(FullKey(key));

return GetObjectImpl<T>(key, use, insteadOf)!;
return GetObjectImpl<T>(key, use, insteadOf, options)!;
}

/// <summary>
/// Loads a configuration value as a JSON string and deserializes the JSON into an object.
/// object. If it doesn't exist, or the configuration value cannot be converted using <see
/// cref="JsonSerializer.Deserialize{TValue}(string, JsonSerializerOptions)"/>, an error is
/// thrown.
/// </summary>
public T RequireObject<T>(string key, JsonSerializerOptions options)
=> RequireObjectImpl<T>(key, nameof(RequireSecretObject), options: options);

/// <summary>
/// Loads a configuration value as a JSON string and deserializes the JSON into an object.
/// object. If it doesn't exist, or the configuration value cannot be converted using <see
Expand All @@ -276,6 +308,15 @@ private T RequireObjectImpl<T>(string key, string? use = null, [CallerMemberName
public T RequireObject<T>(string key)
=> RequireObjectImpl<T>(key, nameof(RequireSecretObject));

/// <summary>
/// Loads a configuration value as a JSON string and deserializes the JSON into a JavaScript
/// object, marking it as a secret. If it doesn't exist, or the configuration value cannot
/// be converted using <see cref="JsonSerializer.Deserialize{TValue}(string, JsonSerializerOptions)"/>,
/// an error is thrown.
/// </summary>
public Output<T> RequireSecretObject<T>(string key, JsonSerializerOptions options)
=> Output.CreateSecret(RequireObjectImpl<T>(key, options: options));

/// <summary>
/// Loads a configuration value as a JSON string and deserializes the JSON into a JavaScript
/// object, marking it as a secret. If it doesn't exist, or the configuration value cannot
Expand Down