Skip to content
This repository has been archived by the owner on Nov 18, 2021. It is now read-only.

Commit

Permalink
encoding/protobuf/jsonpb: add encoder
Browse files Browse the repository at this point in the history
Issue #606

Change-Id: I3bf6cbc1ecd5e83c94b5a7d97c845a35f4b84878
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9371
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
  • Loading branch information
mpvl committed Apr 15, 2021
1 parent a0035de commit 362e3a5
Show file tree
Hide file tree
Showing 8 changed files with 408 additions and 3 deletions.
7 changes: 4 additions & 3 deletions encoding/protobuf/jsonpb/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package jsonpb
package jsonpb_test

import (
"strings"
Expand All @@ -25,6 +25,7 @@ import (
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/parser"
"cuelang.org/go/encoding/json"
"cuelang.org/go/encoding/protobuf/jsonpb"
"cuelang.org/go/encoding/yaml"
"cuelang.org/go/internal/cuetest"
"cuelang.org/go/internal/cuetxtar"
Expand Down Expand Up @@ -85,7 +86,7 @@ func TestParse(t *testing.T) {
}

w := t.Writer(f.Name)
err := NewDecoder(schema).RewriteFile(file)
err := jsonpb.NewDecoder(schema).RewriteFile(file)
if err != nil {
errors.Print(w, err, nil)
continue
Expand Down Expand Up @@ -121,7 +122,7 @@ func TestX(t *testing.T) {
t.Fatal(err)
}

if err := NewDecoder(inst.Value()).RewriteFile(file); err != nil {
if err := jsonpb.NewDecoder(inst.Value()).RewriteFile(file); err != nil {
t.Fatal(err)
}

Expand Down
168 changes: 168 additions & 0 deletions encoding/protobuf/jsonpb/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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
}
84 changes: 84 additions & 0 deletions encoding/protobuf/jsonpb/encoder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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_test

import (
"testing"

"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/parser"
"cuelang.org/go/encoding/protobuf/jsonpb"
"cuelang.org/go/internal/cuetest"
"cuelang.org/go/internal/cuetxtar"
)

func TestEncoder(t *testing.T) {
test := cuetxtar.TxTarTest{
Root: "./testdata/encoder",
Name: "jsonpb",
Update: cuetest.UpdateGoldenFiles,
}

r := cue.Runtime{}

test.Run(t, func(t *cuetxtar.Test) {
// TODO: use high-level API.

var schema cue.Value
var file *ast.File

for _, f := range t.Archive.Files {
switch {
case f.Name == "schema.cue":
inst, err := r.Compile(f.Name, f.Data)
if err != nil {
t.WriteErrors(errors.Promote(err, "test"))
return
}
schema = inst.Value()

case f.Name == "value.cue":
f, err := parser.ParseFile(f.Name, f.Data, parser.ParseComments)
if err != nil {
t.WriteErrors(errors.Promote(err, "test"))
return
}
file = f
}
}

if !schema.Exists() {
inst, err := r.CompileFile(file)
if err != nil {
t.WriteErrors(errors.Promote(err, "test"))
}
schema = inst.Value()
}

err := jsonpb.NewEncoder(schema).RewriteFile(file)
if err != nil {
errors.Print(t, err, nil)
}

b, err := format.Node(file)
if err != nil {
t.Fatal(err)
}
_, _ = t.Write(b)
})
}
2 changes: 2 additions & 0 deletions encoding/protobuf/jsonpb/jsonpb.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@

// Package jsonpb rewrites a CUE expression based upon the Protobuf
// interpretation of JSON.
//
// API Status: DRAFT: API may change without notice.
package jsonpb
68 changes: 68 additions & 0 deletions encoding/protobuf/jsonpb/testdata/encoder/enums.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
-- schema.cue --
enum: [string]:
{ "foo", #enumValue: 1 } |
{ "bar", #enumValue: 2 } @protobuf(1,Enum)

defEnum: [string]: #foo | #bar @protobuf(2,Enum)

#foo: 1
#bar: 2

typeEnum: [string]: #Enum @protobuf(3,Enum)

#Enum: #foo | #bar

// TODO: consider supporting @symbol(foo) or @json(,symbol=foo)
// symbolEnum: [string]:
// { 1, @symbol(foo) } |
// { 2, @symbol(bar) }


singleEnum: #single @protobuf(3,Enum)

#single: 1

badEnum: { string, #enumValue: 1 } | { "two", #enumValue: 2 }


-- value.cue --
enum: asIs: "foo"
enum: asIsUnknown: "foobar"

// Convert integers to strings
defEnum: foo: 1
defEnum: bar: 2
defEnum: baz: 3


typeEnum: foo: 1
typeEnum: bar: 2
typeEnum: baz: 3


// TODO: consider supporting @symbol(foo) or @json(,symbol=foo)
// symbolEnum: foo: "foo"
// symbolEnum: bar: "bar"
// symbolEnum: baz: "baz"

singleEnum: 1

-- out/jsonpb --
enum: asIs: "foo"
enum: asIsUnknown: "foobar"

// Convert integers to strings
defEnum: foo: "foo"
defEnum: bar: "bar"
defEnum: baz: 3

typeEnum: foo: "foo"
typeEnum: bar: "bar"
typeEnum: baz: 3

// TODO: consider supporting @symbol(foo) or @json(,symbol=foo)
// symbolEnum: foo: "foo"
// symbolEnum: bar: "bar"
// symbolEnum: baz: "baz"

singleEnum: 1
27 changes: 27 additions & 0 deletions encoding/protobuf/jsonpb/testdata/encoder/list.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- schema.cue --
a: [...#D]

#D: {
a: int @protobuf(1,int64)
}

b: [1, ...] // Don't include schema fields if not in value

c: [{a: 1}, ...]
c: [...#D]

-- value.cue --
// Hello
a: [
{a: 1},
]

c: [{a: 1}, {a: 2}]

-- out/jsonpb --
// Hello
a: [
{a: "1"},
]

c: [{a: "1"}, {a: "2"}]
Loading

0 comments on commit 362e3a5

Please sign in to comment.