diff --git a/cmd/cue/cmd/flags.go b/cmd/cue/cmd/flags.go index 935a3635c..e5e25883f 100644 --- a/cmd/cue/cmd/flags.go +++ b/cmd/cue/cmd/flags.go @@ -43,6 +43,7 @@ const ( flagPath flagName = "path" flagFiles flagName = "files" flagProtoPath flagName = "proto_path" + flagProtoEnum flagName = "proto_enum" flagWithContext flagName = "with-context" flagOut flagName = "out" flagOutFile flagName = "outfile" @@ -79,6 +80,7 @@ func addOrphanFlags(f *pflag.FlagSet) { f.Bool(string(flagList), false, "concatenate multiple objects into a list") f.Bool(string(flagWithContext), false, "import as object with contextual data") f.StringArrayP(string(flagProtoPath), "I", nil, "paths in which to search for imports") + f.String(string(flagProtoEnum), "int", "mode for rendering enums (int|json)") f.StringP(string(flagGlob), "n", "", "glob filter for file names") f.Bool(string(flagMerge), true, "merge non-CUE files") } diff --git a/cmd/cue/cmd/get_go.go b/cmd/cue/cmd/get_go.go index 4d493d3f9..7c5b46815 100644 --- a/cmd/cue/cmd/get_go.go +++ b/cmd/cue/cmd/get_go.go @@ -676,6 +676,12 @@ func (e *extractor) reportDecl(x *ast.GenDecl) (a []cueast.Decl) { typ := e.pkg.TypesInfo.TypeOf(v.Name) enums := e.consts[typ.String()] name := v.Name.Name + mapNamed := false + underlying := e.pkg.TypesInfo.TypeOf(v.Type) + if b, ok := underlying.Underlying().(*types.Basic); ok && b.Kind() != types.String { + mapNamed = true + } + switch tn, ok := e.pkg.TypesInfo.Defs[v.Name].(*types.TypeName); { case ok: if altType := e.altType(tn.Type()); altType != nil { @@ -695,8 +701,7 @@ func (e *extractor) reportDecl(x *ast.GenDecl) (a []cueast.Decl) { a = append(a, e.def(x.Doc, name, s, true)) break } - // TODO: only print original type if value is not marked as enum. - underlying := e.pkg.TypesInfo.TypeOf(v.Type) + f, _ := e.makeField(name, cuetoken.ISA, underlying, x.Doc, true) a = append(a, f) cueast.SetRelPos(f, cuetoken.NewSection) @@ -708,20 +713,44 @@ func (e *extractor) reportDecl(x *ast.GenDecl) (a []cueast.Decl) { cueast.AddComment(a[len(a)-1], internal.NewComment(false, enumName)) // Constants are mapped as definitions. - var x cueast.Expr = e.ident(enums[0], true) - cueast.SetRelPos(x, cuetoken.Newline) - for _, v := range enums[1:] { - y := e.ident(v, true) - cueast.SetRelPos(y, cuetoken.Newline) - x = cueast.NewBinExpr(cuetoken.OR, x, y) + var exprs []cueast.Expr + var named []cueast.Decl + for _, v := range enums { + label := cueast.NewString(v) + cueast.SetRelPos(label, cuetoken.Blank) + + var x cueast.Expr = e.ident(v, true) + cueast.SetRelPos(x, cuetoken.Newline) + exprs = append(exprs, x) + + if !mapNamed { + continue + } + + named = append(named, &cueast.Field{ + Label: label, + Value: e.ident(v, true), + }) + } + + addField := func(label string, exprs []cueast.Expr) { + f := &cueast.Field{ + Label: cueast.NewIdent(label), + Value: cueast.NewBinExpr(cuetoken.OR, exprs...), + } + cueast.SetRelPos(f, cuetoken.NewSection) + a = append(a, f) } - // a = append(a, e.def(nil, enumName, x, true)) - f := &cueast.Field{ - Label: cueast.NewIdent(enumName), - Value: x, + + addField(enumName, exprs) + if len(named) > 0 { + f := &cueast.Field{ + Label: cueast.NewIdent("#values_" + name), + Value: &cueast.StructLit{Elts: named}, + } + cueast.SetRelPos(f, cuetoken.NewSection) + a = append(a, f) } - a = append(a, f) - cueast.SetRelPos(f, cuetoken.NewSection) } } diff --git a/cmd/cue/cmd/import.go b/cmd/cue/cmd/import.go index a981eb689..cd417c53e 100644 --- a/cmd/cue/cmd/import.go +++ b/cmd/cue/cmd/import.go @@ -334,10 +334,11 @@ func protoMode(b *buildPlan) error { } c := &protobuf.Config{ - Root: root, - Module: module, - Paths: b.encConfig.ProtoPath, - PkgName: b.encConfig.PkgName, + Root: root, + Module: module, + Paths: b.encConfig.ProtoPath, + PkgName: b.encConfig.PkgName, + EnumMode: flagProtoEnum.String(b.cmd), } if module != "" { // We only allow imports from packages within the module if an actual diff --git a/cmd/cue/cmd/testdata/script/get_go_types.txt b/cmd/cue/cmd/testdata/script/get_go_types.txt index a22cc6680..f12366910 100644 --- a/cmd/cue/cmd/testdata/script/get_go_types.txt +++ b/cmd/cue/cmd/testdata/script/get_go_types.txt @@ -120,6 +120,13 @@ const ( High ) +type Level2 Level + +const ( + AnotherLevel1 Level2 = iota + 4 + AnotherLevel2 +) + type CustomJSON struct { } @@ -432,6 +439,13 @@ _#internalIdentifier: #Identifier & "internal" #Medium | #High +#values_Level: { + Unknown: #Unknown + Low: #Low + Medium: #Medium + High: #High +} + // Block comment. // Indented. // @@ -443,6 +457,20 @@ _#internalIdentifier: #Identifier & "internal" #Medium: #Level & 2 #High: #Level & 3 +#Level2: #Level // #enumLevel2 + +#enumLevel2: + #AnotherLevel1 | + #AnotherLevel2 + +#values_Level2: { + AnotherLevel1: #AnotherLevel1 + AnotherLevel2: #AnotherLevel2 +} + +#AnotherLevel1: #Level2 & 4 +#AnotherLevel2: #Level2 & 5 + #CustomJSON: _ #CustomYAML: { diff --git a/cmd/cue/cmd/testdata/script/import_proto.txt b/cmd/cue/cmd/testdata/script/import_proto.txt index dd21a60a7..a7a640065 100644 --- a/cmd/cue/cmd/testdata/script/import_proto.txt +++ b/cmd/cue/cmd/testdata/script/import_proto.txt @@ -37,6 +37,11 @@ option go_package = "acme.com/api/mixer/v1"; // Attributes defines attributes. message Attributes { + enum Type { + TYPE_A = 1; + TYPE_B = 2; + } + // A map of attribute name to its value. map attributes = 1; @@ -171,6 +176,18 @@ import ( // Attributes defines attributes. #Attributes: { + #Type: + #TYPE_A | + #TYPE_B + + #TYPE_A: 1 + #TYPE_B: 2 + + #Type_value: { + TYPE_A: 1 + TYPE_B: 2 + } + // A map of attribute name to its value. attributes?: { [string]: #AttributeValue diff --git a/cmd/cue/cmd/testdata/script/import_proto2.txt b/cmd/cue/cmd/testdata/script/import_proto2.txt new file mode 100644 index 000000000..d752e5632 --- /dev/null +++ b/cmd/cue/cmd/testdata/script/import_proto2.txt @@ -0,0 +1,242 @@ +cd root +cue import proto -I ../include ./... --proto_enum=json +cd .. + +cmp stderr expect-stderr +cmp stdout expect-stdout +cmp root/mixer/v1/attributes_proto_gen.cue expect-attributes_proto_gen.cue +cmp root/mixer/v1/config/client/client_config_proto_gen.cue expect-client_config_proto_gen.cue +cmp root/cue.mod/gen/googleapis.com/acme/test/test_proto_gen.cue expect-test_proto_gen.cue + +-- expect-stdout -- +-- expect-stderr -- +Skipping file "cue.mod/gen/googleapis.com/acme/test/test/test_proto_gen.cue": already exists. +Use -Rf to override. +-- root/cue.mod/module.cue -- +module: "acme.com/api" + +-- root/cue.mod/module.cue -- +module: "acme.com/api" + +-- root/cue.mod/gen/googleapis.com/acme/test/test/test_proto_gen.cue -- +package test_test + +#AnotherTest: { + test?: int32 @protobuf(1) +} +-- root/mixer/v1/attributes.proto -- +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +import "acme/test.proto"; +import "acme/test/test.proto"; + +package acme.mixer.v1; + +option go_package = "acme.com/api/mixer/v1"; + +// Attributes defines attributes. +message Attributes { + enum Type { + TYPE_A = 1; + TYPE_B = 2; + } + + // A map of attribute name to its value. + map attributes = 1; + + // Specifies one attribute value with different type. + message AttributeValue { + // The attribute value. + oneof value { + string string_value = 2; + int64 int64_value = 3; + double double_value = 4; + bool bool_value = 5; + bytes bytes_value = 6; + google.protobuf.Timestamp timestamp_value = 7; + + // Used for values of type STRING_MAP + StringMap string_map_value = 9; + + acme.test.Test test_value = 10; + acme.test.test.AnotherTest test_value = 11; + } + } + + // Defines a string map. + message StringMap { + // Holds a set of name/value pairs. + map entries = 1; + } +} + +-- root/mixer/v1/config/client/client_config.proto -- +syntax = "proto3"; + +import "mixer/v1/attributes.proto"; + +// Describes the configuration state for the Mixer client library that's built into Envoy. +package acme.mixer.v1.config.client; + +option go_package = "acme.com/api/mixer/v1/config/client"; + +// Defines the per-service client configuration. +message ServiceConfig { + bool disable_check_calls = 1; + bool disable_report_calls = 2; + Attributes mixer_attributes = 3; +} +-- include/acme/test.proto -- +syntax = "proto3"; + +package acme.test; + +message Test { + int32 test = 1; +} + +-- include/acme/test/test.proto -- +syntax = "proto3"; + +package acme.test.test; + +// Override the short name only of this package. This notation is seen in some +// gogoproto files. +option go_package = "test_test" + +message AnotherTest { + int32 test = 1; +} + +-- include/google/protobuf/timestamp.proto -- +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option cc_enable_arenas = true; +option go_package = "github.com/golang/protobuf/ptypes/timestamp"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "TimestampProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; + +message Timestamp { + // Represents seconds of UTC time since Unix epoch + // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + // 9999-12-31T23:59:59Z inclusive. + int64 seconds = 1; + + // Non-negative fractions of a second at nanosecond resolution. Negative + // second values with fractions must still have non-negative nanos values + // that count forward in time. Must be from 0 to 999,999,999 + // inclusive. + int32 nanos = 2; +} + +-- expect-attributes_proto_gen.cue -- +package v1 + +import ( + "time" + "googleapis.com/acme/test" + "googleapis.com/acme/test/test:test_test" +) + +// Attributes defines attributes. +#Attributes: { + #Type: {"TYPE_A", #enumValue: 1} | + {"TYPE_B", #enumValue: 2} + + #Type_value: { + TYPE_A: 1 + TYPE_B: 2 + } + + // A map of attribute name to its value. + attributes?: { + [string]: #AttributeValue + } @protobuf(1,map[string]AttributeValue) + + // Specifies one attribute value with different type. + #AttributeValue: { + // The attribute value. + {} | { + stringValue: string @protobuf(2,string,name=string_value) + } | { + int64Value: int64 @protobuf(3,int64,name=int64_value) + } | { + doubleValue: float64 @protobuf(4,double,name=double_value) + } | { + boolValue: bool @protobuf(5,bool,name=bool_value) + } | { + bytesValue: bytes @protobuf(6,bytes,name=bytes_value) + } | { + timestampValue: time.Time @protobuf(7,google.protobuf.Timestamp,name=timestamp_value) + } | { + // Used for values of type STRING_MAP + stringMapValue: #StringMap @protobuf(9,StringMap,name=string_map_value) + } | { + testValue: test.#Test @protobuf(10,acme.test.Test,name=test_value) + } | { + testValue: test_test.#AnotherTest @protobuf(11,acme.test.test.AnotherTest,name=test_value) + } + } + + // Defines a string map. + #StringMap: { + // Holds a set of name/value pairs. + entries?: { + [string]: string + } @protobuf(1,map[string]string) + } +} +-- expect-client_config_proto_gen.cue -- +// Describes the configuration state for the Mixer client library that's built into Envoy. +package client + +import "acme.com/api/mixer/v1" + +// Defines the per-service client configuration. +#ServiceConfig: { + disableCheckCalls?: bool @protobuf(1,bool,name=disable_check_calls) + disableReportCalls?: bool @protobuf(2,bool,name=disable_report_calls) + mixerAttributes?: v1.#Attributes @protobuf(3,Attributes,name=mixer_attributes) +} +-- expect-test_proto_gen.cue -- +package test + +#Test: { + test?: int32 @protobuf(1,int32) +} diff --git a/cue/path.go b/cue/path.go index 11a7bca97..5afaf2a90 100644 --- a/cue/path.go +++ b/cue/path.go @@ -44,6 +44,11 @@ func (sel Selector) IsString() bool { return sel.sel.kind() == adt.StringLabel } +// IsDefinition reports whether sel is a non-hidden definition label type. +func (sel Selector) IsDefinition() bool { + return sel.sel.kind() == adt.DefinitionLabel +} + var ( // AnyField can be used to ask for any single label. // @@ -463,7 +468,7 @@ type pathError struct { errors.Error } -func (p pathError) String() string { return p.Error.Error() } +func (p pathError) String() string { return "" } func (p pathError) optional() bool { return false } func (p pathError) kind() adt.FeatureType { return 0 } func (p pathError) feature(r adt.Runtime) adt.Feature { diff --git a/encoding/protobuf/jsonpb/decoder.go b/encoding/protobuf/jsonpb/decoder.go index 7b616d37a..8f2362760 100644 --- a/encoding/protobuf/jsonpb/decoder.go +++ b/encoding/protobuf/jsonpb/decoder.go @@ -24,6 +24,7 @@ import ( "cuelang.org/go/cue/errors" "cuelang.org/go/cue/literal" "cuelang.org/go/cue/token" + "cuelang.org/go/encoding/protobuf/pbinternal" "github.com/cockroachdb/apd/v2" ) @@ -48,11 +49,14 @@ type Option func() // float: string values are interpreted as numbers, and the values "NaN", // "Infinity", and "-Infinity" are allowed and converted to // to corresponding error values. -// disjunction of strings: -// this is assumed to represent a protobuf enum value. Strings -// are left as is. For integers, the disjunction is resolved -// by converting it to the string that has a corresponding #intValue -// value. +// enums: if a field is of type int and does not have a standard integer +// type for its @protobuf attribute, this is assumed to represent +// a protobuf enum value. Enum names are converted to integers +// by interpreting the definitions of the disjunction constants +// as the symbol names. +// If CUE uses the string representation for enums, then an +// #enumValue integer associated with the string value is used +// for the conversion. // {}: JSON objects representing any values will be left as is. // If the CUE type corresponding to the URL can be determined within // the module context it will be unified. @@ -134,6 +138,8 @@ func (r *rewriter) rewriteDecls(schema cue.Value, decls []ast.Decl) { } } +var enumValuePath = cue.ParsePath("#enumValue").Optional() + func (r *rewriter) rewrite(schema cue.Value, expr ast.Expr) (x ast.Expr) { defer func() { if expr != x && x != nil { @@ -189,14 +195,16 @@ func (r *rewriter) rewrite(schema cue.Value, expr ast.Expr) (x ast.Expr) { } var info literal.NumInfo - if err := literal.ParseNum(str, &info); err != nil { + if err := literal.ParseNum(str, &info); err == nil { + x.Value = str + x.Kind = token.FLOAT + if info.IsInt() { + x.Kind = token.INT + } break } - x.Value = str - x.Kind = token.FLOAT - if info.IsInt() { - x.Kind = token.INT - } + + pbinternal.MatchBySymbol(schema, str, x) case cue.BytesKind: x, q, str := stringValue(expr) @@ -245,7 +253,7 @@ func (r *rewriter) rewrite(schema cue.Value, expr ast.Expr) (x ast.Expr) { values = []cue.Value{schema} // allow single values. } for _, v := range values { - i, err := v.LookupPath(cue.MakePath(cue.Def("#intValue"))).Int64() + i, err := v.LookupPath(enumValuePath).Int64() if err == nil && i == enum { str, err := v.String() if err != nil { diff --git a/encoding/protobuf/jsonpb/testdata/decoder/enums.txtar b/encoding/protobuf/jsonpb/testdata/decoder/enums.txtar index 5c4d1a2b6..e3c41595c 100644 --- a/encoding/protobuf/jsonpb/testdata/decoder/enums.txtar +++ b/encoding/protobuf/jsonpb/testdata/decoder/enums.txtar @@ -1,23 +1,50 @@ -- schema.cue -- -enum: [string]: { - "foo" - #intValue: 1 -} | { - "bar" - #intValue: 2 -} +enum: [string]: + { "foo", #enumValue: 1 } | + { "bar", #enumValue: 2 } -singleEnum: { "single", #intValue: 1 } +defEnum: [string]: #foo | #bar -badEnum: { string, #intValue: 1 } | { "two", #intValue: 2 } +#foo: 1 +#bar: 2 + +typeEnum: [string]: #Enum + +#Enum: #foo | #bar + + +// TODO: consider supporting @symbol(foo) or @json(,symbol=foo) +// symbolEnum: [string]: +// { 1, @symbol(foo) } | +// { 2, @symbol(bar) } + + +singleEnum: { "single", #enumValue: 1 } + +badEnum: { string, #enumValue: 1 } | { "two", #enumValue: 2 } -- data.cue -- enum: asIs: "foo" enum: asIsUnknown: "foobar" +// Convert integers to string enum: numExistFoo: 1 enum: numExistBar: 2 +// Convert strings to integer +defEnum: foo: "foo" +defEnum: bar: "bar" +defEnum: baz: "baz" // unavailable + +typeEnum: foo: "foo" +typeEnum: bar: "bar" +typeEnum: baz: "baz" // unavailable + +// TODO: consider supporting @symbol(foo) or @json(,symbol=foo) +// symbolEnum: foo: "foo" +// symbolEnum: bar: "bar" +// symbolEnum: baz: "baz" + singleEnum: 1 -- errors.cue -- @@ -33,9 +60,24 @@ badEnum: 1 enum: asIs: "foo" enum: asIsUnknown: "foobar" +// Convert integers to string enum: numExistFoo: "foo" enum: numExistBar: "bar" +// Convert strings to integer +defEnum: foo: 1 +defEnum: bar: 2 +defEnum: baz: "baz" // unavailable + +typeEnum: foo: 1 +typeEnum: bar: 2 +typeEnum: baz: "baz" // unavailable + +// TODO: consider supporting @symbol(foo) or @json(,symbol=foo) +// symbolEnum: foo: "foo" +// symbolEnum: bar: "bar" +// symbolEnum: baz: "baz" + singleEnum: "single" -- out/jsonpb/errors.cue -- enum.numNotExists: could not locate integer enum value 3: @@ -45,4 +87,4 @@ enum.numNotExists: could not locate integer enum value 4: enum.tooLarge: invalid enum index: 4111222333444555666777888999: greater than max int64: errors.cue:5:17 badEnum: invalid string enum: non-concrete value string: - schema.cue:11:10 + schema.cue:23:10 diff --git a/encoding/protobuf/parse.go b/encoding/protobuf/parse.go index a272c3479..2e8db8b32 100644 --- a/encoding/protobuf/parse.go +++ b/encoding/protobuf/parse.go @@ -596,6 +596,9 @@ func (p *protoConverter) enum(x *proto.Enum) { // Top-level enum entry. enum := &ast.Field{Label: name} addComments(enum, 1, x.Comment, nil) + if p.current != nil && len(p.current.Elts) > 0 { + ast.SetRelPos(enum, token.NewSection) + } // Top-level enum values entry. valueName := ast.NewIdent(name.Name + "_value") @@ -608,7 +611,6 @@ func (p *protoConverter) enum(x *proto.Enum) { panic(name.Name) } p.addDecl(enum) - p.addDecl(d) numEnums := 0 for _, v := range x.Elements { @@ -617,6 +619,10 @@ func (p *protoConverter) enum(x *proto.Enum) { } } + lastSingle := false + + firstSpace := token.NewSection + // The line comments for an enum field need to attach after the '|', which // is only known at the next iteration. var lastComment *proto.Comment @@ -624,41 +630,69 @@ func (p *protoConverter) enum(x *proto.Enum) { switch y := v.(type) { case *proto.EnumField: // Add enum value to map + intValue := ast.NewLit(token.INT, strconv.Itoa(y.Integer)) f := &ast.Field{ Label: p.stringLit(y.Position, y.Name), - Value: ast.NewLit(token.INT, strconv.Itoa(y.Integer)), + Value: intValue, } valueMap.Elts = append(valueMap.Elts, f) - // add to enum disjunction - value := p.stringLit(y.Position, y.Name) + var e ast.Expr + switch p.state.enumMode { + case "int": + e = ast.NewIdent("#" + y.Name) + ast.SetRelPos(e, token.Newline) - var e ast.Expr = value - // Make the first value the default value. - if i > 0 { - value.ValuePos = newline + f := &ast.Field{ + Label: ast.NewIdent("#" + y.Name), + Value: intValue, + } + ast.SetRelPos(f, firstSpace) + firstSpace = token.Newline + addComments(f, 0, y.Comment, y.InlineComment) + p.addDecl(f) + + case "", "json": + // add to enum disjunction + value := p.stringLit(y.Position, y.Name) + embed := &ast.EmbedDecl{Expr: value} + ast.SetRelPos(embed, token.Blank) + field := &ast.Field{Label: ast.NewIdent("#enumValue"), Value: intValue} + st := &ast.StructLit{ + Lbrace: token.Blank.Pos(), + Elts: []ast.Decl{embed, field}, + } + + addComments(embed, 0, y.Comment, y.InlineComment) + if y.Comment == nil && y.InlineComment == nil { + ast.SetRelPos(field, token.Blank) + ast.SetRelPos(field.Label, token.Blank) + st.Rbrace = token.Blank.Pos() + if i > 0 && lastSingle { + st.Lbrace = token.Newline.Pos() + } + lastSingle = true + } else { + lastSingle = false + } + e = st + + default: + p.state.errs = errors.Append(p.state.errs, + errors.Newf(token.NoPos, "unknown enum mode %q", p.state.enumMode)) + return } - 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}) } } + p.addDecl(d) addComments(enum.Value, 1, nil, lastComment) } diff --git a/encoding/protobuf/pbinternal/symbol.go b/encoding/protobuf/pbinternal/symbol.go new file mode 100644 index 000000000..8a6f5f014 --- /dev/null +++ b/encoding/protobuf/pbinternal/symbol.go @@ -0,0 +1,66 @@ +// Copyright 2021 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 pbinternal + +import ( + "strconv" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/ast" + "cuelang.org/go/cue/token" +) + +// MatchBySymbol finds an integer value for a given symbol name, representing +// an enum value, and sets it in x. +func MatchBySymbol(v cue.Value, name string, x *ast.BasicLit) bool { + if op, a := v.Expr(); op == cue.AndOp { + for _, v := range a { + if MatchBySymbol(v, name, x) { + return true + } + } + } + return matchBySymbol(cue.Dereference(v), name, x) +} + +func matchBySymbol(v cue.Value, name string, x *ast.BasicLit) bool { + switch op, a := v.Expr(); op { + case cue.OrOp, cue.AndOp: + for _, v := range a { + if matchBySymbol(v, name, x) { + return true + } + } + + default: + _, path := v.ReferencePath() + + a := path.Selectors() + if len(a) == 0 { + break + } + if s := a[len(a)-1]; !s.IsDefinition() || s.String()[1:] != name { + break + } + + if i, err := v.Int64(); err == nil { + x.Kind = token.INT + x.Value = strconv.Itoa(int(i)) + return true + } + } + + return false +} diff --git a/encoding/protobuf/protobuf.go b/encoding/protobuf/protobuf.go index 456a19bf7..48f866766 100644 --- a/encoding/protobuf/protobuf.go +++ b/encoding/protobuf/protobuf.go @@ -133,6 +133,20 @@ type Config struct { // PkgName specifies the package name for a generated CUE file. A value // will be derived from the Go package name if undefined. PkgName string + + // EnumMode defines whether enums should be set as integer values, instead + // of strings. + // + // json value is a string, corresponding to the standard JSON mapping + // of Protobuf. The value is associated with a #enumValue + // to allow the json+pb interpretation to interpret integers + // as well. + // + // int value is an integer associated with an #enumValue definition + // The json+pb interpreter uses the definition names in the + // disjunction of the enum to interpret strings. + // + EnumMode string } // An Extractor converts a collection of proto files, typically belonging to one @@ -146,11 +160,12 @@ type Config struct { // according to their Go package import path. // type Extractor struct { - root string - cwd string - module string - paths []string - pkgName string + root string + cwd string + module string + paths []string + pkgName string + enumMode string fileCache map[string]result imports map[string]*build.Instance @@ -175,6 +190,7 @@ func NewExtractor(c *Config) *Extractor { paths: c.Paths, pkgName: c.PkgName, module: c.Module, + enumMode: c.EnumMode, fileCache: map[string]result{}, imports: map[string]*build.Instance{}, } diff --git a/encoding/protobuf/testdata/client_config.proto.out.cue b/encoding/protobuf/testdata/client_config.proto.out.cue index c57dbebb7..84672a0aa 100644 --- a/encoding/protobuf/testdata/client_config.proto.out.cue +++ b/encoding/protobuf/testdata/client_config.proto.out.cue @@ -27,10 +27,12 @@ import ( // Specifies the behavior when the client is unable to connect to Mixer. #NetworkFailPolicy: { // Example of single-value enum. - #FailPolicy: + #FailPolicy: { // If network connection fails, request is allowed and delivered to the // service. "FAIL_OPEN" + #enumValue: 0 + } #FailPolicy_value: FAIL_OPEN: 0 diff --git a/encoding/protobuf/testdata/gateway.proto.out.cue b/encoding/protobuf/testdata/gateway.proto.out.cue index 80ed7a1ad..8f804716d 100644 --- a/encoding/protobuf/testdata/gateway.proto.out.cue +++ b/encoding/protobuf/testdata/gateway.proto.out.cue @@ -324,19 +324,22 @@ package v1alpha3 httpsRedirect?: bool @protobuf(1,bool,name=https_redirect) // TLS modes enforced by the proxy - #TLSmode: + #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" | - + "PASSTHROUGH" + #enumValue: 0 + } | { // Secure connections with standard TLS semantics. - "SIMPLE" | - + "SIMPLE" + #enumValue: 1 + } | { // Secure connections to the upstream using mutual TLS by presenting // client certificates for authentication. - "MUTUAL" | - + "MUTUAL" + #enumValue: 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 @@ -348,6 +351,8 @@ package v1alpha3 // their respective endpoints. Use of this mode assumes that both the // source and the destination are using Istio mTLS to secure traffic. "AUTO_PASSTHROUGH" + #enumValue: 3 + } #TLSmode_value: { PASSTHROUGH: 0 @@ -398,11 +403,22 @@ package v1alpha3 subjectAltNames?: [...string] @protobuf(6,string,name=subject_alt_names) // TLS protocol versions. - #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 + #TLSProtocol: { + "TLS_AUTO"// Automatically choose the optimal TLS version. + #enumValue: 0 + } | { + "TLSV1_0"// TLS version 1.0 + #enumValue: 1 + } | { + "TLSV1_1"// TLS version 1.1 + #enumValue: 2 + } | { + "TLSV1_2"// TLS version 1.2 + #enumValue: 3 + } | { + "TLSV1_3"// TLS version 1.3 + #enumValue: 4 + } #TLSProtocol_value: { TLS_AUTO: 0 diff --git a/encoding/protobuf/testdata/istio.io/api/cue.mod/gen/github.com/golang/protobuf/protoc-gen-go/descriptor/descriptor_proto_gen.cue b/encoding/protobuf/testdata/istio.io/api/cue.mod/gen/github.com/golang/protobuf/protoc-gen-go/descriptor/descriptor_proto_gen.cue index 3d39a1952..2da6ce1b3 100644 --- a/encoding/protobuf/testdata/istio.io/api/cue.mod/gen/github.com/golang/protobuf/protoc-gen-go/descriptor/descriptor_proto_gen.cue +++ b/encoding/protobuf/testdata/istio.io/api/cue.mod/gen/github.com/golang/protobuf/protoc-gen-go/descriptor/descriptor_proto_gen.cue @@ -149,40 +149,48 @@ package descriptor // Describes a field within a message. #FieldDescriptorProto: { - #Type: + #Type: { // 0 is reserved for errors. // Order is weird for historical reasons. - "TYPE_DOUBLE" | - "TYPE_FLOAT" | - + "TYPE_DOUBLE" + #enumValue: 1 + } | {"TYPE_FLOAT", #enumValue: 2} | { // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if // negative values are likely. - "TYPE_INT64" | - "TYPE_UINT64" | - + "TYPE_INT64" + #enumValue: 3 + } | {"TYPE_UINT64", #enumValue: 4} | { // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if // negative values are likely. - "TYPE_INT32" | - "TYPE_FIXED64" | - "TYPE_FIXED32" | - "TYPE_BOOL" | - "TYPE_STRING" | - - // Tag-delimited aggregate. - // Group type is deprecated and not supported in proto3. However, Proto3 - // implementations should still be able to parse the group wire format and - // treat group fields as unknown fields. - "TYPE_GROUP" | - "TYPE_MESSAGE" | // Length-delimited aggregate. - - // New in version 2. - "TYPE_BYTES" | - "TYPE_UINT32" | - "TYPE_ENUM" | - "TYPE_SFIXED32" | - "TYPE_SFIXED64" | - "TYPE_SINT32" | // Uses ZigZag encoding. - "TYPE_SINT64" // Uses ZigZag encoding. + "TYPE_INT32" + #enumValue: 5 + } | {"TYPE_FIXED64", #enumValue: 6} | + {"TYPE_FIXED32", #enumValue: 7} | + {"TYPE_BOOL", #enumValue: 8} | + {"TYPE_STRING", #enumValue: 9} | { + // Tag-delimited aggregate. + // Group type is deprecated and not supported in proto3. However, Proto3 + // implementations should still be able to parse the group wire format and + // treat group fields as unknown fields. + "TYPE_GROUP" + #enumValue: 10 + } | { + "TYPE_MESSAGE"// Length-delimited aggregate. + #enumValue: 11 + } | { + // New in version 2. + "TYPE_BYTES" + #enumValue: 12 + } | {"TYPE_UINT32", #enumValue: 13} | + {"TYPE_ENUM", #enumValue: 14} | + {"TYPE_SFIXED32", #enumValue: 15} | + {"TYPE_SFIXED64", #enumValue: 16} | { + "TYPE_SINT32"// Uses ZigZag encoding. + #enumValue: 17 + } | { + "TYPE_SINT64"// Uses ZigZag encoding. + #enumValue: 18 + } #Type_value: { "TYPE_DOUBLE": 1 @@ -204,11 +212,13 @@ package descriptor "TYPE_SINT32": 17 "TYPE_SINT64": 18 } - #Label: + + #Label: { // 0 is reserved for errors - "LABEL_OPTIONAL" | - "LABEL_REQUIRED" | - "LABEL_REPEATED" + "LABEL_OPTIONAL" + #enumValue: 1 + } | {"LABEL_REQUIRED", #enumValue: 2} | + {"LABEL_REPEATED", #enumValue: 3} #Label_value: { "LABEL_OPTIONAL": 1 @@ -351,15 +361,21 @@ package descriptor javaStringCheckUtf8?: bool @protobuf(27,bool,name=java_string_check_utf8,"default=false") // Generated classes can be optimized for speed or code size. - #OptimizeMode: "SPEED" | // Generate complete code for parsing, serialization, - + #OptimizeMode: { + "SPEED"// Generate complete code for parsing, serialization, + #enumValue: 1 + } | { // etc. - "CODE_SIZE" | - "LITE_RUNTIME" // Generate code using MessageLite and the lite runtime. + "CODE_SIZE"// Use ReflectionOps to implement these methods. + #enumValue: 2 + } | { + "LITE_RUNTIME"// Generate code using MessageLite and the lite runtime. + #enumValue: 3 + } #OptimizeMode_value: { "SPEED": 1 - "CODE_SIZE": 2 // Use ReflectionOps to implement these methods. + "CODE_SIZE": 2 "LITE_RUNTIME": 3 } optimizeFor?: #OptimizeMode @protobuf(9,OptimizeMode,name=optimize_for,"default=SPEED") @@ -498,11 +514,13 @@ package descriptor // options below. This option is not yet implemented in the open source // release -- sorry, we'll try to include it in a future version! ctype?: #CType @protobuf(1,CType,"default=STRING") - #CType: + + #CType: { // Default mode. - "STRING" | - "CORD" | - "STRING_PIECE" + "STRING" + #enumValue: 0 + } | {"CORD", #enumValue: 1} | + {"STRING_PIECE", #enumValue: 2} #CType_value: { "STRING": 0 @@ -529,15 +547,20 @@ package descriptor // This option is an enum to permit additional types to be added, e.g. // goog.math.Integer. jstype?: #JSType @protobuf(6,JSType,"default=JS_NORMAL") - #JSType: - // Use the default type. - "JS_NORMAL" | + #JSType: { + // Use the default type. + "JS_NORMAL" + #enumValue: 0 + } | { // Use JavaScript strings. - "JS_STRING" | - + "JS_STRING" + #enumValue: 1 + } | { // Use JavaScript numbers. "JS_NUMBER" + #enumValue: 2 + } #JSType_value: { "JS_NORMAL": 0 @@ -650,9 +673,13 @@ package descriptor // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, // or neither? HTTP based RPC implementation may choose GET verb for safe // methods, and PUT verb for idempotent methods instead of the default POST. - #IdempotencyLevel: "IDEMPOTENCY_UNKNOWN" | - "NO_SIDE_EFFECTS" | // implies idempotent - "IDEMPOTENT" // idempotent, but may have side effects + #IdempotencyLevel: {"IDEMPOTENCY_UNKNOWN", #enumValue: 0} | { + "NO_SIDE_EFFECTS"// implies idempotent + #enumValue: 1 + } | { + "IDEMPOTENT"// idempotent, but may have side effects + #enumValue: 2 + } #IdempotencyLevel_value: { "IDEMPOTENCY_UNKNOWN": 0 diff --git a/encoding/protobuf/testdata/istio.io/api/mixer/v1/config/client/client_config_proto_gen.cue b/encoding/protobuf/testdata/istio.io/api/mixer/v1/config/client/client_config_proto_gen.cue index 8ae32c209..4d0352c53 100644 --- a/encoding/protobuf/testdata/istio.io/api/mixer/v1/config/client/client_config_proto_gen.cue +++ b/encoding/protobuf/testdata/istio.io/api/mixer/v1/config/client/client_config_proto_gen.cue @@ -27,10 +27,12 @@ import ( // Specifies the behavior when the client is unable to connect to Mixer. #NetworkFailPolicy: { // Example of single-value enum. - #FailPolicy: + #FailPolicy: { // If network connection fails, request is allowed and delivered to the // service. "FAIL_OPEN" + #enumValue: 0 + } #FailPolicy_value: "FAIL_OPEN": 0 diff --git a/encoding/protobuf/testdata/istio.io/api/mixer/v1/mixer_proto_gen.cue b/encoding/protobuf/testdata/istio.io/api/mixer/v1/mixer_proto_gen.cue index 1e3fac491..868af2249 100644 --- a/encoding/protobuf/testdata/istio.io/api/mixer/v1/mixer_proto_gen.cue +++ b/encoding/protobuf/testdata/istio.io/api/mixer/v1/mixer_proto_gen.cue @@ -103,10 +103,19 @@ import ( // This can be used to construct a response cache. #ReferencedAttributes: { // How an attribute's value was matched - #Condition: "CONDITION_UNSPECIFIED" | // should not occur - "ABSENCE" | // match when attribute doesn't exist - "EXACT" | // match when attribute value is an exact byte-for-byte match - "REGEX" // match when attribute value matches the included regex + #Condition: { + "CONDITION_UNSPECIFIED"// should not occur + #enumValue: 0 + } | { + "ABSENCE"// match when attribute doesn't exist + #enumValue: 1 + } | { + "EXACT"// match when attribute value is an exact byte-for-byte match + #enumValue: 2 + } | { + "REGEX"// match when attribute value matches the included regex + #enumValue: 3 + } #Condition_value: { "CONDITION_UNSPECIFIED": 0 @@ -155,9 +164,16 @@ import ( // the request headers. #HeaderOperation: { // Operation type. - #Operation: "REPLACE" | // replaces the header with the given name - "REMOVE" | // removes the header with the given name (the value is ignored) - "APPEND" // appends the value to the header value, or sets it if not present + #Operation: { + "REPLACE"// replaces the header with the given name + #enumValue: 0 + } | { + "REMOVE"// removes the header with the given name (the value is ignored) + #enumValue: 1 + } | { + "APPEND"// appends the value to the header value, or sets it if not present + #enumValue: 2 + } #Operation_value: { "REPLACE": 0 @@ -198,17 +214,20 @@ import ( // next value: 5 // Used to signal how the sets of compressed attributes should be reconstitued server-side. - #RepeatedAttributesSemantics: + #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" | - + "DELTA_ENCODING" + #enumValue: 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" + #enumValue: 1 + } #RepeatedAttributesSemantics_value: { "DELTA_ENCODING": 0 diff --git a/encoding/protobuf/testdata/istio.io/api/networking/v1alpha3/gateway_proto_gen.cue b/encoding/protobuf/testdata/istio.io/api/networking/v1alpha3/gateway_proto_gen.cue index 80dd5cdfa..e06a95628 100644 --- a/encoding/protobuf/testdata/istio.io/api/networking/v1alpha3/gateway_proto_gen.cue +++ b/encoding/protobuf/testdata/istio.io/api/networking/v1alpha3/gateway_proto_gen.cue @@ -324,19 +324,22 @@ package v1alpha3 httpsRedirect?: bool @protobuf(1,bool,name=https_redirect) // TLS modes enforced by the proxy - #TLSmode: + #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" | - + "PASSTHROUGH" + #enumValue: 0 + } | { // Secure connections with standard TLS semantics. - "SIMPLE" | - + "SIMPLE" + #enumValue: 1 + } | { // Secure connections to the upstream using mutual TLS by presenting // client certificates for authentication. - "MUTUAL" | - + "MUTUAL" + #enumValue: 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 @@ -348,6 +351,8 @@ package v1alpha3 // their respective endpoints. Use of this mode assumes that both the // source and the destination are using Istio mTLS to secure traffic. "AUTO_PASSTHROUGH" + #enumValue: 3 + } #TLSmode_value: { "PASSTHROUGH": 0 @@ -398,11 +403,22 @@ package v1alpha3 subjectAltNames?: [...string] @protobuf(6,string,name=subject_alt_names) // TLS protocol versions. - #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 + #TLSProtocol: { + "TLS_AUTO"// Automatically choose the optimal TLS version. + #enumValue: 0 + } | { + "TLSV1_0"// TLS version 1.0 + #enumValue: 1 + } | { + "TLSV1_1"// TLS version 1.1 + #enumValue: 2 + } | { + "TLSV1_2"// TLS version 1.2 + #enumValue: 3 + } | { + "TLSV1_3"// TLS version 1.3 + #enumValue: 4 + } #TLSProtocol_value: { "TLS_AUTO": 0