Skip to content

Commit 64048c1

Browse files
authored
rust: implement relay client (#626)
* rust: update dependencies - chrono->jiff as chrono is going into maintence mode chronotope/chrono#1423 (comment) - async-trait replaced when possible with Rust 1.75+ native async traits; kept for compat with russh - futures->futures_util, underlying library for just the things we need - urlencoding->percent-encoding as it's already a dep of the url module, so just one less dependency - general version updates Verified in the VS Code CLI that tunnel functionality still works as expected with these updates. * hyper and reqwest to latest * pr comments and clippy warnings * rust: implement relay client Rust had a relay host, but never actually implemented a relay client. - Imnplement relay_tunnel_client.rs (of course) - Updated incorrect type generation of some Rust models. These were just never hit in my code paths before, previously list_tunnels/get_tunnels could never return TunnelRelayTunnelEndpoint information. Now our Rust structure matches what Go does where TunnelRelayTunnelEndpoint is a field inside the TunnelEndpoint struct, so its fields are accessible. - GPT 5.5 did some very impressive analysis to find the bug that led to adding `CHANNEL_WRITE_CHUNK_SIZE`. This was just never something I hit consistently enough before to repro and fix. * comments
1 parent ea42818 commit 64048c1

36 files changed

Lines changed: 2176 additions & 506 deletions

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@
33
"ts"
44
],
55
"rust-analyzer.cargo.features": ["connections", "vendored-openssl"],
6+
"rust-analyzer.linkedProjects": [
7+
"rs/Cargo.toml"
8+
],
69
}

cs/src/Contracts/DevTunnels.Contracts.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<RootNamespace>Microsoft.DevTunnels.Contracts</RootNamespace>

