Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
protobuf: fix casing of json attributes with the switch from gogo
Browse files Browse the repository at this point in the history
With the switch from gogo, the `oneof` fields no longer have their
`json:"name"` fields emitted. Protobuf doesn't formally support these
fields and tells users to use the `protojson` package, but we didn't and
the official format is incompatible with the current format we use.

This adds some custom marshaling code to the already existant custom
unmarshaling code to ensure these fields are marshaled with the correct
casing.

Signed-off-by: Jonathan A. Sternberg <[email protected]>
jsternberg committed Nov 15, 2024
1 parent 9a33f71 commit 7a15c23
Showing 2 changed files with 291 additions and 81 deletions.
162 changes: 108 additions & 54 deletions solver/pb/json.go
Original file line number Diff line number Diff line change
@@ -2,58 +2,100 @@ package pb

import "encoding/json"

func (m *Op) UnmarshalJSON(data []byte) error {
var v struct {
Inputs []*Input `json:"inputs,omitempty"`
Op struct {
*Op_Exec
*Op_Source
*Op_File
*Op_Build
*Op_Merge
*Op_Diff
}
Platform *Platform `json:"platform,omitempty"`
Constraints *WorkerConstraints `json:"constraints,omitempty"`
type jsonOp struct {
Inputs []*Input `json:"inputs,omitempty"`
Op struct {
Exec *ExecOp `json:"exec,omitempty"`
Source *SourceOp `json:"source,omitempty"`
File *FileOp `json:"file,omitempty"`
Build *BuildOp `json:"build,omitempty"`
Merge *MergeOp `json:"merge,omitempty"`
Diff *DiffOp `json:"diff,omitempty"`
}
Platform *Platform `json:"platform,omitempty"`
Constraints *WorkerConstraints `json:"constraints,omitempty"`
}

func (m *Op) MarshalJSON() ([]byte, error) {
var v jsonOp
v.Inputs = m.Inputs
switch op := m.Op.(type) {
case *Op_Exec:
v.Op.Exec = op.Exec
case *Op_Source:
v.Op.Source = op.Source
case *Op_File:
v.Op.File = op.File
case *Op_Build:
v.Op.Build = op.Build
case *Op_Merge:
v.Op.Merge = op.Merge
case *Op_Diff:
v.Op.Diff = op.Diff
}
v.Platform = m.Platform
v.Constraints = m.Constraints
return json.Marshal(v)
}

func (m *Op) UnmarshalJSON(data []byte) error {
var v jsonOp
if err := json.Unmarshal(data, &v); err != nil {
return err
}

m.Inputs = v.Inputs
switch {
case v.Op.Op_Exec != nil:
m.Op = v.Op.Op_Exec
case v.Op.Op_Source != nil:
m.Op = v.Op.Op_Source
case v.Op.Op_File != nil:
m.Op = v.Op.Op_File
case v.Op.Op_Build != nil:
m.Op = v.Op.Op_Build
case v.Op.Op_Merge != nil:
m.Op = v.Op.Op_Merge
case v.Op.Op_Diff != nil:
m.Op = v.Op.Op_Diff
case v.Op.Exec != nil:
m.Op = &Op_Exec{v.Op.Exec}
case v.Op.Source != nil:
m.Op = &Op_Source{v.Op.Source}
case v.Op.File != nil:
m.Op = &Op_File{v.Op.File}
case v.Op.Build != nil:
m.Op = &Op_Build{v.Op.Build}
case v.Op.Merge != nil:
m.Op = &Op_Merge{v.Op.Merge}
case v.Op.Diff != nil:
m.Op = &Op_Diff{v.Op.Diff}
}
m.Platform = v.Platform
m.Constraints = v.Constraints
return nil
}

func (m *FileAction) UnmarshalJSON(data []byte) error {
var v struct {
Input InputIndex `json:"input"`
SecondaryInput InputIndex `json:"secondaryInput"`
Output OutputIndex `json:"output"`
Action struct {
*FileAction_Copy
*FileAction_Mkfile
*FileAction_Mkdir
*FileAction_Rm
}
type jsonFileAction struct {
Input InputIndex `json:"input"`
SecondaryInput InputIndex `json:"secondaryInput"`
Output OutputIndex `json:"output"`
Action struct {
Copy *FileActionCopy `json:"copy,omitempty"`
Mkfile *FileActionMkFile `json:"mkfile,omitempty"`
Mkdir *FileActionMkDir `json:"mkdir,omitempty"`
Rm *FileActionRm `json:"rm,omitempty"`
}
}

func (m *FileAction) MarshalJSON() ([]byte, error) {
var v jsonFileAction
v.Input = InputIndex(m.Input)
v.SecondaryInput = InputIndex(m.SecondaryInput)
v.Output = OutputIndex(m.Output)
switch action := m.Action.(type) {
case *FileAction_Copy:
v.Action.Copy = action.Copy
case *FileAction_Mkfile:
v.Action.Mkfile = action.Mkfile
case *FileAction_Mkdir:
v.Action.Mkdir = action.Mkdir
case *FileAction_Rm:
v.Action.Rm = action.Rm
}
return json.Marshal(v)
}

func (m *FileAction) UnmarshalJSON(data []byte) error {
var v jsonFileAction
if err := json.Unmarshal(data, &v); err != nil {
return err
}
@@ -62,35 +104,47 @@ func (m *FileAction) UnmarshalJSON(data []byte) error {
m.SecondaryInput = int64(v.SecondaryInput)
m.Output = int64(v.Output)
switch {
case v.Action.FileAction_Copy != nil:
m.Action = v.Action.FileAction_Copy
case v.Action.FileAction_Mkfile != nil:
m.Action = v.Action.FileAction_Mkfile
case v.Action.FileAction_Mkdir != nil:
m.Action = v.Action.FileAction_Mkdir
case v.Action.FileAction_Rm != nil:
m.Action = v.Action.FileAction_Rm
case v.Action.Copy != nil:
m.Action = &FileAction_Copy{v.Action.Copy}
case v.Action.Mkfile != nil:
m.Action = &FileAction_Mkfile{v.Action.Mkfile}
case v.Action.Mkdir != nil:
m.Action = &FileAction_Mkdir{v.Action.Mkdir}
case v.Action.Rm != nil:
m.Action = &FileAction_Rm{v.Action.Rm}
}
return nil
}

func (m *UserOpt) UnmarshalJSON(data []byte) error {
var v struct {
User struct {
*UserOpt_ByName
*UserOpt_ByID
}
type jsonUserOpt struct {
User struct {
ByName *NamedUserOpt `json:"byName,omitempty"`
ByID uint32 `json:"byId,omitempty"`
}
}

func (m *UserOpt) MarshalJSON() ([]byte, error) {
var v jsonUserOpt
switch userOpt := m.User.(type) {
case *UserOpt_ByName:
v.User.ByName = userOpt.ByName
case *UserOpt_ByID:
v.User.ByID = userOpt.ByID
}
return json.Marshal(v)
}

func (m *UserOpt) UnmarshalJSON(data []byte) error {
var v jsonUserOpt
if err := json.Unmarshal(data, &v); err != nil {
return err
}

switch {
case v.User.UserOpt_ByName != nil:
m.User = v.User.UserOpt_ByName
case v.User.UserOpt_ByID != nil:
m.User = v.User.UserOpt_ByID
case v.User.ByName != nil:
m.User = &UserOpt_ByName{v.User.ByName}
default:
m.User = &UserOpt_ByID{v.User.ByID}
}
return nil
}
210 changes: 183 additions & 27 deletions solver/pb/json_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package pb

import (
"encoding/base64"
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
)

func TestOp_UnmarshalJSON(t *testing.T) {
func TestJSON_Op(t *testing.T) {
for _, tt := range []struct {
name string
op *Op
exp any
}{
{
name: "exec",
@@ -26,6 +28,18 @@ func TestOp_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"Op": map[string]any{
"exec": map[string]any{
"meta": map[string]any{
"args": []any{"echo", "Hello", "World"},
},
"mounts": []any{
map[string]any{"dest": "/", "readonly": true},
},
},
},
},
},
{
name: "source",
@@ -37,6 +51,14 @@ func TestOp_UnmarshalJSON(t *testing.T) {
},
Constraints: &WorkerConstraints{},
},
exp: map[string]any{
"Op": map[string]any{
"source": map[string]any{
"identifier": "local://context",
},
},
"constraints": map[string]any{},
},
},
{
name: "file",
@@ -58,6 +80,25 @@ func TestOp_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"Op": map[string]any{
"file": map[string]any{
"actions": []any{
map[string]any{
"Action": map[string]any{
"copy": map[string]any{
"src": "/foo",
"dest": "/bar",
},
},
"input": 1.0,
"secondaryInput": 0.0,
"output": 2.0,
},
},
},
},
},
},
{
name: "build",
@@ -68,6 +109,13 @@ func TestOp_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"Op": map[string]any{
"build": map[string]any{
"def": map[string]any{},
},
},
},
},
{
name: "merge",
@@ -81,6 +129,16 @@ func TestOp_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"Op": map[string]any{
"merge": map[string]any{
"inputs": []any{
map[string]any{},
map[string]any{"input": 1.0},
},
},
},
},
},
{
name: "diff",
@@ -92,27 +150,46 @@ func TestOp_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"Op": map[string]any{
"diff": map[string]any{
"lower": map[string]any{},
"upper": map[string]any{"input": 1.0},
},
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
// Marshal the operation.
out, err := json.Marshal(tt.op)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)

// Test that we received the correct json object.
t.Run("Marshal", func(t *testing.T) {
var got any
err := json.Unmarshal(out, &got)
require.NoError(t, err)
require.Equal(t, tt.exp, got)
})

exp, got := tt.op, &Op{}
if err := json.Unmarshal(out, got); err != nil {
t.Fatal(err)
}
require.Equal(t, exp, got)
// Verify that unmarshaling the same JSON results in the
// same operation.
t.Run("Unmarshal", func(t *testing.T) {
exp, got := tt.op, &Op{}
err := json.Unmarshal(out, got)
require.NoError(t, err)
require.Equal(t, exp, got)
})
})
}
}

func TestFileAction_UnmarshalJSON(t *testing.T) {
func TestJSON_FileAction(t *testing.T) {
for _, tt := range []struct {
name string
fileAction *FileAction
exp any
}{
{
name: "copy",
@@ -124,6 +201,17 @@ func TestFileAction_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"Action": map[string]any{
"copy": map[string]any{
"src": "/foo",
"dest": "/bar",
},
},
"input": 0.0,
"secondaryInput": 0.0,
"output": 0.0,
},
},
{
name: "mkfile",
@@ -135,6 +223,17 @@ func TestFileAction_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"Action": map[string]any{
"mkfile": map[string]any{
"path": "/foo",
"data": b64str([]byte("Hello, World!")),
},
},
"input": 0.0,
"secondaryInput": 0.0,
"output": 0.0,
},
},
{
name: "mkdir",
@@ -146,6 +245,17 @@ func TestFileAction_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"Action": map[string]any{
"mkdir": map[string]any{
"path": "/foo/bar",
"makeParents": true,
},
},
"input": 0.0,
"secondaryInput": 0.0,
"output": 0.0,
},
},
{
name: "rm",
@@ -157,27 +267,48 @@ func TestFileAction_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"Action": map[string]any{
"rm": map[string]any{
"path": "/foo",
"allowNotFound": true,
},
},
"input": 0.0,
"secondaryInput": 0.0,
"output": 0.0,
},
},
} {
t.Run(tt.name, func(t *testing.T) {
out, err := json.Marshal(tt.fileAction)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)

// Test that we received the correct json object.
t.Run("Marshal", func(t *testing.T) {
var got any
err := json.Unmarshal(out, &got)
require.NoError(t, err)
require.Equal(t, tt.exp, got)
})

exp, got := tt.fileAction, &FileAction{}
if err := json.Unmarshal(out, got); err != nil {
t.Fatal(err)
}
require.Equal(t, exp, got)
// Verify that unmarshaling the same JSON results in the
// same file action.
t.Run("Unmarshal", func(t *testing.T) {
exp, got := tt.fileAction, &FileAction{}
err := json.Unmarshal(out, got)
require.NoError(t, err)
require.Equal(t, exp, got)
})
})
}
}

