Repository: googleapis/gapic-generator-go
Pull request #28 (2018-10-09 17:39:48)

sample: add output handling


Patch:
diff --git a/cmd/gen-go-sample/gapic.go b/cmd/gen-go-sample/gapic.go
index 5a6b22c..fb948f0 100644
--- a/cmd/gen-go-sample/gapic.go
+++ b/cmd/gen-go-sample/gapic.go
@@ -34,6 +34,7 @@ type GAPICMethod struct {
 type SampleValueSet struct {
 	ID         string
 	Parameters SampleParameter
+	OnSuccess  []OutputSpec `yaml:"on_success"`
 }
 
 type SampleParameter struct {
@@ -52,3 +53,8 @@ type GAPICSample struct {
 	// TODO(pongad): Does this mean multiple samples have the same tag?
 	RegionTag string `yaml:"region_tag"`
 }
+
+type OutputSpec struct {
+	Define string
+	Print  []string
+}
diff --git a/cmd/gen-go-sample/inittree.go b/cmd/gen-go-sample/inittree.go
index 793b256..5cc1ee3 100644
--- a/cmd/gen-go-sample/inittree.go
+++ b/cmd/gen-go-sample/inittree.go
@@ -112,7 +112,7 @@ func (t *initTree) parseInit(txt string, info pbinfo.Info) error {
 	// so just insert a dot here so we don't need to treat the first token specially.
 	sc, report := initScanner("." + txt)
 
-	t, r, err := t.parsePath(sc, info)
+	t, r, err := t.parsePathRest(sc, info)
 	if err != nil {
 		return report(err)
 	}
@@ -159,7 +159,7 @@ func (t *initTree) parseInit(txt string, info pbinfo.Info) error {
 
 func (t *initTree) parseSampleArgPath(txt string, info pbinfo.Info, varName string) (*initTree, error) {
 	sc, report := initScanner("." + txt)
-	t, _, err := t.parsePath(sc, info)
+	t, _, err := t.parsePathRest(sc, info)
 	if err != nil {
 		return nil, report(err)
 	}
@@ -171,11 +171,12 @@ func (t *initTree) parseSampleArgPath(txt string, info pbinfo.Info, varName stri
 	return &cp, report(nil)
 }
 
-// parsePath parses a path as defined below. It returns the subtree specified by the path,
-// the last scanned token, and any error.
+// parsePathRest parses pathRest, an "unrooted" path, as defined below.
+// It returns the subtree specified by the path, the last scanned token, and any error.
 //
-// path = ident { '.' ident } .
-func (t *initTree) parsePath(sc *scanner.Scanner, info pbinfo.Info) (*initTree, rune, error) {
+// path = ident pathRest .
+// pathRest = { '.' ident } .
+func (t *initTree) parsePathRest(sc *scanner.Scanner, info pbinfo.Info) (*initTree, rune, error) {
 	for {
 		switch r := sc.Scan(); r {
 		case '.':
@@ -213,6 +214,11 @@ func initScanner(s string) (*scanner.Scanner, func(error) error) {
 		e = errors.E(e, "while scaning: %q", s)
 		report(e)
 	}
+
+	sc.IsIdentRune = func(r rune, i int) bool {
+		return unicode.IsLetter(r) || r == '_' || i == 0 && r == '$' || i > 0 && unicode.IsDigit(r)
+	}
+
 	return &sc, report
 }
 
@@ -268,11 +274,11 @@ func (t *initTree) print(w *bufio.Writer, g *generator, ind int) error {
 
 		var closeBrace bool
 		if oneof, ok := oneofs[k]; ok {
-			fmt.Fprintf(w, "%s: &%s.%s_%s{\n", snakeToCapCamel(oneof), impSpec.Name, desc.GetName(), snakeToCapCamel(k))
+			fmt.Fprintf(w, "%s: &%s.%s_%s{\n", snakeToPascal(oneof), impSpec.Name, desc.GetName(), snakeToPascal(k))
 			closeBrace = true
 			indent(ind + 2)
 		}
-		w.WriteString(snakeToCapCamel(k))
+		w.WriteString(snakeToPascal(k))
 
 		w.WriteString(": ")
 		if err := t.vals[i].print(w, g, ind+1); err != nil {
@@ -291,7 +297,7 @@ func (t *initTree) print(w *bufio.Writer, g *generator, ind int) error {
 	return nil
 }
 
-func snakeToCapCamel(s string) string {
+func snakeToPascal(s string) string {
 	var sb strings.Builder
 	cap := true
 	for _, r := range s {
@@ -306,3 +312,19 @@ func snakeToCapCamel(s string) string {
 	}
 	return sb.String()
 }
+
+func snakeToCamel(s string) string {
+	var sb strings.Builder
+	cap := false
+	for _, r := range s {
+		if r == '_' {
+			cap = true
+		} else if cap {
+			cap = false
+			sb.WriteRune(unicode.ToUpper(r))
+		} else {
+			sb.WriteRune(r)
+		}
+	}
+	return sb.String()
+}
diff --git a/cmd/gen-go-sample/main.go b/cmd/gen-go-sample/main.go
index 61cdda9..1d68c74 100644
--- a/cmd/gen-go-sample/main.go
+++ b/cmd/gen-go-sample/main.go
@@ -15,6 +15,7 @@
 package main
 
 import (
+	"bufio"
 	"bytes"
 	"flag"
 	"fmt"
@@ -299,9 +300,23 @@ func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleV
 	p("  if err != nil {")
 	p("    // TODO: Handle error.")
 	p("  }")
+	p("")
+
+	{
+		st := newSymTab(nil)
+		st.universe["$resp"] = true
+		st.scope["$resp"] = initType{desc: g.descInfo.Type[meth.GetOutputType()]}
 
-	// TODO(pongad): handle output printing
-	p("  fmt.Println(resp)")
+		bw := bufio.NewWriter(g.pt.Writer())
+		for _, out := range valSet.OnSuccess {
+			if err := writeOutputSpec(out, st, g.descInfo, bw); err != nil {
+				return err
+			}
+		}
+		if err := bw.Flush(); err != nil {
+			return err
+		}
+	}
 
 	p("}")
 	p("")
@@ -314,7 +329,7 @@ func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleV
 		// TODO(pongad): some types, like int32, are not supported by flag package.
 		// We have to convert.
 		typ := pbinfo.GoTypeForPrim[argTrees[i].typ.prim]
-		p(`%s := flag.%s(%q, %s, "")`, name, snakeToCapCamel(typ), name, argTrees[i].leafVal)
+		p(`%s := flag.%s(%q, %s, "")`, name, snakeToPascal(typ), name, argTrees[i].leafVal)
 	}
 
 	p("  flag.Parse()")
diff --git a/cmd/gen-go-sample/out.go b/cmd/gen-go-sample/out.go
new file mode 100644
index 0000000..6b2ea20
--- /dev/null
+++ b/cmd/gen-go-sample/out.go
@@ -0,0 +1,184 @@
+// 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 (
+	"bufio"
+	"fmt"
+	"strings"
+	"text/scanner"
+
+	"github.com/googleapis/gapic-generator-go/internal/errors"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
+)
+
+// symTab implements a symbol table.
+//
+// Some languages scope variables at function level. We track this with universe table.
+// Some languages scope variables to the curly-braces. We track this with parent and scope tables.
+//
+// So that our samples work with the first kind of languages, we only allow declaration
+// iff the name hasn't been declared in the universe scope.
+//
+// So that our samples work with the second kind of languages, we only allow variable references
+// in the recursive closure of the current scope.
+type symTab struct {
+	universe map[string]bool
+	parent   *symTab
+	scope    map[string]initType
+}
+
+func newSymTab(parent *symTab) *symTab {
+	if parent == nil {
+		return &symTab{
+			universe: map[string]bool{},
+			scope:    map[string]initType{},
+		}
+	}
+
+	return &symTab{
+		universe: parent.universe,
+		parent:   parent,
+		scope:    map[string]initType{},
+	}
+}
+
+func (st *symTab) get(s string) (initType, bool) {
+	for st != nil {
+		if it, ok := st.scope[s]; ok {
+			return it, true
+		}
+		st = st.parent
+	}
+	return initType{}, false
+}
+
+func writeOutputSpec(out OutputSpec, st *symTab, info pbinfo.Info, w *bufio.Writer) error {
+	used := 0
+	var err error
+
+	if d := out.Define; d != "" {
+		used++
+		err = writeDefine(d, st, info, w)
+	}
+	if p := out.Print; len(p) > 0 {
+		used++
+		err = writePrint(p[0], p[1:], st, info, w)
+	}
+
+	if used == 0 {
+		return errors.E(nil, "OutputSpec not defined")
+	}
+	if used > 1 {
+		return errors.E(nil, "OutputSpec cannot be defined multiple times: %v", out)
+	}
+	return err
+}
+
+// define = ident '=' ident path .
+func writeDefine(txt string, st *symTab, info pbinfo.Info, w *bufio.Writer) error {
+	sc, report := initScanner(txt)
+
+	if sc.Scan() != scanner.Ident {
+		return report(errors.E(nil, "expecting ident, got %s", sc.TokenText()))
+	}
+
+	lhs := sc.TokenText()
+	if st.universe[lhs] {
+		return report(errors.E(nil, "variable already defined: %q", lhs))
+	}
+
+	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)
+	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)
+}
+
+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")
+
+	for _, arg := range pArgs {
+		w.WriteString(", ")
+
+		sc, report := initScanner(arg)
+		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}
+		_, _, 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()))
+		}
+
+		if rootVar == "$resp" {
+			rootVar = "resp"
+		}
+		w.WriteString(snakeToCamel(rootVar))
+		writePathRest(itRoot, w)
+	}
+	w.WriteString(")\n")
+	return nil
+}
+
+// writePathRest writes an "unrooted" path, printing index and fields, but not the left-most identifier.
+func writePathRest(it *initTree, w *bufio.Writer) {
+	// 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]
+	}
+}
diff --git a/internal/txtdiff/diff.go b/internal/txtdiff/diff.go
index 9d1069e..5bcd015 100644
--- a/internal/txtdiff/diff.go
+++ b/internal/txtdiff/diff.go
@@ -24,6 +24,8 @@ import (
 
 var updateGolden = flag.Bool("update_golden", false, "update golden files")
 
+// Diff is a test helper, testing got against contents of goldenFile. If they do not match,
+// Diff fails the test, reporting the diff.
 func Diff(t *testing.T, name, got, goldenFile string) {
 	t.Helper()
 
@@ -46,10 +48,10 @@ func Diff(t *testing.T, name, got, goldenFile string) {
 
 	gotLines := strings.Split(got, "\n")
 	wantLines := strings.Split(want, "\n")
-	t.Errorf("%s: (-got,+want)\n%s", name, lcsDiff(gotLines, wantLines))
+	t.Errorf("%s: (+got,-want)\n%s", name, lcsDiff(gotLines, '+', wantLines, '-'))
 }
 
-func lcsDiff(aLines, bLines []string) string {
+func lcsDiff(aLines []string, aSign rune, bLines []string, bSign rune) string {
 	// Algorithm is described by https://en.wikipedia.org/wiki/Longest_common_subsequence_problem.
 
 	// We require O(n^2) space to memoize LCS. This is not great, however
@@ -80,7 +82,7 @@ func lcsDiff(aLines, bLines []string) string {
 
 	// The article uses recursion. I think iteration is more clear.
 	var diff []string
-	var sign []byte
+	var sign []rune
 
 	i := len(aLines) - 1
 	j := len(bLines) - 1
@@ -92,18 +94,18 @@ func lcsDiff(aLines, bLines []string) string {
 			j--
 		} else if j > 0 && (i == 0 || c[i][j-1] >= c[i-1][j]) {
 			diff = append(diff, bLines[j])
-			sign = append(sign, '+')
+			sign = append(sign, bSign)
 			j--
 		} else if i > 0 && (j == 0 || c[i][j-1] < c[i-1][j]) {
 			diff = append(diff, aLines[i])
-			sign = append(sign, '-')
+			sign = append(sign, aSign)
 			i--
 		}
 	}
 
 	var sb strings.Builder
 	for i := len(diff) - 1; i >= 0; i-- {
-		sb.WriteByte(sign[i])
+		sb.WriteRune(sign[i])
 		sb.WriteByte(' ')
 		sb.WriteString(diff[i])
 		sb.WriteByte('\n')


Test patch:
diff --git a/cmd/gen-go-sample/main_test.go b/cmd/gen-go-sample/main_test.go
index 476892b..80a97d9 100644
--- a/cmd/gen-go-sample/main_test.go
+++ b/cmd/gen-go-sample/main_test.go
@@ -56,8 +56,9 @@ func TestSample(t *testing.T) {
 		Name: proto.String("FooService"),
 		Method: []*descriptor.MethodDescriptorProto{
 			{
-				Name:      proto.String("MyMethod"),
-				InputType: inType.Name,
+				Name:       proto.String("MyMethod"),
+				InputType:  inType.Name,
+				OutputType: inType.Name,
 			},
 		},
 	}
@@ -106,6 +107,11 @@ func TestSample(t *testing.T) {
 				{"b", true},
 			},
 		},
+		OnSuccess: []OutputSpec{
+			{Define: "out_a = $resp.a"},
+			{Print: []string{"x = %s", "$resp.a.x"}},
+			{Print: []string{"y = %s", "out_a.y"}},
+		},
 	}
 	if err := g.genSample("MyService", "MyMethod", "awesome_region", vs); err != nil {
 		t.Fatal(err)
diff --git a/cmd/gen-go-sample/testdata/sample.want b/cmd/gen-go-sample/testdata/sample.want
index a6d0d85..fe4ba61 100644
--- a/cmd/gen-go-sample/testdata/sample.want
+++ b/cmd/gen-go-sample/testdata/sample.want
@@ -50,7 +50,10 @@ req := &foopb.InputType{
 	if err != nil {
 		// TODO: Handle error.
 	}
-	fmt.Println(resp)
+
+outA := resp.A
+fmt.Printf("x = %v\n", resp.A.X)
+fmt.Printf("y = %v\n", outA.Y)
 }
 
 // [END awesome_region]