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

Support nested union types with internal outer classes #44

Merged
merged 6 commits into from
Jan 28, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

<!--#region adapt versions here-->
<MajorVersion>4</MajorVersion>
<MinorAndPatchVersion>2.0</MinorAndPatchVersion>
<MinorAndPatchVersion>2.1</MinorAndPatchVersion>
<!--#endregion-->

<AssemblyVersion>$(MajorVersion).0.0</AssemblyVersion>
Expand Down
5 changes: 4 additions & 1 deletion Source/FunicularSwitch.Generators/UnionType/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ static void WritePartialWithStaticFactories(UnionTypeSchema unionTypeSchema, CSh
var typeParameters = RoslynExtensions.FormatTypeParameters(unionTypeSchema.TypeParameters);

var typeKind = GetTypeKind(unionTypeSchema);
builder.WriteLine($"{(unionTypeSchema.Modifiers.ToSeparatedString(" "))} {typeKind} {unionTypeSchema.TypeName}{typeParameters}");
var actualModifiers = unionTypeSchema.Modifiers
.Select(m => m == "public" ? (unionTypeSchema.IsInternal ? "internal" : "public") : m);

builder.WriteLine($"{(actualModifiers.ToSeparatedString(" "))} {typeKind} {unionTypeSchema.TypeName}{typeParameters}");
using (builder.Scope())
{
foreach (var derivedType in unionTypeSchema.Cases)
Expand Down
6 changes: 3 additions & 3 deletions Source/FunicularSwitch.Generators/UnionType/Parser.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections.Immutable;
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
using FunicularSwitch.Generators.Common;
using FunicularSwitch.Generators.Generation;
using Microsoft.CodeAnalysis;
Expand All @@ -22,7 +21,7 @@ public static GenerationResult<UnionTypeSchema> GetUnionTypeSchema(Compilation c

var fullTypeName = unionTypeSymbol.FullTypeNameWithNamespace();
var fullTypeNameWithTypeParameters = fullTypeName + RoslynExtensions.FormatTypeParameters(typeParameters);
var acc = unionTypeSymbol.DeclaredAccessibility;
var acc = unionTypeSymbol.GetActualAccessibility();
if (acc is Accessibility.Private or Accessibility.Protected)
{
var diag = Diagnostics.UnionTypeIsNotAccessible($"{fullTypeName} needs at least internal accessibility", unionTypeClass.GetLocation());
Expand All @@ -44,7 +43,8 @@ public static GenerationResult<UnionTypeSchema> GetUnionTypeSchema(Compilation c
});


var isPartial = unionTypeClass.Modifiers.HasModifier(SyntaxKind.PartialKeyword);
var isPartial = unionTypeClass.Modifiers.HasModifier(SyntaxKind.PartialKeyword)
&& unionTypeSymbol.ContainingType == null; //for now do not generate factory methods for nested types, we could support that if all containing types are partial
var generateFactoryMethods = isPartial && staticFactoryMethods;

return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//HintName: Attributes.g.cs
using System;

// ReSharper disable once CheckNamespace
namespace FunicularSwitch.Generators
{
/// <summary>
/// Mark an abstract partial type with a single generic argument with the ResultType attribute.
/// This type from now on has Ok | Error semantics with map and bind operations.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
sealed class ResultTypeAttribute : Attribute
{
public ResultTypeAttribute() => ErrorType = typeof(string);
public ResultTypeAttribute(Type errorType) => ErrorType = errorType;

public Type ErrorType { get; set; }
}

/// <summary>
/// Mark a static method or a member method or you error type with the MergeErrorAttribute attribute.
/// Static signature: TError -> TError -> TError. Member signature: TError -> TError
/// We are now able to collect errors and methods like Validate, Aggregate, FirstOk that are useful to combine results are generated.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
sealed class MergeErrorAttribute : Attribute
{
}

/// <summary>
/// Mark a static method with the ExceptionToError attribute.
/// Signature: Exception -> TError
/// This method is always called, when an exception happens in a bind operation.
/// So a call like result.Map(i => i/0) will return an Error produced by the factory method instead of throwing the DivisionByZero exception.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
sealed class ExceptionToError : Attribute
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//HintName: Attributes.g.cs
using System;

// ReSharper disable once CheckNamespace
namespace FunicularSwitch.Generators
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, Inherited = false)]
sealed class UnionTypeAttribute : Attribute
{
public CaseOrder CaseOrder { get; set; } = CaseOrder.Alphabetic;
public bool StaticFactoryMethods { get; set; } = true;
}

enum CaseOrder
{
Alphabetic,
AsDeclared,
Explicit
}

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
sealed class UnionCaseAttribute : Attribute
{
public UnionCaseAttribute(int index) => Index = index;

public int Index { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//HintName: Attributes.g.cs
using System;

// ReSharper disable once CheckNamespace
namespace FunicularSwitch.Generators
{
[AttributeUsage(AttributeTargets.Enum)]
sealed class ExtendedEnumAttribute : Attribute
{
public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared;
public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public;
}

enum EnumCaseOrder
{
Alphabetic,
AsDeclared
}

/// <summary>
/// Generate match methods for all enums defined in assembly that contains AssemblySpecifier.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
class ExtendEnumsAttribute : Attribute
{
public Type AssemblySpecifier { get; }
public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared;
public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public;

public ExtendEnumsAttribute() => AssemblySpecifier = typeof(ExtendEnumsAttribute);

public ExtendEnumsAttribute(Type assemblySpecifier)
{
AssemblySpecifier = assemblySpecifier;
}
}

/// <summary>
/// Generate match methods for Type. Must be enum.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
class ExtendEnumAttribute : Attribute
{
public Type Type { get; }

public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared;

public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public;

public ExtendEnumAttribute(Type type)
{
Type = type;
}
}

enum ExtensionAccessibility
{
Internal,
Public
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//HintName: FunicularSwitchTestOuterInitResultMatchExtension.g.cs
#pragma warning disable 1591
namespace FunicularSwitch.Test
{
internal static partial class InitResultMatchExtension
{
public static T Match<T>(this FunicularSwitch.Test.Outer.InitResult initResult, global::System.Func<FunicularSwitch.Test.Outer.InitResult.Sync_, T> sync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.OneTimeSync_, T> oneTimeSync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.NoSync_, T> noSync) =>
initResult switch
{
FunicularSwitch.Test.Outer.InitResult.Sync_ sync1 => sync(sync1),
FunicularSwitch.Test.Outer.InitResult.OneTimeSync_ oneTimeSync2 => oneTimeSync(oneTimeSync2),
FunicularSwitch.Test.Outer.InitResult.NoSync_ noSync3 => noSync(noSync3),
_ => throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Test.Outer.InitResult: {initResult.GetType().Name}")
};

public static global::System.Threading.Tasks.Task<T> Match<T>(this FunicularSwitch.Test.Outer.InitResult initResult, global::System.Func<FunicularSwitch.Test.Outer.InitResult.Sync_, global::System.Threading.Tasks.Task<T>> sync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.OneTimeSync_, global::System.Threading.Tasks.Task<T>> oneTimeSync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.NoSync_, global::System.Threading.Tasks.Task<T>> noSync) =>
initResult switch
{
FunicularSwitch.Test.Outer.InitResult.Sync_ sync1 => sync(sync1),
FunicularSwitch.Test.Outer.InitResult.OneTimeSync_ oneTimeSync2 => oneTimeSync(oneTimeSync2),
FunicularSwitch.Test.Outer.InitResult.NoSync_ noSync3 => noSync(noSync3),
_ => throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Test.Outer.InitResult: {initResult.GetType().Name}")
};

public static async global::System.Threading.Tasks.Task<T> Match<T>(this global::System.Threading.Tasks.Task<FunicularSwitch.Test.Outer.InitResult> initResult, global::System.Func<FunicularSwitch.Test.Outer.InitResult.Sync_, T> sync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.OneTimeSync_, T> oneTimeSync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.NoSync_, T> noSync) =>
(await initResult.ConfigureAwait(false)).Match(sync, oneTimeSync, noSync);

public static async global::System.Threading.Tasks.Task<T> Match<T>(this global::System.Threading.Tasks.Task<FunicularSwitch.Test.Outer.InitResult> initResult, global::System.Func<FunicularSwitch.Test.Outer.InitResult.Sync_, global::System.Threading.Tasks.Task<T>> sync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.OneTimeSync_, global::System.Threading.Tasks.Task<T>> oneTimeSync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.NoSync_, global::System.Threading.Tasks.Task<T>> noSync) =>
await (await initResult.ConfigureAwait(false)).Match(sync, oneTimeSync, noSync).ConfigureAwait(false);

public static void Switch(this FunicularSwitch.Test.Outer.InitResult initResult, global::System.Action<FunicularSwitch.Test.Outer.InitResult.Sync_> sync, global::System.Action<FunicularSwitch.Test.Outer.InitResult.OneTimeSync_> oneTimeSync, global::System.Action<FunicularSwitch.Test.Outer.InitResult.NoSync_> noSync)
{
switch (initResult)
{
case FunicularSwitch.Test.Outer.InitResult.Sync_ sync1:
sync(sync1);
break;
case FunicularSwitch.Test.Outer.InitResult.OneTimeSync_ oneTimeSync2:
oneTimeSync(oneTimeSync2);
break;
case FunicularSwitch.Test.Outer.InitResult.NoSync_ noSync3:
noSync(noSync3);
break;
default:
throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Test.Outer.InitResult: {initResult.GetType().Name}");
}
}

public static async global::System.Threading.Tasks.Task Switch(this FunicularSwitch.Test.Outer.InitResult initResult, global::System.Func<FunicularSwitch.Test.Outer.InitResult.Sync_, global::System.Threading.Tasks.Task> sync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.OneTimeSync_, global::System.Threading.Tasks.Task> oneTimeSync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.NoSync_, global::System.Threading.Tasks.Task> noSync)
{
switch (initResult)
{
case FunicularSwitch.Test.Outer.InitResult.Sync_ sync1:
await sync(sync1).ConfigureAwait(false);
break;
case FunicularSwitch.Test.Outer.InitResult.OneTimeSync_ oneTimeSync2:
await oneTimeSync(oneTimeSync2).ConfigureAwait(false);
break;
case FunicularSwitch.Test.Outer.InitResult.NoSync_ noSync3:
await noSync(noSync3).ConfigureAwait(false);
break;
default:
throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Test.Outer.InitResult: {initResult.GetType().Name}");
}
}

public static async global::System.Threading.Tasks.Task Switch(this global::System.Threading.Tasks.Task<FunicularSwitch.Test.Outer.InitResult> initResult, global::System.Action<FunicularSwitch.Test.Outer.InitResult.Sync_> sync, global::System.Action<FunicularSwitch.Test.Outer.InitResult.OneTimeSync_> oneTimeSync, global::System.Action<FunicularSwitch.Test.Outer.InitResult.NoSync_> noSync) =>
(await initResult.ConfigureAwait(false)).Switch(sync, oneTimeSync, noSync);

public static async global::System.Threading.Tasks.Task Switch(this global::System.Threading.Tasks.Task<FunicularSwitch.Test.Outer.InitResult> initResult, global::System.Func<FunicularSwitch.Test.Outer.InitResult.Sync_, global::System.Threading.Tasks.Task> sync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.OneTimeSync_, global::System.Threading.Tasks.Task> oneTimeSync, global::System.Func<FunicularSwitch.Test.Outer.InitResult.NoSync_, global::System.Threading.Tasks.Task> noSync) =>
await (await initResult.ConfigureAwait(false)).Switch(sync, oneTimeSync, noSync).ConfigureAwait(false);
}
}
#pragma warning restore 1591
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,31 @@ class Consumer {
return Verify(code);
}

[TestMethod]
public Task Static_factories_for_nested_internal_union_type()
{
var code = @"
using FunicularSwitch.Generators;

namespace FunicularSwitch.Test;

static class Outer
{
[UnionType(CaseOrder = CaseOrder.AsDeclared)]
public partial record InitResult
{
public record Sync_ : InitResult;
public record OneTimeSync_(string TempRepoFolder) : InitResult;
public record NoSync_() : InitResult;
}
}";

return Verify(code);
}


[TestMethod]
public Task No_static_factories_for_interface_union_type()
public Task Static_factories_for_interface_union_type()
{
var code = @"
using FunicularSwitch.Generators;
Expand Down
Loading