Skip to content

Commit 75337ca

Browse files
committed
chore(librariangen): languagecontainer package to parse release-init request
The languagecontainer package parses the request JSON file and calls the corresponding implementation method. The user (Java's language container in this case) doesn't have to implement the parsing logic and the tests. I moved the argument parsing tests to languagecontainer/languagecontainer_test.go from main_test.go.
1 parent 8d9e83e commit 75337ca

File tree

9 files changed

+400
-115
lines changed

9 files changed

+400
-115
lines changed

internal/librariangen/languagecontainer/languagecontainer.go

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,24 @@ package languagecontainer
2222

2323
import (
2424
"context"
25+
"encoding/json"
26+
"flag"
27+
"fmt"
2528
"log/slog"
29+
"os"
30+
"path/filepath"
2631

2732
"cloud.google.com/java/internal/librariangen/languagecontainer/release"
2833
"cloud.google.com/java/internal/librariangen/message"
2934
)
3035

31-
// LanguageContainer defines the interface for language-specific container operations.
32-
type LanguageContainer interface {
33-
ReleaseInit(context.Context, *release.Config) (*message.ReleaseInitResponse, error)
34-
// Other container functions like Generate and Build will also be part of the interface.
36+
// LanguageContainer defines the functions for language-specific container operations.
37+
type LanguageContainer struct {
38+
ReleaseInit func(context.Context, *release.Config) (*message.ReleaseInitResponse, error)
39+
// Other container functions like Generate and Build will also be part of the struct.
3540
}
3641

37-
// Run would accept an implementation of the LanguageContainer interface.
42+
// Run accepts an implementation of the LanguageContainer.
3843
func Run(args []string, container LanguageContainer) int {
3944
// Logic to parse args and call the appropriate method on the container.
4045
// For example, if args[1] is "generate":
@@ -44,26 +49,61 @@ func Run(args []string, container LanguageContainer) int {
4449
if len(args) < 1 {
4550
panic("args must not be empty")
4651
}
47-
switch args[0] {
52+
cmd := args[0]
53+
flags := args[1:]
54+
switch cmd {
4855
case "generate":
4956
slog.Warn("librariangen: generate command is not yet implemented")
5057
return 1
5158
case "configure":
5259
slog.Warn("librariangen: configure command is not yet implemented")
5360
return 1
5461
case "release-init":
55-
// TODO: Parse flags and read request from the release-init-request.json file
56-
// Create release.Config object.
57-
58-
// TODO: Call container's ReleaseInit method with the parsed request
59-
60-
// TODO: Save the response to release-init-response.json file
62+
cfg := &release.Context{}
63+
releaseInitFlags := flag.NewFlagSet("release-init", flag.ContinueOnError)
64+
releaseInitFlags.StringVar(&cfg.LibrarianDir, "librarian", "/librarian", "Path to the librarian-tool input directory. Contains release-init-request.json.")
65+
releaseInitFlags.StringVar(&cfg.RepoDir, "repo", "/repo", "Path to the language repo.")
66+
releaseInitFlags.StringVar(&cfg.OutputDir, "output", "/output", "Path to the output directory.")
67+
if err := releaseInitFlags.Parse(flags); err != nil {
68+
slog.Error("failed to parse flags", "error", err)
69+
return 1
70+
}
71+
requestPath := filepath.Join(cfg.LibrarianDir, "release-init-request.json")
72+
bytes, err := os.ReadFile(requestPath)
73+
if err != nil {
74+
slog.Error("failed to read request file", "path", requestPath, "error", err)
75+
return 1
76+
}
77+
request := &message.ReleaseInitRequest{}
78+
if err := json.Unmarshal(bytes, request); err != nil {
79+
slog.Error("failed to parse request JSON", "error", err)
80+
return 1
81+
}
82+
config := &release.Config{
83+
Context: cfg,
84+
Request: request,
85+
}
86+
response, err := container.ReleaseInit(context.Background(), config)
87+
if err != nil {
88+
slog.Error("release-init failed", "error", err)
89+
return 1
90+
}
91+
bytes, err = json.MarshalIndent(response, "", " ")
92+
if err != nil {
93+
slog.Error("failed to marshal response JSON", "error", err)
94+
return 1
95+
}
96+
responsePath := filepath.Join(cfg.LibrarianDir, "release-init-response.json")
97+
if err := os.WriteFile(responsePath, bytes, 0644); err != nil {
98+
slog.Error("failed to write response file", "path", responsePath, "error", err)
99+
return 1
100+
}
61101
slog.Info("librariangen: release-init command executed successfully")
62102
case "build":
63103
slog.Warn("librariangen: build command is not yet implemented")
64104
return 1
65105
default:
66-
slog.Error("librariangen: unknown command: %s (with flags %v)", args[0], args)
106+
slog.Error(fmt.Sprintf("librariangen: unknown command: %s (with flags %v)", cmd, flags))
67107
return 1
68108
}
69109
return 0
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
// http://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 languagecontainer
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
24+
"cloud.google.com/java/internal/librariangen/languagecontainer/release"
25+
"cloud.google.com/java/internal/librariangen/message"
26+
"github.com/google/go-cmp/cmp"
27+
)
28+
29+
func TestRun(t *testing.T) {
30+
tmpDir := t.TempDir()
31+
if err := os.WriteFile(filepath.Join(tmpDir, "release-init-request.json"), []byte("{}"), 0644); err != nil {
32+
t.Fatal(err)
33+
}
34+
tests := []struct {
35+
name string
36+
args []string
37+
wantCode int
38+
wantErr bool
39+
}{
40+
{
41+
name: "unknown command",
42+
args: []string{"foo"},
43+
wantCode: 1,
44+
},
45+
{
46+
name: "build command",
47+
args: []string{"build"},
48+
wantCode: 1, // Not implemented yet
49+
},
50+
{
51+
name: "configure command",
52+
args: []string{"configure"},
53+
wantCode: 1, // Not implemented yet
54+
},
55+
{
56+
name: "generate command",
57+
args: []string{"generate"},
58+
wantCode: 1, // Not implemented yet
59+
},
60+
{
61+
name: "release-init command success",
62+
args: []string{"release-init", "-librarian", tmpDir},
63+
wantCode: 0,
64+
},
65+
{
66+
name: "release-init command failure",
67+
args: []string{"release-init", "-librarian", tmpDir},
68+
wantCode: 1,
69+
wantErr: true,
70+
},
71+
}
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
container := LanguageContainer{
75+
ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) {
76+
if tt.wantErr {
77+
return nil, os.ErrNotExist
78+
}
79+
return &message.ReleaseInitResponse{}, nil
80+
},
81+
}
82+
if gotCode := Run(tt.args, container); gotCode != tt.wantCode {
83+
t.Errorf("Run() = %v, want %v", gotCode, tt.wantCode)
84+
}
85+
})
86+
}
87+
}
88+
89+
func TestRun_noArgs(t *testing.T) {
90+
defer func() {
91+
if r := recover(); r == nil {
92+
t.Errorf("The code did not panic")
93+
}
94+
}()
95+
Run([]string{}, LanguageContainer{})
96+
}
97+
98+
func TestRun_ReleaseInitWritesResponse(t *testing.T) {
99+
tmpDir := t.TempDir()
100+
if err := os.WriteFile(filepath.Join(tmpDir, "release-init-request.json"), []byte("{}"), 0644); err != nil {
101+
t.Fatal(err)
102+
}
103+
args := []string{"release-init", "-librarian", tmpDir}
104+
want := &message.ReleaseInitResponse{Error: "test error"}
105+
container := LanguageContainer{
106+
ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) {
107+
return want, nil
108+
},
109+
}
110+
111+
if code := Run(args, container); code != 0 {
112+
t.Errorf("Run() = %v, want 0", code)
113+
}
114+
115+
responsePath := filepath.Join(tmpDir, "release-init-response.json")
116+
bytes, err := os.ReadFile(responsePath)
117+
if err != nil {
118+
t.Fatal(err)
119+
}
120+
got := &message.ReleaseInitResponse{}
121+
if err := json.Unmarshal(bytes, got); err != nil {
122+
t.Fatal(err)
123+
}
124+
if diff := cmp.Diff(want, got); diff != "" {
125+
t.Errorf("response mismatch (-want +got):\n%s", diff)
126+
}
127+
}
128+
129+
func TestRun_ReleaseInitReadsContextArgs(t *testing.T) {
130+
tmpDir := t.TempDir()
131+
librarianDir := filepath.Join(tmpDir, "librarian")
132+
if err := os.Mkdir(librarianDir, 0755); err != nil {
133+
t.Fatal(err)
134+
}
135+
if err := os.WriteFile(filepath.Join(librarianDir, "release-init-request.json"), []byte("{}"), 0644); err != nil {
136+
t.Fatal(err)
137+
}
138+
repoDir := filepath.Join(tmpDir, "repo")
139+
if err := os.Mkdir(repoDir, 0755); err != nil {
140+
t.Fatal(err)
141+
}
142+
outputDir := filepath.Join(tmpDir, "output")
143+
if err := os.Mkdir(outputDir, 0755); err != nil {
144+
t.Fatal(err)
145+
}
146+
args := []string{"release-init", "-librarian", librarianDir, "-repo", repoDir, "-output", outputDir}
147+
var gotConfig *release.Config
148+
container := LanguageContainer{
149+
ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) {
150+
gotConfig = c
151+
return &message.ReleaseInitResponse{}, nil
152+
},
153+
}
154+
if code := Run(args, container); code != 0 {
155+
t.Errorf("Run() = %v, want 0", code)
156+
}
157+
if got, want := gotConfig.Context.LibrarianDir, librarianDir; got != want {
158+
t.Errorf("gotConfig.Context.LibrarianDir = %q, want %q", got, want)
159+
}
160+
if got, want := gotConfig.Context.RepoDir, repoDir; got != want {
161+
t.Errorf("gotConfig.Context.RepoDir = %q, want %q", got, want)
162+
}
163+
if got, want := gotConfig.Context.OutputDir, outputDir; got != want {
164+
t.Errorf("gotConfig.Context.OutputDir = %q, want %q", got, want)
165+
}
166+
}

