Skip to content

Commit bc0ea0b

Browse files
committed
feat: #622 add ability to generate arbitrary metadata into metadata.g.ts
1 parent aa25953 commit bc0ea0b

File tree

26 files changed

+571
-29
lines changed

26 files changed

+571
-29
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
# 6.3.1
1+
# 6.4.0
22
- Added `app.UseAppVersionHeader()` middleware and `<CAppUpdateAlert>` component for detecting and notifying users when a new version of the application has been deployed.
33
- Assorted template improvements
4+
- Added `[attribute: CoalesceMetadata<TAttribute>]`, an assembly-level attribute that causes any occurrences of the target attribute and its value to be generated into the TypeScript metadata.
45

56
# 6.3.0
67
- Added Vuetify 4 compatibility to coalesce-vue-vuetify3.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
# [CoalesceMetadata]
3+
4+
`IntelliTect.Coalesce.DataAnnotations.CoalesceMetadataAttribute`
5+
6+
Placed on an assembly, specifies an attribute type whose values should be extracted from any Coalesce-generated types, properties, methods, and parameters, and emitted into the generated TypeScript metadata.
7+
8+
The extracted attribute values are available in the `attributes` property of the corresponding metadata object in `metadata.g.ts`.
9+
10+
## Example Usage
11+
12+
Register the attribute type at the assembly level in your data project (e.g. in any `.cs` file):
13+
14+
```c#
15+
[assembly: CoalesceMetadata<System.ComponentModel.CategoryAttribute>]
16+
```
17+
18+
Then use the attribute on your models:
19+
20+
```c#
21+
[Category("people")]
22+
public class Person
23+
{
24+
[Category("contact")]
25+
public string? Email { get; set; }
26+
27+
[Category("contact")]
28+
public string? Phone { get; set; }
29+
30+
[return: Category("action")]
31+
public ItemResult ChangeSpaces(string newSpaces) { ... }
32+
}
33+
```
34+
35+
## Properties
36+
37+
<Prop def="Type AttributeType" />
38+
39+
The attribute type whose values should be extracted and emitted into metadata. All constructor arguments and explicitly-set named properties on the attribute will be included.
40+
41+
<Prop def="string? Key" />
42+
43+
The key name to use in the generated metadata. If not specified, the attribute type name is used with the `Attribute` suffix removed and camelCased (e.g. `FeatureFlagAttribute``featureFlag`).
44+
45+
## Output Format
46+
47+
- **Parameterless attributes**: Emitted as an empty object `{}`.
48+
- **All other attributes**: Emitted as an object with camelCased property names.
49+
50+
Supported value types include strings, numbers, booleans, enums (emitted as their string representation), and arrays of these types.
51+
52+
## TypeScript Type
53+
54+
The `attributes` property is available on all metadata interfaces:
55+
56+
```ts
57+
readonly attributes?: {
58+
readonly [key: string]: { readonly [key: string]: unknown }
59+
}
60+
```

playground/Coalesce.Domain/AppDbContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using IntelliTect.Coalesce;
22
using IntelliTect.Coalesce.AuditLogging;
3+
using IntelliTect.Coalesce.DataAnnotations;
34
using Microsoft.EntityFrameworkCore;
45
using System;
56
using System.Linq;
67
using System.Security.Cryptography;
78

89
// [assembly: CoalesceConfiguration(NoAutoInclude = true)]
10+
[assembly: CoalesceMetadata<Coalesce.Domain.FeatureFlagAttribute>]
911

1012
namespace Coalesce.Domain;
1113

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
namespace Coalesce.Domain;
4+
5+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Method)]
6+
public class FeatureFlagAttribute : Attribute
7+
{
8+
public string Flag { get; }
9+
10+
public FeatureFlagAttribute(string flag)
11+
{
12+
Flag = flag;
13+
}
14+
}

playground/Coalesce.Domain/Person.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public enum Titles
7070
/// <summary>
7171
/// Email address of the person
7272
/// </summary>
73+
[FeatureFlag("new-email-validation")]
7374
[ClientValidation(IsEmail = true)]
7475
[DataType(DataType.EmailAddress)]
7576
[EmailAddress]

playground/Coalesce.Web.Vue3/src/metadata.g.ts

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/IntelliTect.Coalesce.CodeGeneration.Vue/Generators/Scripts/TsMetadata.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ private void WriteCommonClassMetadata(TypeScriptCodeBuilder b, ClassViewModel mo
137137
}
138138
b.Line("]},");
139139
}
140+
WriteCustomMetadata(b, model);
140141
}
141142

