diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b46bde0..e523be42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: test: strategy: matrix: - go-version: [1.24.x, 1.25.x] + go-version: [1.24.x, 1.25.x, 1.26.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} timeout-minutes: 10 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 62015cdf..4c3748c3 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -21,7 +21,7 @@ jobs: linters: strategy: matrix: - go-version: [1.25.x] + go-version: [1.26.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} timeout-minutes: 10 diff --git a/Makefile b/Makefile index 086a37f0..cb02f730 100644 --- a/Makefile +++ b/Makefile @@ -55,8 +55,8 @@ ci: prepare if [ `arch` == 'x86_64' ]; then \ sudo apt-get -y -q update; \ sudo apt-get -y -q install build-essential; \ - wget -q https://github.com/tinygo-org/tinygo/releases/download/v0.39.0/tinygo_0.39.0_amd64.deb; \ - sudo dpkg -i tinygo_0.39.0_amd64.deb; \ + wget -q https://github.com/tinygo-org/tinygo/releases/download/v0.41.1/tinygo_0.41.1_amd64.deb; \ + sudo dpkg -i tinygo_0.41.1_amd64.deb; \ export PATH=$$PATH:/usr/local/tinygo/bin; \ fi go test -v ./... ./_generated diff --git a/_generated/custom_tags.go b/_generated/custom_tags.go new file mode 100644 index 00000000..b29ab478 --- /dev/null +++ b/_generated/custom_tags.go @@ -0,0 +1,14 @@ +package _generated + +//go:generate msgp -d "tags primary,fallback" + +type CustomTags struct { + Field1 string `primary:"f1_primary"` + Field2 string `fallback:"f2_fallback"` + Field3 string `primary:"f3_primary" fallback:"f3_fallback_ignored"` + Field4 string `msg:"f4_msg"` + Field5 string `msgpack:"f5_msgpack"` + Field6 string `fallback:"f6_fallback" msg:"f6_msg_ignored"` + Field7 string `fallback:"f7_fallback" msgpack:"f7_msgpack_ignored"` + Field8 string `primary:"f8_primary" msg:"f8_msg_ignored" msgpack:"f8_msgpack_ignored"` +} diff --git a/_generated/custom_tags_test.go b/_generated/custom_tags_test.go new file mode 100644 index 00000000..7a1c72d8 --- /dev/null +++ b/_generated/custom_tags_test.go @@ -0,0 +1,93 @@ +package _generated + +import ( + "bytes" + "encoding/json" + "reflect" + "testing" + + "github.com/tinylib/msgp/msgp" +) + +func TestCustomTags(t *testing.T) { + ts := CustomTags{ + Field1: "v1", + Field2: "v2", + Field3: "v3", + Field4: "v4", + Field5: "v5", + Field6: "v6", + Field7: "v7", + Field8: "v8", + } + wantKeys := map[string]any{ + "f1_primary": "v1", + "f2_fallback": "v2", + "f3_primary": "v3", + "f4_msg": "v4", + "f5_msgpack": "v5", + "f6_fallback": "v6", + "f7_fallback": "v7", + "f8_primary": "v8", + } + + t.Run("EncodeDecode", func(t *testing.T) { + var b bytes.Buffer + if err := msgp.Encode(&b, &ts); err != nil { + t.Fatal(err) + } + var got CustomTags + if err := msgp.Decode(&b, &got); err != nil { + t.Fatal(err) + } + if got != ts { + t.Errorf("got %+v, want %+v", got, ts) + } + }) + + t.Run("MarshalUnmarshal", func(t *testing.T) { + buf, err := ts.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + var got CustomTags + left, err := got.UnmarshalMsg(buf) + if err != nil { + t.Fatal(err) + } + if len(left) != 0 { + t.Errorf("%d bytes left after unmarshal", len(left)) + } + if got != ts { + t.Errorf("got %+v, want %+v", got, ts) + } + }) + + t.Run("WireKeys", func(t *testing.T) { + buf, err := ts.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + var jsonBuf bytes.Buffer + if _, err := msgp.UnmarshalAsJSON(&jsonBuf, buf); err != nil { + t.Fatal(err) + } + got := map[string]any{} + if err := json.Unmarshal(jsonBuf.Bytes(), &got); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, wantKeys) { + t.Errorf("got %v, want %v", got, wantKeys) + } + }) + + t.Run("Msgsize", func(t *testing.T) { + buf, err := ts.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + if est := ts.Msgsize(); est < len(buf) { + t.Errorf("Msgsize %d underestimates actual %d", est, len(buf)) + } + }) +} diff --git a/parse/directives.go b/parse/directives.go index b03eeb86..597ac21f 100644 --- a/parse/directives.go +++ b/parse/directives.go @@ -48,6 +48,7 @@ var directives = map[string]directive{ // and then add it to this list. var earlyDirectives = map[string]directive{ "tag": tag, + "tags": tag, "pointer": pointer, "maps": maps, } @@ -237,12 +238,28 @@ func asvartuple(text []string, f *FileSet) error { } //msgp:tag {tagname} +//msgp:tags {tag1},{tag2},... +// +// The tag/tags directive accepts a comma-separated priority list; fields are +// read from the first tag that has a non-empty value, falling back to msg +// then msgpack if none of the listed tags match. func tag(text []string, f *FileSet) error { - if len(text) != 2 { + if len(text) < 2 { return nil } - f.tagName = strings.TrimSpace(text[1]) - infof("using field tag %q\n", f.tagName) + var names []string + for _, t := range text[1:] { + for n := range strings.SplitSeq(t, ",") { + if n = strings.TrimSpace(n); n != "" { + names = append(names, n) + } + } + } + + f.tagNames = append(names, "msg", "msgpack") // Add defaults + if len(names) > 0 { + infof("using field tags %q\n", strings.Join(names, ",")) + } return nil } diff --git a/parse/getast.go b/parse/getast.go index 4e69ecd0..bc5c7cb0 100644 --- a/parse/getast.go +++ b/parse/getast.go @@ -45,8 +45,8 @@ type FileSet struct { NoDuplicates bool // Reject duplicate keys for all types NoDupTypes map[string]struct{} // Reject duplicate keys for specific types only - tagName string // tag to read field names from - pointerRcv bool // generate with pointer receivers. + tagNames []string // tags to read field names from, in priority order + pointerRcv bool // generate with pointer receivers. } // File parses a file at the relative path @@ -65,6 +65,7 @@ func File(name string, unexported bool, directives []string) (*FileSet, error) { Directives: append([]string{}, directives...), ArrayLimit: math.MaxUint32, MapLimit: math.MaxUint32, + tagNames: []string{"msg", "msgpack"}, } fset := token.NewFileSet() @@ -527,15 +528,12 @@ func (fs *FileSet) getField(f *ast.Field) []gen.StructField { var extension, flatten bool // parse tag; otherwise field name is field tag if f.Tag != nil { + st := reflect.StructTag(strings.Trim(f.Tag.Value, "`")) var body string - if fs.tagName != "" { - body = reflect.StructTag(strings.Trim(f.Tag.Value, "`")).Get(fs.tagName) - } - if body == "" { - body = reflect.StructTag(strings.Trim(f.Tag.Value, "`")).Get("msg") - } - if body == "" { - body = reflect.StructTag(strings.Trim(f.Tag.Value, "`")).Get("msgpack") + for _, name := range fs.tagNames { + if body = st.Get(name); body != "" { + break + } } tags := strings.Split(body, ",") if len(tags) >= 2 {