Skip to content

Commit

Permalink
Merge pull request #142 from Encamina/@mramos/factorial-improvements
Browse files Browse the repository at this point in the history
v8.1.9 preview-02 release
  • Loading branch information
MarioRamosEs authored Oct 22, 2024
2 parents abc854e + 2a5a47d commit e01c1eb
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 7 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,18 @@ Previous classification is not required if changes are simple or all belong to t

## [8.1.9]

### Major Changes

- Created a new project `Encamina.Enmarcha.Aspire` to handle configurations and extensions for Aspire.
- Added `ResourceBuilderExtensions` class in `Encamina.Enmarcha.Aspire.Extensions`, which provides extension methods for configuring resources.
- The method `WithEnvironment<T>` was added to allow adding an array of environment variables to resources in a type-safe manner.

### Minor Changes

- Added the `AuthenticationRequired` property to `SmtpClientOptions.cs`, which is set to `true` by default. This indicates that authentication is required to connect to the SMTP server. If set to `false`, the server does not require authentication, meaning no username or password is needed for the connection.
- Added the `AtLeastOneRequiredAttribute` Data Annotation to validate that at least one of the specified properties has a value.
- Enchanced `JsonUtils` with new methods: `FastCheckIsJson` and `IsAnAdaptiveCard`.
- Added new `AtLeastOneRequiredSchemaFilter` to ensure OpenAPI schemas enforce that at least one of the specified properties is required, by modifying the schema to use the `anyOf` rule in Swagger documentation generation.

## [8.1.8]

Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<PropertyGroup>
<VersionPrefix>8.1.9</VersionPrefix>
<VersionSuffix>preview-01</VersionSuffix>
<VersionSuffix>preview-02</VersionSuffix>
</PropertyGroup>

<!--
Expand Down
8 changes: 7 additions & 1 deletion Enmarcha.sln
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encamina.Enmarcha.SemanticK
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encamina.Enmarcha.Conversation.Abstractions", "src\Encamina.Enmarcha.Conversation.Abstractions\Encamina.Enmarcha.Conversation.Abstractions.csproj", "{F15072A5-238C-4584-87D2-916EDE9A6BAB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Encamina.Enmarcha.Conversation", "src\Encamina.Enmarcha.Conversation\Encamina.Enmarcha.Conversation.csproj", "{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Encamina.Enmarcha.Conversation", "src\Encamina.Enmarcha.Conversation\Encamina.Enmarcha.Conversation.csproj", "{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Encamina.Enmarcha.Aspire", "src\Encamina.Enmarcha.Aspire\Encamina.Enmarcha.Aspire.csproj", "{68632D97-CE5C-4817-9ECF-E8A47993A287}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -394,6 +396,10 @@ Global
{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{854C7D01-FF78-4D69-9EC7-8ECBA0E0FA74}.Release|Any CPU.Build.0 = Release|Any CPU
{68632D97-CE5C-4817-9ECF-E8A47993A287}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68632D97-CE5C-4817-9ECF-E8A47993A287}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68632D97-CE5C-4817-9ECF-E8A47993A287}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68632D97-CE5C-4817-9ECF-E8A47993A287}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Encamina.Enmarcha.Core\Encamina.Enmarcha.Core.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Reflection;

using Encamina.Enmarcha.Core.DataAnnotations;

using Microsoft.OpenApi.Models;

using Swashbuckle.AspNetCore.SwaggerGen;

namespace Encamina.Enmarcha.AspNet.OpenApi.Swashbuckle.SchemaFilters;

/// <summary>
/// This filter enforces that at least one of a specified set of properties must be required in an OpenAPI schema.
/// It processes types decorated with the <see cref="AtLeastOneRequiredAttribute"/>.
/// </summary>
public sealed class AtLeastOneRequiredSchemaFilter : ISchemaFilter
{
/// <summary>
/// Applies the filter to modify the OpenAPI schema based on the <see cref="AtLeastOneRequiredAttribute"/>.
/// It removes individual property requirements and replaces them with an 'anyOf' requirement
/// for the specified properties, ensuring that at least one of them is present in the request.
/// </summary>
/// <param name="schema">The <see cref="OpenApiSchema"/> to modify.</param>
/// <param name="context">
/// The <see cref="SchemaFilterContext"/> containing metadata about the context, including the type being processed.
/// </param>
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var attribute = context.Type.GetCustomAttribute<AtLeastOneRequiredAttribute>();
if (attribute != null)
{
// Add 'anyOf' to the schema with the specified properties
schema.AnyOf = attribute.PropertyNames.Select(propertyName => new OpenApiSchema
{
Required = new HashSet<string> { propertyName },
}).ToList();
}
}
}
19 changes: 19 additions & 0 deletions src/Encamina.Enmarcha.Aspire/Encamina.Enmarcha.Aspire.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<PropertyGroup>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="8.2.1" />
</ItemGroup>