142143
private void WriteExternalTypeMetadata(TypeScriptCodeBuilder b, ClassViewModel model)
@@ -198,6 +199,8 @@ private void WriteEnumMetadata(TypeScriptCodeBuilder b, TypeViewModel model)
198199
}
199200
}
200201
b.Line("]),");
202+
203+
WriteCustomMetadata(b, model);
201204
}
202205
}
203206

@@ -548,6 +551,8 @@ private void WriteClassMethodMetadata(TypeScriptCodeBuilder b, ClassViewModel mo
548551
b.StringProp("transportType", method.TransportType.ToString().Replace("Result", "").ToLower());
549552
b.StringProp("httpMethod", method.ApiActionHttpMethod.ToString().ToUpperInvariant());
550553

554+
WriteCustomMetadata(b, method);
555+
551556

552557
int hiddenAreaFlags = (int)method.HiddenAreas;
553558
if (hiddenAreaFlags != 0)
@@ -666,6 +671,7 @@ private void WriteValueCommonMetadata(TypeScriptCodeBuilder b, ValueViewModel va
666671
}
667672

668673
WriteTypeCommonMetadata(b, value.Type, value);
674+
WriteCustomMetadata(b, value);
669675
}
670676

671677
/// <summary>
@@ -788,4 +794,39 @@ private void WriteTypeCommonMetadata(TypeScriptCodeBuilder b, TypeViewModel type
788794
break;
789795
}
790796
}
797+
798+
private void WriteCustomMetadata(TypeScriptCodeBuilder b, IAttributeProvider provider)
799+
{
800+
var entries = provider.GetCustomMetadata();
801+
if (!entries.Any()) return;
802+
803+
using (b.Block("attributes:", ','))
804+
{
805+
foreach (var entry in entries)
806+
{
807+
using (b.Block($"{entry.Key}:", ','))
808+
{
809+
foreach (var (propName, propValue) in entry.Properties)
810+
{
811+
b.Prop(propName.ToCamelCase(), ValueToJsLiteral(propValue));
812+
}
813+
}
814+
}
815+
}
816+
}
817+
818+
private static string ValueToJsLiteral(object value) => value switch
819+
{
820+
null => "null",
821+
true => "true",
822+
false => "false",
823+
string s => $"\"{s.EscapeStringLiteralForTypeScript()}\"",
824+
int or long or short or byte or sbyte or uint or ulong or ushort => value.ToString(),
825+
float f => f.ToString(System.Globalization.CultureInfo.InvariantCulture),
826+
double d => d.ToString(System.Globalization.CultureInfo.InvariantCulture),
827+
decimal m => m.ToString(System.Globalization.CultureInfo.InvariantCulture),
828+
TypeViewModel tv => $"\"{tv.FullyQualifiedName.EscapeStringLiteralForTypeScript()}\"",
829+
object[] arr => $"[{string.Join(", ", arr.Select(ValueToJsLiteral))}]",
830+
_ => $"\"{value.ToString().EscapeStringLiteralForTypeScript()}\""
831+
};
791832
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using IntelliTect.Coalesce.DataAnnotations;
2+
using System;
3+
using System.ComponentModel.DataAnnotations;
4+
5+
[assembly: CoalesceMetadata<IntelliTect.Coalesce.Testing.TargetClasses.CustomMetadataTargetAttribute>]
6+
[assembly: CoalesceMetadata<IntelliTect.Coalesce.Testing.TargetClasses.CustomMetadataMarkerAttribute>]
7+
8+
namespace IntelliTect.Coalesce.Testing.TargetClasses;
9+
10+
[AttributeUsage(AttributeTargets.All)]
11+
public class CustomMetadataTargetAttribute : Attribute
12+
{
13+
public string Name { get; }
14+
public int Value { get; set; }
15+
16+
public CustomMetadataTargetAttribute(string name)
17+
{
18+
Name = name;
19+
}
20+
}
21+
22+
[AttributeUsage(AttributeTargets.All)]
23+
public class CustomMetadataMarkerAttribute : Attribute { }

src/IntelliTect.Coalesce.Testing/TargetClasses/TestDbContext/ComplexModel.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
namespace IntelliTect.Coalesce.Testing.TargetClasses.TestDbContext;
1717

