Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-go-cover.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: "1.23"
go-version: "1.26"
- name: Checkout code
uses: actions/checkout@v2
- name: Go Coverage
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: "1.23"
go-version: "1.26"
- name: Checkout code
uses: actions/checkout@v2
- name: Get dependencies
Expand Down
17 changes: 5 additions & 12 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
# Go Linters - GitHub Actions
name: linters
on: [push]
jobs:

# Check linters on latest-ubuntu with default version of Go.
on: [push, pull_request]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
- uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version: "1.26"
- name: Checkout code
uses: actions/checkout@v2
- name: Install golangci-lint
run: |
go version
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.6
- name: Run required linters in .golangci.yml plus hard-coded ones here
run: $(go env GOPATH)/bin/golangci-lint run --timeout=3m
- name: Run optional linters (not required to pass)
run: $(go env GOPATH)/bin/golangci-lint run --timeout=3m --issues-exit-code=0 -E dupl -E gocritic -E lll -E prealloc
run: make -w lint
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@

# Dependency directories (remove the comment below to include it)
# vendor/

# tools-bin directory is created automatically when running make. It contains
# pre-built tools needs for the make process (such as golanci-lint)
tools-bin/*
23 changes: 18 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,24 @@ GO111MODULE := on

GOPKG := github.com/veraison/eat

GOLINT ?= golangci-lint
TOPDIR := $(realpath $(dir $(lastword $(MAKEFILE_LIST))))

GOLINT_ARGS ?= run --timeout=3m -E dupl -E gocritic -E prealloc

GOLINT_VERSION = v2.13.2
GOLINT = $(TOPDIR)/tools-bin/golangci-lint
GOLINT_STAMP = $(TOPDIR)/tools-bin/golangci-lint-$(GOLINT_VERSION).stamp

$(GOLINT): $(GOLINT_STAMP)

$(GOLINT_STAMP):
mkdir -p $(dir $(GOLINT))
touch $(GOLINT_STAMP)
GOBIN=$(dir $(GOLINT)) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLINT_VERSION)

.PHONY: lint
lint: $(GOLINT)
$(GOLINT) $(GOLINT_ARGS)

ifeq ($(MAKECMDGOALS),lint)
GOLINT_ARGS ?= run --timeout=3m
Expand All @@ -16,9 +33,6 @@ else
endif
endif

.PHONY: lint lint-extra
lint lint-extra: ; $(GOLINT) $(GOLINT_ARGS)

ifeq ($(MAKECMDGOALS),test)
GOTEST_ARGS ?= -v -race $(GOPKG)
else
Expand Down Expand Up @@ -51,7 +65,6 @@ help:
@echo " * test: run unit tests for $(GOPKG)"
@echo " * test-cover: run unit tests and measure coverage for $(GOPKG)"
@echo " * lint: lint sources using default configuration"
@echo " * lint-extra: lint sources using default configuration and some extra checkers"
@echo " * presubmit: check you are ready to push your local branch to remote"
@echo " * help: print this menu"
@echo " * licenses: check licenses of dependent packages"
58 changes: 57 additions & 1 deletion int_or_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,49 @@ import (
"errors"
"fmt"
"math"
"reflect"
"strconv"
)

// IntOrStringMapFromAny creates a map[IntOrString]any from the provided value
// if possible, returning an error otherwose.
func IntOrStringMapFromAny(value any) (map[IntOrString]any, error) {
valueInfo := reflect.ValueOf(value)
if valueInfo.Kind() == reflect.Pointer {
if valueInfo.IsNil() {
return nil, errors.New("nil pointer")
}

value = valueInfo.Elem().Interface()
}
Comment thread
setrofim marked this conversation as resolved.

var ret map[IntOrString]any

switch t := value.(type) {
case map[IntOrString]any:
ret = t
case map[string]any:
ret = make(map[IntOrString]any)
for k, v := range t {
ret[IntOrStringFromString(k)] = v
}
case map[any]any:
ret = make(map[IntOrString]any)
for k, v := range t {
convertedK, err := IntOrStringFromAny(k)
if err != nil {
return nil, err
}

ret[convertedK] = v
}
default:
return nil, fmt.Errorf("cannot convert %T to map[eat.IntOrString]any", value)
}

return ret, nil
}

// IntOrString has an underlying value that is either an int or string.
type IntOrString struct {
val any
Expand Down Expand Up @@ -62,6 +102,13 @@ func IntOrStringFromAny(val any) (IntOrString, error) {
return IntOrString{}, fmt.Errorf("out of range: %d", t)
}
ret.val = int(t)
case int:
ret.val = t
case uint:
if uint64(t) > math.MaxInt64 {
return IntOrString{}, fmt.Errorf("out of range: %d", t)
}
ret.val = int(t)
case float64:
ret.val = int(t)
case string:
Expand All @@ -72,7 +119,7 @@ func IntOrStringFromAny(val any) (IntOrString, error) {
ret.val = t
}
default:
return IntOrString{}, fmt.Errorf("unexpected algorithm value: %v(%T)", t, t)
return IntOrString{}, fmt.Errorf("cannot convert %v (%T) to IntOrString", t, t)
}

return ret, nil
Expand Down Expand Up @@ -171,3 +218,12 @@ func (o *IntOrString) UnmarshalJSON(data []byte) error {

return nil
}

func (o IntOrString) MarshalText() ([]byte, error) {
return []byte(o.String()), nil
}

func (o *IntOrString) UnmarshalText(data []byte) error {
*o = IntOrStringFromString(string(data))
return nil
}
Comment on lines +222 to +229

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these two do not roundrtip: string("1") becomes int(1). Worth documenting it (also, these are public methods and should have their doc strings).

@setrofim setrofim Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do. string("1") and int(1) both convert to IntOrString{1} (When given a string, IntOrString attempts strconv.Atoi() for precisely this reason: https://github.com/veraison/eat/blob/setrofim/misc/int_or_string.go#L70)

Regarding doc comments -- I thought that for interface implementations, the comments are on the interface, not the implementation? (e.g. we don't usually put doc comments on MarshalCBOR either).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do. string("1") and int(1) both convert to IntOrString{1} (When given a string, IntOrString attempts strconv.Atoi() for precisely this reason: https://github.com/veraison/eat/blob/setrofim/misc/int_or_string.go#L70)

oh, right! Sorry for the noise.

Regarding doc comments -- I thought that for interface implementations, the comments are on the interface, not the implementation? (e.g. we don't usually put doc comments on MarshalCBOR either).

It's a method exported by the package, so strictly speaking we should document it. Why not making it private then?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to be public for the same reason as MarshalCBOR(), etc.

134 changes: 131 additions & 3 deletions int_or_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package eat

import (
"encoding/json"
"math"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -31,6 +33,10 @@ func Test_IntOrStringFromAny(t *testing.T) {
title: "ok int64",
input: int64(1),
},
{
title: "ok int",
input: int(1),
},
{
title: "ok uint8",
input: uint8(1),
Expand All @@ -47,6 +53,10 @@ func Test_IntOrStringFromAny(t *testing.T) {
title: "ok uint64",
input: uint64(1),
},
{
title: "ok uint",
input: uint(1),
},
{
title: "ok float64",
input: 1.0,
Expand All @@ -58,12 +68,22 @@ func Test_IntOrStringFromAny(t *testing.T) {
{
title: "err bool",
input: true,
err: "unexpected algorithm value: true(bool)",
err: "cannot convert true (bool) to IntOrString",
},
{
title: "err struct",
input: struct{}{},
err: "unexpected algorithm value: {}(struct {})",
err: "cannot convert {} (struct {}) to IntOrString",
},
{
title: "err uint64 too big",
input: uint64(math.MaxInt64) + 1,
err: "out of range: 9223372036854775808",
},
{
title: "err uint too big",
input: uint(math.MaxInt64) + 1,
err: "out of range: 9223372036854775808",
},
}

Expand Down Expand Up @@ -152,7 +172,7 @@ func TestIntOrString_UnmarshalJSON_negative(t *testing.T) {
assert.ErrorContains(t, err, "unexpected end of JSON input")

err = ios.UnmarshalJSON([]byte(`true`))
assert.ErrorContains(t, err, "unexpected algorithm value: true(bool)")
assert.ErrorContains(t, err, "cannot convert true (bool) to IntOrString")
}

func TestIntOrString_misc(t *testing.T) {
Expand All @@ -171,3 +191,111 @@ func TestIntOrString_misc(t *testing.T) {
ios = IntOrStringFromString("1")
assert.Equal(t, 1, ios.Int())
}

func TestIntOrString_JSON_key(t *testing.T) {
toEncode := map[IntOrString]any{
IntOrStringFromString("foo"): 1,
}

expectedJSON := `{"foo": 1}`
encoded, err := json.Marshal(toEncode)
assert.NoError(t, err)
assert.JSONEq(t, expectedJSON, string(encoded))

var decoded map[IntOrString]any
err = json.Unmarshal(encoded, &decoded)
assert.NoError(t, err)
assert.EqualValues(t, 1, int(decoded[IntOrStringFromString("foo")].(float64)))
}

func TestIntOrString_text_marshaling(t *testing.T) {
ios := IntOrStringFromInt(1)
text, err := ios.MarshalText()
assert.NoError(t, err)
assert.Equal(t, []byte("1"), text)

var other IntOrString
err = other.UnmarshalText(text)
assert.NoError(t, err)
assert.Equal(t, ios, other)
}

func TestIntOrStringMapFromAny(t *testing.T) {
testCases := []struct {
title string
input any
expected map[IntOrString]any
err string
}{
{
title: "ok map[IntOrString]any",
input: map[IntOrString]any{
IntOrStringFromString("foo"): true,
IntOrStringFromInt(1): false,
},
expected: map[IntOrString]any{
IntOrStringFromString("foo"): true,
IntOrStringFromInt(1): false,
},
},
{
title: "ok map[any]any",
input: map[any]any{
"foo": true,
1: false,
},
expected: map[IntOrString]any{
IntOrStringFromString("foo"): true,
IntOrStringFromInt(1): false,
},
},
{
title: "ok map[string]any",
input: map[string]any{
"foo": true,
},
expected: map[IntOrString]any{
IntOrStringFromString("foo"): true,
},
},
{
title: "ok pointer",
input: &map[string]any{
"foo": true,
},
expected: map[IntOrString]any{
IntOrStringFromString("foo"): true,
},
},
{
title: "err bad type",
input: 1,
err: "cannot convert int to map[eat.IntOrString]any",
},
{
title: "err bad map",
input: &map[any]any{
true: "foo",
},
err: "cannot convert true (bool) to IntOrString",
},
{
title: "err nil",
input: (*IntOrString)(nil),
err: "nil pointer",
},
}

for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
iosMap, err := IntOrStringMapFromAny(tc.input)

if tc.err == "" {
assert.NoError(t, err)
assert.EqualValues(t, tc.expected, iosMap)
} else {
assert.ErrorContains(t, err, tc.err)
}
})
}
}
14 changes: 5 additions & 9 deletions nested_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,9 @@ type NestedToken struct {
// NewNestedToken returns a new NestedToken from the provided type and data. An
// error is returned if either the type or the data are invalid, or if they
// don't match.
func NewNestedToken(typ string, data []byte) (*NestedToken, error) {
providedType, err := NestedTokenTypeFromString(typ)
if err != nil {
return nil, err
}
func NewNestedToken(typ NestedTokenType, data []byte) (*NestedToken, error) {

if providedType == NestedTokenCBOR {
if typ == NestedTokenCBOR {
if err := validateInnerCBORTags(data); err != nil {
return nil, fmt.Errorf("cbor: %w", err)
}
Expand All @@ -94,14 +90,14 @@ func NewNestedToken(typ string, data []byte) (*NestedToken, error) {
return nil, err
}

if providedType != identifiedType {
if typ != identifiedType {
return nil, fmt.Errorf("provided type is %q, but data appears to contain %q",
providedType.String(), identifiedType.String())
typ.String(), identifiedType.String())
}
}

return &NestedToken{
Type: providedType,
Type: typ,
Data: data,
}, nil
}
Expand Down
Loading
Loading