Skip to content

Commit

Permalink
Allow customizing json ignore attribute for serialized classes (#3451)
Browse files Browse the repository at this point in the history
Co-authored-by: Ferdinando Papale <[email protected]>
  • Loading branch information
nirinchev and papafe authored Sep 20, 2023
1 parent f19fb72 commit 1fdc207
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 48 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
## vNext (TBD)

### Enhancements
* None
* Added support for customizing the ignore attribute applied on certain generated properties of Realm models. The configuration option is called `realm.custom_ignore_attribute` and can be set in a global configuration file (more information about global configuration files can be found in the [.NET documentation](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files)). The Realm generator will treat this as an opaque string, that will be appended to the `IgnoreDataMember` and `XmlIgnore` attributes already applied on these members. The attributes must be fully qualified unless the namespace they reside in is added to a global usings file. For example, this is how you would add `JsonIgnore` from `System.Text.Json`:

```
realm.custom_ignore_attribute = [System.Text.Json.Serialization.JsonIgnore]
```
(Issue [#2579](https://github.com/realm/realm-dotnet/issues/2579))

### Fixed
* None
Expand Down
33 changes: 23 additions & 10 deletions Realm/Realm.SourceGenerator/ClassCodeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,29 @@ internal class ClassCodeBuilder
};

private readonly ClassInfo _classInfo;
private readonly Lazy<string> _ignoreFieldAttribute;

private readonly string _helperClassName;
private readonly string _accessorInterfaceName;
private readonly string _managedAccessorClassName;
private readonly string _unmanagedAccessorClassName;

public ClassCodeBuilder(ClassInfo classInfo)
public ClassCodeBuilder(ClassInfo classInfo, GeneratorConfig generatorConfig)
{
_classInfo = classInfo;

_ignoreFieldAttribute = new(() =>
{
var result = "[IgnoreDataMember, XmlIgnore]";
var customAttribute = generatorConfig.CustomIgnoreAttribute;
if (!string.IsNullOrEmpty(customAttribute))
{
result += customAttribute;
}
return result;
});

var className = _classInfo.Name;

_helperClassName = $"{className}ObjectHelper";
Expand Down Expand Up @@ -287,36 +300,36 @@ private string GeneratePartialClass(string interfaceString, string managedAccess
private {_accessorInterfaceName} Accessor => _accessor ??= new {_unmanagedAccessorClassName}(typeof({_classInfo.Name}));
/// <inheritdoc />
[IgnoreDataMember, XmlIgnore]
{_ignoreFieldAttribute.Value}
public bool IsManaged => Accessor.IsManaged;
/// <inheritdoc />
[IgnoreDataMember, XmlIgnore]
{_ignoreFieldAttribute.Value}
public bool IsValid => Accessor.IsValid;
/// <inheritdoc />
[IgnoreDataMember, XmlIgnore]
{_ignoreFieldAttribute.Value}
public bool IsFrozen => Accessor.IsFrozen;
/// <inheritdoc />
[IgnoreDataMember, XmlIgnore]
{_ignoreFieldAttribute.Value}
public Realms.Realm? Realm => Accessor.Realm;
/// <inheritdoc />
[IgnoreDataMember, XmlIgnore]
{_ignoreFieldAttribute.Value}
public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!;
/// <inheritdoc />
[IgnoreDataMember, XmlIgnore]
{_ignoreFieldAttribute.Value}
public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi;
/// <inheritdoc />
[IgnoreDataMember, XmlIgnore]
{_ignoreFieldAttribute.Value}
public int BacklinksCount => Accessor.BacklinksCount;
{(_classInfo.ObjectType != ObjectType.EmbeddedObject ? string.Empty :
@"/// <inheritdoc />
[IgnoreDataMember, XmlIgnore]
$@"/// <inheritdoc />
{_ignoreFieldAttribute.Value}
public Realms.IRealmObjectBase? Parent => Accessor.GetParent();")}
void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults)
Expand Down
11 changes: 5 additions & 6 deletions Realm/Realm.SourceGenerator/CodeEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ namespace Realms.SourceGenerator
internal class CodeEmitter
{
private readonly GeneratorExecutionContext _context;
private readonly GeneratorConfig _generatorConfig;

public CodeEmitter(GeneratorExecutionContext context)
public CodeEmitter(GeneratorExecutionContext context, GeneratorConfig generatorConfig)
{
_context = context;
_generatorConfig = generatorConfig;
}

public void Emit(ParsingResults parsingResults)
Expand All @@ -45,7 +47,7 @@ public void Emit(ParsingResults parsingResults)

try
{
var generatedSource = new ClassCodeBuilder(classInfo).GenerateSource();
var generatedSource = new ClassCodeBuilder(classInfo, _generatorConfig).GenerateSource();

// Replace all occurrences of at least 3 newlines with only 2
var formattedSource = Regex.Replace(generatedSource, @$"[{Environment.NewLine}]{{3,}}", $"{Environment.NewLine}{Environment.NewLine}");
Expand All @@ -65,9 +67,6 @@ public void Emit(ParsingResults parsingResults)
}
}

private static bool ShouldEmit(ClassInfo classInfo)
{
return !classInfo.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error);
}
private static bool ShouldEmit(ClassInfo classInfo) => classInfo.Diagnostics.All(d => d.Severity != DiagnosticSeverity.Error);
}
}
10 changes: 10 additions & 0 deletions Realm/Realm.SourceGenerator/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,20 @@ private enum Id
RealmObjectWithoutAutomaticProperty = 25,
ParentOfNestedClassIsNotPartial = 27,
IndexedPrimaryKey = 28,
InvalidGeneratorConfiguration = 1000,
}

#region Errors

public static Diagnostic InvalidConfiguration(string field, string description)
{
return CreateDiagnosticError(
Id.InvalidGeneratorConfiguration,
"Invalid source generator configuration",
$"The generator configuration for {field} is invalid: {description}",
Location.None);
}

public static Diagnostic UnexpectedError(string className, string message, string stackTrace)
{
return CreateDiagnosticError(
Expand Down
24 changes: 16 additions & 8 deletions Realm/Realm.SourceGenerator/DiagnosticsEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,30 @@ namespace Realms.SourceGenerator
{
internal class DiagnosticsEmitter
{
private GeneratorExecutionContext _context;
private readonly GeneratorExecutionContext _context;

public DiagnosticsEmitter(GeneratorExecutionContext context)
public DiagnosticsEmitter(GeneratorExecutionContext context, GeneratorConfig generatorConfig)
{
_context = context;
}

public void Emit(ParsingResults parsingResults)
{
foreach (var classInfo in parsingResults.ClassInfo)
var customIgnoreAttribute = generatorConfig.CustomIgnoreAttribute;
if (!string.IsNullOrEmpty(customIgnoreAttribute))
{
if (!classInfo.Diagnostics.Any())
if (!customIgnoreAttribute!.StartsWith("[") || !customIgnoreAttribute.EndsWith("]"))
{
continue;
_context.ReportDiagnostic(Diagnostics.InvalidConfiguration(
field: "realm.custom_ignore_attribute",
description: $"The attribute(s) string should start with '[' and end with ']'. Actual value: {customIgnoreAttribute}."));

generatorConfig.CustomIgnoreAttribute = null;
}
}
}

public void Emit(ParsingResults parsingResults)
{
foreach (var classInfo in parsingResults.ClassInfo.Where(classInfo => classInfo.Diagnostics.Any()))
{
try
{
SerializeDiagnostics(_context, classInfo);
Expand Down
10 changes: 6 additions & 4 deletions Realm/Realm.SourceGenerator/GeneratorConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,20 @@

namespace Realms.SourceGenerator
{
internal class GeneratorConfig
internal record GeneratorConfig(bool IgnoreObjectsNullability)
{
public bool IgnoreObjectsNullability { get; private set; }
public string? CustomIgnoreAttribute { get; set; }

public static GeneratorConfig ParseConfig(AnalyzerConfigOptions analyzerConfigOptions)
{
analyzerConfigOptions.TryGetValue("realm.ignore_objects_nullability", out var ignoreObjectsNullabilityString);
var ignoreObjectsNullability = ignoreObjectsNullabilityString == "true";

return new GeneratorConfig
analyzerConfigOptions.TryGetValue("realm.custom_ignore_attribute", out var customIgnoreAttribute);

return new GeneratorConfig(ignoreObjectsNullability)
{
IgnoreObjectsNullability = ignoreObjectsNullability
CustomIgnoreAttribute = customIgnoreAttribute
};
}
}
Expand Down
6 changes: 3 additions & 3 deletions Realm/Realm.SourceGenerator/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,11 @@ private PropertyTypeInfo GetPropertyTypeInfo(ClassInfo classInfo, PropertyInfo p
return propertyTypeInfo;
}

if (propertySymbol is INamedTypeSymbol { SpecialType: SpecialType.System_DateTime })
if (typeSymbol is INamedTypeSymbol { SpecialType: SpecialType.System_DateTime })
{
classInfo.Diagnostics.Add(Diagnostics.DateTimeNotSupported(classInfo.Name, propertySymbol.Name, propertyLocation));
}
else if (propertySymbol.Type.Name == "List")
else if (typeSymbol.Name == "List")
{
classInfo.Diagnostics.Add(Diagnostics.ListWithoutInterface(classInfo.Name, propertySymbol.Name, propertyLocation));
}
Expand Down Expand Up @@ -434,7 +434,7 @@ INamedTypeSymbol when typeSymbol.IsValidIntegerType() => PropertyTypeInfo.Int,
INamedTypeSymbol when typeSymbol.SpecialType == SpecialType.System_Double => PropertyTypeInfo.Double,
INamedTypeSymbol when typeSymbol.SpecialType == SpecialType.System_String => PropertyTypeInfo.String,
INamedTypeSymbol when typeSymbol.SpecialType == SpecialType.System_Decimal || typeSymbol.Name == "Decimal128" => PropertyTypeInfo.Decimal,
ITypeSymbol when typeSymbol.ToDisplayString() == "byte[]" => PropertyTypeInfo.Data,
_ when typeSymbol.ToDisplayString() == "byte[]" => PropertyTypeInfo.Data,
INamedTypeSymbol when typeSymbol.Name == "ObjectId" => PropertyTypeInfo.ObjectId,
INamedTypeSymbol when typeSymbol.Name == "Guid" => PropertyTypeInfo.Guid,
INamedTypeSymbol when typeSymbol.Name == "DateTimeOffset" => PropertyTypeInfo.Date,
Expand Down
4 changes: 2 additions & 2 deletions Realm/Realm.SourceGenerator/RealmGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ public void Execute(GeneratorExecutionContext context)
var parser = new Parser(context, generatorConfig);
var parsingResults = parser.Parse(scr.RealmClasses);

var diagnosticsEmitter = new DiagnosticsEmitter(context);
var diagnosticsEmitter = new DiagnosticsEmitter(context, generatorConfig);
diagnosticsEmitter.Emit(parsingResults);

var codeEmitter = new CodeEmitter(context);
var codeEmitter = new CodeEmitter(context, generatorConfig);
codeEmitter.Emit(parsingResults);
}
}
Expand Down
30 changes: 24 additions & 6 deletions Realm/Realm/DatabaseTypes/RealmCollectionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,27 +93,45 @@ public event PropertyChangedEventHandler? PropertyChanged
}
}

[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public int Count
{
get => IsValid ? Handle.Value.Count() : 0;
}

[IgnoreDataMember, XmlIgnore] // XmlIgnore seems to be needed here as IgnoreDataMember is not sufficient for XmlSerializer.
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public ObjectSchema? ObjectSchema => Metadata?.Schema;

Metadata? IMetadataObject.Metadata => Metadata;

[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public bool IsManaged => Realm != null;

[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public bool IsValid => Handle.Value.IsValid;

[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public bool IsFrozen => Realm?.IsFrozen == true;

[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public Realm Realm { get; }

IThreadConfinedHandle IThreadConfined.Handle => Handle.Value;
Expand Down
38 changes: 31 additions & 7 deletions Realm/Realm/DatabaseTypes/RealmObjectBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,30 @@ public event PropertyChangedEventHandler? PropertyChanged
/// Gets the accessor that encapsulates the methods and properties used by the object for its functioning.
/// </summary>
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
IRealmAccessor IRealmObjectBase.Accessor => _accessor;

/// <summary>
/// Gets a value indicating whether the object has been associated with a Realm, either at creation or via
/// <see cref="Realm.Add{T}(T, bool)"/>.
/// </summary>
/// <value><c>true</c> if object belongs to a Realm; <c>false</c> if standalone.</value>
[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public bool IsManaged => _accessor.IsManaged;

/// <summary>
/// Gets an object encompassing the dynamic API for this RealmObjectBase instance.
/// </summary>
/// <value>A <see cref="Dynamic"/> instance that wraps this RealmObject.</value>
[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public DynamicObjectApi DynamicApi => _accessor.DynamicApi;

/// <summary>
Expand All @@ -102,7 +111,10 @@ public event PropertyChangedEventHandler? PropertyChanged
/// Unmanaged objects are always considered valid.
/// </summary>
/// <value><c>true</c> if managed and part of the Realm or unmanaged; <c>false</c> if managed but deleted.</value>
[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public bool IsValid => _accessor.IsValid;

/// <summary>
Expand All @@ -112,21 +124,30 @@ public event PropertyChangedEventHandler? PropertyChanged
/// </summary>
/// <value><c>true</c> if the object is frozen and immutable; <c>false</c> otherwise.</value>
/// <seealso cref="FrozenObjectsExtensions.Freeze{T}(T)"/>
[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public bool IsFrozen => _accessor.IsFrozen;

/// <summary>
/// Gets the <see cref="Realm"/> instance this object belongs to, or <c>null</c> if it is unmanaged.
/// </summary>
/// <value>The <see cref="Realm"/> instance this object belongs to.</value>
[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public Realm? Realm => _accessor.Realm;

/// <summary>
/// Gets the <see cref="Schema.ObjectSchema"/> instance that describes how the <see cref="Realm"/> this object belongs to sees it.
/// </summary>
/// <value>A collection of properties describing the underlying schema of this object.</value>
[IgnoreDataMember, XmlIgnore] // XmlIgnore seems to be needed here as IgnoreDataMember is not sufficient for XmlSerializer.
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public ObjectSchema? ObjectSchema => _accessor.ObjectSchema;

/// <summary>
Expand All @@ -136,7 +157,10 @@ public event PropertyChangedEventHandler? PropertyChanged
/// This property is not observable so the <see cref="PropertyChanged"/> event will not fire when its value changes.
/// </remarks>
/// <value>The number of objects referring to this one.</value>
[IgnoreDataMember]
[IgnoreDataMember, XmlIgnore]
#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonIgnore]
#endif
public int BacklinksCount => _accessor.BacklinksCount;

internal RealmObjectBase()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ is_global = true

# This document can be used to experiment with the source generator configurations

# realm.ignore_objects_nullability = true
# realm.ignore_objects_nullability = true
# realm.custom_ignore_attribute = [System.Text.Json.Serialization.JsonIgnore]

0 comments on commit 1fdc207

Please sign in to comment.