Skip to content

Commit 2b6d7e4

Browse files
authored
feat(sidekick/swift): non-string maps (#6361)
Protobuf can define maps where the key is not a string. These require custom serialization and deserialization. It may be possible to reduce the size of the generated code, but I cannot find a way at the moment.
1 parent cb788d6 commit 2b6d7e4

3 files changed

Lines changed: 62 additions & 0 deletions

File tree

internal/sidekick/swift/annotate_field.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ type fieldAnnotations struct {
3737
// This is used in the mustache templates, which sometimes need to refer to the underlying type.
3838
BaseFieldType string
3939

40+
// KeyType is the key's Swift type for maps and empty otherwise.
41+
KeyType string
42+
43+
// ValueType is the value's Swift type for maps and empty otherwise.
44+
ValueType string
45+
4046
// PackageName is the name of the package defining the type of this field.
4147
PackageName string
4248

@@ -58,6 +64,12 @@ type fieldAnnotations struct {
5864
InitializerType string
5965
}
6066

67+
// IsStringKeyed returns true if the field is a map field and the key is a
68+
// string type.
69+
func (a *fieldAnnotations) IsStringKeyed() bool {
70+
return a.KeyType == "Swift.String"
71+
}
72+
6173
func (c *codec) annotateField(field *api.Field) error {
6274
parts, err := c.fieldTypeName(field)
6375
if err != nil {
@@ -71,10 +83,13 @@ func (c *codec) annotateField(field *api.Field) error {
7183
if err != nil {
7284
return err
7385
}
86+
7487
annotations := &fieldAnnotations{
7588
Name: camelCase(field.Name),
7689
FieldType: parts.Full,
7790
BaseFieldType: parts.Base,
91+
KeyType: parts.Key,
92+
ValueType: parts.Value,
7893
PackageName: packageName,
7994
DocLines: docLines,
8095
InitializerType: parts.Full,

internal/sidekick/swift/annotate_message.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ func (c *codec) annotateMessage(message *api.Message, model *modelAnnotations) e
143143
}
144144
annotations.DependsOn[dep.Name] = dep
145145
}
146+
if field.Map && !fieldCodec.IsStringKeyed() {
147+
// In ProtoJSON map fields with non-string keys need to be
148+
// serialized as JSON objects with key fields. In the generated
149+
// Swift code, that requires a custom implementation of the
150+
// `Decodable` and `Encodable` protocol.
151+
annotations.CustomSerialization = true
152+
}
146153
}
147154
}
148155

internal/sidekick/swift/templates/common/message.mustache

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public struct {{Codec.Name}}: Codable, Equatable, {{Codec.Model.WktPackage}}._An
8484
let container = try decoder.container(keyedBy: CodingKeys.self)
8585
{{#Fields}}
8686
{{^IsOneOf}}
87+
{{#Singular}}
8788
{{#Optional}}
8889
self.{{Codec.Name}} = try container.decodeIfPresent({{{Codec.BaseFieldType}}}.self, forKey: .{{Codec.Name}})
8990
{{/Optional}}
@@ -95,6 +96,28 @@ public struct {{Codec.Name}}: Codable, Equatable, {{Codec.Model.WktPackage}}._An
9596
self.{{Codec.Name}} = try container.decode({{{Codec.FieldType}}}.self, forKey: .{{Codec.Name}})
9697
{{/Codec.Recursive}}
9798
{{/Optional}}
99+
{{/Singular}}
100+
{{#Repeated}}
101+
self.{{Codec.Name}} = try container.decode({{{Codec.FieldType}}}.self, forKey: .{{Codec.Name}})
102+
{{/Repeated}}
103+
{{#Map}}
104+
{{#Codec.IsStringKeyed}}
105+
self.{{Codec.Name}} = try container.decode({{{Codec.FieldType}}}.self, forKey: .{{Codec.Name}})
106+
{{/Codec.IsStringKeyed}}
107+
{{^Codec.IsStringKeyed}}
108+
self.{{Codec.Name}} = try { () throws in
109+
let stringKeyed = try container.decode([Swift.String : {{{Codec.ValueType}}}].self, forKey: .{{Codec.Name}})
110+
let tuples = try stringKeyed.lazy.map { (key, value) throws -> ({{{Codec.KeyType}}}, {{{Codec.ValueType}}}) in
111+
guard let newKey = {{{Codec.KeyType}}}(key) else {
112+
throw DecodingError.typeMismatch(
113+
{{{Codec.KeyType}}}.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Incorrect type for map key '{{Codec.Name}}'"))
114+
}
115+
return (newKey, value)
116+
}
117+
return Dictionary(uniqueKeysWithValues: tuples)
118+
}()
119+
{{/Codec.IsStringKeyed}}
120+
{{/Map}}
98121
{{/IsOneOf}}
99122
{{/Fields}}
100123
{{#OneOfs}}
@@ -119,7 +142,24 @@ public struct {{Codec.Name}}: Codable, Equatable, {{Codec.Model.WktPackage}}._An
119142
var container = encoder.container(keyedBy: CodingKeys.self)
120143
{{#Fields}}
121144
{{^IsOneOf}}
145+
{{#Map}}
146+
{{#Codec.IsStringKeyed}}
147+
{{! string keys have easy encoding }}
148+
try container.encode(self.{{Codec.Name}}, forKey: .{{Codec.Name}})
149+
{{/Codec.IsStringKeyed}}
150+
{{^Codec.IsStringKeyed}}
151+
do {
152+
let stringKeyed = Dictionary(
153+
uniqueKeysWithValues: self.{{Codec.Name}}.lazy.map { (Swift.String($0), $1) }
154+
)
155+
try container.encode(stringKeyed, forKey: .{{Codec.Name}})
156+
}
157+
{{/Codec.IsStringKeyed}}
158+
{{/Map}}
159+
{{^Map}}
160+
{{! non-maps have easy encoding }}
122161
try container.encode(self.{{Codec.Name}}, forKey: .{{Codec.Name}})
162+
{{/Map}}
123163
{{/IsOneOf}}
124164
{{/Fields}}
125165
{{#OneOfs}}

0 commit comments

Comments
 (0)