Skip to content

Commit

Permalink
Fixing properties with duplicate names (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gekctek authored Apr 24, 2023
1 parent 8320db2 commit ee0551b
Show file tree
Hide file tree
Showing 18 changed files with 1,236 additions and 716 deletions.
36 changes: 33 additions & 3 deletions src/Candid/Utilities/StringUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,37 @@ public static string ToPascalCase(string value)
{
IEnumerable<string> tokens = GetTokens(value)
.Select(t => char.ToUpper(t[0]) + t.Substring(1));
return string.Join("", tokens);
return TransformFirstAlpha(string.Join("", tokens), char.ToUpper); // Handles non letter first char
}
public static string ToCamelCase(string value)
{
IEnumerable<string> tokens = GetTokens(value)
.Select((t, i) => (i == 0 ? t[0] : char.ToUpper(t[0])) + t.Substring(1));
return string.Join("", tokens);
return TransformFirstAlpha(string.Join("", tokens), char.ToLower); // Handles non letter first char
}

public static string TransformFirstAlpha(string stringValue, Func<char, char> transform)
{
int i = 0;
while (i < stringValue.Length && !Char.IsLetter(stringValue[i]))
{
i++;
}
if (i == stringValue.Length)
{
return stringValue;
}
string newValue = "";
if (i > 0)
{
newValue += stringValue.Substring(0, i);
}
newValue += transform(stringValue[i]);
if (i + 1 < stringValue.Length)
{
newValue += stringValue.Substring(i + 1);
}
return newValue;
}

private static IEnumerable<string> GetTokens(string value)
Expand Down Expand Up @@ -62,9 +86,15 @@ private static IEnumerable<string> GetTokens(string value)

IEnumerable<string> SplitOn(char c)
{
return value
bool startsWithChar = value.StartsWith(c);
IEnumerable<string> v = value
.Split(new[] { c }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.ToLower());
if (startsWithChar)
{
v = v.Prepend(c.ToString());
}
return v;
}
}

Expand Down
23 changes: 23 additions & 0 deletions src/ClientGenerator/RoslynTypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,29 @@ private static ClassDeclarationSyntax GenerateClass(
// TODO
throw new NotImplementedException("Null, empty or reserved properties are not yet supported");
}
var uniquePropertyNames = new HashSet<ValueName>();
ClassProperty FixDuplicates(ClassProperty property)
{
ValueName name = property.Name;
int i = 0;
while (!uniquePropertyNames.Add(name))
{
// Add number suffix till there are no duplicates
i++;
name = property.Name.WithSuffix(i.ToString());
}
if (i > 0)
{
return new ClassProperty(name, property.Type, property.Access, property.HasSetter, property.Attributes);
}
return property;
}
properties = properties
.Select(FixDuplicates) // Fix if there are duplicate names
.ToList();
optionalProperties = optionalProperties
?.Select(FixDuplicates) // Fix if there are duplicate names
.ToList();
List<ConstructorDeclarationSyntax> constructors = new();
IEnumerable<PropertyDeclarationSyntax> properySyntaxList = properties
.Concat(optionalProperties ?? new List<ClassProperty>())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ public static ValueName Default(CandidTag tag, bool keepCandidCase)
string propertyName = !keepCandidCase
? StringUtil.ToPascalCase(stringValue)
: stringValue;
string variableName = StringUtil.ToCamelCase(stringValue);
string variableName = !keepCandidCase
? StringUtil.ToCamelCase(stringValue)
: StringUtil.TransformFirstAlpha(stringValue, char.ToLower); // Still should have lower first char
if (IsKeyword(propertyName))
{
// Add @ before reserved words
Expand All @@ -162,11 +164,17 @@ public static ValueName Default(CandidTag tag, bool keepCandidCase)
return new ValueName(propertyName, variableName, tag);
}


private static bool IsKeyword(string value)
{
// TODO better way to check for reserved names
return ReservedWords.Contains(value);
}

