Skip to content

Commit 0be6af3

Browse files
authored
feat(librariangen): first part of implementing configure (#12944)
This is effectively the skeleton around the real work, which is yet to be implemented. Towards googleapis/librarian#981
1 parent 7a85df3 commit 0be6af3

File tree

6 files changed

+518
-4
lines changed

6 files changed

+518
-4
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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 configure
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"errors"
21+
"fmt"
22+
"log/slog"
23+
"os"
24+
"path/filepath"
25+
26+
"cloud.google.com/go/internal/postprocessor/librarian/librariangen/request"
27+
)
28+
29+
// NewAPIStatus is the API.Status value used to represent "this is a new API being configured".
30+
const NewAPIStatus = "new"
31+
32+
// Test substitution vars.
33+
var (
34+
requestParse = Parse
35+
responseSave = saveResponse
36+
)
37+
38+
// Config holds the internal librariangen configuration for the configure command.
39+
type Config struct {
40+
// LibrarianDir is the path to the librarian-tool input directory.
41+
// It is expected to contain the configure-request.json file.
42+
LibrarianDir string
43+
// InputDir is the path to the .librarian/generator-input directory from the
44+
// language repository.
45+
InputDir string
46+
// OutputDir is the path to the empty directory where librariangen writes
47+
// its output for global files.
48+
OutputDir string
49+
// SourceDir is the path to a complete checkout of the googleapis repository.
50+
SourceDir string
51+
// RepoDir is the path to a read-only mount of existing relevant (global or library-specific)
52+
// files in the language repository.
53+
RepoDir string
54+
}
55+
56+
// Validate ensures that the configuration is valid.
57+
func (c *Config) Validate() error {
58+
if c.LibrarianDir == "" {
59+
return errors.New("librariangen: librarian directory must be set")
60+
}
61+
if c.InputDir == "" {
62+
return errors.New("librariangen: input directory must be set")
63+
}
64+
if c.OutputDir == "" {
65+
return errors.New("librariangen: output directory must be set")
66+
}
67+
if c.SourceDir == "" {
68+
return errors.New("librariangen: source directory must be set")
69+
}
70+
if c.RepoDir == "" {
71+
return errors.New("librariangen: repo directory must be set")
72+
}
73+
return nil
74+
}
75+
76+
// Configure configures a new library, or a new API within an existing library.
77+
// This is effectively the entry point of the "configure" container command.
78+
func Configure(ctx context.Context, cfg *Config) error {
79+
if err := cfg.Validate(); err != nil {
80+
return fmt.Errorf("librariangen: invalid configuration: %w", err)
81+
}
82+
slog.Debug("librariangen: configure command started")
83+
configureReq, err := readConfigureReq(cfg.LibrarianDir)
84+
if err != nil {
85+
return fmt.Errorf("librariangen: failed to read request: %w", err)
86+
}
87+
library, err := findLibraryToConfigure(configureReq)
88+
if err != nil {
89+
return err
90+
}
91+
92+
response, err := configureLibrary(cfg, library)
93+
if err != nil {
94+
return err
95+
}
96+
if err := saveConfigureResp(response, cfg.LibrarianDir); err != nil {
97+
return fmt.Errorf("librariangen: failed to save response: %w", err)
98+
}
99+
100+
return nil
101+
}
102+
103+
// readConfigureReq reads generate-request.json from the librarian-tool input directory.
104+
// The request file tells librariangen which library and APIs to generate.
105+
// It is prepared by the Librarian tool and mounted at /librarian.
106+
func readConfigureReq(librarianDir string) (*Request, error) {
107+
reqPath := filepath.Join(librarianDir, "configure-request.json")
108+
slog.Debug("librariangen: reading generate request", "path", reqPath)
109+
110+
configureReq, err := requestParse(reqPath)
111+
if err != nil {
112+
return nil, err
113+
}
114+
slog.Debug("librariangen: successfully unmarshalled request")
115+
return configureReq, nil
116+
}
117+
118+
// saveConfigureResp saves the response in configure-response.json in the librarian-tool input directory.
119+
// The response file tells Librarian how to reconfigure the library in its state file.
120+
func saveConfigureResp(resp *request.Library, librarianDir string) error {
121+
respPath := filepath.Join(librarianDir, "configure-response.json")
122+
slog.Debug("librariangen: saving configure response", "path", respPath)
123+
124+
if err := responseSave(resp, respPath); err != nil {
125+
return err
126+
}
127+
slog.Debug("librariangen: successfully marshalled response")
128+
return nil
129+
}
130+
131+
func findLibraryToConfigure(req *Request) (*request.Library, error) {
132+
var library *request.Library
133+
for _, candidate := range req.Libraries {
134+
var hasNewAPI bool
135+
for _, api := range candidate.APIs {
136+
if api.Status == NewAPIStatus {
137+
if hasNewAPI {
138+
return nil, fmt.Errorf("librariangen: library %s has multiple new APIs", candidate.ID)
139+
}
140+
hasNewAPI = true
141+
}
142+
}
143+
if hasNewAPI {
144+
if library != nil {
145+
return nil, fmt.Errorf("librariangen: multiple libraries have new APIs (at least %s and %s)", library.ID, candidate.ID)
146+
}
147+
library = candidate
148+
}
149+
}
150+
if library == nil {
151+
return nil, fmt.Errorf("librariangen: no libraries have new APIs")
152+
}
153+
return library, nil
154+
}
155+
156+
// configureLibrary performs the real work of configuring a new or updated module,
157+
// creating files and populating the state file entry.
158+
func configureLibrary(cfg *Config, library *request.Library) (*request.Library, error) {
159+
return nil, errors.New("configure unimplemented")
160+
}
161+
162+
// Request corresponds to a librarian configure request.
163+
// It is unmarshalled from the configure-request.json file. Note that
164+
// this request is in a different form from most other requests, as it
165+
// contains all libraries.
166+
type Request struct {
167+
// All libraries configured within the repository.
168+
Libraries []*request.Library `json:"libraries"`
169+
}
170+
171+
// Parse reads a configure-request.json file from the given path and unmarshals
172+
// it into a ConfigureRequest struct.
173+
func Parse(path string) (*Request, error) {
174+
data, err := os.ReadFile(path)
175+
if err != nil {
176+
return nil, fmt.Errorf("librariangen: failed to read request file from %s: %w", path, err)
177+
}
178+
179+
var req Request
180+
if err := json.Unmarshal(data, &req); err != nil {
181+
return nil, fmt.Errorf("librariangen: failed to unmarshal request file %s: %w", path, err)
182+
}
183+
184+
return &req, nil
185+
}

0 commit comments

Comments
 (0)