cs/tools/TunnelsSDK.Generator/RustContractWriter.cs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,33 @@ private void WriteInterfaceContract(
188188

189189
s.Append(FormatDocComment(type.GetDocumentationCommentXml(), ""));
190190
s.Append("#[derive(Clone, Debug, Deserialize, Serialize");
191-
if (DefaultDerivers.Contains(rsName))
191+
// Add Default for types in the explicit list, and for types that will be
192+
// embedded into their base type via #[serde(flatten)].
193+
var willBeEmbedded = type.BaseType?.ToString() is string bt &&
194+
bt.StartsWith(this.csNamespace) &&
195+
allTypes.Any((t) => SymbolEqualityComparer.Default.Equals(t.BaseType, type.BaseType));
196+
if (DefaultDerivers.Contains(rsName) || willBeEmbedded)
192197
{
193198
s.Append(", Default");
194199
}
195200
s.AppendLine(")]");
196201
s.AppendLine("#[serde(rename_all(serialize = \"camelCase\", deserialize = \"camelCase\"))]");
197202
s.Append($"pub struct {rsName} {{");
198203

204+
// Check if this type has derived types. If so, we embed the derived types
205+
// into this base type (like Go's struct embedding) rather than having
206+
// derived types embed the base.
207+
var derivedTypes = allTypes.Where(
208+
(t) => SymbolEqualityComparer.Default.Equals(t.BaseType, type)).ToArray();
209+
199210
var fullBaseType = type.BaseType?.ToString();
200-
if (fullBaseType != null && fullBaseType.StartsWith(this.csNamespace))
211+
// Only add #[serde(flatten)] pub base if the base type does NOT embed
212+
// derived types. When a base type has derived types, it embeds them
213+
// (like Go's struct embedding), so derived types must not embed back.
214+
var baseEmbedsDerived = fullBaseType != null &&
215+
fullBaseType.StartsWith(this.csNamespace) &&
216+
allTypes.Any((t) => SymbolEqualityComparer.Default.Equals(t.BaseType, type.BaseType));
217+
if (fullBaseType != null && fullBaseType.StartsWith(this.csNamespace) && !baseEmbedsDerived)
201218
{
202219
var rsBaseType = fullBaseType.Substring(this.csNamespace.Length + 1);
203220
s.AppendLine();
@@ -206,6 +223,10 @@ private void WriteInterfaceContract(
206223
imports.Add($"crate::contracts::{rsBaseType}");
207224
}
208225

226+
// A type is "embedded" if its base type embeds it via #[serde(flatten)].
227+
// In that case, all fields must tolerate missing values in JSON.
228+
var isEmbeddedType = baseEmbedsDerived;
229+
209230
var properties = type.GetMembers()
210231
.OfType<IPropertySymbol>()
211232
.Where((p) => !p.IsStatic)
@@ -216,7 +237,19 @@ private void WriteInterfaceContract(
216237
{
217238
s.AppendLine();
218239
s.Append(FormatDocComment(property.GetDocumentationCommentXml(), " "));
219-
AppendStructProperty(type, property, imports, s);
240+
AppendStructProperty(type, property, imports, s, isEmbeddedType);
241+
}
242+
243+
// Embed derived types via #[serde(flatten)], similar to Go's struct
244+
// embedding. This allows the base type to deserialize fields from all
245+
// derived types.
246+
foreach (var derivedType in derivedTypes.OrderBy((t) => t.Name))
247+
{
248+
var fieldName = ToSnakeCase(derivedType.Name);
249+
s.AppendLine();
250+
s.AppendLine(" #[serde(flatten)]");
251+
s.AppendLine($" pub {fieldName}: {derivedType.Name},");
252+
imports.Add($"crate::contracts::{derivedType.Name}");
220253
}
221254

222255
s.AppendLine("}");
@@ -394,7 +427,7 @@ private string FormatDocComment(string? comment, string prefix)
394427

395428
return s.ToString();
396429
}
397-
private void AppendStructProperty(ITypeSymbol parentType, IPropertySymbol property, SortedSet<string> imports, StringBuilder s)
430+
private void AppendStructProperty(ITypeSymbol parentType, IPropertySymbol property, SortedSet<string> imports, StringBuilder s, bool isEmbeddedType = false)
398431
{
399432
var csType = property.Type.ToString();
400433
var isNullable = csType.EndsWith("?");
@@ -418,7 +451,9 @@ private void AppendStructProperty(ITypeSymbol parentType, IPropertySymbol proper
418451
if (isArray)
419452
{
420453
csType = csType.Substring(0, csType.Length - 2);
421-
if (isNullable || ignoreWhenDefault)
454+
// When a type is embedded (flattened) into a base type, all array
455+
// fields must default to empty since they may not be present in JSON.
456+
if (isNullable || ignoreWhenDefault || isEmbeddedType)
422457
{
423458
serdeDeclarations.Add("skip_serializing_if = \"Vec::is_empty\"");
424459
serdeDeclarations.Add("default");
@@ -432,6 +467,11 @@ private void AppendStructProperty(ITypeSymbol parentType, IPropertySymbol proper
432467
serdeDeclarations.Add("default");
433468
}
434469

470+
if (isNullable)
471+
{
472+
serdeDeclarations.Add("skip_serializing_if = \"Option::is_none\"");
473+
}
474+
435475
if (serdeDeclarations.Count > 0)
436476
{
437477
s.AppendLine($" #[serde({string.Join(", ", serdeDeclarations)})]");
@@ -472,7 +512,7 @@ private void AppendStructProperty(ITypeSymbol parentType, IPropertySymbol proper
472512
"long" => "i64",
473513
"ulong" => "u64",
474514
"string" => "String",
475-
"System.DateTime" => "DateTime<Utc>",
515+
"System.DateTime" => "Timestamp",
476516
"System.Text.RegularExpressions.Regex" => "regexp.Regexp",
477517
"System.Collections.Generic.IDictionary<string, string>"
478518
=> "HashMap<String, String>",
@@ -494,7 +534,7 @@ private void AppendStructProperty(ITypeSymbol parentType, IPropertySymbol proper
494534

495535
if (csType == "System.DateTime")
496536
{
497-
imports.Add("chrono::{DateTime, Utc}");
537+
imports.Add("jiff::Timestamp");
498538
}
499539
else if (csType.Contains("IDictionary<"))
500540
{

0 commit comments

Comments
 (0)