Repository: googleapis/gapic-generator-go
Pull request #3 (2018-09-18 19:39:23)

initial work on samplegen


Patch:
diff --git a/cmd/gen-go-sample/gapic.go b/cmd/gen-go-sample/gapic.go
new file mode 100644
index 0000000..02fe7ec
--- /dev/null
+++ b/cmd/gen-go-sample/gapic.go
@@ -0,0 +1,48 @@
+// Copyright 2018 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
+
+type GAPICConfig struct {
+	Interfaces []GAPICInterface
+}
+
+type GAPICInterface struct {
+	Name    string
+	Methods []GAPICMethod
+}
+
+type GAPICMethod struct {
+	Name            string
+	SampleValueSets []SampleValueSet `yaml:"sample_value_sets"`
+	Samples         struct {
+		Standalone []GAPICSample
+	}
+}
+
+type SampleValueSet struct {
+	ID         string
+	Parameters SampleParameter
+}
+
+type SampleParameter struct {
+	Defaults []string
+}
+
+type GAPICSample struct {
+	ValueSets []string `yaml:"value_sets"`
+
+	// TODO(pongad): Does this mean multiple samples have the same tag?
+	RegionTag string `yaml:"region_tag"`
+}
diff --git a/cmd/gen-go-sample/main.go b/cmd/gen-go-sample/main.go
new file mode 100644
index 0000000..e364b64
--- /dev/null
+++ b/cmd/gen-go-sample/main.go
@@ -0,0 +1,246 @@
+// Copyright 2018 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 (
+	"bytes"
+	"flag"
+	"fmt"
+	"go/format"
+	"io/ioutil"
+	"log"
+	"os"
+	"sort"
+	"strings"
+
+	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/googleapis/gapic-generator-go/internal/errors"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
+	"github.com/googleapis/gapic-generator-go/internal/printer"
+	yaml "gopkg.in/yaml.v2"
+)
+
+func main() {
+	descFname := flag.String("desc", "", "proto descriptor")
+	gapicFname := flag.String("gapic", "", "gapic config")
+	nofmt := flag.Bool("nofmt", false, "skip gofmt, useful for debugging code with syntax error")
+	flag.Parse()
+
+	var (
+		gen = generator{
+			imports: map[pbinfo.ImportSpec]bool{},
+		}
+
+		donec = make(chan struct{})
+	)
+
+	go func() {
+		f, err := os.Open(*gapicFname)
+		if err != nil {
+			log.Fatal(errors.E(err, "cannot read GAPIC config file"))
+		}
+		defer f.Close()
+
+		if err := yaml.NewDecoder(f).Decode(&gen.gapic); err != nil {
+			log.Fatal(errors.E(err, "error reading GAPIC config file"))
+		}
+		donec <- struct{}{}
+	}()
+	go func() {
+		descBytes, err := ioutil.ReadFile(*descFname)
+		if err != nil {
+			log.Fatal(errors.E(err, "cannot read proto descriptor file"))
+		}
+
+		if err := proto.Unmarshal(descBytes, &gen.desc); err != nil {
+			log.Fatal(errors.E(err, "error reading proto descriptor file"))
+		}
+
+		gen.descInfo = pbinfo.Of(gen.desc.GetFile())
+		donec <- struct{}{}
+	}()
+
+	<-donec
+	<-donec
+
+	// TODO: split some parts of this loop into functions, so we can idiomatically
+	// chain errors.
+	for _, iface := range gen.gapic.Interfaces {
+		for _, meth := range iface.Methods {
+			valSets := map[string]SampleValueSet{}
+			for _, vs := range meth.SampleValueSets {
+				valSets[vs.ID] = vs
+			}
+
+			for _, sam := range meth.Samples.Standalone {
+				for _, vsID := range sam.ValueSets {
+					vs, ok := valSets[vsID]
+					if !ok {
+						log.Fatal(errors.E(nil, "value set not found: %q", vsID))
+					}
+
+					gen.reset()
+					if err := gen.genSample(iface.Name, meth.Name, sam.RegionTag, vs); err != nil {
+						log.Fatal(errors.E(err, "value set: %q", vs))
+					}
+					if err := gen.commit(!*nofmt); err != nil {
+						log.Fatal(errors.E(err, "value set: %q", vs))
+					}
+				}
+			}
+		}
+	}
+}
+
+type generator struct {
+	desc     descriptor.FileDescriptorSet
+	descInfo pbinfo.Info
+	gapic    GAPICConfig
+
+	pt      printer.P
+	imports map[pbinfo.ImportSpec]bool
+}
+
+func (g *generator) reset() {
+	g.pt.Reset()
+	for imp := range g.imports {
+		delete(g.imports, imp)
+	}
+}
+
+func (g *generator) commit(gofmt bool) error {
+	// We'll gofmt unless user asks us to not, so no need to think too hard about sorting
+	// "correctly". We just want a deterministic output here.
+	var imports []pbinfo.ImportSpec
+	for imp := range g.imports {
+		imports = append(imports, imp)
+	}
+	sort.Slice(imports, func(i, j int) bool {
+		// All non-stdlib imports come after stdlib imports.
+		iDot := strings.IndexByte(imports[i].Path, '.') >= 0
+		jDot := strings.IndexByte(imports[j].Path, '.') >= 0
+		if iDot != jDot {
+			return jDot
+		}
+
+		if imports[i].Path != imports[j].Path {
+			return imports[i].Path < imports[j].Path
+		}
+		return imports[i].Name < imports[j].Name
+	})
+
+	firstNonStd := sort.Search(len(imports), func(i int) bool { return strings.IndexByte(imports[i].Path, '.') >= 0 })
+
+	// TODO(pongad): add license
+	var file bytes.Buffer
+	file.WriteString("package main\n")
+	file.WriteString("import(\n")
+	for i, imp := range imports {
+		if i == firstNonStd {
+			file.WriteByte('\n')
+		}
+		fmt.Fprintf(&file, "%s %q\n", imp.Name, imp.Path)
+	}
+	file.WriteString(")\n")
+	file.Write(g.pt.Bytes())
+
+	b := file.Bytes()
+	if gofmt {
+		b2, err := format.Source(b)
+		if err != nil {
+			return err
+		}
+		b = b2
+	}
+	_, err := os.Stdout.Write(b)
+	return err
+}
+
+func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleValueSet) error {
+	serv := g.descInfo.Serv["."+ifaceName]
+	if serv == nil {
+		return errors.E(nil, "can't find service: %q", ifaceName)
+	}
+
+	servSpec, err := g.descInfo.ImportSpec(serv)
+	if err != nil {
+		return errors.E(err, "can't import service: %q", ifaceName)
+	}
+
+	var meth *descriptor.MethodDescriptorProto
+	for _, m := range serv.GetMethod() {
+		if m.GetName() == methName {
+			meth = m
+			break
+		}
+	}
+	if meth == nil {
+		return errors.E(nil, "service %q doesn't have method %q", serv.GetName(), methName)
+	}
+
+	p := g.pt.Printf
+
+	p("// [START %s]", regTag)
+	p("")
+
+	// TODO(pongad): The package of the client is not the package of the service!
+	// TODO(pongad): properly reduce service name instead of hardcoding "NewClient"
+	p("func sample%s() {", methName)
+	p("  ctx := context.Background()")
+	p("  c := %s.%s(ctx)", servSpec.Name, "NewClient")
+	p("")
+	g.imports[pbinfo.ImportSpec{Path: "context"}] = true
+
+	// TODO(pongad): properly create the request.
+	inType := g.descInfo.Type[meth.GetInputType()]
+	if inType == nil {
+		return errors.E(nil, "can't find input type %q", meth.GetInputType())
+	}
+	inSpec, err := g.descInfo.ImportSpec(inType)
+	if err != nil {
+		return errors.E(err, "can't import input type: %q", inType)
+	}
+
+	p("  req := %s.%s{", inSpec.Name, inType.GetName())
+	for _, def := range valSet.Parameters.Defaults {
+		p("// %s", def)
+	}
+	p("  }")
+
+	// TODO(pongad): handle non-unary
+	p("  resp, err := c.%s(ctx, req)", methName)
+	p("  if err != nil {")
+	p("    // TODO: Handle error.")
+	p("  }")
+
+	// TODO(pongad): handle output printing
+	p("  fmt.Println(resp)")
+
+	p("}")
+	p("")
+	p("// [END %s]", regTag)
+	p("")
+
+	p("func main() {")
+	p("  sample%s()", methName)
+	p("}")
+	p("")
+
+	g.imports[servSpec] = true
+	g.imports[inSpec] = true
+	g.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
+	return nil
+}
diff --git a/go.mod b/go.mod
index b32ec56..f239ab1 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
 	github.com/golang/protobuf v1.2.0
 	github.com/google/go-cmp v0.2.0
 	google.golang.org/genproto v0.0.0-20180914223249-4b56f30a1fd9
