Skip to content

Commit

Permalink
feat: #235 support const fields
Browse files Browse the repository at this point in the history
  • Loading branch information
ascott18 committed Feb 18, 2025
1 parent 35bb4a1 commit 5da9ff1
Show file tree
Hide file tree
Showing 18 changed files with 117 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Added `$search`, `$filter`, and `$orderBy` shorthands to ListViewModel.
- Coalesce's Vite middleware (`UseViteDevelopmentServer`) now checks if your installed NPM packages match what's defined in package.json and package-lock.json, presenting an in-browser warning if they do not. This helps avoid forgetting to reinstall packages after pulling down changes in multi-developer projects.
- Added a `filter` prop to `c-input` for enum inputs to restrict the values available for selection.
- Const fields in C#, if annotated with `[Coalesce]`, are now emitted into generated TypeScript.

# 5.3.3

Expand Down
2 changes: 2 additions & 0 deletions docs/modeling/model-components/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ If such a property is defined as an auto-property, the `[NotMapped]` attribute s
### Init-only Properties
Properties on [CRUD Models](/modeling/model-types/crud.md) that use an `init` accessor rather than a `set` accessor will be implicitly treated as required, and can also only have a value provided when the entity is created for the first time. Any values provided during save actions for init-only properties when updating an existing entity will be ignored.

### Const Fields
Const fields declared on your models, services, and data sources, if annotated with `[Coalesce]`, will be emitted into the generated TypeScript Models and ViewModels. For example, `[Coalesce] public const int MagicNumber = 42;`.


## Property Customization
Expand Down
9 changes: 9 additions & 0 deletions playground/Coalesce.Domain/Case.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ public enum Statuses

#nullable disable

[Coalesce]
public const int MagicNumber = 42;
[Coalesce]
public const string MagicString = "42";
[Coalesce]
public const Statuses MagicEnum = Statuses.ClosedNoSolution;

public const int MagicUnexposedNumber = 42;

/// <summary>
/// The Primary key for the Case object
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions playground/Coalesce.Web.Vue3/src/models.g.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions playground/Coalesce.Web.Vue3/src/viewmodels.g.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using IntelliTect.Coalesce.CodeGeneration.Generation;
using IntelliTect.Coalesce.CodeGeneration.Utilities;
using IntelliTect.Coalesce.DataAnnotations;
using IntelliTect.Coalesce.TypeDefinition;
using IntelliTect.Coalesce.TypeDefinition.Enums;
Expand Down Expand Up @@ -318,33 +319,7 @@ TypeDiscriminator.Enum or
var defaultValue = prop.DefaultValue;
if (defaultValue is not null)
{
if (
prop.Type.TsTypeKind is TypeDiscriminator.String &&
defaultValue is string stringValue
)
{
b.StringProp("defaultValue", stringValue);
}
else if (
prop.Type.TsTypeKind is TypeDiscriminator.Boolean &&
defaultValue is true or false
)
{
b.Prop("defaultValue", defaultValue.ToString().ToLowerInvariant());
}
else if (
prop.Type.TsTypeKind is TypeDiscriminator.Number or TypeDiscriminator.Enum &&
double.TryParse(defaultValue.ToString(), out _)
)
{
b.Prop("defaultValue", defaultValue.ToString());
}
else
{
throw new InvalidOperationException(
$"Default value {defaultValue} does not match property type {prop.Type}, " +
$"or type does not support default values.");
}
b.Prop("defaultValue", defaultValue.ValueLiteralForTypeScript());
}

