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

[POC] Denote the interdependency between parameters in a parameter set #571

Merged
merged 11 commits into from
Jun 12, 2024
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## Next Release

- Enforce one-or-other for `Shipment` and `Batch` parameters in `Pickup.Create` parameter set
- Add internal parameter dependency utility

## v6.5.2 (2024-06-12)

- Fix `Shipment` parameter requirement for `Pickup.Create` parameter set
Expand Down
4 changes: 4 additions & 0 deletions EasyPost.Tests/ExceptionsTests/ExceptionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public void TestExceptionConstructors()
{
const string testMessage = "This is a test message.";
const string testPropertyName = "test_property";
const string testPropertyName2 = "test_property2";
Type testType = typeof(ExceptionsTests);

// Test the base EasyPostError constructor
Expand Down Expand Up @@ -170,6 +171,9 @@ public void TestExceptionConstructors()
InvalidParameterError invalidParameterError = new(testPropertyName);
Assert.Equal($"{string.Format(CultureInfo.InvariantCulture, Constants.ErrorMessages.InvalidParameter, testPropertyName)}. ", invalidParameterError.Message);

InvalidParameterPairError invalidParameterPairError = new(testPropertyName, testPropertyName2);
Assert.Equal($"{string.Format(CultureInfo.InvariantCulture, Constants.ErrorMessages.InvalidParameterPair, testPropertyName, testPropertyName2)}. ", invalidParameterPairError.Message);

JsonDeserializationError jsonDeserializationError = new(testType);
Assert.Equal(string.Format(CultureInfo.InvariantCulture, Constants.ErrorMessages.JsonDeserializationError, testType.FullName), jsonDeserializationError.Message);

Expand Down
229 changes: 208 additions & 21 deletions EasyPost.Tests/ParametersTests/ParametersTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,37 +225,157 @@ public void TestRequiredAndOptionalParameterValidation()
Assert.Throws<Exceptions.General.MissingParameterError>(() => parametersWithOnlyOptionalParameterSet.ToDictionary());
}

