Repository: googleapis/gapic-generator-go
Pull request #102 (2019-03-02 00:17:20)

SampleGen: generate file input for bytes fields
- When `parameters.attributes.read_file` is true, substitute the field with contents from a local file, whose name is what originally set to this field.
- Unlike languages for which we implemented SampleGen in gapic-generator, Go initializes the request object in a nested way, so we have to generate a local variable for the bytes read from the local file. If `SampleArgumentName` is also specified for this parameter, we use that with a `Content` suffix as the name of the local variable. If `SampleArgumentName` is not specified, we use the protobuf field name with a `Content` suffix instead.

hzyi-google: @vchudnov-g @noahdietz @beccasaurus PTAL

@pongad FYI since we discussed about this. But don't feel obliged to review this PR :)
hzyi-google: PTAL
hzyi-google: PTAL @vchudnov-g @beccasaurus 

Patch:
diff --git a/cmd/gen-go-sample/bytes.go b/cmd/gen-go-sample/bytes.go
index 3388652..a747c43 100644
--- a/cmd/gen-go-sample/bytes.go
+++ b/cmd/gen-go-sample/bytes.go
@@ -14,8 +14,76 @@
 
 package main
 
+import (
+	"bytes"
+	"fmt"
+	"text/scanner"
+
+	"github.com/googleapis/gapic-generator-go/internal/errors"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
+)
+
 func bytesFmt() func(*generator, string) (string, error) {
 	return func(_ *generator, s string) (string, error) {
 		return "[]byte(" + s + ")", nil
 	}
 }
