Skip to content

Commit de628ec

Browse files
authored
feat(sidekick): split model.rs into modules (#2213)
In Rust, the documentation browser shows the code, and this code was getting too large to render effectively. For some crates, the module is also too large for some IDE defaults.
1 parent f7a7c8e commit de628ec

27 files changed

+695
-112
lines changed

internal/sidekick/internal/api/model.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ type API struct {
154154
State *APIState
155155
}
156156

157+
// HasMessages returns true if the API contains messages (most do).
158+
//
159+
// This is useful in the mustache templates to skip code that only makes sense
160+
// when per-message code follows.
161+
func (api *API) HasMessages() bool {
162+
return len(api.Messages) != 0
163+
}
164+
157165
// APIState contains helpful information that can be used when generating
158166
// clients.
159167
type APIState struct {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package api
16+
17+
import "testing"
18+
19+
func TestHasMessages(t *testing.T) {
20+
m := &Message{
21+
Name: "Message",
22+
Package: "test",
23+
ID: ".test.Message",
24+
}
25+
model := NewTestAPI([]*Message{m}, []*Enum{}, []*Service{})
26+
27+
if !model.HasMessages() {
28+
t.Errorf("expected HasMessages() == true for: %v", model)
29+
}
30+
31+
e := &Enum{
32+
Name: "Enum",
33+
Package: "test",
34+
ID: ".test.Enum",
35+
}
36+
model = NewTestAPI([]*Message{}, []*Enum{e}, []*Service{})
37+
if model.HasMessages() {
38+
t.Errorf("expected HasMessages() == false for: %v", model)
39+
}
40+
}

internal/sidekick/internal/rust/generate_test.go

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,57 @@ import (
1919
"os/exec"
2020
"path"
2121
"path/filepath"
22+
"slices"
23+
"strings"
2224
"testing"
2325

2426
"github.com/googleapis/librarian/internal/sidekick/internal/config"
2527
"github.com/googleapis/librarian/internal/sidekick/internal/parser"
2628
)
2729

2830
var (
29-
testdataDir, _ = filepath.Abs("../../testdata")
31+
testdataDir, _ = filepath.Abs("../../testdata")
32+
expectedInNosvc = []string{
33+
"README.md",
34+
"Cargo.toml",
35+
path.Join("src", "lib.rs"),
36+
path.Join("src", "model.rs"),
37+
path.Join("src", "model", "debug.rs"),
38+
path.Join("src", "model", "deserialize.rs"),
39+
path.Join("src", "model", "serialize.rs"),
40+
}
41+
expectedInCrate = append(expectedInNosvc,
42+
path.Join("src", "builder.rs"),
43+
path.Join("src", "client.rs"),
44+
path.Join("src", "tracing.rs"),
45+
path.Join("src", "transport.rs"),
46+
path.Join("src", "stub.rs"),
47+
path.Join("src", "stub", "dynamic.rs"),
48+
)
49+
expectedInClient = []string{
50+
path.Join("mod.rs"),
51+
path.Join("model.rs"),
52+
path.Join("model", "debug.rs"),
53+
path.Join("model", "deserialize.rs"),
54+
path.Join("model", "serialize.rs"),
55+
path.Join("builder.rs"),
56+
path.Join("client.rs"),
57+
path.Join("tracing.rs"),
58+
path.Join("transport.rs"),
59+
path.Join("stub.rs"),
60+
path.Join("stub", "dynamic.rs"),
61+
}
62+
unexpectedInClient = []string{
63+
"README.md",
64+
"Cargo.toml",
65+
path.Join("src", "lib.rs"),
66+
}
67+
expectedInModule = []string{
68+
path.Join("mod.rs"),
69+
path.Join("debug.rs"),
70+
path.Join("deserialize.rs"),
71+
path.Join("serialize.rs"),
72+
}
3073
)
3174

3275
func TestRustFromOpenAPI(t *testing.T) {
@@ -47,7 +90,7 @@ func TestRustFromOpenAPI(t *testing.T) {
4790
if err := Generate(model, outDir, cfg); err != nil {
4891
t.Fatal(err)
4992
}
50-
for _, expected := range []string{"README.md", "Cargo.toml", "src/lib.rs"} {
93+
for _, expected := range expectedInCrate {
5194
filename := path.Join(outDir, expected)
5295
stat, err := os.Stat(filename)
5396
if os.IsNotExist(err) {
@@ -57,6 +100,7 @@ func TestRustFromOpenAPI(t *testing.T) {
57100
t.Errorf("generated files should not be executable %s: %o", filename, stat.Mode())
58101
}
59102
}
103+
importsModelModules(t, path.Join(outDir, "src", "model.rs"))
60104
}
61105

62106
func TestRustFromProtobuf(t *testing.T) {
@@ -80,7 +124,91 @@ func TestRustFromProtobuf(t *testing.T) {
80124
if err := Generate(model, outDir, cfg); err != nil {
81125
t.Fatal(err)
82126
}
83-
for _, expected := range []string{"README.md", "Cargo.toml", "src/lib.rs"} {
127+
for _, expected := range expectedInCrate {
128+
filename := path.Join(outDir, expected)
129+
stat, err := os.Stat(filename)
130+
if os.IsNotExist(err) {
131+
t.Errorf("missing %s: %s", filename, err)
132+
}
133+
if stat.Mode().Perm()|0666 != 0666 {
134+
t.Errorf("generated files should not be executable %s: %o", filename, stat.Mode())
135+
}
136+
}
137+
importsModelModules(t, path.Join(outDir, "src", "model.rs"))
138+
}
139+
140+
func TestRustClient(t *testing.T) {
141+
requireProtoc(t)
142+
for _, override := range []string{"http-client", "grpc-client"} {
143+
outDir := t.TempDir()
144+
145+
cfg := &config.Config{
146+
General: config.GeneralConfig{
147+
SpecificationFormat: "protobuf",
148+
ServiceConfig: "google/cloud/secretmanager/v1/secretmanager_v1.yaml",
149+
SpecificationSource: "google/cloud/secretmanager/v1",
150+
},
151+
Source: map[string]string{
152+
"googleapis-root": path.Join(testdataDir, "googleapis"),
153+
},
154+
Codec: map[string]string{
155+
"copyright-year": "2025",
156+
"template-override": path.Join("templates", override),
157+
},
158+
}
159+
model, err := parser.CreateModel(cfg)
160+
if err != nil {
161+
t.Fatal(err)
162+
}
163+
if err := Generate(model, outDir, cfg); err != nil {
164+
t.Fatal(err)
165+
}
166+
for _, expected := range expectedInClient {
167+
filename := path.Join(outDir, expected)
168+
stat, err := os.Stat(filename)
169+
if os.IsNotExist(err) {
170+
t.Errorf("missing %s: %s", filename, err)
171+
}
172+
if stat.Mode().Perm()|0666 != 0666 {
173+
t.Errorf("generated files should not be executable %s: %o", filename, stat.Mode())
174+
}
175+
}
176+
for _, unexpected := range unexpectedInClient {
177+
filename := path.Join(outDir, unexpected)
178+
if stat, err := os.Stat(filename); err == nil {
179+
t.Errorf("did not expect file %s, got=%v", unexpected, stat)
180+
}
181+
}
182+
importsModelModules(t, path.Join(outDir, "model.rs"))
183+
}
184+
}
185+
186+
func TestRustNosvc(t *testing.T) {
187+
requireProtoc(t)
188+
outDir := t.TempDir()
189+
190+
cfg := &config.Config{
191+
General: config.GeneralConfig{
192+
SpecificationFormat: "protobuf",
193+
ServiceConfig: "google/cloud/secretmanager/v1/secretmanager_v1.yaml",
194+
SpecificationSource: "google/cloud/secretmanager/v1",
195+
},
196+
Source: map[string]string{
197+
"googleapis-root": path.Join(testdataDir, "googleapis"),
198+
},
199+
Codec: map[string]string{
200+
"copyright-year": "2025",
201+
"template-override": path.Join("templates", "nosvc"),
202+
},
203+
}
204+
model, err := parser.CreateModel(cfg)
205+
if err != nil {
206+
t.Fatal(err)
207+
}
208+
if err := Generate(model, outDir, cfg); err != nil {
209+
t.Fatal(err)
210+
}
211+
for _, expected := range expectedInNosvc {
84212
filename := path.Join(outDir, expected)
85213
stat, err := os.Stat(filename)
86214
if os.IsNotExist(err) {
@@ -90,6 +218,7 @@ func TestRustFromProtobuf(t *testing.T) {
90218
t.Errorf("generated files should not be executable %s: %o", filename, stat.Mode())
91219
}
92220
}
221+
importsModelModules(t, path.Join(outDir, "src", "model.rs"))
93222
}
94223

95224
func TestRustModuleRpc(t *testing.T) {
@@ -118,7 +247,7 @@ func TestRustModuleRpc(t *testing.T) {
118247
t.Fatal(err)
119248
}
120249

121-
for _, expected := range []string{"mod.rs"} {
250+
for _, expected := range expectedInModule {
122251
filename := path.Join(outDir, "rpc", expected)
123252
stat, err := os.Stat(filename)
124253
if os.IsNotExist(err) {
@@ -128,6 +257,7 @@ func TestRustModuleRpc(t *testing.T) {
128257
t.Errorf("generated files should not be executable %s: %o", filename, stat.Mode())
129258
}
130259
}
260+
importsModelModules(t, path.Join(outDir, "rpc", "mod.rs"))
131261
}
132262

133263
func TestRustBootstrapWkt(t *testing.T) {
@@ -157,7 +287,7 @@ func TestRustBootstrapWkt(t *testing.T) {
157287
t.Fatal(err)
158288
}
159289

160-
for _, expected := range []string{"mod.rs"} {
290+
for _, expected := range expectedInModule {
161291
filename := path.Join(outDir, "wkt", expected)
162292
stat, err := os.Stat(filename)
163293
if os.IsNotExist(err) {
@@ -169,6 +299,20 @@ func TestRustBootstrapWkt(t *testing.T) {
169299
}
170300
}
171301

302+
func importsModelModules(t *testing.T, filename string) {
303+
t.Helper()
304+
contents, err := os.ReadFile(filename)
305+
if err != nil {
306+
t.Fatal(err)
307+
}
308+
lines := strings.Split(string(contents), "\n")
309+
for _, want := range []string{"mod debug;", "mod serialize;", "mod deserialize;"} {
310+
if !slices.Contains(lines, want) {
311+
t.Errorf("expected file %s to have a line matching %q, got:\n%s", filename, want, contents)
312+
}
313+
}
314+
}
315+
172316
func requireProtoc(t *testing.T) {
173317
t.Helper()
174318
if _, err := exec.LookPath("protoc"); err != nil {

0 commit comments

Comments
 (0)