-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add a prototype generator (#20)
This prototype is meant to be run as a protoc plugin and yield a HTTP/JSON client library. It is missing many features and has many todos. Currently this is only tested by one "golden" style test validates generated output matches cached testdata.
- Loading branch information
Showing
27 changed files
with
3,201 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Copyright 2024 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 | ||
# | ||
# https://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. | ||
|
||
golden: | ||
go test github.com/googleapis/google-cloud-rust/generator/cmd/protoc-gen-gclient -update-golden | ||
|
||
test: | ||
go test ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# generator | ||
|
||
A tool for generating client libraries. | ||
|
||
## Example Run | ||
|
||
Run the following command from the generator directory: | ||
|
||
```bash | ||
go install github.com/googleapis/google-cloud-rust/generator/cmd/protoc-gen-gclient | ||
protoc -I cmd/protoc-gen-gclient/testdata/smprotos \ | ||
-I /path/to/googleapis \ | ||
--gclient_out=. \ | ||
--gclient_opt=capture-input=true,language=rust \ | ||
cmd/protoc-gen-gclient/testdata/smprotos/resources.proto \ | ||
cmd/protoc-gen-gclient/testdata/smprotos/service.proto | ||
``` | ||
|
||
or to playback an old input without the need for `protoc`: | ||
|
||
```bash | ||
go run github.com/googleapis/google-cloud-rust/generator/cmd/protoc-gen-gclient -input-path=cmd/protoc-gen-gclient/testdata/rust/rust.bin | ||
``` | ||
|
||
## General TODOs | ||
|
||
- convert proto links into nice rustdoc | ||
- fix documentation indentation after first line |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
// Copyright 2024 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 | ||
// | ||
// https://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 main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"io" | ||
"log/slog" | ||
"os" | ||
"slices" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/googleapis/google-cloud-rust/generator/src/genclient" | ||
"github.com/googleapis/google-cloud-rust/generator/src/genclient/translator/protobuf" | ||
"google.golang.org/protobuf/proto" | ||
"google.golang.org/protobuf/types/pluginpb" | ||
) | ||
|
||
func main() { | ||
inputPath := flag.String("input-path", "", "the path to a binary input file in the format of pluginpb.CodeGeneratorRequest") | ||
outDir := flag.String("out-dir", "", "the path to the output directory") | ||
templateDir := flag.String("template-dir", "templates/", "the path to the template directory") | ||
flag.Parse() | ||
|
||
if err := run(*inputPath, *outDir, *templateDir); err != nil { | ||
slog.Error(err.Error()) | ||
os.Exit(1) | ||
} | ||
slog.Info("Generation Completed Successfully") | ||
} | ||
|
||
func run(inputPath, outDir, templateDir string) error { | ||
var reqBytes []byte | ||
var err error | ||
if inputPath == "" { | ||
reqBytes, err = io.ReadAll(os.Stdin) | ||
if err != nil { | ||
return err | ||
} | ||
} else { | ||
reqBytes, err = os.ReadFile(inputPath) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
genReq := &pluginpb.CodeGeneratorRequest{} | ||
if err := proto.Unmarshal(reqBytes, genReq); err != nil { | ||
return err | ||
} | ||
|
||
opts, err := parseOpts(genReq.GetParameter()) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if opts.CaptureInput { | ||
// Remove capture-input param from the captured input | ||
ss := slices.DeleteFunc(strings.Split(genReq.GetParameter(), ","), func(s string) bool { | ||
return strings.Contains(s, "capture-input") | ||
}) | ||
genReq.Parameter = proto.String(strings.Join(ss, ",")) | ||
reqBytes, err = proto.Marshal(genReq) | ||
if err != nil { | ||
return err | ||
} | ||
if err := os.WriteFile(fmt.Sprintf("sample-input-%s.bin", time.Now().Format(time.RFC3339)), reqBytes, 0644); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
req, err := protobuf.NewTranslator(&protobuf.Options{ | ||
Request: genReq, | ||
OutDir: outDir, | ||
Language: opts.Language, | ||
TemplateDir: templateDir, | ||
}).Translate() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
resp := protobuf.NewCodeGeneratorResponse(genclient.Generate(req)) | ||
outBytes, err := proto.Marshal(resp) | ||
if err != nil { | ||
return err | ||
} | ||
if _, err := os.Stdout.Write(outBytes); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type protobufOptions struct { | ||
CaptureInput bool | ||
Language string | ||
} | ||
|
||
func parseOpts(optStr string) (*protobufOptions, error) { | ||
opts := &protobufOptions{} | ||
for _, s := range strings.Split(strings.TrimSpace(optStr), ",") { | ||
if s == "" { | ||
slog.Warn("empty option string, skipping") | ||
continue | ||
} | ||
sp := strings.Split(s, "=") | ||
if len(sp) > 2 { | ||
slog.Warn("too many `=` in option string, skipping", "option", s) | ||
continue | ||
} | ||
switch sp[0] { | ||
case "capture-input": | ||
b, err := strconv.ParseBool(sp[1]) | ||
if err != nil { | ||
slog.Error("invalid bool in option string, skipping", "option", s) | ||
return nil, err | ||
} | ||
opts.CaptureInput = b | ||
case "language": | ||
opts.Language = strings.ToLower(strings.TrimSpace(sp[1])) | ||
default: | ||
slog.Warn("unknown option", "option", s) | ||
} | ||
} | ||
return opts, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// Copyright 2024 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 | ||
// | ||
// https://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 main | ||
|
||
import ( | ||
"flag" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
) | ||
|
||
var updateGolden = flag.Bool("update-golden", false, "update golden files") | ||
|
||
func TestMain(m *testing.M) { | ||
flag.Parse() | ||
os.Exit(m.Run()) | ||
} | ||
|
||
func TestRun_Rust(t *testing.T) { | ||
tDir := t.TempDir() | ||
if err := run("testdata/rust/rust.bin", tDir, "../../templates"); err != nil { | ||
t.Fatal(err) | ||
} | ||
diff(t, "testdata/rust/golden", tDir) | ||
} | ||
|
||
func diff(t *testing.T, goldenDir, outputDir string) { | ||
files, err := os.ReadDir(outputDir) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if *updateGolden { | ||
for _, f := range files { | ||
b, err := os.ReadFile(filepath.Join(outputDir, f.Name())) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
outFileName := filepath.Join(goldenDir, f.Name()) | ||
t.Logf("writing golden file %s", outFileName) | ||
if err := os.WriteFile(outFileName, b, os.ModePerm); err != nil { | ||
t.Fatal(err) | ||
} | ||
} | ||
return | ||
} | ||
for _, f := range files { | ||
want, err := os.ReadFile(filepath.Join(goldenDir, f.Name())) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
got, err := os.ReadFile(filepath.Join(outputDir, f.Name())) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if diff := cmp.Diff(want, got); diff != "" { | ||
t.Errorf("mismatch(-want, +got): %s", diff) | ||
} | ||
} | ||
} |
97 changes: 97 additions & 0 deletions
97
generator/cmd/protoc-gen-gclient/testdata/rust/golden/lib.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
use std::sync::Arc; | ||
|
||
pub mod model; | ||
|
||
#[derive(Clone, Debug)] | ||
pub struct Client { | ||
inner: Arc<ClientRef>, | ||
} | ||
|
||
#[derive(Debug)] | ||
struct ClientRef { | ||
http_client: reqwest::Client, | ||
token: String, | ||
} | ||
|
||
impl Client { | ||
pub fn new(tok: String) -> Self { | ||
let client = reqwest::Client::builder().build().unwrap(); | ||
let inner = ClientRef { | ||
http_client: client, | ||
token: tok, | ||
}; | ||
Self { | ||
inner: Arc::new(inner), | ||
} | ||
} | ||
|
||
/// Secret Manager Service | ||
/// | ||
/// Manages secrets and operations using those secrets. Implements a REST | ||
/// model with the following objects: | ||
/// | ||
/// * [Secret][google.cloud.secretmanager.v1.Secret] | ||
/// * [SecretVersion][google.cloud.secretmanager.v1.SecretVersion] | ||
pub fn secret_manager_service(&self) -> SecretManagerService { | ||
SecretManagerService { | ||
client: self.clone(), | ||
base_path: "https://secretmanager.googleapis.com/".to_string(), | ||
} | ||
} | ||
} | ||
|
||
/// Secret Manager Service | ||
/// | ||
/// Manages secrets and operations using those secrets. Implements a REST | ||
/// model with the following objects: | ||
/// | ||
/// * [Secret][google.cloud.secretmanager.v1.Secret] | ||
/// * [SecretVersion][google.cloud.secretmanager.v1.SecretVersion] | ||
#[derive(Debug)] | ||
pub struct SecretManagerService { | ||
client: Client, | ||
base_path: String, | ||
} | ||
|
||
impl SecretManagerService { | ||
|
||
/// Creates a new [Secret][google.cloud.secretmanager.v1.Secret] containing no | ||
/// [SecretVersions][google.cloud.secretmanager.v1.SecretVersion]. | ||
pub async fn create_secret(&self, req: model::CreateSecretRequest) -> Result<model::Secret, Box<dyn std::error::Error>> { | ||
let client = self.client.inner.clone(); | ||
let res = client.http_client | ||
.post(format!( | ||
"{}/v1/{}/secrets", | ||
self.base_path, | ||
req.parent, | ||
)) | ||
.query(&[("alt", "json")]) | ||
.query(&[("secretId", req.secret_id.as_str())]) | ||
.bearer_auth(&client.token) | ||
.json(&req.secret) | ||
.send().await?; | ||
if !res.status().is_success() { | ||
return Err("sorry the api you are looking for is not available, please try again".into()); | ||
} | ||
res.json::<model::Secret>.await? | ||
} | ||
|
||
/// Gets metadata for a given [Secret][google.cloud.secretmanager.v1.Secret]. | ||
pub async fn get_secret(&self, req: model::GetSecretRequest) -> Result<model::Secret, Box<dyn std::error::Error>> { | ||
let client = self.client.inner.clone(); | ||
let res = client.http_client | ||
.get(format!( | ||
"{}/v1/{}", | ||
self.base_path, | ||
req.name, | ||
)) | ||
.query(&[("alt", "json")]) | ||
.query(&[("name", req.name.as_str())]) | ||
.bearer_auth(&client.token) | ||
.send().await?; | ||
if !res.status().is_success() { | ||
return Err("sorry the api you are looking for is not available, please try again".into()); | ||
} | ||
res.json::<model::Secret>.await? | ||
} | ||
} |
Oops, something went wrong.