internal/librariangen/languagecontainer/release/release.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
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+
// http://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 release contains types for language container's release command.
116
package release
217

318
import "cloud.google.com/java/internal/librariangen/message"
419

5-
// The ReleaseInitContext has the directory paths for the release-init command.
20+
// Context has the directory paths for the release-init command.
621
// https://github.com/googleapis/librarian/blob/main/doc/language-onboarding.md#release-init
7-
type ReleaseInitContext struct {
22+
type Context struct {
823
LibrarianDir string
924
RepoDir string
1025
OutputDir string
1126
}
1227

28+
// The Config for the release-init command. This holds the context (the directory paths)
29+
// and the request parsed from the release-init-request.json file.
1330
type Config struct {
14-
Context *ReleaseInitContext
31+
Context *Context
1532
// This request is parsed from the release-init-request.json file in
1633
// the LibrarianDir of the context.
1734
Request *message.ReleaseInitRequest
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
// http://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 release
16+
17+
import (
18+
"encoding/json"
19+
"os"
20+
"path/filepath"
21+
"testing"
22+
23+
"cloud.google.com/java/internal/librariangen/message"
24+
"github.com/google/go-cmp/cmp"
25+
)
26+
27+
func TestReadReleaseInitRequest(t *testing.T) {
28+
want := &message.ReleaseInitRequest{
29+
Libraries: []*message.Library{
30+
{
31+
ID: "google-cloud-secretmanager-v1",
32+
Version: "1.3.0",
33+
Changes: []*message.Change{
34+
{
35+
Type: "feat",
36+
Subject: "add new UpdateRepository API",
37+
Body: "This adds the ability to update a repository's properties.",
38+
PiperCLNumber: "786353207",
39+
CommitHash: "9461532e7d19c8d71709ec3b502e5d81340fb661",
40+
},
41+
{
42+
Type: "docs",
43+
Subject: "fix typo in BranchRule comment",
44+
Body: "",
45+
PiperCLNumber: "786353207",
46+
CommitHash: "9461532e7d19c8d71709ec3b502e5d81340fb661",
47+
},
48+
},
49+
APIs: []message.API{
50+
{
51+
Path: "google/cloud/secretmanager/v1",
52+
},
53+
{
54+
Path: "google/cloud/secretmanager/v1beta",
55+
},
56+
},
57+
SourcePaths: []string{
58+
"secretmanager",
59+
"other/location/secretmanager",
60+
},
61+
ReleaseTriggered: true,
62+
},
63+
},
64+
}
65+
bytes, err := os.ReadFile(filepath.Join("..", "testdata", "release-init-request.json"))
66+
if err != nil {
67+
t.Fatal(err)
68+
}
69+
got := &message.ReleaseInitRequest{}
70+
if err := json.Unmarshal(bytes, got); err != nil {
71+
t.Fatal(err)
72+
}
73+
if diff := cmp.Diff(want, got); diff != "" {
74+
t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff)
75+
}
76+
}

0 commit comments

Comments
 (0)