blob: 18681f79ee74f6de0e1523051ed85d9e7bfdd2d6 [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/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/literal"
"cuelang.org/go/cue/token"
"cuelang.org/go/encoding/protobuf/pbinternal"
"cuelang.org/go/internal/core/adt"
"cuelang.org/go/internal/value"
pbast "github.com/protocolbuffers/txtpbfmt/ast"
"github.com/protocolbuffers/txtpbfmt/parser"
"github.com/protocolbuffers/txtpbfmt/unquote"
)
// Option defines options for the decoder.
// There are currently no options.
type Option func(*options)
type options struct {
}
// NewDecoder returns a new Decoder
func NewDecoder(option ...Option) *Decoder {
d := &Decoder{}
_ = d.m // work around linter bug.
return d
}
// A Decoder caches conversions of cue.Value between calls to its methods.
type Decoder struct {
m map[*adt.Vertex]*mapping
}
type decoder struct {
*Decoder
// Reset on each call
errs errors.Error
file *token.File
}
// Parse parses the given textproto bytes and converts them to a CUE expression,
// using schema as the guideline for conversion using the following rules:
//
// - the @protobuf attribute is optional, but is necessary for:
// - interpreting protobuf maps
// - using a name different from the CUE name
// - fields in the textproto that have no corresponding field in
// schema are ignored
//
// NOTE: the filename is used for associating position information. However,
// currently no position information is associated with the text proto because
// the position information of github.com/protocolbuffers/txtpbfmt is too
// unreliable to be useful.
func (d *Decoder) Parse(schema cue.Value, filename string, b []byte) (ast.Expr, error) {
dec := decoder{Decoder: d}
// dec.errs = nil
f := token.NewFile(filename, 0, len(b))
f.SetLinesForContent(b)
dec.file = f
cfg := parser.Config{}
nodes, err := parser.ParseWithConfig(b, cfg)
if err != nil {
return nil, errors.Newf(token.NoPos, "textproto: %v", err)
}
m := dec.parseSchema(schema)
if dec.errs != nil {
return nil, dec.errs
}
n := dec.decodeMsg(m, nodes)
if dec.errs != nil {
return nil, dec.errs
}
return n, nil
}
// Don't expose until the protobuf APIs settle down.
// func (d *decoder) Decode(schema cue.Value, textpbfmt) (cue.Value, error) {
// }
type mapping struct {
children map[string]*fieldInfo
}
type fieldInfo struct {
pbinternal.Info
msg *mapping
// keytype, for now
}
func (d *decoder) addErr(err error) {
d.errs = errors.Append(d.errs, errors.Promote(err, "textproto"))
}
func (d *decoder) addErrf(pos pbast.Position, format string, args ...interface{}) {
err := errors.Newf(d.protoPos(pos), "textproto: "+format, args...)
d.errs = errors.Append(d.errs, err)
}
func (d *decoder) protoPos(p pbast.Position) token.Pos {
return d.file.Pos(int(p.Byte), token.NoRelPos)
}
// parseSchema walks over a CUE "type", converts it to an internal data
// structure that is used for parsing text proto, and writes it to
//
func (d *decoder) parseSchema(schema cue.Value) *mapping {
_, v := value.ToInternal(schema)
if v == nil {
return nil
}
if d.m == nil {
d.m = map[*adt.Vertex]*mapping{}
} else if m := d.m[v]; m != nil {
return m
}
m := &mapping{children: map[string]*fieldInfo{}}
i, err := schema.Fields()
if err != nil {
d.addErr(err)
return nil
}
for i.Next() {
info, err := pbinternal.FromIter(i)
if err != nil {
d.addErr(err)
continue
}
var msg *mapping
switch info.CompositeType {
case pbinternal.Normal:
switch info.ValueType {
case pbinternal.Message:
msg = d.parseSchema(i.Value())
}
case pbinternal.List, pbinternal.Map:
e, _ := i.Value().Elem()
if e.IncompleteKind() == cue.StructKind {
msg = d.parseSchema(e)
}
}
m.children[info.Name] = &fieldInfo{
Info: info,
msg: msg,
}
}
d.m[v] = m
return m
}
func (d *decoder) decodeMsg(m *mapping, n []*pbast.Node) ast.Expr {
st := &ast.StructLit{}
var listMap map[string]*ast.ListLit
for _, x := range n {
if x.Values == nil && x.Children == nil {
if cg := addComments(x.PreComments...); cg != nil {
ast.SetRelPos(cg, token.NewSection)
st.Elts = append(st.Elts, cg)
continue
}
}
if m == nil {
continue
}
f, ok := m.children[x.Name]
if !ok {
continue // ignore unknown fields
}
var value ast.Expr
switch f.CompositeType {
default:
value = d.decodeValue(f, x)
case pbinternal.List:
if listMap == nil {
listMap = make(map[string]*ast.ListLit)
}
list := listMap[f.CUEName]
if list == nil {
list = &ast.ListLit{}
listMap[f.CUEName] = list
value = list
}
if len(x.Values) == 1 || f.ValueType == pbinternal.Message {
v := d.decodeValue(f, x)
if value == nil {
if cg := addComments(x.PreComments...); cg != nil {
cg.Doc = true
ast.AddComment(v, cg)
}
}
if cg := addComments(x.PostValuesComments...); cg != nil {
cg.Position = 4
ast.AddComment(v, cg)
}
list.Elts = append(list.Elts, v)
break
}
var last ast.Expr
// Handle [1, 2, 3]
for _, v := range x.Values {
if v.Value == "" {
if cg := addComments(v.PreComments...); cg != nil {
if last != nil {
cg.Position = 4
ast.AddComment(last, cg)
} else {
cg.Position = 1
ast.AddComment(list, cg)
}
}
continue
}
y := *x
y.Values = []*pbast.Value{v}
last = d.decodeValue(f, &y)
list.Elts = append(list.Elts, last)
}
if cg := addComments(x.PostValuesComments...); cg != nil {
if last != nil {
cg.Position = 4
ast.AddComment(last, cg)
} else {
cg.Position = 1
ast.AddComment(list, cg)
}
}
if cg := addComments(x.ClosingBraceComment); cg != nil {
cg.Position = 4
ast.AddComment(list, cg)
}
case pbinternal.Map:
// mapValue: {
// key: 123
// value: "string"
// }
if k := len(x.Values); k > 0 {
d.addErrf(x.Start, "values not allowed for Message type; found %d", k)
}
var (
key ast.Label
val ast.Expr
)
for _, c := range x.Children {
if len(c.Values) != 1 {
d.addErrf(x.Start, "expected 1 value, found %d", len(c.Values))
continue
}
s := c.Values[0].Value
switch c.Name {
case "key":
if strings.HasPrefix(s, `"`) {
key = &ast.BasicLit{Kind: token.STRING, Value: s}
} else {
key = ast.NewString(s)
}
case "value":
val = d.decodeValue(f, c)
if cg := addComments(x.ClosingBraceComment); cg != nil {
cg.Line = true
ast.AddComment(val, cg)
}
default:
d.addErrf(c.Start, "unsupported key name %q in map", c.Name)
continue
}
}
if key != nil && val != nil {
value = ast.NewStruct(key, val)
}
}
if value != nil {
var label ast.Label
if s := f.CUEName; ast.IsValidIdent(s) {
label = ast.NewIdent(s)
} else {
label = ast.NewString(s)
}
// TODO: convert line number information. However, position
// information in textpbfmt packages is too wonky to be useful
f := &ast.Field{
Label: label,
Value: value,
// Attrs: []*ast.Attribute{{Text: f.attr.}},
}
if cg := addComments(x.PreComments...); cg != nil {
cg.Doc = true
ast.AddComment(f, cg)
}
st.Elts = append(st.Elts, f)
}
}
return st
}
func addComments(lines ...string) (cg *ast.CommentGroup) {
var a []*ast.Comment
for _, c := range lines {
if !strings.HasPrefix(c, "#") {
continue
}
a = append(a, &ast.Comment{Text: "//" + c[1:]})
}
if a != nil {
cg = &ast.CommentGroup{List: a}
}
return cg
}
func (d *decoder) decodeValue(f *fieldInfo, n *pbast.Node) (x ast.Expr) {
if f.ValueType == pbinternal.Message {
if k := len(n.Values); k > 0 {
d.addErrf(n.Start, "values not allowed for Message type; found %d", k)
}
x = d.decodeMsg(f.msg, n.Children)
if cg := addComments(n.ClosingBraceComment); cg != nil {
cg.Line = true
cg.Position = 4
ast.AddComment(x, cg)
}
return x
}
if len(n.Values) != 1 {
d.addErrf(n.Start, "expected 1 value, found %d", len(n.Values))
return nil
}
v := n.Values[0]
defer func() {
if cg := addComments(v.PreComments...); cg != nil {
cg.Doc = true
ast.AddComment(x, cg)
}
if cg := addComments(v.InlineComment); cg != nil {
cg.Line = true
cg.Position = 2
ast.AddComment(x, cg)
}
}()
switch f.ValueType {
case pbinternal.String, pbinternal.Bytes:
s, err := unquote.Unquote(n)
if err != nil {
d.addErrf(n.Start, "invalid string or bytes: %v", err)
}
if f.ValueType == pbinternal.String {
s = literal.String.Quote(s)
} else {
s = literal.Bytes.Quote(s)
}
return &ast.BasicLit{Kind: token.STRING, Value: s}
case pbinternal.Bool:
switch v.Value {
case "true":
return ast.NewBool(true)
case "false":
default:
d.addErrf(n.Start, "invalid bool %s", v.Value)
}
return ast.NewBool(false)
case pbinternal.Int, pbinternal.Float:
s := v.Value
switch s {
case "inf", "nan":
// TODO: include message.
return &ast.BottomLit{}
}
var info literal.NumInfo
if err := literal.ParseNum(s, &info); err != nil {
var x ast.BasicLit
if pbinternal.MatchBySymbol(f.Value, s, &x) {
return &x
}
d.addErrf(n.Start, "invalid number %s", s)
}
if !info.IsInt() {
return &ast.BasicLit{Kind: token.FLOAT, Value: s}
}
return &ast.BasicLit{Kind: token.INT, Value: info.String()}
default:
panic(fmt.Sprintf("unexpected type %v", f.ValueType))
}
}