Repository: googleapis/gapic-generator-go
Pull request #30 (2018-10-11 01:11:10)

sample: handle loops in output
and handle multiple files too.


Patch:
diff --git a/cmd/gen-go-sample/gapic.go b/cmd/gen-go-sample/gapic.go
index fb948f0..9fb9397 100644
--- a/cmd/gen-go-sample/gapic.go
+++ b/cmd/gen-go-sample/gapic.go
@@ -57,4 +57,11 @@ type GAPICSample struct {
 type OutputSpec struct {
 	Define string
 	Print  []string
+	Loop   *LoopSpec
+}
+
+type LoopSpec struct {
+	Collection string
+	Variable   string
+	Body       []OutputSpec
 }
diff --git a/cmd/gen-go-sample/main.go b/cmd/gen-go-sample/main.go
index 8279c3b..8ea1f1a 100644
--- a/cmd/gen-go-sample/main.go
+++ b/cmd/gen-go-sample/main.go
@@ -15,15 +15,14 @@
 package main
 
 import (
-	"bufio"
 	"bytes"
 	"flag"
 	"fmt"
 	"go/format"
-	"io"
 	"io/ioutil"
 	"log"
 	"os"
+	"path/filepath"
 	"sort"
 	"strings"
 	"time"
@@ -42,6 +41,7 @@ func main() {
 	gapicFname := flag.String("gapic", "", "gapic config")
 	clientPkg := flag.String("clientpkg", "", "the package of the client, in format 'url/to/client/pkg;name'")
 	nofmt := flag.Bool("nofmt", false, "skip gofmt, useful for debugging code with syntax error")
+	outDir := flag.String("o", ".", "directory to write samples to")
 	flag.Parse()
 
 	gen := generator{
@@ -81,12 +81,16 @@ func main() {
 		donec <- struct{}{}
 	}()
 
+	if err := os.MkdirAll(*outDir, 0755); err != nil {
+		log.Fatal(err)
+	}
+
 	<-donec
 	<-donec
 
 	for _, iface := range gen.gapic.Interfaces {
 		for _, meth := range iface.Methods {
-			if err := genMethodSamples(&gen, iface, meth, *nofmt); err != nil {
+			if err := genMethodSamples(&gen, iface, meth, *nofmt, *outDir); err != nil {
 				err = errors.E(err, "generating: %s", iface.Name+"."+meth.Name)
 				log.Fatal(err)
 			}
@@ -94,7 +98,7 @@ func main() {
 	}
 }
 
-func genMethodSamples(gen *generator, iface GAPICInterface, meth GAPICMethod, nofmt bool) error {
+func genMethodSamples(gen *generator, iface GAPICInterface, meth GAPICMethod, nofmt bool, outDir string) error {
 	valSets := map[string]SampleValueSet{}
 	for _, vs := range meth.SampleValueSets {
 		valSets[vs.ID] = vs
@@ -111,7 +115,13 @@ func genMethodSamples(gen *generator, iface GAPICInterface, meth GAPICMethod, no
 			if err := gen.genSample(iface.Name, meth.Name, sam.RegionTag, vs); err != nil {
 				return err
 			}
-			if err := gen.commit(!nofmt, time.Now().Year(), os.Stdout); err != nil {
+			content, err := gen.commit(!nofmt, time.Now().Year())
+			if err != nil {
+				return err
+			}
+
+			fname := iface.Name + "_" + meth.Name + "_" + vsID + ".go"
+			if err := ioutil.WriteFile(filepath.Join(outDir, fname), content, 0644); err != nil {
 				return err
 			}
 		}
@@ -137,7 +147,7 @@ func (g *generator) reset() {
 	}
 }
 
-func (g *generator) commit(gofmt bool, year int, w io.Writer) error {
+func (g *generator) commit(gofmt bool, year int) ([]byte, 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
@@ -177,12 +187,11 @@ func (g *generator) commit(gofmt bool, year int, w io.Writer) error {
 	if gofmt {
 		b2, err := format.Source(b)
 		if err != nil {
-			return errors.E(err, "syntax error, run with -nofmt to find out why?")
+			return nil, errors.E(err, "syntax error, run with -nofmt to find out why?")
 		}
 		b = b2
 	}
-	_, err := w.Write(b)
-	return err
+	return b, nil
 }
 
 func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleValueSet) error {
@@ -306,14 +315,15 @@ func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleV
 		st.universe["$resp"] = true
 		st.scope["$resp"] = initType{desc: g.descInfo.Type[meth.GetOutputType()]}
 
-		bw := bufio.NewWriter(g.pt.Writer())
 		for _, out := range valSet.OnSuccess {
-			if err := writeOutputSpec(out, st, g.descInfo, bw); err != nil {
+			if err := writeOutputSpec(out, st, g); err != nil {
 				return err
 			}
 		}
-		if err := bw.Flush(); err != nil {
-			return err
+
+		if len(valSet.OnSuccess) == 0 {
+			p("fmt.Println(resp)")
+			g.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
 		}
 	}
 
@@ -338,7 +348,6 @@ func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleV
 	p("")
 
 	g.imports[inSpec] = true
-	g.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
 	g.imports[pbinfo.ImportSpec{Path: "flag"}] = true
 	return nil
 }
diff --git a/cmd/gen-go-sample/out.go b/cmd/gen-go-sample/out.go
index 6b2ea20..3607100 100644
--- a/cmd/gen-go-sample/out.go
+++ b/cmd/gen-go-sample/out.go
@@ -15,8 +15,6 @@
 package main
 
 import (
-	"bufio"
-	"fmt"
 	"strings"
 	"text/scanner"
 
@@ -65,17 +63,30 @@ func (st *symTab) get(s string) (initType, bool) {
 	return initType{}, false
 }
 
-func writeOutputSpec(out OutputSpec, st *symTab, info pbinfo.Info, w *bufio.Writer) error {
+func (st *symTab) put(ident string, typ initType) error {
+	if st.universe[ident] {
+		return errors.E(nil, "variable already defined: %s", ident)
+	}
+	st.universe[ident] = true
+	st.scope[ident] = typ
+	return nil
+}
+
+func writeOutputSpec(out OutputSpec, st *symTab, gen *generator) error {
 	used := 0
 	var err error
 
 	if d := out.Define; d != "" {
 		used++
-		err = writeDefine(d, st, info, w)
+		err = writeDefine(d, st, gen)
 	}
 	if p := out.Print; len(p) > 0 {
 		used++
-		err = writePrint(p[0], p[1:], st, info, w)
+		err = writePrint(p[0], p[1:], st, gen)
+	}
+	if l := out.Loop; l != nil {
+		used++
+		err = writeLoop(l, st, gen)
 	}
 
 	if used == 0 {
@@ -87,8 +98,8 @@ func writeOutputSpec(out OutputSpec, st *symTab, info pbinfo.Info, w *bufio.Writ
 	return err
 }
 
-// define = ident '=' ident path .
-func writeDefine(txt string, st *symTab, info pbinfo.Info, w *bufio.Writer) error {
+// define = ident '=' path .
+func writeDefine(txt string, st *symTab, gen *generator) error {
 	sc, report := initScanner(txt)
 
 	if sc.Scan() != scanner.Ident {
@@ -103,82 +114,95 @@ func writeDefine(txt string, st *symTab, info pbinfo.Info, w *bufio.Writer) erro
 	if sc.Scan() != '=' {
 		return report(errors.E(nil, "expecting '=', got %s", sc.TokenText()))
 	}
-	if sc.Scan() != scanner.Ident {
-		return report(errors.E(nil, "expecting ident, got %s", sc.TokenText()))
-	}
 
-	rootVar := sc.TokenText()
-	rootTyp, ok := st.get(rootVar)
-	if !ok {
-		return report(errors.E(nil, "variable not found: %q", rootVar))
-	}
-
-	itRoot := &initTree{typ: rootTyp}
-	itLeaf, _, err := itRoot.parsePathRest(sc, info)
+	path, typ, err := writePath(sc, st, gen.descInfo)
 	if err != nil {
 		return report(err)
 	}
-	if sc.Scan() != scanner.EOF {
-		return report(errors.E(nil, "expected EOF, found %q", sc.TokenText()))
-	}
-
-	st.scope[lhs] = itLeaf.typ
-	st.universe[lhs] = true
-
-	if rootVar == "$resp" {
-		rootVar = "resp"
-	}
-	fmt.Fprintf(w, "%s := %s", snakeToCamel(lhs), snakeToCamel(rootVar))
-	writePathRest(itRoot, w)
-	w.WriteByte('\n')
-
-	return report(nil)
+	gen.pt.Printf("%s := %s", snakeToCamel(lhs), path)
+	return st.put(lhs, typ)
 }
 
 var fmtStrReplacer = strings.NewReplacer("%%", "%%", "%s", "%v")
 
-func writePrint(pFmt string, pArgs []string, st *symTab, info pbinfo.Info, w *bufio.Writer) error {
-	fmt.Fprintf(w, "fmt.Printf(%q", fmtStrReplacer.Replace(pFmt)+"\n")
-
+func writePrint(pFmt string, pArgs []string, st *symTab, gen *generator) error {
+	var sb strings.Builder
 	for _, arg := range pArgs {
-		w.WriteString(", ")
+		sb.WriteString(", ")
 
 		sc, report := initScanner(arg)
-		if sc.Scan() != scanner.Ident {
-			return report(errors.E(nil, "expecting ident, got %s", sc.TokenText()))
+		path, _, err := writePath(sc, st, gen.descInfo)
+		if err != nil {
+			return report(nil)
 		}
+		sb.WriteString(path)
+	}
 
-		rootVar := sc.TokenText()
-		rootTyp, ok := st.get(rootVar)
-		if !ok {
-			return report(errors.E(nil, "variable not found: %q", rootVar))
-		}
+	gen.pt.Printf("fmt.Printf(%q%s)", fmtStrReplacer.Replace(pFmt)+"\n", sb.String())
+	gen.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
 
-		itRoot := &initTree{typ: rootTyp}
-		_, _, err := itRoot.parsePathRest(sc, info)
-		if err != nil {
-			return report(err)
-		}
-		if sc.Scan() != scanner.EOF {
-			return report(errors.E(nil, "expected EOF, found %q", sc.TokenText()))
-		}
+	return nil
+}
+
+func writeLoop(l *LoopSpec, st *symTab, gen *generator) error {
+	p := gen.pt.Printf
+
+	sc, report := initScanner(l.Collection)
+	path, typ, err := writePath(sc, st, gen.descInfo)
+	if err = report(err); err != nil {
+		return err
+	}
+
+	p("for _, %s := range %s {", snakeToCamel(l.Variable), path)
+
+	stInner := newSymTab(st)
+	stInner.put(l.Variable, typ)
 
-		if rootVar == "$resp" {
-			rootVar = "resp"
+	for _, b := range l.Body {
+		if err := writeOutputSpec(b, stInner, gen); err != nil {
+			return err
 		}
-		w.WriteString(snakeToCamel(rootVar))
-		writePathRest(itRoot, w)
 	}
-	w.WriteString(")\n")
+	p("}")
 	return nil
 }
 
-// writePathRest writes an "unrooted" path, printing index and fields, but not the left-most identifier.
-func writePathRest(it *initTree, w *bufio.Writer) {
+func writePath(sc *scanner.Scanner, st *symTab, info pbinfo.Info) (string, initType, error) {
+	if sc.Scan() != scanner.Ident {
+		return "", initType{}, errors.E(nil, "expecting ident, got %s", sc.TokenText())
+	}
+
+	rootVar := sc.TokenText()
+	rootTyp, ok := st.get(rootVar)
+	if !ok {
+		return "", initType{}, errors.E(nil, "variable not found: %q", rootVar)
+	}
+
+	itRoot := &initTree{typ: rootTyp}
+	itLeaf, _, err := itRoot.parsePathRest(sc, info)
+	if err != nil {
+		return "", initType{}, err
+	}
+	if sc.Scan() != scanner.EOF {
+		return "", initType{}, errors.E(nil, "expected EOF, found %q", sc.TokenText())
+	}
+
+	if rootVar == "$resp" {
+		rootVar = "resp"
+	} else {
+		rootVar = snakeToCamel(rootVar)
+	}
+
+	var sb strings.Builder
+	sb.WriteString(rootVar)
+
 	// TODO(pongad): This doesn't handle oneofs properly.
-	for len(it.keys) > 0 {
-		w.WriteRune('.')
-		w.WriteString(snakeToPascal(it.keys[0]))
-		it = it.vals[0]
+	for it := itRoot; len(it.keys) > 0; it = it.vals[0] {
+		// Use Get method instead of direct field access so we properly deal with unset messages.
+		sb.WriteString(".Get")
+		sb.WriteString(snakeToPascal(it.keys[0]))
+		sb.WriteString("()")
 	}
+
+	return sb.String(), itLeaf.typ, nil
 }


Test patch:
diff --git a/cmd/gen-go-sample/main_test.go b/cmd/gen-go-sample/main_test.go
index 80a97d9..b462f99 100644
--- a/cmd/gen-go-sample/main_test.go
+++ b/cmd/gen-go-sample/main_test.go
@@ -16,7 +16,6 @@ package main
 
 import (
 	"path/filepath"
-	"strings"
 	"testing"
 
 	"github.com/golang/protobuf/proto"
@@ -26,6 +25,10 @@ import (
 )
 
 func TestSample(t *testing.T) {
+	labelp := func(l descriptor.FieldDescriptorProto_Label) *descriptor.FieldDescriptorProto_Label {
+		return &l
+	}
+
 	inType := &descriptor.DescriptorProto{
 		Name: proto.String("InputType"),
 		OneofDecl: []*descriptor.OneofDescriptorProto{
@@ -36,6 +39,7 @@ func TestSample(t *testing.T) {
 			{Name: proto.String("b"), Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING)},
 			{Name: proto.String("e"), TypeName: proto.String("EType")},
 			{Name: proto.String("f"), Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING), OneofIndex: proto.Int32(0)},
+			{Name: proto.String("r"), Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING), Label: labelp(descriptor.FieldDescriptorProto_LABEL_REPEATED)},
 		},
 	}
 	aType := &descriptor.DescriptorProto{
@@ -111,19 +115,27 @@ func TestSample(t *testing.T) {
 			{Define: "out_a = $resp.a"},
 			{Print: []string{"x = %s", "$resp.a.x"}},
 			{Print: []string{"y = %s", "out_a.y"}},
+			{
+				Loop: &LoopSpec{
+					Variable:   "r",
+					Collection: "$resp.r",
+					Body: []OutputSpec{
+						{Print: []string{"resp.r contains %s", "r"}},
+					},
+				},
+			},
 		},
 	}
 	if err := g.genSample("MyService", "MyMethod", "awesome_region", vs); err != nil {
 		t.Fatal(err)
 	}
 
-	var sb strings.Builder
-
 	// Don't format. Format can change with Go version.
 	gofmt := false
 	year := 2018
-	if err := g.commit(gofmt, year, &sb); err != nil {
+	content, err := g.commit(gofmt, year)
+	if err != nil {
 		t.Fatal(err)
 	}
-	txtdiff.Diff(t, "TestSample", sb.String(), filepath.Join("testdata", "sample.want"))
+	txtdiff.Diff(t, "TestSample", string(content), filepath.Join("testdata", "sample.want"))
 }
diff --git a/cmd/gen-go-sample/test.bash b/cmd/gen-go-sample/test.bash
index a6fbee1..a6f8059 100755
--- a/cmd/gen-go-sample/test.bash
+++ b/cmd/gen-go-sample/test.bash
@@ -25,7 +25,10 @@ if [ -z $COMMON_PROTO ]; then
 	exit 1
 fi
 
+OUT=${OUT:-testdata/out}
+
 ./gen-go-sample $* \
   -clientpkg 'cloud.google.com/go/language/apiv1;language' \
   -gapic "$GOOGLEAPIS/google/cloud/language/v1/language_gapic.yaml" \
+  -o "$OUT" \
   -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
index 16137c4..dd57572 100644
--- a/cmd/gen-go-sample/testdata/sample.want
+++ b/cmd/gen-go-sample/testdata/sample.want
@@ -50,9 +50,12 @@ req := &foopb.InputType{
 		return err
 	}
 
-outA := resp.A
-fmt.Printf("x = %v\n", resp.A.X)
-fmt.Printf("y = %v\n", outA.Y)
+	outA := resp.GetA()
+	fmt.Printf("x = %v\n", resp.GetA().GetX())
+	fmt.Printf("y = %v\n", outA.GetY())
+	for _, r := range resp.GetR() {
+		fmt.Printf("resp.r contains %v\n", r)
+	}
 	return nil
 }