Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow customizing json ignore attribute for serialized classes #3451

Merged
merged 3 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
}

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]
Loading