func TestUserOpt_UnmarshalJSON(t *testing.T) {
func TestJSON_UserOpt(t *testing.T) {
for _, tt := range []struct {
name string
userOpt *UserOpt
exp any
}{
{
name: "byName",
@@ -188,6 +319,13 @@ func TestUserOpt_UnmarshalJSON(t *testing.T) {
},
},
},
exp: map[string]any{
"User": map[string]any{
"byName": map[string]any{
"name": "foo",
},
},
},
},
{
name: "byId",
@@ -196,19 +334,37 @@ func TestUserOpt_UnmarshalJSON(t *testing.T) {
ByID: 2,
},
},
exp: map[string]any{
"User": map[string]any{
"byId": 2.0,
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
out, err := json.Marshal(tt.userOpt)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)

// Test that we received the correct json object.
t.Run("Marshal", func(t *testing.T) {
var got any
err := json.Unmarshal(out, &got)
require.NoError(t, err)
require.Equal(t, tt.exp, got)
})

exp, got := tt.userOpt, &UserOpt{}
if err := json.Unmarshal(out, got); err != nil {
t.Fatal(err)
}
require.Equal(t, exp, got)
// Verify that unmarshaling the same JSON results in the
// same user option.
t.Run("Unmarshal", func(t *testing.T) {
exp, got := tt.userOpt, &UserOpt{}
err := json.Unmarshal(out, got)
require.NoError(t, err)
require.Equal(t, exp, got)
})
})
}
}

func b64str(src []byte) string {
return string(base64.StdEncoding.AppendEncode(nil, src))
}

0 comments on commit 7a15c23

Please sign in to comment.