Skip to content

Commit 742680f

Browse files
committed
JSON: refactor metadata about types and member to use Flags instead a set of booleans
- user a single Flags enum to store most boolean properties on the type itself - add a "SoruceGenerated" flag to detect dynamic vs generated types
1 parent da634ec commit 742680f

File tree

6 files changed

+130
-92
lines changed

6 files changed

+130
-92
lines changed

Doxense.Core/Serialization/JSON/CrystalJsonParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ internal static bool TryParseMicrosoftDateTime(ReadOnlySpan<char> value, out Dat
442442
throw JsonBindingException.CannotDeserializeCustomTypeNoTypeDefinition(data, type);
443443
}
444444

445-
if (typeDef.IsPolymorphic())
445+
if (typeDef.IsPolymorphic)
446446
{
447447
var discriminator = data[typeDef.TypeDiscriminatorProperty?.Value ?? "$type"];
448448

@@ -506,7 +506,7 @@ internal static bool TryParseMicrosoftDateTime(ReadOnlySpan<char> value, out Dat
506506
foreach (var member in typeDef.Members)
507507
{
508508
// skip readonly members
509-
if (member.ReadOnly) continue;
509+
if (member.IsReadOnly) continue;
510510

511511
// do we have a value for this field?
512512
if (!data.TryGetValue(member.Name, out var child)

Doxense.Core/Serialization/JSON/CrystalJsonSettings.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
namespace Doxense.Serialization.Json
2828
{
29+
using System.Collections.Frozen;
2930
using System.Text;
3031
using Doxense.Collections.Caching;
3132

@@ -740,7 +741,7 @@ static CrystalJsonSettings()
740741
var s2 = new CrystalJsonSettings(s.Flags | OptionFlags.EnumsAsString);
741742
defaults[(int) s2.Flags] = s2;
742743
}
743-
Cached = new(defaults, valueFactory: (v) => new((OptionFlags) v));
744+
Cached = new(defaults.ToFrozenDictionary(), valueFactory: (v) => new((OptionFlags) v));
744745
}
745746

746747
internal static CrystalJsonSettings Create(OptionFlags flags) => Cached.GetOrAdd((int) flags);

Doxense.Core/Serialization/JSON/Reflection/CrystalJsonMemberDefinition.cs

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,18 @@
2626

2727
namespace Doxense.Serialization.Json
2828
{
29-
using System.Reflection;
29+
[Flags]
30+
public enum CrystalJsonMemberFlags
31+
{
32+
None = 0,
33+
SourceGenerated = 1 << 0,
34+
NotNull = 1 << 1,
35+
NonZeroDefault = 1 << 2,
36+
ReadOnly = 1 << 3,
37+
InitOnly = 1 << 4,
38+
Required = 1 << 5,
39+
Key = 1 << 6,
40+
}
3041

3142
/// <summary>Structure that holds the cached serialization metadata for a field or property of a class or struct</summary>
3243
[DebuggerDisplay("Name={Name}, Type={Type}")]
@@ -44,22 +55,22 @@ public sealed record CrystalJsonMemberDefinition
4455
/// <remarks>Represent the original name in the c# code, while <see cref="Name"/> is the name in the JSON object</remarks>
4556
public required string OriginalName { get; init; }
4657

58+
/// <summary>Flags for this member</summary>
59+
/// <returns>The flags are used by the various helper properties like <see cref="IsReadOnly"/>, <see cref="IsReadOnly"/> and so on</returns>
60+
public required CrystalJsonMemberFlags Flags { get; init; }
61+
4762
/// <summary>Optional <see cref="JsonPropertyAttribute"/> attribute that was applied to this member</summary>
4863
public JsonPropertyAttribute? Attributes { get; init; }
49-
50-
/// <summary>Original <see cref="PropertyInfo"/> or <see cref="FieldInfo"/> of the member in its declaring type</summary>
51-
public required MemberInfo Member { get; init; }
64+
//REVIEW: this is only used by the writer to "remember" how to format enums inside a Dictionary (attribute set on the property that holds the dictionary)
65+
//TODO: get rid of this, and expose the settings directly on this type?
5266

5367
/// <summary>If <see langword="true"/>, the field has a <see cref="DefaultValue"/> that is not the default for this type.</summary>
5468
/// <remarks>This is <see langword="false"/> if the default is <see langword="null"/>, <see langword="false"/>, <see langword="0"/>, etc...</remarks>
55-
public bool HasDefaultValue { get; init; }
69+
public bool HasDefaultValue => this.Flags.HasFlag(CrystalJsonMemberFlags.NonZeroDefault);
5670

5771
/// <summary>Default value for this member (when it is missing)</summary>
5872
public object? DefaultValue { get; init; }
5973

60-
/// <summary>Flag set to <see langword="true"/> when the member is read-only or init-only</summary>
61-
public bool ReadOnly { get; init; }
62-
6374
/// <summary>Func that can return the value of this member in an instance of the containing type</summary>
6475
public required Func<object, object?> Getter { get; init; }
6576

@@ -86,34 +97,8 @@ public sealed record CrystalJsonMemberDefinition
8697
/// </code></remarks>
8798
public Type? NullableOfType { get; init; }
8899

89-
/// <summary>The member cannot be null, or is annotated with <see cref="System.Diagnostics.CodeAnalysis.NotNullAttribute"/></summary>
90-
/// <remarks>Examples: <code>
91-
/// int Foo { get; ... } // IsNotNull == true
92-
/// int? Foo { get; ... } // IsNotNull == false
93-
/// string Foo { get; ... } // IsNotNull == true
94-
/// string? Foo { get; ... } // IsNotNull == false
95-
/// </code></remarks>
96-
public bool IsNotNull { get; init; }
97-
98-
/// <summary>The member if a reference type that is declared as nullable in its parent type</summary>
99-
/// <remarks>Examples: <code>
100-
/// int Foo { get; ... } // IsNullableRefType == false
101-
/// int? Foo { get; ... } // IsNullableRefType == false
102-
/// string Foo { get; ... } // IsNullableRefType == false
103-
/// string? Foo { get; ... } // IsNullableRefType == true
104-
/// </code></remarks>
105-
public bool IsNullableRefType => !this.IsNotNull && this.NullableOfType == null;
106-
107-
/// <summary>The member if a nullable value type</summary>
108-
/// <remarks>Examples: <code>
109-
/// int Foo { get; ... } // IsNullableValueType == false
110-
/// int? Foo { get; ... } // IsNullableValueType == true
111-
/// string Foo { get; ... } // IsNullableValueType == false
112-
/// string? Foo { get; ... } // IsNullableValueType == false
113-
/// </code></remarks>
114-
public bool IsNullableValueType => this.NullableOfType != null;
115-
116-
public bool IsNonNullableValueType => this.NullableOfType == null && this.Type.IsValueType;
100+
/// <summary>Flag set to <see langword="true"/> when the member is read-only or init-only</summary>
101+
public bool IsReadOnly => this.Flags.HasFlag(CrystalJsonMemberFlags.ReadOnly);
117102

118103
/// <summary>The member has the required keyword and cannot be null</summary>
119104
/// <remarks>Examples: <code>
@@ -124,7 +109,7 @@ public sealed record CrystalJsonMemberDefinition
124109
/// required int Foo { get; ... } // IsRequired == true
125110
/// required string Foo { get; ... } // IsRequired == true
126111
/// </code></remarks>
127-
public bool IsRequired { get; init; }
112+
public bool IsRequired => this.Flags.HasFlag(CrystalJsonMemberFlags.Required);
128113

129114
/// <summary>The member has the <see cref="System.ComponentModel.DataAnnotations.KeyAttribute"/> attribute</summary>
130115
/// <remarks>Examples: <code>
@@ -133,7 +118,7 @@ public sealed record CrystalJsonMemberDefinition
133118
/// [Key]
134119
/// int Id { get; ... } // IsKey == true
135120
/// </code></remarks>
136-
public bool IsKey { get; init; }
121+
public bool IsKey => this.Flags.HasFlag(CrystalJsonMemberFlags.Key);
137122

138123
/// <summary>The member is a read-only field, or a property with an init-only setter</summary>
139124
/// <remarks>Examples: <code>
@@ -144,7 +129,36 @@ public sealed record CrystalJsonMemberDefinition
144129
/// readonly int Id; // IsInitOnly == true
145130
/// int Id { get; init; } // IsInitOnly == true
146131
/// </code></remarks>
147-
public bool IsInitOnly { get; init; }
132+
public bool IsInitOnly => this.Flags.HasFlag(CrystalJsonMemberFlags.InitOnly);
133+
134+
/// <summary>The member cannot be null, or is annotated with <see cref="System.Diagnostics.CodeAnalysis.NotNullAttribute"/></summary>
135+
/// <remarks>Examples: <code>
136+
/// int Foo { get; ... } // IsNotNull == true
137+
/// int? Foo { get; ... } // IsNotNull == false
138+
/// string Foo { get; ... } // IsNotNull == true
139+
/// string? Foo { get; ... } // IsNotNull == false
140+
/// </code></remarks>
141+
public bool IsNotNull => this.Flags.HasFlag(CrystalJsonMemberFlags.NotNull);
142+
143+
/// <summary>The member if a reference type that is declared as nullable in its parent type</summary>
144+
/// <remarks>Examples: <code>
145+
/// int Foo { get; ... } // IsNullableRefType == false
146+
/// int? Foo { get; ... } // IsNullableRefType == false
147+
/// string Foo { get; ... } // IsNullableRefType == false
148+
/// string? Foo { get; ... } // IsNullableRefType == true
149+
/// </code></remarks>
150+
public bool IsNullableRefType => !this.IsNotNull && this.NullableOfType == null;
151+
152+
/// <summary>The member if a nullable value type</summary>
153+
/// <remarks>Examples: <code>
154+
/// int Foo { get; ... } // IsNullableValueType == false
155+
/// int? Foo { get; ... } // IsNullableValueType == true
156+
/// string Foo { get; ... } // IsNullableValueType == false
157+
/// string? Foo { get; ... } // IsNullableValueType == false
158+
/// </code></remarks>
159+
public bool IsNullableValueType => this.NullableOfType != null;
160+
161+
public bool IsNonNullableValueType => this.NullableOfType == null && this.Type.IsValueType;
148162

149163
}
150164

Doxense.Core/Serialization/JSON/Reflection/CrystalJsonTypeDefinition.cs

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,28 @@ namespace Doxense.Serialization.Json
2828
{
2929
using System.Collections.Frozen;
3030

31-
[DebuggerDisplay("Type={Type.Name}, ReqClass={RequiresClassAttribute}, IsAnonymousType={IsAnonymousType}, ClassId={ClassId}")]
31+
[Flags]
32+
public enum CrystalJsonTypeFlags
33+
{
34+
None = 0,
35+
SourceGenerated = 1 << 0,
36+
DefaultIsNull = 1 << 1,
37+
Polymorphic = 1 << 2,
38+
NonConstructible = 1 << 3,
39+
Sealed = 1 << 4,
40+
Anonymous = 1 << 5,
41+
}
42+
43+
[DebuggerDisplay("Type={Type.FullName}")]
3244
[PublicAPI]
3345
public sealed record CrystalJsonTypeDefinition
3446
{
3547

3648
/// <summary>Type of the object</summary>
3749
public Type Type { get; init; }
3850

51+
public CrystalJsonTypeFlags Flags { get; init; }
52+
3953
public Type? BaseType { get; init; }
4054

4155
public JsonEncodedPropertyName? TypeDiscriminatorProperty { get; init; }
@@ -46,18 +60,6 @@ public sealed record CrystalJsonTypeDefinition
4660
/// <summary>Custom ClassId for this type</summary>
4761
public FrozenDictionary<JsonValue, Type>? DerivedTypeMap { get; init; }
4862

49-
/// <summary>Specifies if this is an anonymous type that does not have a valid name</summary>
50-
/// <remarks>ex: <c>CrystalJson.Serialize(new { "Hello": "World" })</c></remarks>
51-
public bool IsAnonymousType { get; init; }
52-
53-
/// <summary>Specifies if the type cannot possibly be derived (sealed class, or value type)</summary>
54-
/// <remarks>If <see langword="false"/>, a type-check will have to be performed during serialization, which add some overhead</remarks>
55-
public bool IsSealed { get; init; }
56-
57-
/// <summary>Types with a default value that is <see langword="null"/></summary>
58-
/// <remarks><see langword="true"/> for Reference Types, or for <see cref="Nullable{T}"/>.</remarks>
59-
public bool DefaultIsNull { get; init; }
60-
6163
/// <summary>Specifies if the type can have a value of <see langword="null"/> (ref types, or <see cref="Nullable{T}"/>)</summary>
6264
public Type? NullableOfType { get; init; }
6365

@@ -70,30 +72,48 @@ public sealed record CrystalJsonTypeDefinition
7072
/// <summary>Definitions of the fields and properties of this type</summary>
7173
public CrystalJsonMemberDefinition[] Members { get; init; }
7274

73-
public CrystalJsonTypeDefinition(Type type, CrystalJsonTypeBinder? customBinder, Func<object>? generator, CrystalJsonMemberDefinition[] members, Type? baseType, JsonEncodedPropertyName? typeDiscriminatorProperty, JsonValue? typeDiscriminatorValue, FrozenDictionary<JsonValue, Type>? derivedTypeMap)
75+
public CrystalJsonTypeDefinition(Type type, CrystalJsonTypeFlags flags, CrystalJsonTypeBinder? customBinder, Func<object>? generator, CrystalJsonMemberDefinition[] members, Type? baseType, JsonEncodedPropertyName? typeDiscriminatorProperty, JsonValue? typeDiscriminatorValue, IReadOnlyDictionary<JsonValue, Type>? derivedTypeMap)
7476
{
7577
Contract.NotNull(type);
7678
Contract.NotNull(members);
7779

78-
//// If not provided, generate a type name that looks like "Namespace.ClassName, AssemblyName", which is the format expected by Type.GetType(..)
79-
//classId ??= type.GetAssemblyName();
80+
var nullableOfType = CrystalJsonTypeResolver.GetNullableType(type);
81+
82+
if (type.IsAnonymousType()) flags |= CrystalJsonTypeFlags.Anonymous;
83+
if (type.IsSealed) flags |= CrystalJsonTypeFlags.Sealed;
84+
if (!type.IsValueType || nullableOfType != null) flags |= CrystalJsonTypeFlags.Sealed;
85+
if (type.IsInterface || type.IsAbstract) flags |= CrystalJsonTypeFlags.NonConstructible;
86+
if (typeDiscriminatorProperty != null) flags |= CrystalJsonTypeFlags.Polymorphic;
8087

8188
this.Type = type;
89+
this.Flags = flags;
8290
this.BaseType = baseType;
8391
this.TypeDiscriminatorProperty = typeDiscriminatorProperty;
8492
this.TypeDiscriminatorValue = typeDiscriminatorValue;
85-
this.DerivedTypeMap = derivedTypeMap;
86-
this.IsAnonymousType = type.IsAnonymousType();
87-
this.IsSealed = CrystalJsonTypeResolver.IsSealedType(type);
88-
this.NullableOfType = CrystalJsonTypeResolver.GetNullableType(type);
89-
this.DefaultIsNull = !type.IsValueType || this.NullableOfType != null;
90-
//this.RequiresClassAttribute = (type.IsInterface || type.IsAbstract) && !this.IsAnonymousType;
93+
this.DerivedTypeMap = derivedTypeMap?.ToFrozenDictionary();
94+
this.NullableOfType = nullableOfType;
9195
this.CustomBinder = customBinder;
9296
this.Generator = generator;
9397
this.Members = members;
9498
}
9599

96-
public bool IsPolymorphic() => this.TypeDiscriminatorProperty != null;
100+
public bool IsPolymorphic => this.Flags.HasFlag(CrystalJsonTypeFlags.Polymorphic);
101+
102+
/// <summary>Specifies if this is an anonymous type that does not have a valid name</summary>
103+
/// <remarks>ex: <c>CrystalJson.Serialize(new { "Hello": "World" })</c></remarks>
104+
public bool IsAnonymousType => this.Flags.HasFlag(CrystalJsonTypeFlags.Anonymous);
105+
106+
/// <summary>Specifies if the type cannot possibly be derived (sealed class, or value type)</summary>
107+
/// <remarks>If <see langword="false"/>, a type-check will have to be performed during serialization, which add some overhead</remarks>
108+
public bool IsSealed => this.Flags.HasFlag(CrystalJsonTypeFlags.Sealed);
109+
110+
/// <summary>Types with a default value that is <see langword="null"/></summary>
111+
/// <remarks><see langword="true"/> for Reference Types, or for <see cref="Nullable{T}"/>.</remarks>
112+
public bool DefaultIsNull => this.Flags.HasFlag(CrystalJsonTypeFlags.DefaultIsNull);
113+
114+
/// <summary>Specifies if the type cannot be constructed directly (interface, abstract class, no public ctor, ...)</summary>
115+
/// <remarks><see langword="true"/> for interfaces or abstract types</remarks>
116+
public bool IsNonConstructible => this.Flags.HasFlag(CrystalJsonTypeFlags.NonConstructible);
97117

98118
}
99119

0 commit comments

Comments
 (0)