+
+// fileInfo keeps track of information we need to generate reading all bytes of a file
+// and assign them to a local variable.
+type fileInfo struct {
+	// fileName is the text of the file name. If it's a string literal, it's already quoted.
+	fileName string
+	// varName is the name of the local variable to hold the bytes of the file.
+	varName string
+}
+
+const fileContentSuffix = "Bytes"
+
+// fileVarName generates a name for the local variable that hold bytes from a local file. It is only
+// used when SampleArgumentName is not provided.
+func fileVarName(param string) (string, error) {
+	// TODO: if there are multiple fields whose values are to be copied from local files,
+	// this method cannot generate a unique variable name for each of them when:
+	// 1) SampleArgumentName are not provided for some or all of these fields
+	// 2) these fields have the same name
+	// Considering the collision only happens in very rare cases, it is not handled for now
+	sc, _ := initScanner("." + param)
+
+	var varName string
+	for {
+		switch r := sc.Scan(); r {
+		case '%':
+			return "", errors.E(nil, "bad format for bytes field, unexpected '%%'")
+		case '.':
+			if r := sc.Scan(); r != scanner.Ident {
+				return "", errors.E(nil, "expected ident, found %q", sc.TokenText())
+			}
+			varName = sc.TokenText()
+		case '[':
+			if r := sc.Scan(); r != scanner.Int {
+				return "", errors.E(nil, "expected int, found %q", sc.TokenText())
+			}
+			if r := sc.Scan(); r != ']' {
+				return "", errors.E(nil, "expected ']', found %q", sc.TokenText())
+			}
+		case scanner.EOF:
+			return snakeToCamel(varName), nil
+		default:
+			return "", errors.E(nil, "unhandled rune: %c", r)
+		}
+	}
+}
+
+// readFile generates code that read all bytes from a local file and assign
+// to a local variable.
+func readFile(info *fileInfo, buf *bytes.Buffer, g *generator) {
+	vn := info.varName
+	fn := info.fileName
+	g.imports[pbinfo.ImportSpec{Path: "io/ioutil"}] = true
+
+	fmt.Fprintf(buf, "%s, err := ioutil.ReadFile(%s)\n", vn, fn)
+	fmt.Fprintf(buf, "if err != nil {\n")
+	fmt.Fprintf(buf, "\treturn err\n")
+	fmt.Fprintf(buf, "}\n\n")
+}
diff --git a/cmd/gen-go-sample/main.go b/cmd/gen-go-sample/main.go
index 2093b13..c6de1d9 100644
--- a/cmd/gen-go-sample/main.go
+++ b/cmd/gen-go-sample/main.go
@@ -227,6 +227,7 @@ func (g *generator) genSample(ifaceName string, methConf GAPICMethod, regTag str
 		argNames  []string
 		flagNames []string
 		argTrees  []*initTree
+		files     []*fileInfo
 	)
 
 	itree := initTree{
@@ -271,6 +272,43 @@ func (g *generator) genSample(ifaceName string, methConf GAPICMethod, regTag str
 
 	// Some parts of request object are from arguments.
 	for _, attr := range valSet.Parameters.Attributes {
+		if attr.ReadFile {
+			var varName string
+			if attr.SampleArgumentName != "" {
+				varName = snakeToCamel(attr.SampleArgumentName) + fileContentSuffix
+			} else {
+				varName, err = fileVarName(attr.Parameter)
+				if err != nil {
+					return errors.E(err, "can't determine variable name to store bytes from local file")
+				}
+			}
+
+			subTree, err := itree.parseSampleArgPath(
+				attr.Parameter,
+				g.descInfo,
+				varName,
+			)
+			if err != nil {
+				return errors.E(err, "can't set sample function argument: %q", attr.Parameter)
+			}
+			if subTree.typ.prim != descriptor.FieldDescriptorProto_TYPE_BYTES {
+				return errors.E(nil, "can only assign file contents to bytes field")
+			}
+			subTree.typ.prim = descriptor.FieldDescriptorProto_TYPE_STRING
+			subTree.typ.valFmt = nil
+			if subTree.leafVal == "" {
+				return errors.E(nil, "default value not given: %q", attr.Parameter)
+			}
+			fileName := subTree.leafVal
+			if attr.SampleArgumentName != "" {
+				fileName = snakeToCamel(attr.SampleArgumentName)
+				argNames = append(argNames, fileName)
+				flagNames = append(flagNames, attr.SampleArgumentName)
+				argTrees = append(argTrees, subTree)
+			}
+			files = append(files, &fileInfo{fileName, varName})
+			continue
+		}
 		if attr.SampleArgumentName == "" {
 			continue
 		}
@@ -318,14 +356,18 @@ func (g *generator) genSample(ifaceName string, methConf GAPICMethod, regTag str
 			}
 			buf.WriteByte('\n')
 		}
-		prependLines(&buf, "// ")
+		prependLines(&buf, "// ", false)
+
+		for _, info := range files {
+			readFile(info, &buf, g)
+		}
 
 		buf.WriteString("req := ")
 		if err := itree.Print(&buf, g); err != nil {
 			return errors.E(err, "can't initialize request object")
 		}
 		buf.WriteByte('\n')
-		prependLines(&buf, "\t")
+		prependLines(&buf, "\t", true)
 
 		if _, err := buf.WriteTo(g.pt.Writer()); err != nil {
 			return err
@@ -466,7 +508,8 @@ func (g *generator) handleOut(meth *descriptor.MethodDescriptorProto, valSet Sam
 // prependLines adds prefix to every line in b. A line is defined as a possibly empty run
 // of non-newlines terminated by a newline character.
 // If b doesn't end with a newline, prependLines panics.
-func prependLines(b *bytes.Buffer, prefix string) {
+// If skipEmptyLine is true, prependLines does not prepend prefix to empty lines.
+func prependLines(b *bytes.Buffer, prefix string, skipEmptyLine bool) {
 	if b.Len() == 0 {
 		return
 	}
@@ -481,7 +524,9 @@ func prependLines(b *bytes.Buffer, prefix string) {
 		if l == "" {
 			continue
 		}
-		b.WriteString(prefix)
+		if !skipEmptyLine || l != "\n" {
+			b.WriteString(prefix)
+		}
 		b.WriteString(l)
 	}
 }


Test patch:
diff --git a/cmd/gen-go-sample/main_test.go b/cmd/gen-go-sample/main_test.go
index dd73a8c..b26c578 100644
--- a/cmd/gen-go-sample/main_test.go
+++ b/cmd/gen-go-sample/main_test.go
@@ -39,6 +39,8 @@ func TestUnary(t *testing.T) {
 				`e = FOO`,
 				`f = "in a oneof"`,
 				`bytes = "mybytes"`,
+				`data_alice = "path/to/local/file/alice.txt"`,
+				`data_bob = "path/to/local/file/bob.txt"`,
 
 				`a_array[0].x = 0`,
 				`a_array[0].y = 1`,
@@ -52,6 +54,8 @@ func TestUnary(t *testing.T) {
 				{Parameter: "a.x", SampleArgumentName: "the_x"},
 				{Parameter: "b", SampleArgumentName: "the_b"},
 				{Parameter: "resource_field%foo", SampleArgumentName: "the_foo"},
+				{Parameter: "data_alice", ReadFile: true},
+				{Parameter: "data_bob", SampleArgumentName: "bob_file", ReadFile: true},
 			},
 		},
 		OnSuccess: []OutputSpec{
@@ -160,6 +164,8 @@ func initTestGenerator() *generator {
 			{Name: proto.String("b"), Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING)},
 			{Name: proto.String("e"), TypeName: proto.String(".foo.AType.EType")},
 			{Name: proto.String("f"), Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING), OneofIndex: proto.Int32(0)},
+			{Name: proto.String("data_alice"), Type: typep(descriptor.FieldDescriptorProto_TYPE_BYTES)},
+			{Name: proto.String("data_bob"), Type: typep(descriptor.FieldDescriptorProto_TYPE_BYTES)},
 			{Name: proto.String("r"), Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING), Label: labelp(descriptor.FieldDescriptorProto_LABEL_REPEATED)},
 			{Name: proto.String("resource_field"), Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING)},
 			{Name: proto.String("bytes"), Type: typep(descriptor.FieldDescriptorProto_TYPE_BYTES)},
diff --git a/cmd/gen-go-sample/testdata/sample_unary.want b/cmd/gen-go-sample/testdata/sample_unary.want
index ed09ab3..3eba9a6 100644
--- a/cmd/gen-go-sample/testdata/sample_unary.want
+++ b/cmd/gen-go-sample/testdata/sample_unary.want
@@ -19,6 +19,7 @@ import(
  "context"
  "flag"
  "fmt"
+ "io/ioutil"
  "log"
 
 foo "path.to/client/foo"
@@ -26,7 +27,7 @@ foopb "path.to/pb/foo"
 )
 // [START awesome_region]
 
-func sampleUnaryMethod(theX int64, theB string, theFoo string) error {
+func sampleUnaryMethod(theX int64, theB string, theFoo string, bobFile string) error {
 	ctx := context.Background()
 	c, err := foo.NewClient(ctx)
 	if err != nil {
@@ -36,6 +37,17 @@ func sampleUnaryMethod(theX int64, theB string, theFoo string) error {
 	// theX := 42
 	// theB := "foobar"
 	// theFoo := "myfoo"
+	// bobFile := "path/to/local/file/bob.txt"
+	dataAlice, err := ioutil.ReadFile("path/to/local/file/alice.txt")
+	if err != nil {
+		return err
+	}
+
+	bobFileBytes, err := ioutil.ReadFile(bobFile)
+	if err != nil {
+		return err
+	}
+
 	req := &foopb.InputType{
 		ResourceField: fmt.Sprintf("foos/%s/bars/%s", theFoo, "mybar"),
 		A: &foopb.AType{
@@ -48,6 +60,8 @@ func sampleUnaryMethod(theX int64, theB string, theFoo string) error {
 			F: "in a oneof",
 		},
 		Bytes: []byte("mybytes"),
+		DataAlice: dataAlice,
+		DataBob: bobFileBytes,
 		AArray: []*foopb.AType{
 			&foopb.AType{
 				X: 0,
@@ -79,8 +93,9 @@ func main() {
 	theX := flag.Int64("the_x", 42, "")
 	theB := flag.String("the_b", "foobar", "")
 	theFoo := flag.String("the_foo", "myfoo", "")
+	bobFile := flag.String("bob_file", "path/to/local/file/bob.txt", "")
 	flag.Parse()
-	if err := sampleUnaryMethod(*theX, *theB, *theFoo); err != nil {
+	if err := sampleUnaryMethod(*theX, *theB, *theFoo, *bobFile); err != nil {
 		log.Fatal(err)
 	}
 }