Skip to content

Commit 6a4a57e

Browse files
ldetmergemini-code-assist[bot]zhumin8
authored
feat(create): add implementation for rust create (#3248)
Add implementation for rust create. This copies the [logic from sidekick](https://github.com/googleapis/librarian/blob/9a37edc3c97115c755010d6bbb3b1ac23c8e1c2d/internal/sidekick/sidekick/rust_generate.go#L49) minus calling the tidy command as rust team no longer requires that. Example output: ldetmer/google-cloud-rust#1 For #3072 --------- Signed-off-by: ldetmer <1771267+ldetmer@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Min Zhu <zhumin@google.com>
1 parent 788466f commit 6a4a57e

File tree

10 files changed

+332
-84
lines changed

10 files changed

+332
-84
lines changed

internal/librarian/create.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"time"
2626

2727
"github.com/googleapis/librarian/internal/config"
28+
"github.com/googleapis/librarian/internal/librarian/rust"
29+
2830
"github.com/googleapis/librarian/internal/yaml"
2931
"github.com/urfave/cli/v3"
3032
)
@@ -75,10 +77,10 @@ func createCommand() *cli.Command {
7577
}
7678

7779
func runCreate(ctx context.Context, name, specSource, serviceConfig, output, specFormat string) error {
78-
return runCreateWithGenerator(ctx, name, specSource, serviceConfig, output, specFormat, &Generate{})
80+
return create(ctx, name, specSource, serviceConfig, output, specFormat, &Generate{}, &rust.RustHelp{})
7981
}
8082

81-
func runCreateWithGenerator(ctx context.Context, libraryName, specSource, serviceConfig, output, specFormat string, gen Generator) error {
83+
func create(ctx context.Context, libraryName, specSource, serviceConfig, output, specFormat string, gen Generator, rustHelper rust.RustHelper) error {
8284
cfg, err := yaml.Read[config.Config](librarianConfigPath)
8385
if err != nil {
8486
return fmt.Errorf("%w: %v", errNoYaml, err)
@@ -98,8 +100,13 @@ func runCreateWithGenerator(ctx context.Context, libraryName, specSource, servic
98100
}
99101
switch cfg.Language {
100102
case "rust":
101-
//TODO: add create logic
102-
return gen.Run(ctx, false, libraryName)
103+
if err := rustHelper.HelperPrepareCargoWorkspace(ctx, output); err != nil {
104+
return err
105+
}
106+
if err := gen.Run(ctx, false, libraryName); err != nil {
107+
return err
108+
}
109+
return rustHelper.HelperFormatAndValidateLibrary(ctx, output)
103110
default:
104111
return errUnsupportedLanguage
105112
}

internal/librarian/create_test.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"testing"
2323

2424
"github.com/googleapis/librarian/internal/config"
25+
"github.com/googleapis/librarian/internal/librarian/rust"
2526
"gopkg.in/yaml.v3"
2627
)
2728

@@ -50,6 +51,29 @@ func (m *mockGenerator) Run(ctx context.Context, all bool, libraryName string) e
5051
return nil
5152
}
5253

54+
type mockRustHelper struct {
55+
prepareCalled bool
56+
prepareCallArgs struct {
57+
outputDir string
58+
}
59+
validateCalled bool
60+
validateCallArgs struct {
61+
outputDir string
62+
}
63+
}
64+
65+
func (m *mockRustHelper) HelperPrepareCargoWorkspace(ctx context.Context, outputDir string) error {
66+
m.prepareCalled = true
67+
m.prepareCallArgs.outputDir = outputDir
68+
return nil
69+
}
70+
71+
func (m *mockRustHelper) HelperFormatAndValidateLibrary(ctx context.Context, outputDir string) error {
72+
m.validateCalled = true
73+
m.validateCallArgs.outputDir = outputDir
74+
return nil
75+
}
76+
5377
func TestCreateLibrary(t *testing.T) {
5478

5579
for _, test := range []struct {
@@ -95,8 +119,9 @@ func TestCreateLibrary(t *testing.T) {
95119
createLibrarianYaml(t, libExists, libExistsOutput, test.language, "")
96120
}
97121
var gen Generator = &mockGenerator{}
122+
var rustHelper rust.RustHelper = &mockRustHelper{}
98123

99-
err := runCreateWithGenerator(context.Background(), test.libName, "", "", test.output, defaultSpecFormat, gen)
124+
err := create(context.Background(), test.libName, "", "", test.output, defaultSpecFormat, gen, rustHelper)
100125
if test.wantErr != nil {
101126
if !errors.Is(err, test.wantErr) {
102127
t.Errorf("want error %v, got %v", test.wantErr, err)

internal/librarian/rust/helper.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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 rust provides Rust functionality for librarian that is also being used by sidekick package.
16+
package rust
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"os"
22+
"path"
23+
24+
"github.com/googleapis/librarian/internal/command"
25+
"github.com/pelletier/go-toml/v2"
26+
)
27+
28+
// RustHelper interface used for mocking in tests.
29+
type RustHelper interface {
30+
HelperPrepareCargoWorkspace(ctx context.Context, outputDir string) error
31+
HelperFormatAndValidateLibrary(ctx context.Context, outputDir string) error
32+
}
33+
34+
// RustHelp struct implements RustHelper interface.
35+
type RustHelp struct {
36+
}
37+
38+
// HelperPrepareCargoWorkspace encapsulates prepareCargoWorkspace command.
39+
func (r *RustHelp) HelperPrepareCargoWorkspace(ctx context.Context, outputDir string) error {
40+
return PrepareCargoWorkspace(ctx, outputDir)
41+
}
42+
43+
// HelperFormatAndValidateLibrary encapsulates formatAndValidateLibrary command.
44+
func (r *RustHelp) HelperFormatAndValidateLibrary(ctx context.Context, outputDir string) error {
45+
return FormatAndValidateLibrary(ctx, outputDir)
46+
}
47+
48+
// getPackageName retrieves the packagename from a Cargo.toml file.
49+
func getPackageName(output string) (string, error) {
50+
cargo := CargoConfig{}
51+
filename := path.Join(output, "Cargo.toml")
52+
contents, err := os.ReadFile(filename)
53+
if err != nil {
54+
return "", fmt.Errorf("failed to read %s: %w", filename, err)
55+
}
56+
if err = toml.Unmarshal(contents, &cargo); err != nil {
57+
return "", fmt.Errorf("error unmarshaling %s: %w", filename, err)
58+
}
59+
return cargo.Package.Name, nil
60+
}
61+
62+
// PrepareCargoWorkspace creates a new cargo package in the specified output directory.
63+
func PrepareCargoWorkspace(ctx context.Context, outputDir string) error {
64+
if err := VerifyRustTools(ctx); err != nil {
65+
return err
66+
}
67+
if err := command.Run(ctx, "cargo", "new", "--vcs", "none", "--lib", outputDir); err != nil {
68+
return err
69+
}
70+
if err := command.Run(ctx, "taplo", "fmt", "Cargo.toml"); err != nil {
71+
return err
72+
}
73+
return nil
74+
}
75+
76+
// FormatAndValidateLibrary runs formatter, typos checks, tests tasks on the specified output directory.
77+
func FormatAndValidateLibrary(ctx context.Context, outputDir string) error {
78+
manifestPath := path.Join(outputDir, "Cargo.toml")
79+
if err := command.Run(ctx, "cargo", "fmt", "--manifest-path", manifestPath); err != nil {
80+
return err
81+
}
82+
if err := command.Run(ctx, "cargo", "test", "--manifest-path", manifestPath); err != nil {
83+
return err
84+
}
85+
if err := command.Run(ctx, "env", "RUSTDOCFLAGS=-D warnings", "cargo", "doc", "--manifest-path", manifestPath, "--no-deps"); err != nil {
86+
return err
87+
}
88+
if err := command.Run(ctx, "cargo", "clippy", "--manifest-path", manifestPath, "--", "--deny", "warnings"); err != nil {
89+
return err
90+
}
91+
return addNewFilesToGit(ctx, outputDir)
92+
}
93+
94+
// VerifyRustTools verifies that all required Rust tools are installed.
95+
func VerifyRustTools(ctx context.Context) error {
96+
if err := command.Run(ctx, "cargo", "--version"); err != nil {
97+
return fmt.Errorf("got an error trying to run `cargo --version`, the instructions on https://www.rust-lang.org/learn/get-started may solve this problem: %w", err)
98+
}
99+
if err := command.Run(ctx, "taplo", "--version"); err != nil {
100+
return fmt.Errorf("got an error trying to run `taplo --version`, please install using `cargo install taplo-cli`: %w", err)
101+
}
102+
if err := command.Run(ctx, "git", "--version"); err != nil {
103+
return fmt.Errorf("got an error trying to run `git --version`, the instructions on https://github.com/git-guides/install-git may solve this problem: %w", err)
104+
}
105+
return nil
106+
}
107+
108+
// addNewFilesToGit addes newly created library files and mod files to git to be committed.
109+
func addNewFilesToGit(ctx context.Context, outputDir string) error {
110+
if err := command.Run(ctx, "git", "add", outputDir); err != nil {
111+
return err
112+
}
113+
return command.Run(ctx, "git", "add", "Cargo.lock", "Cargo.toml")
114+
}
115+
116+
// CargoConfig is the configuration for a cargo package.
117+
type CargoConfig struct {
118+
Package CargoPackage `toml:"package"`
119+
}
120+
121+
// CargoPackage is a cargo package.
122+
type CargoPackage struct {
123+
Name string `toml:"name"`
124+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 rust
16+
17+
import (
18+
"bytes"
19+
"os"
20+
"path"
21+
"path/filepath"
22+
"strings"
23+
"testing"
24+
25+
cmdtest "github.com/googleapis/librarian/internal/command"
26+
)
27+
28+
func TestGetPackageName(t *testing.T) {
29+
expectedPackageName := "new-lib-format"
30+
got, err := getPackageName("testdata/new-lib-format")
31+
if err != nil {
32+
t.Fatalf("error getting package name %v", err)
33+
}
34+
if got != expectedPackageName {
35+
t.Errorf("want packageName %s, got %s", expectedPackageName, got)
36+
}
37+
}
38+
39+
func TestPrepareCargoWorkspace(t *testing.T) {
40+
cmdtest.RequireCommand(t, "cargo")
41+
cmdtest.RequireCommand(t, "taplo")
42+
libName := "new-lib"
43+
testdataDir, err := filepath.Abs("./testdata")
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
t.Chdir(testdataDir)
48+
outputDir := path.Join(testdataDir, libName)
49+
if err := PrepareCargoWorkspace(t.Context(), outputDir); err != nil {
50+
t.Fatal(err)
51+
}
52+
expectedFile := path.Join(outputDir, "Cargo.toml")
53+
if _, err := os.Stat(expectedFile); err != nil {
54+
t.Fatal(err)
55+
}
56+
got, err := os.ReadFile(expectedFile)
57+
if err != nil {
58+
t.Fatal(err)
59+
}
60+
expectedCargoContent := "name = \"new-lib\""
61+
if !strings.Contains(string(got), expectedCargoContent) {
62+
t.Errorf("%q missing expected string: %q", got, expectedCargoContent)
63+
}
64+
os.RemoveAll(outputDir)
65+
cmdtest.Run(t.Context(), "git", "restore", "--source=HEAD", "--staged", "--worktree", ".")
66+
}
67+
68+
func TestFormatAndValidateCreatedLibrary(t *testing.T) {
69+
cmdtest.RequireCommand(t, "cargo")
70+
cmdtest.RequireCommand(t, "env")
71+
cmdtest.RequireCommand(t, "git")
72+
testdataDir, err := filepath.Abs("./testdata")
73+
libName := "new-lib-format"
74+
t.Chdir(testdataDir)
75+
fileToFormat := path.Join(testdataDir, libName, "src", "main.rs")
76+
if err := FormatAndValidateLibrary(t.Context(), path.Join(testdataDir, libName)); err != nil {
77+
t.Fatal(err)
78+
}
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
data, err := os.ReadFile(fileToFormat)
83+
if err != nil {
84+
t.Fatal(err)
85+
}
86+
lineCount := bytes.Count(data, []byte("\n"))
87+
if len(data) > 0 && !bytes.HasSuffix(data, []byte("\n")) {
88+
lineCount++
89+
}
90+
if lineCount != 6 {
91+
t.Errorf("formatting should have given us 6 lines but got: %d", lineCount)
92+
}
93+
cmdtest.Run(t.Context(), "git", "restore", "--source=HEAD", "--staged", "--worktree", ".")
94+
cmdtest.Run(t.Context(), "git", "clean", "-fd", ".")
95+
}

internal/librarian/rust/testdata/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[workspace]
2+
members = ["new-lib-format"]
3+
resolver = "2"
4+
5+
[workspace.package]
6+
edition = "2024"
7+
authors = ["Google LLC"]
8+
license = "Apache-2.0"
9+
repository = "https://github.com/googleapis/google-cloud-rust/tree/main"
10+
keywords = ["gcp", "google-cloud", "google-cloud-rust", "sdk"]
11+
categories = ["network-programming"]
12+
rust-version = "1.85.0"
13+
14+
[workspace.dependencies]
15+
async-trait = "0.1"
16+
bytes = "1"
17+
gax = { version = "1", package = "google-cloud-gax" }
18+
iam_v1 = { version = "1", package = "google-cloud-iam-v1" }
19+
location = { version = "1", package = "google-cloud-location" }
20+
reqwest = "0.12"
21+
serde = "1"
22+
serde_json = "1"
23+
serde_with = "3"
24+
time = "0.3"
25+
tokio-test = "0.4"
26+
wkt = { version = "1", package = "google-cloud-wkt" }

internal/librarian/rust/testdata/new-lib-format/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
# Code generated by sidekick. DO NOT EDIT.
16+
17+
[package]
18+
name = "new-lib-format"
19+
version = "0.1.0"
20+
description = "Google Cloud Client Libraries for Rust - Access Approval API"
21+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fn main(){
2+
let x=5;
3+
if x==5{println!("bad spacing");}
4+
}

0 commit comments

Comments
 (0)