| // 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) addErr(err errors.Error) { |
| e.errs = errors.Append(e.errs, err) |
| } |
| |
| func (e *encoder) addErrf(p token.Pos, schema cue.Value, format string, args ...interface{}) { |
| format = "%s: " + format |
| args = append([]interface{}{schema.Path()}, args...) |
| e.addErr(errors.Newf(p, format, args...)) |
| } |
| |
| 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 |
| } |