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