Skip to content

Commit 8c2416a

Browse files
feat(sidekick): Generate setter samples for oneof fields. (#2573)
1 parent 892f42b commit 8c2416a

File tree

11 files changed

+674
-397
lines changed

11 files changed

+674
-397
lines changed

internal/sidekick/internal/rust/annotate.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,10 @@ type oneOfAnnotation struct {
400400
StructQualifiedName string
401401
FieldType string
402402
DocLines []string
403+
// The best field to show in a oneof related samples.
404+
// Non deprecated fields are preferred, then scalar, repeated, map fields
405+
// in that order.
406+
ExampleField *api.Field
403407
// If set, this enum is only enabled when some features are enabled.
404408
FeatureGates []string
405409
FeatureGatesOp string
@@ -440,6 +444,9 @@ type fieldAnnotations struct {
440444
// If true, this is a `wkt::NullValue` field, and also requires super-extra
441445
// custom deserialization.
442446
IsWktNullValue bool
447+
// If this field is part of a oneof group, this will contain the other fields
448+
// in the group.
449+
OtherFieldsInGroup []*api.Field
443450
}
444451

445452
// SkipIfIsEmpty returns true if the field should be skipped if it is empty.
@@ -982,6 +989,29 @@ func (c *codec) annotateOneOf(oneof *api.OneOf, message *api.Message, model *api
982989
qualifiedName := fmt.Sprintf("%s::%s", scope, enumName)
983990
relativeEnumName := strings.TrimPrefix(qualifiedName, c.modulePath+"::")
984991
structQualifiedName := fullyQualifiedMessageName(message, c.modulePath, model.PackageName, c.packageMapping)
992+
993+
bestField := slices.MaxFunc(oneof.Fields, func(f1 *api.Field, f2 *api.Field) int {
994+
if f1.Deprecated == f2.Deprecated {
995+
if f1.Map == f2.Map {
996+
if f1.Repeated == f2.Repeated {
997+
return 0
998+
} else if f1.Repeated {
999+
return -1
1000+
} else {
1001+
return 1
1002+
}
1003+
} else if f1.Map {
1004+
return -1
1005+
} else {
1006+
return 1
1007+
}
1008+
} else if f1.Deprecated {
1009+
return -1
1010+
} else {
1011+
return 1
1012+
}
1013+
})
1014+
9851015
oneof.Codec = &oneOfAnnotation{
9861016
FieldName: toSnake(oneof.Name),
9871017
SetterName: toSnakeNoMangling(oneof.Name),
@@ -991,6 +1021,7 @@ func (c *codec) annotateOneOf(oneof *api.OneOf, message *api.Message, model *api
9911021
StructQualifiedName: structQualifiedName,
9921022
FieldType: fmt.Sprintf("%s::%s", scope, enumName),
9931023
DocLines: c.formatDocComments(oneof.Documentation, oneof.ID, model.State, message.Scopes()),
1024+
ExampleField: bestField,
9941025
}
9951026
}
9961027

@@ -1101,6 +1132,9 @@ func (c *codec) annotateField(field *api.Field, message *api.Message, model *api
11011132
ann.SerdeAs = c.messageFieldSerdeAs(field)
11021133
}
11031134
}
1135+
if field.Group != nil {
1136+
ann.OtherFieldsInGroup = language.FilterSlice(field.Group.Fields, func(f *api.Field) bool { return field != f })
1137+
}
11041138
}
11051139

11061140
func (c *codec) annotateEnum(e *api.Enum, model *api.API, full bool) {

internal/sidekick/internal/rust/annotate_test.go

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -488,9 +488,6 @@ func TestOneOfAnnotations(t *testing.T) {
488488
codec := createRustCodec()
489489
annotateModel(model, codec)
490490

491-
// Stops the recursion when comparing fields.
492-
ignore := cmpopts.IgnoreFields(api.Field{}, "Group")
493-
494491
if diff := cmp.Diff(&oneOfAnnotation{
495492
FieldName: "r#type",
496493
SetterName: "type",
@@ -500,10 +497,14 @@ func TestOneOfAnnotations(t *testing.T) {
500497
StructQualifiedName: "crate::model::Message",
501498
FieldType: "crate::model::message::Type",
502499
DocLines: []string{"/// Say something clever about this oneof."},
503-
}, group.Codec, ignore); diff != "" {
500+
ExampleField: singular,
501+
}, group.Codec, cmpopts.IgnoreFields(api.OneOf{}, "Codec")); diff != "" {
504502
t.Errorf("mismatch in oneof annotations (-want, +got)\n:%s", diff)
505503
}
506504

505+
// Stops the recursion when comparing fields.
506+
ignore := cmpopts.IgnoreFields(api.Field{}, "Codec")
507+
507508
if diff := cmp.Diff(&fieldAnnotations{
508509
FieldName: "oneof_field",
509510
SetterName: "oneof_field",
@@ -515,6 +516,7 @@ func TestOneOfAnnotations(t *testing.T) {
515516
AddQueryParameter: `let builder = req.oneof_field().iter().fold(builder, |builder, p| builder.query(&[("oneofField", p)]));`,
516517
KeyType: "",
517518
ValueType: "",
519+
OtherFieldsInGroup: []*api.Field{repeated, map_field, integer_field, boxed_field},
518520
}, singular.Codec, ignore); diff != "" {
519521
t.Errorf("mismatch in field annotations (-want, +got)\n:%s", diff)
520522
}
@@ -530,6 +532,7 @@ func TestOneOfAnnotations(t *testing.T) {
530532
AddQueryParameter: `let builder = req.oneof_field_repeated().iter().fold(builder, |builder, p| builder.query(&[("oneofFieldRepeated", p)]));`,
531533
KeyType: "",
532534
ValueType: "",
535+
OtherFieldsInGroup: []*api.Field{singular, map_field, integer_field, boxed_field},
533536
}, repeated.Codec, ignore); diff != "" {
534537
t.Errorf("mismatch in field annotations (-want, +got)\n:%s", diff)
535538
}
@@ -550,6 +553,7 @@ func TestOneOfAnnotations(t *testing.T) {
550553
IsBoxed: true,
551554
SerdeAs: "std::collections::HashMap<wkt::internal::I32, wkt::internal::F32>",
552555
SkipIfIsDefault: true,
556+
OtherFieldsInGroup: []*api.Field{singular, repeated, integer_field, boxed_field},
553557
}, map_field.Codec, ignore); diff != "" {
554558
t.Errorf("mismatch in field annotations (-want, +got)\n:%s", diff)
555559
}
@@ -565,6 +569,7 @@ func TestOneOfAnnotations(t *testing.T) {
565569
AddQueryParameter: `let builder = req.oneof_field_integer().iter().fold(builder, |builder, p| builder.query(&[("oneofFieldInteger", p)]));`,
566570
SerdeAs: "wkt::internal::I64",
567571
SkipIfIsDefault: true,
572+
OtherFieldsInGroup: []*api.Field{singular, repeated, map_field, boxed_field},
568573
}, integer_field.Codec, ignore); diff != "" {
569574
t.Errorf("mismatch in field annotations (-want, +got)\n:%s", diff)
570575
}
@@ -581,6 +586,7 @@ func TestOneOfAnnotations(t *testing.T) {
581586
IsBoxed: true,
582587
SerdeAs: "wkt::internal::F64",
583588
SkipIfIsDefault: true,
589+
OtherFieldsInGroup: []*api.Field{singular, repeated, map_field, integer_field},
584590
}, boxed_field.Codec, ignore); diff != "" {
585591
t.Errorf("mismatch in field annotations (-want, +got)\n:%s", diff)
586592
}
@@ -624,7 +630,7 @@ func TestOneOfConflictAnnotations(t *testing.T) {
624630
annotateModel(model, codec)
625631

626632
// Stops the recursion when comparing fields.
627-
ignore := cmpopts.IgnoreFields(api.Field{}, "Group")
633+
ignore := cmpopts.IgnoreFields(api.OneOf{}, "Codec")
628634

629635
want := &oneOfAnnotation{
630636
FieldName: "nested_thing",
@@ -635,6 +641,7 @@ func TestOneOfConflictAnnotations(t *testing.T) {
635641
StructQualifiedName: "crate::model::Message",
636642
FieldType: "crate::model::message::NestedThingOneOf",
637643
DocLines: []string{"/// Say something clever about this oneof."},
644+
ExampleField: singular,
638645
}
639646
if diff := cmp.Diff(want, group.Codec, ignore); diff != "" {
640647
t.Errorf("mismatch in oneof annotations (-want, +got)\n:%s", diff)
@@ -1818,3 +1825,94 @@ func TestEnumAnnotationsValuesForExamples(t *testing.T) {
18181825
})
18191826
}
18201827
}
1828+
1829+
func TestOneOfExampleFieldSelection(t *testing.T) {
1830+
deprecated := &api.Field{
1831+
Name: "deprecated_field",
1832+
ID: ".test.Message.deprecated_field",
1833+
Typez: api.STRING_TYPE,
1834+
IsOneOf: true,
1835+
Deprecated: true,
1836+
}
1837+
map_field := &api.Field{
1838+
Name: "map_field",
1839+
ID: ".test.Message.map_field",
1840+
Typez: api.MESSAGE_TYPE,
1841+
TypezID: ".test.$Map",
1842+
IsOneOf: true,
1843+
Map: true,
1844+
}
1845+
repeated := &api.Field{
1846+
Name: "repeated_field",
1847+
ID: ".test.Message.repeated_field",
1848+
Typez: api.STRING_TYPE,
1849+
Repeated: true,
1850+
IsOneOf: true,
1851+
}
1852+
scalar := &api.Field{
1853+
Name: "scalar_field",
1854+
ID: ".test.Message.scalar_field",
1855+
Typez: api.INT32_TYPE,
1856+
IsOneOf: true,
1857+
}
1858+
message_field := &api.Field{
1859+
Name: "message_field",
1860+
ID: ".test.Message.message_field",
1861+
Typez: api.MESSAGE_TYPE,
1862+
TypezID: ".test.AnotherMessage",
1863+
IsOneOf: true,
1864+
}
1865+
1866+
testCases := []struct {
1867+
name string
1868+
fields []*api.Field
1869+
want *api.Field
1870+
}{
1871+
{
1872+
name: "all types",
1873+
fields: []*api.Field{deprecated, map_field, repeated, scalar, message_field},
1874+
want: scalar,
1875+
},
1876+
{
1877+
name: "no scalars",
1878+
fields: []*api.Field{deprecated, map_field, repeated},
1879+
want: repeated,
1880+
},
1881+
{
1882+
name: "only map and deprecated",
1883+
fields: []*api.Field{deprecated, map_field},
1884+
want: map_field,
1885+
},
1886+
{
1887+
name: "only deprecated",
1888+
fields: []*api.Field{deprecated},
1889+
want: deprecated,
1890+
},
1891+
}
1892+
1893+
for _, tc := range testCases {
1894+
t.Run(tc.name, func(t *testing.T) {
1895+
group := &api.OneOf{
1896+
Name: "test_oneof",
1897+
ID: ".test.Message.test_oneof",
1898+
Fields: tc.fields,
1899+
}
1900+
message := &api.Message{
1901+
Name: "Message",
1902+
ID: ".test.Message",
1903+
Package: "test",
1904+
Fields: tc.fields,
1905+
OneOfs: []*api.OneOf{group},
1906+
}
1907+
model := api.NewTestAPI([]*api.Message{message}, []*api.Enum{}, []*api.Service{})
1908+
api.CrossReference(model)
1909+
codec := createRustCodec()
1910+
annotateModel(model, codec)
1911+
1912+
got := group.Codec.(*oneOfAnnotation).ExampleField
1913+
if diff := cmp.Diff(tc.want, got); diff != "" {
1914+
t.Errorf("mismatch in ExampleField (-want, +got)\n:%s", diff)
1915+
}
1916+
})
1917+
}
1918+
}

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

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,7 @@ impl {{Codec.Name}} {
132132
{{/Codec.BasicFields}}
133133
{{#OneOfs}}
134134

135-
/// Sets the value of [{{Codec.FieldName}}][{{Codec.StructQualifiedName}}::{{Codec.SetterName}}].
136-
///
137-
/// Note that all the setters affecting `{{Codec.FieldName}}` are mutually
138-
/// exclusive.
135+
{{> /templates/common/setter_preamble/oneof}}
139136
pub fn set_{{Codec.SetterName}}<T: std::convert::Into<std::option::Option<{{{Codec.FieldType}}}>>>(mut self, v: T) -> Self
140137
{
141138
self.{{Codec.FieldName}} = v.into();
@@ -158,14 +155,7 @@ impl {{Codec.Name}} {
158155
})
159156
}
160157

161-
/// Sets the value of [{{Group.Codec.FieldName}}][{{Codec.FQMessageName}}::{{Group.Codec.FieldName}}]
162-
/// to hold a `{{Codec.BranchName}}`.
163-
///
164-
/// Note that all the setters affecting `{{Group.Codec.FieldName}}` are
165-
/// mutually exclusive.
166-
{{#Deprecated}}
167-
#[deprecated]
168-
{{/Deprecated}}
158+
{{> /templates/common/oneof_variants}}
169159
{{#Singular}}
170160
pub fn set_{{Codec.SetterName}}<T: std::convert::Into<{{{Codec.FieldType}}}>>(mut self, v: T) -> Self {
171161
self.{{Group.Codec.FieldName}} = std::option::Option::Some(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{{!
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
}}
16+
/// Sets the value of [{{Group.Codec.FieldName}}][{{Codec.FQMessageName}}::{{Group.Codec.FieldName}}]
17+
/// to hold a `{{Codec.BranchName}}`.
18+
///
19+
/// Note that all the setters affecting `{{Group.Codec.FieldName}}` are
20+
/// mutually exclusive.
21+
{{#Singular}}
22+
{{> /templates/common/setter_preamble/singular_value_samples}}
23+
{{/Singular}}
24+
{{#Repeated}}
25+
{{> /templates/common/setter_preamble/repeated_samples}}
26+
{{/Repeated}}
27+
{{#Map}}
28+
{{> /templates/common/setter_preamble/map_samples}}
29+
{{/Map}}
30+
{{#ModelCodec.GenerateSetterSamples}}
31+
/// ```
32+
/// assert!(x.{{Codec.FieldName}}().is_some());
33+
{{#Codec.OtherFieldsInGroup}}
34+
/// assert!(x.{{Codec.FieldName}}().is_none());
35+
{{/Codec.OtherFieldsInGroup}}
36+
/// ```
37+
{{/ModelCodec.GenerateSetterSamples}}
38+
{{#Deprecated}}
39+
#[deprecated]
40+
{{/Deprecated}}

0 commit comments

Comments
 (0)