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 jsonpb
16
17import (
18 "encoding/base64"
19 "strings"
20
21 "github.com/cockroachdb/apd/v3"
22
23 "cuelang.org/go/cue"
24 "cuelang.org/go/cue/ast"
25 "cuelang.org/go/cue/ast/astutil"
26 "cuelang.org/go/cue/errors"
27 "cuelang.org/go/cue/literal"
28 "cuelang.org/go/cue/token"
29 "cuelang.org/go/encoding/protobuf/pbinternal"
30)
31
32// Option is an option.
33//
34// There are currently no options.
35type Option func()
36
37// A Decoder interprets CUE expressions as JSON protobuf encodings
38// based on an underlying schema.
39//
40// It bases the mapping on the underlying CUE type, without consulting Protobuf
41// attributes.
42//
43// Mappings per CUE type:
44//
45// for any CUE type:
46// null is omitted if null is not specifically allowed.
47// bytes: if the expression is a string, it is reinterpreted using a
48// base64 encoding. Either standard or URL-safe base64 encoding
49// with/without paddings are accepted.
50// int: string values are interpreted as integers
51// float: string values are interpreted as numbers, and the values "NaN",
52// "Infinity", and "-Infinity" are allowed and converted
53// to corresponding error values.
54// enums: if a field is of type int and does not have a standard integer
55// type for its @protobuf attribute, this is assumed to represent
56// a protobuf enum value. Enum names are converted to integers
57// by interpreting the definitions of the disjunction constants
58// as the symbol names.
59// If CUE uses the string representation for enums, then an
60// #enumValue integer associated with the string value is used
61// for the conversion.
62// {}: JSON objects representing any values will be left as is.
63// If the CUE type corresponding to the URL can be determined within
64// the module context it will be unified.
65// time.Time / time.Duration:
66// left as is
67// _: left as is.
68type Decoder struct {
69 schema cue.Value
70}
71
72// NewDecoder creates a Decoder for the given schema.
73func NewDecoder(schema cue.Value, options ...Option) *Decoder {
74 return &Decoder{schema: schema}
75}
76
77// RewriteFile modifies file, interpreting it in terms of the given schema
78// according to the protocol buffer to JSON mapping defined in the protocol
79// buffer spec.
80//
81// RewriteFile is idempotent, calling it multiple times on an expression gives
82// the same result.
83func (d *Decoder) RewriteFile(file *ast.File) error {
84 var r rewriter
85 r.rewriteDecls(d.schema, file.Decls)
86 return r.errs
87}
88
89// RewriteExpr modifies expr, interpreting it in terms of the given schema
90// according to the protocol buffer to JSON mapping defined in the
91// protocol buffer spec.
92//
93// RewriteExpr is idempotent, calling it multiples times on an expression gives
94// the same result.
95func (d *Decoder) RewriteExpr(expr ast.Expr) (ast.Expr, error) {
96 var r rewriter
97 x := r.rewrite(d.schema, expr)
98 return x, r.errs
99}
100
101type rewriter struct {
102 errs errors.Error
103}
104
105func (r *rewriter) addErr(err errors.Error) {
106 r.errs = errors.Append(r.errs, err)
107}
108
109func (r *rewriter) addErrf(p token.Pos, schema cue.Value, format string, args ...interface{}) {
110 format = "%s: " + format
111 args = append([]interface{}{schema.Path()}, args...)
112 r.addErr(errors.Newf(p, format, args...))
113}
114
115func (r *rewriter) rewriteDecls(schema cue.Value, decls []ast.Decl) {
116 for _, f := range decls {
117 field, ok := f.(*ast.Field)
118 if !ok {
119 continue
120 }
121 sel := cue.Label(field.Label)
122 if !sel.IsString() {
123 continue
124 }
125
126 v := schema.LookupPath(cue.MakePath(sel))
127 if !v.Exists() {
128 f := schema.Template()
129 if f == nil {
130 continue
131 }
132 v = f(sel.String())
133 }
134 if !v.Exists() {
135 continue
136 }
137
138 field.Value = r.rewrite(v, field.Value)
139 }
140}
141
142var enumValuePath = cue.ParsePath("#enumValue").Optional()
143
144func (r *rewriter) rewrite(schema cue.Value, expr ast.Expr) (x ast.Expr) {
145 defer func() {
146 if expr != x && x != nil {
147 astutil.CopyMeta(x, expr)
148 }
149 }()
150
151 switch x := expr.(type) {
152 case *ast.BasicLit:
153 if x.Kind != token.NULL {
154 break
155 }
156 if schema.IncompleteKind()&cue.NullKind != 0 {
157 break
158 }
159 switch v, _ := schema.Default(); {
160 case v.IsConcrete():
161 if x, _ := v.Syntax(cue.Final()).(ast.Expr); x != nil {
162 return x
163 }
164 default: // default value for type
165 if x := zeroValue(schema, x); x != nil {
166 return x
167 }
168 }
169
170 case *ast.StructLit:
171 r.rewriteDecls(schema, x.Elts)
172 return x
173
174 case *ast.ListLit:
175 elem := schema.LookupPath(cue.MakePath(cue.AnyIndex))
176 iter, _ := schema.List()
177 for i, e := range x.Elts {
178 v := elem
179 if iter.Next() {
180 v = iter.Value()
181 }
182 if !v.Exists() {
183 break
184 }
185 x.Elts[i] = r.rewrite(v, e)
186 }
187
188 return x
189 }
190
191 switch schema.IncompleteKind() {
192 case cue.IntKind, cue.FloatKind, cue.NumberKind:
193 x, q, str := stringValue(expr)
194 if x == nil || !q.IsDouble() {
195 break
196 }
197
198 var info literal.NumInfo
199 if err := literal.ParseNum(str, &info); err == nil {
200 x.Value = str
201 x.Kind = token.FLOAT
202 if info.IsInt() {
203 x.Kind = token.INT
204 }
205 break
206 }
207
208 pbinternal.MatchBySymbol(schema, str, x)
209
210 case cue.BytesKind:
211 x, q, str := stringValue(expr)
212 if x == nil && q.IsDouble() {
213 break
214 }
215
216 var b []byte
217 var err error
218 for _, enc := range base64Encodings {
219 if b, err = enc.DecodeString(str); err == nil {
220 break
221 }
222 }
223 if err != nil {
224 r.addErrf(expr.Pos(), schema, "failed to decode base64: %v", err)
225 return expr
226 }
227
228 quoter := literal.Bytes
229 if q.IsMulti() {
230 ws := q.Whitespace()
231 tabs := (strings.Count(ws, " ")+3)/4 + strings.Count(ws, "\t")
232 quoter = quoter.WithTabIndent(tabs)
233 }
234 x.Value = quoter.Quote(string(b))
235 return x
236
237 case cue.StringKind:
238 if s, ok := expr.(*ast.BasicLit); ok && s.Kind == token.INT {
239 var info literal.NumInfo
240 if err := literal.ParseNum(s.Value, &info); err != nil || !info.IsInt() {
241 break
242 }
243 var d apd.Decimal
244 if err := info.Decimal(&d); err != nil {
245 break
246 }
247 enum, err := d.Int64()
248 if err != nil {
249 r.addErrf(expr.Pos(), schema, "invalid enum index: %v", err)
250 return expr
251 }
252 op, values := schema.Expr()
253 if op != cue.OrOp {
254 values = []cue.Value{schema} // allow single values.
255 }
256 for _, v := range values {
257 i, err := v.LookupPath(enumValuePath).Int64()
258 if err == nil && i == enum {
259 str, err := v.String()
260 if err != nil {
261 r.addErr(errors.Wrapf(err, v.Pos(), "invalid string enum"))
262 return expr
263 }
264 s.Kind = token.STRING
265 s.Value = literal.String.Quote(str)
266
267 return s
268 }
269 }
270 r.addErrf(expr.Pos(), schema,
271 "could not locate integer enum value %d", enum)
272 }
273
274 case cue.StructKind, cue.TopKind:
275 // TODO: Detect and mix in type.
276 }
277 return expr
278}
279
280func zeroValue(v cue.Value, x *ast.BasicLit) ast.Expr {
281 switch v.IncompleteKind() {
282 case cue.StringKind:
283 x.Kind = token.STRING
284 x.Value = `""`
285
286 case cue.BytesKind:
287 x.Kind = token.STRING
288 x.Value = `''`
289
290 case cue.BoolKind:
291 x.Kind = token.FALSE
292 x.Value = "false"
293
294 case cue.NumberKind, cue.IntKind, cue.FloatKind:
295 x.Kind = token.INT
296 x.Value = "0"
297
298 case cue.StructKind:
299 return ast.NewStruct()
300
301 case cue.ListKind:
302 return &ast.ListLit{}
303
304 default:
305 return nil
306 }
307 return x
308}
309
310func stringValue(x ast.Expr) (b *ast.BasicLit, q literal.QuoteInfo, str string) {
311 b, ok := x.(*ast.BasicLit)
312 if !ok || b.Kind != token.STRING {
313 return nil, q, ""
314 }
315 q, p, _, err := literal.ParseQuotes(b.Value, b.Value)
316 if err != nil {
317 return nil, q, ""
318 }
319
320 str, err = q.Unquote(b.Value[p:])
321 if err != nil {
322 return nil, q, ""
323 }
324
325 return b, q, str
326}
327
328// These are all the allowed base64 encodings.
329var base64Encodings = []base64.Encoding{
330 *base64.StdEncoding,
331 *base64.URLEncoding,
332 *base64.RawStdEncoding,
333 *base64.RawURLEncoding,
334}