Repository: googleapis/gapic-generator-go
Pull request #49 (2018-11-01 17:42:58)

sample: make resource names work


Patch:
diff --git a/cmd/gen-go-sample/gapic.go b/cmd/gen-go-sample/gapic.go
index 9ad2457..aedc145 100644
--- a/cmd/gen-go-sample/gapic.go
+++ b/cmd/gen-go-sample/gapic.go
@@ -15,7 +15,8 @@
 package main
 
 type GAPICConfig struct {
-	Interfaces []GAPICInterface
+	Interfaces  []GAPICInterface
+	Collections []ResourceName
 }
 
 type GAPICInterface struct {
@@ -29,6 +30,9 @@ type GAPICMethod struct {
 	Samples         struct {
 		Standalone []GAPICSample
 	}
+
+	// map[fieldName]ResourceName.EntityName
+	FieldNamePatterns map[string]string `yaml:"field_name_patterns"`
 }
 
 type SampleValueSet struct {
@@ -65,3 +69,8 @@ type LoopSpec struct {
 	Variable   string
 	Body       []OutputSpec
 }
+
+type ResourceName struct {
+	EntityName  string `yaml:"entity_name"`
+	NamePattern string `yaml:"name_pattern"`
+}
diff --git a/cmd/gen-go-sample/inittree.go b/cmd/gen-go-sample/inittree.go
index b1f6b7b..5e4d58d 100644
--- a/cmd/gen-go-sample/inittree.go
+++ b/cmd/gen-go-sample/inittree.go
@@ -37,8 +37,9 @@ import (
 type initType struct {
 	// If the type is a protobuf "compound type", desc describes the type.
 	// If the type is a primitive, prim tells us what the type is.
-	desc pbinfo.ProtoType
-	prim descriptor.FieldDescriptorProto_Type
+	desc    pbinfo.ProtoType
+	namePat *namePattern
+	prim    descriptor.FieldDescriptorProto_Type
 
 	repeated bool
 
@@ -71,11 +72,24 @@ func (t *initTree) get(k string, info pbinfo.Info) (*initTree, error) {
 		}
 	}
 
+	if np := t.typ.namePat; np != nil {
+		if _, ok := np.pos[k]; !ok {
+			return nil, errors.E(nil, "resource name has no component: %q", k)
+		}
+
+		v := new(initTree)
+		v.typ.prim = descriptor.FieldDescriptorProto_TYPE_STRING
+
+		t.keys = append(t.keys, k)
+		t.vals = append(t.vals, v)
+		return v, nil
+	}
+
 	var fields []*descriptor.FieldDescriptorProto
 	if msg, ok := t.typ.desc.(*descriptor.DescriptorProto); ok {
 		fields = msg.Field
 	} else {
-		return nil, errors.E(nil, "type does not have fields: %T", t.typ.desc)
+		return nil, errors.E(nil, "not a message type: %T", t.typ.desc)
 	}
 
 	v := new(initTree)
@@ -205,13 +219,21 @@ func (t *initTree) parseSampleArgPath(txt string, info pbinfo.Info, varName stri
 func (t *initTree) parsePathRest(sc *scanner.Scanner, info pbinfo.Info) (*initTree, rune, error) {
 	for {
 		switch r := sc.Scan(); r {
-		case '.':
+		case '.', '%':
 			if t.typ.repeated {
 				return nil, 0, errors.E(nil, "cannot access member of repeated field")
 			}
 			if r := sc.Scan(); r != scanner.Ident {
 				return nil, r, errors.E(nil, "expected ident, found %q", sc.TokenText())
 			}
+
+			if r == '.' && t.typ.desc == nil {
+				return nil, r, errors.E(nil, "field is not a message")
+			}
+			if r == '%' && t.typ.namePat == nil {
+				return nil, r, errors.E(nil, "field is not a resource name")
+			}
+
 			if t2, err := t.get(sc.TokenText(), info); err != nil {
 				return nil, 0, err
 			} else {
@@ -275,6 +297,7 @@ func (t *initTree) Print(w io.Writer, g *generator) error {
 }
 
 func (t *initTree) print(w *bufio.Writer, g *generator, ind int) error {
+	// TODO(pongad): method is getting long; split and test
 	if prim := t.typ.prim; prim != 0 && t.leafVal == "" {
 		return errors.E(nil, "init value not defined for primitive type: %s", prim)
 	}
@@ -290,6 +313,25 @@ func (t *initTree) print(w *bufio.Writer, g *generator, ind int) error {
 		return nil
 	}
 
+	if np := t.typ.namePat; np != nil {
+		fmt.Fprintf(w, "fmt.Sprintf(%q", np.fmtSpec())
+
+		for i := 1; i < len(np.pieces); i += 2 {
+			placeHolder := np.pieces[i]
+			val, err := t.get(placeHolder, g.descInfo)
+			if err != nil {
+				return err
+			}
+
+			// leafVal strings are already quoted.
+			fmt.Fprintf(w, ", %s", val.leafVal)
+		}
+
+		w.WriteByte(')')
+		g.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
+		return nil
+	}
+
 	desc := t.typ.desc
 	if desc == nil {
 		return errors.E(nil, "internal error? value neither primitive nor compound type")
diff --git a/cmd/gen-go-sample/main.go b/cmd/gen-go-sample/main.go
index 0a5ad19..3330eea 100644
--- a/cmd/gen-go-sample/main.go
+++ b/cmd/gen-go-sample/main.go
@@ -112,7 +112,7 @@ func genMethodSamples(gen *generator, iface GAPICInterface, meth GAPICMethod, no
 			}
 
 			gen.reset()
-			if err := gen.genSample(iface.Name, meth.Name, sam.RegionTag, vs); err != nil {
+			if err := gen.genSample(iface.Name, meth, sam.RegionTag, vs); err != nil {
 				return errors.E(err, "value set: %s", vsID)
 			}
 			content, err := gen.commit(!nofmt, time.Now().Year())
@@ -194,7 +194,9 @@ func (g *generator) commit(gofmt bool, year int) ([]byte, error) {
 	return b, nil
 }
 
-func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleValueSet) error {
+func (g *generator) genSample(ifaceName string, methConf GAPICMethod, regTag string, valSet SampleValueSet) error {
+	// TODO(pongad): This method's error cases are not well tested. Split and test.
+
 	g.imports[g.clientPkg] = true
 	serv := g.descInfo.Serv["."+ifaceName]
 	if serv == nil {
@@ -203,13 +205,13 @@ func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleV
 
 	var meth *descriptor.MethodDescriptorProto
 	for _, m := range serv.GetMethod() {
-		if m.GetName() == methName {
+		if m.GetName() == methConf.Name {
 			meth = m
 			break
 		}
 	}
 	if meth == nil {
-		return errors.E(nil, "service %q doesn't have method %q", serv.GetName(), methName)
+		return errors.E(nil, "service %q doesn't have method %q", serv.GetName(), methConf.Name)
 	}
 
 	inType := g.descInfo.Type[meth.GetInputType()]
@@ -229,11 +231,44 @@ func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleV
 	itree := initTree{
 		typ: initType{desc: inType},
 	}
+
+	// Set up resource names. We need this info when setting up request object.
+	for field, entName := range methConf.FieldNamePatterns {
+		var pat string
+		for _, col := range g.gapic.Collections {
+			if col.EntityName == entName {
+				pat = col.NamePattern
+				break
+			}
+		}
+		if pat == "" {
+			return errors.E(nil, "undefined resource name: %q", entName)
+		}
+
+		namePat, err := parseNamePattern(pat)
+		if err != nil {
+			return err
+		}
+
+		subTree, err := itree.get(field, g.descInfo)
+		if err != nil {
+			return errors.E(err, "cannot set up resource name: %q", entName)
+		}
+		if typ := subTree.typ; typ.prim != descriptor.FieldDescriptorProto_TYPE_STRING {
+			return errors.E(err, "cannot set up resource name for %q, need field to be string, got %v", field, typ)
+		}
+		subTree.typ.prim = 0
+		subTree.typ.namePat = &namePat
+	}
+
+	// Set up request object.
 	for _, def := range valSet.Parameters.Defaults {
 		if err := itree.parseInit(def, g.descInfo); err != nil {
 			return errors.E(err, "can't set default value: %q", def)
 		}
 	}
+
+	// Some parts of request object are from arguments.
 	for _, attr := range valSet.Parameters.Attributes {
 		if attr.SampleArgumentName == "" {
 			continue
@@ -262,7 +297,7 @@ func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleV
 		argStr = sb.String()[2:]
 	}
 
-	p("func sample%s(%s) error {", methName, argStr)
+	p("func sample%s(%s) error {", meth.GetName(), argStr)
 	p("  ctx := context.Background()")
 	p("  c, err := %s.New%sClient(ctx)", g.clientPkg.Name, pbinfo.ReduceServName(serv.GetName(), g.clientPkg.Name))
 	p("  if err != nil {")
@@ -329,7 +364,7 @@ func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleV
 	}
 
 	p("  flag.Parse()")
-	p("  if err := sample%s(%s); err != nil {", methName, flagArgs(argNames))
+	p("  if err := sample%s(%s); err != nil {", meth.GetName(), flagArgs(argNames))
 	p("    log.Fatal(err)")
 	p("  }")
 	p("}")
diff --git a/cmd/gen-go-sample/resource.go b/cmd/gen-go-sample/resource.go
new file mode 100644
index 0000000..ffee643
--- /dev/null
+++ b/cmd/gen-go-sample/resource.go
@@ -0,0 +1,79 @@
+// 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 (
+	"strings"
+
+	"github.com/googleapis/gapic-generator-go/internal/errors"
+)
+
+// namePattern represents a specially formatted string used in API definitions.
+// The format is written in string form like this: "shelves/{shelf}/books/{book}".
+// The curly braces delimit placeholders.
+type namePattern struct {
+	// Even positions are literal strings.
+	// Odd positions are placeholders with braces already stripped.
+	pieces []string
+
+	// maps placeholder to the location in pieces
+	pos map[string]int
+}
+
+func parseNamePattern(s string) (namePattern, error) {
+	origPat := s
+	var pieces []string
+
+	for {
+		p := strings.IndexByte(s, '{')
+		if p < 0 {
+			if s != "" {
+				pieces = append(pieces, s)
+			}
+			break
+		}
+		pieces = append(pieces, s[:p])
+		s = s[p:]
+
+		p = strings.IndexByte(s, '}')
+		if p < 0 {
+			return namePattern{}, errors.E(nil, "unclosed brace: %q", origPat)
+		}
+		pieces = append(pieces, s[1:p])
+		s = s[p+1:]
+	}
+
+	pos := map[string]int{}
+	for i := 1; i < len(pieces); i += 2 {
+		piece := pieces[i]
+		if _, exist := pos[piece]; exist {
+			return namePattern{}, errors.E(nil, "duplicate placeholder %q: %q", piece, origPat)
+		}
+		pos[piece] = i
+	}
+	return namePattern{pieces, pos}, nil
+}
+
+func (np namePattern) fmtSpec() string {
+	var sb strings.Builder
+	for i := range np.pieces {
+		if i%2 == 0 {
+			sb.WriteString(np.pieces[i])
+		} else {
+			sb.WriteString("%s")
+		}
+	}
+	return sb.String()
+}


Test patch:
diff --git a/cmd/gen-go-sample/main_test.go b/cmd/gen-go-sample/main_test.go
index fc791fb..ca59ab0 100644
--- a/cmd/gen-go-sample/main_test.go
+++ b/cmd/gen-go-sample/main_test.go
@@ -43,10 +43,14 @@ func TestUnary(t *testing.T) {
 				`a_array[0].y = 1`,
 				`a_array[1].x = 2`,
 				`a_array[1].y = 3`,
+
+				`resource_field%foo="myfoo"`,
+				`resource_field%bar="mybar"`,
 			},
 			Attributes: []SampleAttribute{
 				{"a.x", "the_x"},
 				{"b", "the_b"},
+				{"resource_field%foo", "the_foo"},
 			},
 		},
 		OnSuccess: []OutputSpec{
@@ -64,7 +68,12 @@ func TestUnary(t *testing.T) {
 			},
 		},
 	}
-	if err := g.genSample("foo.FooService", "UnaryMethod", "awesome_region", vs); err != nil {
+
+	methConf := GAPICMethod{
+		Name:              "UnaryMethod",
+		FieldNamePatterns: map[string]string{"resource_field": "foobar_thing"},
+	}
+	if err := g.genSample("foo.FooService", methConf, "awesome_region", vs); err != nil {
 		t.Fatal(err)
 	}
 
@@ -101,7 +110,7 @@ func TestSample_InitError(t *testing.T) {
 			},
 		}
 
-		if err := g.genSample("foo.FooService", "UnaryMethod", "awesome_region", vs); err == nil {
+		if err := g.genSample("foo.FooService", GAPICMethod{Name: "UnaryMethod"}, "awesome_region", vs); err == nil {
 			t.Errorf("expected error from init config: %s", tst)
 		}
 	}
@@ -115,7 +124,7 @@ func TestPaging(t *testing.T) {
 	vs := SampleValueSet{
 		ID: "my_value_set",
 	}
-	if err := g.genSample("foo.FooService", "PagingMethod", "awesome_region", vs); err != nil {
+	if err := g.genSample("foo.FooService", GAPICMethod{Name: "PagingMethod"}, "awesome_region", vs); err != nil {
 		t.Fatal(err)
 	}
 	compare(t, g, filepath.Join("testdata", "sample_paging.want"))
@@ -128,7 +137,7 @@ func TestEmpty(t *testing.T) {
 	vs := SampleValueSet{
 		ID: "my_value_set",
 	}
-	if err := g.genSample("foo.FooService", "EmptyMethod", "awesome_region", vs); err != nil {
+	if err := g.genSample("foo.FooService", GAPICMethod{Name: "EmptyMethod"}, "awesome_region", vs); err != nil {
 		t.Fatal(err)
 	}
 	compare(t, g, filepath.Join("testdata", "sample_empty.want"))
@@ -151,6 +160,7 @@ func initTestGenerator() *generator {
 			{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("r"), Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING), Label: labelp(descriptor.FieldDescriptorProto_LABEL_REPEATED)},
+			{Name: proto.String("resource_field"), Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING)},
 		},
 	}
 	eType := &descriptor.EnumDescriptorProto{
@@ -217,6 +227,11 @@ func initTestGenerator() *generator {
 		clientPkg: pbinfo.ImportSpec{Path: "path.to/client/foo", Name: "foo"},
 		imports:   map[pbinfo.ImportSpec]bool{},
 		descInfo:  pbinfo.Of([]*descriptor.FileDescriptorProto{file}),
+		gapic: GAPICConfig{
+			Collections: []ResourceName{
+				{EntityName: "foobar_thing", NamePattern: "foos/{foo}/bars/{bar}"},
+			},
+		},
 	}
 }
 
diff --git a/cmd/gen-go-sample/resource_test.go b/cmd/gen-go-sample/resource_test.go
new file mode 100644
index 0000000..db93e4e
--- /dev/null
+++ b/cmd/gen-go-sample/resource_test.go
@@ -0,0 +1,58 @@
+// 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 (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestParseNamePattern(t *testing.T) {
+	t.Run("Error", func(t *testing.T) {
+		for _, s := range []string{
+			"foos/{foo",
+			"foos/{foo}/{foo}",
+		} {
+			if _, err := parseNamePattern(s); err == nil {
+				t.Errorf("parseNamePattern(%q) OK, want error", s)
+			}
+		}
+	})
+
+	for _, tst := range []struct {
+		s      string
+		pieces []string
+	}{
+		{"foos/{foo}", []string{"foos/", "foo"}},
+		{"foos/{foo}/bars/{bar}", []string{"foos/", "foo", "/bars/", "bar"}},
+	} {
+		np, err := parseNamePattern(tst.s)
+		if err != nil {
+			t.Errorf("parseNamePattern(%q) errors %q, want %q", tst.s, err, tst.pieces)
+			continue
+		}
+		if diff := cmp.Diff(tst.pieces, np.pieces); diff != "" {
+			t.Errorf("parseNamePattern(%q).pieces: (got=-, want=+):\n%s", tst.s, diff)
+			continue
+		}
+		for i := 1; i < len(np.pieces); i += 2 {
+			p := np.pieces[i]
+			if got := np.pos[p]; got != i {
+				t.Errorf("parseNamePattern(%q).pos[%q] = %d, want %d", tst.s, p, got, i)
+			}
+		}
+	}
+}
diff --git a/cmd/gen-go-sample/testdata/sample_unary.want b/cmd/gen-go-sample/testdata/sample_unary.want
index 4185aec..441550d 100644
--- a/cmd/gen-go-sample/testdata/sample_unary.want
+++ b/cmd/gen-go-sample/testdata/sample_unary.want
@@ -26,7 +26,7 @@ foopb "path.to/pb/foo"
 )
 // [START awesome_region]
 
-func sampleUnaryMethod(theX int64, theB string) error {
+func sampleUnaryMethod(theX int64, theB string, theFoo string) error {
 	ctx := context.Background()
 	c, err := foo.NewClient(ctx)
 	if err != nil {
@@ -35,7 +35,9 @@ func sampleUnaryMethod(theX int64, theB string) error {
 
 	// theX := 42
 	// theB := "foobar"
+	// theFoo := "myfoo"
 	req := &foopb.InputType{
+		ResourceField: fmt.Sprintf("foos/%s/bars/%s", theFoo, "mybar"),
 		A: &foopb.AType{
 			X: theX,
 			Y: 3.14159,
@@ -75,8 +77,9 @@ func sampleUnaryMethod(theX int64, theB string) error {
 func main() {
 	theX := flag.Int64("theX", 42, "")
 	theB := flag.String("theB", "foobar", "")
+	theFoo := flag.String("theFoo", "myfoo", "")
 	flag.Parse()
-	if err := sampleUnaryMethod(*theX, *theB); err != nil {
+	if err := sampleUnaryMethod(*theX, *theB, *theFoo); err != nil {
 		log.Fatal(err)
 	}
 }