cuelang.org/go@v0.10.1/encoding/protobuf/protobuf.go (about)

     1  // Copyright 2019 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package protobuf defines functionality for parsing protocol buffer
    16  // definitions and instances.
    17  //
    18  // Proto definition mapping follows the guidelines of mapping Proto to JSON as
    19  // discussed in https://developers.google.com/protocol-buffers/docs/proto3, and
    20  // carries some of the mapping further when possible with CUE.
    21  //
    22  // # Package Paths
    23  //
    24  // If a .proto file contains a go_package directive, it will be used as the
    25  // destination package for the generated .cue files. A common use case is to
    26  // generate the CUE in the same directory as the .proto definition. If a
    27  // destination package is not within the current CUE module, it will be written
    28  // relative to the pkg directory.
    29  //
    30  // If a .proto file does not specify go_package, it will convert a proto package
    31  // "google.parent.sub" to the import path "googleapis.com/google/parent/sub".
    32  // It is safe to mix packages with and without a go_package within the same
    33  // project.
    34  //
    35  // # Type Mappings
    36  //
    37  // The following type mappings of definitions apply:
    38  //
    39  //	Proto type     CUE type/def     Comments
    40  //	message        struct           Message fields become CUE fields, whereby
    41  //	                                names are mapped to lowerCamelCase.
    42  //	enum           e1 | e2 | ...    Where ex are strings. A separate mapping is
    43  //	                                generated to obtain the numeric values.
    44  //	map<K, V>      { <>: V }        All keys are converted to strings.
    45  //	repeated V     [...V]           null is accepted as the empty list [].
    46  //	bool           bool
    47  //	string         string
    48  //	bytes          bytes            A base64-encoded string when converted to JSON.
    49  //	int32, fixed32 int32            An integer with bounds as defined by int32.
    50  //	uint32         uint32           An integer with bounds as defined by uint32.
    51  //	int64, fixed64 int64            An integer with bounds as defined by int64.
    52  //	uint64         uint64           An integer with bounds as defined by uint64.
    53  //	float          float32          A number with bounds as defined by float32.
    54  //	double         float64          A number with bounds as defined by float64.
    55  //	Struct         struct           See struct.proto.
    56  //	Value          _                See struct.proto.
    57  //	ListValue      [...]            See struct.proto.
    58  //	NullValue      null             See struct.proto.
    59  //	BoolValue      bool             See struct.proto.
    60  //	StringValue    string           See struct.proto.
    61  //	NumberValue    number           See struct.proto.
    62  //	StringValue    string           See struct.proto.
    63  //	Empty          close({})
    64  //	Timestamp      time.Time        See struct.proto.
    65  //	Duration       time.Duration    See struct.proto.
    66  //
    67  // # Annotations
    68  //
    69  // Protobuf definitions can be annotated with CUE constraints that are included
    70  // in the generated CUE:
    71  //
    72  //	(cue.val)     string        CUE expression defining a constraint for this
    73  //	                            field. The string may refer to other fields
    74  //	                            in a message definition using their JSON name.
    75  //
    76  //	(cue.opt)     FieldOptions
    77  //	   required   bool          Defines the field is required. Use with
    78  //	                            caution.
    79  package protobuf
    80  
    81  // TODO mappings:
    82  //
    83  // Wrapper types	various types	2, "2", "foo", true, "true", null, 0, …	Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer.
    84  // FieldMask	string	"f.fooBar,h"	See field_mask.proto.
    85  //   Any            {"@type":"url",  See struct.proto.
    86  //                   f1: value,
    87  //                   ...}
    88  
    89  import (
    90  	"os"
    91  	"path/filepath"
    92  	"slices"
    93  	"strings"
    94  
    95  	"cuelang.org/go/cue/ast"
    96  	"cuelang.org/go/cue/build"
    97  	"cuelang.org/go/cue/errors"
    98  	"cuelang.org/go/cue/format"
    99  	"cuelang.org/go/cue/parser"
   100  	"cuelang.org/go/cue/token"
   101  	"cuelang.org/go/internal"
   102  	"cuelang.org/go/mod/module"
   103  
   104  	// Generated protobuf CUE may use builtins. Ensure that these can always be
   105  	// found, even if the user does not use cue/load or another package that
   106  	// triggers its loading.
   107  	//
   108  	// TODO: consider whether just linking in the necessary packages suffices.
   109  	// It probably does, but this may reorder some of the imports, which may,
   110  	// in turn, change the numbering, which can be confusing while debugging.
   111  	_ "cuelang.org/go/pkg"
   112  )
   113  
   114  // Config specifies the environment into which to parse a proto definition file.
   115  type Config struct {
   116  	// Root specifies the root of the CUE project, which typically coincides
   117  	// with, for example, a version control repository root or the Go module.
   118  	// Any imports of proto files within the directory tree of this of this root
   119  	// are considered to be "project files" and are generated at the
   120  	// corresponding location with this hierarchy. Any other imports are
   121  	// considered to be external. Files for such imports are rooted under the
   122  	// $Root/pkg/, using the Go package path specified in the .proto file.
   123  	Root string
   124  
   125  	// Module is the Go package import path of the module root. It is the value
   126  	// as after "module" in a cue.mod/modules.cue file, if a module file is
   127  	// present.
   128  	Module string // TODO: determine automatically if unspecified.
   129  
   130  	// Paths defines the include directory in which to search for imports.
   131  	Paths []string
   132  
   133  	// PkgName specifies the package name for a generated CUE file. A value
   134  	// will be derived from the Go package name if undefined.
   135  	PkgName string
   136  
   137  	// EnumMode defines whether enums should be set as integer values, instead
   138  	// of strings.
   139  	//
   140  	//    json    value is a string, corresponding to the standard JSON mapping
   141  	//            of Protobuf. The value is associated with a #enumValue
   142  	//            to allow the json+pb interpretation to interpret integers
   143  	//            as well.
   144  	//
   145  	//    int     value is an integer associated with an #enumValue definition
   146  	//            The json+pb interpreter uses the definition names in the
   147  	//            disjunction of the enum to interpret strings.
   148  	//
   149  	EnumMode string
   150  }
   151  
   152  // An Extractor converts a collection of proto files, typically belonging to one
   153  // repo or module, to CUE. It thereby observes the CUE package layout.
   154  //
   155  // CUE observes the same package layout as Go and requires .proto files to have
   156  // the go_package directive. Generated CUE files are put in the same directory
   157  // as their corresponding .proto files if the .proto files are located in the
   158  // specified Root (or current working directory if none is specified).
   159  // All other imported files are assigned to the CUE pkg dir ($Root/pkg)
   160  // according to their Go package import path.
   161  type Extractor struct {
   162  	root     string
   163  	cwd      string
   164  	module   string
   165  	paths    []string
   166  	pkgName  string
   167  	enumMode string
   168  
   169  	fileCache map[string]result
   170  	imports   map[string]*build.Instance
   171  
   172  	errs errors.Error
   173  	done bool
   174  }
   175  
   176  type result struct {
   177  	p   *protoConverter
   178  	err error
   179  }
   180  
   181  // NewExtractor creates an Extractor. If the configuration contained any errors
   182  // it will be observable by the Err method fo the Extractor. It is safe,
   183  // however, to only check errors after building the output.
   184  func NewExtractor(c *Config) *Extractor {
   185  	var modulePath string
   186  	// We don't want to consider the module's major version as
   187  	// part of the path when checking to see a protobuf package
   188  	// declares itself as part of that module.
   189  	// TODO(rogpeppe) the Go package path might itself include a major
   190  	// version, so we should probably consider that too.
   191  	if c.Module != "" {
   192  		var ok bool
   193  		modulePath, _, ok = module.SplitPathVersion(c.Module)
   194  		if !ok {
   195  			modulePath = c.Module
   196  
   197  		}
   198  	}
   199  	cwd, _ := os.Getwd()
   200  	b := &Extractor{
   201  		root:      c.Root,
   202  		cwd:       cwd,
   203  		paths:     c.Paths,
   204  		pkgName:   c.PkgName,
   205  		module:    modulePath,
   206  		enumMode:  c.EnumMode,
   207  		fileCache: map[string]result{},
   208  		imports:   map[string]*build.Instance{},
   209  	}
   210  
   211  	if b.root == "" {
   212  		b.root = b.cwd
   213  	}
   214  
   215  	return b
   216  }
   217  
   218  // Err returns the errors accumulated during testing. The returned error may be
   219  // of type cuelang.org/go/cue/errors.List.
   220  func (b *Extractor) Err() error {
   221  	return b.errs
   222  }
   223  
   224  func (b *Extractor) addErr(err error) {
   225  	b.errs = errors.Append(b.errs, errors.Promote(err, "unknown error"))
   226  }
   227  
   228  // AddFile adds a proto definition file to be converted into CUE by the builder.
   229  // Relatives paths are always taken relative to the Root with which the b is
   230  // configured.
   231  //
   232  // AddFile assumes that the proto file compiles with protoc and may not report
   233  // an error if it does not. Imports are resolved using the paths defined in
   234  // Config.
   235  func (b *Extractor) AddFile(filename string, src interface{}) error {
   236  	if b.done {
   237  		err := errors.Newf(token.NoPos,
   238  			"protobuf: cannot call AddFile: Instances was already called")
   239  		b.errs = errors.Append(b.errs, err)
   240  		return err
   241  	}
   242  	if b.root != b.cwd && !filepath.IsAbs(filename) {
   243  		filename = filepath.Join(b.root, filename)
   244  	}
   245  	_, err := b.parse(filename, src)
   246  	return err
   247  }
   248  
   249  // TODO: some way of (recursively) adding multiple proto files with filter.
   250  
   251  // Files returns a File for each proto file that was added or imported,
   252  // recursively.
   253  func (b *Extractor) Files() (files []*ast.File, err error) {
   254  	defer func() { err = b.Err() }()
   255  	b.done = true
   256  
   257  	instances, err := b.Instances()
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	for _, p := range instances {
   263  		files = append(files, p.Files...)
   264  	}
   265  	return files, nil
   266  }
   267  
   268  // Instances creates a build.Instances for every package for which a proto file
   269  // was added to the builder. This includes transitive dependencies. It does not
   270  // write the generated files to disk.
   271  //
   272  // The returned instances can be passed to cue.Build to generated the
   273  // corresponding CUE instances.
   274  //
   275  // All import paths are located within the specified Root, where external
   276  // packages are located under $Root/pkg. Instances for builtin (like time)
   277  // packages may be omitted, and if not will have no associated files.
   278  func (b *Extractor) Instances() (instances []*build.Instance, err error) {
   279  	defer func() { err = b.Err() }()
   280  	b.done = true
   281  
   282  	for _, r := range b.fileCache {
   283  		if r.err != nil {
   284  			b.addErr(r.err)
   285  			continue
   286  		}
   287  		inst := b.getInst(r.p)
   288  		if inst == nil {
   289  			continue
   290  		}
   291  
   292  		// Set canonical CUE path for generated file.
   293  		f := r.p.file
   294  		base := filepath.Base(f.Filename)
   295  		base = base[:len(base)-len(".proto")] + "_proto_gen.cue"
   296  		f.Filename = filepath.Join(inst.Dir, base)
   297  		buf, err := format.Node(f)
   298  		if err != nil {
   299  			b.addErr(err)
   300  			// return nil, err
   301  			continue
   302  		}
   303  		f, err = parser.ParseFile(f.Filename, buf, parser.ParseComments)
   304  		if err != nil {
   305  			b.addErr(err)
   306  			continue
   307  		}
   308  
   309  		inst.Files = append(inst.Files, f)
   310  
   311  		for pkg := range r.p.imported {
   312  			inst.ImportPaths = append(inst.ImportPaths, pkg)
   313  		}
   314  	}
   315  
   316  	for _, p := range b.imports {
   317  		instances = append(instances, p)
   318  		slices.Sort(p.ImportPaths)
   319  		p.ImportPaths = slices.Compact(p.ImportPaths)
   320  		for _, i := range p.ImportPaths {
   321  			if imp := b.imports[i]; imp != nil {
   322  				p.Imports = append(p.Imports, imp)
   323  			}
   324  		}
   325  
   326  		slices.SortFunc(p.Files, func(a, b *ast.File) int {
   327  			return strings.Compare(a.Filename, b.Filename)
   328  		})
   329  	}
   330  	slices.SortFunc(instances, func(a, b *build.Instance) int {
   331  		return strings.Compare(a.ImportPath, b.ImportPath)
   332  	})
   333  
   334  	if err != nil {
   335  		return instances, err
   336  	}
   337  	return instances, nil
   338  }
   339  
   340  func (b *Extractor) getInst(p *protoConverter) *build.Instance {
   341  	if b.errs != nil {
   342  		return nil
   343  	}
   344  	importPath := p.qualifiedImportPath()
   345  	if importPath == "" {
   346  		err := errors.Newf(token.NoPos,
   347  			"no package clause for proto package %q in file %s", p.id, p.file.Filename)
   348  		b.errs = errors.Append(b.errs, err)
   349  		// TODO: find an alternative. Is proto package good enough?
   350  		return nil
   351  	}
   352  
   353  	dir := b.root
   354  	path := p.importPath()
   355  	file := p.file.Filename
   356  	if !filepath.IsAbs(file) {
   357  		file = filepath.Join(b.root, p.file.Filename)
   358  	}
   359  	// Determine whether the generated file should be included in place, or
   360  	// within cue.mod.
   361  	inPlace := strings.HasPrefix(file, b.root)
   362  	if !strings.HasPrefix(path, b.module) {
   363  		// b.module is either "", in which case we assume the setting for
   364  		// inPlace, or not, in which case the module in the protobuf must
   365  		// correspond with that of the proto package.
   366  		inPlace = false
   367  	}
   368  	if !inPlace {
   369  		dir = filepath.Join(internal.GenPath(dir), path)
   370  	} else {
   371  		dir = filepath.Dir(p.file.Filename)
   372  	}
   373  
   374  	// TODO: verify module name from go_package option against that of actual
   375  	// CUE module. Maybe keep this old code for some strict mode?
   376  	// want := filepath.Dir(p.file.Filename)
   377  	// dir = filepath.Join(dir, path[len(b.module)+1:])
   378  	// if !filepath.IsAbs(want) {
   379  	// 	want = filepath.Join(b.root, want)
   380  	// }
   381  	// if dir != want {
   382  	// 	err := errors.Newf(token.NoPos,
   383  	// 		"file %s mapped to inconsistent path %s; module name %q may be inconsistent with root dir %s",
   384  	// 		want, dir, b.module, b.root,
   385  	// 	)
   386  	// 	b.errs = errors.Append(b.errs, err)
   387  	// }
   388  
   389  	inst := b.imports[importPath]
   390  	if inst == nil {
   391  		inst = &build.Instance{
   392  			Root:        b.root,
   393  			Dir:         dir,
   394  			ImportPath:  importPath,
   395  			PkgName:     p.shortPkgName,
   396  			DisplayPath: p.protoPkg,
   397  		}
   398  		b.imports[importPath] = inst
   399  	}
   400  	return inst
   401  }
   402  
   403  // Extract parses a single proto file and returns its contents translated to a CUE
   404  // file. If src is not nil, it will use this as the contents of the file. It may
   405  // be a string, []byte or io.Reader. Otherwise Extract will open the given file
   406  // name at the fully qualified path.
   407  //
   408  // Extract assumes the proto file compiles with protoc and may not report an error
   409  // if it does not. Imports are resolved using the paths defined in Config.
   410  func Extract(filename string, src interface{}, c *Config) (f *ast.File, err error) {
   411  	if c == nil {
   412  		c = &Config{}
   413  	}
   414  	b := NewExtractor(c)
   415  
   416  	p, err := b.parse(filename, src)
   417  	if err != nil {
   418  		return nil, err
   419  	}
   420  	p.file.Filename = filename[:len(filename)-len(".proto")] + "_gen.cue"
   421  	return p.file, b.Err()
   422  }
   423  
   424  // TODO
   425  // func GenDefinition
   426  
   427  // func MarshalText(cue.Value) (string, error) {
   428  // 	return "", nil
   429  // }
   430  
   431  // func MarshalBytes(cue.Value) ([]byte, error) {
   432  // 	return nil, nil
   433  // }
   434  
   435  // func UnmarshalText(descriptor cue.Value, b string) (ast.Expr, error) {
   436  // 	return nil, nil
   437  // }
   438  
   439  // func UnmarshalBytes(descriptor cue.Value, b []byte) (ast.Expr, error) {
   440  // 	return nil, nil
   441  // }