diff --git a/crates/bindings-csharp/BSATN.Codegen/Utils.cs b/crates/bindings-csharp/BSATN.Codegen/Utils.cs index deb0f562d2..2a8b6e9bdd 100644 --- a/crates/bindings-csharp/BSATN.Codegen/Utils.cs +++ b/crates/bindings-csharp/BSATN.Codegen/Utils.cs @@ -182,9 +182,10 @@ IEnumerable values _ => constant.Value, }; - public static T ParseAs(this AttributeData attrData) + public static T ParseAs(this AttributeData attrData, System.Type? type = null) where T : Attribute { + type ??= typeof(T); var ctorArgs = attrData.ConstructorArguments.Select(ResolveConstant).ToArray(); // For now only support attributes with a single constructor. // @@ -193,10 +194,10 @@ public static T ParseAs(this AttributeData attrData) // which prevent APIs like `Activator.CreateInstance` from finding the constructor. // // Expand logic in the future if it ever becomes actually necessary. - var attr = (T)typeof(T).GetConstructors().Single().Invoke(ctorArgs); + var attr = (T)type.GetConstructors().Single().Invoke(ctorArgs); foreach (var arg in attrData.NamedArguments) { - typeof(T).GetProperty(arg.Key).SetValue(attr, ResolveConstant(arg.Value)); + type.GetProperty(arg.Key).SetValue(attr, ResolveConstant(arg.Value)); } return attr; } diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#PublicTable.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#PublicTable.verified.cs index 7a1e03c156..9ef203aad6 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#PublicTable.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#PublicTable.verified.cs @@ -255,7 +255,12 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar Indexes: [], Constraints: [ - new(nameof(PublicTable), 0, nameof(Id), (SpacetimeDB.ColumnAttrs)15) + new( + nameof(PublicTable), + 0, + nameof(Id), + SpacetimeDB.Internal.ColumnAttrs.PrimaryKeyAuto + ) ], Sequences: [], // "system" | "user" diff --git a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Timers.SendMessageTimer.verified.cs b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Timers.SendMessageTimer.verified.cs index 26f8b11a88..1b38c1d4af 100644 --- a/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Timers.SendMessageTimer.verified.cs +++ b/crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#Timers.SendMessageTimer.verified.cs @@ -73,7 +73,7 @@ SpacetimeDB.BSATN.ITypeRegistrar registrar nameof(SendMessageTimer), 1, nameof(ScheduledId), - (SpacetimeDB.ColumnAttrs)15 + SpacetimeDB.Internal.ColumnAttrs.PrimaryKeyAuto ) ], Sequences: [], diff --git a/crates/bindings-csharp/Codegen/Module.cs b/crates/bindings-csharp/Codegen/Module.cs index 264e0380d9..915d3d4785 100644 --- a/crates/bindings-csharp/Codegen/Module.cs +++ b/crates/bindings-csharp/Codegen/Module.cs @@ -4,11 +4,37 @@ namespace SpacetimeDB.Codegen; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using SpacetimeDB.Internal; using static Utils; +readonly record struct ColumnAttr(ColumnAttrs Mask, string? Table = null) +{ + private static readonly ImmutableDictionary AttrTypes = ImmutableArray + .Create( + typeof(AutoIncAttribute), + typeof(PrimaryKeyAttribute), + typeof(UniqueAttribute), + typeof(IndexedAttribute) + ) + .ToImmutableDictionary(t => t.FullName); + + public static ColumnAttr Parse(AttributeData attrData) + { + if ( + attrData.AttributeClass is not { } attrClass + || !AttrTypes.TryGetValue(attrClass.ToString(), out var attrType) + ) + { + return default; + } + var attr = attrData.ParseAs(attrType); + return new(attr.Mask, attr.Table); + } +} + record ColumnDeclaration : MemberDeclaration { - public readonly EquatableArray<(string? table, ColumnAttrs mask)> Attrs; + public readonly EquatableArray Attrs; public readonly bool IsEquatable; public readonly string FullTableName; @@ -22,11 +48,16 @@ bool isEquatable ) : base(name, type, typeInfo) { - Attrs = new(ImmutableArray.Create((default(string), attrs))); + Attrs = new(ImmutableArray.Create(new ColumnAttr(attrs))); IsEquatable = isEquatable; FullTableName = tableName; } + // A helper to combine multiple column attributes into a single mask. + // Note: it doesn't check the table names, this is left up to the caller. + private static ColumnAttrs CombineColumnAttrs(IEnumerable attrs) => + attrs.Aggregate(ColumnAttrs.UnSet, (mask, attr) => mask | attr.Mask); + public ColumnDeclaration(string tableName, IFieldSymbol field) : base(field) { @@ -35,21 +66,12 @@ public ColumnDeclaration(string tableName, IFieldSymbol field) Attrs = new( field .GetAttributes() - .Select(a => - ( - table: a.NamedArguments.FirstOrDefault(a => a.Key == "Table").Value.Value - as string, - attr: a.AttributeClass?.ToString() switch - { - "SpacetimeDB.AutoIncAttribute" => ColumnAttrs.AutoInc, - "SpacetimeDB.PrimaryKeyAttribute" => ColumnAttrs.PrimaryKey, - "SpacetimeDB.UniqueAttribute" => ColumnAttrs.Unique, - "SpacetimeDB.IndexedAttribute" => ColumnAttrs.Indexed, - _ => ColumnAttrs.UnSet, - } - ) + .Select(ColumnAttr.Parse) + .Where(a => a.Mask != ColumnAttrs.UnSet) + .GroupBy( + a => a.Table, + (key, group) => new ColumnAttr(CombineColumnAttrs(group), key) ) - .Where(a => a.attr != ColumnAttrs.UnSet) .ToImmutableArray() ); @@ -75,7 +97,7 @@ or SpecialType.System_Int64 _ => false, }; - var attrs = Attrs.Aggregate(ColumnAttrs.UnSet, (xs, x) => xs | x.mask); + var attrs = CombineColumnAttrs(Attrs); if (attrs.HasFlag(ColumnAttrs.AutoInc) && !isInteger) { @@ -107,9 +129,7 @@ or SpecialType.System_Int64 } public ColumnAttrs GetAttrs(string tableName) => - Attrs - .Where(x => x.table == null || x.table == tableName) - .Aggregate(ColumnAttrs.UnSet, (xs, x) => xs | x.mask); + CombineColumnAttrs(Attrs.Where(x => x.Table == null || x.Table == tableName)); // For the `TableDesc` constructor. public string GenerateColumnDef() => @@ -128,16 +148,11 @@ record TableView public TableView(TableDeclaration table, AttributeData data) { - Name = - data.NamedArguments.FirstOrDefault(x => x.Key == "Name").Value.Value as string - ?? table.ShortName; - - IsPublic = data.NamedArguments.Any(pair => pair is { Key: "Public", Value.Value: true }); + var attr = data.ParseAs(); - Scheduled = data - .NamedArguments.Where(pair => pair.Key == "Scheduled") - .Select(pair => (string?)pair.Value.Value) - .SingleOrDefault(); + Name = attr.Name ?? table.ShortName; + IsPublic = attr.Public; + Scheduled = attr.Scheduled; } } @@ -347,7 +362,7 @@ public override Scope.Extensions ToExtensions() nameof({{ShortName}}), {{tuple.pos}}, nameof({{tuple.col.Name}}), - (SpacetimeDB.ColumnAttrs){{(int)tuple.attr}} + SpacetimeDB.Internal.ColumnAttrs.{{tuple.attr}} ) """ ) diff --git a/crates/bindings-csharp/Runtime/Attrs.cs b/crates/bindings-csharp/Runtime/Attrs.cs index 578d0293d9..a9843309d9 100644 --- a/crates/bindings-csharp/Runtime/Attrs.cs +++ b/crates/bindings-csharp/Runtime/Attrs.cs @@ -1,73 +1,87 @@ -namespace SpacetimeDB; - -[Flags] -public enum ColumnAttrs : byte +namespace SpacetimeDB { - UnSet = 0b0000, - Indexed = 0b0001, - AutoInc = 0b0010, - Unique = Indexed | 0b0100, - Identity = Unique | AutoInc, - PrimaryKey = Unique | 0b1000, - PrimaryKeyAuto = PrimaryKey | AutoInc, + namespace Internal + { + [Flags] + public enum ColumnAttrs : byte + { + UnSet = 0b0000, + Indexed = 0b0001, + AutoInc = 0b0010, + Unique = Indexed | 0b0100, + Identity = Unique | AutoInc, + PrimaryKey = Unique | 0b1000, + PrimaryKeyAuto = PrimaryKey | AutoInc, + } - // A legacy alias, originally defined as `PrimaryKey | Identity` which is numerically same as above. - PrimaryKeyIdentity = PrimaryKeyAuto, -} + [AttributeUsage(AttributeTargets.Field)] + public abstract class ColumnAttribute : Attribute + { + public string? Table { get; init; } + internal abstract ColumnAttrs Mask { get; } + } + } -/// -/// Registers a type as the row structure of a SpacetimeDB table, enabling codegen for it. -/// -/// -/// Multiple [Table] attributes per type are supported. This is useful to reuse row types. -/// Each attribute instance must have a unique name and will create a SpacetimeDB table. -/// -/// -[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true)] -public sealed class TableAttribute : Attribute -{ /// - /// This identifier is used to name the SpacetimeDB table on the host as well as the - /// table handle structures generated to access the table from within a reducer call. + /// Registers a type as the row structure of a SpacetimeDB table, enabling codegen for it. /// - /// Defaults to the nameof of the target type. + /// + /// Multiple [Table] attributes per type are supported. This is useful to reuse row types. + /// Each attribute instance must have a unique name and will create a SpacetimeDB table. + /// /// - public string? Name { get; init; } + [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true)] + public sealed class TableAttribute : Attribute + { + /// + /// This identifier is used to name the SpacetimeDB table on the host as well as the + /// table handle structures generated to access the table from within a reducer call. + /// + /// Defaults to the nameof of the target type. + /// + public string? Name { get; init; } - /// - /// Set to true to make the table visible to everyone. - /// - /// Defaults to the table only being visible to its owner. - /// - public bool Public { get; init; } = false; + /// + /// Set to true to make the table visible to everyone. + /// + /// Defaults to the table only being visible to its owner. + /// + public bool Public { get; init; } = false; - public string? Scheduled { get; init; } -} + public string? Scheduled { get; init; } + } -[AttributeUsage(AttributeTargets.Field)] -public abstract class ColumnAttribute : Attribute -{ - public string? Table { get; init; } -} - -public sealed class AutoIncAttribute : ColumnAttribute { } + public sealed class AutoIncAttribute : Internal.ColumnAttribute + { + internal override Internal.ColumnAttrs Mask => Internal.ColumnAttrs.AutoInc; + } -public sealed class PrimaryKeyAttribute : ColumnAttribute { } + public sealed class PrimaryKeyAttribute : Internal.ColumnAttribute + { + internal override Internal.ColumnAttrs Mask => Internal.ColumnAttrs.PrimaryKey; + } -public sealed class UniqueAttribute : ColumnAttribute { } + public sealed class UniqueAttribute : Internal.ColumnAttribute + { + internal override Internal.ColumnAttrs Mask => Internal.ColumnAttrs.Unique; + } -public sealed class IndexedAttribute : ColumnAttribute { } + public sealed class IndexedAttribute : Internal.ColumnAttribute + { + internal override Internal.ColumnAttrs Mask => Internal.ColumnAttrs.Indexed; + } -public static class ReducerKind -{ - public const string Init = "__init__"; - public const string Update = "__update__"; - public const string Connect = "__identity_connected__"; - public const string Disconnect = "__identity_disconnected__"; -} + public static class ReducerKind + { + public const string Init = "__init__"; + public const string Update = "__update__"; + public const string Connect = "__identity_connected__"; + public const string Disconnect = "__identity_disconnected__"; + } -[AttributeUsage(AttributeTargets.Method, Inherited = false)] -public sealed class ReducerAttribute(string? name = null) : Attribute -{ - public string? Name => name; + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class ReducerAttribute(string? name = null) : Attribute + { + public string? Name => name; + } }