Repository: googleapis/gapic-generator-go
Pull request #7 (2018-09-21 01:42:39)

make simple init tree work
Only struct members and no validation for now.


Patch:
diff --git a/README.md b/README.md
index b5bb96a..645803b 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,25 @@ API Client Generator for Go
 
 [![CircleCI](https://circleci.com/gh/googleapis/gapic-generator-go.svg?style=svg)](https://circleci.com/gh/googleapis/gapic-generator-go)
 
-The generator is incomplete. Please do not use it for anything mission critical.
+A generator for protocol buffer described APIs for and in Go.
 
-This generator generates API client libraries for APIs specified by protocol buffers.
+This is a generator for API client libraries for APIs specified by protocol buffers, such as those inside Google.
+It takes a protocol buffer (with particular annotations) and uses it to generate a client library.
+
+Purpose
+-------
+We aim for this generator to replace the [older monolithic generator](https://github.com/googleapis/gapic-generator).
+Some areas we hope to improve over the old generator are:
+- using explicit normalized format for specifying APIs,
+- simpler, faster implementation, and
+- better error reporting.
+
+Disclaimer
+----------
+This generator is currently experimental. Please don't use it for anything mission-critical.
+
+Go Version Supported
+--------------------
+The generator itself supports the latest version.
+
+The generated code is compatible with Go 1.6.
diff --git a/cmd/gen-go-sample/inittree.go b/cmd/gen-go-sample/inittree.go
new file mode 100644
index 0000000..66071d7
--- /dev/null
+++ b/cmd/gen-go-sample/inittree.go
@@ -0,0 +1,158 @@
+// 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"
+	"io"
+	"strings"
+	"text/scanner"
+	"unicode"
+
+	"github.com/googleapis/gapic-generator-go/internal/errors"
+)
+
+// initTree represents a node in the initialization tree.
+type initTree struct {
+	// T is either a composite value (struct/array/map), where keys and vals are set,
+	// or a simple value, where leafVal is set.
+
+	// Use array representation; we need order, and we probably won't
+	// have many pairs anyway.
+	keys []string
+	vals []*initTree
+
+	// Text of the literal. If the literal is a string, it's already quoted.
+	leafVal string
+}
+
+func (t *initTree) get(k string) *initTree {
+	for i, key := range t.keys {
+		if k == key {
+			return t.vals[i]
+		}
+	}
+
+	v := new(initTree)
+	t.keys = append(t.keys, k)
+	t.vals = append(t.vals, v)
+	return v
+}
+
+func (t *initTree) Parse(txt string) error {
+	var sc scanner.Scanner
+	sc.Init(strings.NewReader(txt))
+	sc.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings
+
+	// Scanner reports error by calling sc.Error; we save the error so we always report
+	// the first.
+	var err error
+	report := func(e error) error {
+		if err == nil {
+			err = e
+		}
+		return err
+	}
+	sc.Error = func(_ *scanner.Scanner, msg string) {
+		e := errors.E(nil, msg)
+		e = errors.E(e, "while scaning: %q", txt)
+		report(e)
+	}
+
+	// Just do simple structs for now.
+	// TODO(pongad): allow map and array index.
+	// spec = ident { '.' ident } [ '=' value ] .
+
+	if sc.Scan() != scanner.Ident {
+		return report(errors.E(nil, "expected ident, found %q", sc.TokenText()))
+	}
+	t = t.get(sc.TokenText())
+
+	for {
+		switch sc.Scan() {
+		case '=':
+			goto equal
+		case scanner.EOF:
+			// TODO: set t.leafVal to zero value of type.
+			return report(nil)
+
+		case '.':
+			if sc.Scan() != scanner.Ident {
+				return report(errors.E(nil, "expected ident, found %q", sc.TokenText()))
+			}
+			t = t.get(sc.TokenText())
+
+		default:
+			return report(errors.E(nil, "unexpected %q", sc.TokenText()))
+		}
+	}
+
+	// TODO(pongad): check that value and type match
+	// TODO(pongad): check that we don't write two values onto the same node
+	// TODO(pongad): handle resource names
+	// TODO(pongad): properly validate and print enums
+equal:
+	switch r := sc.Scan(); r {
+	case scanner.Int, scanner.Float, scanner.String, scanner.Ident:
+		t.leafVal = sc.TokenText()
+	default:
+		return report(errors.E(nil, "expected value, found %q", sc.TokenText()))
+	}
+
+	if sc.Scan() != scanner.EOF {
+		return report(errors.E(nil, "expected EOF, found %q", sc.TokenText()))
+	}
+	return err
+}
+
+func (t *initTree) Print(w io.Writer) error {
+	bw := bufio.NewWriter(w)
+	t.print(bw, 1)
+	return bw.Flush()
+}
+
+func (t *initTree) print(w *bufio.Writer, ind int) {
+	if v := t.leafVal; v != "" {
+		w.WriteString(v)
+		return
+	}
+
+	// TODO(pongad): Figure out how to print type.
+	w.WriteString("TYPE{\n")
+	for i, k := range t.keys {
+		for i := 0; i < ind; i++ {
+			w.WriteByte('\t')
+		}
+		snakeToCapCamel(w, k)
+		w.WriteString(": ")
+		t.vals[i].print(w, ind+1)
+		w.WriteString(",\n")
+	}
+	w.WriteString("}")
+}
+
+func snakeToCapCamel(w *bufio.Writer, s string) {
+	cap := true
+	for _, r := range s {
+		if r == '_' {
+			cap = true
+		} else if cap {
+			cap = false
+			w.WriteRune(unicode.ToUpper(r))
+		} else {
+			w.WriteRune(r)
+		}
+	}
+}
diff --git a/cmd/gen-go-sample/main.go b/cmd/gen-go-sample/main.go
index e364b64..048b475 100644
--- a/cmd/gen-go-sample/main.go
+++ b/cmd/gen-go-sample/main.go
@@ -24,10 +24,12 @@ import (
 	"os"
 	"sort"
 	"strings"
+	"time"
 
 	"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/license"
 	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
 	"github.com/googleapis/gapic-generator-go/internal/printer"
 	yaml "gopkg.in/yaml.v2"
@@ -97,7 +99,7 @@ func main() {
 						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))
+						log.Fatal(errors.E(err, "can't commit value set: %q", vs))
 					}
 				}
 			}
