Skip to content

Commit

Permalink
add minimum service config support (#79)
Browse files Browse the repository at this point in the history
* add minimum service config support

* add serviceConfig type

* fix pkg comment spacing
  • Loading branch information
noahdietz authored Jan 17, 2019
1 parent b1be68f commit ed83cd0
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 21 deletions.
3 changes: 3 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# THIS SCRIPT IS MEANT ONLY TO BE USED IN THE GAPIC-GENERATOR-GO DOCKER IMAGE

GO_GAPIC_PACKAGE=
GAPIC_SERVICE_CONFIG=

# enable extended globbing for flag pattern matching
shopt -s extglob
Expand All @@ -24,6 +25,7 @@ shopt -s extglob
while true; do
case "$1" in
--go-gapic-package ) GO_GAPIC_PACKAGE="go-gapic-package=$2"; shift 2 ;;
--gapic-service-config ) GAPIC_SERVICE_CONFIG="gapic-service-config=/conf/$2"; shift 2;;
--go-gapic* ) echo "Skipping unrecognized go-gapic flag: $1" >&2; shift ;;
--* | +([[:word:][:punct:]]) ) shift ;;
* ) break ;;
Expand All @@ -38,4 +40,5 @@ fi
protoc --proto_path=/protos/ --proto_path=/in/ \
--go_gapic_out=/out/ \
--go_gapic_opt="$GO_GAPIC_PACKAGE" \
--go_gapic_opt="$GAPIC_SERVICE_CONFIG" \
`find /in/ -name *.proto`
1 change: 1 addition & 0 deletions gapic.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fi

