cue-lang / cue

The home of the CUE language! Validate and define text-based and dynamic configuration
https://cuelang.org
Apache License 2.0
5.02k stars 287 forks source link

cue: EncodeType doesn't unify embedded Go struct marked with a json inline tag with struct definition #1772

Open yangzhares opened 2 years ago

yangzhares commented 2 years ago

What version of CUE are you using (cue version)?

$ cue version
cue version v0.4.3 darwin/amd64

$ go version
go version go1.17.6 darwin/amd64

Does this issue reproduce with the latest release?

Yes

What did you do?

When did convert a Go type to CUE value via EncodeType, embedded Go struct marked with a json inline tag doesn't unify with struct definition, an empty CUE field will map to CUE value of embedded Go struct.

Reproduce this via below code:

package main

import (
    "fmt"

    "cuelang.org/go/cue/cuecontext"
    "cuelang.org/go/cue/format"
)

func main() {
    ctx := cuecontext.New()
    val := ctx.EncodeType(&Spec{})
    node := val.Syntax()
    raw, _ := format.Node(node)
    fmt.Println(string(raw))
}

type Spec struct {
    Example Example `json:"example"`
}

type Example struct {
    Hello `json:",inline"`
    World `json:",inline"`
}

type Hello struct {
    Hello string `json:"hello"`
}

type World struct {
    World string `json:"world"`
}

What did you expect to see?

{
    example: {
        hello: string
        world: string
    }
}

What did you see instead?

{
    example: {
        "": {
            hello: string
            world: string
        }
    }
}
rogpeppe commented 2 years ago

Nice find! This does indeed look like a bug with the Go-to-CUE type conversion logic. Thanks for the nice small reproducer too. Here's some slightly simpler code that reproduces the issue and also verifies that the Go type does encode correctly:

package main

import (
    "encoding/json"
    "fmt"

    "cuelang.org/go/cue/cuecontext"
    "cuelang.org/go/cue/format"
)

func main() {
    ctx := cuecontext.New()
    val := ctx.EncodeType(Example{})
    node := val.Syntax()
    raw, _ := format.Node(node)
    fmt.Printf("-- gotype.cue --\n%s\n", raw)
    data, _ := json.Marshal(Example{
        Hello: Hello{"one"},
    })
    fmt.Printf("-- gotype.json --\n%s\n", data)
}

type Example struct {
    Hello
}

type Hello struct {
    Hello string `json:"hello"`
}

This prints the following on my machine (with CUE v0.4.3):

-- gotype.cue --
{
    "": {
        hello: string
    }
}
-- gotype.json --
{"hello":"one"}
jlarfors commented 10 months ago

I ran into this today, using structs from a 3rd party package. My workaround was to create local versions of those structs, with the added json tags.

Are there any updates on the issue? I'm working on something that could expose this to end users, which would be a potential stoppper...

jlarfors commented 10 months ago

After more testing I realised the 3rd party structs I was using had nested embedding on quite some levels... Hence I wrote a hacky little function to unify embedded structs with it's parent. This might be helpful for others coming here who want a hacky fix:

func cueRemoveEmbeddedAnomaly(val cue.Value) (cue.Value, error) {
    iter, err := val.Fields(cue.All())
    if err != nil {
        return val, err
    }
    newVal := val.Context().CompileString("{}")
    for iter.Next() {
        field := iter.Value()
        fieldPath := cue.MakePath(iter.Selector())
        label, ok := field.Label()
        if !ok {
            continue
        }

        if field.Kind() != cue.StructKind {
            newVal = newVal.FillPath(fieldPath, field)
            continue
        }

        child, err := cueRemoveEmbeddedAnomaly(field)
        if err != nil {
            return val, err
        }
        if label == "" {
            newVal = newVal.Unify(child)
            continue
        }
        newVal = newVal.FillPath(fieldPath, child)
    }

    return newVal, nil
}

Now if you run your test like this, it should give the desired output:


func TestEncodeTypeWithEmbeddedHack(t *testing.T) {
    type Hello struct {
        Hello string `json:"hello"`
    }
    type Example struct {
        Hello
    }

    ctx := cuecontext.New()
    val := ctx.EncodeType(Example{})
    val, err := cueRemoveEmbeddedAnomaly(val)
    if err != nil {
        t.Fatal(err)
    }
    node := val.Syntax()
    raw, _ := format.Node(node)
    fmt.Printf("-- gotype.cue --\n%s\n", raw)
    data, _ := json.Marshal(Example{
        Hello: Hello{"one"},
    })
    fmt.Printf("-- gotype.json --\n%s\n", data)
}