<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;

using Encamina.Enmarcha.Aspire.Extensions;

namespace Encamina.Enmarcha.Aspire.Extensions;

/// <summary>
/// Provides extension methods for configuring Aspire resources.
/// </summary>
public static class ResourceBuilderExtensions
{
/// <summary>
/// Adds an environment variable array to the resource.
/// </summary>
/// <typeparam name="T">The resource type.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="name">The name of the environment variable.</param>
/// <param name="values">The array of values of the environment variable.</param>
/// <returns>A resource configured with the specified environment variable.</returns>
public static IResourceBuilder<T> WithEnvironment<T>(this IResourceBuilder<T> builder, string name, string[] values) where T : IResourceWithEnvironment
{
return builder.WithEnvironment(context =>
{
for (var i = 0; i < values.Length; i++)
{
context.EnvironmentVariables[$"{name}:{i}"] = values[i];
}
});
}
}
30 changes: 30 additions & 0 deletions src/Encamina.Enmarcha.Aspire/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Enmarcha Aspire

[![Nuget package](https://img.shields.io/nuget/v/Encamina.Enmarcha.Aspire)](https://www.nuget.org/packages/Encamina.Enmarcha.Aspire)

Aspire Resource Builder Extensions is a set of tools designed to simplify the configuration of cloud-native resources with environment variables. This package is ideal for projects that need to manage distributed application resources in a cloud environment using environment-specific configurations.

## Setup

### Nuget package

First, [install NuGet](http://docs.nuget.org/docs/start-here/installing-nuget). Then, install [Encamina.Enmarcha.Aspire](https://www.nuget.org/packages/Encamina.Enmarcha.Aspire) from the package manager console:

PM> Install-Package Encamina.Enmarcha.Aspire

### .NET CLI:

[Install .NET CLI](https://learn.microsoft.com/en-us/dotnet/core/tools/). Next, install [Encamina.Enmarcha.Aspire](https://www.nuget.org/packages/Encamina.Enmarcha.Aspire) from the .NET CLI:

dotnet add package Encamina.Enmarcha.Aspire

## How to use

### Resource Builder Extensions

The `ResourceBuilderExtensions` class provides a set of extension methods to configure Aspire resources.

```csharp
var agentProjects = builder.AddProject<Projects.AgentProjects>(appIdProjects)
.WithEnvironment(Constants.Settings.Options.PendingTaskStatuses, pendingTaskStatuses)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;

using Encamina.Enmarcha.Core.Extensions;

using ValidationResultMessages = Encamina.Enmarcha.Core.DataAnnotations.Resources.ValudationResultMessages;

namespace Encamina.Enmarcha.Core.DataAnnotations;

/// <summary>
/// Attribute that enforces validation on a class by requiring at least one of the specified properties to have a non-null or non-empty value.
/// </summary>
/// <remarks>
/// This attribute is typically used on models to ensure that at least one of the specified properties is populated.
/// If none of the specified properties have a value, the validation will fail.
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class AtLeastOneRequiredAttribute : ValidationAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="AtLeastOneRequiredAttribute"/> class with the specified property names.
/// </summary>
/// <param name="propertyNames">An array of property names that must be validated.</param>
public AtLeastOneRequiredAttribute(params string[] propertyNames)
{
PropertyNames = propertyNames;
}

/// <summary>
/// Gets the array of property names that must be validated to ensure at least one has a value.
/// </summary>
public string[] PropertyNames { get; }

/// <summary>
/// Validates that at least one of the specified properties has a non-null or non-empty value.
/// </summary>
/// <param name="value">The object to validate, typically the class instance this attribute is applied to.</param>
/// <param name="validationContext">Provides contextual information about the validation operation.</param>
/// <returns>
/// <see cref="ValidationResult.Success"/> if at least one property has a value; otherwise, a <see cref="ValidationResult"/> indicating the error.
/// </returns>
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is null)
{
return ValidationResult.Success;
}

var type = value.GetType();
var atLeastOneHasValue = false;

foreach (var propertyName in PropertyNames)
{
var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
if (property == null)
{
return new ValidationResult(ValidationResultMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ValidationResultMessages.PropertyNotFound), propertyName));
}

var propertyValue = property.GetValue(value);
if (propertyValue is string str)
{
if (!string.IsNullOrWhiteSpace(str))
{
atLeastOneHasValue = true;
break;

Check warning on line 66 in src/Encamina.Enmarcha.Core/DataAnnotations/AtLeastOneRequiredAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Refactor the code in order to remove this break statement. (https://rules.sonarsource.com/csharp/RSPEC-1227)
}
}
else if (propertyValue != null)

Check warning on line 69 in src/Encamina.Enmarcha.Core/DataAnnotations/AtLeastOneRequiredAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Add the missing 'else' clause with either the appropriate action or a suitable comment as to why no action is taken. (https://rules.sonarsource.com/csharp/RSPEC-126)
{
atLeastOneHasValue = true;
break;

Check warning on line 72 in src/Encamina.Enmarcha.Core/DataAnnotations/AtLeastOneRequiredAttribute.cs

View workflow job for this annotation

GitHub Actions / CI

Refactor the code in order to remove this break statement. (https://rules.sonarsource.com/csharp/RSPEC-1227)
}
}

if (!atLeastOneHasValue)
{
var propertyNames = string.Join(", ", PropertyNames);
return new ValidationResult(ValidationResultMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ValidationResultMessages.MissingRequiredProperty), propertyNames));
}

return ValidationResult.Success;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public RequiredIfAttribute(string[] conditionalPropertyNames, object[] condition

if (propertyInfo == null)
{
return new ValidationResult(ValudationResultMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ValudationResultMessages.MissingPropertyRequiredIf), propertyName));
return new ValidationResult(ValudationResultMessages.ResourceManager.GetFormattedStringByCurrentCulture(nameof(ValudationResultMessages.PropertyNotFound), propertyName));
}

var propertyValue = propertyInfo.GetValue(objInstance);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,13 @@
<data name="ValuieIsInvalidUri" xml:space="preserve">
<value>The value of '{0}' must be a valid and well formed URI!</value>
</data>
<data name="MissingPropertyRequiredIf" xml:space="preserve">
<data name="PropertyNotFound" xml:space="preserve">
<value>Property {0} not found!</value>
</data>
<data name="PropertiesConditionsMissmatchRequiredIf" xml:space="preserve">
<value>The number of conditional property names must match the number of expected conditional values.</value>
</data>
<data name="MissingRequiredProperty" xml:space="preserve">
<value>At least one of the following properties must be provided: {0}.</value>
</data>
</root>
67 changes: 67 additions & 0 deletions src/Encamina.Enmarcha.Core/JsonUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,73 @@ public static class JsonUtils
/// <param name="options">A valid instance of <see cref="JsonSerializerOptions"/> with options for the deserialization.</param>
/// <returns>A valid instance of <typeparamref name="T"/> obtained from the JSON deserialization.</returns>
public static T? DeserializeAnonymousType<T>(string json, T anonymousType, JsonSerializerOptions? options = default) => JsonSerializer.Deserialize<T>(json, options);

Check warning on line 23 in src/Encamina.Enmarcha.Core/JsonUtils.cs

View workflow job for this annotation

GitHub Actions / CI

Use the overloading mechanism instead of the optional parameters. (https://rules.sonarsource.com/csharp/RSPEC-2360)

Check warning on line 23 in src/Encamina.Enmarcha.Core/JsonUtils.cs

View workflow job for this annotation

GitHub Actions / CI

Use the overloading mechanism instead of the optional parameters. (https://rules.sonarsource.com/csharp/RSPEC-2360)

/// <summary>
/// Performs a fast check to determine if the input string is a JSON object.
/// This method only checks if the string starts with '{' and ends with '}'.
/// It does not validate the entire JSON structure.
/// </summary>
/// <param name="input">The input string to check.</param>
/// <returns>
/// <c>true</c> if the input starts with '{' and ends with '}'; otherwise, <c>false</c>.
/// </returns>
public static bool FastCheckIsJson(string input)
{
input = input.Trim();
return input.StartsWith('{') && input.EndsWith('}');
}

/// <summary>
/// Determines whether the given JSON string represents an Adaptive Card.
/// An Adaptive Card must have a 'type' property with the value 'AdaptiveCard' and a Adaptive Card schema '$schema'.
/// </summary>
/// <param name="input">The input string to check, which is expected to be a JSON object.</param>
/// <returns>
/// <c>true</c> if the input is a valid JSON object representing an Adaptive Card; otherwise, <c>false</c>.
/// </returns>
public static bool IsAnAdaptiveCard(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return false;
}

if (!FastCheckIsJson(input))
{
return false;
}

try
{
using var document = JsonDocument.Parse(input);
var root = document.RootElement;

if (root.ValueKind != JsonValueKind.Object)
{
return false;
}

// Check for required properties: 'type' and '$schema'
if (!root.TryGetProperty("type", out var typeElement) ||
!root.TryGetProperty("$schema", out var schemaElement))
{
return false;
}

return string.Equals(typeElement.GetString(), @"AdaptiveCard", StringComparison.OrdinalIgnoreCase) &&
schemaElement.GetString()!.EndsWith(@"adaptivecards.io/schemas/adaptive-card.json", StringComparison.OrdinalIgnoreCase);
}
catch (JsonException)
{
// input is not a valid JSON object
return false;
}
catch (InvalidOperationException)
{
// Handle potential exceptions due to invalid JSON structure
return false;
}
}
}

#pragma warning restore IDE0060

0 comments on commit e01c1eb

Please sign in to comment.