public ValueName WithSuffix(string suffix)
{
return new ValueName(this.PropertyName + suffix, this.VariableName + suffix, this.CandidTag);
}
}

}
2 changes: 2 additions & 0 deletions test/Candid.Tests/Candid.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
<ItemGroup>
<None Remove="Generators\Files\AnonymousTuples.did" />
<None Remove="Generators\Files\Dex.did" />
<None Remove="Generators\Files\DuplicatePropertyNames.did" />
<None Remove="Generators\Files\Governance.did" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Generators\Files\DuplicatePropertyNames.did" />
<EmbeddedResource Include="Generators\Files\AnonymousTuples.did" />
<EmbeddedResource Include="Generators\Files\Dex.did" />
<EmbeddedResource Include="Generators\Files\Governance.did" />
Expand Down
1 change: 1 addition & 0 deletions test/Candid.Tests/Generators/ClientGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class ClientGeneratorTests
[InlineData("Governance")]
[InlineData("Dex")]
[InlineData("AnonymousTuples")]
[InlineData("DuplicatePropertyNames")]
public void GenerateClients(string serviceName)
{
string fileText = GetFileText(serviceName + ".did");
Expand Down
8 changes: 8 additions & 0 deletions test/Candid.Tests/Generators/Files/DuplicatePropertyNames.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type Result = {
a : nat64;
_a : nat64;
};

service : () -> {
a : () -> (Result) query;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using EdjCase.ICP.Agent.Agents;
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid;
using Test;
using EdjCase.ICP.Agent.Responses;

namespace Test
{
public class DuplicatePropertyNamesApiClient
{
public IAgent Agent { get; }

public Principal CanisterId { get; }

public CandidConverter Converter { get; }

public DuplicatePropertyNamesApiClient(IAgent agent, Principal canisterId, CandidConverter converter = default)
{
this.Agent = agent;
this.CanisterId = canisterId;
this.Converter = converter;
}

public async System.Threading.Tasks.Task<Models.Result> A()
{
CandidArg arg = CandidArg.FromCandid();
QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "a", arg);
CandidArg reply = response.ThrowOrGetReply();
return reply.ToObjects<Models.Result>(this.Converter);
}
}
}

Type File: 'Result'

using EdjCase.ICP.Candid.Mapping;

namespace Test.Models
{
public class Result
{
[CandidName("a")]
public ulong A { get; set; }

[CandidName("_a")]
public ulong _A { get; set; }

public Result(ulong a, ulong _a)
{
this.A = a;
this._A = _a;
}

public Result()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using EdjCase.ICP.Agent.Agents;
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid;
using Test;
using EdjCase.ICP.Agent.Responses;

namespace Test
{
public class DuplicatePropertyNamesApiClient
{
public IAgent Agent { get; }

public Principal CanisterId { get; }

public CandidConverter Converter { get; }

public DuplicatePropertyNamesApiClient(IAgent agent, Principal canisterId, CandidConverter converter = default)
{
this.Agent = agent;
this.CanisterId = canisterId;
this.Converter = converter;
}

public async System.Threading.Tasks.Task<Models.Result> a()
{
CandidArg arg = CandidArg.FromCandid();
QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "a", arg);
CandidArg reply = response.ThrowOrGetReply();
return reply.ToObjects<Models.Result>(this.Converter);
}
}
}

Type File: 'Result'

namespace Test.Models
{
public class Result
{
public ulong a { get; set; }

public ulong _a { get; set; }

public Result(ulong a, ulong _a)
{
this.a = a;
this._a = _a;
}

public Result()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using EdjCase.ICP.Agent.Agents;
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid;
using Test;
using EdjCase.ICP.Agent.Responses;

namespace Test
{
public class DuplicatePropertyNamesApiClient
{
public IAgent Agent { get; }

public Principal CanisterId { get; }

public EdjCase.ICP.Candid.CandidConverter? Converter { get; }

public DuplicatePropertyNamesApiClient(IAgent agent, Principal canisterId, CandidConverter? converter = default)
{
this.Agent = agent;
this.CanisterId = canisterId;
this.Converter = converter;
}

public async System.Threading.Tasks.Task<Models.Result> A()
{
CandidArg arg = CandidArg.FromCandid();
QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "a", arg);
CandidArg reply = response.ThrowOrGetReply();
return reply.ToObjects<Models.Result>(this.Converter);
}
}
}

Type File: 'Result'

using EdjCase.ICP.Candid.Mapping;

namespace Test.Models
{
public class Result
{
[CandidName("a")]
public ulong A { get; set; }

[CandidName("_a")]
public ulong _A { get; set; }

public Result(ulong a, ulong _a)
{
this.A = a;
this._A = _a;
}

public Result()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using EdjCase.ICP.Agent.Agents;
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid;
using Test;
using EdjCase.ICP.Agent.Responses;

namespace Test
{
public class DuplicatePropertyNamesApiClient
{
public IAgent Agent { get; }

public Principal CanisterId { get; }

public EdjCase.ICP.Candid.CandidConverter? Converter { get; }

public DuplicatePropertyNamesApiClient(IAgent agent, Principal canisterId, CandidConverter? converter = default)
{
this.Agent = agent;
this.CanisterId = canisterId;
this.Converter = converter;
}

public async System.Threading.Tasks.Task<Models.Result> a()
{
CandidArg arg = CandidArg.FromCandid();
QueryResponse response = await this.Agent.QueryAsync(this.CanisterId, "a", arg);
CandidArg reply = response.ThrowOrGetReply();
return reply.ToObjects<Models.Result>(this.Converter);
}
}
}

Type File: 'Result'

namespace Test.Models
{
public class Result
{
public ulong a { get; set; }

public ulong _a { get; set; }

public Result(ulong a, ulong _a)
{
this.a = a;
this._a = _a;
}

public Result()
{
}
}
}
Loading

0 comments on commit ee0551b

Please sign in to comment.