Skip to content

Commit f7a6555

Browse files
feat(dart): Ability to load API keys from env vars (#2538)
Requires APIs with services to set `api-keys-environment-variables` e.g. `api-keys-environment-variables = "GOOGLE_API_KEY,GEMINI_API_KEY"` The generated Dart code looks like: ```dart factory GenerativeService.fromApiKey([String? apiKey]) { if (apiKey == null) { for (var key in _apiKeys) { apiKey = getEnvironmentVariable(key); if (apiKey != null) break; } } if (apiKey == null) { throw ArgumentError( 'apiKey or one of these environment variables must ' 'be set to an API key: ${_apiKeys.join(", ")}', ); } return GenerativeService(client: auth.clientViaApiKey(apiKey)); } ``` --------- Signed-off-by: Brian Quinlan <[email protected]> Co-authored-by: Nate Bosch <[email protected]>
1 parent 43554e7 commit f7a6555

File tree

6 files changed

+86
-41
lines changed

6 files changed

+86
-41
lines changed

internal/sidekick/internal/dart/annotate.go

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package dart
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"log/slog"
2021
"slices"
@@ -47,14 +48,15 @@ type modelAnnotations struct {
4748
DefaultHost string
4849
DocLines []string
4950
// A reference to an optional hand-written part file.
50-
PartFileReference string
51-
PackageDependencies []packageDependency
52-
Imports []string
53-
DevDependencies []string
54-
DoNotPublish bool
55-
RepositoryURL string
56-
ReadMeAfterTitleText string
57-
ReadMeQuickstartText string
51+
PartFileReference string
52+
PackageDependencies []packageDependency
53+
Imports []string
54+
DevDependencies []string
55+
DoNotPublish bool
56+
RepositoryURL string
57+
ReadMeAfterTitleText string
58+
ReadMeQuickstartText string
59+
ApiKeyEnvironmentVariables []string
5860
}
5961

6062
// HasServices returns true if the model has services.
@@ -216,19 +218,28 @@ func newAnnotateModel(model *api.API) *annotateModel {
216218
// [Template.Services] field.
217219
func (annotate *annotateModel) annotateModel(options map[string]string) error {
218220
var (
219-
packageNameOverride string
220-
generationYear string
221-
packageVersion string
222-
partFileReference string
223-
doNotPublish bool
224-
devDependencies = []string{}
225-
repositoryURL string
226-
readMeAfterTitleText string
227-
readMeQuickstartText string
221+
packageNameOverride string
222+
generationYear string
223+
packageVersion string
224+
partFileReference string
225+
doNotPublish bool
226+
devDependencies = []string{}
227+
repositoryURL string
228+
readMeAfterTitleText string
229+
readMeQuickstartText string
230+
apiKeyEnvironmentVariables = []string{}
228231
)
229232

230233
for key, definition := range options {
231234
switch {
235+
case key == "api-keys-environment-variables":
236+
// api-keys-environment-variables = "GOOGLE_API_KEY,GEMINI_API_KEY"
237+
// A comma-separated list of environment variables to look for searching for
238+
// a API key.
239+
apiKeyEnvironmentVariables = strings.Split(definition, ",")
240+
for i := range apiKeyEnvironmentVariables {
241+
apiKeyEnvironmentVariables[i] = strings.TrimSpace(apiKeyEnvironmentVariables[i])
242+
}
232243
case key == "package-name-override":
233244
packageNameOverride = definition
234245
case key == "copyright-year":
@@ -321,6 +332,11 @@ func (annotate *annotateModel) annotateModel(options map[string]string) error {
321332
if err != nil {
322333
return err
323334
}
335+
336+
if len(model.Services) > 0 && len(apiKeyEnvironmentVariables) == 0 {
337+
return errors.New("all packages that define a service must define 'api-keys-environment-variables'")
338+
}
339+
324340
ann := &modelAnnotations{
325341
Parent: model,
326342
PackageName: packageName(model, packageNameOverride),
@@ -336,15 +352,16 @@ func (annotate *annotateModel) annotateModel(options map[string]string) error {
336352
}
337353
return ""
338354
}(),
339-
DocLines: formatDocComments(model.Description, model.State),
340-
Imports: calculateImports(annotate.imports),
341-
PartFileReference: partFileReference,
342-
PackageDependencies: packageDependencies,
343-
DevDependencies: devDependencies,
344-
DoNotPublish: doNotPublish,
345-
RepositoryURL: repositoryURL,
346-
ReadMeAfterTitleText: readMeAfterTitleText,
347-
ReadMeQuickstartText: readMeQuickstartText,
355+
DocLines: formatDocComments(model.Description, model.State),
356+
Imports: calculateImports(annotate.imports),
357+
PartFileReference: partFileReference,
358+
PackageDependencies: packageDependencies,
359+
DevDependencies: devDependencies,
360+
DoNotPublish: doNotPublish,
361+
RepositoryURL: repositoryURL,
362+
ReadMeAfterTitleText: readMeAfterTitleText,
363+
ReadMeQuickstartText: readMeQuickstartText,
364+
ApiKeyEnvironmentVariables: apiKeyEnvironmentVariables,
348365
}
349366

350367
model.Codec = ann
@@ -438,6 +455,7 @@ func calculateImports(imports map[string]bool) []string {
438455
func (annotate *annotateModel) annotateService(s *api.Service) {
439456
// Add a package:http import if we're generating a service.
440457
annotate.imports[httpImport] = true
458+
annotate.imports[authImport] = true
441459

442460
// Some methods are skipped.
443461
methods := language.FilterSlice(s.Methods, func(m *api.Method) bool {

internal/sidekick/internal/dart/annotate_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import (
2525

2626
var (
2727
requiredConfig = map[string]string{
28-
"package:google_cloud_gax": "^1.2.3",
29-
"package:http": "^4.5.6"}
28+
"api-keys-environment-variables": "GOOGLE_API_KEY,GEMINI_API_KEY",
29+
"package:googleapis_auth": "^2.0.0",
30+
"package:google_cloud_gax": "^1.2.3",
31+
"package:http": "^4.5.6"}
3032
)
3133

3234
func TestAnnotateModel(t *testing.T) {
@@ -121,7 +123,11 @@ func TestAnnotateModel_Options(t *testing.T) {
121123
{
122124
map[string]string{"google_cloud_gax": "^1.2.3", "package:http": "1.2.0"},
123125
func(t *testing.T, am *annotateModel) {
124-
if diff := cmp.Diff(map[string]string{"google_cloud_gax": "^1.2.3", "http": "1.2.0"}, am.dependencyConstraints); diff != "" {
126+
if diff := cmp.Diff(map[string]string{
127+
"google_cloud_gax": "^1.2.3",
128+
"googleapis_auth": "^2.0.0",
129+
"http": "1.2.0"},
130+
am.dependencyConstraints); diff != "" {
125131
t.Errorf("mismatch in annotateModel.dependencyConstraints (-want, +got)\n:%s", diff)
126132
}
127133
},

internal/sidekick/internal/dart/dart.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
)
2727

2828
var typedDataImport = "dart:typed_data"
29+
var authImport = "package:googleapis_auth/auth_io.dart as auth"
2930
var httpImport = "package:http/http.dart as http"
3031
var commonImport = "package:google_cloud_gax/gax.dart"
3132
var commonHelpersImport = "package:google_cloud_gax/src/encoding.dart"

internal/sidekick/internal/dart/generate_test.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,18 @@ func TestFromProtobuf(t *testing.T) {
4646
"googleapis-root": path.Join(testdataDir, "googleapis"),
4747
},
4848
Codec: map[string]string{
49-
"copyright-year": "2025",
50-
"not-for-publication": "true",
51-
"version": "0.1.0",
52-
"skip-format": "true",
53-
"package:google_cloud_gax": "^1.2.3",
54-
"package:http": "^4.5.6",
55-
"package:google_cloud_location": "^7.8.9",
56-
"package:google_cloud_protobuf": "^0.1.2",
57-
"proto:google.protobuf": "package:google_cloud_protobuf/protobuf.dart",
58-
"proto:google.cloud.location": "package:google_cloud_location/location.dart",
49+
"api-keys-environment-variables": "GOOGLE_API_KEY,GEMINI_API_KEY",
50+
"copyright-year": "2025",
51+
"not-for-publication": "true",
52+
"version": "0.1.0",
53+
"skip-format": "true",
54+
"package:googleapis_auth": "^2.0.0",
55+
"package:google_cloud_gax": "^1.2.3",
56+
"package:http": "^4.5.6",
57+
"package:google_cloud_location": "^7.8.9",
58+
"package:google_cloud_protobuf": "^0.1.2",
59+
"proto:google.protobuf": "package:google_cloud_protobuf/protobuf.dart",
60+
"proto:google.cloud.location": "package:google_cloud_location/location.dart",
5961
},
6062
}
6163
model, err := parser.CreateModel(cfg)

internal/sidekick/internal/dart/templates/lib/main.dart.mustache

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ library;
3232

3333
part '{{Codec.PartFileReference}}';
3434
{{/Codec.PartFileReference}}
35+
36+
{{#Codec.HasServices}}
37+
const _apiKeys = [
38+
{{#Codec.ApiKeyEnvironmentVariables}}
39+
"{{{.}}}",
40+
{{/Codec.ApiKeyEnvironmentVariables}}
41+
];
42+
{{/Codec.HasServices}}
43+
3544
{{#Services}}
3645
{{> service}}
3746
{{/Services}}

internal/sidekick/internal/dart/templates/lib/service.mustache

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@ limitations under the License.
1818
{{{.}}}
1919
{{/Codec.DocLines}}
2020
final class {{Codec.Name}} {
21-
static const String _host = '{{DefaultHost}}';
22-
21+
static const _host = '{{DefaultHost}}';
2322
final ServiceClient _client;
2423
2524
{{Codec.Name}}({required http.Client client})
2625
: _client = ServiceClient(client: client);
26+
27+
factory {{Codec.Name}}.fromApiKey([String? apiKey]) {
28+
apiKey ??= _apiKeys.map(environmentVariable).nonNulls.firstOrNull;
29+
if (apiKey == null) {
30+
throw ArgumentError('apiKey or one of these environment variables must '
31+
'be set to an API key: ${_apiKeys.join(", ")}');
32+
}
33+
return {{Codec.Name}}(client: auth.clientViaApiKey(apiKey));
34+
}
35+
2736
{{#Codec.Methods}}
2837
{{> method}}
2938
{{/Codec.Methods}}

0 commit comments

Comments
 (0)