# Generate the client library.
docker run \
--mount type=bind,source=${PROTO_PATH},destination=/conf,readonly \
--mount type=bind,source=${PROTO_PATH}/${IN},destination=/in/${IN},readonly \
--mount type=bind,source=$OUT,destination=/out \
--rm \
Expand Down
13 changes: 9 additions & 4 deletions internal/gengapic/client_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,19 @@ func (g *generator) clientOptions(serv *descriptor.ServiceDescriptorProto, servN

// defaultClientOptions
{
eHost, err := proto.GetExtension(serv.Options, annotations.E_DefaultHost)
if err != nil {
var host string
if eHost, err := proto.GetExtension(serv.Options, annotations.E_DefaultHost); err == nil {
host = *eHost.(*string)
} else if g.serviceConfig != nil {
// TODO(ndietz) remove this once default_host annotation is acepted
host = g.serviceConfig.Name
} else {
return errors.E(err, "cannot read default host")
}

p("func default%sClientOptions() []option.ClientOption {", servName)
p(" return []option.ClientOption{")
p(` option.WithEndpoint("%s:443"),`, *eHost.(*string))
p(` option.WithEndpoint("%s:443"),`, host)
p(" option.WithScopes(DefaultAuthScopes()...),")
p(" }")
p("}")
Expand Down Expand Up @@ -187,7 +192,7 @@ func (g *generator) clientInit(serv *descriptor.ServiceDescriptorProto, servName

// client struct
{
p("// %sClient is a client for interacting with %s API.", servName, g.apiName)
p("// %sClient is a client for interacting with %s.", servName, g.apiName)
p("//")
p("// Methods, except Close, may be called concurrently. However, fields must not be modified concurrently with method calls.")
p("type %sClient struct {", servName)
Expand Down
2 changes: 1 addition & 1 deletion internal/gengapic/client_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestClientOpt(t *testing.T) {

func TestClientInit(t *testing.T) {
var g generator
g.apiName = "Awesome Foo"
g.apiName = "Awesome Foo API"
g.imports = map[pbinfo.ImportSpec]bool{}

servPlain := &descriptor.ServiceDescriptorProto{
Expand Down
66 changes: 63 additions & 3 deletions internal/gengapic/doc_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package gengapic

import (
"sort"
"strings"

"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/protoc-gen-go/descriptor"
Expand All @@ -34,9 +35,22 @@ func (g *generator) genDocFile(pkgPath, pkgName string, year int, scopes []strin
p(license.Apache, year)
p("")

if an := g.apiName; an != "" {
if g.apiName != "" {
p("// Package %s is an auto-generated package for the", pkgName)
p("// %s API.", an)
p("// %s.", g.apiName)
}

// TODO(ndietz) figure out how to include this without the service config
if g.serviceConfig != nil && g.serviceConfig.Documentation != nil {
wrapped := wrapString(g.serviceConfig.Documentation.Summary, 75)

if len(wrapped) > 0 && g.apiName != "" {
p("//")
}

for _, line := range wrapped {
p("// %s", strings.TrimSpace(line))
}
}

p("package %s // import %q", pkgName, pkgPath)
Expand Down Expand Up @@ -119,9 +133,14 @@ func (g *generator) genDocFile(pkgPath, pkgName string, year int, scopes []strin
}
}

func collectScopes(servs []*descriptor.ServiceDescriptorProto) ([]string, error) {
func collectScopes(servs []*descriptor.ServiceDescriptorProto, config *serviceConfig) ([]string, error) {
scopeSet := map[string]bool{}
for _, s := range servs {
// TODO(ndietz) remove this once oauth scopes annotation is accepted
if s.GetOptions() == nil {
continue
}

eOauth, err := proto.GetExtension(s.Options, annotations.E_Oauth)
if err == proto.ErrMissingExtension {
continue
Expand All @@ -134,10 +153,51 @@ func collectScopes(servs []*descriptor.ServiceDescriptorProto) ([]string, error)
}
}

// TODO(ndietz) remove this once oauth scopes annotation is accepted
if len(scopeSet) == 0 && config != nil && config.Authentication != nil {
if len(config.Authentication.Rules) > 0 {
for _, rule := range config.Authentication.Rules {
if rule.Selector == "*" {
if rule.Oauth != nil {
if rule.Oauth.CanonicalScopes != "nil" {
scopes := strings.Split(rule.Oauth.CanonicalScopes, ",")
for _, sc := range scopes {
scopeSet[sc] = true
}
break
}
}
}
}
}
}

var scopes []string
for sc := range scopeSet {
scopes = append(scopes, sc)
}
sort.Strings(scopes)
return scopes, nil
}

func wrapString(str string, max int) []string {
var lines []string
var line string

if str == "" {
return lines
}

split := strings.Fields(str)
for _, w := range split {
if len(line)+len(w)+1 > max {
lines = append(lines, line)
line = ""
}

line += " " + w
}
lines = append(lines, line)

return lines
}
8 changes: 7 additions & 1 deletion internal/gengapic/doc_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ import (

func TestDocFile(t *testing.T) {
var g generator
g.apiName = "Awesome Foo"
g.apiName = "Awesome Foo API"
g.serviceConfig = &serviceConfig{
Documentation: &configDocumentation{
Summary: "The Awesome Foo API is really really awesome. It enables the use of Foo with Buz and Baz to acclerate bar.",
},
}

g.genDocFile("path/to/awesome", "awesome", 42, []string{"https://foo.bar.com/auth", "https://zip.zap.com/auth"})
txtdiff.Diff(t, "doc_file", g.pt.String(), filepath.Join("testdata", "doc_file.want"))
}
49 changes: 37 additions & 12 deletions internal/gengapic/gengapic.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ package gengapic

import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"unicode"
"unicode/utf8"

"gopkg.in/yaml.v2"

"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/protoc-gen-go/descriptor"
plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
Expand All @@ -42,30 +45,44 @@ const (

func Gen(genReq *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, error) {
var pkgPath, pkgName, outDir string
var g generator

if genReq.Parameter == nil {
return nil, errors.E(nil, paramError)
}

// parse plugin params, ignoring unknown values
for _, s := range strings.Split(*genReq.Parameter, ",") {
if e := strings.IndexByte(*genReq.Parameter, '='); e > 0 && s[:e] == "go-gapic-package" {
p := strings.IndexByte(*genReq.Parameter, ';')

if p < 0 {
return nil, errors.E(nil, paramError)
if e := strings.IndexByte(s, '='); e > 0 {
switch s[:e] {
case "go-gapic-package":
p := strings.IndexByte(s, ';')

if p < 0 {
return nil, errors.E(nil, paramError)
}

pkgPath = s[e+1 : p]
pkgName = s[p+1:]
outDir = filepath.FromSlash(pkgPath)
case "gapic-service-config":
f, err := os.Open(s[e+1:])
if err != nil {
return nil, errors.E(nil, "error opening service config: %v", err)
}

err = yaml.NewDecoder(f).Decode(&g.serviceConfig)
if err != nil {
return nil, errors.E(nil, "error decoding service config: %v", err)
}
}

pkgPath = (*genReq.Parameter)[e+1 : p]
pkgName = (*genReq.Parameter)[p+1:]
outDir = filepath.FromSlash(pkgPath)
}
}

if pkgPath == "" || pkgName == "" || outDir == "" {
return nil, errors.E(nil, paramError)
}

var g generator
g.init(genReq.ProtoFile)

var genServs []*descriptor.ServiceDescriptorProto
Expand All @@ -83,11 +100,16 @@ func Gen(genReq *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, er
}
}
}

// favor annotations over service config
if eMeta != nil {
// Without this, the doc is going to be a little bad but this is not an error.
nameParts := append([]string(nil), eMeta.PackageNamespace...)
nameParts = append(nameParts, eMeta.ProductName)
nameParts = append(nameParts, eMeta.ProductName, "API")
g.apiName = strings.Join(nameParts, " ")
} else if g.serviceConfig != nil {
// TODO(ndietz) remove this once metadata/packaging annotations are accepted
g.apiName = g.serviceConfig.Title
}

for _, s := range genServs {
Expand All @@ -114,7 +136,7 @@ func Gen(genReq *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, er
}

g.reset()
scopes, err := collectScopes(genServs)
scopes, err := collectScopes(genServs, g.serviceConfig)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -150,6 +172,9 @@ type generator struct {

// Human-readable name of the API used in docs
apiName string

// Parsed service config from plugin option
serviceConfig *serviceConfig
}

func (g *generator) init(files []*descriptor.FileDescriptorProto) {
Expand Down
49 changes: 49 additions & 0 deletions internal/gengapic/service_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2019 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 gengapic

// serviceConfig represents a gapic service config
// Deprecated: workaround for not having annotations yet; to be removed
type serviceConfig struct {
Name string
Title string
Documentation *configDocumentation
Authentication *configAuthentication
}

// configDocumentation represents gapic service config documentation section
// Deprecated: workaround for not having annotations yet; to be removed
type configDocumentation struct {
Summary string
}

// configAuthentication represents gapic service config authentication section
// Deprecated: workaround for not having annotations yet; to be removed
type configAuthentication struct {
Rules []*configAuthRules
}

// configAuthRules represents gapic service config auth rules
// Deprecated: workaround for not having annotations yet; to be removed
type configAuthRules struct {
Selector string
Oauth *configAuthRulesOauth
}

// configAuthRulesOauth represents a gapic service config oauth rule
// Deprecated: workaround for not having annotations yet; to be removed
type configAuthRulesOauth struct {
CanonicalScopes string `yaml:"canonical_scopes"`
}
3 changes: 3 additions & 0 deletions internal/gengapic/testdata/doc_file.want
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

// Package awesome is an auto-generated package for the
// Awesome Foo API.
//
// The Awesome Foo API is really really awesome. It enables the use of Foo
// with Buz and Baz to acclerate bar.
package awesome // import "path/to/awesome"

import (
Expand Down

0 comments on commit ed83cd0

Please sign in to comment.