@@ -144,8 +146,8 @@ func (g *generator) commit(gofmt bool) error {
 
 	firstNonStd := sort.Search(len(imports), func(i int) bool { return strings.IndexByte(imports[i].Path, '.') >= 0 })
 
-	// TODO(pongad): add license
 	var file bytes.Buffer
+	fmt.Fprintf(&file, license.Apache, time.Now().Year())
 	file.WriteString("package main\n")
 	file.WriteString("import(\n")
 	for i, imp := range imports {
@@ -161,7 +163,7 @@ func (g *generator) commit(gofmt bool) error {
 	if gofmt {
 		b2, err := format.Source(b)
 		if err != nil {
-			return err
+			return errors.E(err, "syntax error, run with -nofmt to find out why?")
 		}
 		b = b2
 	}
@@ -214,11 +216,25 @@ func (g *generator) genSample(ifaceName, methName, regTag string, valSet SampleV
 		return errors.E(err, "can't import input type: %q", inType)
 	}
 
-	p("  req := %s.%s{", inSpec.Name, inType.GetName())
+	var itree initTree
 	for _, def := range valSet.Parameters.Defaults {
-		p("// %s", def)
+		if err := itree.Parse(def); err != nil {
+			return errors.E(err, "can't set default value: %q", def)
+		}
+	}
+	{
+		w := g.pt.Writer()
+
+		if _, err := w.Write([]byte("req := ")); err != nil {
+			return err
+		}
+		if err := itree.Print(g.pt.Writer()); err != nil {
+			return err
+		}
+		if _, err := w.Write([]byte{'\n'}); err != nil {
+			return err
+		}
 	}
-	p("  }")
 
 	// TODO(pongad): handle non-unary
 	p("  resp, err := c.%s(ctx, req)", methName)
diff --git a/cmd/protoc-gen-gogapic/main.go b/cmd/protoc-gen-go_gapic/main.go
similarity index 100%
rename from cmd/protoc-gen-gogapic/main.go
rename to cmd/protoc-gen-go_gapic/main.go
diff --git a/gengapic/client_init.go b/gengapic/client_init.go
index c7aa1d5..ac155f3 100644
--- a/gengapic/client_init.go
+++ b/gengapic/client_init.go
@@ -20,6 +20,7 @@ import (
 	"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"
 	"google.golang.org/genproto/googleapis/api/annotations"
 )
 
@@ -28,22 +29,15 @@ func (g *generator) clientOptions(serv *descriptor.ServiceDescriptorProto, servN
 
 	// CallOptions struct
 	{
-		var maxNameLen int
-		for _, m := range serv.Method {
-			if l := len(*m.Name); maxNameLen < l {
-				maxNameLen = l
-			}
-		}
-
 		p("// %[1]sCallOptions contains the retry settings for each method of %[1]sClient.", servName)
 		p("type %sCallOptions struct {", servName)
 		for _, m := range serv.Method {
-			p("%s%s[]gax.CallOption", *m.Name, spaces(maxNameLen-len(*m.Name)+1))
+			p("%s []gax.CallOption", *m.Name)
 		}
 		p("}")
 		p("")
 
-		g.imports[importSpec{"gax", "github.com/googleapis/gax-go"}] = true
+		g.imports[pbinfo.ImportSpec{"gax", "github.com/googleapis/gax-go"}] = true
 	}
 
 	// defaultClientOptions
@@ -61,7 +55,7 @@ func (g *generator) clientOptions(serv *descriptor.ServiceDescriptorProto, servN
 		p("}")
 		p("")
 
-		g.imports[importSpec{path: "google.golang.org/api/option"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/option"}] = true
 	}
 
 	// defaultCallOptions
@@ -98,8 +92,8 @@ func (g *generator) clientOptions(serv *descriptor.ServiceDescriptorProto, servN
 			p("}")
 			p("")
 
-			g.imports[importSpec{path: "time"}] = true
-			g.imports[importSpec{path: "google.golang.org/grpc/codes"}] = true
+			g.imports[pbinfo.ImportSpec{Path: "time"}] = true
+			g.imports[pbinfo.ImportSpec{Path: "google.golang.org/grpc/codes"}] = true
 		}
 
 		p("  return &%sCallOptions{", servName)
@@ -125,7 +119,7 @@ func (g *generator) clientInit(serv *descriptor.ServiceDescriptorProto, servName
 		}
 	}
 
-	imp, err := g.importSpec(serv)
+	imp, err := g.descInfo.ImportSpec(serv)
 	if err != nil {
 		return err
 	}
@@ -142,7 +136,7 @@ func (g *generator) clientInit(serv *descriptor.ServiceDescriptorProto, servName
 		p("")
 
 		p("// The gRPC API client.")
-		p("%s %s.%sClient", grpcClientField(servName), imp.name, serv.GetName())
+		p("%s %s.%sClient", grpcClientField(servName), imp.Name, serv.GetName())
 		p("")
 
 		if hasLRO {
@@ -152,7 +146,7 @@ func (g *generator) clientInit(serv *descriptor.ServiceDescriptorProto, servName
 			p("LROClient *lroauto.OperationsClient")
 			p("")
 
-			g.imports[importSpec{name: "lroauto", path: "cloud.google.com/go/longrunning/autogen"}] = true
+			g.imports[pbinfo.ImportSpec{Name: "lroauto", Path: "cloud.google.com/go/longrunning/autogen"}] = true
 		}
 
 		p("// The call options for this service.")
@@ -164,8 +158,8 @@ func (g *generator) clientInit(serv *descriptor.ServiceDescriptorProto, servName
 		p("}")
 		p("")
 
-		g.imports[importSpec{path: "google.golang.org/grpc"}] = true
-		g.imports[importSpec{path: "google.golang.org/grpc/metadata"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/grpc"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/grpc/metadata"}] = true
 	}
 
 	// Client constructor
@@ -185,7 +179,7 @@ func (g *generator) clientInit(serv *descriptor.ServiceDescriptorProto, servName
 		p("    conn:        conn,")
 		p("    CallOptions: default%sCallOptions(),", servName)
 		p("")
-		p("    %s: %s.New%sClient(conn),", grpcClientField(servName), imp.name, serv.GetName())
+		p("    %s: %s.New%sClient(conn),", grpcClientField(servName), imp.Name, serv.GetName())
 		p("  }")
 		p("  c.setGoogleClientInfo()")
 		p("")
@@ -207,8 +201,8 @@ func (g *generator) clientInit(serv *descriptor.ServiceDescriptorProto, servName
 		p("}")
 		p("")
 
-		g.imports[importSpec{path: "google.golang.org/api/transport"}] = true
-		g.imports[importSpec{path: "golang.org/x/net/context"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/transport"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "golang.org/x/net/context"}] = true
 	}
 
 	// Connection()
@@ -242,7 +236,7 @@ func (g *generator) clientInit(serv *descriptor.ServiceDescriptorProto, servName
 		p("}")
 		p("")
 
-		g.imports[importSpec{path: "cloud.google.com/go/internal/version"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "cloud.google.com/go/internal/version"}] = true
 	}
 	return nil
 }
diff --git a/gengapic/doc_file.go b/gengapic/doc_file.go
index f38c451..3d6c28c 100644
--- a/gengapic/doc_file.go
+++ b/gengapic/doc_file.go
@@ -19,6 +19,7 @@ import (
 
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/googleapis/gapic-generator-go/internal/license"
 	"google.golang.org/genproto/googleapis/api/annotations"
 )
 
@@ -29,7 +30,7 @@ import (
 func (g *generator) genDocFile(pkgPath, pkgName string, year int, scopes []string) {
 	p := g.printf
 
-	p(apacheLicense, year)
+	p(license.Apache, year)
 	p("")
 
 	p("// Package %s is an auto-generated package for the", pkgName)
diff --git a/gengapic/example.go b/gengapic/example.go
index 25b1e05..2400ff4 100644
--- a/gengapic/example.go
+++ b/gengapic/example.go
@@ -17,6 +17,7 @@ package gengapic
 import (
 	"github.com/golang/protobuf/protoc-gen-go/descriptor"
 	"github.com/googleapis/gapic-generator-go/internal/errors"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
 )
 
 func (g *generator) genExampleFile(serv *descriptor.ServiceDescriptorProto, pkgName string) error {
@@ -29,7 +30,7 @@ func (g *generator) genExampleFile(serv *descriptor.ServiceDescriptorProto, pkgN
 	p("  _ = c")
 	p("}")
 	p("")
-	g.imports[importSpec{path: "golang.org/x/net/context"}] = true
+	g.imports[pbinfo.ImportSpec{Path: "golang.org/x/net/context"}] = true
 
 	for _, m := range serv.Method {
 		if err := g.exampleMethod(pkgName, servName, m); err != nil {
@@ -52,12 +53,12 @@ func (g *generator) exampleInitClient(pkgName, servName string) {
 func (g *generator) exampleMethod(pkgName, servName string, m *descriptor.MethodDescriptorProto) error {
 	p := g.printf
 
-	inType := g.types[m.GetInputType()]
+	inType := g.descInfo.Type[m.GetInputType()]
 	if inType == nil {
 		return errors.E(nil, "cannot find type %q, malformed descriptor?", m.GetInputType())
 	}
 
-	inSpec, err := g.importSpec(inType)
+	inSpec, err := g.descInfo.ImportSpec(inType)
 	if err != nil {
 		return err
 	}
@@ -69,7 +70,7 @@ func (g *generator) exampleMethod(pkgName, servName string, m *descriptor.Method
 
 	if !m.GetClientStreaming() && !m.GetServerStreaming() {
 		p("")
-		p("req := &%s.%s{", inSpec.name, *inType.Name)
+		p("req := &%s.%s{", inSpec.Name, *inType.Name)
 		p("  // TODO: Fill request struct fields.")
 		p("}")
 	}
@@ -146,10 +147,10 @@ func (g *generator) examplePagingCall(m *descriptor.MethodDescriptorProto) {
 	p("  _ = resp")
 	p("}")
 
-	g.imports[importSpec{path: "google.golang.org/api/iterator"}] = true
+	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/iterator"}] = true
 }
 
-func (g *generator) exampleBidiCall(m *descriptor.MethodDescriptorProto, inType *descriptor.DescriptorProto, inSpec importSpec) {
+func (g *generator) exampleBidiCall(m *descriptor.MethodDescriptorProto, inType *descriptor.DescriptorProto, inSpec pbinfo.ImportSpec) {
 	p := g.printf
 
 	p("stream, err := c.%s(ctx)", m.GetName())
@@ -158,7 +159,7 @@ func (g *generator) exampleBidiCall(m *descriptor.MethodDescriptorProto, inType
 	p("}")
 
 	p("go func() {")
-	p("  reqs := []*%s.%s{", inSpec.name, inType.GetName())
+	p("  reqs := []*%s.%s{", inSpec.Name, inType.GetName())
 	p("    // TODO: Create requests.")
 	p("  }")
 	p("  for _, req := range reqs {")
@@ -181,5 +182,5 @@ func (g *generator) exampleBidiCall(m *descriptor.MethodDescriptorProto, inType
 	p("  _ = resp")
 	p("}")
 
-	g.imports[importSpec{path: "io"}] = true
+	g.imports[pbinfo.ImportSpec{Path: "io"}] = true
 }
diff --git a/gengapic/gengapic.go b/gengapic/gengapic.go
index cff193a..d70d97f 100644
--- a/gengapic/gengapic.go
+++ b/gengapic/gengapic.go
@@ -27,6 +27,9 @@ import (
 	"github.com/golang/protobuf/protoc-gen-go/descriptor"
 	plugin "github.com/golang/protobuf/protoc-gen-go/plugin"
 	"github.com/googleapis/gapic-generator-go/internal/errors"
+	"github.com/googleapis/gapic-generator-go/internal/license"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
+	"github.com/googleapis/gapic-generator-go/internal/printer"
 	"google.golang.org/genproto/googleapis/api/annotations"
 )
 
@@ -36,9 +39,6 @@ const (
 	lroType   = ".google.longrunning.Operation"
 )
 
-var tabsCache = strings.Repeat("\t", 20)
-var spacesCache = strings.Repeat(" ", 100)
-
 func Gen(genReq *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, error) {
 	var pkgPath, pkgName string
 	if genReq.Parameter == nil {
@@ -94,7 +94,7 @@ func Gen(genReq *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, er
 		if err := g.genExampleFile(s, pkgName); err != nil {
 			return nil, errors.E(err, "example: %s", s.GetName())
 		}
-		g.imports[importSpec{path: pkgPath}] = true
+		g.imports[pbinfo.ImportSpec{Path: pkgPath}] = true
 		g.commit(outFile+"_client_example_test.go", pkgName+"_test")
 	}
 
@@ -106,7 +106,7 @@ func Gen(genReq *plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorResponse, er
 	g.genDocFile(pkgPath, pkgName, time.Now().Year(), scopes)
 	g.resp.File = append(g.resp.File, &plugin.CodeGeneratorResponse_File{
 		Name:    proto.String(filepath.Join(outDir, "doc.go")),
-		Content: proto.String(g.sb.String()),
+		Content: proto.String(g.pt.String()),
 	})
 
 	return &g.resp, nil
@@ -122,52 +122,28 @@ func strContains(a []string, s string) bool {
 }
 
 type generator struct {
-	sb strings.Builder
-
-	// current indentation level
-	in int
-
-	resp plugin.CodeGeneratorResponse
-
-	// Maps services and messages to the file containing them,
-	// so we can figure out the import.
-	parentFile map[proto.Message]*descriptor.FileDescriptorProto
+	pt printer.P
 
-	// Maps type names to their messages
-	types map[string]*descriptor.DescriptorProto
+	descInfo pbinfo.Info
 
 	// Maps proto elements to their comments
 	comments map[proto.Message]string
 
-	imports map[importSpec]bool
+	resp plugin.CodeGeneratorResponse
+
+	imports map[pbinfo.ImportSpec]bool
 
 	// Human-readable name of the API used in docs
 	apiName string
 }
 
 func (g *generator) init(files []*descriptor.FileDescriptorProto) {
-	g.parentFile = map[proto.Message]*descriptor.FileDescriptorProto{}
-	g.types = map[string]*descriptor.DescriptorProto{}
+	g.descInfo = pbinfo.Of(files)
+
 	g.comments = map[proto.Message]string{}
-	g.imports = map[importSpec]bool{}
+	g.imports = map[pbinfo.ImportSpec]bool{}
 
 	for _, f := range files {
-		// parentFile
-		for _, m := range f.MessageType {
-			g.parentFile[m] = f
-		}
-		for _, s := range f.Service {
-			g.parentFile[s] = f
-		}
-
-		// types
-		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())
-			g.types[fullyQualifiedName] = m
-		}
-
-		// comment
 		for _, loc := range f.GetSourceCodeInfo().GetLocation() {
 			// p is an array with format [f1, i1, f2, i2, ...]
 			// - f1 refers to the protobuf field tag
@@ -188,38 +164,6 @@ func (g *generator) init(files []*descriptor.FileDescriptorProto) {
 	}
 }
 
-// importSpec reports the importSpec for package containing protobuf element e.
-func (g *generator) importSpec(e proto.Message) (importSpec, error) {
-	fdesc := g.parentFile[e]
-
-	pkg := fdesc.GetOptions().GetGoPackage()
-	if pkg == "" {
-		var eTxt interface{} = e
-		if et, ok := eTxt.(interface{ GetName() string }); ok {
-			eTxt = et.GetName()
-		}
-		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
-	}
-}
-
 // printf formatted-prints to sb, using the print syntax from fmt package.
 //
 // It automatically keeps track of indentation caused by curly-braces.
@@ -230,48 +174,26 @@ func (g *generator) importSpec(e proto.Message) (importSpec, error) {
 // Currently it's not terribly difficult to confuse the auto-indenter.
 // To fix-up, manipulate g.in or write to g.sb directly.
 func (g *generator) printf(s string, a ...interface{}) {
-	s = strings.TrimSpace(s)
-	if s == "" {
-		g.sb.WriteByte('\n')
-		return
-	}
-
-	for i := 0; i < len(s) && s[i] == '}'; i++ {
-		g.in--
-	}
-
-	in := g.in
-	for in > len(tabsCache) {
-		g.sb.WriteString(tabsCache)
-		in -= len(tabsCache)
-	}
-	g.sb.WriteString(tabsCache[:in])
-
-	fmt.Fprintf(&g.sb, s, a...)
-	g.sb.WriteByte('\n')
-
-	for i := len(s) - 1; i >= 0 && s[i] == '{'; i-- {
-		g.in++
-	}
+	g.pt.Printf(s, a...)
 }
 
 func (g *generator) commit(fileName, pkgName string) {
 	var header strings.Builder
-	fmt.Fprintf(&header, apacheLicense, time.Now().Year())
+	fmt.Fprintf(&header, license.Apache, time.Now().Year())
 	fmt.Fprintf(&header, "package %s\n\n", pkgName)
 
-	var imps []importSpec
+	var imps []pbinfo.ImportSpec
 	for imp := range g.imports {
 		imps = append(imps, imp)
 	}
 	impDiv := sortImports(imps)
 
-	writeImp := func(is importSpec) {
+	writeImp := func(is pbinfo.ImportSpec) {
 		s := "\t%[2]q\n"
-		if is.name != "" {
+		if is.Name != "" {
 			s = "\t%s %q\n"
 		}
-		fmt.Fprintf(&header, s, is.name, is.path)
+		fmt.Fprintf(&header, s, is.Name, is.Path)
 	}
 
 	header.WriteString("import (\n")
@@ -294,7 +216,7 @@ func (g *generator) commit(fileName, pkgName string) {
 	// Trim trailing newlines so we have only one.
 	// NOTE(pongad): This might be an overkill since we have gofmt,
 	// but the rest of the file already conforms to gofmt, so we might as well?
-	body := g.sb.String()
+	body := g.pt.String()
 	if !strings.HasSuffix(body, "\n") {
 		body += "\n"
 	}
@@ -311,8 +233,7 @@ func (g *generator) commit(fileName, pkgName string) {
 }
 
 func (g *generator) reset() {
-	g.sb.Reset()
-	g.in = 0
+	g.pt.Reset()
 	for k := range g.imports {
 		delete(g.imports, k)
 	}
@@ -406,14 +327,14 @@ func (g *generator) genMethod(servName string, serv *descriptor.ServiceDescripto
 }
 
 func (g *generator) unaryCall(servName string, m *descriptor.MethodDescriptorProto) error {
-	inType := g.types[*m.InputType]
-	outType := g.types[*m.OutputType]
+	inType := g.descInfo.Type[*m.InputType]
+	outType := g.descInfo.Type[*m.OutputType]
 
-	inSpec, err := g.importSpec(inType)
+	inSpec, err := g.descInfo.ImportSpec(inType)
 	if err != nil {
 		return err
 	}
-	outSpec, err := g.importSpec(outType)
+	outSpec, err := g.descInfo.ImportSpec(outType)
 	if err != nil {
 		return err
 	}
@@ -421,11 +342,11 @@ func (g *generator) unaryCall(servName string, m *descriptor.MethodDescriptorPro
 	p := g.printf
 
 	p("func (c *%sClient) %s(ctx context.Context, req *%s.%s, opts ...gax.CallOption) (*%s.%s, error) {",
-		servName, *m.Name, inSpec.name, *inType.Name, outSpec.name, *outType.Name)
+		servName, *m.Name, inSpec.Name, *inType.Name, outSpec.Name, *outType.Name)
 
 	g.insertMetadata()
 	g.appendCallOpts(m)
-	p("var resp *%s.%s", outSpec.name, *outType.Name)
+	p("var resp *%s.%s", outSpec.Name, *outType.Name)
 	p("err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {")
 	p("  var err error")
 	p("  resp, err = %s", grpcClientCall(servName, *m.Name))
@@ -446,9 +367,9 @@ func (g *generator) unaryCall(servName string, m *descriptor.MethodDescriptorPro
 }
 
 func (g *generator) emptyUnaryCall(servName string, m *descriptor.MethodDescriptorProto) error {
-	inType := g.types[*m.InputType]
+	inType := g.descInfo.Type[*m.InputType]
 
-	inSpec, err := g.importSpec(inType)
+	inSpec, err := g.descInfo.ImportSpec(inType)
 	if err != nil {
 		return err
 	}
@@ -456,7 +377,7 @@ func (g *generator) emptyUnaryCall(servName string, m *descriptor.MethodDescript
 	p := g.printf
 
 	p("func (c *%sClient) %s(ctx context.Context, req *%s.%s, opts ...gax.CallOption) error {",
-		servName, m.GetName(), inSpec.name, inType.GetName())
+		servName, m.GetName(), inSpec.Name, inType.GetName())
 
 	g.insertMetadata()
 	g.appendCallOpts(m)
@@ -513,13 +434,6 @@ func (g *generator) comment(s string) {
 	}
 }
 
-func spaces(n int) string {
-	if n > len(spacesCache) {
-		return strings.Repeat(" ", n)
-	}
-	return spacesCache[:n]
-}
-
 // reduceServName removes redundant components from the service name.
 // For example, FooServiceV2 -> Foo.
 // The returned name is used as part of longer names, like FooClient.
diff --git a/gengapic/imports.go b/gengapic/imports.go
index 791b05c..18afb8a 100644
--- a/gengapic/imports.go
+++ b/gengapic/imports.go
@@ -17,30 +17,28 @@ package gengapic
 import (
 	"sort"
 	"strings"
-)
 
-type importSpec struct {
-	name, path string
-}
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
+)
 
 // sortImports sorts the import specs,
 // and returns the index of the first non-standard import.
-func sortImports(a []importSpec) int {
+func sortImports(a []pbinfo.ImportSpec) int {
 	sort.Slice(a, func(i, j int) bool {
-		iDot := strings.IndexByte(a[i].path, '.') >= 0
-		jDot := strings.IndexByte(a[j].path, '.') >= 0
+		iDot := strings.IndexByte(a[i].Path, '.') >= 0
+		jDot := strings.IndexByte(a[j].Path, '.') >= 0
 
 		// standard import (without dots) comes first
 		if iDot != jDot {
 			return jDot
 		}
 
-		if a[i].path != a[j].path {
-			return a[i].path < a[j].path
+		if a[i].Path != a[j].Path {
+			return a[i].Path < a[j].Path
 		}
-		return a[i].name < a[j].name
+		return a[i].Name < a[j].Name
 	})
 	return sort.Search(len(a), func(i int) bool {
-		return strings.IndexByte(a[i].path, '.') >= 0
+		return strings.IndexByte(a[i].Path, '.') >= 0
 	})
 }
diff --git a/gengapic/lro.go b/gengapic/lro.go
index ee471bd..5e1769f 100644
--- a/gengapic/lro.go
+++ b/gengapic/lro.go
@@ -14,18 +14,21 @@
 
 package gengapic
 
-import "github.com/golang/protobuf/protoc-gen-go/descriptor"
+import (
+	"github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
+)
 
 func (g *generator) lroCall(servName string, m *descriptor.MethodDescriptorProto) error {
-	inType := g.types[m.GetInputType()]
-	outType := g.types[m.GetOutputType()]
+	inType := g.descInfo.Type[m.GetInputType()]
+	outType := g.descInfo.Type[m.GetOutputType()]
 
-	inSpec, err := g.importSpec(inType)
+	inSpec, err := g.descInfo.ImportSpec(inType)
 	if err != nil {
 		return err
 	}
 
-	outSpec, err := g.importSpec(outType)
+	outSpec, err := g.descInfo.ImportSpec(outType)
 	if err != nil {
 		return err
 	}
@@ -34,11 +37,11 @@ func (g *generator) lroCall(servName string, m *descriptor.MethodDescriptorProto
 	p := g.printf
 
 	p("func (c *%sClient) %s(ctx context.Context, req *%s.%s, opts ...gax.CallOption) (*%s, error) {",
-		servName, *m.Name, inSpec.name, *inType.Name, lroType)
+		servName, *m.Name, inSpec.Name, *inType.Name, lroType)
 
 	g.insertMetadata()
 	g.appendCallOpts(m)
-	p("  var resp *%s.%s", outSpec.name, *outType.Name)
+	p("  var resp *%s.%s", outSpec.Name, *outType.Name)
 	p("  err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {")
 	p("    var err error")
 	p("    resp, err = %s", grpcClientCall(servName, *m.Name))
@@ -54,7 +57,7 @@ func (g *generator) lroCall(servName string, m *descriptor.MethodDescriptorProto
 	p("}")
 	p("")
 
-	g.imports[importSpec{path: "cloud.google.com/go/longrunning"}] = true
+	g.imports[pbinfo.ImportSpec{Path: "cloud.google.com/go/longrunning"}] = true
 	g.imports[inSpec] = true
 	g.imports[outSpec] = true
 	return nil
@@ -89,7 +92,7 @@ func (g *generator) lroType(servName string, m *descriptor.MethodDescriptorProto
 		p("}")
 		p("")
 
-		g.imports[importSpec{name: "longrunningpb", path: "google.golang.org/genproto/googleapis/longrunning"}] = true
+		g.imports[pbinfo.ImportSpec{Name: "longrunningpb", Path: "google.golang.org/genproto/googleapis/longrunning"}] = true
 	}
 
 	// Wait
@@ -106,7 +109,7 @@ func (g *generator) lroType(servName string, m *descriptor.MethodDescriptorProto
 		p("}")
 		p("")
 
-		g.imports[importSpec{path: "time"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "time"}] = true
 	}
 
 	// Poll
diff --git a/gengapic/paging.go b/gengapic/paging.go
index cc070c7..f3e9e1a 100644
--- a/gengapic/paging.go
+++ b/gengapic/paging.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/golang/protobuf/protoc-gen-go/descriptor"
 	"github.com/googleapis/gapic-generator-go/internal/errors"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
 )
 
 var primitiveFieldToGoType = [...]string{
@@ -44,9 +45,9 @@ var primitiveFieldToGoType = [...]string{
 type iterType struct {
 	iterTypeName, elemTypeName string
 
-	// If the elem type is a message, elemImports contains importSpec for the type.
+	// If the elem type is a message, elemImports contains pbinfo.ImportSpec for the type.
 	// Otherwise, len(elemImports)==0.
-	elemImports []importSpec
+	elemImports []pbinfo.ImportSpec
 }
 
 // iterTypeOf deduces iterType from a field to be iterated over.
@@ -56,17 +57,17 @@ func (g *generator) iterTypeOf(elemField *descriptor.FieldDescriptorProto) (iter
 
 	switch t := *elemField.Type; {
 	case t == descriptor.FieldDescriptorProto_TYPE_MESSAGE:
-		eType := g.types[elemField.GetTypeName()]
+		eType := g.descInfo.Type[elemField.GetTypeName()]
 
-		imp, err := g.importSpec(eType)
+		imp, err := g.descInfo.ImportSpec(eType)
 		if err != nil {
 			return iterType{}, err
 		}
 
-		pt.elemTypeName = fmt.Sprintf("*%s.%s", imp.name, eType.GetName())
+		pt.elemTypeName = fmt.Sprintf("*%s.%s", imp.Name, eType.GetName())
 		pt.iterTypeName = *eType.Name + "Iterator"
 
-		pt.elemImports = []importSpec{imp}
+		pt.elemImports = []pbinfo.ImportSpec{imp}
 
 	case t == descriptor.FieldDescriptorProto_TYPE_ENUM:
 		log.Panic("iterating enum not supported yet")
@@ -96,11 +97,11 @@ func (g *generator) pagingField(m *descriptor.MethodDescriptorProto) (*descripto
 		elemFields                      []*descriptor.FieldDescriptorProto
 	)
 
-	inType := g.types[m.GetInputType()]
+	inType := g.descInfo.Type[m.GetInputType()]
 	if inType == nil {
 		return nil, errors.E(nil, "cannot find message type %q, malformed descriptor?", m.GetInputType())
 	}
-	outType := g.types[m.GetOutputType()]
+	outType := g.descInfo.Type[m.GetOutputType()]
 	if outType == nil {
 		return nil, errors.E(nil, "cannot find message type %q, malformed descriptor?", m.GetOutputType())
 	}
@@ -134,29 +135,29 @@ func (g *generator) pagingField(m *descriptor.MethodDescriptorProto) (*descripto
 }
 
 func (g *generator) pagingCall(servName string, m *descriptor.MethodDescriptorProto, elemField *descriptor.FieldDescriptorProto, pt iterType) error {
-	inType := g.types[*m.InputType]
-	outType := g.types[*m.OutputType]
+	inType := g.descInfo.Type[*m.InputType]
+	outType := g.descInfo.Type[*m.OutputType]
 
-	inSpec, err := g.importSpec(inType)
+	inSpec, err := g.descInfo.ImportSpec(inType)
 	if err != nil {
 		return err
 	}
-	outSpec, err := g.importSpec(outType)
+	outSpec, err := g.descInfo.ImportSpec(outType)
 	if err != nil {
 		return err
 	}
 
 	p := g.printf
 	p("func (c *%sClient) %s(ctx context.Context, req *%s.%s, opts ...gax.CallOption) *%s {",
-		servName, *m.Name, inSpec.name, *inType.Name, pt.iterTypeName)
+		servName, *m.Name, inSpec.Name, *inType.Name, pt.iterTypeName)
 
 	g.insertMetadata()
 	g.appendCallOpts(m)
 
 	p("it := &%s{}", pt.iterTypeName)
-	p("req = proto.Clone(req).(*%s.%s)", inSpec.name, *inType.Name)
+	p("req = proto.Clone(req).(*%s.%s)", inSpec.Name, *inType.Name)
 	p("it.InternalFetch = func(pageSize int, pageToken string) ([]%s, string, error) {", pt.elemTypeName)
-	p("  var resp *%s.%s", outSpec.name, *outType.Name)
+	p("  var resp *%s.%s", outSpec.Name, *outType.Name)
 	p("  req.PageToken = pageToken")
 	p("  if pageSize > math.MaxInt32 {")
 	p("    req.PageSize = math.MaxInt32")
@@ -190,9 +191,9 @@ func (g *generator) pagingCall(servName string, m *descriptor.MethodDescriptorPr
 	p("}")
 	p("")
 
-	g.imports[importSpec{path: "math"}] = true
-	g.imports[importSpec{path: "github.com/golang/protobuf/proto"}] = true
-	g.imports[importSpec{path: "google.golang.org/api/iterator"}] = true
+	g.imports[pbinfo.ImportSpec{Path: "math"}] = true
+	g.imports[pbinfo.ImportSpec{Path: "github.com/golang/protobuf/proto"}] = true
+	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/iterator"}] = true
 	g.imports[inSpec] = true
 	g.imports[outSpec] = true
 	for _, spec := range pt.elemImports {
diff --git a/gengapic/stream.go b/gengapic/stream.go
index 3a6c51b..5f9ed9f 100644
--- a/gengapic/stream.go
+++ b/gengapic/stream.go
@@ -19,17 +19,17 @@ import "github.com/golang/protobuf/protoc-gen-go/descriptor"
 func (g *generator) bidiCall(servName string, s *descriptor.ServiceDescriptorProto, m *descriptor.MethodDescriptorProto) error {
 	p := g.printf
 
-	servSpec, err := g.importSpec(s)
+	servSpec, err := g.descInfo.ImportSpec(s)
 	if err != nil {
 		return err
 	}
 	g.imports[servSpec] = true
 
 	p("func (c *%sClient) %s(ctx context.Context, opts ...gax.CallOption) (%s.%s_%sClient, error) {",
-		servName, m.GetName(), servSpec.name, s.GetName(), m.GetName())
+		servName, m.GetName(), servSpec.Name, s.GetName(), m.GetName())
 	g.insertMetadata()
 	g.appendCallOpts(m)
-	p("  var resp %s.%s_%sClient", servSpec.name, s.GetName(), m.GetName())
+	p("  var resp %s.%s_%sClient", servSpec.Name, s.GetName(), m.GetName())
 
 	p("  err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {")
 	p("    var err error")
diff --git a/gengapic/license.go b/internal/license/license.go
similarity index 94%
rename from gengapic/license.go
rename to internal/license/license.go
index 007a659..99fd8bc 100644
--- a/gengapic/license.go
+++ b/internal/license/license.go
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package gengapic
+package license
 
-const apacheLicense = `// Copyright %d Google LLC
+const Apache = `// Copyright %d Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/internal/printer/printer.go b/internal/printer/printer.go
index 42dbee2..ac682e4 100644
--- a/internal/printer/printer.go
+++ b/internal/printer/printer.go
@@ -18,6 +18,7 @@ package printer
 import (
 	"bytes"
 	"fmt"
+	"io"
 	"strings"
 )
 
@@ -65,7 +66,16 @@ func (p *P) Printf(s string, args ...interface{}) {
 	}
 }
 
+// Writer returns a writer that writes to p's underlying buffer without performing indentation.
+func (p *P) Writer() io.Writer {
+	return &p.buf
+}
+
 // 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()
 }
+
+func (p *P) String() string {
+	return p.buf.String()
+}


Test patch:
diff --git a/cmd/gen-go-sample/inittree_test.go b/cmd/gen-go-sample/inittree_test.go
new file mode 100644
index 0000000..cf18665
--- /dev/null
+++ b/cmd/gen-go-sample/inittree_test.go
@@ -0,0 +1,65 @@
+// 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"
+	"testing"
+)
+
+func TestTree(t *testing.T) {
+	specs := []string{
+		`a.b = 1`,
+		`a.c = "xyz"`,
+		`a.d = 2.718281828`,
+		`x = 123`,
+	}
+
+	var root initTree
+	for _, s := range specs {
+		if err := root.Parse(s); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	for _, tst := range []struct {
+		path []string
+		val  string
+	}{
+		{[]string{"a", "b"}, "1"},
+		{[]string{"a", "c"}, `"xyz"`},
+		{[]string{"a", "d"}, "2.718281828"},
+		{[]string{"x"}, "123"},
+	} {
+		node := &root
+		for _, p := range tst.path {
+			node = node.get(p)
+		}
+		if node.leafVal != tst.val {
+			t.Errorf("%s = %q, want %q", strings.Join(tst.path, "->"), node.leafVal, tst.val)
+		}
+	}
+
+	if t.Failed() {
+		t.SkipNow()
+	}
+
+	var buf strings.Builder
+	buf.WriteByte('\n')
+	if err := root.Print(&buf); err != nil {
+		t.Error(err)
+	}
+	t.Log(buf.String())
+}
diff --git a/cmd/gen-go-sample/main_test.go b/cmd/gen-go-sample/main_test.go
index 251c3fa..e67903b 100644
--- a/cmd/gen-go-sample/main_test.go
+++ b/cmd/gen-go-sample/main_test.go
@@ -84,7 +84,11 @@ func TestSample(t *testing.T) {
 	vs := SampleValueSet{
 		ID: "my_value_set",
 		Parameters: SampleParameter{
-			Defaults: []string{"foo=bar"},
+			Defaults: []string{
+				`a.x = 42`,
+				`a.y = 3.14159`,
+				`b = "foobar"`,
+			},
 		},
 	}
 	if err := g.genSample("MyService", "MyMethod", "awesome_region", vs); err != nil {
diff --git a/cmd/gen-go-sample/test.bash b/cmd/gen-go-sample/test.bash
index 3bf7221..9104ac5 100755
--- a/cmd/gen-go-sample/test.bash
+++ b/cmd/gen-go-sample/test.bash
@@ -25,6 +25,6 @@ if [ -z $COMMON_PROTO ]; then
 	exit 1
 fi
 
-./gen-go-sample \
+./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
index 4ce6501..0e1ca8b 100644
--- a/cmd/gen-go-sample/testdata/sample.want
+++ b/cmd/gen-go-sample/testdata/sample.want
@@ -4,9 +4,13 @@ func sampleMyMethod() {
 	ctx := context.Background()
 	c := mypackagepb.NewClient(ctx)
 
-	req := mypackagepb.InputType{
-		// foo=bar
-	}
+req := TYPE{
+	A: TYPE{
+		X: 42,
+		Y: 3.14159,
+},
+	B: "foobar",
+}
 	resp, err := c.MyMethod(ctx, req)
 	if err != nil {
 		// TODO: Handle error.
diff --git a/gengapic/client_init_test.go b/gengapic/client_init_test.go
index f0c58b5..be69986 100644
--- a/gengapic/client_init_test.go
+++ b/gengapic/client_init_test.go
@@ -23,6 +23,7 @@ import (
 	"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"
 	"google.golang.org/genproto/googleapis/api/annotations"
 )
 
@@ -47,7 +48,7 @@ func diff(t *testing.T, name, got, goldenFile string) {
 
 func TestClientOpt(t *testing.T) {
 	var g generator
-	g.imports = map[importSpec]bool{}
+	g.imports = map[pbinfo.ImportSpec]bool{}
 
 	serv := &descriptor.ServiceDescriptorProto{
 		Method: []*descriptor.MethodDescriptorProto{
@@ -78,14 +79,14 @@ func TestClientOpt(t *testing.T) {
 			t.Error(err)
 			continue
 		}
-		diff(t, tst.tstName, g.sb.String(), filepath.Join("testdata", tst.tstName+".want"))
+		diff(t, tst.tstName, g.pt.String(), filepath.Join("testdata", tst.tstName+".want"))
 	}
 }
 
 func TestClientInit(t *testing.T) {
 	var g generator
 	g.apiName = "Awesome Foo"
-	g.imports = map[importSpec]bool{}
+	g.imports = map[pbinfo.ImportSpec]bool{}
 
 	servPlain := &descriptor.ServiceDescriptorProto{
 		Name: proto.String("Foo"),
@@ -110,7 +111,7 @@ func TestClientInit(t *testing.T) {
 		{tstName: "empty_client_init", servName: "", serv: servPlain},
 		{tstName: "lro_client_init", servName: "Foo", serv: servLRO},
 	} {
-		g.parentFile = map[proto.Message]*descriptor.FileDescriptorProto{
+		g.descInfo.ParentFile = map[proto.Message]*descriptor.FileDescriptorProto{
 			tst.serv: &descriptor.FileDescriptorProto{
 				Options: &descriptor.FileOptions{
 					GoPackage: proto.String("mypackage"),
@@ -123,6 +124,6 @@ func TestClientInit(t *testing.T) {
 
 		g.reset()
 		g.clientInit(tst.serv, tst.servName)
-		diff(t, tst.tstName, g.sb.String(), filepath.Join("testdata", tst.tstName+".want"))
+		diff(t, tst.tstName, g.pt.String(), filepath.Join("testdata", tst.tstName+".want"))
 	}
 }
diff --git a/gengapic/doc_file_test.go b/gengapic/doc_file_test.go
index 1e0e87b..2103982 100644
--- a/gengapic/doc_file_test.go
+++ b/gengapic/doc_file_test.go
@@ -23,5 +23,5 @@ func TestDocFile(t *testing.T) {
 	var g generator
 	g.apiName = "Awesome Foo"
 	g.genDocFile("path/to/awesome", "awesome", 42, []string{"https://foo.bar.com/auth", "https://zip.zap.com/auth"})
-	diff(t, "doc_file", g.sb.String(), filepath.Join("testdata", "doc_file.want"))
+	diff(t, "doc_file", g.pt.String(), filepath.Join("testdata", "doc_file.want"))
 }
diff --git a/gengapic/example_test.go b/gengapic/example_test.go
index 31f2322..7d3d50b 100644
--- a/gengapic/example_test.go
+++ b/gengapic/example_test.go
@@ -20,11 +20,12 @@ import (
 
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
 )
 
 func TestExample(t *testing.T) {
 	var g generator
-	g.imports = map[importSpec]bool{}
+	g.imports = map[pbinfo.ImportSpec]bool{}
 
 	inputType := &descriptor.DescriptorProto{
 		Name: proto.String("InputType"),
@@ -81,8 +82,8 @@ func TestExample(t *testing.T) {
 	for _, typ := range []*descriptor.DescriptorProto{
 		inputType, outputType, pageInputType, pageOutputType,
 	} {
-		g.types[".my.pkg."+*typ.Name] = typ
-		g.parentFile[typ] = file
+		g.descInfo.Type[".my.pkg."+*typ.Name] = typ
+		g.descInfo.ParentFile[typ] = file
 	}
 
 	serv := &descriptor.ServiceDescriptorProto{
@@ -125,7 +126,7 @@ func TestExample(t *testing.T) {
 	} {
 		g.reset()
 		g.genExampleFile(serv, tst.pkgName)
-		diff(t, tst.tstName, g.sb.String(), filepath.Join("testdata", tst.tstName+".want"))
+		diff(t, tst.tstName, g.pt.String(), filepath.Join("testdata", tst.tstName+".want"))
 	}
 }
 
@@ -137,11 +138,11 @@ func commonTypes(g *generator) {
 		Name: proto.String("Operation"),
 	}
 
-	g.types = map[string]*descriptor.DescriptorProto{
+	g.descInfo.Type = map[string]*descriptor.DescriptorProto{
 		emptyType: empty,
 		lroType:   lro,
 	}
-	g.parentFile = map[proto.Message]*descriptor.FileDescriptorProto{
+	g.descInfo.ParentFile = map[proto.Message]*descriptor.FileDescriptorProto{
 		empty: &descriptor.FileDescriptorProto{
 			Options: &descriptor.FileOptions{
 				GoPackage: proto.String("github.com/golang/protobuf/ptypes/empty"),
diff --git a/gengapic/gengapic_test.go b/gengapic/gengapic_test.go
index 57c5955..22bdad2 100644
--- a/gengapic/gengapic_test.go
+++ b/gengapic/gengapic_test.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/googleapis/gapic-generator-go/internal/pbinfo"
 )
 
 func TestComment(t *testing.T) {
@@ -37,9 +38,9 @@ func TestComment(t *testing.T) {
 			want: "// abc\n// def\n",
 		},
 	} {
-		g.sb.Reset()
+		g.pt.Reset()
 		g.comment(tst.in)
-		if got := g.sb.String(); got != tst.want {
+		if got := g.pt.String(); got != tst.want {
 			t.Errorf("comment(%q) = %q, want %q", tst.in, got, tst.want)
 		}
 	}
@@ -66,9 +67,9 @@ func TestMethodDoc(t *testing.T) {
 		},
 	} {
 		g.comments[m] = tst.in
-		g.sb.Reset()
+		g.pt.Reset()
 		g.methodDoc(m)
-		if got := g.sb.String(); got != tst.want {
+		if got := g.pt.String(); got != tst.want {
 			t.Errorf("comment(%q) = %q, want %q", tst.in, got, tst.want)
 		}
 	}
@@ -168,16 +169,16 @@ func TestGenMethod(t *testing.T) {
 	serv := &descriptor.ServiceDescriptorProto{}
 
 	var g generator
-	g.imports = map[importSpec]bool{}
+	g.imports = map[pbinfo.ImportSpec]bool{}
 
 	commonTypes(&g)
 	for _, typ := range []*descriptor.DescriptorProto{
 		inputType, outputType, pageInputType, pageOutputType,
 	} {
-		g.types[".my.pkg."+*typ.Name] = typ
-		g.parentFile[typ] = file
+		g.descInfo.Type[".my.pkg."+*typ.Name] = typ
+		g.descInfo.ParentFile[typ] = file
 	}
-	g.parentFile[serv] = file
+	g.descInfo.ParentFile[serv] = file
 
 	meths := []*descriptor.MethodDescriptorProto{
 		{
@@ -216,7 +217,7 @@ func TestGenMethod(t *testing.T) {
 		if err := g.genMethod("Foo", serv, m, &aux); err != nil {
 			t.Error(err)
 		} else {
-			diff(t, m.GetName(), g.sb.String(), filepath.Join("testdata", "method_"+m.GetName()+".want"))
+			diff(t, m.GetName(), g.pt.String(), filepath.Join("testdata", "method_"+m.GetName()+".want"))
 		}
 	}
 }
diff --git a/gengapic/paging_test.go b/gengapic/paging_test.go
index f176f2c..62a2906 100644
--- a/gengapic/paging_test.go
+++ b/gengapic/paging_test.go
@@ -20,6 +20,7 @@ import (
 	"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"
 )
 
 func TestPagingField(t *testing.T) {
@@ -36,60 +37,59 @@ func TestPagingField(t *testing.T) {
 		Label: labelp(descriptor.FieldDescriptorProto_LABEL_REPEATED),
 	}
 
-	g := &generator{
-		types: map[string]*descriptor.DescriptorProto{
-			"Foo": &descriptor.DescriptorProto{
-				Name: proto.String("Foo"),
-			},
-			"PageIn": &descriptor.DescriptorProto{
-				Name: proto.String("PageIn"),
-				Field: []*descriptor.FieldDescriptorProto{
-					{
-						Name:  proto.String("page_size"),
-						Type:  typep(descriptor.FieldDescriptorProto_TYPE_INT32),
-						Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
-					},
-					{
-						Name:  proto.String("page_token"),
-						Type:  typep(descriptor.FieldDescriptorProto_TYPE_STRING),
-						Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
-					},
+	g := &generator{}
+	g.descInfo.Type = map[string]*descriptor.DescriptorProto{
+		"Foo": &descriptor.DescriptorProto{
+			Name: proto.String("Foo"),
+		},
+		"PageIn": &descriptor.DescriptorProto{
+			Name: proto.String("PageIn"),
+			Field: []*descriptor.FieldDescriptorProto{
+				{
+					Name:  proto.String("page_size"),
+					Type:  typep(descriptor.FieldDescriptorProto_TYPE_INT32),
+					Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
+				},
+				{
+					Name:  proto.String("page_token"),
+					Type:  typep(descriptor.FieldDescriptorProto_TYPE_STRING),
+					Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
 				},
 			},
-			"PageOut": &descriptor.DescriptorProto{
-				Name: proto.String("PageOut"),
-				Field: []*descriptor.FieldDescriptorProto{
-					{
-						Name:  proto.String("next_page_token"),
-						Type:  typep(descriptor.FieldDescriptorProto_TYPE_STRING),
-						Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
-					},
-					resField,
+		},
+		"PageOut": &descriptor.DescriptorProto{
+			Name: proto.String("PageOut"),
+			Field: []*descriptor.FieldDescriptorProto{
+				{
+					Name:  proto.String("next_page_token"),
+					Type:  typep(descriptor.FieldDescriptorProto_TYPE_STRING),
+					Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
 				},
+				resField,
 			},
-			"BadPageOut1": &descriptor.DescriptorProto{
-				Name: proto.String("BadPageOut1"),
-				Field: []*descriptor.FieldDescriptorProto{
-					{
-						Name:  proto.String("next_page_token"),
-						Type:  typep(descriptor.FieldDescriptorProto_TYPE_STRING),
-						Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
-					},
-					// No repeated field
+		},
+		"BadPageOut1": &descriptor.DescriptorProto{
+			Name: proto.String("BadPageOut1"),
+			Field: []*descriptor.FieldDescriptorProto{
+				{
+					Name:  proto.String("next_page_token"),
+					Type:  typep(descriptor.FieldDescriptorProto_TYPE_STRING),
+					Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
 				},
+				// No repeated field
 			},
-			"BadPageOut2": &descriptor.DescriptorProto{
-				Name: proto.String("BadPageOut2"),
-				Field: []*descriptor.FieldDescriptorProto{
-					{
-						Name:  proto.String("next_page_token"),
-						Type:  typep(descriptor.FieldDescriptorProto_TYPE_STRING),
-						Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
-					},
-					// Too many repeated field
-					resField,
-					resField,
+		},
+		"BadPageOut2": &descriptor.DescriptorProto{
+			Name: proto.String("BadPageOut2"),
+			Field: []*descriptor.FieldDescriptorProto{
+				{
+					Name:  proto.String("next_page_token"),
+					Type:  typep(descriptor.FieldDescriptorProto_TYPE_STRING),
+					Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
 				},
+				// Too many repeated field
+				resField,
+				resField,
 			},
 		},
 	}
@@ -151,13 +151,15 @@ func TestIterTypeOf(t *testing.T) {
 		Name: proto.String("Foo"),
 	}
 	g := &generator{
-		types: map[string]*descriptor.DescriptorProto{
-			*msgType.Name: msgType,
-		},
-		parentFile: map[proto.Message]*descriptor.FileDescriptorProto{
-			msgType: &descriptor.FileDescriptorProto{
-				Options: &descriptor.FileOptions{
-					GoPackage: proto.String("path/to/foo;foo"),
+		descInfo: pbinfo.Info{
+			Type: map[string]*descriptor.DescriptorProto{
+				*msgType.Name: msgType,
+			},
+			ParentFile: map[proto.Message]*descriptor.FileDescriptorProto{
+				msgType: &descriptor.FileDescriptorProto{
+					Options: &descriptor.FileOptions{
+						GoPackage: proto.String("path/to/foo;foo"),
+					},
 				},
 			},
 		},
@@ -193,14 +195,14 @@ func TestIterTypeOf(t *testing.T) {
 			want: iterType{
 				iterTypeName: "FooIterator",
 				elemTypeName: "*foopb.Foo",
-				elemImports:  []importSpec{{name: "foopb", path: "path/to/foo"}},
+				elemImports:  []pbinfo.ImportSpec{{Name: "foopb", Path: "path/to/foo"}},
 			},
 		},
 	} {
 		got, err := g.iterTypeOf(tst.field)
 		if err != nil {
 			t.Error(err)
-		} else if diff := cmp.Diff(tst.want, got, cmp.AllowUnexported(got, importSpec{})); diff != "" {
+		} else if diff := cmp.Diff(tst.want, got, cmp.AllowUnexported(got, pbinfo.ImportSpec{})); diff != "" {
 			t.Errorf("%d: (got=-, want=+):\n%s", i, diff)
 		}
 	}
diff --git a/test.sh b/test.sh
index 6216d9c..e424ad1 100755
--- a/test.sh
+++ b/test.sh
@@ -39,10 +39,10 @@ OUT=${OUT:-testdata/out}
 mkdir -p "$OUT"
 
 generate() {
-	protoc --gogapic_out "$OUT" -I "$COMMON_PROTO" -I "$GOOGLEAPIS" $*
+	protoc --go_gapic_out "$OUT" -I "$COMMON_PROTO" -I "$GOOGLEAPIS" $*
 }
 
-generate --gogapic_opt 'cloud.google.com/go/vision/apiv1;vision' $GOOGLEAPIS/google/cloud/vision/v1/*.proto
-generate --gogapic_opt 'cloud.google.com/go/speech/apiv1;speech' $GOOGLEAPIS/google/cloud/speech/v1/*.proto
-generate --gogapic_opt 'cloud.google.com/go/pubsub/apiv1;pubsub' $GOOGLEAPIS/google/pubsub/v1/*.proto
-generate --gogapic_opt 'cloud.google.com/go/logging/apiv2;logging' $GOOGLEAPIS/google/logging/v2/*.proto
+generate --go_gapic_opt 'cloud.google.com/go/vision/apiv1;vision' $GOOGLEAPIS/google/cloud/vision/v1/*.proto
+generate --go_gapic_opt 'cloud.google.com/go/speech/apiv1;speech' $GOOGLEAPIS/google/cloud/speech/v1/*.proto
+generate --go_gapic_opt 'cloud.google.com/go/pubsub/apiv1;pubsub' $GOOGLEAPIS/google/pubsub/v1/*.proto
+generate --go_gapic_opt 'cloud.google.com/go/logging/apiv2;logging' $GOOGLEAPIS/google/logging/v2/*.proto