cuelang.org/go@v0.13.0/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 103 // Generated protobuf CUE may use builtins. Ensure that these can always be 104 // found, even if the user does not use cue/load or another package that 105 // triggers its loading. 106 // 107 // TODO: consider whether just linking in the necessary packages suffices. 108 // It probably does, but this may reorder some of the imports, which may, 109 // in turn, change the numbering, which can be confusing while debugging. 110 _ "cuelang.org/go/pkg" 111 ) 112 113 // Config specifies the environment into which to parse a proto definition file. 114 type Config struct { 115 // Root specifies the root of the CUE project, which typically coincides 116 // with, for example, a version control repository root or the Go module. 117 // Any imports of proto files within the directory tree of this of this root 118 // are considered to be "project files" and are generated at the 119 // corresponding location with this hierarchy. Any other imports are 120 // considered to be external. Files for such imports are rooted under the 121 // $Root/pkg/, using the Go package path specified in the .proto file. 122 Root string 123 124 // Module is the Go package import path of the module root. It is the value 125 // as after "module" in a cue.mod/modules.cue file, if a module file is 126 // present. 127 Module string // TODO: determine automatically if unspecified. 128 129 // Paths defines the include directory in which to search for imports. 130 Paths []string 131 132 // PkgName specifies the package name for a generated CUE file. A value 133 // will be derived from the Go package name if undefined. 134 PkgName string 135 136 // EnumMode defines whether enums should be set as integer values, instead 137 // of strings. 138 // 139 // json value is a string, corresponding to the standard JSON mapping 140 // of Protobuf. The value is associated with a #enumValue 141 // to allow the json+pb interpretation to interpret integers 142 // as well. 143 // 144 // int value is an integer associated with an #enumValue definition 145 // The json+pb interpreter uses the definition names in the 146 // disjunction of the enum to interpret strings. 147 // 148 EnumMode string 149 } 150 151 // An Extractor converts a collection of proto files, typically belonging to one 152 // repo or module, to CUE. It thereby observes the CUE package layout. 153 // 154 // CUE observes the same package layout as Go and requires .proto files to have 155 // the go_package directive. Generated CUE files are put in the same directory 156 // as their corresponding .proto files if the .proto files are located in the 157 // specified Root (or current working directory if none is specified). 158 // All other imported files are assigned to the CUE pkg dir ($Root/pkg) 159 // according to their Go package import path. 160 type Extractor struct { 161 root string 162 cwd string 163 module string 164 paths []string 165 pkgName string 166 enumMode string 167 168 fileCache map[string]result 169 imports map[string]*build.Instance 170 171 errs errors.Error 172 done bool 173 } 174 175 type result struct { 176 p *protoConverter 177 err error 178 } 179 180 // NewExtractor creates an Extractor. If the configuration contained any errors 181 // it will be observable by the Err method fo the Extractor. It is safe, 182 // however, to only check errors after building the output. 183 func NewExtractor(c *Config) *Extractor { 184 var modulePath string 185 // We don't want to consider the module's major version as 186 // part of the path when checking to see a protobuf package 187 // declares itself as part of that module. 188 // TODO(rogpeppe) the Go package path might itself include a major 189 // version, so we should probably consider that too. 190 if c.Module != "" { 191 modulePath, _, _ = ast.SplitPackageVersion(c.Module) 192 } 193 cwd, _ := os.Getwd() 194 b := &Extractor{ 195 root: c.Root, 196 cwd: cwd, 197 paths: c.Paths, 198 pkgName: c.PkgName, 199 module: modulePath, 200 enumMode: c.EnumMode, 201 fileCache: map[string]result{}, 202 imports: map[string]*build.Instance{}, 203 } 204 205 if b.root == "" { 206 b.root = b.cwd 207 } 208 209 return b 210 } 211 212 // Err returns the errors accumulated during testing. The returned error may be 213 // of type [errors.List]. 214 func (b *Extractor) Err() error { 215 return b.errs 216 } 217 218 func (b *Extractor) addErr(err error) { 219 b.errs = errors.Append(b.errs, errors.Promote(err, "unknown error")) 220 } 221 222 // AddFile adds a proto definition file to be converted into CUE by the builder. 223 // Relatives paths are always taken relative to the Root with which the b is 224 // configured. 225 // 226 // AddFile assumes that the proto file compiles with protoc and may not report 227 // an error if it does not. Imports are resolved using the paths defined in 228 // Config. 229 func (b *Extractor) AddFile(filename string, src interface{}) error { 230 if b.done { 231 err := errors.Newf(token.NoPos, 232 "protobuf: cannot call AddFile: Instances was already called") 233 b.errs = errors.Append(b.errs, err) 234 return err 235 } 236 if b.root != b.cwd && !filepath.IsAbs(filename) { 237 filename = filepath.Join(b.root, filename) 238 } 239 _, err := b.parse(filename, src) 240 return err 241 } 242 243 // TODO: some way of (recursively) adding multiple proto files with filter. 244 245 // Files returns a File for each proto file that was added or imported, 246 // recursively. 247 func (b *Extractor) Files() (files []*ast.File, err error) { 248 defer func() { err = b.Err() }() 249 b.done = true 250 251 instances, err := b.Instances() 252 if err != nil { 253 return nil, err 254 } 255 256 for _, p := range instances { 257 files = append(files, p.Files...) 258 } 259 return files, nil 260 } 261 262 // Instances creates a build.Instances for every package for which a proto file 263 // was added to the builder. This includes transitive dependencies. It does not 264 // write the generated files to disk. 265 // 266 // The returned instances can be passed to cue.Build to generated the 267 // corresponding CUE instances. 268 // 269 // All import paths are located within the specified Root, where external 270 // packages are located under $Root/pkg. Instances for builtin (like time) 271 // packages may be omitted, and if not will have no associated files. 272 func (b *Extractor) Instances() (instances []*build.Instance, err error) { 273 defer func() { err = b.Err() }() 274 b.done = true 275 276 for _, r := range b.fileCache { 277 if r.err != nil { 278 b.addErr(r.err) 279 continue 280 } 281 inst := b.getInst(r.p) 282 if inst == nil { 283 continue 284 } 285 286 // Set canonical CUE path for generated file. 287 f := r.p.file 288 base := filepath.Base(f.Filename) 289 base = base[:len(base)-len(".proto")] + "_proto_gen.cue" 290 f.Filename = filepath.Join(inst.Dir, base) 291 buf, err := format.Node(f) 292 if err != nil { 293 b.addErr(err) 294 // return nil, err 295 continue 296 } 297 f, err = parser.ParseFile(f.Filename, buf, parser.ParseComments) 298 if err != nil { 299 b.addErr(err) 300 continue 301 } 302 303 inst.Files = append(inst.Files, f) 304 305 for pkg := range r.p.imported { 306 inst.ImportPaths = append(inst.ImportPaths, pkg) 307 } 308 } 309 310 for _, p := range b.imports { 311 instances = append(instances, p) 312 slices.Sort(p.ImportPaths) 313 p.ImportPaths = slices.Compact(p.ImportPaths) 314 for _, i := range p.ImportPaths { 315 if imp := b.imports[i]; imp != nil { 316 p.Imports = append(p.Imports, imp) 317 } 318 } 319 320 slices.SortFunc(p.Files, func(a, b *ast.File) int { 321 return strings.Compare(a.Filename, b.Filename) 322 }) 323 } 324 slices.SortFunc(instances, func(a, b *build.Instance) int { 325 return strings.Compare(a.ImportPath, b.ImportPath) 326 }) 327 328 if err != nil { 329 return instances, err 330 } 331 return instances, nil 332 } 333 334 func (b *Extractor) getInst(p *protoConverter) *build.Instance { 335 if b.errs != nil { 336 return nil 337 } 338 importPath := p.qualifiedImportPath() 339 if importPath == "" { 340 err := errors.Newf(token.NoPos, 341 "no package clause for proto package %q in file %s", p.id, p.file.Filename) 342 b.errs = errors.Append(b.errs, err) 343 // TODO: find an alternative. Is proto package good enough? 344 return nil 345 } 346 347 dir := b.root 348 path := p.importPath() 349 file := p.file.Filename 350 if !filepath.IsAbs(file) { 351 file = filepath.Join(b.root, p.file.Filename) 352 } 353 // Determine whether the generated file should be included in place, or 354 // within cue.mod. 355 inPlace := strings.HasPrefix(file, b.root) 356 if !strings.HasPrefix(path, b.module) { 357 // b.module is either "", in which case we assume the setting for 358 // inPlace, or not, in which case the module in the protobuf must 359 // correspond with that of the proto package. 360 inPlace = false 361 } 362 if !inPlace { 363 dir = filepath.Join(internal.GenPath(dir), path) 364 } else { 365 dir = filepath.Dir(p.file.Filename) 366 } 367 368 // TODO: verify module name from go_package option against that of actual 369 // CUE module. Maybe keep this old code for some strict mode? 370 // want := filepath.Dir(p.file.Filename) 371 // dir = filepath.Join(dir, path[len(b.module)+1:]) 372 // if !filepath.IsAbs(want) { 373 // want = filepath.Join(b.root, want) 374 // } 375 // if dir != want { 376 // err := errors.Newf(token.NoPos, 377 // "file %s mapped to inconsistent path %s; module name %q may be inconsistent with root dir %s", 378 // want, dir, b.module, b.root, 379 // ) 380 // b.errs = errors.Append(b.errs, err) 381 // } 382 383 inst := b.imports[importPath] 384 if inst == nil { 385 inst = &build.Instance{ 386 Root: b.root, 387 Dir: dir, 388 ImportPath: importPath, 389 PkgName: p.shortPkgName, 390 DisplayPath: p.protoPkg, 391 } 392 b.imports[importPath] = inst 393 } 394 return inst 395 } 396 397 // Extract parses a single proto file and returns its contents translated to a CUE 398 // file. If src is not nil, it will use this as the contents of the file. It may 399 // be a string, []byte or [io.Reader]. Otherwise Extract will open the given file 400 // name at the fully qualified path. 401 // 402 // Extract assumes the proto file compiles with protoc and may not report an error 403 // if it does not. Imports are resolved using the paths defined in Config. 404 func Extract(filename string, src interface{}, c *Config) (f *ast.File, err error) { 405 if c == nil { 406 c = &Config{} 407 } 408 b := NewExtractor(c) 409 410 p, err := b.parse(filename, src) 411 if err != nil { 412 return nil, err 413 } 414 p.file.Filename = filename[:len(filename)-len(".proto")] + "_gen.cue" 415 return p.file, b.Err() 416 } 417 418 // TODO 419 // func GenDefinition 420 421 // func MarshalText(cue.Value) (string, error) { 422 // return "", nil 423 // } 424 425 // func MarshalBytes(cue.Value) ([]byte, error) { 426 // return nil, nil 427 // } 428 429 // func UnmarshalText(descriptor cue.Value, b string) (ast.Expr, error) { 430 // return nil, nil 431 // } 432 433 // func UnmarshalBytes(descriptor cue.Value, b []byte) (ast.Expr, error) { 434 // return nil, nil 435 // }