this repo has no description
at master 334 lines 8.4 kB view raw
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}