// 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 jsonpb import ( "strconv" "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" ) // TODO: Options: // - Convert integer strings. // - URL encoder // - URL decoder // An Encoder rewrites CUE values according to the Protobuf to JSON mappings, // based on a given CUE schema. // // It bases the mapping on the underlying CUE type, without consulting Protobuf // attributes. // // Mappings per CUE type: // // for any CUE type: // int: if the expression value is an integer and the schema value is // an int64, it is converted to a string. // {}: JSON objects representing any values will be left as is. // If the CUE type corresponding to the URL can be determined within // the module context it will be unified. // _: Adds a `@type` URL (TODO). type Encoder struct { schema cue.Value } // NewEncoder creates an Encoder for the given schema. func NewEncoder(schema cue.Value, options ...Option) *Encoder { return &Encoder{schema: schema} } // RewriteFile modifies file, modifying it to conform to the Protocol buffer // to JSON mapping it in terms of the given schema. // // RewriteFile is idempotent, calling it multiples times on an expression gives // the same result. func (e *Encoder) RewriteFile(file *ast.File) error { var enc encoder enc.rewriteDecls(e.schema, file.Decls) return enc.errs } // RewriteExpr modifies file, modifying it to conform to the Protocol buffer // to JSON mapping it in terms of the given schema. // // RewriteExpr is idempotent, calling it multiples times on an expression gives // the same result. func (e *Encoder) RewriteExpr(expr ast.Expr) (ast.Expr, error) { var enc encoder x := enc.rewrite(e.schema, expr) return x, enc.errs } type encoder struct { errs errors.Error } func (e *encoder) rewriteDecls(schema cue.Value, decls []ast.Decl) { for _, f := range decls { field, ok := f.(*ast.Field) if !ok { continue } sel := cue.Label(field.Label) if !sel.IsString() { continue } v := schema.LookupPath(cue.MakePath(sel.Optional())) if !v.Exists() { continue } field.Value = e.rewrite(v, field.Value) } } func (e *encoder) rewrite(schema cue.Value, expr ast.Expr) (x ast.Expr) { switch x := expr.(type) { case *ast.ListLit: for i, elem := range x.Elts { v := schema.LookupPath(cue.MakePath(cue.Index(i).Optional())) if !v.Exists() { break } x.Elts[i] = e.rewrite(v, elem) } return expr case *ast.StructLit: e.rewriteDecls(schema, x.Elts) return expr case *ast.BasicLit: if x.Kind != token.INT { break } info, err := pbinternal.FromValue("", schema) if err != nil { break } switch info.Type { case "int64", "fixed64", "sfixed64", "uint64": b, ok := expr.(*ast.BasicLit) if schema.IncompleteKind() == cue.IntKind && ok && b.Kind == token.INT { b.Kind = token.STRING b.Value = literal.String.Quote(b.Value) } case "int32", "fixed32", "sfixed32", "uint32", "float", "double": case "varint": default: if !info.IsEnum { break } i, err := strconv.ParseInt(x.Value, 10, 32) if err != nil { break } if s := pbinternal.MatchByInt(schema, i); s != "" { x.Kind = token.STRING x.Value = literal.String.Quote(s) } } } return expr }