blob: dcb9ed3e003a764cc073eb6e8e09b302c9e9910b [file] [log] [blame]
// 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 textproto
import (
"fmt"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/errors"
"cuelang.org/go/encoding/protobuf/pbinternal"
"github.com/protocolbuffers/txtpbfmt/ast"
pbast "github.com/protocolbuffers/txtpbfmt/ast"
"github.com/protocolbuffers/txtpbfmt/parser"
)
// Encoder marshals CUE into text proto.
//
type Encoder struct {
// Schema
}
// NewEncoder returns a new encoder, where the given options are default
// options.
func NewEncoder(options ...Option) *Encoder {
return &Encoder{}
}
// Encode converts a CUE value to a text proto file.
//
// Fields do not need to have a @protobuf attribute except for in the following
// cases:
//
// - it is explicitly required that only fields with an attribute are exported
// - a struct represents a Protobuf map
// - custom naming
//
func (e *Encoder) Encode(v cue.Value, options ...Option) ([]byte, error) {
n := &pbast.Node{}
enc := &encoder{}
enc.encodeMsg(n, v)
if enc.errs != nil {
return nil, enc.errs
}
// Pretty printing does not do errors, and returns a string (why o why?).
s := parser.Pretty(n.Children, 0)
return []byte(s), nil
}
type encoder struct {
errs errors.Error
}
func (e *encoder) addErr(err error) {
e.errs = errors.Append(e.errs, errors.Promote(err, "textproto"))
}
func (e *encoder) encodeMsg(parent *pbast.Node, v cue.Value) {
i, err := v.Fields()
if err != nil {
e.addErr(err)
return
}
for i.Next() {
v := i.Value()
if !v.IsConcrete() {
continue
}
info, err := pbinternal.FromIter(i)
if err != nil {
e.addErr(err)
}
switch info.CompositeType {
case pbinternal.List:
elems, err := v.List()
if err != nil {
e.addErr(err)
return
}
for first := true; elems.Next(); first = false {
n := &pbast.Node{Name: info.Name}
if first {
copyMeta(n, v)
}
elem := elems.Value()
copyMeta(n, elem)
parent.Children = append(parent.Children, n)
e.encodeValue(n, elem)
}
case pbinternal.Map:
i, err := v.Fields()
if err != nil {
e.addErr(err)
return
}
for first := true; i.Next(); first = false {
n := &pbast.Node{Name: info.Name}
if first {
copyMeta(n, v)
}
parent.Children = append(parent.Children, n)
var key *pbast.Node
switch info.KeyType {
case pbinternal.String, pbinternal.Bytes:
key = pbast.StringNode("key", i.Label())
default:
key = &pbast.Node{
Name: "key",
Values: []*ast.Value{{Value: i.Label()}},
}
}
n.Children = append(n.Children, key)
value := &pbast.Node{Name: "value"}
e.encodeValue(value, i.Value())
n.Children = append(n.Children, value)
}
default:
n := &pbast.Node{Name: info.Name}
copyMeta(n, v)
e.encodeValue(n, v)
// Don't add if there are no values or children.
parent.Children = append(parent.Children, n)
}
}
}
// copyMeta copies metadata from nodes to values.
//
// TODO: also copy positions. The textproto API is rather messy and complex,
// though, and so far it seems to be quite buggy too. Not sure if it is worth
// the effort.
func copyMeta(x *pbast.Node, v cue.Value) {
for _, doc := range v.Doc() {
s := strings.TrimRight(doc.Text(), "\n")
for _, c := range strings.Split(s, "\n") {
x.PreComments = append(x.PreComments, "# "+c)
}
}
}
func (e *encoder) encodeValue(n *pbast.Node, v cue.Value) {
var value string
switch v.Kind() {
case cue.StructKind:
e.encodeMsg(n, v)
case cue.StringKind:
s, err := v.String()
if err != nil {
e.addErr(err)
}
sn := pbast.StringNode("foo", s)
n.Values = append(n.Values, sn.Values...)
case cue.BytesKind:
b, err := v.Bytes()
if err != nil {
e.addErr(err)
}
sn := pbast.StringNode("foo", string(b))
n.Values = append(n.Values, sn.Values...)
case cue.BoolKind:
value = fmt.Sprint(v)
n.Values = append(n.Values, &pbast.Value{Value: value})
case cue.IntKind, cue.FloatKind, cue.NumberKind:
d, _ := v.Decimal()
value := d.String()
if info, _ := pbinternal.FromValue("", v); !info.IsEnum {
} else if i, err := v.Int64(); err != nil {
} else if s := pbinternal.MatchByInt(v, i); s != "" {
value = s
}
n.Values = append(n.Values, &pbast.Value{Value: value})
default:
e.addErr(errors.Newf(v.Pos(), "textproto: unknown type %v", v.Kind()))
}
}