Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dotnet add package SimplyWorks.PrimitiveTypes
- 🎯 **API Handlers** - CQRS interfaces for commands and queries
- 🔧 **Extensions** - Type conversion and utility methods
- ☁️ **Cloud Storage** - File operation abstractions
- 🏷️ **Contract labels** - `[Contract("orders.msg.placed", Major = 2)]` names the published shape of a type and its major version, for libraries and schema tools to read

## � Usage Example

Expand Down
67 changes: 67 additions & 0 deletions SW.PrimitiveTypes.UnitTests/ContractAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace SW.PrimitiveTypes.UnitTests
{
[TestClass]
public class ContractAttributeTests
{
[Contract("orders.msg.placed", Major = 2)]
class OrderPlaced { }

class OrderPlacedWithGift : OrderPlaced { }

[Contract("orders.document.invoice")]
struct Invoice { }

[TestMethod]
public void Reads_key_and_major()
{
var c = ContractAttribute.Of(typeof(OrderPlaced));
Assert.AreEqual("orders.msg.placed", c.Key);
Assert.AreEqual(2, c.Major);
Assert.AreEqual("orders.msg.placed@2", c.Id);
Assert.AreEqual("orders.msg.placed@2", c.ToString());
}

[TestMethod]
public void Major_defaults_to_1() =>
Assert.AreEqual("orders.document.invoice@1", ContractAttribute.Of(typeof(Invoice)).Id);

[TestMethod]
public void A_derived_type_does_not_take_its_base_contract()
{
Assert.IsNull(ContractAttribute.Of(typeof(OrderPlacedWithGift)));
Assert.IsNull(Attribute.GetCustomAttribute(typeof(OrderPlacedWithGift), typeof(ContractAttribute), inherit: true));
}

[TestMethod]
public void An_unlabelled_type_or_null_has_none()
{
Assert.IsNull(ContractAttribute.Of(typeof(string)));
Assert.IsNull(ContractAttribute.Of(null));
}

[DataTestMethod]
[DataRow("orders.msg.placed", 1, true)]
[DataRow("orders", 3, true)]
[DataRow("orders2.v2", 1, true)]
[DataRow("Orders.msg.placed", 1, false)]
[DataRow("orders..placed", 1, false)]
[DataRow("orders.msg.", 1, false)]
[DataRow("orders-msg", 1, false)]
[DataRow("orders.msg.placed", 0, false)]
[DataRow("orders\n", 1, false)]
[DataRow("orders.msg.placed\n", 1, false)]
[DataRow(" orders", 1, false)]
public void Checks_its_form(string key, int major, bool wellFormed) =>
Assert.AreEqual(wellFormed, new ContractAttribute(key) { Major = major }.IsWellFormed);

[TestMethod]
public void A_missing_key_is_refused()
{
Assert.ThrowsException<ArgumentNullException>(() => new ContractAttribute(null));
Assert.ThrowsException<ArgumentNullException>(() => new ContractAttribute(" "));
}
}
}
47 changes: 47 additions & 0 deletions SW.PrimitiveTypes/Contracts/ContractAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Reflection;
using System.Text.RegularExpressions;

namespace SW.PrimitiveTypes
{
/// <summary>
/// Names the contract a type's shape is published as: a stable key and a major version that changes
/// only on a breaking change. It's a label and does nothing by itself; libraries that move the type
/// (a bus publisher, an API host, an adapter runner) and schema tools read it.
/// </summary>
/// <remarks>
/// Not inherited: a derived type has a shape of its own, so it names its own contract.
/// The constructor rejects only a missing key, because an attribute constructor that throws breaks
/// every caller that reflects over the type. Use <see cref="IsWellFormed"/> to check the rest.
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)]
public class ContractAttribute : Attribute
{
// \z, not $: in .NET "$" also matches before a final newline, which would let "orders\n" through.
static readonly Regex KeyFormat = new Regex("^[a-z0-9]+(\\.[a-z0-9]+)*\\z", RegexOptions.CultureInvariant);

public ContractAttribute(string key)
{
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
Key = key;
}

/// <summary>Lowercase, dot-separated: "orders.msg.placed".</summary>
public string Key { get; }

/// <summary>Changes only when the shape changes in a way old readers can't handle.</summary>
public int Major { get; set; } = 1;

/// <summary>"orders.msg.placed@1".</summary>
public string Id => $"{Key}@{Major}";

/// <summary>The key is lowercase and dot-separated, and the major is at least 1.</summary>
public bool IsWellFormed => KeyFormat.IsMatch(Key) && Major >= 1;

/// <summary>The contract declared on the type itself, or null.</summary>
public static ContractAttribute Of(Type type) =>
type?.GetCustomAttribute<ContractAttribute>(inherit: false);

public override string ToString() => Id;
}
}
Loading