1818
[SemanticKernel("ComplexModel", DeleteEnabled = true, SaveEnabled = true)]
19+
[CustomMetadataTarget("ClassLevel", Value = 42)]
1920
public class ComplexModel
2021
{
2122
[Coalesce]
@@ -54,6 +55,8 @@ public class ComplexModel
5455
public EnumPk EnumPk { get; set; }
5556

5657
[Search]
58+
[CustomMetadataTarget("PropLevel")]
59+
[CustomMetadataMarker]
5760
public DateTimeOffset DateTimeOffset { get; set; }
5861

5962
public DateTimeOffset? DateTimeOffsetNullable { get; set; }
@@ -190,11 +193,12 @@ This is a second line in the string.
190193
// Add other kinds of properties, relationships, etc... as needed.
191194

192195
[Coalesce, Execute]
196+
[CustomMetadataTarget("MethodLevel", Value = 99)]
193197
public ExternalParent MethodWithManyParams(
194198
ExternalParent singleExternal,
195199
ICollection<ExternalParent> collectionExternal,
196200
IFile file,
197-
string strParam,
201+
[CustomMetadataTarget("ParamLevel")] string strParam,
198202
string[] stringsParam,
199203
DateTime dateTime,
200204
int integer,
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using IntelliTect.Coalesce.Testing.TargetClasses.TestDbContext;
2+
using IntelliTect.Coalesce.Testing.Util;
3+
using IntelliTect.Coalesce.TypeDefinition;
4+
5+
namespace IntelliTect.Coalesce.Tests.TypeDefinition;
6+
7+
public class CustomMetadataProviderTests
8+
{
9+
[Test]
10+
[ClassViewModelData(typeof(ComplexModel))]
11+
public async Task ClassLevel_ExtractsConstructorAndNamedArgs(ClassViewModelData data)
12+
{
13+
ClassViewModel vm = data;
14+
var results = vm.GetCustomMetadata().ToList();
15+
16+
await Assert.That(results).Count().IsEqualTo(1);
17+
await Assert.That(results[0].Key).IsEqualTo("customMetadataTarget");
18+
await Assert.That(results[0].Properties.Any(kv =>
19+
string.Equals(kv.Key, "name", StringComparison.OrdinalIgnoreCase)
20+
&& (string)kv.Value! == "ClassLevel")).IsTrue();
21+
await Assert.That(results[0].Properties.Any(kv =>
22+
string.Equals(kv.Key, "value", StringComparison.OrdinalIgnoreCase)
23+
&& kv.Value is int and 42)).IsTrue();
24+
}
25+
26+
[Test]
27+
[ClassViewModelData(typeof(ComplexModel))]
28+
public async Task PropertyLevel_ExtractsMultipleAttributes(ClassViewModelData data)
29+
{
30+
ClassViewModel vm = data;
31+
var results = vm.PropertyByName(nameof(ComplexModel.DateTimeOffset))!
32+
.GetCustomMetadata().ToList();
33+
34+
await Assert.That(results).Count().IsEqualTo(2);
35+
36+
var target = results.Single(r => r.Key == "customMetadataTarget");
37+
await Assert.That(target.Properties.Any(kv =>
38+
string.Equals(kv.Key, "name", StringComparison.OrdinalIgnoreCase)
39+
&& (string)kv.Value! == "PropLevel")).IsTrue();
40+
41+
var marker = results.Single(r => r.Key == "customMetadataMarker");
42+
await Assert.That(marker.Properties).IsEmpty();
43+
}
44+
45+
[Test]
46+
[ClassViewModelData(typeof(ComplexModel))]
47+
public async Task MethodLevel_ExtractsAttributes(ClassViewModelData data)
48+
{
49+
ClassViewModel vm = data;
50+
var results = vm.MethodByName(nameof(ComplexModel.MethodWithManyParams))!
51+
.GetCustomMetadata().ToList();
52+
53+
await Assert.That(results).Count().IsEqualTo(1);
54+
await Assert.That(results[0].Key).IsEqualTo("customMetadataTarget");
55+
await Assert.That(results[0].Properties.Any(kv =>
56+
string.Equals(kv.Key, "name", StringComparison.OrdinalIgnoreCase)
57+
&& (string)kv.Value! == "MethodLevel")).IsTrue();
58+
await Assert.That(results[0].Properties.Any(kv =>
59+
string.Equals(kv.Key, "value", StringComparison.OrdinalIgnoreCase)
60+
&& kv.Value is int and 99)).IsTrue();
61+
}
62+
63+
[Test]
64+
[ClassViewModelData(typeof(ComplexModel))]
65+
public async Task ParameterLevel_ExtractsAttributes(ClassViewModelData data)
66+
{
67+
ClassViewModel vm = data;
68+
var results = vm.MethodByName(nameof(ComplexModel.MethodWithManyParams))!
69+
.Parameters.Single(p => p.Name == "strParam")
70+
.GetCustomMetadata().ToList();
71+
72+
await Assert.That(results).Count().IsEqualTo(1);
73+
await Assert.That(results[0].Key).IsEqualTo("customMetadataTarget");
74+
await Assert.That(results[0].Properties.Any(kv =>
75+
string.Equals(kv.Key, "name", StringComparison.OrdinalIgnoreCase)
76+
&& (string)kv.Value! == "ParamLevel")).IsTrue();
77+
}
78+
}

0 commit comments

Comments
 (0)