diff --git a/internal/librariangen/go.mod b/internal/librariangen/go.mod index 52d7daf52b..87e854c400 100644 --- a/internal/librariangen/go.mod +++ b/internal/librariangen/go.mod @@ -1,3 +1,5 @@ module cloud.google.com/java/internal/librariangen go 1.24.4 + +require github.com/google/go-cmp v0.7.0 // indirect diff --git a/internal/librariangen/go.sum b/internal/librariangen/go.sum new file mode 100644 index 0000000000..40e761ae7c --- /dev/null +++ b/internal/librariangen/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= diff --git a/internal/librariangen/request/request.go b/internal/librariangen/request/request.go new file mode 100644 index 0000000000..72734e1edc --- /dev/null +++ b/internal/librariangen/request/request.go @@ -0,0 +1,78 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package request + +import ( + "encoding/json" + "fmt" + "os" +) + +// Library is the combination of all the fields used by CLI requests and responses. +// Each CLI command has its own request/response type, but they all use Library. +type Library struct { + ID string `json:"id,omitempty"` + Version string `json:"version,omitempty"` + APIs []API `json:"apis,omitempty"` + // SourcePaths are the directories to which librarian contributes code. + // For Go, this is typically the Go module directory. + SourcePaths []string `json:"source_roots,omitempty"` + // PreserveRegex are files/directories to leave untouched during generation. + // This is useful for preserving handwritten helper files or customizations. + PreserveRegex []string `json:"preserve_regex,omitempty"` + // RemoveRegex are files/directories to remove during generation. + RemoveRegex []string `json:"remove_regex,omitempty"` + // Changes are the changes being released (in a release request) + Changes []*Change `json:"changes,omitempty"` + // Specifying a tag format allows librarian to honor this format when creating + // a tag for the release of the library. The replacement values of {id} and {version} + // permitted to reference the values configured in the library. If not specified + // the assumed format is {id}-{version}. e.g., {id}/v{version}. + TagFormat string `yaml:"tag_format,omitempty" json:"tag_format,omitempty"` + // ReleaseTriggered indicates whether this library is being released (in a release request) + ReleaseTriggered bool `json:"release_triggered,omitempty"` +} + +// API corresponds to a single API definition within a librarian request/response. +type API struct { + Path string `json:"path,omitempty"` + ServiceConfig string `json:"service_config,omitempty"` +} + +// Change represents a single commit change for a library. +type Change struct { + Type string `json:"type"` + Subject string `json:"subject"` + Body string `json:"body"` + PiperCLNumber string `json:"piper_cl_number"` + SourceCommitHash string `json:"source_commit_hash"` +} + +// ParseLibrary reads a file from the given path and unmarshals +// it into a Library struct. This is used for build and generate, where the requests +// are simply the library, with no wrapping. +func ParseLibrary(path string) (*Library, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("librariangen: failed to read request file from %s: %w", path, err) + } + + var req Library + if err := json.Unmarshal(data, &req); err != nil { + return nil, fmt.Errorf("librariangen: failed to unmarshal request file %s: %w", path, err) + } + + return &req, nil +} \ No newline at end of file diff --git a/internal/librariangen/request/request_test.go b/internal/librariangen/request/request_test.go new file mode 100644 index 0000000000..03f05b2b23 --- /dev/null +++ b/internal/librariangen/request/request_test.go @@ -0,0 +1,98 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package request + +import ( + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParseLibrary(t *testing.T) { + testCases := []struct { + name string + content string + want *Library + wantErr bool + }{ + { + name: "valid request", + content: `{ + "id": "asset", + "version": "1.15.0", + "apis": [ + { + "path": "google/cloud/asset/v1", + "service_config": "cloudasset_v1.yaml" + } + ], + "source_roots": ["asset/apiv1"], + "preserve_regex": ["asset/apiv1/foo.go"], + "remove_regex": ["asset/apiv1/bar.go"] + }`, + want: &Library{ + ID: "asset", + Version: "1.15.0", + APIs: []API{ + { + Path: "google/cloud/asset/v1", + ServiceConfig: "cloudasset_v1.yaml", + }, + }, + SourcePaths: []string{"asset/apiv1"}, + PreserveRegex: []string{"asset/apiv1/foo.go"}, + RemoveRegex: []string{"asset/apiv1/bar.go"}, + }, + wantErr: false, + }, + { + name: "malformed json", + content: `{"id": "foo",`, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tmpDir := t.TempDir() + reqPath := filepath.Join(tmpDir, "generate-request.json") + if err := os.WriteFile(reqPath, []byte(tc.content), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + got, err := ParseLibrary(reqPath) + + if (err != nil) != tc.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr) + return + } + + if !tc.wantErr { + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("Parse() mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestParseLibrary_FileNotFound(t *testing.T) { + _, err := ParseLibrary("non-existent-file.json") + if err == nil { + t.Error("Parse() expected error for non-existent file, got nil") + } +} \ No newline at end of file