Skip to content

Commit 8fd2f80

Browse files
authored
impl(sidekick): parser for discovery files (#1882)
1 parent 7190537 commit 8fd2f80

File tree

4 files changed

+201
-5
lines changed

4 files changed

+201
-5
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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 parser
16+
17+
import (
18+
"os"
19+
20+
"github.com/googleapis/librarian/internal/sidekick/internal/api"
21+
"github.com/googleapis/librarian/internal/sidekick/internal/parser/discovery"
22+
"google.golang.org/genproto/googleapis/api/serviceconfig"
23+
)
24+
25+
// ParseDisco reads discovery docs specifications and converts them into
26+
// the `api.API` model.
27+
func ParseDisco(source, serviceConfigFile string, options map[string]string) (*api.API, error) {
28+
contents, err := os.ReadFile(source)
29+
if err != nil {
30+
return nil, err
31+
}
32+
var serviceConfig *serviceconfig.Service
33+
if serviceConfigFile != "" {
34+
cfg, err := readServiceConfig(findServiceConfigPath(serviceConfigFile, options))
35+
if err != nil {
36+
return nil, err
37+
}
38+
serviceConfig = cfg
39+
}
40+
return discovery.NewAPI(serviceConfig, contents)
41+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 parser
16+
17+
import (
18+
"testing"
19+
20+
"github.com/google/go-cmp/cmp"
21+
)
22+
23+
func TestDisco_Parse(t *testing.T) {
24+
// Mixing Compute and Secret Manager like this is fine in tests.
25+
got, err := ParseDisco(discoSourceFile, secretManagerYamlFullPath, map[string]string{})
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
wantName := "secretmanager"
30+
wantTitle := "Secret Manager API"
31+
wantDescription := "Stores sensitive data such as API keys, passwords, and certificates.\nProvides convenience while improving security."
32+
wantPackageName := "google.cloud.secretmanager.v1"
33+
if got.Name != wantName {
34+
t.Errorf("want = %q; got = %q", wantName, got.Name)
35+
}
36+
if got.Title != wantTitle {
37+
t.Errorf("want = %q; got = %q", wantTitle, got.Title)
38+
}
39+
if diff := cmp.Diff(got.Description, wantDescription); diff != "" {
40+
t.Errorf("description mismatch (-want, +got):\n%s", diff)
41+
}
42+
if got.PackageName != wantPackageName {
43+
t.Errorf("want = %q; got = %q", wantPackageName, got.PackageName)
44+
}
45+
}
46+
47+
func TestDisco_ParseNoServiceConfig(t *testing.T) {
48+
got, err := ParseDisco(discoSourceFile, "", map[string]string{})
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
wantName := "compute"
53+
wantTitle := "Compute Engine API"
54+
wantDescription := "Creates and runs virtual machines on Google Cloud Platform. "
55+
if got.Name != wantName {
56+
t.Errorf("want = %q; got = %q", wantName, got.Name)
57+
}
58+
if got.Title != wantTitle {
59+
t.Errorf("want = %q; got = %q", wantTitle, got.Title)
60+
}
61+
if diff := cmp.Diff(got.Description, wantDescription); diff != "" {
62+
t.Errorf("description mismatch (-want, +got):\n%s", diff)
63+
}
64+
}
65+
66+
func TestDisco_ParseBadFiles(t *testing.T) {
67+
if _, err := ParseDisco("-invalid-file-name-", secretManagerYamlFullPath, map[string]string{}); err == nil {
68+
t.Fatalf("expected error with invalid source file name")
69+
}
70+
71+
if _, err := ParseDisco(discoSourceFile, "-invalid-file-name-", map[string]string{}); err == nil {
72+
t.Fatalf("expected error with invalid service config yaml file name")
73+
}
74+
}

internal/sidekick/internal/parser/parser.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ func CreateModel(config *config.Config) (*api.API, error) {
2828
var err error
2929
var model *api.API
3030
switch config.General.SpecificationFormat {
31+
case "disco":
32+
model, err = ParseDisco(config.General.SpecificationSource, config.General.ServiceConfig, config.Source)
3133
case "openapi":
3234
model, err = ParseOpenAPI(config.General.SpecificationSource, config.General.ServiceConfig, config.Source)
3335
case "protobuf":

internal/sidekick/internal/parser/parser_test.go

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,58 @@ import (
1919
"path/filepath"
2020
"testing"
2121

22+
"github.com/google/go-cmp/cmp"
2223
"github.com/googleapis/librarian/internal/sidekick/internal/config"
2324
)
2425

2526
var (
26-
testdataDir, _ = filepath.Abs("../../testdata")
27+
testdataDir, _ = filepath.Abs("../../testdata")
28+
discoSourceFile = path.Join(testdataDir, "disco/compute.v1.json")
29+
secretManagerYamlRelative = "google/cloud/secretmanager/v1/secretmanager_v1.yaml"
30+
secretManagerYamlFullPath = path.Join(testdataDir, "googleapis", secretManagerYamlRelative)
2731
)
2832

33+
func TestCreateModelDisco(t *testing.T) {
34+
cfg := &config.Config{
35+
General: config.GeneralConfig{
36+
SpecificationFormat: "disco",
37+
ServiceConfig: secretManagerYamlFullPath,
38+
SpecificationSource: discoSourceFile,
39+
},
40+
}
41+
got, err := CreateModel(cfg)
42+
if err != nil {
43+
t.Fatal(err)
44+
}
45+
wantName := "secretmanager"
46+
wantTitle := "Secret Manager API"
47+
wantDescription := "Stores sensitive data such as API keys, passwords, and certificates.\nProvides convenience while improving security."
48+
wantPackageName := "google.cloud.secretmanager.v1"
49+
if got.Name != wantName {
50+
t.Errorf("want = %q; got = %q", wantName, got.Name)
51+
}
52+
if got.Title != wantTitle {
53+
t.Errorf("want = %q; got = %q", wantTitle, got.Title)
54+
}
55+
if diff := cmp.Diff(got.Description, wantDescription); diff != "" {
56+
t.Errorf("description mismatch (-want, +got):\n%s", diff)
57+
}
58+
if got.PackageName != wantPackageName {
59+
t.Errorf("want = %q; got = %q", wantPackageName, got.PackageName)
60+
}
61+
// This is strange, but we want to verify the package name override from
62+
// the service config YAML applies to the message IDs too.
63+
wantMessage := ".google.cloud.secretmanager.v1.ZoneSetPolicyRequest"
64+
if _, ok := got.State.MessageByID[wantMessage]; !ok {
65+
t.Errorf("missing message %s in MessageByID index", wantMessage)
66+
}
67+
}
68+
2969
func TestCreateModelOpenAPI(t *testing.T) {
3070
cfg := &config.Config{
3171
General: config.GeneralConfig{
3272
SpecificationFormat: "openapi",
33-
ServiceConfig: path.Join(testdataDir, "googleapis/google/cloud/secretmanager/v1/secretmanager_v1.yaml"),
73+
ServiceConfig: secretManagerYamlFullPath,
3474
SpecificationSource: path.Join(testdataDir, "openapi/secretmanager_openapi_v1.json"),
3575
},
3676
}
@@ -50,7 +90,7 @@ func TestCreateModelProtobuf(t *testing.T) {
5090
cfg := &config.Config{
5191
General: config.GeneralConfig{
5292
SpecificationFormat: "protobuf",
53-
ServiceConfig: "google/cloud/secretmanager/v1/secretmanager_v1.yaml",
93+
ServiceConfig: secretManagerYamlRelative,
5494
SpecificationSource: "google/cloud/secretmanager/v1",
5595
},
5696
Source: map[string]string{
@@ -73,7 +113,7 @@ func TestCreateModelOverrides(t *testing.T) {
73113
cfg := &config.Config{
74114
General: config.GeneralConfig{
75115
SpecificationFormat: "protobuf",
76-
ServiceConfig: "google/cloud/secretmanager/v1/secretmanager_v1.yaml",
116+
ServiceConfig: secretManagerYamlRelative,
77117
SpecificationSource: "google/cloud/secretmanager/v1",
78118
},
79119
Source: map[string]string{
@@ -108,7 +148,7 @@ func TestCreateModelNone(t *testing.T) {
108148
cfg := &config.Config{
109149
General: config.GeneralConfig{
110150
SpecificationFormat: "none",
111-
ServiceConfig: "google/cloud/secretmanager/v1/secretmanager_v1.yaml",
151+
ServiceConfig: secretManagerYamlRelative,
112152
SpecificationSource: "none",
113153
},
114154
Source: map[string]string{
@@ -126,3 +166,42 @@ func TestCreateModelNone(t *testing.T) {
126166
t.Errorf("expected `nil` model with source format == none")
127167
}
128168
}
169+
170+
func TestCreateModelUnknown(t *testing.T) {
171+
cfg := &config.Config{
172+
General: config.GeneralConfig{
173+
SpecificationFormat: "--unknown--",
174+
ServiceConfig: secretManagerYamlRelative,
175+
SpecificationSource: "none",
176+
},
177+
Source: map[string]string{
178+
"googleapis-root": path.Join(testdataDir, "googleapis"),
179+
"name-override": "Name Override",
180+
"title-override": "Title Override",
181+
"description-override": "Description Override",
182+
},
183+
}
184+
if got, err := CreateModel(cfg); err == nil {
185+
t.Errorf("expected error with unknown specification format, got=%v", got)
186+
}
187+
}
188+
189+
func TestCreateModelBadParse(t *testing.T) {
190+
cfg := &config.Config{
191+
General: config.GeneralConfig{
192+
SpecificationFormat: "openapi",
193+
ServiceConfig: secretManagerYamlRelative,
194+
// Note the mismatch between the format and the file contents.
195+
SpecificationSource: discoSourceFile,
196+
},
197+
Source: map[string]string{
198+
"googleapis-root": path.Join(testdataDir, "googleapis"),
199+
"name-override": "Name Override",
200+
"title-override": "Title Override",
201+
"description-override": "Description Override",
202+
},
203+
}
204+
if got, err := CreateModel(cfg); err == nil {
205+
t.Errorf("expected error with bad specification, got=%v", got)
206+
}
207+
}

0 commit comments

Comments
 (0)