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 raw candid values on properties like CandidFunc #135

Merged
merged 4 commits into from
Oct 26, 2024
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ Collection of Internet Computer Protocol (ICP) libraries for .NET/Blazor/Unity

## See each individual project README for more in depth guides


# 🎮 Unity Integration

- Download latest `agent.unitypackage` from: https://github.com/edjCase/ICP.NET/releases
Expand All @@ -20,7 +19,9 @@ Collection of Internet Computer Protocol (ICP) libraries for .NET/Blazor/Unity
- Start coding 💻

# 📡 Generating a client for a canister

You can specify all the models and api calls yourself, but this is a tool to automatically generate a client and models based on the canister or .did file

- Prerequisite: Have .Net 6 installed (https://dotnet.microsoft.com/en-us/download/dotnet)
- Navigate to directory of .Net project
```
Expand Down Expand Up @@ -87,9 +88,13 @@ You can specify all the models and api calls yourself, but this is a tool to aut
- SHIP IT! 🚀

# Breaking change migrations

## 3.x.x => 4.x.x

The big change here was around variant classes and their attributes. Before the option types were defined by the attribute on each enum member, but in 4.x.x it changed to using method return types and having not type information in attributes. Also the VariantAttribute now gets the enum type from the Tag property vs the attribute

### Version 3

```
[Variant(typeof(MyVariantTag))] // Required to flag as variant and define options with enum
public class MyVariant
Expand All @@ -109,7 +114,9 @@ public enum MyVariantTag
Option2
}
```

### Version 4

```
[Variant] // Required to flag as variant
public class MyVariant
Expand All @@ -136,6 +143,7 @@ public enum MyVariantTag
Option2
}
```

# Candid Related Links

- [IC Http Interface Spec](https://smartcontracts.org/docs/current/references/ic-interface-spec)
Expand Down
14 changes: 14 additions & 0 deletions src/Candid/API.xml

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

72 changes: 56 additions & 16 deletions src/Candid/Mapping/IResolvableTypeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid.Models.Types;
using EdjCase.ICP.Candid.Models.Values;
using EdjCase.ICP.Candid.Parsers;
using System;
using System.Collections;
using System.Collections.Generic;
Expand Down Expand Up @@ -332,9 +333,14 @@ private static IResolvableTypeInfo BuildTypeInfo(Type objType)
VariantAttribute? variantAttribute = objType.GetCustomAttribute<VariantAttribute>();
if (variantAttribute != null)
{
return BuildVariant(objType, variantAttribute);
return BuildVariant(objType);
}

if (typeof(CandidValue).IsAssignableFrom(objType))
{
// If RawCandidValueAttribute is on the property, then it will never reach here
throw new InvalidOperationException("Raw candid values must have the `CandidTypeDefinitionAttribute` on the property");
}

// Assume anything else is a record
return BuildRecord(objType);
Expand Down Expand Up @@ -481,7 +487,7 @@ private static IResolvableTypeInfo BuildEnumVariant(Type enumType)
return new ResolvedTypeInfo(enumType, candidType, mapper);
}

private static IResolvableTypeInfo BuildVariant(Type objType, VariantAttribute attribute)
private static IResolvableTypeInfo BuildVariant(Type objType)
{
List<PropertyInfo> properties = objType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
Expand Down Expand Up @@ -519,7 +525,7 @@ private static IResolvableTypeInfo BuildVariant(Type objType, VariantAttribute a
t => t
);

var optionTypes = new Dictionary<CandidTag, (Type Type, bool UseOptionalOverride)>();
var optionTypes = new Dictionary<CandidTag, (Type Type, bool UseOptionalOverride, CandidType? CandidType)>();
foreach (MethodInfo classMethod in objType.GetMethods(BindingFlags.Instance | BindingFlags.Public))
{
CandidTag tag;
Expand All @@ -539,7 +545,9 @@ private static IResolvableTypeInfo BuildVariant(Type objType, VariantAttribute a
CandidOptionalAttribute? optionalAttribute = classMethod.GetCustomAttribute<CandidOptionalAttribute>();
bool useOptionalOverride = optionalAttribute != null;

optionTypes.Add(tag, (classMethod.ReturnType, useOptionalOverride));
CandidTypeDefinitionAttribute? rawCandidTypeAttribute = classMethod.GetCustomAttribute<CandidTypeDefinitionAttribute>();

optionTypes.Add(tag, (classMethod.ReturnType, useOptionalOverride, rawCandidTypeAttribute?.Type));
}
foreach (PropertyInfo property in properties)
{
Expand All @@ -566,14 +574,15 @@ private static IResolvableTypeInfo BuildVariant(Type objType, VariantAttribute a
}
CandidOptionalAttribute? optionalAttribute = property.GetCustomAttribute<CandidOptionalAttribute>();
bool useOptionalOverride = optionalAttribute != null;
if (!optionTypes.TryGetValue(tag, out (Type, bool) optionType))
CandidTypeDefinitionAttribute? rawCandidTypeAttribute = property.GetCustomAttribute<CandidTypeDefinitionAttribute>();
if (!optionTypes.TryGetValue(tag, out (Type, bool, CandidType?) optionType))
{
// Add if not already added by a method
optionTypes.Add(tag, (property.PropertyType, useOptionalOverride));
optionTypes.Add(tag, (property.PropertyType, useOptionalOverride, rawCandidTypeAttribute?.Type));
}
else
{
if (optionType != (property.PropertyType, useOptionalOverride))
if (optionType != (property.PropertyType, useOptionalOverride, rawCandidTypeAttribute?.Type))
{
throw new Exception($"Conflict: Variant '{objType.FullName}' defines a property '{property.Name}' and a method with different types for an option");
}
Expand All @@ -589,19 +598,21 @@ private static IResolvableTypeInfo BuildVariant(Type objType, VariantAttribute a
MemberInfo enumOption = variantTagProperty.PropertyType.GetMember(tagName.Name).First();
Type? optionType = null;
bool useOptionalOverride = false;
if (optionTypes.TryGetValue(tagName, out (Type Type, bool UseOptionalOverride) o))
CandidType? candidType = null;
if (optionTypes.TryGetValue(tagName, out (Type Type, bool UseOptionalOverride, CandidType? CandidType) o))
{
optionType = o.Type;
useOptionalOverride = o.UseOptionalOverride;
candidType = o.CandidType;
}
var tagAttribute = enumOption.GetCustomAttribute<CandidTagAttribute>();
CandidTag tag = tagAttribute?.Tag ?? tagName;
return (tag, new VariantMapper.Option(tagEnum, optionType, useOptionalOverride));
return (tag, new VariantMapper.Option(tagEnum, optionType, useOptionalOverride, candidType));
})
.ToDictionary(k => k.Item1, k => k.Item2);

List<Type> dependencies = options.Values
.Where(v => v.Type != null)
.Where(v => v.Type != null && v.CandidType == null)
.Select(v => v.Type!)
.Distinct()
.ToList();
Expand All @@ -617,7 +628,13 @@ private static IResolvableTypeInfo BuildVariant(Type objType, VariantAttribute a
{
return new CandidPrimitiveType(PrimitiveType.Null);
}
return resolvedDependencies[o.Value.Type];
CandidType type = o.Value.CandidType ?? resolvedDependencies[o.Value.Type];
if (o.Value.UseOptionalOverride)
{
// Property is really optional type
type = new CandidOptionalType(type);
}
return type;
}
);
var type = new CandidVariantType(optionCandidTypes);
Expand Down Expand Up @@ -673,10 +690,15 @@ private static IResolvableTypeInfo BuildRecord(Type objType)
}
CandidOptionalAttribute? optionalAttribute = property.GetCustomAttribute<CandidOptionalAttribute>();
bool useOptionalOverride = optionalAttribute != null;
PropertyMetaData propertyMetaData = new(property, useOptionalOverride);

CandidTypeDefinitionAttribute? rawCandidValueAttribute = property.GetCustomAttribute<CandidTypeDefinitionAttribute>();


PropertyMetaData propertyMetaData = new(property, useOptionalOverride, rawCandidValueAttribute?.Type);
propertyMetaDataMap.Add(tag, propertyMetaData);
}
List<Type> dependencies = propertyMetaDataMap
.Where(p => p.Value.CandidType is null)
.Select(p => p.Value.PropertyInfo.PropertyType)
.ToList();
return new ComplexTypeInfo(objType, dependencies, (resolvedMappings) =>
Expand All @@ -686,7 +708,7 @@ private static IResolvableTypeInfo BuildRecord(Type objType)
p => p.Key,
p =>
{
CandidType type = resolvedMappings[p.Value.PropertyInfo.PropertyType];
CandidType type = p.Value.CandidType ?? resolvedMappings[p.Value.PropertyInfo.PropertyType];
if (p.Value.UseOptionalOverride)
{
// Property is really optional type
Expand All @@ -702,10 +724,28 @@ private static IResolvableTypeInfo BuildRecord(Type objType)
}

}

internal record PropertyMetaData(
PropertyInfo PropertyInfo,
bool UseOptionalOverride
);

bool UseOptionalOverride,
CandidType? CandidType
)
{
private bool? isGenericNullableCache;

public bool IsGenericNullable
{
get
{
{
if (!this.isGenericNullableCache.HasValue)
{
this.isGenericNullableCache = this.PropertyInfo.PropertyType.IsGenericType
&& this.PropertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>);
}
return this.isGenericNullableCache.Value;
}
}
}
};
}
21 changes: 21 additions & 0 deletions src/Candid/Mapping/MapperAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid.Models.Types;
using EdjCase.ICP.Candid.Parsers;
using System;

namespace EdjCase.ICP.Candid.Mapping
Expand Down Expand Up @@ -117,4 +119,23 @@ public VariantOptionAttribute(string tag) : this(CandidTag.FromName(tag))

}
}

/// <summary>
/// An attribute to specify the candid type definition for a value
/// This is REQUIRED for any raw candid values
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Method)]
public class CandidTypeDefinitionAttribute : Attribute
{
/// <summary>
/// The candid type definition for the value
/// </summary>
public CandidType Type { get; }

/// <param name="definition">The candid type definition in string format https://github.com/dfinity/candid/blob/master/spec/Candid.md#core-grammar</param>
public CandidTypeDefinitionAttribute(string definition)
{
this.Type = CandidTextParser.Parse(definition);
}
}
}
32 changes: 32 additions & 0 deletions src/Candid/Mapping/Mappers/RawCandidMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid.Models.Types;
using EdjCase.ICP.Candid.Models.Values;
using System;

namespace EdjCase.ICP.Candid.Mapping.Mappers
{
internal class RawCandidMapper : ICandidValueMapper
{
public CandidType CandidType { get; }

public RawCandidMapper(CandidType candidType)
{
this.CandidType = candidType ?? throw new ArgumentNullException(nameof(candidType));
}

public object Map(CandidValue value, CandidConverter converter)
{
return value;
}

public CandidValue Map(object value, CandidConverter converter)
{
return (CandidValue)value;
}

public CandidType? GetMappedCandidType(Type type)
{
return this.CandidType;
}
}
}
50 changes: 30 additions & 20 deletions src/Candid/Mapping/Mappers/RecordMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,25 @@ public object Map(CandidValue value, CandidConverter converter)
if (record.Fields.TryGetValue(tag, out CandidValue fieldCandidValue))
{
Type t = property.PropertyInfo.PropertyType;
if (t.IsGenericType
&& t.GetGenericTypeDefinition() == typeof(Nullable<>))
if (typeof(CandidValue).IsAssignableFrom(t))
{
// Get T of Nullable<T>
t = t.GetGenericArguments()[0];
}
if (property.UseOptionalOverride
&& fieldCandidValue is CandidOptional o
&& o.Value.IsNull())
{
// If not using OptionalValue and the value is opt null,
// that is the same as setting to null, so just skip
fieldValue = null;
// If the property is a candid value, just set it
fieldValue = fieldCandidValue;
}
else
{
fieldValue = converter.ToObject(t, fieldCandidValue);
if (property.UseOptionalOverride
&& fieldCandidValue is CandidOptional o
&& o.Value.IsNull())
{
// If not using OptionalValue and the value is opt null,
// that is the same as setting to null, so just skip
fieldValue = null;
}
else
{
fieldValue = converter.ToObject(t, fieldCandidValue);
}
}

if (fieldValue != null)
Expand Down Expand Up @@ -79,18 +81,26 @@ public CandidValue Map(object value, CandidConverter converter)
foreach ((CandidTag tag, PropertyMetaData property) in this.Properties)
{
object? propValue = property.PropertyInfo.GetValue(value);
CandidValue v;
if (propValue == null)
CandidValue v; if (propValue is CandidValue cv)
{
v = new CandidOptional(null);
// If the property is a candid value, just set it
v = cv;
}
else
{
v = converter.FromObject(propValue);
if (property.UseOptionalOverride)
if (propValue == null)
{
v = new CandidOptional(null);
}
else
{
// Wrap in candid optional if has override [CandidOptional]
v = new CandidOptional(v);
v = converter.FromObject(propValue);
if (property.UseOptionalOverride || property.IsGenericNullable)
{
// Wrap in candid optional if has override [CandidOptional]
// or is Nullable<> (Nullable<T> gives just T if setting the value)
v = new CandidOptional(v);
}
}
}
fields.Add(tag, v);
Expand Down
Loading
Loading