Skip to content

Commit

Permalink
Merge pull request #11 from SteveDunn/develop
Browse files Browse the repository at this point in the history
Fixes a few issues
  • Loading branch information
SteveDunn authored Dec 3, 2021
2 parents 2257444 + 9ae3006 commit fe3d3d3
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 129 deletions.
Binary file modified .github/workflows/build.yaml
Binary file not shown.
72 changes: 52 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![Build](https://github.com/stevedunn/vogen/actions/workflows/build.yaml/badge.svg) [![GitHub release](https://img.shields.io/github/release/stevedunn/vogen.svg)](https://GitHub.com/stevedunn/vogen/releases/) [![GitHub license](https://img.shields.io/github/license/stevedunn/vogen.svg)](https://github.com/SteveDunn/Vogen/blob/main/LICENSE)
[![GitHub issues](https://img.shields.io/github/issues/Naereen/StrapDown.js.svg)](https://GitHub.com/stevedunn/vogen/issues/) [![GitHub issues-closed](https://img.shields.io/github/issues-closed/Naereen/StrapDown.js.svg)](https://GitHub.com/stevedunn/vogen/issues?q=is%3Aissue+is%3Aclosed)
[![Vogen stable version](https://badgen.net/nuget/v/vogen)](https://nuget.org/packages/newtonsoft.json)
[![Vogen stable version](https://badgen.net/nuget/v/vogen)](https://nuget.org/packages/vogen)

<p align="center">
<img src="./assets/cavey.png">
Expand All @@ -16,7 +16,7 @@ Primitive Obsession AKA **StringlyTyped** means being obsessed with primitives.
## What is the repository?

This is a semi-opinionated library which generates [Value Objects](https://wiki.c2.com/?ValueObject) that wrap simple primitives such as `int`, `string`, `double` etc. The main goal of this project is to have almost the same speed and memory performance as using primitives.
This is a semi-opinionated library which generates [Value Objects](https://wiki.c2.com/?ValueObject) that wrap simple primitives such as `int`, `string`, `double` etc. The main goal of this project is to achieve **almost the same speed and memory performance as using primitives directly**.

Some examples:

Expand Down Expand Up @@ -46,7 +46,7 @@ var customerId = CustomerId.From(42);

```csharp
[ValueObject(typeof(int))]
public partial class CustomerId
public partial struct CustomerId
{
}
```
Expand All @@ -63,19 +63,12 @@ public partial class CustomerId
}
```

This generates the constructor and equality code. If your type better suits a value-type, which the example above does (being an `int`), then just change `class` to `struct`:
```csharp
[ValueObject(typeof(int))]
public partial struct CustomerId
{
}
```

This allows us to have more _strongly typed_ domain objects instead of primitives, which makes the code easier to read and enforces better method signatures, so instead of:
Value Object allow more _strongly typed_ domain objects instead of primitives, which makes the code easier to read and enforces tighter method signatures, so instead of:

``` cs
public void DoSomething(int customerId, int supplierId, int amount)
```

we can have:

``` cs
Expand All @@ -102,21 +95,60 @@ A customer ID likely cannot be *fully* represented by an `int`. An `int` can be

So, we need some validation to ensure the **constraints** of a customer ID are met. Because it's in `int`, we can't be sure if it's been checked beforehand, so we need to check it every time we use it. Because it's a primitive, someone might've changed the value, so even if we're 100% sure we've checked it before, it still might need checking again.

So far, we've used as an example, a customer ID of value `42`. In C#, it may come as no surprise that "`42 == 42`" (*I haven't checked that in JavaScript!*). But, in our **domain**, should `42` always equal `42`? Probably not if you're comparing a Supplier ID of `42` to a Customer ID of `42`! But primitives won't help you here (remember, `42 == 42`!) Given this signature:
So far, we've used as an example, a customer ID of value `42`. In C#, it may come as no surprise that "`42 == 42`" (*I haven't checked that in JavaScript!*). But, in our **domain**, should `42` always equal `42`? Probably not if you're comparing a Supplier ID of `42` to a Customer ID of `42`! But primitives won't help you here (remember, `42 == 42`!).

``` cs
public void DoSomething(int customerId, int supplierId, int amount)
```csharp
(42 == 42) // true
(SuppliedId.From(42) == SupplierId.From(42)) // true
(SuppliedId.From(42) == VendorId.From(42)) // compilation error
```

.. the compiler won't tell you you've messed it up by accidentally swapping customer and supplier IDs.
But sometimes, we need to denote that a Value Object isn't valid or hasn't been set. We don't want anyone _outside_ of the object doing this as it could be used accidentally. It's common to have `Unspecified` instances, e.g.

But by using ValueObjects, that signature becomes much more strongly typed:
```csharp
public class Person
{
public Age Age { get; } = Age.Unspecified;
}
```

``` cs
public void DoSomething(CustomerId customerId, SupplierId supplierId, Amount amount)
We can do that with an `Instance` attribute:

```csharp
[ValueObject(typeof(int))]
[Instance("Unspecified", -1)]
public readonly partial struct Age
{
public static Validation Validate(int value) =>
value > 0 ? Validation.Ok : Validation.Invalid("Must be greater than zero.");
}
```

This generates `public static Age Unspecified = new Age(-1);`. The constructor is `private`, so only this type can (deliberately) create _invalid_ instances.

Now, when we use `Age`, our validation becomes clearer:

```csharp
public void Process(Person person) {
if(person.Age == Age.Unspecified) {
// age not specified.
}
}
```

We can also specify other instance properties:

```csharp
[ValueObject(typeof(int))]
[Instance("Freezing", 0)]
[Instance("Boiling", 100)]
public readonly partial struct Centigrade
{
public static Validation Validate(float value) =>
value >= -273 ? Validation.Ok : Validation.Invalid("Cannot be colder than absolute zero");
}
```

Now, the caller can't mess up the ordering of parameters, and the objects themselves are guaranteed to be valid and immutable.

# FAQ

Expand Down
1 change: 1 addition & 0 deletions Vogen.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantSuppressNullableWarningExpression/@EntryIndexedValue">ERROR</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMember_002EGlobal/@EntryIndexedValue">WARNING</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Beckham/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Daves/@EntryIndexedValue">True</s:Boolean>
Expand Down
30 changes: 30 additions & 0 deletions src/Vogen.Examples/NoDefaulting.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using Vogen;
#pragma warning disable CS0219

namespace Vogen.Examples.NoDefaulting
{
/*
You shouldn't be allowed to `default` a Value Object as it bypasses
any validation you might have added.
*/

public class Naughty
{
public Naughty()
{
// uncomment for - error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
// CustomerId c = default;
// var c2 = default(CustomerId);

// VendorId v = default;
// var v2 = default(VendorId);
}
}

[ValueObject(typeof(int))]
public partial struct CustomerId { }

[ValueObject(typeof(int))]
public partial class VendorId { }
}
31 changes: 30 additions & 1 deletion src/Vogen.Examples/RepresentingNullObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,37 @@
using System;
using Vogen;

namespace Vogen.Examples
namespace Vogen.Examples.Instances
{
[ValueObject(typeof(int))]
[Instance("Freezing", 0)]
[Instance("Boiling", 100)]
public readonly partial struct Centigrade
{
public static Validation Validate(float value) =>
value >= -273 ? Validation.Ok : Validation.Invalid("Cannot be colder than absolute zero");
}

// bug - https://github.com/SteveDunn/Vogen/issues/10
// [ValueObject(typeof(float))]
// [Instance("Freezing", 0.0f)]
// [Instance("Boiling", 100.0f)]
// [Instance("AbsoluteZero", -273.15f)]
// public readonly partial struct Centigrade
// {
// public static Validation Validate(float value) =>
// value >= AbsoluteZero.Value ? Validation.Ok : Validation.Invalid("Cannot be colder than absolute zero");
// }

[ValueObject(typeof(int))]
[Instance("Unspecified", -1)]
[Instance("Invalid", -2)]
public readonly partial struct Age
{
public static Validation Validate(int value) =>
value > 0 ? Validation.Ok : Validation.Invalid("Must be greater than zero.");
}

[ValueObject(typeof(int))]
[Instance("Unspecified", 0)]
[Instance("Invalid", -1)]
Expand Down
3 changes: 2 additions & 1 deletion src/Vogen/Diagnostics/DiagnosticCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ public enum DiagnosticCode
ValidationMustBeStatic = 5,
InstanceMethodCannotHaveNullArgumentName = 6,
InstanceMethodCannotHaveNullArgumentValue = 7,
CannotHaveUserConstructors = 8
CannotHaveUserConstructors = 8,
UsingDefaultProhibited = 9
}
23 changes: 22 additions & 1 deletion src/Vogen/Diagnostics/DiagnosticCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ internal class DiagnosticCollection : IEnumerable<Diagnostic>
"Types cannot be nested",
"Type '{0}' must specify an underlying type");

private static readonly DiagnosticDescriptor _usingDefaultProhibited = CreateDescriptor(
DiagnosticCode.UsingDefaultProhibited,
"Using default of Value Objects is prohibited",
"Type '{0}' cannot be constructed with default as it is prohibited.");

private static readonly DiagnosticDescriptor _cannotHaveUserConstructors = CreateDescriptor(
DiagnosticCode.CannotHaveUserConstructors,
"Cannot have user defined constructors",
Expand Down Expand Up @@ -70,6 +75,9 @@ public void AddValidationMustBeStatic(MethodDeclarationSyntax member) =>
public void AddMustSpecifyUnderlyingType(INamedTypeSymbol underlyingType) =>
AddDiagnostic(_mustSpecifyUnderlyingType, underlyingType.Locations, underlyingType.Name);

public void AddUsingDefaultProhibited(Location locationOfDefaultStatement, string voClassName) =>
AddDiagnostic(_usingDefaultProhibited, voClassName, locationOfDefaultStatement);

public void AddCannotHaveUserConstructors(IMethodSymbol constructor) =>
AddDiagnostic(_cannotHaveUserConstructors, constructor.Locations);

Expand All @@ -92,11 +100,24 @@ private static DiagnosticDescriptor CreateDescriptor(DiagnosticCode code, string
return new DiagnosticDescriptor(code.Format(), title, messageFormat, "RestEaseGeneration", severity, isEnabledByDefault: true, customTags: tags);
}

private void AddDiagnostic(DiagnosticDescriptor descriptor, string name, Location location)
{
var diagnostic = Diagnostic.Create(descriptor, location, name);

AddDiagnostic(diagnostic);
}

private void AddDiagnostic(DiagnosticDescriptor descriptor, IEnumerable<Location> locations, params object?[] args)
{
var locationsList = (locations as IReadOnlyList<Location>) ?? locations.ToList();

var diagnostic = Diagnostic.Create(
descriptor,
locationsList.Count == 0 ? Location.None : locationsList[0],
locationsList.Skip(1),
args);

AddDiagnostic(Diagnostic.Create(descriptor, locationsList.Count == 0 ? Location.None : locationsList[0], locationsList.Skip(1), args));
AddDiagnostic(diagnostic);
}

private void AddDiagnostic(DiagnosticDescriptor descriptor, Location? location, params object?[] args) =>
Expand Down
Loading

0 comments on commit fe3d3d3

Please sign in to comment.