+	gopkg.in/yaml.v2 v2.2.1
 )
 
 replace google.golang.org/genproto => ./vendor/google.golang.org/genproto
diff --git a/go.sum b/go.sum
index 6bac941..4438e0d 100644
--- a/go.sum
+++ b/go.sum
@@ -12,3 +12,6 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal/pbinfo/pbinfo.go b/internal/pbinfo/pbinfo.go
new file mode 100644
index 0000000..74cb0b9
--- /dev/null
+++ b/internal/pbinfo/pbinfo.go
@@ -0,0 +1,111 @@
+// Copyright 2018 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 pbinfo provides convenience types for looking up protobuf elements.
+package pbinfo
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/googleapis/gapic-generator-go/internal/errors"
+)
+
+type Info struct {
+	// Maps services and messages to the file containing them,
+	// so we can figure out the import.
+	ParentFile map[proto.Message]*descriptor.FileDescriptorProto
+
+	// Maps type names to their messages.
+	Type map[string]*descriptor.DescriptorProto
+
+	// Maps service names to their descriptors.
+	Serv map[string]*descriptor.ServiceDescriptorProto
+}
+
+// Of creates Info from given protobuf files.
+func Of(files []*descriptor.FileDescriptorProto) Info {
+	info := Info{
+		ParentFile: map[proto.Message]*descriptor.FileDescriptorProto{},
+		Type:       map[string]*descriptor.DescriptorProto{},
+		Serv:       map[string]*descriptor.ServiceDescriptorProto{},
+	}
+
+	for _, f := range files {
+		// ParentFile
+		for _, m := range f.MessageType {
+			info.ParentFile[m] = f
+		}
+		for _, s := range f.Service {
+			info.ParentFile[s] = f
+		}
+
+		// Type
+		for _, m := range f.MessageType {
+			// In descriptors, putting the dot in front means the name is fully-qualified.
+			fullyQualifiedName := fmt.Sprintf(".%s.%s", f.GetPackage(), m.GetName())
+			info.Type[fullyQualifiedName] = m
+		}
+
+		// Serv
+		for _, s := range f.Service {
+			fullyQualifiedName := fmt.Sprintf(".%s.%s", f.GetPackage(), s.GetName())
+			info.Serv[fullyQualifiedName] = s
+		}
+	}
+
+	return info
+}
+
+type ImportSpec struct {
+	Name, Path string
+}
+
+// ImportSpec reports the ImportSpec for package containing protobuf element e.
+func (in *Info) ImportSpec(e proto.Message) (ImportSpec, error) {
+	var eTxt interface{} = e
+	if et, ok := eTxt.(interface{ GetName() string }); ok {
+		eTxt = et.GetName()
+	}
+
+	fdesc := in.ParentFile[e]
+	if fdesc == nil {
+		return ImportSpec{}, errors.E(nil, "can't determine import path for %v; can't find parent file", eTxt)
+	}
+
+	pkg := fdesc.GetOptions().GetGoPackage()
+	if pkg == "" {
+		return ImportSpec{}, errors.E(nil, "can't determine import path for %v, file %q missing `option go_package`", eTxt, fdesc.GetName())
+	}
+
+	if p := strings.IndexByte(pkg, ';'); p >= 0 {
+		return ImportSpec{Path: pkg[:p], Name: pkg[p+1:] + "pb"}, nil
+	}
+
+	for {
+		p := strings.LastIndexByte(pkg, '/')
+		if p < 0 {
+			return ImportSpec{Path: pkg, Name: pkg + "pb"}, nil
+		}
+		elem := pkg[p+1:]
+		if len(elem) >= 2 && elem[0] == 'v' && elem[1] >= '0' && elem[1] <= '9' {
+			// It's a version number; skip so we get a more meaningful name
+			pkg = pkg[:p]
+			continue
+		}
+		return ImportSpec{Path: pkg, Name: elem + "pb"}, nil
+	}
+}
diff --git a/internal/printer/printer.go b/internal/printer/printer.go
new file mode 100644
index 0000000..42dbee2
--- /dev/null
+++ b/internal/printer/printer.go
@@ -0,0 +1,71 @@
+// Copyright 2018 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 printer implements auto-indenting printer.
+package printer
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+)
+
+type P struct {
+	buf    bytes.Buffer
+	indent int
+}
+
+// Reset resets p but retains the underlying storage for use by future Printfs.
+func (p *P) Reset() {
+	p.buf.Reset()
+	p.indent = 0
+}
+
+// Printf format-writes to p's buffer. The formatting is similar to the fmt package,
+// and the written content can be retrieved with Bytes. Printf is line-oriented: each call
+// should correspond to a new line in the output. Printf automatically inserts a newline
+// at the end of each call.
+//
+// Printf automatically keeps track of curly-braces and indents lines accordingly.
+// The indentation is not "gofmt-correct" but should print code in "curly brace languages"
+// well enough that callers don't need to keep track of indentation level.
+// Printf ignores leading and trailing whitespaces in s; callers can use them for visual aid
+// without interfering with automatic indentation.
+func (p *P) Printf(s string, args ...interface{}) {
+	s = strings.TrimSpace(s)
+	if s == "" {
+		p.buf.WriteByte('\n')
+		return
+	}
+
+	for i := 0; i < len(s) && s[i] == '}'; i++ {
+		p.indent--
+	}
+
+	for i := 0; i < p.indent; i++ {
+		p.buf.WriteByte('\t')
+	}
+
+	fmt.Fprintf(&p.buf, s, args...)
+	p.buf.WriteByte('\n')
+
+	for i := len(s) - 1; i >= 0 && s[i] == '{'; i-- {
+		p.indent++
+	}
+}
+
+// Bytes returns the bytes written by Printf. It is valid up to the next call to Printf or Reset.
+func (p *P) Bytes() []byte {
+	return p.buf.Bytes()
+}


