Skip to content

Commit a0035de

Browse files
committed
encoding/protobuf/textproto: add encoder
Doesn't really copy positioning and all comments, but the textproto API is sort of hit or miss anyway whether this information is supported at all. So good enough for now. Issue #5 Change-Id: Ia0d09a0c4b92756f68c2a09a114311b363fef33a Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9370 Reviewed-by: CUE cueckoo <[email protected]> Reviewed-by: Marcel van Lohuizen <[email protected]>
1 parent 4a288d5 commit a0035de

File tree

9 files changed

+496
-1
lines changed

9 files changed

+496
-1
lines changed

encoding/protobuf/pbinternal/attribute.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package pbinternal
1616

1717
import (
1818
"strings"
19+
"unicode"
20+
"unicode/utf8"
1921

2022
"cuelang.org/go/cue"
2123
)
@@ -50,6 +52,8 @@ type Info struct {
5052
ValueType ValueType
5153
Type string
5254

55+
IsEnum bool
56+
5357
// For maps only
5458
KeyType ValueType // only for maps
5559
KeyTypeString string
@@ -97,7 +101,7 @@ func FromValue(name string, v cue.Value) (info Info, err error) {
97101

98102
case cue.StructKind:
99103
if strings.HasPrefix(info.Type, "map[") {
100-
a := strings.SplitN(info.Type[len("map["):], ",", 2)
104+
a := strings.SplitN(info.Type[len("map["):], "]", 2)
101105
info.KeyTypeString = strings.TrimSpace(a[0])
102106
switch info.KeyTypeString {
103107
case "string":
@@ -133,6 +137,8 @@ func FromValue(name string, v cue.Value) (info Info, err error) {
133137

134138
case cue.IntKind:
135139
info.ValueType = Int
140+
r, _ := utf8.DecodeRuneInString(info.Type)
141+
info.IsEnum = unicode.In(r, unicode.Upper)
136142

137143
case cue.FloatKind, cue.NumberKind:
138144
info.ValueType = Float

encoding/protobuf/pbinternal/symbol.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,47 @@ func matchBySymbol(v cue.Value, name string, x *ast.BasicLit) bool {
6464

6565
return false
6666
}
67+
68+
// MatchByInt finds a symbol for a given enum value and sets it in x.
69+
func MatchByInt(v cue.Value, val int64) string {
70+
if op, a := v.Expr(); op == cue.AndOp {
71+
for _, v := range a {
72+
if s := MatchByInt(v, val); s != "" {
73+
return s
74+
}
75+
}
76+
}
77+
v = cue.Dereference(v)
78+
return matchByInt(v, val)
79+
}
80+
81+
func matchByInt(v cue.Value, val int64) string {
82+
switch op, a := v.Expr(); op {
83+
case cue.OrOp, cue.AndOp:
84+
for _, v := range a {
85+
if s := matchByInt(v, val); s != "" {
86+
return s
87+
}
88+
}
89+
90+
default:
91+
if i, err := v.Int64(); err != nil || i != val {
92+
break
93+
}
94+
95+
_, path := v.ReferencePath()
96+
a := path.Selectors()
97+
if len(a) == 0 {
98+
break
99+
}
100+
101+
sel := a[len(a)-1]
102+
if !sel.IsDefinition() {
103+
break
104+
}
105+
106+
return sel.String()[1:]
107+
}
108+
109+
return ""
110+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2021 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 textproto
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"cuelang.org/go/cue"
22+
"cuelang.org/go/cue/errors"
23+
"cuelang.org/go/encoding/protobuf/pbinternal"
24+
25+
"github.com/protocolbuffers/txtpbfmt/ast"
26+
pbast "github.com/protocolbuffers/txtpbfmt/ast"
27+
"github.com/protocolbuffers/txtpbfmt/parser"
28+
)
29+
30+
// Encoder marshals CUE into text proto.
31+
//
32+
type Encoder struct {
33+
// Schema
34+
}
35+
36+
// NewEncoder returns a new encoder, where the given options are default
37+
// options.
38+
func NewEncoder(options ...Option) *Encoder {
39+
return &Encoder{}
40+
}
41+
42+
// Encode converts a CUE value to a text proto file.
43+
//
44+
// Fields do not need to have a @protobuf attribute except for in the following
45+
// cases:
46+
//
47+
// - it is explicitly required that only fields with an attribute are exported
48+
// - a struct represents a Protobuf map
49+
// - custom naming
50+
//
51+
func (e *Encoder) Encode(v cue.Value, options ...Option) ([]byte, error) {
52+
n := &pbast.Node{}
53+
enc := &encoder{}
54+
55+
enc.encodeMsg(n, v)
56+
57+
if enc.errs != nil {
58+
return nil, enc.errs
59+
}
60+
61+
// Pretty printing does not do errors, and returns a string (why o why?).
62+
s := parser.Pretty(n.Children, 0)
63+
return []byte(s), nil
64+
}
65+
66+
type encoder struct {
67+
errs errors.Error
68+
}
69+
70+
func (e *encoder) addErr(err error) {
71+
e.errs = errors.Append(e.errs, errors.Promote(err, "textproto"))
72+
}
73+
74+
func (e *encoder) encodeMsg(parent *pbast.Node, v cue.Value) {
75+
i, err := v.Fields()
76+
if err != nil {
77+
e.addErr(err)
78+
return
79+
}
80+
for i.Next() {
81+
v := i.Value()
82+
if !v.IsConcrete() {
83+
continue
84+
}
85+
86+
info, err := pbinternal.FromIter(i)
87+
if err != nil {
88+
e.addErr(err)
89+
}
90+
91+
switch info.CompositeType {
92+
case pbinternal.List:
93+
elems, err := v.List()
94+
if err != nil {
95+
e.addErr(err)
96+
return
97+
}
98+
for first := true; elems.Next(); first = false {
99+
n := &pbast.Node{Name: info.Name}
100+
if first {
101+
copyMeta(n, v)
102+
}
103+
elem := elems.Value()
104+
copyMeta(n, elem)
105+
parent.Children = append(parent.Children, n)
106+
e.encodeValue(n, elem)
107+
}
108+
109+
case pbinternal.Map:
110+
i, err := v.Fields()
111+
if err != nil {
112+
e.addErr(err)
113+
return
114+
}
115+
for first := true; i.Next(); first = false {
116+
n := &pbast.Node{Name: info.Name}
117+
if first {
118+
copyMeta(n, v)
119+
}
120+
parent.Children = append(parent.Children, n)
121+
var key *pbast.Node
122+
switch info.KeyType {
123+
case pbinternal.String, pbinternal.Bytes:
124+
key = pbast.StringNode("key", i.Label())
125+
default:
126+
key = &pbast.Node{
127+
Name: "key",
128+
Values: []*ast.Value{{Value: i.Label()}},
129+
}
130+
}
131+
n.Children = append(n.Children, key)
132+
133+
value := &pbast.Node{Name: "value"}
134+
e.encodeValue(value, i.Value())
135+
n.Children = append(n.Children, value)
136+
}
137+
138+
default:
139+
n := &pbast.Node{Name: info.Name}
140+
copyMeta(n, v)
141+
e.encodeValue(n, v)
142+
// Don't add if there are no values or children.
143+
parent.Children = append(parent.Children, n)
144+
}
145+
}
146+
}
147+
148+
// copyMeta copies metadata from nodes to values.
149+
//
150+
// TODO: also copy positions. The textproto API is rather messy and complex,
151+
// though, and so far it seems to be quite buggy too. Not sure if it is worth
152+
// the effort.
153+
func copyMeta(x *pbast.Node, v cue.Value) {
154+
for _, doc := range v.Doc() {
155+
s := strings.TrimRight(doc.Text(), "\n")
156+
for _, c := range strings.Split(s, "\n") {
157+
x.PreComments = append(x.PreComments, "# "+c)
158+
}
159+
}
160+
}
161+
162+
func (e *encoder) encodeValue(n *pbast.Node, v cue.Value) {
163+
var value string
164+
switch v.Kind() {
165+
case cue.StructKind:
166+
e.encodeMsg(n, v)
167+
168+
case cue.StringKind:
169+
s, err := v.String()
170+
if err != nil {
171+
e.addErr(err)
172+
}
173+
sn := pbast.StringNode("foo", s)
174+
n.Values = append(n.Values, sn.Values...)
175+
176+
case cue.BytesKind:
177+
b, err := v.Bytes()
178+
if err != nil {
179+
e.addErr(err)
180+
}
181+
sn := pbast.StringNode("foo", string(b))
182+
n.Values = append(n.Values, sn.Values...)
183+
184+
case cue.BoolKind:
185+
value = fmt.Sprint(v)
186+
n.Values = append(n.Values, &pbast.Value{Value: value})
187+
188+
case cue.IntKind, cue.FloatKind, cue.NumberKind:
189+
d, _ := v.Decimal()
190+
value := d.String()
191+
192+
if info, _ := pbinternal.FromValue("", v); !info.IsEnum {
193+
} else if i, err := v.Int64(); err != nil {
194+
} else if s := pbinternal.MatchByInt(v, i); s != "" {
195+
value = s
196+
}
197+
198+
n.Values = append(n.Values, &pbast.Value{Value: value})
199+
200+
default:
201+
e.addErr(errors.Newf(v.Pos(), "textproto: unknown type %v", v.Kind()))
202+
}
203+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2021 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 textproto_test
16+
17+
import (
18+
"strings"
19+
"testing"
20+
21+
"cuelang.org/go/cue"
22+
"cuelang.org/go/cue/errors"
23+
"cuelang.org/go/encoding/protobuf/textproto"
24+
"cuelang.org/go/internal/cuetest"
25+
"cuelang.org/go/internal/cuetxtar"
26+
)
27+
28+
func TestEncode(t *testing.T) {
29+
test := cuetxtar.TxTarTest{
30+
Root: "./testdata/encoder",
31+
Name: "encode",
32+
Update: cuetest.UpdateGoldenFiles,
33+
}
34+
35+
r := cue.Runtime{}
36+
37+
test.Run(t, func(t *cuetxtar.Test) {
38+
// TODO: use high-level API.
39+
40+
var schema, value cue.Value
41+
42+
for _, f := range t.Archive.Files {
43+
switch {
44+
case strings.HasSuffix(f.Name, ".cue"):
45+
inst, err := r.Compile(f.Name, f.Data)
46+
if err != nil {
47+
t.WriteErrors(errors.Promote(err, "test"))
48+
return
49+
}
50+
switch f.Name {
51+
case "schema.cue":
52+
schema = inst.Value()
53+
case "value.cue":
54+
value = inst.Value()
55+
}
56+
}
57+
}
58+
59+
v := schema.Unify(value)
60+
if err := v.Err(); err != nil {
61+
t.WriteErrors(errors.Promote(err, "test"))
62+
return
63+
}
64+
65+
b, err := textproto.NewEncoder().Encode(v)
66+
if err != nil {
67+
t.WriteErrors(errors.Promote(err, "test"))
68+
return
69+
}
70+
_, _ = t.Write(b)
71+
72+
})
73+
}

0 commit comments

Comments
 (0)