private sealed class ParameterSetWithRequiredAndOptionalParameters : Parameters.BaseParameters<EasyPostObject>
[Fact]
[Testing.Exception]
public void TestOneOrOtherDependentTopLevelParameters()
{
[TopLevelRequestParameter(Necessity.Required, "test", "required")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? RequiredParameter { get; set; }
// Either A or B must be set, but not both.

// Should throw exception if both set.
var parametersWithOneOrOtherInterdependenceBothSet = new ParameterSetWithOneOrOtherDependentTopLevelParameters
{
AParam = "A",
BParam = "B",
};

Assert.Throws<Exceptions.General.InvalidParameterPairError>(() => parametersWithOneOrOtherInterdependenceBothSet.ToDictionary());

// Should throw exception if neither set.
var parametersWithOneOrOtherInterdependenceNeitherSet = new ParameterSetWithOneOrOtherDependentTopLevelParameters();

Assert.Throws<Exceptions.General.InvalidParameterPairError>(() => parametersWithOneOrOtherInterdependenceNeitherSet.ToDictionary());

// Should not throw exception if only A is set.
var parametersWithOneOrOtherInterdependenceOnlyASet = new ParameterSetWithOneOrOtherDependentTopLevelParameters
{
AParam = "A",
};

try
{
parametersWithOneOrOtherInterdependenceOnlyASet.ToDictionary();
}
catch (Exceptions.General.InvalidParameterPairError)
{
Assert.Fail("Should not throw exception if only A is set.");
}

// Should not throw exception if only B is set.
var parametersWithOneOrOtherInterdependenceOnlyBSet = new ParameterSetWithOneOrOtherDependentTopLevelParameters
{
BParam = "B",
};

[TopLevelRequestParameter(Necessity.Optional, "test", "optional")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? OptionalParameter { get; set; }
try
{
parametersWithOneOrOtherInterdependenceOnlyBSet.ToDictionary();
}
catch (Exceptions.General.InvalidParameterPairError)
{
Assert.Fail("Should not throw exception if only B is set.");
}
}

private sealed class ParameterSetWithCompetingParameters : Parameters.BaseParameters<EasyPostObject>
[Fact]
[Testing.Exception]
public void TestBothOrNeitherDependentTopLevelParameters()
{
[TopLevelRequestParameter(Necessity.Optional, "location")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? AParam { get; set; }
// Either both A and B must be set, or neither.

// Should throw exception if only A is set.
var parametersWithBothOrNeitherInterdependenceOnlyASet = new ParameterSetWithBothOrNeitherDependentTopLevelParameters
{
AParam = "A",
};

Assert.Throws<Exceptions.General.InvalidParameterPairError>(() => parametersWithBothOrNeitherInterdependenceOnlyASet.ToDictionary());

[TopLevelRequestParameter(Necessity.Optional, "location")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? BParam { get; set; }
// Should throw exception if only B is set.
var parametersWithBothOrNeitherInterdependenceOnlyBSet = new ParameterSetWithBothOrNeitherDependentTopLevelParameters
{
BParam = "B",
};

Assert.Throws<Exceptions.General.InvalidParameterPairError>(() => parametersWithBothOrNeitherInterdependenceOnlyBSet.ToDictionary());

// Should not throw exception if both A and B are set.
var parametersWithBothOrNeitherInterdependenceBothSet = new ParameterSetWithBothOrNeitherDependentTopLevelParameters
{
AParam = "A",
BParam = "B",
};

try
{
parametersWithBothOrNeitherInterdependenceBothSet.ToDictionary();
}
catch (Exceptions.General.InvalidParameterPairError)
{
Assert.Fail("Should not throw exception if both A and B are set.");
}

// Should not throw exception if neither A nor B are set.
var parametersWithBothOrNeitherInterdependenceNeitherSet = new ParameterSetWithBothOrNeitherDependentTopLevelParameters();

try
{
parametersWithBothOrNeitherInterdependenceNeitherSet.ToDictionary();
}
catch (Exceptions.General.InvalidParameterPairError)
{
Assert.Fail("Should not throw exception if neither A nor B are set.");
}
}

private sealed class ParameterSetWithCompetingParametersNonAlphabetic : Parameters.BaseParameters<EasyPostObject>
[Fact]
[Testing.Exception]
public void TestDependentNestedParameters()
{
[TopLevelRequestParameter(Necessity.Optional, "location")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? BParam { get; set; }
// Either A or B must be set, but not both.

// Should throw exception if both set.
var parametersWithInterdependenceBothSet = new ParameterSetWithOneOrOtherDependentTopLevelParameters
{
AParam = "A",
BParam = "B",
};

Assert.Throws<Exceptions.General.InvalidParameterPairError>(() => parametersWithInterdependenceBothSet.ToDictionary());

// Should throw exception if neither set.
var parametersWithInterdependenceNeitherSet = new ParameterSetWithOneOrOtherDependentTopLevelParameters();

Assert.Throws<Exceptions.General.InvalidParameterPairError>(() => parametersWithInterdependenceNeitherSet.ToDictionary());

// Should not throw exception if only A is set.
var parametersWithInterdependenceOnlyASet = new ParameterSetWithOneOrOtherDependentTopLevelParameters
{
AParam = "A",
};

[TopLevelRequestParameter(Necessity.Optional, "location")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? AParam { get; set; }
try
{
parametersWithInterdependenceOnlyASet.ToDictionary();
}
catch (Exceptions.General.InvalidParameterPairError)
{
Assert.Fail("Should not throw exception if only A is set.");
}

// Should not throw exception if only B is set.
var parametersWithInterdependenceOnlyBSet = new ParameterSetWithOneOrOtherDependentTopLevelParameters
{
BParam = "B",
};

try
{
parametersWithInterdependenceOnlyBSet.ToDictionary();
}
catch (Exceptions.General.InvalidParameterPairError)
{
Assert.Fail("Should not throw exception if only B is set.");
}
}

/// <summary>
Expand Down Expand Up @@ -426,6 +546,8 @@ public void TestParameterMatchOverrideFunction()
#endregion
}

#region Fixtures

#pragma warning disable CA1852 // Can be sealed
[SuppressMessage("ReSharper", "UnusedMember.Local")]
internal class ExampleDecoratorParameters : Parameters.BaseParameters<EasyPostObject>
Expand Down Expand Up @@ -460,5 +582,70 @@ internal class ExampleMatchParameters : Parameters.BaseParameters<ExampleMatchPa
public string? Prop1 { get; set; }
}

internal sealed class ParameterSetWithRequiredAndOptionalParameters : Parameters.BaseParameters<EasyPostObject>
{
[TopLevelRequestParameter(Necessity.Required, "test", "required")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? RequiredParameter { get; set; }

[TopLevelRequestParameter(Necessity.Optional, "test", "optional")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? OptionalParameter { get; set; }
}

internal sealed class ParameterSetWithCompetingParameters : Parameters.BaseParameters<EasyPostObject>
{
[TopLevelRequestParameter(Necessity.Optional, "location")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? AParam { get; set; }

[TopLevelRequestParameter(Necessity.Optional, "location")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? BParam { get; set; }
}

internal sealed class ParameterSetWithCompetingParametersNonAlphabetic : Parameters.BaseParameters<EasyPostObject>
{
[TopLevelRequestParameter(Necessity.Optional, "location")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? BParam { get; set; }

[TopLevelRequestParameter(Necessity.Optional, "location")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? AParam { get; set; }
}

internal sealed class ParameterSetWithOneOrOtherDependentTopLevelParameters : Parameters.BaseParameters<EasyPostObject>
{
[TopLevelRequestParameter(Necessity.Optional, "a_param")]
[TopLevelRequestParameterDependents(IndependentStatus.IfSet, DependentStatus.MustNotBeSet, "BParam")]
[TopLevelRequestParameterDependents(IndependentStatus.IfNotSet, DependentStatus.MustBeSet, "BParam")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? AParam { get; set; }

[TopLevelRequestParameter(Necessity.Optional, "b_param")]
[TopLevelRequestParameterDependents(IndependentStatus.IfSet, DependentStatus.MustNotBeSet, "AParam")]
[TopLevelRequestParameterDependents(IndependentStatus.IfNotSet, DependentStatus.MustBeSet, "AParam")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? BParam { get; set; }
}

internal sealed class ParameterSetWithBothOrNeitherDependentTopLevelParameters : Parameters.BaseParameters<EasyPostObject>
{
[TopLevelRequestParameter(Necessity.Optional, "a_param")]
[TopLevelRequestParameterDependents(IndependentStatus.IfSet, DependentStatus.MustBeSet, "BParam")]
[TopLevelRequestParameterDependents(IndependentStatus.IfNotSet, DependentStatus.MustNotBeSet, "BParam")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? AParam { get; set; }

[TopLevelRequestParameter(Necessity.Optional, "b_param")]
[TopLevelRequestParameterDependents(IndependentStatus.IfSet, DependentStatus.MustBeSet, "AParam")]
[TopLevelRequestParameterDependents(IndependentStatus.IfNotSet, DependentStatus.MustNotBeSet, "AParam")]
// ReSharper disable once UnusedAutoPropertyAccessor.Local
public string? BParam { get; set; }
}

#pragma warning restore CA1852 // Can be sealed

#endregion
}
1 change: 1 addition & 0 deletions EasyPost/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public static class ErrorMessages
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public const string InvalidApiKeyType = "Invalid API key type.";
public const string InvalidParameter = "Invalid parameter: {0}.";
public const string InvalidParameterPair = "Invalid parameter pair: '{0}' and '{1}'.";
public const string InvalidWebhookSignature = "Webhook does not contain a valid HMAC signature.";
public const string JsonDeserializationError = "Error deserializing JSON into object of type {0}.";
public const string JsonNoDataToDeserialize = "No data to deserialize.";
Expand Down
21 changes: 21 additions & 0 deletions EasyPost/Exceptions/General/InvalidParameterPairError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Globalization;

namespace EasyPost.Exceptions.General
{
/// <summary>
/// Represents an error that occurs due to an invalid parameter pair.
/// </summary>
public class InvalidParameterPairError : ValidationError
{
/// <summary>
/// Initializes a new instance of the <see cref="InvalidParameterPairError" /> class.
/// </summary>
/// <param name="firstParameterName">The name of the first parameter in the pair.</param>
/// <param name="secondParameterName">The name of the second parameter in the pair.</param>
/// <param name="followUpMessage">Additional message to include in error message.</param>
internal InvalidParameterPairError(string firstParameterName, string secondParameterName, string? followUpMessage = "")
nwithan8 marked this conversation as resolved.
Show resolved Hide resolved
: base($"{string.Format(CultureInfo.InvariantCulture, Constants.ErrorMessages.InvalidParameterPair, firstParameterName, secondParameterName)}. {followUpMessage}")
{
}
}
}
22 changes: 22 additions & 0 deletions EasyPost/Parameters/BaseParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ public virtual Dictionary<string, object> ToDictionary()

object? value = property.GetValue(this);

// Check dependent parameters before we finish handling the current parameter
IEnumerable<TopLevelRequestParameterDependentsAttribute> dependentParameterAttributes = property.GetCustomAttributes<TopLevelRequestParameterDependentsAttribute>();
foreach (TopLevelRequestParameterDependentsAttribute dependentParameterAttribute in dependentParameterAttributes)
{
Tuple<bool, string> dependentParameterResult = dependentParameterAttribute.DependentsAreCompliant(this, value);
if (!dependentParameterResult.Item1)
{
throw new InvalidParameterPairError(firstParameterName: property.Name, secondParameterName: dependentParameterResult.Item2, followUpMessage: "Please verify the interdependence of these parameters.");
}
}

// If the value is null, check the necessity of the parameter
if (value == null)
{
Expand Down Expand Up @@ -128,6 +139,17 @@ public virtual Dictionary<string, object> ToSubDictionary(Type parentParameterOb

object? value = property.GetValue(this);

// Check dependent parameters before we finish handling the current parameter
IEnumerable<NestedRequestParameterDependentsAttribute> dependentParameterAttributes = property.GetCustomAttributes<NestedRequestParameterDependentsAttribute>();
foreach (NestedRequestParameterDependentsAttribute dependentParameterAttribute in dependentParameterAttributes)
{
Tuple<bool, string> dependentParameterResult = dependentParameterAttribute.DependentsAreCompliant(this, value);
if (!dependentParameterResult.Item1)
{
throw new InvalidParameterPairError(firstParameterName: property.Name, secondParameterName: dependentParameterResult.Item2, followUpMessage: "Please verify the interdependence of these parameters.");
}
}

// If the value is null, check the necessity of the parameter
if (value == null)
{
Expand Down
4 changes: 4 additions & 0 deletions EasyPost/Parameters/Pickup/Create.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class Create : BaseParameters<Models.API.Pickup>, IPickupParameter
/// <see cref="Models.API.Batch"/> being set for pickup (required if <see cref="Shipment"/> not provided).
/// </summary>
[TopLevelRequestParameter(Necessity.Optional, "pickup", "batch")]
[TopLevelRequestParameterDependents(IndependentStatus.IfSet, DependentStatus.MustNotBeSet, "Shipment")]
[TopLevelRequestParameterDependents(IndependentStatus.IfNotSet, DependentStatus.MustBeSet, "Shipment")]
public IBatchParameter? Batch { get; set; }

/// <summary>
Expand Down Expand Up @@ -65,6 +67,8 @@ public class Create : BaseParameters<Models.API.Pickup>, IPickupParameter
/// <see cref="Models.API.Shipment"/> being set for pickup (required if <see cref="Batch"/> not provided).
/// </summary>
[TopLevelRequestParameter(Necessity.Optional, "pickup", "shipment")]
[TopLevelRequestParameterDependents(IndependentStatus.IfSet, DependentStatus.MustNotBeSet, "Batch")]
[TopLevelRequestParameterDependents(IndependentStatus.IfNotSet, DependentStatus.MustBeSet, "Batch")]
public IShipmentParameter? Shipment { get; set; }

#endregion
Expand Down
Loading
Loading