Test patch:
diff --git a/cmd/gen-go-sample/main_test.go b/cmd/gen-go-sample/main_test.go
new file mode 100644
index 0000000..251c3fa
--- /dev/null
+++ b/cmd/gen-go-sample/main_test.go
@@ -0,0 +1,94 @@
+// Copyright 2018 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"
+	"io/ioutil"
+	"path/filepath"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/google/go-cmp/cmp"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
+)
+
+var updateGolden = flag.Bool("update_golden", false, "update golden files")
+
+func diff(t *testing.T, name, got, goldenFile string) {
+	t.Helper()
+
+	if *updateGolden {
+		if err := ioutil.WriteFile(goldenFile, []byte(got), 0644); err != nil {
+			t.Fatal(err)
+		}
+	}
+	want, err := ioutil.ReadFile(goldenFile)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if diff := cmp.Diff(got, string(want)); diff != "" {
+		t.Errorf("%s: (-got,+want)\n%s", name, diff)
+	}
+}
+
+func TestSample(t *testing.T) {
+	inType := &descriptor.DescriptorProto{
+		Name: proto.String("InputType"),
+	}
+	serv := &descriptor.ServiceDescriptorProto{
+		Name: proto.String("MyService"),
+		Method: []*descriptor.MethodDescriptorProto{
+			{
+				Name:      proto.String("MyMethod"),
+				InputType: inType.Name,
+			},
+		},
+	}
+	file := &descriptor.FileDescriptorProto{
+		Options: &descriptor.FileOptions{
+			GoPackage: proto.String("path/to/mypackage;mypackage"),
+		},
+	}
+
+	g := generator{
+		imports: map[pbinfo.ImportSpec]bool{},
+		descInfo: pbinfo.Info{
+			Serv: map[string]*descriptor.ServiceDescriptorProto{
+				".MyService": serv,
+			},
+			ParentFile: map[proto.Message]*descriptor.FileDescriptorProto{
+				serv:           file,
+				serv.Method[0]: file,
+				inType:         file,
+			},
+			Type: map[string]*descriptor.DescriptorProto{
+				"InputType": inType,
+			},
+		},
+	}
+
+	vs := SampleValueSet{
+		ID: "my_value_set",
+		Parameters: SampleParameter{
+			Defaults: []string{"foo=bar"},
+		},
+	}
+	if err := g.genSample("MyService", "MyMethod", "awesome_region", vs); err != nil {
+		t.Fatal(err)
+	}
+	diff(t, "TestSample", string(g.pt.Bytes()), filepath.Join("testdata", "sample.want"))
+}
diff --git a/cmd/gen-go-sample/test.bash b/cmd/gen-go-sample/test.bash
new file mode 100755
index 0000000..3bf7221
--- /dev/null
+++ b/cmd/gen-go-sample/test.bash
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Copyright 2018 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.
+
+set -e
+
+if [ -z $GOOGLEAPIS ]; then
+	echo >&2 "need GOOGLEAPIS variable"
+	exit 1
+fi
+if [ -z $COMMON_PROTO ]; then
+	echo >&2 "need COMMON_PROTO variable"
+	exit 1
+fi
+
+./gen-go-sample \
+  -gapic "$GOOGLEAPIS/google/cloud/language/v1/language_gapic.yaml" \
+  -desc <(protoc -o /dev/stdout --include_imports -I "$COMMON_PROTO" -I "$GOOGLEAPIS" "$GOOGLEAPIS"/google/cloud/language/v1/*.proto)
diff --git a/cmd/gen-go-sample/testdata/sample.want b/cmd/gen-go-sample/testdata/sample.want
new file mode 100644
index 0000000..4ce6501
--- /dev/null
+++ b/cmd/gen-go-sample/testdata/sample.want
@@ -0,0 +1,22 @@
+// [START awesome_region]
+
+func sampleMyMethod() {
+	ctx := context.Background()
+	c := mypackagepb.NewClient(ctx)
+
+	req := mypackagepb.InputType{
+		// foo=bar
+	}
+	resp, err := c.MyMethod(ctx, req)
+	if err != nil {
+		// TODO: Handle error.
+	}
+	fmt.Println(resp)
+}
+
+// [END awesome_region]
+
+func main() {
+	sampleMyMethod()
+}
+