internal/protobuf: add proto definition extraction

Change-Id: Ia356bcb951e30bcc8030b51cbc5d9949391e84dc
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/1980
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/go.mod b/go.mod
index eb5f5ea..9e941a7 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,10 @@
 require (
 	github.com/BurntSushi/toml v0.3.1 // indirect
 	github.com/cockroachdb/apd v1.1.0
+	github.com/emicklei/proto v1.6.11
 	github.com/ghodss/yaml v1.0.0
 	github.com/google/go-cmp v0.2.0
+	github.com/kr/pretty v0.1.0
 	github.com/lib/pq v1.0.0 // indirect
 	github.com/mitchellh/go-homedir v1.0.0
 	github.com/pkg/errors v0.8.0 // indirect
diff --git a/go.sum b/go.sum
index b88224a..20e8381 100644
--- a/go.sum
+++ b/go.sum
@@ -9,6 +9,8 @@
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emicklei/proto v1.6.11 h1:KZHE0+iwVLth2D/K8jat9rs70K6TFWyol8ihrOdrbM0=
+github.com/emicklei/proto v1.6.11/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -20,6 +22,11 @@
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/logrusorgru/aurora v0.0.0-20180419164547-d694e6f975a9/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
diff --git a/internal/protobuf/parse.go b/internal/protobuf/parse.go
new file mode 100644
index 0000000..8b8ede9
--- /dev/null
+++ b/internal/protobuf/parse.go
@@ -0,0 +1,620 @@
+// Copyright 2019 CUE Authors
+//
+// 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
+//
+//     http://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 protobuf
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"path"
+	"path/filepath"
+	"sort"
+	"strconv"
+	"strings"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/parser"
+	"cuelang.org/go/cue/token"
+	"github.com/emicklei/proto"
+	"golang.org/x/xerrors"
+)
+
+type sharedState struct {
+	paths []string
+}
+
+func (s *sharedState) parse(filename string, r io.Reader) (p *protoConverter, err error) {
+	// Determine files to convert.
+	if r == nil {
+		f, err := os.Open(filename)
+		if err != nil {
+			return nil, xerrors.Errorf("protobuf: %w", err)
+		}
+		defer f.Close()
+		r = f
+	}
+
+	parser := proto.NewParser(r)
+	if filename != "" {
+		parser.Filename(filename)
+	}
+	d, err := parser.Parse()
+	if err != nil {
+		return nil, xerrors.Errorf("protobuf: %w", err)
+	}
+
+	p = &protoConverter{
+		state:   s,
+		used:    map[string]bool{},
+		symbols: map[string]bool{},
+	}
+
+	defer func() {
+		switch x := recover().(type) {
+		case nil:
+		case protoError:
+			err = &ProtoError{
+				Filename: filename,
+				Path:     strings.Join(p.path, "."),
+				Err:      x.error,
+			}
+		default:
+			panic(x)
+		}
+	}()
+
+	p.file = &ast.File{Filename: filename}
+
+	p.addNames(d.Elements)
+
+	// Parse package definitions.
+	for _, e := range d.Elements {
+		switch x := e.(type) {
+		case *proto.Package:
+			p.protoPkg = x.Name
+		case *proto.Option:
+			if x.Name == "go_package" {
+				str, err := strconv.Unquote(x.Constant.SourceRepresentation())
+				if err != nil {
+					failf("unquoting package filed: %v", err)
+				}
+				split := strings.Split(str, ";")
+				p.goPkgPath = split[0]
+				switch len(split) {
+				case 1:
+					p.goPkg = path.Base(str)
+				case 2:
+					p.goPkg = split[1]
+				default:
+					failf("unexpected ';' in %q", str)
+				}
+				p.file.Name = ast.NewIdent(p.goPkg)
+				// name.AddComment(comment(x.Comment, true))
+				// name.AddComment(comment(x.InlineComment, false))
+			}
+		}
+	}
+
+	for _, e := range d.Elements {
+		switch x := e.(type) {
+		case *proto.Import:
+			p.doImport(x)
+		}
+	}
+
+	imports := &ast.ImportDecl{}
+	p.file.Decls = append(p.file.Decls, imports)
+
+	for _, e := range d.Elements {
+		p.topElement(e)
+	}
+
+	used := []string{}
+	for k := range p.used {
+		used = append(used, k)
+	}
+	sort.Strings(used)
+
+	for _, v := range used {
+		imports.Specs = append(imports.Specs, &ast.ImportSpec{
+			Path: &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(v)},
+		})
+	}
+
+	if len(imports.Specs) == 0 {
+		p.file.Decls = p.file.Decls[1:]
+	}
+
+	return p, nil
+}
+
+// A protoConverter converts a proto definition to CUE. Proto files map to
+// CUE files one to one.
+type protoConverter struct {
+	state *sharedState
+
+	proto3 bool
+
+	protoPkg  string
+	goPkg     string
+	goPkgPath string
+
+	// w bytes.Buffer
+	file   *ast.File
+	inBody bool
+
+	imports map[string]string
+	used    map[string]bool
+
+	path    []string
+	scope   []map[string]mapping // for symbols resolution within package.
+	symbols map[string]bool      // symbols provided by package
+}
+
+type mapping struct {
+	ref string
+	pkg *protoConverter
+}
+
+type pkgInfo struct {
+	importPath string // the import path
+	goPath     string // The Go import path
+	shortName  string // Used for the cue package path, default is base of goPath
+}
+
+func (p *protoConverter) addRef(from, to string) {
+	top := p.scope[len(p.scope)-1]
+	if _, ok := top[from]; ok {
+		failf("entity %q already defined", from)
+	}
+	top[from] = mapping{ref: to}
+}
+
+func (p *protoConverter) addNames(elems []proto.Visitee) {
+	p.scope = append(p.scope, map[string]mapping{})
+	for _, e := range elems {
+		var name string
+		switch x := e.(type) {
+		case *proto.Message:
+			if x.IsExtend {
+				continue
+			}
+			name = x.Name
+		case *proto.Enum:
+			name = x.Name
+		default:
+			continue
+		}
+		sym := strings.Join(append(p.path, name), ".")
+		p.symbols[sym] = true
+		p.addRef(name, strings.Join(append(p.path, name), "_"))
+	}
+}
+
+func (p *protoConverter) popNames() {
+	p.scope = p.scope[:len(p.scope)-1]
+}
+
+func (p *protoConverter) resolve(name string, options []*proto.Option) string {
+	if strings.HasPrefix(name, ".") {
+		return p.resolveTopScope(name[1:], options)
+	}
+	for i := len(p.scope) - 1; i > 0; i-- {
+		if m, ok := p.scope[i][name]; ok {
+			return m.ref
+		}
+	}
+	return p.resolveTopScope(name, options)
+}
+
+func (p *protoConverter) resolveTopScope(name string, options []*proto.Option) string {
+	for i := 0; i < len(name); i++ {
+		k := strings.IndexByte(name[i:], '.')
+		i += k
+		if k == -1 {
+			i = len(name)
+		}
+		if m, ok := p.scope[0][name[:i]]; ok {
+			if m.pkg != nil {
+				p.used[m.pkg.goPkgPath] = true
+			}
+			return m.ref + name[i:]
+		}
+	}
+	if s, ok := protoToCUE(name, options); ok {
+		return s
+	}
+	failf("name %q not found", name)
+	return ""
+}
+
+func (p *protoConverter) doImport(v *proto.Import) {
+	if v.Filename == "cuelang/cue.proto" {
+		return
+	}
+
+	filename := ""
+	for _, p := range p.state.paths {
+		name := filepath.Join(p, v.Filename)
+		_, err := os.Stat(name)
+		if err != nil {
+			continue
+		}
+		filename = name
+		break
+	}
+
+	if filename == "" {
+		p.mustBuiltinPackage(v.Filename)
+		return
+	}
+
+	imp, err := p.state.parse(filename, nil)
+	if err != nil {
+		fail(err)
+	}
+
+	prefix := ""
+	if imp.goPkgPath != p.goPkgPath {
+		prefix = imp.goPkg + "."
+	}
+
+	pkgNamespace := strings.Split(imp.protoPkg, ".")
+	curNamespace := strings.Split(p.protoPkg, ".")
+	for {
+		for k := range imp.symbols {
+			ref := k
+			if len(pkgNamespace) > 0 {
+				ref = strings.Join(append(pkgNamespace, k), ".")
+			}
+			if _, ok := p.scope[0][ref]; !ok {
+				pkg := imp
+				if imp.goPkgPath == p.goPkgPath {
+					pkg = nil
+				}
+				p.scope[0][ref] = mapping{prefix + k, pkg}
+			}
+		}
+		if len(pkgNamespace) == 0 {
+			break
+		}
+		if len(curNamespace) == 0 || pkgNamespace[0] != curNamespace[0] {
+			break
+		}
+		pkgNamespace = pkgNamespace[1:]
+		curNamespace = curNamespace[1:]
+	}
+}
+
+func (p *protoConverter) stringLit(s string) *ast.BasicLit {
+	return &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(s)}
+}
+
+func (p *protoConverter) ref() *ast.Ident {
+	return ast.NewIdent(strings.Join(p.path, "_"))
+}
+
+func (p *protoConverter) subref(name string) *ast.Ident {
+	return ast.NewIdent(strings.Join(append(p.path, name), "_"))
+}
+
+func (p *protoConverter) addTag(f *ast.Field, body string) {
+	tag := "@protobuf(" + body + ")"
+	f.Attrs = append(f.Attrs, &ast.Attribute{Text: tag})
+}
+
+func (p *protoConverter) topElement(v proto.Visitee) {
+	switch x := v.(type) {
+	case *proto.Syntax:
+		p.proto3 = x.Value == "proto3"
+
+	case *proto.Comment:
+		if p.inBody {
+			p.file.Decls = append(p.file.Decls, comment(x, true))
+		} else {
+			addComments(p.file, 0, x, nil)
+		}
+
+	case *proto.Enum:
+		p.enum(x)
+
+	case *proto.Package:
+		if doc := x.Doc(); doc != nil {
+			addComments(p.file, 0, doc, nil)
+		}
+		// p.inBody bool
+
+	case *proto.Message:
+		p.message(x)
+
+	case *proto.Option:
+	case *proto.Import:
+		// already handled.
+
+	default:
+		failf("unsupported type %T", x)
+	}
+}
+
+func (p *protoConverter) message(v *proto.Message) {
+	defer func(saved []string) { p.path = saved }(p.path)
+	p.path = append(p.path, v.Name)
+
+	p.addNames(v.Elements)
+	defer p.popNames()
+
+	// TODO: handle IsExtend/ proto2
+
+	s := &ast.StructLit{
+		// TOOD: set proto file position.
+	}
+
+	ref := p.ref()
+	if v.Comment == nil {
+		ref.NamePos = newSection
+	}
+	f := &ast.Field{Label: ref, Value: s}
+	addComments(f, 1, v.Comment, nil)
+
+	// In CUE a message is always defined at the top level.
+	p.file.Decls = append(p.file.Decls, f)
+
+	for i, e := range v.Elements {
+		p.messageField(s, i, e)
+	}
+}
+
+func (p *protoConverter) messageField(s *ast.StructLit, i int, v proto.Visitee) {
+	switch x := v.(type) {
+	case *proto.Comment:
+		s.Elts = append(s.Elts, comment(x, true))
+
+	case *proto.NormalField:
+		f := p.parseField(s, i, x.Field)
+
+		if x.Repeated {
+			f.Value = &ast.ListLit{
+				Ellipsis: token.Pos(token.NoSpace),
+				Type:     f.Value,
+			}
+		}
+
+	case *proto.MapField:
+		f := &ast.Field{}
+
+		// All keys are converted to strings.
+		// TODO: support integer keys.
+		f.Label = &ast.TemplateLabel{Ident: ast.NewIdent("_")}
+		f.Value = ast.NewIdent(p.resolve(x.Type, x.Options))
+
+		name := labelName(x.Name)
+		f = &ast.Field{
+			Label: ast.NewIdent(name),
+			Value: &ast.StructLit{Elts: []ast.Decl{f}},
+		}
+		addComments(f, i, x.Comment, x.InlineComment)
+
+		o := optionParser{message: s, field: f}
+		o.tags = fmt.Sprintf("%d,type=map<%s,%s>", x.Sequence, x.KeyType, x.Type)
+		if x.Name != name {
+			o.tags += "," + x.Name
+		}
+		s.Elts = append(s.Elts, f)
+		o.parse(x.Options)
+		p.addTag(f, o.tags)
+
+	case *proto.Enum:
+		p.enum(x)
+
+	case *proto.Message:
+		p.message(x)
+
+	case *proto.Oneof:
+		p.oneOf(x)
+
+	default:
+		failf("unsupported type %T", v)
+	}
+}
+
+// enum converts a proto enum definition to CUE.
+//
+// An enum will generate two top-level definitions:
+//
+//    Enum:
+//      "Value1" |
+//      "Value2" |
+//      "Value3"
+//
+// and
+//
+//    Enum_value: {
+//        "Value1": 0
+//        "Value2": 1
+//    }
+//
+// Enums are always defined at the top level. The name of a nested enum
+// will be prefixed with the name of its parent and an underscore.
+func (p *protoConverter) enum(x *proto.Enum) {
+	if len(x.Elements) == 0 {
+		failf("empty enum")
+	}
+
+	name := p.subref(x.Name)
+
+	p.addNames(x.Elements)
+
+	if len(p.path) == 0 {
+		defer func() { p.path = p.path[:0] }()
+		p.path = append(p.path, x.Name)
+	}
+
+	// Top-level enum entry.
+	enum := &ast.Field{Label: name}
+	addComments(enum, 1, x.Comment, nil)
+
+	// Top-level enum values entry.
+	valueName := ast.NewIdent(name.Name + "_value")
+	valueName.NamePos = newSection
+	valueMap := &ast.StructLit{}
+	d := &ast.Field{Label: valueName, Value: valueMap}
+	// addComments(valueMap, 1, x.Comment, nil)
+
+	p.file.Decls = append(p.file.Decls, enum, d)
+
+	// The line comments for an enum field need to attach after the '|', which
+	// is only known at the next iteration.
+	var lastComment *proto.Comment
+	for i, v := range x.Elements {
+		switch y := v.(type) {
+		case *proto.EnumField:
+			// Add enum value to map
+			f := &ast.Field{
+				Label: p.stringLit(y.Name),
+				Value: &ast.BasicLit{Value: strconv.Itoa(y.Integer)},
+			}
+			valueMap.Elts = append(valueMap.Elts, f)
+
+			// add to enum disjunction
+			value := p.stringLit(y.Name)
+
+			var e ast.Expr = value
+			// Make the first value the default value.
+			if i == 0 {
+				e = &ast.UnaryExpr{OpPos: newline, Op: token.MUL, X: value}
+			} else {
+				value.ValuePos = newline
+			}
+			addComments(e, i, y.Comment, nil)
+			if enum.Value != nil {
+				e = &ast.BinaryExpr{X: enum.Value, Op: token.OR, Y: e}
+				if cg := comment(lastComment, false); cg != nil {
+					cg.Position = 2
+					e.AddComment(cg)
+				}
+			}
+			enum.Value = e
+
+			if y.Comment != nil {
+				lastComment = nil
+				addComments(f, 0, nil, y.InlineComment)
+			} else {
+				lastComment = y.InlineComment
+			}
+
+			// a := fmt.Sprintf("@protobuf(enum,name=%s)", y.Name)
+			// f.Attrs = append(f.Attrs, &ast.Attribute{Text: a})
+		}
+	}
+	addComments(enum.Value, 1, nil, lastComment)
+}
+
+func (p *protoConverter) oneOf(x *proto.Oneof) {
+	f := &ast.Field{
+		Label: p.ref(),
+	}
+	f.AddComment(comment(x.Comment, true))
+
+	p.file.Decls = append(p.file.Decls, f)
+
+	for _, v := range x.Elements {
+		s := &ast.StructLit{}
+		switch x := v.(type) {
+		case *proto.OneOfField:
+			f := p.parseField(s, 0, x.Field)
+			f.Optional = token.NoPos
+
+		default:
+			p.messageField(s, 1, v)
+		}
+		var e ast.Expr = s
+		if f.Value != nil {
+			e = &ast.BinaryExpr{X: f.Value, Op: token.OR, Y: s}
+		}
+		f.Value = e
+	}
+}
+
+func (p *protoConverter) parseField(s *ast.StructLit, i int, x *proto.Field) *ast.Field {
+	f := &ast.Field{}
+	addComments(f, i, x.Comment, x.InlineComment)
+
+	name := labelName(x.Name)
+	f.Label = ast.NewIdent(name)
+	typ := p.resolve(x.Type, x.Options)
+	f.Value = ast.NewIdent(typ)
+	s.Elts = append(s.Elts, f)
+
+	o := optionParser{message: s, field: f}
+
+	// body of @protobuf tag: sequence[,type][,name=<name>][,...]
+	o.tags += fmt.Sprint(x.Sequence)
+	if x.Type != typ {
+		o.tags += ",type=" + x.Type
+	}
+	if x.Name != name {
+		o.tags += ",name=" + x.Name
+	}
+	o.parse(x.Options)
+	p.addTag(f, o.tags)
+
+	if !o.required {
+		f.Optional = token.Pos(token.NoSpace)
+	}
+	return f
+}
+
+type optionParser struct {
+	message  *ast.StructLit
+	field    *ast.Field
+	required bool
+	tags     string
+}
+
+func (p *optionParser) parse(options []*proto.Option) {
+
+	// TODO: handle options
+	// - translate options to tags
+	// - interpret CUE options.
+	for _, o := range options {
+		switch o.Name {
+		case "(cue_opt).required":
+			p.required = true
+			// TODO: Dropping comments. Maybe add a dummy tag?
+
+		case "(cue.val)":
+			// TODO: set filename and base offset.
+			fset := token.NewFileSet()
+			expr, err := parser.ParseExpr(fset, "", o.Constant.Source)
+			if err != nil {
+				failf("invalid cue.val value: %v", err)
+			}
+			// Any further checks will be done at the end.
+			constraint := &ast.Field{Label: p.field.Label, Value: expr}
+			addComments(constraint, 1, o.Comment, o.InlineComment)
+			p.message.Elts = append(p.message.Elts, constraint)
+			if !p.required {
+				constraint.Optional = token.Pos(token.NoSpace)
+			}
+
+		default:
+			// TODO: dropping comments. Maybe add dummy tag?
+
+			// TODO: should CUE support nested attributes?
+			source := o.Constant.SourceRepresentation()
+			p.tags += "," + quote("option("+o.Name+","+source+")")
+		}
+	}
+}
diff --git a/internal/protobuf/protobuf.go b/internal/protobuf/protobuf.go
new file mode 100644
index 0000000..4280d9c
--- /dev/null
+++ b/internal/protobuf/protobuf.go
@@ -0,0 +1,88 @@
+// Copyright 2019 CUE Authors
+//
+// 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
+//
+//     http://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 protobuf defines functionality for parsing protocol buffer
+// definitions and instances.
+//
+// TODO: this package can become public once we have found a good nest for it.
+package protobuf
+
+import (
+	"fmt"
+	"io"
+
+	"cuelang.org/go/cue/ast"
+)
+
+// Config specifies the environment into which to parse a proto definition file.
+type Config struct {
+	Paths []string
+}
+
+// Parse parses a single proto file and returns its contents translated to
+// a CUE file. Imports are resolved using the path define in Config.
+// If body is not nil, it will use this as the contents of the file. Otherwise
+// Parse will open the given file name at the fully qualified path.
+//
+// The following field options are supported:
+//    (cue.val)     string        CUE constraint for this field. The string may
+//                                refer to other fields in a message definition.
+//    (cue.opt)     FieldOptions
+//       required   bool          Defines the field is required. Use with
+//                                caution.
+func Parse(filename string, body io.Reader, c *Config) (f *ast.File, err error) {
+	state := &sharedState{
+		paths: c.Paths,
+	}
+	p, err := state.parse(filename, body)
+	if err != nil {
+		return nil, err
+	}
+	return p.file, nil
+}
+
+// ProtoError describes the location and cause of an error.
+type ProtoError struct {
+	Filename string
+	Path     string
+	Err      error
+}
+
+func (p *ProtoError) Unwrap() error { return p.Err }
+
+func (p *ProtoError) Error() string {
+	if p.Path == "" {
+		return fmt.Sprintf("parse of file %q failed: %v", p.Filename, p.Err)
+	}
+	return fmt.Sprintf("parse of file %q failed at %s: %v", p.Filename, p.Path, p.Err)
+}
+
+// TODO
+// func GenDefinition
+
+// func MarshalText(cue.Value) (string, error) {
+// 	return "", nil
+// }
+
+// func MarshalBytes(cue.Value) ([]byte, error) {
+// 	return nil, nil
+// }
+
+// func UnmarshalText(descriptor cue.Value, b string) (ast.Expr, error) {
+// 	return nil, nil
+// }
+
+// func UnmarshalBytes(descriptor cue.Value, b []byte) (ast.Expr, error) {
+// 	return nil, nil
+// }
diff --git a/internal/protobuf/protobuf_test.go b/internal/protobuf/protobuf_test.go
new file mode 100644
index 0000000..d75126d
--- /dev/null
+++ b/internal/protobuf/protobuf_test.go
@@ -0,0 +1,68 @@
+// Copyright 2019 CUE Authors
+//
+// 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
+//
+//     http://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 protobuf
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"path/filepath"
+	"testing"
+
+	"cuelang.org/go/cue/format"
+	"github.com/kr/pretty"
+)
+
+var update *bool = flag.Bool("update", false, "update the test output")
+
+func TestParseDefinitions(t *testing.T) {
+	testCases := []string{
+		"networking/v1alpha3/gateway.proto",
+		"mixer/v1/attributes.proto",
+		"mixer/v1/config/client/client_config.proto",
+	}
+	for _, file := range testCases {
+		t.Run(file, func(t *testing.T) {
+			filename := filepath.Join("testdata", filepath.FromSlash(file))
+			c := &Config{
+				Paths: []string{"testdata"},
+			}
+
+			out := &bytes.Buffer{}
+
+			if f, err := Parse(filename, nil, c); err != nil {
+				fmt.Fprintln(out, err)
+			} else {
+				format.Node(out, f)
+			}
+
+			wantFile := filepath.Join("testdata", filepath.Base(file)+".out.cue")
+			if *update {
+				ioutil.WriteFile(wantFile, out.Bytes(), 0644)
+				return
+			}
+
+			b, err := ioutil.ReadFile(wantFile)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if desc := pretty.Diff(out.String(), string(b)); len(desc) > 0 {
+				t.Errorf("files differ:\n%v", desc)
+			}
+		})
+	}
+}
diff --git a/internal/protobuf/testdata/attributes.proto.out.cue b/internal/protobuf/testdata/attributes.proto.out.cue
new file mode 100644
index 0000000..73f70ad
--- /dev/null
+++ b/internal/protobuf/testdata/attributes.proto.out.cue
@@ -0,0 +1,136 @@
+
+//  Copyright 2016 Istio Authors
+// 
+//  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
+// 
+//      http://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 v1
+
+import "time"
+
+//  Attributes represents a set of typed name/value pairs. Many of Mixer's
+//  API either consume and/or return attributes.
+// 
+//  Istio uses attributes to control the runtime behavior of services running in the service mesh.
+//  Attributes are named and typed pieces of metadata describing ingress and egress traffic and the
+//  environment this traffic occurs in. An Istio attribute carries a specific piece
+//  of information such as the error code of an API request, the latency of an API request, or the
+//  original IP address of a TCP connection. For example:
+// 
+//  ```yaml
+//  request.path: xyz/abc
+//  request.size: 234
+//  request.time: 12:34:56.789 04/17/2017
+//  source.ip: 192.168.0.1
+//  target.service: example
+//  ```
+// 
+//  A given Istio deployment has a fixed vocabulary of attributes that it understands.
+//  The specific vocabulary is determined by the set of attribute producers being used
+//  in the deployment. The primary attribute producer in Istio is Envoy, although
+//  specialized Mixer adapters and services can also generate attributes.
+// 
+//  The common baseline set of attributes available in most Istio deployments is defined
+//  [here](https://istio.io/docs/reference/config/policy-and-telemetry/attribute-vocabulary/).
+// 
+//  Attributes are strongly typed. The supported attribute types are defined by
+//  [ValueType](https://github.com/istio/api/blob/master/policy/v1beta1/value_type.proto).
+//  Each type of value is encoded into one of the so-called transport types present
+//  in this message.
+// 
+//  Defines a map of attributes in uncompressed format.
+//  Following places may use this message:
+//  1) Configure Istio/Proxy with static per-proxy attributes, such as source.uid.
+//  2) Service IDL definition to extract api attributes for active requests.
+//  3) Forward attributes from client proxy to server proxy for HTTP requests.
+Attributes: {
+	//  A map of attribute name to its value.
+	attributes <_>: Attributes_AttributeValue
+}
+
+//  Specifies one attribute value with different type.
+Attributes_AttributeValue: {
+}
+//  The attribute value.
+Attributes_AttributeValue: {
+	//  Used for values of type STRING, DNS_NAME, EMAIL_ADDRESS, and URI
+	stringValue: string @protobuf(2,name=string_value)
+} | {
+	//  Used for values of type INT64
+	int64Value: int64 @protobuf(3,name=int64_value)
+} | {
+	//  Used for values of type DOUBLE
+	doubleValue: float64 @protobuf(4,type=double,name=double_value)
+} | {
+	//  Used for values of type BOOL
+	boolValue: bool @protobuf(5,name=bool_value)
+} | {
+	//  Used for values of type BYTES
+	bytesValue: bytes @protobuf(6,name=bytes_value)
+} | {
+	//  Used for values of type TIMESTAMP
+	timestampValue: time.Time @protobuf(7,type=google.protobuf.Timestamp,name=timestamp_value)
+} | {
+	//  Used for values of type DURATION
+	durationValue: time.Duration @protobuf(8,type=google.protobuf.Duration,name=duration_value)
+} | {
+	//  Used for values of type STRING_MAP
+	stringMapValue: Attributes_StringMap @protobuf(9,type=StringMap,name=string_map_value)
+}
+
+//  Defines a string map.
+Attributes_StringMap: {
+	//  Holds a set of name/value pairs.
+	entries <_>: string
+}
+
+//  Defines a list of attributes in compressed format optimized for transport.
+//  Within this message, strings are referenced using integer indices into
+//  one of two string dictionaries. Positive integers index into the global
+//  deployment-wide dictionary, whereas negative integers index into the message-level
+//  dictionary instead. The message-level dictionary is carried by the
+//  `words` field of this message, the deployment-wide dictionary is determined via
+//  configuration.
+CompressedAttributes: {
+	//  The message-level dictionary.
+	words?: [...string] @protobuf(1)
+
+	//  Holds attributes of type STRING, DNS_NAME, EMAIL_ADDRESS, URI
+	strings <_>: int32
+
+	//  Holds attributes of type INT64
+	int64s <_>: int64
+
+	//  Holds attributes of type DOUBLE
+	doubles <_>: float64
+
+	//  Holds attributes of type BOOL
+	bools <_>: bool
+
+	//  Holds attributes of type TIMESTAMP
+	timestamps <_>: time.Time
+
+	//  Holds attributes of type DURATION
+	durations <_>: time.Duration
+
+	//  Holds attributes of type BYTES
+	bytes <_>: bytes
+
+	//  Holds attributes of type STRING_MAP
+	stringMaps <_>: StringMap
+}
+
+//  A map of string to string. The keys and values in this map are dictionary
+//  indices (see the [Attributes][istio.mixer.v1.CompressedAttributes] message for an explanation)
+StringMap: {
+	//  Holds a set of name/value pairs.
+	entries <_>: int32
+}
diff --git a/internal/protobuf/testdata/client_config.proto.out.cue b/internal/protobuf/testdata/client_config.proto.out.cue
new file mode 100644
index 0000000..880309e
--- /dev/null
+++ b/internal/protobuf/testdata/client_config.proto.out.cue
@@ -0,0 +1,193 @@
+
+//  Copyright 2017 Istio Authors
+// 
+//    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
+// 
+//        http://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.
+
+//  $title: Mixer Client
+//  $description: Configuration state for the Mixer client library.
+//  $location: https://istio.io/docs/reference/config/policy-and-telemetry/istio.mixer.v1.config.client
+
+//  Describes the configuration state for the Mixer client library that's built into Envoy.
+package client
+
+import (
+	"istio.io/api/mixer/v1"
+	"time"
+)
+
+//  Specifies the behavior when the client is unable to connect to Mixer.
+NetworkFailPolicy: {
+
+	//  Specifies the behavior when the client is unable to connect to Mixer.
+	policy?: NetworkFailPolicy_FailPolicy @protobuf(1,type=FailPolicy)
+
+	//  Max retries on transport error.
+	maxRetry?: uint32 @protobuf(2,name=max_retry)
+
+	//  Base time to wait between retries.  Will be adjusted by exponential
+	//  backoff and jitter.
+	baseRetryWait?: time.Duration @protobuf(3,type=google.protobuf.Duration,name=base_retry_wait)
+
+	//  Max time to wait between retries.
+	maxRetryWait?: time.Duration @protobuf(4,type=google.protobuf.Duration,name=max_retry_wait)
+}
+
+//  Describes the policy.
+NetworkFailPolicy_FailPolicy:
+	//  If network connection fails, request is allowed and delivered to the
+	//  service.
+	*"FAIL_OPEN" |
+
+	//  If network connection fails, request is rejected.
+	"FAIL_CLOSE"
+
+NetworkFailPolicy_FailPolicy_value: {
+	"FAIL_OPEN":  0
+	"FAIL_CLOSE": 1
+}
+
+//  Defines the per-service client configuration.
+ServiceConfig: {
+	//  If true, do not call Mixer Check.
+	disableCheckCalls?: bool @protobuf(1,name=disable_check_calls)
+
+	//  If true, do not call Mixer Report.
+	disableReportCalls?: bool @protobuf(2,name=disable_report_calls)
+
+	//  Send these attributes to Mixer in both Check and Report. This
+	//  typically includes the "destination.service" attribute.
+	//  In case of a per-route override, per-route attributes take precedence
+	//  over the attributes supplied in the client configuration.
+	mixerAttributes?: v1.Attributes @protobuf(3,type=Attributes,name=mixer_attributes)
+
+	//  HTTP API specifications to generate API attributes.
+	httpApiSpec?: [...HTTPAPISpec] @protobuf(4,name=http_api_spec)
+
+	//  Quota specifications to generate quota requirements.
+	quotaSpec?: [...QuotaSpec] @protobuf(5,name=quota_spec)
+
+	//  Specifies the behavior when the client is unable to connect to Mixer.
+	//  This is the service-level policy. It overrides
+	//  [mesh-level
+	//  policy][istio.mixer.v1.config.client.TransportConfig.network_fail_policy].
+	networkFailPolicy?: NetworkFailPolicy @protobuf(7,name=network_fail_policy)
+
+	//  Default attributes to forward to upstream. This typically
+	//  includes the "source.ip" and "source.uid" attributes.
+	//  In case of a per-route override, per-route attributes take precedence
+	//  over the attributes supplied in the client configuration.
+	// 
+	//  Forwarded attributes take precedence over the static Mixer attributes.
+	//  The full order of application is as follows:
+	//  1. static Mixer attributes from the filter config;
+	//  2. static Mixer attributes from the route config;
+	//  3. forwarded attributes from the source filter config (if any);
+	//  4. forwarded attributes from the source route config (if any);
+	//  5. derived attributes from the request metadata.
+	forwardAttributes?: v1.Attributes @protobuf(8,type=Attributes,name=forward_attributes)
+}
+
+//  Defines the transport config on how to call Mixer.
+TransportConfig: {
+	//  The flag to disable check cache.
+	disableCheckCache?: bool @protobuf(1,name=disable_check_cache)
+
+	//  The flag to disable quota cache.
+	disableQuotaCache?: bool @protobuf(2,name=disable_quota_cache)
+
+	//  The flag to disable report batch.
+	disableReportBatch?: bool @protobuf(3,name=disable_report_batch)
+
+	//  Specifies the behavior when the client is unable to connect to Mixer.
+	//  This is the mesh level policy. The default value for policy is FAIL_OPEN.
+	networkFailPolicy?: NetworkFailPolicy @protobuf(4,name=network_fail_policy)
+
+	//  Specify refresh interval to write Mixer client statistics to Envoy share
+	//  memory. If not specified, the interval is 10 seconds.
+	statsUpdateInterval?: time.Duration @protobuf(5,type=google.protobuf.Duration,name=stats_update_interval)
+
+	//  Name of the cluster that will forward check calls to a pool of mixer
+	//  servers. Defaults to "mixer_server". By using different names for
+	//  checkCluster and reportCluster, it is possible to have one set of
+	//  Mixer servers handle check calls, while another set of Mixer servers
+	//  handle report calls.
+	// 
+	//  NOTE: Any value other than the default "mixer_server" will require the
+	//  Istio Grafana dashboards to be reconfigured to use the new name.
+	checkCluster?: string @protobuf(6,name=check_cluster)
+
+	//  Name of the cluster that will forward report calls to a pool of mixer
+	//  servers. Defaults to "mixer_server". By using different names for
+	//  checkCluster and reportCluster, it is possible to have one set of
+	//  Mixer servers handle check calls, while another set of Mixer servers
+	//  handle report calls.
+	// 
+	//  NOTE: Any value other than the default "mixer_server" will require the
+	//  Istio Grafana dashboards to be reconfigured to use the new name.
+	reportCluster?: string @protobuf(7,name=report_cluster)
+
+	//  Default attributes to forward to Mixer upstream. This typically
+	//  includes the "source.ip" and "source.uid" attributes. These
+	//  attributes are consumed by the proxy in front of mixer.
+	attributesForMixerProxy?: v1.Attributes @protobuf(8,type=Attributes,name=attributes_for_mixer_proxy)
+}
+
+//  Defines the client config for HTTP.
+HttpClientConfig: {
+	//  The transport config.
+	transport?: TransportConfig @protobuf(1)
+
+	//  Map of control configuration indexed by destination.service. This
+	//  is used to support per-service configuration for cases where a
+	//  mixerclient serves multiple services.
+	serviceConfigs <_>: ServiceConfig
+
+	//  Default destination service name if none was specified in the
+	//  client request.
+	defaultDestinationService?: string @protobuf(3,name=default_destination_service)
+
+	//  Default attributes to send to Mixer in both Check and
+	//  Report. This typically includes "destination.ip" and
+	//  "destination.uid" attributes.
+	mixerAttributes?: v1.Attributes @protobuf(4,type=Attributes,name=mixer_attributes)
+
+	//  Default attributes to forward to upstream. This typically
+	//  includes the "source.ip" and "source.uid" attributes.
+	forwardAttributes?: v1.Attributes @protobuf(5,type=Attributes,name=forward_attributes)
+}
+
+//  Defines the client config for TCP.
+TcpClientConfig: {
+	//  The transport config.
+	transport?: TransportConfig @protobuf(1)
+
+	//  Default attributes to send to Mixer in both Check and
+	//  Report. This typically includes "destination.ip" and
+	//  "destination.uid" attributes.
+	mixerAttributes?: v1.Attributes @protobuf(2,type=Attributes,name=mixer_attributes)
+
+	//  If set to true, disables Mixer check calls.
+	disableCheckCalls?: bool @protobuf(3,name=disable_check_calls)
+
+	//  If set to true, disables Mixer check calls.
+	disableReportCalls?: bool @protobuf(4,name=disable_report_calls)
+
+	//  Quota specifications to generate quota requirements.
+	//  It applies on the new TCP connections.
+	connectionQuotaSpec?: QuotaSpec @protobuf(5,name=connection_quota_spec)
+
+	//  Specify report interval to send periodical reports for long TCP
+	//  connections. If not specified, the interval is 10 seconds. This interval
+	//  should not be less than 1 second, otherwise it will be reset to 1 second.
+	reportInterval?: time.Duration @protobuf(6,type=google.protobuf.Duration,name=report_interval)
+}
diff --git a/internal/protobuf/testdata/cue.proto b/internal/protobuf/testdata/cue.proto
new file mode 100644
index 0000000..4bbb948
--- /dev/null
+++ b/internal/protobuf/testdata/cue.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package cue;
+
+import "google/protobuf/descriptor.proto";
+
+option go_package = "cuelang.org/cueproto";
+option java_package = "org.cuelang.cueproto";
+
+message FieldOptions {
+    bool required = 1;
+}
+
+extend google.protobuf.FieldOptions {
+    string val = 123456;
+    FieldOptions opt = 1069;
+}
+
diff --git a/internal/protobuf/testdata/cue.proto.out.cue b/internal/protobuf/testdata/cue.proto.out.cue
new file mode 100644
index 0000000..3a46082
--- /dev/null
+++ b/internal/protobuf/testdata/cue.proto.out.cue
@@ -0,0 +1,8 @@
+package proto
+
+FieldOptions required?: bool @protobuf(1)
+
+google.protobuf.FieldOptions: {
+	val?: string       @protobuf(123456)
+	opt?: FieldOptions @protobuf(1069)
+}
diff --git a/internal/protobuf/testdata/gateway.proto.out.cue b/internal/protobuf/testdata/gateway.proto.out.cue
new file mode 100644
index 0000000..14e4624
--- /dev/null
+++ b/internal/protobuf/testdata/gateway.proto.out.cue
@@ -0,0 +1,451 @@
+
+//  Copyright 2019 CUE Authors
+// 
+//  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
+//  
+//      http://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.
+
+//  Copyright 2017 Istio Authors
+// 
+//    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
+// 
+//        http://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.
+
+//  $title: Gateway
+//  $description: Configuration affecting edge load balancer.
+//  $location: https://istio.io/docs/reference/config/networking/v1alpha3/gateway.html
+
+//  `Gateway` describes a load balancer operating at the edge of the mesh
+//  receiving incoming or outgoing HTTP/TCP connections. The specification
+//  describes a set of ports that should be exposed, the type of protocol to
+//  use, SNI configuration for the load balancer, etc.
+// 
+//  For example, the following Gateway configuration sets up a proxy to act
+//  as a load balancer exposing port 80 and 9080 (http), 443 (https),
+//  9443(https) and port 2379 (TCP) for ingress.  The gateway will be
+//  applied to the proxy running on a pod with labels `app:
+//  my-gateway-controller`. While Istio will configure the proxy to listen
+//  on these ports, it is the responsibility of the user to ensure that
+//  external traffic to these ports are allowed into the mesh.
+// 
+//  ```yaml
+//  apiVersion: networking.istio.io/v1alpha3
+//  kind: Gateway
+//  metadata:
+//    name: my-gateway
+//    namespace: some-config-namespace
+//  spec:
+//    selector:
+//      app: my-gateway-controller
+//    servers:
+//    - port:
+//        number: 80
+//        name: http
+//        protocol: HTTP
+//      hosts:
+//      - uk.bookinfo.com
+//      - eu.bookinfo.com
+//      tls:
+//        httpsRedirect: true # sends 301 redirect for http requests
+//    - port:
+//        number: 443
+//        name: https-443
+//        protocol: HTTPS
+//      hosts:
+//      - uk.bookinfo.com
+//      - eu.bookinfo.com
+//      tls:
+//        mode: SIMPLE # enables HTTPS on this port
+//        serverCertificate: /etc/certs/servercert.pem
+//        privateKey: /etc/certs/privatekey.pem
+//    - port:
+//        number: 9443
+//        name: https-9443
+//        protocol: HTTPS
+//      hosts:
+//      - "bookinfo-namespace/*.bookinfo.com"
+//      tls:
+//        mode: SIMPLE # enables HTTPS on this port
+//        credentialName: bookinfo-secret # fetches certs from Kubernetes secret
+//    - port:
+//        number: 9080
+//        name: http-wildcard
+//        protocol: HTTP
+//      hosts:
+//      - "*"
+//    - port:
+//        number: 2379 # to expose internal service via external port 2379
+//        name: mongo
+//        protocol: MONGO
+//      hosts:
+//      - "*"
+//  ```
+// 
+//  The Gateway specification above describes the L4-L6 properties of a load
+//  balancer. A `VirtualService` can then be bound to a gateway to control
+//  the forwarding of traffic arriving at a particular host or gateway port.
+// 
+//  For example, the following VirtualService splits traffic for
+//  `https://uk.bookinfo.com/reviews`, `https://eu.bookinfo.com/reviews`,
+//  `http://uk.bookinfo.com:9080/reviews`,
+//  `http://eu.bookinfo.com:9080/reviews` into two versions (prod and qa) of
+//  an internal reviews service on port 9080. In addition, requests
+//  containing the cookie "user: dev-123" will be sent to special port 7777
+//  in the qa version. The same rule is also applicable inside the mesh for
+//  requests to the "reviews.prod.svc.cluster.local" service. This rule is
+//  applicable across ports 443, 9080. Note that `http://uk.bookinfo.com`
+//  gets redirected to `https://uk.bookinfo.com` (i.e. 80 redirects to 443).
+// 
+//  ```yaml
+//  apiVersion: networking.istio.io/v1alpha3
+//  kind: VirtualService
+//  metadata:
+//    name: bookinfo-rule
+//    namespace: bookinfo-namespace
+//  spec:
+//    hosts:
+//    - reviews.prod.svc.cluster.local
+//    - uk.bookinfo.com
+//    - eu.bookinfo.com
+//    gateways:
+//    - some-config-namespace/my-gateway
+//    - mesh # applies to all the sidecars in the mesh
+//    http:
+//    - match:
+//      - headers:
+//          cookie:
+//            exact: "user=dev-123"
+//      route:
+//      - destination:
+//          port:
+//            number: 7777
+//          host: reviews.qa.svc.cluster.local
+//    - match:
+//      - uri:
+//          prefix: /reviews/
+//      route:
+//      - destination:
+//          port:
+//            number: 9080 # can be omitted if it's the only port for reviews
+//          host: reviews.prod.svc.cluster.local
+//        weight: 80
+//      - destination:
+//          host: reviews.qa.svc.cluster.local
+//        weight: 20
+//  ```
+// 
+//  The following VirtualService forwards traffic arriving at (external)
+//  port 27017 to internal Mongo server on port 5555. This rule is not
+//  applicable internally in the mesh as the gateway list omits the
+//  reserved name `mesh`.
+// 
+//  ```yaml
+//  apiVersion: networking.istio.io/v1alpha3
+//  kind: VirtualService
+//  metadata:
+//    name: bookinfo-Mongo
+//    namespace: bookinfo-namespace
+//  spec:
+//    hosts:
+//    - mongosvr.prod.svc.cluster.local # name of internal Mongo service
+//    gateways:
+//    - some-config-namespace/my-gateway # can omit the namespace if gateway is in same
+//                                         namespace as virtual service.
+//    tcp:
+//    - match:
+//      - port: 27017
+//      route:
+//      - destination:
+//          host: mongo.prod.svc.cluster.local
+//          port:
+//            number: 5555
+//  ```
+// 
+//  It is possible to restrict the set of virtual services that can bind to
+//  a gateway server using the namespace/hostname syntax in the hosts field.
+//  For example, the following Gateway allows any virtual service in the ns1
+//  namespace to bind to it, while restricting only the virtual service with
+//  foo.bar.com host in the ns2 namespace to bind to it.
+// 
+//  ```yaml
+//  apiVersion: networking.istio.io/v1alpha3
+//  kind: Gateway
+//  metadata:
+//    name: my-gateway
+//    namespace: some-config-namespace
+//  spec:
+//    selector:
+//      app: my-gateway-controller
+//    servers:
+//    - port:
+//        number: 80
+//        name: http
+//        protocol: HTTP
+//      hosts:
+//      - "ns1/*"
+//      - "ns2/foo.bar.com"
+//  ```
+// 
+package v1alpha3
+
+Gateway: {
+	//  REQUIRED: A list of server specifications.
+	servers?: [...Server] @protobuf(1)
+
+	//  REQUIRED: One or more labels that indicate a specific set of pods/VMs
+	//  on which this gateway configuration should be applied. The scope of
+	//  label search is restricted to the configuration namespace in which the
+	//  the resource is present. In other words, the Gateway resource must
+	//  reside in the same namespace as the gateway workload instance.
+	selector <_>: string
+	selector?: {<name>: name}
+}
+
+//  `Server` describes the properties of the proxy on a given load balancer
+//  port. For example,
+// 
+//  ```yaml
+//  apiVersion: networking.istio.io/v1alpha3
+//  kind: Gateway
+//  metadata:
+//    name: my-ingress
+//  spec:
+//    selector:
+//      app: my-ingress-gateway
+//    servers:
+//    - port:
+//        number: 80
+//        name: http2
+//        protocol: HTTP2
+//      hosts:
+//      - "*"
+//  ```
+// 
+//  Another example
+// 
+//  ```yaml
+//  apiVersion: networking.istio.io/v1alpha3
+//  kind: Gateway
+//  metadata:
+//    name: my-tcp-ingress
+//  spec:
+//    selector:
+//      app: my-tcp-ingress-gateway
+//    servers:
+//    - port:
+//        number: 27018
+//        name: mongo
+//        protocol: MONGO
+//      hosts:
+//      - "*"
+//  ```
+// 
+//  The following is an example of TLS configuration for port 443
+// 
+//  ```yaml
+//  apiVersion: networking.istio.io/v1alpha3
+//  kind: Gateway
+//  metadata:
+//    name: my-tls-ingress
+//  spec:
+//    selector:
+//      app: my-tls-ingress-gateway
+//    servers:
+//    - port:
+//        number: 443
+//        name: https
+//        protocol: HTTPS
+//      hosts:
+//      - "*"
+//      tls:
+//        mode: SIMPLE
+//        serverCertificate: /etc/certs/server.pem
+//        privateKey: /etc/certs/privatekey.pem
+//  ```
+Server: {
+	//  REQUIRED: The Port on which the proxy should listen for incoming
+	//  connections.
+	port?: Port @protobuf(1)
+	port?: >10 & <100
+
+	//  $hide_from_docs
+	//  The ip or the Unix domain socket to which the listener should be bound
+	//  to. Format: `x.x.x.x` or `unix:///path/to/uds` or `unix://@foobar`
+	//  (Linux abstract namespace). When using Unix domain sockets, the port
+	//  number should be 0.
+	bind?: string @protobuf(4)
+
+	//  REQUIRED. One or more hosts exposed by this gateway.
+	//  While typically applicable to
+	//  HTTP services, it can also be used for TCP services using TLS with SNI.
+	//  A host is specified as a `dnsName` with an optional `namespace/` prefix.
+	//  The `dnsName` should be specified using FQDN format, optionally including
+	//  a wildcard character in the left-most component (e.g., `prod/*.example.com`).
+	//  Set the `dnsName` to `*` to select all `VirtualService` hosts from the
+	//  specified namespace (e.g.,`prod/*`). If no `namespace/` is specified,
+	//  the `VirtualService` hosts will be selected from any available namespace.
+	//  Any associated `DestinationRule` in the same namespace will also be used.
+	// 
+	//  A `VirtualService` must be bound to the gateway and must have one or
+	//  more hosts that match the hosts specified in a server. The match
+	//  could be an exact match or a suffix match with the server's hosts. For
+	//  example, if the server's hosts specifies `*.example.com`, a
+	//  `VirtualService` with hosts `dev.example.com` or `prod.example.com` will
+	//  match. However, a `VirtualService` with host `example.com` or
+	//  `newexample.com` will not match.
+	// 
+	//  NOTE: Only virtual services exported to the gateway's namespace
+	//  (e.g., `exportTo` value of `*`) can be referenced.
+	//  Private configurations (e.g., `exportTo` set to `.`) will not be
+	//  available. Refer to the `exportTo` setting in `VirtualService`,
+	//  `DestinationRule`, and `ServiceEntry` configurations for details.
+	hosts?: [...string] @protobuf(2)
+
+	//  Set of TLS related options that govern the server's behavior. Use
+	//  these options to control if all http requests should be redirected to
+	//  https, and the TLS modes to use.
+	tls?: Server_TLSOptions @protobuf(3,type=TLSOptions)
+
+	//  The loopback IP endpoint or Unix domain socket to which traffic should
+	//  be forwarded to by default. Format should be `127.0.0.1:PORT` or
+	//  `unix:///path/to/socket` or `unix://@foobar` (Linux abstract namespace).
+	defaultEndpoint?: string @protobuf(5,name=default_endpoint)
+}
+
+Server_TLSOptions: {
+	//  If set to true, the load balancer will send a 301 redirect for all
+	//  http connections, asking the clients to use HTTPS.
+	httpsRedirect?: bool @protobuf(1,name=https_redirect)
+
+	//  Optional: Indicates whether connections to this port should be
+	//  secured using TLS. The value of this field determines how TLS is
+	//  enforced.
+	mode?: Server_TLSOptions_TLSmode @protobuf(2,type=TLSmode)
+
+	//  Extra comment.
+
+	//  REQUIRED if mode is `SIMPLE` or `MUTUAL`. The path to the file
+	//  holding the server-side TLS certificate to use.
+	serverCertificate?: string @protobuf(3,name=server_certificate)
+
+	//  REQUIRED if mode is `SIMPLE` or `MUTUAL`. The path to the file
+	//  holding the server's private key.
+	privateKey?: string @protobuf(4,name=private_key)
+
+	//  REQUIRED if mode is `MUTUAL`. The path to a file containing
+	//  certificate authority certificates to use in verifying a presented
+	//  client side certificate.
+	caCertificates?: string @protobuf(5,name=ca_certificates)
+
+	//  The credentialName stands for a unique identifier that can be used
+	//  to identify the serverCertificate and the privateKey. The
+	//  credentialName appended with suffix "-cacert" is used to identify
+	//  the CaCertificates associated with this server. Gateway workloads
+	//  capable of fetching credentials from a remote credential store such
+	//  as Kubernetes secrets, will be configured to retrieve the
+	//  serverCertificate and the privateKey using credentialName, instead
+	//  of using the file system paths specified above. If using mutual TLS,
+	//  gateway workload instances will retrieve the CaCertificates using
+	//  credentialName-cacert. The semantics of the name are platform
+	//  dependent.  In Kubernetes, the default Istio supplied credential
+	//  server expects the credentialName to match the name of the
+	//  Kubernetes secret that holds the server certificate, the private
+	//  key, and the CA certificate (if using mutual TLS). Set the
+	//  `ISTIO_META_USER_SDS` metadata variable in the gateway's proxy to
+	//  enable the dynamic credential fetching feature.
+	credentialName?: string @protobuf(10,name=credential_name)
+
+	//  A list of alternate names to verify the subject identity in the
+	//  certificate presented by the client.
+	subjectAltNames?: [...string] @protobuf(6,name=subject_alt_names)
+
+	//  Optional: Minimum TLS protocol version.
+	minProtocolVersion?: Server_TLSOptions_TLSProtocol @protobuf(7,type=TLSProtocol,name=min_protocol_version)
+
+	//  Optional: Maximum TLS protocol version.
+	maxProtocolVersion?: Server_TLSOptions_TLSProtocol @protobuf(8,type=TLSProtocol,name=max_protocol_version)
+
+	//  Optional: If specified, only support the specified cipher list.
+	//  Otherwise default to the default cipher list supported by Envoy.
+	cipherSuites?: [...string] @protobuf(9,name=cipher_suites)
+}
+
+//  TLS modes enforced by the proxy
+Server_TLSOptions_TLSmode:
+	//  The SNI string presented by the client will be used as the match
+	//  criterion in a VirtualService TLS route to determine the
+	//  destination service from the service registry.
+	*"PASSTHROUGH" |
+
+	//  Secure connections with standard TLS semantics.
+	"SIMPLE" |
+
+	//  Secure connections to the upstream using mutual TLS by presenting
+	//  client certificates for authentication.
+	"MUTUAL" |
+
+	//  Similar to the passthrough mode, except servers with this TLS mode
+	//  do not require an associated VirtualService to map from the SNI
+	//  value to service in the registry. The destination details such as
+	//  the service/subset/port are encoded in the SNI value. The proxy
+	//  will forward to the upstream (Envoy) cluster (a group of
+	//  endpoints) specified by the SNI value. This server is typically
+	//  used to provide connectivity between services in disparate L3
+	//  networks that otherwise do not have direct connectivity between
+	//  their respective endpoints. Use of this mode assumes that both the
+	//  source and the destination are using Istio mTLS to secure traffic.
+	"AUTO_PASSTHROUGH"
+
+Server_TLSOptions_TLSmode_value: {
+	"PASSTHROUGH":      0
+	"SIMPLE":           1
+	"MUTUAL":           2
+	"AUTO_PASSTHROUGH": 3
+}
+
+//  TLS protocol versions.
+Server_TLSOptions_TLSProtocol:
+	*"TLS_AUTO" | //  Automatically choose the optimal TLS version.
+	"TLSV1_0" | //  TLS version 1.0
+	"TLSV1_1" | //  TLS version 1.1
+	"TLSV1_2" | //  TLS version 1.2
+	"TLSV1_3" //  TLS version 1.3
+
+Server_TLSOptions_TLSProtocol_value: {
+	"TLS_AUTO": 0
+	"TLSV1_0":  1
+	"TLSV1_1":  2
+	"TLSV1_2":  3
+	"TLSV1_3":  4
+}
+
+//  Port describes the properties of a specific port of a service.
+Port: {
+	//  REQUIRED: A valid non-negative integer port number.
+	number?: uint32 @protobuf(1)
+
+	//  REQUIRED: The protocol exposed on the port.
+	//  MUST BE one of HTTP|HTTPS|GRPC|HTTP2|MONGO|TCP|TLS.
+	//  TLS implies the connection will be routed based on the SNI header to
+	//  the destination without terminating the TLS connection.
+	protocol?: string @protobuf(2)
+
+	//  Label assigned to the port.
+	name?: string @protobuf(3)
+}
diff --git a/internal/protobuf/testdata/mixer/v1/attributes.proto b/internal/protobuf/testdata/mixer/v1/attributes.proto
new file mode 100644
index 0000000..32fa4fc
--- /dev/null
+++ b/internal/protobuf/testdata/mixer/v1/attributes.proto
@@ -0,0 +1,148 @@
+// Copyright 2016 Istio Authors
+//
+// 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
+//
+//     http://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.
+
+syntax = "proto3";
+
+package istio.mixer.v1;
+
+option go_package = "istio.io/api/mixer/v1";
+
+import "gogoproto/gogo.proto";
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+
+option (gogoproto.goproto_getters_all) = false;
+option (gogoproto.equal_all) = false;
+option (gogoproto.gostring_all) = false;
+option (gogoproto.stable_marshaler_all) = true;
+option cc_enable_arenas = true;
+
+// Attributes represents a set of typed name/value pairs. Many of Mixer's
+// API either consume and/or return attributes.
+//
+// Istio uses attributes to control the runtime behavior of services running in the service mesh.
+// Attributes are named and typed pieces of metadata describing ingress and egress traffic and the
+// environment this traffic occurs in. An Istio attribute carries a specific piece
+// of information such as the error code of an API request, the latency of an API request, or the
+// original IP address of a TCP connection. For example:
+//
+// ```yaml
+// request.path: xyz/abc
+// request.size: 234
+// request.time: 12:34:56.789 04/17/2017
+// source.ip: 192.168.0.1
+// target.service: example
+// ```
+//
+// A given Istio deployment has a fixed vocabulary of attributes that it understands.
+// The specific vocabulary is determined by the set of attribute producers being used
+// in the deployment. The primary attribute producer in Istio is Envoy, although
+// specialized Mixer adapters and services can also generate attributes.
+//
+// The common baseline set of attributes available in most Istio deployments is defined
+// [here](https://istio.io/docs/reference/config/policy-and-telemetry/attribute-vocabulary/).
+//
+// Attributes are strongly typed. The supported attribute types are defined by
+// [ValueType](https://github.com/istio/api/blob/master/policy/v1beta1/value_type.proto).
+// Each type of value is encoded into one of the so-called transport types present
+// in this message.
+//
+// Defines a map of attributes in uncompressed format.
+// Following places may use this message:
+// 1) Configure Istio/Proxy with static per-proxy attributes, such as source.uid.
+// 2) Service IDL definition to extract api attributes for active requests.
+// 3) Forward attributes from client proxy to server proxy for HTTP requests.
+message Attributes {
+  // A map of attribute name to its value.
+  map<string, AttributeValue> attributes = 1;
+
+  // Specifies one attribute value with different type.
+  message AttributeValue {
+    // The attribute value.
+    oneof value {
+      // Used for values of type STRING, DNS_NAME, EMAIL_ADDRESS, and URI
+      string string_value = 2;
+
+      // Used for values of type INT64
+      int64 int64_value = 3;
+
+      // Used for values of type DOUBLE
+      double double_value = 4;
+
+      // Used for values of type BOOL
+      bool bool_value = 5;
+
+      // Used for values of type BYTES
+      bytes bytes_value = 6;
+
+      // Used for values of type TIMESTAMP
+      google.protobuf.Timestamp timestamp_value = 7;
+
+      // Used for values of type DURATION
+      google.protobuf.Duration duration_value = 8;
+
+      // Used for values of type STRING_MAP
+      StringMap string_map_value = 9;
+    }
+  }
+
+  // Defines a string map.
+  message StringMap {
+    // Holds a set of name/value pairs.
+    map<string, string> entries = 1;
+  }
+}
+
+// Defines a list of attributes in compressed format optimized for transport.
+// Within this message, strings are referenced using integer indices into
+// one of two string dictionaries. Positive integers index into the global
+// deployment-wide dictionary, whereas negative integers index into the message-level
+// dictionary instead. The message-level dictionary is carried by the
+// `words` field of this message, the deployment-wide dictionary is determined via
+// configuration.
+message CompressedAttributes {
+  // The message-level dictionary.
+  repeated string words = 1;
+
+  // Holds attributes of type STRING, DNS_NAME, EMAIL_ADDRESS, URI
+  map<sint32, sint32> strings = 2;
+
+  // Holds attributes of type INT64
+  map<sint32, int64> int64s = 3;
+
+  // Holds attributes of type DOUBLE
+  map<sint32, double> doubles = 4;
+
+  // Holds attributes of type BOOL
+  map<sint32, bool> bools = 5;
+
+  // Holds attributes of type TIMESTAMP
+  map<sint32, google.protobuf.Timestamp> timestamps = 6 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
+
+  // Holds attributes of type DURATION
+  map<sint32, google.protobuf.Duration> durations = 7 [(gogoproto.nullable) = false, (gogoproto.stdduration) = true];
+
+  // Holds attributes of type BYTES
+  map<sint32, bytes> bytes = 8;
+
+  // Holds attributes of type STRING_MAP
+  map<sint32, StringMap> string_maps = 9 [(gogoproto.nullable) = false];
+}
+
+// A map of string to string. The keys and values in this map are dictionary
+// indices (see the [Attributes][istio.mixer.v1.CompressedAttributes] message for an explanation)
+message StringMap {
+  // Holds a set of name/value pairs.
+  map<sint32, sint32> entries = 1;
+}
diff --git a/internal/protobuf/testdata/mixer/v1/config/client/api_spec.proto b/internal/protobuf/testdata/mixer/v1/config/client/api_spec.proto
new file mode 100644
index 0000000..92dfefc
--- /dev/null
+++ b/internal/protobuf/testdata/mixer/v1/config/client/api_spec.proto
@@ -0,0 +1,241 @@
+// Copyright 2017 Istio Authors
+//
+//   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
+//
+//       http://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.
+
+syntax = "proto3";
+
+package istio.mixer.v1.config.client;
+
+option go_package="istio.io/api/mixer/v1/config/client";
+
+import "gogoproto/gogo.proto";
+
+import "mixer/v1/attributes.proto";
+import "mixer/v1/config/client/service.proto";
+
+option (gogoproto.goproto_getters_all) = false;
+option (gogoproto.equal_all) = false;
+option (gogoproto.gostring_all) = false;
+option (gogoproto.stable_marshaler_all) = true;
+
+// HTTPAPISpec defines the canonical configuration for generating
+// API-related attributes from HTTP requests based on the method and
+// uri templated path matches. It is sufficient for defining the API
+// surface of a service for the purposes of API attribute
+// generation. It is not intended to represent auth, quota,
+// documentation, or other information commonly found in other API
+// specifications, e.g. OpenAPI.
+//
+// Existing standards that define operations (or methods) in terms of
+// HTTP methods and paths can be normalized to this format for use in
+// Istio. For example, a simple petstore API described by OpenAPIv2
+// [here](https://github.com/googleapis/gnostic/blob/master/examples/v2.0/yaml/petstore-simple.yaml)
+// can be represented with the following HTTPAPISpec.
+//
+// ```yaml
+// apiVersion: config.istio.io/v1alpha2
+// kind: HTTPAPISpec
+// metadata:
+//   name: petstore
+//   namespace: default
+// spec:
+//   attributes:
+//     attributes:
+//       api.service:
+//         stringValue: petstore.swagger.io
+//       api.version:
+//         stringValue: 1.0.0
+//   patterns:
+//   - attributes:
+//       attributes:
+//         api.operation:
+//           stringValue: findPets
+//     httpMethod: GET
+//     uriTemplate: /api/pets
+//   - attributes:
+//       attributes:
+//         api.operation:
+//           stringValue: addPet
+//     httpMethod: POST
+//     uriTemplate: /api/pets
+//   - attributes:
+//       attributes:
+//         api.operation:
+//           stringValue: findPetById
+//     httpMethod: GET
+//     uriTemplate: /api/pets/{id}
+//   - attributes:
+//       attributes:
+//         api.operation:
+//           stringValue: deletePet
+//     httpMethod: DELETE
+//     uriTemplate: /api/pets/{id}
+//   api_keys:
+//   - query: api-key
+// ```
+message HTTPAPISpec {
+  // List of attributes that are generated when *any* of the HTTP
+  // patterns match. This list typically includes the "api.service"
+  // and "api.version" attributes.
+  Attributes attributes = 1;
+
+  // List of HTTP patterns to match.
+  repeated HTTPAPISpecPattern patterns = 2;
+
+  // List of APIKey that describes how to extract an API-KEY from an
+  // HTTP request. The first API-Key match found in the list is used,
+  // i.e. 'OR' semantics.
+  //
+  // The following default policies are used to generate the
+  // `request.api_key` attribute if no explicit APIKey is defined.
+  //
+  //     `query: key, `query: api_key`, and then `header: x-api-key`
+  //
+  repeated APIKey api_keys = 3;
+}
+
+// HTTPAPISpecPattern defines a single pattern to match against
+// incoming HTTP requests. The per-pattern list of attributes is
+// generated if both the http_method and uri_template match. In
+// addition, the top-level list of attributes in the HTTPAPISpec is also
+// generated.
+//
+// ```yaml
+// pattern:
+// - attributes
+//     api.operation: doFooBar
+//   httpMethod: GET
+//   uriTemplate: /foo/bar
+// ```
+message HTTPAPISpecPattern {
+  // List of attributes that are generated if the HTTP request matches
+  // the specified http_method and uri_template. This typically
+  // includes the "api.operation" attribute.
+  Attributes attributes = 1;
+
+  // HTTP request method to match against as defined by
+  // [rfc7231](https://tools.ietf.org/html/rfc7231#page-21). For
+  // example: GET, HEAD, POST, PUT, DELETE.
+  string http_method = 2;
+
+  oneof pattern {
+    // URI template to match against as defined by
+    // [rfc6570](https://tools.ietf.org/html/rfc6570). For example, the
+    // following are valid URI templates:
+    //
+    //     /pets
+    //     /pets/{id}
+    //     /dictionary/{term:1}/{term}
+    //     /search{?q*,lang}
+    //
+    string uri_template = 3;
+
+    // EXPERIMENTAL:
+    //
+    // ecmascript style regex-based match as defined by
+    // [EDCA-262](http://en.cppreference.com/w/cpp/regex/ecmascript). For
+    // example,
+    //
+    //     "^/pets/(.*?)?"
+    //
+    string regex = 4;
+  }
+}
+
+// APIKey defines the explicit configuration for generating the
+// `request.api_key` attribute from HTTP requests.
+//
+// See [API Keys](https://swagger.io/docs/specification/authentication/api-keys)
+// for a general overview of API keys as defined by OpenAPI.
+message APIKey {
+  oneof key {
+    // API Key is sent as a query parameter. `query` represents the
+    // query string parameter name.
+    //
+    // For example, `query=api_key` should be used with the
+    // following request:
+    //
+    //     GET /something?api_key=abcdef12345
+    //
+    string query = 1;
+
+    // API key is sent in a request header. `header` represents the
+    // header name.
+    //
+    // For example, `header=X-API-KEY` should be used with the
+    // following request:
+    //
+    //     GET /something HTTP/1.1
+    //     X-API-Key: abcdef12345
+    //
+    string header = 2;
+
+    // API key is sent in a
+    // [cookie](https://swagger.io/docs/specification/authentication/cookie-authentication),
+    //
+    // For example, `cookie=X-API-KEY` should be used for the
+    // following request:
+    //
+    //     GET /something HTTP/1.1
+    //     Cookie: X-API-KEY=abcdef12345
+    //
+    string cookie = 3;
+  }
+}
+
+// HTTPAPISpecReference defines a reference to an HTTPAPISpec. This is
+// typically used for establishing bindings between an HTTPAPISpec and an
+// IstioService. For example, the following defines an
+// HTTPAPISpecReference for service `foo` in namespace `bar`.
+//
+// ```yaml
+// - name: foo
+//   namespace: bar
+// ```
+message HTTPAPISpecReference {
+  // REQUIRED. The short name of the HTTPAPISpec. This is the resource
+  // name defined by the metadata name field.
+  string name = 1;
+
+  // Optional namespace of the HTTPAPISpec. Defaults to the encompassing
+  // HTTPAPISpecBinding's metadata namespace field.
+  string namespace = 2;
+}
+
+// HTTPAPISpecBinding defines the binding between HTTPAPISpecs and one or more
+// IstioService. For example, the following establishes a binding
+// between the HTTPAPISpec `petstore` and service `foo` in namespace `bar`.
+//
+// ```yaml
+// apiVersion: config.istio.io/v1alpha2
+// kind: HTTPAPISpecBinding
+// metadata:
+//   name: my-binding
+//   namespace: default
+// spec:
+//   services:
+//   - name: foo
+//     namespace: bar
+//   api_specs:
+//   - name: petstore
+//     namespace: default
+// ```
+message HTTPAPISpecBinding {
+  // REQUIRED. One or more services to map the listed HTTPAPISpec onto.
+  repeated IstioService services = 1;
+
+  // REQUIRED. One or more HTTPAPISpec references that should be mapped to
+  // the specified service(s). The aggregate collection of match
+  // conditions defined in the HTTPAPISpecs should not overlap.
+  repeated HTTPAPISpecReference api_specs = 2;
+}
diff --git a/internal/protobuf/testdata/mixer/v1/config/client/client_config.proto b/internal/protobuf/testdata/mixer/v1/config/client/client_config.proto
new file mode 100644
index 0000000..a229699
--- /dev/null
+++ b/internal/protobuf/testdata/mixer/v1/config/client/client_config.proto
@@ -0,0 +1,198 @@
+// Copyright 2017 Istio Authors
+//
+//   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
+//
+//       http://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.
+
+syntax = "proto3";
+
+import "gogoproto/gogo.proto";
+import "google/protobuf/duration.proto";
+
+import "mixer/v1/attributes.proto";
+import "mixer/v1/config/client/api_spec.proto";
+import "mixer/v1/config/client/quota.proto";
+
+// $title: Mixer Client
+// $description: Configuration state for the Mixer client library.
+// $location: https://istio.io/docs/reference/config/policy-and-telemetry/istio.mixer.v1.config.client
+
+// Describes the configuration state for the Mixer client library that's built into Envoy.
+package istio.mixer.v1.config.client;
+
+option go_package = "istio.io/api/mixer/v1/config/client";
+
+option (gogoproto.goproto_getters_all) = false;
+option (gogoproto.equal_all) = false;
+option (gogoproto.gostring_all) = false;
+option (gogoproto.stable_marshaler_all) = true;
+
+// Specifies the behavior when the client is unable to connect to Mixer.
+message NetworkFailPolicy {
+  // Describes the policy.
+  enum FailPolicy {
+    // If network connection fails, request is allowed and delivered to the
+    // service.
+    FAIL_OPEN = 0;
+
+    // If network connection fails, request is rejected.
+    FAIL_CLOSE = 1;
+  }
+
+  // Specifies the behavior when the client is unable to connect to Mixer.
+  FailPolicy policy = 1;
+
+  // Max retries on transport error.
+  uint32 max_retry = 2;
+
+  // Base time to wait between retries.  Will be adjusted by exponential
+  // backoff and jitter.
+  google.protobuf.Duration base_retry_wait = 3;
+
+  // Max time to wait between retries.
+  google.protobuf.Duration max_retry_wait = 4;
+}
+
+// Defines the per-service client configuration.
+message ServiceConfig {
+  // If true, do not call Mixer Check.
+  bool disable_check_calls = 1;
+
+  // If true, do not call Mixer Report.
+  bool disable_report_calls = 2;
+
+  // Send these attributes to Mixer in both Check and Report. This
+  // typically includes the "destination.service" attribute.
+  // In case of a per-route override, per-route attributes take precedence
+  // over the attributes supplied in the client configuration.
+  Attributes mixer_attributes = 3;
+
+  // HTTP API specifications to generate API attributes.
+  repeated HTTPAPISpec http_api_spec = 4;
+
+  // Quota specifications to generate quota requirements.
+  repeated QuotaSpec quota_spec = 5;
+
+  // Specifies the behavior when the client is unable to connect to Mixer.
+  // This is the service-level policy. It overrides
+  // [mesh-level
+  // policy][istio.mixer.v1.config.client.TransportConfig.network_fail_policy].
+  NetworkFailPolicy network_fail_policy = 7;
+
+  // Default attributes to forward to upstream. This typically
+  // includes the "source.ip" and "source.uid" attributes.
+  // In case of a per-route override, per-route attributes take precedence
+  // over the attributes supplied in the client configuration.
+  //
+  // Forwarded attributes take precedence over the static Mixer attributes.
+  // The full order of application is as follows:
+  // 1. static Mixer attributes from the filter config;
+  // 2. static Mixer attributes from the route config;
+  // 3. forwarded attributes from the source filter config (if any);
+  // 4. forwarded attributes from the source route config (if any);
+  // 5. derived attributes from the request metadata.
+  Attributes forward_attributes = 8;
+}
+
+// Defines the transport config on how to call Mixer.
+message TransportConfig {
+  // The flag to disable check cache.
+  bool disable_check_cache = 1;
+
+  // The flag to disable quota cache.
+  bool disable_quota_cache = 2;
+
+  // The flag to disable report batch.
+  bool disable_report_batch = 3;
+
+  // Specifies the behavior when the client is unable to connect to Mixer.
+  // This is the mesh level policy. The default value for policy is FAIL_OPEN.
+  NetworkFailPolicy network_fail_policy = 4;
+
+  // Specify refresh interval to write Mixer client statistics to Envoy share
+  // memory. If not specified, the interval is 10 seconds.
+  google.protobuf.Duration stats_update_interval = 5;
+
+  // Name of the cluster that will forward check calls to a pool of mixer
+  // servers. Defaults to "mixer_server". By using different names for
+  // checkCluster and reportCluster, it is possible to have one set of
+  // Mixer servers handle check calls, while another set of Mixer servers
+  // handle report calls.
+  //
+  // NOTE: Any value other than the default "mixer_server" will require the
+  // Istio Grafana dashboards to be reconfigured to use the new name.
+  string check_cluster = 6;
+
+  // Name of the cluster that will forward report calls to a pool of mixer
+  // servers. Defaults to "mixer_server". By using different names for
+  // checkCluster and reportCluster, it is possible to have one set of
+  // Mixer servers handle check calls, while another set of Mixer servers
+  // handle report calls.
+  //
+  // NOTE: Any value other than the default "mixer_server" will require the
+  // Istio Grafana dashboards to be reconfigured to use the new name.
+  string report_cluster = 7;
+
+  // Default attributes to forward to Mixer upstream. This typically
+  // includes the "source.ip" and "source.uid" attributes. These
+  // attributes are consumed by the proxy in front of mixer.
+  Attributes attributes_for_mixer_proxy = 8;
+}
+
+// Defines the client config for HTTP.
+message HttpClientConfig {
+  // The transport config.
+  TransportConfig transport = 1;
+
+  // Map of control configuration indexed by destination.service. This
+  // is used to support per-service configuration for cases where a
+  // mixerclient serves multiple services.
+  map<string, ServiceConfig> service_configs = 2;
+
+  // Default destination service name if none was specified in the
+  // client request.
+  string default_destination_service = 3;
+
+  // Default attributes to send to Mixer in both Check and
+  // Report. This typically includes "destination.ip" and
+  // "destination.uid" attributes.
+  Attributes mixer_attributes = 4;
+
+  // Default attributes to forward to upstream. This typically
+  // includes the "source.ip" and "source.uid" attributes.
+  Attributes forward_attributes = 5;
+}
+
+// Defines the client config for TCP.
+message TcpClientConfig {
+  // The transport config.
+  TransportConfig transport = 1;
+
+  // Default attributes to send to Mixer in both Check and
+  // Report. This typically includes "destination.ip" and
+  // "destination.uid" attributes.
+  Attributes mixer_attributes = 2;
+
+  // If set to true, disables Mixer check calls.
+  bool disable_check_calls = 3;
+
+  // If set to true, disables Mixer check calls.
+  bool disable_report_calls = 4;
+
+  // Quota specifications to generate quota requirements.
+  // It applies on the new TCP connections.
+  QuotaSpec connection_quota_spec = 5;
+
+  // Specify report interval to send periodical reports for long TCP
+  // connections. If not specified, the interval is 10 seconds. This interval
+  // should not be less than 1 second, otherwise it will be reset to 1 second.
+  google.protobuf.Duration report_interval = 6;
+}
diff --git a/internal/protobuf/testdata/mixer/v1/config/client/quota.proto b/internal/protobuf/testdata/mixer/v1/config/client/quota.proto
new file mode 100644
index 0000000..5ea594c
--- /dev/null
+++ b/internal/protobuf/testdata/mixer/v1/config/client/quota.proto
@@ -0,0 +1,145 @@
+// Copyright 2017 Istio Authors
+//
+//   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
+//
+//       http://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.
+
+syntax = "proto3";
+
+package istio.mixer.v1.config.client;
+
+option go_package="istio.io/api/mixer/v1/config/client";
+
+import "gogoproto/gogo.proto";
+import "mixer/v1/config/client/service.proto";
+
+option (gogoproto.goproto_getters_all) = false;
+option (gogoproto.equal_all) = false;
+option (gogoproto.gostring_all) = false;
+option (gogoproto.stable_marshaler_all) = true;
+
+// Specifies runtime quota rules.
+//  * Uses Istio attributes to match individual requests
+//  * Specifies list of quotas to use for matched requests.
+//
+// Example1:
+// Charge "request_count" quota with 1 amount for all requests.
+//
+//   QuotaSpec:
+//     - rules
+//       - quotas:
+//           quota: request_count
+//           charge: 1
+//
+// Example2:
+// For HTTP POST requests with path are prefixed with /books or
+// api.operation is create_books, charge two quotas:
+// * write_count of 1 amount
+// * request_count of 5 amount.
+//
+// ```yaml
+// QuotaSpec:
+//   - rules:
+//     - match:
+//         clause:
+//           request.path:
+//             string_prefix: /books
+//           request.http_method:
+//             string_exact: POST
+//     - match:
+//         clause:
+//           api.operation:
+//             string_exact: create_books
+//     - quotas:
+//         quota: write_count
+//         charge: 1
+//     - quotas:
+//         quota: request_count
+//         charge: 5
+// ```
+
+// Determines the quotas used for individual requests.
+message QuotaSpec {
+  // A list of Quota rules.
+  repeated QuotaRule rules = 1;
+}
+
+// Specifies a rule with list of matches and list of quotas.
+// If any clause matched, the list of quotas will be used.
+message QuotaRule {
+  // If empty, match all request.
+  // If any of match is true, it is matched.
+  repeated AttributeMatch match = 1;
+
+  // The list of quotas to charge.
+  repeated Quota quotas = 2;
+}
+
+// Describes how to match a given string in HTTP headers. Match is
+// case-sensitive.
+message StringMatch {
+  oneof match_type {
+    // exact string match
+    string exact = 1;
+    // prefix-based match
+    string prefix = 2;
+    // ECMAscript style regex-based match
+    string regex = 3;
+  }
+}
+
+// Specifies a match clause to match Istio attributes
+message AttributeMatch {
+  // Map of attribute names to StringMatch type.
+  // Each map element specifies one condition to match.
+  //
+  // Example:
+  //
+  //   clause:
+  //     source.uid:
+  //       exact: SOURCE_UID
+  //     request.http_method:
+  //       exact: POST
+  map<string, StringMatch> clause = 1;
+}
+
+// Specifies a quota to use with quota name and amount.
+message Quota {
+  // The quota name to charge
+  string quota = 1;
+
+  // The quota amount to charge
+  int64  charge = 2;
+}
+
+// QuotaSpecBinding defines the binding between QuotaSpecs and one or more
+// IstioService.
+message QuotaSpecBinding {
+  // REQUIRED. One or more services to map the listed QuotaSpec onto.
+  repeated IstioService services = 1;
+
+  // QuotaSpecReference uniquely identifies the QuotaSpec used in the
+  // Binding.
+  message QuotaSpecReference {
+    // REQUIRED. The short name of the QuotaSpec. This is the resource
+    // name defined by the metadata name field.
+    string name = 1;
+
+    // Optional namespace of the QuotaSpec. Defaults to the value of the
+    // metadata namespace field.
+    string namespace = 2;
+  }
+
+  // REQUIRED. One or more QuotaSpec references that should be mapped to
+  // the specified service(s). The aggregate collection of match
+  // conditions defined in the QuotaSpecs should not overlap.
+  repeated QuotaSpecReference quota_specs = 2;
+}
diff --git a/internal/protobuf/testdata/mixer/v1/config/client/service.proto b/internal/protobuf/testdata/mixer/v1/config/client/service.proto
new file mode 100644
index 0000000..01de2e9
--- /dev/null
+++ b/internal/protobuf/testdata/mixer/v1/config/client/service.proto
@@ -0,0 +1,58 @@
+// Copyright 2017 Istio Authors
+//
+//   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
+//
+//       http://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.
+
+syntax = "proto3";
+
+package istio.mixer.v1.config.client;
+
+option go_package="istio.io/api/mixer/v1/config/client";
+
+import "gogoproto/gogo.proto";
+
+option (gogoproto.goproto_getters_all) = false;
+option (gogoproto.equal_all) = false;
+option (gogoproto.gostring_all) = false;
+option (gogoproto.stable_marshaler_all) = true;
+
+// NOTE: this is a duplicate of proxy.v1.config.IstioService from
+// proxy/v1alpha1/config/route_rules.proto.
+//
+// Mixer protobufs have gogoproto specific options which are not
+// compatiable with the proxy's vanilla protobufs. Ideally, these
+// protobuf options be reconciled so fundamental Istio concepts and
+// types can be shared by components. Until then, make a copy of
+// IstioService for mixerclient to use.
+
+// IstioService identifies a service and optionally service version.
+// The FQDN of the service is composed from the name, namespace, and implementation-specific domain suffix
+// (e.g. on Kubernetes, "reviews" + "default" + "svc.cluster.local" -> "reviews.default.svc.cluster.local").
+message IstioService {
+  // The short name of the service such as "foo".
+  string name = 1;
+
+  // Optional namespace of the service. Defaults to value of metadata namespace field.
+  string namespace = 2;
+
+  // Domain suffix used to construct the service FQDN in implementations that support such specification.
+  string domain = 3;
+
+  // The service FQDN.
+  string service = 4;
+
+  // Optional one or more labels that uniquely identify the service version.
+  //
+  // *Note:* When used for a VirtualService destination, labels MUST be empty.
+  //
+  map<string, string> labels = 5;
+}
diff --git a/internal/protobuf/testdata/mixer/v1/mixer.proto b/internal/protobuf/testdata/mixer/v1/mixer.proto
new file mode 100644
index 0000000..04ace67
--- /dev/null
+++ b/internal/protobuf/testdata/mixer/v1/mixer.proto
@@ -0,0 +1,265 @@
+// Copyright 2016 Istio Authors
+//
+// 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
+//
+//     http://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.
+
+syntax = "proto3";
+
+// This package defines the Mixer API that the sidecar proxy uses to perform
+// precondition checks, manage quotas, and report telemetry.
+package istio.mixer.v1;
+
+option go_package = "istio.io/api/mixer/v1";
+option cc_generic_services = true;
+
+import "gogoproto/gogo.proto";
+import "google/protobuf/duration.proto";
+import "google/rpc/status.proto";
+import "mixer/v1/attributes.proto";
+
+option (gogoproto.goproto_getters_all) = false;
+option (gogoproto.equal_all) = false;
+option (gogoproto.gostring_all) = false;
+option cc_enable_arenas = true;
+
+// Mixer provides three core features:
+//
+// - *Precondition Checking*. Enables callers to verify a number of preconditions
+// before responding to an incoming request from a service consumer.
+// Preconditions can include whether the service consumer is properly
+// authenticated, is on the service’s whitelist, passes ACL checks, and more.
+//
+// - *Quota Management*. Enables services to allocate and free quota on a number
+// of dimensions, Quotas are used as a relatively simple resource management tool
+// to provide some fairness between service consumers when contending for limited
+// resources. Rate limits are examples of quotas.
+//
+// - *Telemetry Reporting*. Enables services to report logging and monitoring.
+// In the future, it will also enable tracing and billing streams intended for
+// both the service operator as well as for service consumers.
+service Mixer {
+  // Checks preconditions and allocate quota before performing an operation.
+  // The preconditions enforced depend on the set of supplied attributes and
+  // the active configuration.
+  rpc Check(CheckRequest) returns (CheckResponse) {}
+
+  // Reports telemetry, such as logs and metrics.
+  // The reported information depends on the set of supplied attributes and the
+  // active configuration.
+  rpc Report(ReportRequest) returns (ReportResponse) {}
+}
+
+// Used to get a thumbs-up/thumbs-down before performing an action.
+message CheckRequest {
+  // parameters for a quota allocation
+  message QuotaParams {
+    // Amount of quota to allocate
+    int64 amount = 1;
+
+    // When true, supports returning less quota than what was requested.
+    bool best_effort = 2;
+  }
+
+  // The attributes to use for this request.
+  //
+  // Mixer's configuration determines how these attributes are used to
+  // establish the result returned in the response.
+  CompressedAttributes attributes = 1 [(gogoproto.nullable) = false];
+
+  // The number of words in the global dictionary, used with to populate the attributes.
+  // This value is used as a quick way to determine whether the client is using a dictionary that
+  // the server understands.
+  uint32 global_word_count = 2;
+
+  // Used for deduplicating `Check` calls in the case of failed RPCs and retries. This should be a UUID
+  // per call, where the same UUID is used for retries of the same call.
+  string deduplication_id = 3;
+
+  // The individual quotas to allocate
+  map<string, QuotaParams> quotas = 4 [(gogoproto.nullable) = false];
+}
+
+// The response generated by the Check method.
+message CheckResponse {
+
+  // Expresses the result of a precondition check.
+  message PreconditionResult {
+    reserved 4;
+
+    // A status code of OK indicates all preconditions were satisfied. Any other code indicates not
+    // all preconditions were satisfied and details describe why.
+    google.rpc.Status status = 1 [(gogoproto.nullable) = false];
+
+    // The amount of time for which this result can be considered valid.
+    google.protobuf.Duration valid_duration = 2 [(gogoproto.nullable) = false, (gogoproto.stdduration) = true];
+
+    // The number of uses for which this result can be considered valid.
+    int32 valid_use_count = 3;
+
+    // The total set of attributes that were used in producing the result
+    // along with matching conditions.
+    ReferencedAttributes referenced_attributes = 5;
+
+    // An optional routing directive, used to manipulate the traffic metadata
+    // whenever all preconditions are satisfied.
+    RouteDirective route_directive = 6;
+  }
+
+  // Expresses the result of a quota allocation.
+  message QuotaResult {
+    // The amount of time for which this result can be considered valid.
+    google.protobuf.Duration valid_duration = 1 [(gogoproto.nullable) = false, (gogoproto.stdduration) = true];
+
+    // The amount of granted quota. When `QuotaParams.best_effort` is true, this will be >= 0.
+    // If `QuotaParams.best_effort` is false, this will be either 0 or >= `QuotaParams.amount`.
+    int64 granted_amount = 2;
+
+    // The total set of attributes that were used in producing the result
+    // along with matching conditions.
+    ReferencedAttributes referenced_attributes = 5 [(gogoproto.nullable) = false];
+  }
+
+  // The precondition check results.
+  PreconditionResult precondition = 2 [(gogoproto.nullable) = false];
+
+  // The resulting quota, one entry per requested quota.
+  map<string, QuotaResult> quotas = 3 [(gogoproto.nullable) = false];
+}
+
+// Describes the attributes that were used to determine the response.
+// This can be used to construct a response cache.
+message ReferencedAttributes {
+  // How an attribute's value was matched
+  enum Condition {
+    CONDITION_UNSPECIFIED = 0;    // should not occur
+    ABSENCE = 1;                  // match when attribute doesn't exist
+    EXACT = 2;                    // match when attribute value is an exact byte-for-byte match
+    REGEX = 3;                    // match when attribute value matches the included regex
+  }
+
+  // Describes a single attribute match.
+  message AttributeMatch {
+    // The name of the attribute. This is a dictionary index encoded in a manner identical
+    // to all strings in the [CompressedAttributes][istio.mixer.v1.CompressedAttributes] message.
+    sint32 name = 1;
+
+    // The kind of match against the attribute value.
+    Condition condition = 2;
+
+    // If a REGEX condition is provided for a STRING_MAP attribute,
+    // clients should use the regex value to match against map keys.
+    string regex = 3;
+
+    // A key in a STRING_MAP. When multiple keys from a STRING_MAP
+    // attribute were referenced, there will be multiple AttributeMatch
+    // messages with different map_key values. Values for map_key SHOULD
+    // be ignored for attributes that are not STRING_MAP.
+    //
+    // Indices for the keys are used (taken either from the
+    // message dictionary from the `words` field or the global dictionary).
+    //
+    // If no map_key value is provided for a STRING_MAP attribute, the
+    // entire STRING_MAP will be used.
+    sint32 map_key = 4;
+  }
+
+  // The message-level dictionary. Refer to [CompressedAttributes][istio.mixer.v1.CompressedAttributes] for information
+  // on using dictionaries.
+  repeated string words = 1;
+
+  // Describes a set of attributes.
+  repeated AttributeMatch attribute_matches = 2 [(gogoproto.nullable) = false];
+}
+
+// Operation on HTTP headers to replace, append, or remove a header. Header
+// names are normalized to lower-case with dashes, e.g.  "x-request-id".
+// Pseudo-headers ":path", ":authority", and ":method" are supported to modify
+// the request headers.
+message HeaderOperation {
+  // Operation type.
+  enum Operation {
+    REPLACE = 0;  // replaces the header with the given name
+    REMOVE = 1;   // removes the header with the given name (the value is ignored)
+    APPEND = 2;   // appends the value to the header value, or sets it if not present
+  }
+  // Header name.
+  string name = 1;
+
+  // Header value.
+  string value = 2;
+
+  // Header operation.
+  Operation operation = 3;
+}
+
+// Expresses the routing manipulation actions to be performed on behalf of
+// Mixer in response to a precondition check.
+message RouteDirective {
+  // Operations on the request headers.
+  repeated HeaderOperation request_header_operations = 1 [(gogoproto.nullable) = false];
+
+  // Operations on the response headers.
+  repeated HeaderOperation response_header_operations = 2 [(gogoproto.nullable) = false];
+
+  // If set, enables a direct response without proxying the request to the routing
+  // destination. Required to be a value in the 2xx or 3xx range.
+  uint32 direct_response_code = 3;
+
+  // Supplies the response body for the direct response.
+  // If this setting is omitted, no body is included in the generated response.
+  string direct_response_body = 4;
+}
+
+// Used to report telemetry after performing one or more actions.
+message ReportRequest {
+  // next value: 5
+
+  // Used to signal how the sets of compressed attributes should be reconstitued server-side.
+  enum RepeatedAttributesSemantics {
+    // Use delta encoding between sets of compressed attributes to reduce the overall on-wire
+    // request size. Each individual set of attributes is used to modify the previous set.
+    // NOTE: There is no way with this encoding to specify attribute value deletion. This 
+    // option should be used with extreme caution.
+    DELTA_ENCODING = 0;
+
+    // Treat each set of compressed attributes as complete - independent from other sets
+    // in this request. This will result in on-wire duplication of attributes and values, but
+    // will allow for proper accounting of absent values in overall encoding.
+    INDEPENDENT_ENCODING = 1;
+  }
+
+  // The attributes to use for this request.
+  //
+  // Each `Attributes` element represents the state of a single action. Multiple actions
+  // can be provided in a single message in order to improve communication efficiency. The
+  // client can accumulate a set of actions and send them all in one single message.
+  repeated CompressedAttributes attributes = 1 [(gogoproto.nullable) = false];
+
+  // Indicates how to decode the attributes sets in this request.
+  RepeatedAttributesSemantics repeated_attributes_semantics = 4;
+
+  // The default message-level dictionary for all the attributes.
+  // Individual attribute messages can have their own dictionaries, but if they don't
+  // then this set of words, if it is provided, is used instead.
+  //
+  // This makes it possible to share the same dictionary for all attributes in this
+  // request, which can substantially reduce the overall request size.
+  repeated string default_words = 2;
+
+  // The number of words in the global dictionary.
+  // To detect global dictionary out of sync between client and server.
+  uint32 global_word_count = 3;
+}
+
+// Used to carry responses to telemetry reports
+message ReportResponse {
+}
diff --git a/internal/protobuf/testdata/networking/v1alpha3/gateway.proto b/internal/protobuf/testdata/networking/v1alpha3/gateway.proto
new file mode 100644
index 0000000..e06fe5d
--- /dev/null
+++ b/internal/protobuf/testdata/networking/v1alpha3/gateway.proto
@@ -0,0 +1,448 @@
+// Copyright 2019 CUE Authors
+//
+// 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
+// 
+//     http://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.
+
+// Copyright 2017 Istio Authors
+//
+//   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
+//
+//       http://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.
+
+
+syntax = "proto3";
+
+// $title: Gateway
+// $description: Configuration affecting edge load balancer.
+// $location: https://istio.io/docs/reference/config/networking/v1alpha3/gateway.html
+
+// `Gateway` describes a load balancer operating at the edge of the mesh
+// receiving incoming or outgoing HTTP/TCP connections. The specification
+// describes a set of ports that should be exposed, the type of protocol to
+// use, SNI configuration for the load balancer, etc.
+//
+// For example, the following Gateway configuration sets up a proxy to act
+// as a load balancer exposing port 80 and 9080 (http), 443 (https),
+// 9443(https) and port 2379 (TCP) for ingress.  The gateway will be
+// applied to the proxy running on a pod with labels `app:
+// my-gateway-controller`. While Istio will configure the proxy to listen
+// on these ports, it is the responsibility of the user to ensure that
+// external traffic to these ports are allowed into the mesh.
+//
+// ```yaml
+// apiVersion: networking.istio.io/v1alpha3
+// kind: Gateway
+// metadata:
+//   name: my-gateway
+//   namespace: some-config-namespace
+// spec:
+//   selector:
+//     app: my-gateway-controller
+//   servers:
+//   - port:
+//       number: 80
+//       name: http
+//       protocol: HTTP
+//     hosts:
+//     - uk.bookinfo.com
+//     - eu.bookinfo.com
+//     tls:
+//       httpsRedirect: true # sends 301 redirect for http requests
+//   - port:
+//       number: 443
+//       name: https-443
+//       protocol: HTTPS
+//     hosts:
+//     - uk.bookinfo.com
+//     - eu.bookinfo.com
+//     tls:
+//       mode: SIMPLE # enables HTTPS on this port
+//       serverCertificate: /etc/certs/servercert.pem
+//       privateKey: /etc/certs/privatekey.pem
+//   - port:
+//       number: 9443
+//       name: https-9443
+//       protocol: HTTPS
+//     hosts:
+//     - "bookinfo-namespace/*.bookinfo.com"
+//     tls:
+//       mode: SIMPLE # enables HTTPS on this port
+//       credentialName: bookinfo-secret # fetches certs from Kubernetes secret
+//   - port:
+//       number: 9080
+//       name: http-wildcard
+//       protocol: HTTP
+//     hosts:
+//     - "*"
+//   - port:
+//       number: 2379 # to expose internal service via external port 2379
+//       name: mongo
+//       protocol: MONGO
+//     hosts:
+//     - "*"
+// ```
+//
+// The Gateway specification above describes the L4-L6 properties of a load
+// balancer. A `VirtualService` can then be bound to a gateway to control
+// the forwarding of traffic arriving at a particular host or gateway port.
+//
+// For example, the following VirtualService splits traffic for
+// `https://uk.bookinfo.com/reviews`, `https://eu.bookinfo.com/reviews`,
+// `http://uk.bookinfo.com:9080/reviews`,
+// `http://eu.bookinfo.com:9080/reviews` into two versions (prod and qa) of
+// an internal reviews service on port 9080. In addition, requests
+// containing the cookie "user: dev-123" will be sent to special port 7777
+// in the qa version. The same rule is also applicable inside the mesh for
+// requests to the "reviews.prod.svc.cluster.local" service. This rule is
+// applicable across ports 443, 9080. Note that `http://uk.bookinfo.com`
+// gets redirected to `https://uk.bookinfo.com` (i.e. 80 redirects to 443).
+//
+// ```yaml
+// apiVersion: networking.istio.io/v1alpha3
+// kind: VirtualService
+// metadata:
+//   name: bookinfo-rule
+//   namespace: bookinfo-namespace
+// spec:
+//   hosts:
+//   - reviews.prod.svc.cluster.local
+//   - uk.bookinfo.com
+//   - eu.bookinfo.com
+//   gateways:
+//   - some-config-namespace/my-gateway
+//   - mesh # applies to all the sidecars in the mesh
+//   http:
+//   - match:
+//     - headers:
+//         cookie:
+//           exact: "user=dev-123"
+//     route:
+//     - destination:
+//         port:
+//           number: 7777
+//         host: reviews.qa.svc.cluster.local
+//   - match:
+//     - uri:
+//         prefix: /reviews/
+//     route:
+//     - destination:
+//         port:
+//           number: 9080 # can be omitted if it's the only port for reviews
+//         host: reviews.prod.svc.cluster.local
+//       weight: 80
+//     - destination:
+//         host: reviews.qa.svc.cluster.local
+//       weight: 20
+// ```
+//
+// The following VirtualService forwards traffic arriving at (external)
+// port 27017 to internal Mongo server on port 5555. This rule is not
+// applicable internally in the mesh as the gateway list omits the
+// reserved name `mesh`.
+//
+// ```yaml
+// apiVersion: networking.istio.io/v1alpha3
+// kind: VirtualService
+// metadata:
+//   name: bookinfo-Mongo
+//   namespace: bookinfo-namespace
+// spec:
+//   hosts:
+//   - mongosvr.prod.svc.cluster.local # name of internal Mongo service
+//   gateways:
+//   - some-config-namespace/my-gateway # can omit the namespace if gateway is in same
+//                                        namespace as virtual service.
+//   tcp:
+//   - match:
+//     - port: 27017
+//     route:
+//     - destination:
+//         host: mongo.prod.svc.cluster.local
+//         port:
+//           number: 5555
+// ```
+//
+// It is possible to restrict the set of virtual services that can bind to
+// a gateway server using the namespace/hostname syntax in the hosts field.
+// For example, the following Gateway allows any virtual service in the ns1
+// namespace to bind to it, while restricting only the virtual service with
+// foo.bar.com host in the ns2 namespace to bind to it.
+//
+// ```yaml
+// apiVersion: networking.istio.io/v1alpha3
+// kind: Gateway
+// metadata:
+//   name: my-gateway
+//   namespace: some-config-namespace
+// spec:
+//   selector:
+//     app: my-gateway-controller
+//   servers:
+//   - port:
+//       number: 80
+//       name: http
+//       protocol: HTTP
+//     hosts:
+//     - "ns1/*"
+//     - "ns2/foo.bar.com"
+// ```
+//
+package istio.networking.v1alpha3;
+
+import "cuelang/cue.proto";
+
+// GO PACKAGE
+option go_package = "istio.io/api/networking/v1alpha3"; // INLINE
+
+message Gateway {
+  // REQUIRED: A list of server specifications.
+  repeated Server servers = 1;
+
+  // REQUIRED: One or more labels that indicate a specific set of pods/VMs
+  // on which this gateway configuration should be applied. The scope of
+  // label search is restricted to the configuration namespace in which the
+  // the resource is present. In other words, the Gateway resource must
+  // reside in the same namespace as the gateway workload instance.
+  map<string, string> selector = 2 [ (cue.val) = "{<name>: name}"];
+}
+
+// `Server` describes the properties of the proxy on a given load balancer
+// port. For example,
+//
+// ```yaml
+// apiVersion: networking.istio.io/v1alpha3
+// kind: Gateway
+// metadata:
+//   name: my-ingress
+// spec:
+//   selector:
+//     app: my-ingress-gateway
+//   servers:
+//   - port:
+//       number: 80
+//       name: http2
+//       protocol: HTTP2
+//     hosts:
+//     - "*"
+// ```
+//
+// Another example
+//
+// ```yaml
+// apiVersion: networking.istio.io/v1alpha3
+// kind: Gateway
+// metadata:
+//   name: my-tcp-ingress
+// spec:
+//   selector:
+//     app: my-tcp-ingress-gateway
+//   servers:
+//   - port:
+//       number: 27018
+//       name: mongo
+//       protocol: MONGO
+//     hosts:
+//     - "*"
+// ```
+//
+// The following is an example of TLS configuration for port 443
+//
+// ```yaml
+// apiVersion: networking.istio.io/v1alpha3
+// kind: Gateway
+// metadata:
+//   name: my-tls-ingress
+// spec:
+//   selector:
+//     app: my-tls-ingress-gateway
+//   servers:
+//   - port:
+//       number: 443
+//       name: https
+//       protocol: HTTPS
+//     hosts:
+//     - "*"
+//     tls:
+//       mode: SIMPLE
+//       serverCertificate: /etc/certs/server.pem
+//       privateKey: /etc/certs/privatekey.pem
+// ```
+message Server {
+  // REQUIRED: The Port on which the proxy should listen for incoming
+  // connections.
+  Port port = 1 [(cue.val) = ">10 & <100"];
+
+  // $hide_from_docs
+  // The ip or the Unix domain socket to which the listener should be bound
+  // to. Format: `x.x.x.x` or `unix:///path/to/uds` or `unix://@foobar`
+  // (Linux abstract namespace). When using Unix domain sockets, the port
+  // number should be 0.
+  string bind = 4;
+
+  // REQUIRED. One or more hosts exposed by this gateway.
+  // While typically applicable to
+  // HTTP services, it can also be used for TCP services using TLS with SNI.
+  // A host is specified as a `dnsName` with an optional `namespace/` prefix.
+  // The `dnsName` should be specified using FQDN format, optionally including
+  // a wildcard character in the left-most component (e.g., `prod/*.example.com`).
+  // Set the `dnsName` to `*` to select all `VirtualService` hosts from the
+  // specified namespace (e.g.,`prod/*`). If no `namespace/` is specified,
+  // the `VirtualService` hosts will be selected from any available namespace.
+  // Any associated `DestinationRule` in the same namespace will also be used.
+  //
+  // A `VirtualService` must be bound to the gateway and must have one or
+  // more hosts that match the hosts specified in a server. The match
+  // could be an exact match or a suffix match with the server's hosts. For
+  // example, if the server's hosts specifies `*.example.com`, a
+  // `VirtualService` with hosts `dev.example.com` or `prod.example.com` will
+  // match. However, a `VirtualService` with host `example.com` or
+  // `newexample.com` will not match.
+  //
+  // NOTE: Only virtual services exported to the gateway's namespace
+  // (e.g., `exportTo` value of `*`) can be referenced.
+  // Private configurations (e.g., `exportTo` set to `.`) will not be
+  // available. Refer to the `exportTo` setting in `VirtualService`,
+  // `DestinationRule`, and `ServiceEntry` configurations for details.
+  repeated string hosts = 2;
+
+  message TLSOptions {
+    // If set to true, the load balancer will send a 301 redirect for all
+    // http connections, asking the clients to use HTTPS.
+    bool https_redirect = 1;
+
+    // TLS modes enforced by the proxy
+    enum TLSmode {
+      // The SNI string presented by the client will be used as the match
+      // criterion in a VirtualService TLS route to determine the
+      // destination service from the service registry.
+      PASSTHROUGH = 0;
+
+      // Secure connections with standard TLS semantics.
+      SIMPLE = 1;
+
+      // Secure connections to the upstream using mutual TLS by presenting
+      // client certificates for authentication.
+      MUTUAL = 2;
+
+      // Similar to the passthrough mode, except servers with this TLS mode
+      // do not require an associated VirtualService to map from the SNI
+      // value to service in the registry. The destination details such as
+      // the service/subset/port are encoded in the SNI value. The proxy
+      // will forward to the upstream (Envoy) cluster (a group of
+      // endpoints) specified by the SNI value. This server is typically
+      // used to provide connectivity between services in disparate L3
+      // networks that otherwise do not have direct connectivity between
+      // their respective endpoints. Use of this mode assumes that both the
+      // source and the destination are using Istio mTLS to secure traffic.
+      AUTO_PASSTHROUGH = 3;
+    };
+
+    // Optional: Indicates whether connections to this port should be
+    // secured using TLS. The value of this field determines how TLS is
+    // enforced.
+    TLSmode mode = 2;
+
+    // Extra comment.
+
+    // REQUIRED if mode is `SIMPLE` or `MUTUAL`. The path to the file
+    // holding the server-side TLS certificate to use.
+    string server_certificate = 3;
+
+    // REQUIRED if mode is `SIMPLE` or `MUTUAL`. The path to the file
+    // holding the server's private key.
+    string private_key = 4;
+
+    // REQUIRED if mode is `MUTUAL`. The path to a file containing
+    // certificate authority certificates to use in verifying a presented
+    // client side certificate.
+    string ca_certificates = 5;
+
+    // The credentialName stands for a unique identifier that can be used
+    // to identify the serverCertificate and the privateKey. The
+    // credentialName appended with suffix "-cacert" is used to identify
+    // the CaCertificates associated with this server. Gateway workloads
+    // capable of fetching credentials from a remote credential store such
+    // as Kubernetes secrets, will be configured to retrieve the
+    // serverCertificate and the privateKey using credentialName, instead
+    // of using the file system paths specified above. If using mutual TLS,
+    // gateway workload instances will retrieve the CaCertificates using
+    // credentialName-cacert. The semantics of the name are platform
+    // dependent.  In Kubernetes, the default Istio supplied credential
+    // server expects the credentialName to match the name of the
+    // Kubernetes secret that holds the server certificate, the private
+    // key, and the CA certificate (if using mutual TLS). Set the
+    // `ISTIO_META_USER_SDS` metadata variable in the gateway's proxy to
+    // enable the dynamic credential fetching feature.
+    string credential_name = 10;
+
+    // A list of alternate names to verify the subject identity in the
+    // certificate presented by the client.
+    repeated string subject_alt_names = 6;
+
+    // TLS protocol versions.
+    enum TLSProtocol {
+      TLS_AUTO = 0; // Automatically choose the optimal TLS version.
+
+      TLSV1_0 = 1; // TLS version 1.0
+
+      TLSV1_1 = 2; // TLS version 1.1
+    
+      TLSV1_2 = 3; // TLS version 1.2
+
+      TLSV1_3 = 4; // TLS version 1.3
+    }
+
+    // Optional: Minimum TLS protocol version.
+    TLSProtocol min_protocol_version = 7;
+
+    // Optional: Maximum TLS protocol version.
+    TLSProtocol max_protocol_version = 8;
+
+    // Optional: If specified, only support the specified cipher list.
+    // Otherwise default to the default cipher list supported by Envoy.
+    repeated string cipher_suites = 9;
+  }
+
+  // Set of TLS related options that govern the server's behavior. Use
+  // these options to control if all http requests should be redirected to
+  // https, and the TLS modes to use.
+  TLSOptions tls = 3;
+
+  // The loopback IP endpoint or Unix domain socket to which traffic should
+  // be forwarded to by default. Format should be `127.0.0.1:PORT` or
+  // `unix:///path/to/socket` or `unix://@foobar` (Linux abstract namespace).
+  string default_endpoint = 5;
+}
+
+
+// Port describes the properties of a specific port of a service.
+message Port {
+  // REQUIRED: A valid non-negative integer port number.
+  uint32 number = 1;
+
+  // REQUIRED: The protocol exposed on the port.
+  // MUST BE one of HTTP|HTTPS|GRPC|HTTP2|MONGO|TCP|TLS.
+  // TLS implies the connection will be routed based on the SNI header to
+  // the destination without terminating the TLS connection.
+  string protocol = 2;
+
+  // Label assigned to the port.
+  string name = 3;
+}
diff --git a/internal/protobuf/types.go b/internal/protobuf/types.go
new file mode 100644
index 0000000..99e5af4
--- /dev/null
+++ b/internal/protobuf/types.go
@@ -0,0 +1,76 @@
+// Copyright 2019 CUE Authors
+//
+// 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
+//
+//     http://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 protobuf
+
+import "github.com/emicklei/proto"
+
+func protoToCUE(typ string, options []*proto.Option) (ref string, ok bool) {
+	t, ok := scalars[typ]
+	return t, ok
+}
+
+var scalars = map[string]string{
+	// Differing
+	"sint32":   "int32",
+	"sint64":   "int64",
+	"fixed32":  "uint32",
+	"fixed64":  "uint64",
+	"sfixed32": "int32",
+	"sfixed64": "int64",
+
+	// Identical to CUE
+	"int32":  "int32",
+	"int64":  "int64",
+	"uint32": "uint32",
+	"uint64": "uint64",
+
+	"double": "float64",
+	"float":  "float32",
+
+	"bool":   "bool",
+	"string": "string",
+	"bytes":  "bytes",
+}
+
+var timePkg = &protoConverter{
+	goPkg:     "time",
+	goPkgPath: "time",
+}
+
+func (p *protoConverter) setBuiltin(from, to string, pkg *protoConverter) {
+	p.scope[0][from] = mapping{to, pkg}
+}
+
+func (p *protoConverter) mustBuiltinPackage(file string) {
+	// Map some builtin types to their JSON/CUE mappings.
+	switch file {
+	case "gogoproto/gogo.proto":
+
+	case "google/protobuf/duration.proto":
+		p.setBuiltin("google.protobuf.Duration", "time.Duration", timePkg)
+
+	case "google/protobuf/timestamp.proto":
+		p.setBuiltin("google.protobuf.Timestamp", "time.Time", timePkg)
+
+	case "google/protobuf/any.proto":
+		p.setBuiltin("google.protobuf.Any", "_", nil)
+
+	case "google/protobuf/empty.proto":
+		p.setBuiltin("google.protobuf.Empty", "{}", nil)
+
+	default:
+		failf("import %q not found", file)
+	}
+}
diff --git a/internal/protobuf/util.go b/internal/protobuf/util.go
new file mode 100644
index 0000000..e001fa8
--- /dev/null
+++ b/internal/protobuf/util.go
@@ -0,0 +1,89 @@
+// Copyright 2019 CUE Authors
+//
+// 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
+//
+//     http://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 protobuf
+
+import (
+	"strings"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/token"
+	"github.com/emicklei/proto"
+	"golang.org/x/xerrors"
+)
+
+// failf panics with a marked error that can be intercepted upon returning
+// from parsing.
+func failf(format string, args ...interface{}) {
+	panic(protoError{xerrors.Errorf(format, args...)})
+}
+
+func fail(err error) {
+	panic(protoError{err})
+}
+
+type protoError struct {
+	error
+}
+
+var (
+	newline    = token.Pos(token.Newline)
+	newSection = token.Pos(token.NewSection)
+)
+
+func addComments(f ast.Node, i int, doc, inline *proto.Comment) bool {
+	cg := comment(doc, true)
+	if cg != nil && len(cg.List) > 0 && i > 0 {
+		cg.List[0].Slash = newSection
+	}
+	f.AddComment(cg)
+	f.AddComment(comment(inline, false))
+	return doc != nil
+}
+
+func comment(c *proto.Comment, doc bool) *ast.CommentGroup {
+	if c == nil || len(c.Lines) == 0 {
+		return nil
+	}
+	cg := &ast.CommentGroup{}
+	if doc {
+		cg.Doc = true
+	} else {
+		cg.Line = true
+		cg.Position = 10
+	}
+	for _, s := range c.Lines {
+		cg.List = append(cg.List, &ast.Comment{Text: "// " + s})
+	}
+	return cg
+}
+
+func quote(s string) string {
+	if !strings.ContainsAny(s, `"\`) {
+		return s
+	}
+	esc := `\#`
+	for strings.Contains(s, esc) {
+		esc += "#"
+	}
+	return esc[1:] + `"` + s + `"` + esc[1:]
+}
+
+func labelName(s string) string {
+	split := strings.Split(s, "_")
+	for i := 1; i < len(split); i++ {
+		split[i] = strings.Title(split[i])
+	}
+	return strings.Join(split, "")
+}