List<string> rules = GetValidationRules(prop, (prop.ReferenceNavigationProperty ?? prop).DisplayName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using IntelliTect.Coalesce.CodeGeneration.Generation;
using IntelliTect.Coalesce.CodeGeneration.Utilities;
using IntelliTect.Coalesce.CodeGeneration.Vue.Utils;
using IntelliTect.Coalesce.TypeDefinition;
using IntelliTect.Coalesce.Utilities;
Expand Down Expand Up @@ -55,6 +56,8 @@ public override Task<string> BuildOutputAsync()

using (b.Block($"export class {name}"))
{
WriteConsts(b, model);

b.DocComment($"Mutates the input object and its descendents into a valid {name} implementation.");
using (b.Block($"static convert(data?: Partial<{name}>): {name}"))
{
Expand Down Expand Up @@ -92,6 +95,8 @@ public override Task<string> BuildOutputAsync()
{
b.Line($"readonly $metadata = {sourceMeta}");

WriteConsts(b, source);

if (source.DataSourceParameters.Any())
{
foreach (var param in source.DataSourceParameters)
Expand Down Expand Up @@ -138,5 +143,15 @@ public override Task<string> BuildOutputAsync()

return Task.FromResult(b.ToString());
}

static internal void WriteConsts(TypeScriptCodeBuilder b, ClassViewModel vm)
{
if (vm.ClientConsts.Any()) b.Line();

foreach (var value in vm.ClientConsts)
{
b.Line($"static {value.Name.ToCamelCase()} = {value.ValueLiteralForTypeScript("")}");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ private void WriteViewModel(TypeScriptCodeBuilder b, ClassViewModel model)
b.Line($"static DataSources = {modelName}.DataSources;");
}

WriteConsts(b, model);

foreach (var prop in model.ClientProperties)
{

Expand Down Expand Up @@ -178,6 +180,8 @@ private void WriteListViewModel(TypeScriptCodeBuilder b, ClassViewModel model)
b.Line($"static DataSources = {modelName}.DataSources;");
}

WriteConsts(b, model);

foreach (var method in model.ClientMethods.Where(m => m.IsStatic))
{
WriteMethodCaller(b, method);
Expand All @@ -201,6 +205,8 @@ private static void WriteServiceViewModel(TypeScriptCodeBuilder b, ClassViewMode

using (b.Block($"export class {viewModelName} extends ServiceViewModel<typeof {metadataName}, $apiClients.{name}ApiClient>"))
{
WriteConsts(b, model);

foreach (var method in model.ClientMethods)
{
WriteMethodCaller(b, method);
Expand Down Expand Up @@ -286,5 +292,15 @@ static string PropValue(ParameterViewModel p, string prefix)
}
}
}

static internal void WriteConsts(TypeScriptCodeBuilder b, ClassViewModel vm)
{
if (vm.ClientConsts.Any()) b.Line();

foreach (var value in vm.ClientConsts)
{
b.Line($"static {value.Name.ToCamelCase()} = {value.ValueLiteralForTypeScript("$models.")}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using IntelliTect.Coalesce.TypeDefinition;
using IntelliTect.Coalesce.Utilities;
using System.Collections.Generic;
using System.Linq;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ namespace IntelliTect.Coalesce.Tests.TargetClasses.TestDbContext
{
public class ComplexModel
{
[Coalesce]
public const int MagicNumber = 42;
[Coalesce]
public const string MagicString = "42";
[Coalesce]
public const EnumPkId MagicEnum = EnumPkId.Value10;

[DefaultOrderBy(FieldOrder = 2)]
public int ComplexModelId { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ await query.FindItemAsync(PersonId),
[Coalesce, DefaultDataSource]
public class WithoutCases : StandardDataSource<Person, AppDbContext>
{
[Coalesce]
public const int MagicNumber = 42;

public WithoutCases(CrudContext<AppDbContext> context) : base(context) { }

public override IQueryable<Person> GetQuery(IDataSourceParameters parameters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ namespace IntelliTect.Coalesce.Tests.TargetClasses
[Coalesce, Service]
public interface IWeatherService
{
[Coalesce]
public const int MagicNumber = 42;

Task<WeatherData> GetWeatherAsync(TestDbContext.AppDbContext parameterDbContext, Location location, DateTimeOffset? dateTime, SkyConditions? conditions);

Task<ItemResult<IFile>> FileUploadDownload(IFile file);
Expand Down
4 changes: 2 additions & 2 deletions src/IntelliTect.Coalesce/DataAnnotations/CoalesceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
namespace IntelliTect.Coalesce
{
/// <summary>
/// The targeted class or method should be exposed by Coalesce.
/// The targeted class or member should be exposed by Coalesce.
/// Different types will be exposed in different ways. See documentation for details.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Interface | AttributeTargets.Enum)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Interface | AttributeTargets.Enum | AttributeTargets.Field)]
public sealed class CoalesceAttribute : Attribute
{
/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions src/IntelliTect.Coalesce/TypeDefinition/ClassViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ protected ClassViewModel(TypeViewModel type)
public abstract bool IsStatic { get; }
public abstract bool IsRecord { get; }

public abstract IEnumerable<LiteralViewModel> ClientConsts { get; }

public string FullyQualifiedName => Type.FullyQualifiedName;


Expand Down
26 changes: 26 additions & 0 deletions src/IntelliTect.Coalesce/TypeDefinition/LiteralViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using IntelliTect.Coalesce.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IntelliTect.Coalesce.TypeDefinition
{
public record LiteralViewModel(TypeViewModel Type, object Value, string? Name = null)
{
public string ValueLiteralForTypeScript(string? modelPrefix = null) => Value switch
{
null => "null",
_ => Type.TsTypeKind switch
{
TypeDiscriminator.String => '"' + Value.ToString().EscapeStringLiteralForTypeScript() + '"',
TypeDiscriminator.Boolean => Value.ToString()!.ToLowerInvariant(),
TypeDiscriminator.Enum => modelPrefix != null && Type.EnumValues.First(e => e.Value.Equals(Value)) is EnumMember em
? modelPrefix + Type.ClientTypeName + "." + em.Name
: Value.ToString()!,
_ => Value.ToString()!,
}
};
}
}
6 changes: 4 additions & 2 deletions src/IntelliTect.Coalesce/TypeDefinition/PropertyViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ private protected PropertyViewModel(ClassViewModel effectiveParent, ClassViewMod
/// <summary>
/// Returns whether or not the property may be exposed to the client.
/// </summary>
public bool IsClientProperty => !IsInternalUse && HasGetter && !Type.IsInternalUse;
public bool IsClientProperty => !IsInternalUse && HasGetter && !Type.IsInternalUse && !IsStatic;

public bool PureTypeOnContext => Object?.IsDbMappedType ?? false;

Expand Down Expand Up @@ -241,7 +241,9 @@ propName is null
/// <summary>
/// Returns the default value specified by <see cref="DefaultValueAttribute"/>, if present.
/// </summary>
public object? DefaultValue => this.GetAttributeValue<DefaultValueAttribute>(nameof(DefaultValueAttribute.Value));
public LiteralViewModel? DefaultValue => this.GetAttributeValue<DefaultValueAttribute>(nameof(DefaultValueAttribute.Value)) is { } defaultValue
? new(Type, defaultValue)
: null;

/// <summary>
/// If true, there is an API controller that is serving this type of data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,7 @@ protected override IReadOnlyCollection<PropertyViewModel> RawProperties(ClassVie
.Select(t => ReflectionTypeViewModel.GetOrCreate(ReflectionRepository, t))
.ToList()
.AsReadOnly();

public override IEnumerable<LiteralViewModel> ClientConsts => throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using Microsoft.CodeAnalysis;
using IntelliTect.Coalesce.Helpers;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace IntelliTect.Coalesce.TypeDefinition
{
Expand Down Expand Up @@ -109,5 +110,14 @@ void AddSymbolMethods(INamedTypeSymbol symbol)
.GetTypeMembers()
.Select(t => SymbolTypeViewModel.GetOrCreate(ReflectionRepository, t))
.ToList().AsReadOnly();

public override IEnumerable<LiteralViewModel> ClientConsts => Symbol.GetMembers()
.OfType<IFieldSymbol>()
.Where(m =>
m.IsConst &&
m.HasConstantValue &&
m.GetAttributeProvider().HasAttribute<CoalesceAttribute>()
)
.Select(m => new LiteralViewModel(SymbolTypeViewModel.GetOrCreate(ReflectionRepository, m.Type), m.ConstantValue!, m.Name));
}
}

0 comments on commit 5da9ff1

Please sign in to comment.