Merge pull request #3292 from crazy-max/compose-filtered-spec

bake: ignore unrelated fields when parsing compose files
This commit is contained in:
Tõnis Tiigi
2025-07-13 23:36:05 -07:00
committed by GitHub
15 changed files with 694 additions and 488 deletions
+64 -25
View File
@@ -11,6 +11,7 @@ import (
"github.com/compose-spec/compose-go/v2/consts" "github.com/compose-spec/compose-go/v2/consts"
"github.com/compose-spec/compose-go/v2/dotenv" "github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/loader"
composeschema "github.com/compose-spec/compose-go/v2/schema"
composetypes "github.com/compose-spec/compose-go/v2/types" composetypes "github.com/compose-spec/compose-go/v2/types"
"github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/buildflags"
dockeropts "github.com/docker/cli/opts" dockeropts "github.com/docker/cli/opts"
@@ -35,21 +36,7 @@ func ParseComposeFiles(fs []File) (*Config, error) {
} }
func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Config, error) { func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Config, error) {
if envs == nil { cfg, err := loadComposeFiles(cfgs, envs)
envs = make(map[string]string)
}
cfg, err := loader.LoadWithContext(context.Background(), composetypes.ConfigDetails{
ConfigFiles: cfgs,
Environment: envs,
}, func(options *loader.Options) {
projectName := "bake"
if v, ok := envs[consts.ComposeProjectName]; ok && v != "" {
projectName = v
}
options.SetProjectName(projectName, false)
options.SkipNormalization = true
options.Profiles = []string{"*"}
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -208,6 +195,67 @@ func ParseCompose(cfgs []composetypes.ConfigFile, envs map[string]string) (*Conf
return &c, nil return &c, nil
} }
func loadComposeFiles(cfgs []composetypes.ConfigFile, envs map[string]string, options ...func(*loader.Options)) (*composetypes.Project, error) {
if envs == nil {
envs = make(map[string]string)
}
cfgDetails := composetypes.ConfigDetails{
ConfigFiles: cfgs,
Environment: envs,
}
raw, err := loader.LoadModelWithContext(context.Background(), cfgDetails, append([]func(*loader.Options){func(opts *loader.Options) {
projectName := "bake"
if v, ok := envs[consts.ComposeProjectName]; ok && v != "" {
projectName = v
}
opts.SetProjectName(projectName, false)
opts.SkipNormalization = true
opts.SkipValidation = true
}}, options...)...)
if err != nil {
return nil, err
}
filtered := make(map[string]any)
for _, key := range []string{"services", "secrets"} {
if key == "services" {
if services, ok := raw["services"].(map[string]any); ok {
filteredServices := make(map[string]any)
for svcName, svc := range services {
if svc == nil {
filteredServices[svcName] = map[string]any{}
} else if svcMap, ok := svc.(map[string]any); ok {
filteredService := make(map[string]any)
for _, svcField := range []string{"image", "build", "environment", "env_file"} {
if val, ok := svcMap[svcField]; ok {
filteredService[svcField] = val
}
}
filteredServices[svcName] = filteredService
}
}
filtered["services"] = filteredServices
}
} else if v, ok := raw[key]; ok {
filtered[key] = v
}
}
if err := composeschema.Validate(filtered); err != nil {
return nil, err
}
return loader.ModelToProject(filtered, loader.ToOptions(&cfgDetails, append([]func(*loader.Options){func(options *loader.Options) {
options.SkipNormalization = true
options.Profiles = []string{"*"}
}}, options...)), composetypes.ConfigDetails{
ConfigFiles: cfgs,
Environment: envs,
})
}
func validateComposeFile(dt []byte, fn string) (bool, error) { func validateComposeFile(dt []byte, fn string) (bool, error) {
envs, err := composeEnv() envs, err := composeEnv()
if err != nil { if err != nil {
@@ -225,16 +273,7 @@ func validateComposeFile(dt []byte, fn string) (bool, error) {
} }
func validateCompose(dt []byte, envs map[string]string) error { func validateCompose(dt []byte, envs map[string]string) error {
_, err := loader.LoadWithContext(context.Background(), composetypes.ConfigDetails{ _, err := loadComposeFiles([]composetypes.ConfigFile{{Content: dt}}, envs, func(options *loader.Options) {
ConfigFiles: []composetypes.ConfigFile{
{
Content: dt,
},
},
Environment: envs,
}, func(options *loader.Options) {
options.SetProjectName("bake", false)
options.SkipNormalization = true
// consistency is checked later in ParseCompose to ensure multiple // consistency is checked later in ParseCompose to ensure multiple
// compose files can be merged together // compose files can be merged together
options.SkipConsistencyCheck = true options.SkipConsistencyCheck = true
+36 -11
View File
@@ -192,6 +192,7 @@ services:
_, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil) _, err := ParseCompose([]composetypes.ConfigFile{{Content: dt}}, nil)
require.Error(t, err) require.Error(t, err)
require.ErrorContains(t, err, `has neither an image nor a build context specified`)
} }
func TestAdvancedNetwork(t *testing.T) { func TestAdvancedNetwork(t *testing.T) {
@@ -610,7 +611,6 @@ func TestValidateComposeFile(t *testing.T) {
fn string fn string
dt []byte dt []byte
isCompose bool isCompose bool
wantErr bool
}{ }{
{ {
name: "empty service", name: "empty service",
@@ -620,7 +620,6 @@ services:
foo: foo:
`), `),
isCompose: true, isCompose: true,
wantErr: true,
}, },
{ {
name: "build", name: "build",
@@ -631,7 +630,6 @@ services:
build: . build: .
`), `),
isCompose: true, isCompose: true,
wantErr: false,
}, },
{ {
name: "image", name: "image",
@@ -642,7 +640,6 @@ services:
image: nginx image: nginx
`), `),
isCompose: true, isCompose: true,
wantErr: false,
}, },
{ {
name: "unknown ext", name: "unknown ext",
@@ -653,7 +650,6 @@ services:
image: nginx image: nginx
`), `),
isCompose: true, isCompose: true,
wantErr: false,
}, },
{ {
name: "hcl", name: "hcl",
@@ -664,18 +660,13 @@ target "default" {
} }
`), `),
isCompose: false, isCompose: false,
wantErr: false,
}, },
} }
for _, tt := range cases { for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
isCompose, err := validateComposeFile(tt.dt, tt.fn) isCompose, err := validateComposeFile(tt.dt, tt.fn)
assert.Equal(t, tt.isCompose, isCompose) assert.Equal(t, tt.isCompose, isCompose)
if tt.wantErr { require.NoError(t, err)
require.Error(t, err)
} else {
require.NoError(t, err)
}
}) })
} }
} }
@@ -888,6 +879,40 @@ services:
require.Equal(t, map[string]*string{"TEST_VALUE": ptrstr("abc"), "FOO_VALUE": ptrstr("abc")}, c.Targets[0].Args) require.Equal(t, map[string]*string{"TEST_VALUE": ptrstr("abc"), "FOO_VALUE": ptrstr("abc")}, c.Targets[0].Args)
} }
func TestUnknownField(t *testing.T) {
tmpdir := t.TempDir()
dt := []byte(`
services:
webapp:
bar: baz
build:
context: .
foo:
- bar.baz
`)
chdir(t, tmpdir)
_, err := ParseComposeFiles([]File{{Name: "compose.yml", Data: dt}})
require.NoError(t, err)
}
func TestUnknownBuildField(t *testing.T) {
tmpdir := t.TempDir()
dt := []byte(`
services:
webapp:
build:
context: .
foo: bar
`)
chdir(t, tmpdir)
_, err := ParseComposeFiles([]File{{Name: "compose.yml", Data: dt}})
require.Error(t, err)
require.ErrorContains(t, err, `additional properties 'foo' not allowed`)
}
// chdir changes the current working directory to the named directory, // chdir changes the current working directory to the named directory,
// and then restore the original working directory at the end of the test. // and then restore the original working directory at the end of the test.
func chdir(t *testing.T, dir string) { func chdir(t *testing.T, dir string) {
+2 -2
View File
@@ -6,7 +6,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/semver/v3 v3.2.1
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.2
github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/config v1.27.27
github.com/compose-spec/compose-go/v2 v2.7.1 github.com/compose-spec/compose-go/v2 v2.7.2-0.20250703132301-891fce532a51 // main
github.com/containerd/console v1.0.5 github.com/containerd/console v1.0.5
github.com/containerd/containerd/v2 v2.1.1 github.com/containerd/containerd/v2 v2.1.1
github.com/containerd/continuity v0.4.5 github.com/containerd/continuity v0.4.5
@@ -105,7 +105,7 @@ require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gnostic-models v0.6.8 // indirect
+4 -4
View File
@@ -62,8 +62,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.7.1 h1:EUIbuaD0R/J1KA+FbJMNbcS9+jt/CVudbp5iHqUllSs= github.com/compose-spec/compose-go/v2 v2.7.2-0.20250703132301-891fce532a51 h1:AjI75N9METifYMZK7eNt8XIgY9Sryv+1w3XDA7X2vZQ=
github.com/compose-spec/compose-go/v2 v2.7.1/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU= github.com/compose-spec/compose-go/v2 v2.7.2-0.20250703132301-891fce532a51/go.mod h1:Zow/3eYNOnl2T4qLGZEizf8d/ht1qfy09G7WGOSzGOY=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
@@ -159,8 +159,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+6 -6
View File
@@ -320,17 +320,17 @@ func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir strin
// LoadWithContext reads a ConfigDetails and returns a fully loaded configuration as a compose-go Project // LoadWithContext reads a ConfigDetails and returns a fully loaded configuration as a compose-go Project
func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) { func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
opts := toOptions(&configDetails, options) opts := ToOptions(&configDetails, options)
dict, err := loadModelWithContext(ctx, &configDetails, opts) dict, err := loadModelWithContext(ctx, &configDetails, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return modelToProject(dict, opts, configDetails) return ModelToProject(dict, opts, configDetails)
} }
// LoadModelWithContext reads a ConfigDetails and returns a fully loaded configuration as a yaml dictionary // LoadModelWithContext reads a ConfigDetails and returns a fully loaded configuration as a yaml dictionary
func LoadModelWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (map[string]any, error) { func LoadModelWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (map[string]any, error) {
opts := toOptions(&configDetails, options) opts := ToOptions(&configDetails, options)
return loadModelWithContext(ctx, &configDetails, opts) return loadModelWithContext(ctx, &configDetails, opts)
} }
@@ -348,7 +348,7 @@ func loadModelWithContext(ctx context.Context, configDetails *types.ConfigDetail
return load(ctx, *configDetails, opts, nil) return load(ctx, *configDetails, opts, nil)
} }
func toOptions(configDetails *types.ConfigDetails, options []func(*Options)) *Options { func ToOptions(configDetails *types.ConfigDetails, options []func(*Options)) *Options {
opts := &Options{ opts := &Options{
Interpolate: &interp.Options{ Interpolate: &interp.Options{
Substitute: template.Substitute, Substitute: template.Substitute,
@@ -557,8 +557,8 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,
return dict, nil return dict, nil
} }
// modelToProject binds a canonical yaml dict into compose-go structs // ModelToProject binds a canonical yaml dict into compose-go structs
func modelToProject(dict map[string]interface{}, opts *Options, configDetails types.ConfigDetails) (*types.Project, error) { func ModelToProject(dict map[string]interface{}, opts *Options, configDetails types.ConfigDetails) (*types.Project, error) {
project := &types.Project{ project := &types.Project{
Name: opts.projectName, Name: opts.projectName,
WorkingDir: configDetails.WorkingDir, WorkingDir: configDetails.WorkingDir,
+6
View File
@@ -117,6 +117,12 @@ func checkConsistency(project *types.Project) error { //nolint:gocyclo
} }
} }
for model := range s.Models {
if _, ok := project.Models[model]; !ok {
return fmt.Errorf("service %q refers to undefined model %s: %w", s.Name, model, errdefs.ErrInvalid)
}
}
for _, secret := range s.Secrets { for _, secret := range s.Secrets {
if _, ok := project.Secrets[secret.Source]; !ok { if _, ok := project.Secrets[secret.Source]; !ok {
return fmt.Errorf("service %q refers to undefined secret %s: %w", s.Name, secret.Source, errdefs.ErrInvalid) return fmt.Errorf("service %q refers to undefined secret %s: %w", s.Name, secret.Source, errdefs.ErrInvalid)
+3
View File
@@ -16,3 +16,6 @@ indent_style = tab
[*.nix] [*.nix]
indent_size = 2 indent_size = 2
[.golangci.yaml]
indent_size = 2
+31 -15
View File
@@ -1,23 +1,39 @@
run: version: "2"
timeout: 5m
linters-settings: run:
gci: timeout: 10m
sections:
- standard
- default
- prefix(github.com/go-viper/mapstructure)
golint:
min-confidence: 0
goimports:
local-prefixes: github.com/go-viper/maptstructure
linters: linters:
disable-all: true enable:
- govet
- ineffassign
# - misspell
- nolintlint
# - revive
disable:
- errcheck
- staticcheck
- unused
settings:
misspell:
locale: US
nolintlint:
allow-unused: false # report any unused nolint directives
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
formatters:
enable: enable:
- gci - gci
- gofmt - gofmt
- gofumpt - gofumpt
- goimports - goimports
- staticcheck # - golines
# - stylecheck
settings:
gci:
sections:
- standard
- default
- localmodule
+5 -4
View File
@@ -1,8 +1,9 @@
# mapstructure # mapstructure
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/go-viper/mapstructure/ci.yaml?branch=main&style=flat-square)](https://github.com/go-viper/mapstructure/actions?query=workflow%3ACI) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/go-viper/mapstructure/ci.yaml?style=flat-square)](https://github.com/go-viper/mapstructure/actions/workflows/ci.yaml)
[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/go-viper/mapstructure/v2) [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/go-viper/mapstructure)
![Go Version](https://img.shields.io/badge/go%20version-%3E=1.18-61CFDD.svg?style=flat-square) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/go-viper/mapstructure?style=flat-square&color=61CFDD)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/go-viper/mapstructure/badge?style=flat-square)](https://deps.dev/go/github.com%252Fgo-viper%252Fmapstructure%252Fv2)
mapstructure is a Go library for decoding generic map values to structures mapstructure is a Go library for decoding generic map values to structures
and vice versa, while providing helpful error handling. and vice versa, while providing helpful error handling.
@@ -29,7 +30,7 @@ The API is the same, so you don't need to change anything else.
Here is a script that can help you with the migration: Here is a script that can help you with the migration:
```shell ```shell
sed -i 's/github.com\/mitchellh\/mapstructure/github.com\/go-viper\/mapstructure\/v2/g' $(find . -type f -name '*.go') sed -i 's|github.com/mitchellh/mapstructure|github.com/go-viper/mapstructure/v2|g' $(find . -type f -name '*.go')
``` ```
If you need more time to migrate your code, that is absolutely fine. If you need more time to migrate your code, that is absolutely fine.
+82 -5
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"net/url"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@@ -36,6 +37,30 @@ func typedDecodeHook(h DecodeHookFunc) DecodeHookFunc {
return nil return nil
} }
// cachedDecodeHook takes a raw DecodeHookFunc (an interface{}) and turns
// it into a closure to be used directly
// if the type fails to convert we return a closure always erroring to keep the previous behaviour
func cachedDecodeHook(raw DecodeHookFunc) func(from reflect.Value, to reflect.Value) (interface{}, error) {
switch f := typedDecodeHook(raw).(type) {
case DecodeHookFuncType:
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
return f(from.Type(), to.Type(), from.Interface())
}
case DecodeHookFuncKind:
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
return f(from.Kind(), to.Kind(), from.Interface())
}
case DecodeHookFuncValue:
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
return f(from, to)
}
default:
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
return nil, errors.New("invalid decode hook signature")
}
}
}
// DecodeHookExec executes the given decode hook. This should be used // DecodeHookExec executes the given decode hook. This should be used
// since it'll naturally degrade to the older backwards compatible DecodeHookFunc // since it'll naturally degrade to the older backwards compatible DecodeHookFunc
// that took reflect.Kind instead of reflect.Type. // that took reflect.Kind instead of reflect.Type.
@@ -61,17 +86,25 @@ func DecodeHookExec(
// The composed funcs are called in order, with the result of the // The composed funcs are called in order, with the result of the
// previous transformation. // previous transformation.
func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc { func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
cached := make([]func(from reflect.Value, to reflect.Value) (interface{}, error), 0, len(fs))
for _, f := range fs {
cached = append(cached, cachedDecodeHook(f))
}
return func(f reflect.Value, t reflect.Value) (interface{}, error) { return func(f reflect.Value, t reflect.Value) (interface{}, error) {
var err error var err error
data := f.Interface() data := f.Interface()
newFrom := f newFrom := f
for _, f1 := range fs { for _, c := range cached {
data, err = DecodeHookExec(f1, newFrom, t) data, err = c(newFrom, t)
if err != nil { if err != nil {
return nil, err return nil, err
} }
newFrom = reflect.ValueOf(data) if v, ok := data.(reflect.Value); ok {
newFrom = v
} else {
newFrom = reflect.ValueOf(data)
}
} }
return data, nil return data, nil
@@ -81,13 +114,17 @@ func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc {
// OrComposeDecodeHookFunc executes all input hook functions until one of them returns no error. In that case its value is returned. // OrComposeDecodeHookFunc executes all input hook functions until one of them returns no error. In that case its value is returned.
// If all hooks return an error, OrComposeDecodeHookFunc returns an error concatenating all error messages. // If all hooks return an error, OrComposeDecodeHookFunc returns an error concatenating all error messages.
func OrComposeDecodeHookFunc(ff ...DecodeHookFunc) DecodeHookFunc { func OrComposeDecodeHookFunc(ff ...DecodeHookFunc) DecodeHookFunc {
cached := make([]func(from reflect.Value, to reflect.Value) (interface{}, error), 0, len(ff))
for _, f := range ff {
cached = append(cached, cachedDecodeHook(f))
}
return func(a, b reflect.Value) (interface{}, error) { return func(a, b reflect.Value) (interface{}, error) {
var allErrs string var allErrs string
var out interface{} var out interface{}
var err error var err error
for _, f := range ff { for _, c := range cached {
out, err = DecodeHookExec(f, a, b) out, err = c(a, b)
if err != nil { if err != nil {
allErrs += err.Error() + "\n" allErrs += err.Error() + "\n"
continue continue
@@ -144,6 +181,26 @@ func StringToTimeDurationHookFunc() DecodeHookFunc {
} }
} }
// StringToURLHookFunc returns a DecodeHookFunc that converts
// strings to *url.URL.
func StringToURLHookFunc() DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{},
) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(&url.URL{}) {
return data, nil
}
// Convert it by parsing
return url.Parse(data.(string))
}
}
// StringToIPHookFunc returns a DecodeHookFunc that converts // StringToIPHookFunc returns a DecodeHookFunc that converts
// strings to net.IP // strings to net.IP
func StringToIPHookFunc() DecodeHookFunc { func StringToIPHookFunc() DecodeHookFunc {
@@ -333,6 +390,26 @@ func StringToNetIPAddrPortHookFunc() DecodeHookFunc {
} }
} }
// StringToNetIPPrefixHookFunc returns a DecodeHookFunc that converts
// strings to netip.Prefix.
func StringToNetIPPrefixHookFunc() DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{},
) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(netip.Prefix{}) {
return data, nil
}
// Convert it by parsing
return netip.ParsePrefix(data.(string))
}
}
// StringToBasicTypeHookFunc returns a DecodeHookFunc that converts // StringToBasicTypeHookFunc returns a DecodeHookFunc that converts
// strings to basic types. // strings to basic types.
// int8, uint8, int16, uint16, int32, uint32, int64, uint64, int, uint, float32, float64, bool, byte, rune, complex64, complex128 // int8, uint8, int16, uint16, int32, uint32, int64, uint64, int, uint, float32, float64, bool, byte, rune, complex64, complex128
+74
View File
@@ -0,0 +1,74 @@
package mapstructure
import (
"fmt"
"reflect"
)
// Error interface is implemented by all errors emitted by mapstructure.
//
// Use [errors.As] to check if an error implements this interface.
type Error interface {
error
mapstructure()
}
// DecodeError is a generic error type that holds information about
// a decoding error together with the name of the field that caused the error.
type DecodeError struct {
name string
err error
}
func newDecodeError(name string, err error) *DecodeError {
return &DecodeError{
name: name,
err: err,
}
}
func (e *DecodeError) Name() string {
return e.name
}
func (e *DecodeError) Unwrap() error {
return e.err
}
func (e *DecodeError) Error() string {
return fmt.Sprintf("'%s' %s", e.name, e.err)
}
func (*DecodeError) mapstructure() {}
// ParseError is an error type that indicates a value could not be parsed
// into the expected type.
type ParseError struct {
Expected reflect.Value
Value any
Err error
}
func (e *ParseError) Error() string {
return fmt.Sprintf("cannot parse value as '%s': %s", e.Expected.Type(), e.Err)
}
func (*ParseError) mapstructure() {}
// UnconvertibleTypeError is an error type that indicates a value could not be
// converted to the expected type.
type UnconvertibleTypeError struct {
Expected reflect.Value
Value any
}
func (e *UnconvertibleTypeError) Error() string {
return fmt.Sprintf(
"expected type '%s', got unconvertible type '%s'",
e.Expected.Type(),
reflect.TypeOf(e.Value),
)
}
func (*UnconvertibleTypeError) mapstructure() {}
+126 -304
View File
@@ -2,30 +2,28 @@
"nodes": { "nodes": {
"cachix": { "cachix": {
"inputs": { "inputs": {
"devenv": "devenv_2", "devenv": [
"devenv"
],
"flake-compat": [ "flake-compat": [
"devenv", "devenv"
"flake-compat"
], ],
"nixpkgs": [ "git-hooks": [
"devenv", "devenv"
"nixpkgs"
], ],
"pre-commit-hooks": [ "nixpkgs": "nixpkgs"
"devenv",
"pre-commit-hooks"
]
}, },
"locked": { "locked": {
"lastModified": 1712055811, "lastModified": 1742042642,
"narHash": "sha256-7FcfMm5A/f02yyzuavJe06zLa9hcMHsagE28ADcmQvk=", "narHash": "sha256-D0gP8srrX0qj+wNYNPdtVJsQuFzIng3q43thnHXQ/es=",
"owner": "cachix", "owner": "cachix",
"repo": "cachix", "repo": "cachix",
"rev": "02e38da89851ec7fec3356a5c04bc8349cae0e30", "rev": "a624d3eaf4b1d225f918de8543ed739f2f574203",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "cachix", "owner": "cachix",
"ref": "latest",
"repo": "cachix", "repo": "cachix",
"type": "github" "type": "github"
} }
@@ -33,52 +31,21 @@
"devenv": { "devenv": {
"inputs": { "inputs": {
"cachix": "cachix", "cachix": "cachix",
"flake-compat": "flake-compat_2", "flake-compat": "flake-compat",
"nix": "nix_2", "git-hooks": "git-hooks",
"nixpkgs": "nixpkgs_2",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1717245169,
"narHash": "sha256-+mW3rTBjGU8p1THJN0lX/Dd/8FbnF+3dB+mJuSaxewE=",
"owner": "cachix",
"repo": "devenv",
"rev": "c3f9f053c077c6f88a3de5276d9178c62baa3fc3",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"devenv_2": {
"inputs": {
"flake-compat": [
"devenv",
"cachix",
"flake-compat"
],
"nix": "nix", "nix": "nix",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs_3"
"poetry2nix": "poetry2nix",
"pre-commit-hooks": [
"devenv",
"cachix",
"pre-commit-hooks"
]
}, },
"locked": { "locked": {
"lastModified": 1708704632, "lastModified": 1744876578,
"narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=", "narHash": "sha256-8MTBj2REB8t29sIBLpxbR0+AEGJ7f+RkzZPAGsFd40c=",
"owner": "cachix", "owner": "cachix",
"repo": "devenv", "repo": "devenv",
"rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196", "rev": "7ff7c351bba20d0615be25ecdcbcf79b57b85fe1",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "cachix", "owner": "cachix",
"ref": "python-rewrite",
"repo": "devenv", "repo": "devenv",
"type": "github" "type": "github"
} }
@@ -86,27 +53,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1673956053, "lastModified": 1733328505,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -116,15 +67,37 @@
} }
}, },
"flake-parts": { "flake-parts": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1712014858,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": { "inputs": {
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1717285511, "lastModified": 1743550720,
"narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", "rev": "c621e8422220273271f52058f618c94e405bb0f5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -133,39 +106,28 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils": { "git-hooks": {
"inputs": { "inputs": {
"systems": "systems" "flake-compat": [
"devenv"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1689068808, "lastModified": 1742649964,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
"owner": "numtide", "owner": "cachix",
"repo": "flake-utils", "repo": "git-hooks.nix",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "cachix",
"repo": "flake-utils", "repo": "git-hooks.nix",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github" "type": "github"
} }
}, },
@@ -173,7 +135,7 @@
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"devenv", "devenv",
"pre-commit-hooks", "git-hooks",
"nixpkgs" "nixpkgs"
] ]
}, },
@@ -191,166 +153,109 @@
"type": "github" "type": "github"
} }
}, },
"nix": { "libgit2": {
"inputs": { "flake": false,
"flake-compat": "flake-compat",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": { "locked": {
"lastModified": 1712911606, "lastModified": 1697646580,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=", "narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
"owner": "domenkozar", "owner": "libgit2",
"repo": "nix", "repo": "libgit2",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12", "rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "domenkozar", "owner": "libgit2",
"ref": "devenv-2.21", "repo": "libgit2",
"repo": "nix",
"type": "github" "type": "github"
} }
}, },
"nix-github-actions": { "nix": {
"inputs": { "inputs": {
"nixpkgs": [ "flake-compat": [
"devenv", "devenv"
"cachix", ],
"devenv", "flake-parts": "flake-parts",
"poetry2nix", "libgit2": "libgit2",
"nixpkgs" "nixpkgs": "nixpkgs_2",
"nixpkgs-23-11": [
"devenv"
],
"nixpkgs-regression": [
"devenv"
],
"pre-commit-hooks": [
"devenv"
] ]
}, },
"locked": { "locked": {
"lastModified": 1688870561, "lastModified": 1741798497,
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=", "narHash": "sha256-E3j+3MoY8Y96mG1dUIiLFm2tZmNbRvSiyN7CrSKuAVg=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nix_2": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression_2"
},
"locked": {
"lastModified": 1712911606,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
"owner": "domenkozar", "owner": "domenkozar",
"repo": "nix", "repo": "nix",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12", "rev": "f3f44b2baaf6c4c6e179de8cbb1cc6db031083cd",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "domenkozar", "owner": "domenkozar",
"ref": "devenv-2.21", "ref": "devenv-2.24",
"repo": "nix", "repo": "nix",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1692808169, "lastModified": 1733212471,
"narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=", "narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9201b5ff357e781bf014d0330d18555695df7ba8", "rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixpkgs-unstable", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": { "nixpkgs-lib": {
"locked": { "locked": {
"lastModified": 1717284937, "lastModified": 1743296961,
"narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=", "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=",
"type": "tarball", "owner": "nix-community",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" "repo": "nixpkgs.lib",
}, "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa",
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "nix-community",
"repo": "nixpkgs", "repo": "nixpkgs.lib",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-regression_2": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1710695816,
"narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "614b4613980a522ba49f0d194531beddbb7220d3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1713361204, "lastModified": 1717432640,
"narHash": "sha256-TA6EDunWTkc5FvDCqU3W2T3SFn0gRZqh6D/hJnM02MM=", "narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1733477122,
"narHash": "sha256-qamMCz5mNpQmgBwc8SB5tVMlD5sbwVIToVZtSxMph9s=",
"owner": "cachix", "owner": "cachix",
"repo": "devenv-nixpkgs", "repo": "devenv-nixpkgs",
"rev": "285676e87ad9f0ca23d8714a6ab61e7e027020c6", "rev": "7bd9e84d0452f6d2e63b6e6da29fe73fac951857",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -360,13 +265,13 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_3": { "nixpkgs_4": {
"locked": { "locked": {
"lastModified": 1717112898, "lastModified": 1744536153,
"narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=", "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -376,94 +281,11 @@
"type": "github" "type": "github"
} }
}, },
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1692876271,
"narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-utils": "flake-utils_2",
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1713775815,
"narHash": "sha256-Wu9cdYTnGQQwtT20QQMg7jzkANKQjwBD9iccfGKkfls=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "2ac4dcbf55ed43f3be0bae15e181f08a57af24a4",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"devenv": "devenv", "devenv": "devenv",
"flake-parts": "flake-parts", "flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_3" "nixpkgs": "nixpkgs_4"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
} }
} }
}, },
+27 -20
View File
@@ -5,35 +5,42 @@
devenv.url = "github:cachix/devenv"; devenv.url = "github:cachix/devenv";
}; };
outputs = inputs@{ flake-parts, ... }: outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } { flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ imports = [
inputs.devenv.flakeModule inputs.devenv.flakeModule
]; ];
systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
perSystem = { config, self', inputs', pkgs, system, ... }: rec { perSystem =
devenv.shells = { { pkgs, ... }:
default = { rec {
languages = { devenv.shells = {
go.enable = true; default = {
languages = {
go.enable = true;
};
pre-commit.hooks = {
nixpkgs-fmt.enable = true;
};
packages = with pkgs; [
golangci-lint
];
# https://github.com/cachix/devenv/issues/528#issuecomment-1556108767
containers = pkgs.lib.mkForce { };
}; };
pre-commit.hooks = { ci = devenv.shells.default;
nixpkgs-fmt.enable = true;
};
packages = with pkgs; [
golangci-lint
];
# https://github.com/cachix/devenv/issues/528#issuecomment-1556108767
containers = pkgs.lib.mkForce { };
}; };
ci = devenv.shells.default;
}; };
};
}; };
} }
+226 -90
View File
@@ -115,15 +115,36 @@
// //
// When decoding from a struct to any other value, you may use the // When decoding from a struct to any other value, you may use the
// ",omitempty" suffix on your tag to omit that value if it equates to // ",omitempty" suffix on your tag to omit that value if it equates to
// the zero value. The zero value of all types is specified in the Go // the zero value, or a zero-length element. The zero value of all types is
// specification. // specified in the Go specification.
// //
// For example, the zero type of a numeric type is zero ("0"). If the struct // For example, the zero type of a numeric type is zero ("0"). If the struct
// field value is zero and a numeric type, the field is empty, and it won't // field value is zero and a numeric type, the field is empty, and it won't
// be encoded into the destination type. // be encoded into the destination type. And likewise for the URLs field, if the
// slice is nil or empty, it won't be encoded into the destination type.
// //
// type Source struct { // type Source struct {
// Age int `mapstructure:",omitempty"` // Age int `mapstructure:",omitempty"`
// URLs []string `mapstructure:",omitempty"`
// }
//
// # Omit Zero Values
//
// When decoding from a struct to any other value, you may use the
// ",omitzero" suffix on your tag to omit that value if it equates to the zero
// value. The zero value of all types is specified in the Go specification.
//
// For example, the zero type of a numeric type is zero ("0"). If the struct
// field value is zero and a numeric type, the field is empty, and it won't
// be encoded into the destination type. And likewise for the URLs field, if the
// slice is nil, it won't be encoded into the destination type.
//
// Note that if the field is a slice, and it is empty but not nil, it will
// still be encoded into the destination type.
//
// type Source struct {
// Age int `mapstructure:",omitzero"`
// URLs []string `mapstructure:",omitzero"`
// } // }
// //
// # Unexported fields // # Unexported fields
@@ -222,6 +243,12 @@ type DecoderConfig struct {
// will affect all nested structs as well. // will affect all nested structs as well.
ErrorUnset bool ErrorUnset bool
// AllowUnsetPointer, if set to true, will prevent fields with pointer types
// from being reported as unset, even if ErrorUnset is true and the field was
// not present in the input data. This allows pointer fields to be optional
// without triggering an error when they are missing.
AllowUnsetPointer bool
// ZeroFields, if set to true, will zero fields before writing them. // ZeroFields, if set to true, will zero fields before writing them.
// For example, a map will be emptied before decoded values are put in // For example, a map will be emptied before decoded values are put in
// it. If this is false, a map will be merged. // it. If this is false, a map will be merged.
@@ -266,6 +293,10 @@ type DecoderConfig struct {
// defaults to "mapstructure" // defaults to "mapstructure"
TagName string TagName string
// The option of the value in the tag that indicates a field should
// be squashed. This defaults to "squash".
SquashTagOption string
// IgnoreUntaggedFields ignores all struct fields without explicit // IgnoreUntaggedFields ignores all struct fields without explicit
// TagName, comparable to `mapstructure:"-"` as default behaviour. // TagName, comparable to `mapstructure:"-"` as default behaviour.
IgnoreUntaggedFields bool IgnoreUntaggedFields bool
@@ -274,6 +305,10 @@ type DecoderConfig struct {
// field name or tag. Defaults to `strings.EqualFold`. This can be used // field name or tag. Defaults to `strings.EqualFold`. This can be used
// to implement case-sensitive tag values, support snake casing, etc. // to implement case-sensitive tag values, support snake casing, etc.
MatchName func(mapKey, fieldName string) bool MatchName func(mapKey, fieldName string) bool
// DecodeNil, if set to true, will cause the DecodeHook (if present) to run
// even if the input is nil. This can be used to provide default values.
DecodeNil bool
} }
// A Decoder takes a raw interface value and turns it into structured // A Decoder takes a raw interface value and turns it into structured
@@ -283,7 +318,8 @@ type DecoderConfig struct {
// structure. The top-level Decode method is just a convenience that sets // structure. The top-level Decode method is just a convenience that sets
// up the most basic Decoder. // up the most basic Decoder.
type Decoder struct { type Decoder struct {
config *DecoderConfig config *DecoderConfig
cachedDecodeHook func(from reflect.Value, to reflect.Value) (interface{}, error)
} }
// Metadata contains information about decoding a structure that // Metadata contains information about decoding a structure that
@@ -401,6 +437,10 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) {
config.TagName = "mapstructure" config.TagName = "mapstructure"
} }
if config.SquashTagOption == "" {
config.SquashTagOption = "squash"
}
if config.MatchName == nil { if config.MatchName == nil {
config.MatchName = strings.EqualFold config.MatchName = strings.EqualFold
} }
@@ -408,6 +448,9 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) {
result := &Decoder{ result := &Decoder{
config: config, config: config,
} }
if config.DecodeHook != nil {
result.cachedDecodeHook = cachedDecodeHook(config.DecodeHook)
}
return result, nil return result, nil
} }
@@ -426,19 +469,26 @@ func (d *Decoder) Decode(input interface{}) error {
return err return err
} }
// isNil returns true if the input is nil or a typed nil pointer.
func isNil(input interface{}) bool {
if input == nil {
return true
}
val := reflect.ValueOf(input)
return val.Kind() == reflect.Ptr && val.IsNil()
}
// Decodes an unknown data type into a specific reflection value. // Decodes an unknown data type into a specific reflection value.
func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error { func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error {
var inputVal reflect.Value var (
if input != nil { inputVal = reflect.ValueOf(input)
inputVal = reflect.ValueOf(input) outputKind = getKind(outVal)
decodeNil = d.config.DecodeNil && d.cachedDecodeHook != nil
// We need to check here if input is a typed nil. Typed nils won't )
// match the "input == nil" below so we check that here. if isNil(input) {
if inputVal.Kind() == reflect.Ptr && inputVal.IsNil() { // Typed nils won't match the "input == nil" below, so reset input.
input = nil input = nil
}
} }
if input == nil { if input == nil {
// If the data is nil, then we don't set anything, unless ZeroFields is set // If the data is nil, then we don't set anything, unless ZeroFields is set
// to true. // to true.
@@ -449,30 +499,46 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e
d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) d.config.Metadata.Keys = append(d.config.Metadata.Keys, name)
} }
} }
return nil if !decodeNil {
return nil
}
} }
if !inputVal.IsValid() { if !inputVal.IsValid() {
// If the input value is invalid, then we just set the value if !decodeNil {
// to be the zero value. // If the input value is invalid, then we just set the value
outVal.Set(reflect.Zero(outVal.Type())) // to be the zero value.
if d.config.Metadata != nil && name != "" { outVal.Set(reflect.Zero(outVal.Type()))
d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) if d.config.Metadata != nil && name != "" {
d.config.Metadata.Keys = append(d.config.Metadata.Keys, name)
}
return nil
}
// Hooks need a valid inputVal, so reset it to zero value of outVal type.
switch outputKind {
case reflect.Struct, reflect.Map:
var mapVal map[string]interface{}
inputVal = reflect.ValueOf(mapVal) // create nil map pointer
case reflect.Slice, reflect.Array:
var sliceVal []interface{}
inputVal = reflect.ValueOf(sliceVal) // create nil slice pointer
default:
inputVal = reflect.Zero(outVal.Type())
} }
return nil
} }
if d.config.DecodeHook != nil { if d.cachedDecodeHook != nil {
// We have a DecodeHook, so let's pre-process the input. // We have a DecodeHook, so let's pre-process the input.
var err error var err error
input, err = DecodeHookExec(d.config.DecodeHook, inputVal, outVal) input, err = d.cachedDecodeHook(inputVal, outVal)
if err != nil { if err != nil {
return fmt.Errorf("error decoding '%s': %w", name, err) return newDecodeError(name, err)
} }
} }
if isNil(input) {
return nil
}
var err error var err error
outputKind := getKind(outVal)
addMetaKey := true addMetaKey := true
switch outputKind { switch outputKind {
case reflect.Bool: case reflect.Bool:
@@ -503,7 +569,7 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e
err = d.decodeFunc(name, input, outVal) err = d.decodeFunc(name, input, outVal)
default: default:
// If we reached this point then we weren't able to decode it // If we reached this point then we weren't able to decode it
return fmt.Errorf("%s: unsupported type: %s", name, outputKind) return newDecodeError(name, fmt.Errorf("unsupported type: %s", outputKind))
} }
// If we reached here, then we successfully decoded SOMETHING, so // If we reached here, then we successfully decoded SOMETHING, so
@@ -564,9 +630,10 @@ func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value)
dataValType := dataVal.Type() dataValType := dataVal.Type()
if !dataValType.AssignableTo(val.Type()) { if !dataValType.AssignableTo(val.Type()) {
return fmt.Errorf( return newDecodeError(name, &UnconvertibleTypeError{
"'%s' expected type '%s', got '%s'", Expected: val,
name, val.Type(), dataValType) Value: data,
})
} }
val.Set(dataVal) val.Set(dataVal)
@@ -617,9 +684,10 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value)
} }
if !converted { if !converted {
return fmt.Errorf( return newDecodeError(name, &UnconvertibleTypeError{
"'%s' expected type '%s', got unconvertible type '%s', value: '%v'", Expected: val,
name, val.Type(), dataVal.Type(), data) Value: data,
})
} }
return nil return nil
@@ -653,20 +721,28 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) er
if err == nil { if err == nil {
val.SetInt(i) val.SetInt(i)
} else { } else {
return fmt.Errorf("cannot parse '%s' as int: %s", name, err) return newDecodeError(name, &ParseError{
Expected: val,
Value: data,
Err: err,
})
} }
case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number":
jn := data.(json.Number) jn := data.(json.Number)
i, err := jn.Int64() i, err := jn.Int64()
if err != nil { if err != nil {
return fmt.Errorf( return newDecodeError(name, &ParseError{
"error decoding json.Number into %s: %s", name, err) Expected: val,
Value: data,
Err: err,
})
} }
val.SetInt(i) val.SetInt(i)
default: default:
return fmt.Errorf( return newDecodeError(name, &UnconvertibleTypeError{
"'%s' expected type '%s', got unconvertible type '%s', value: '%v'", Expected: val,
name, val.Type(), dataVal.Type(), data) Value: data,
})
} }
return nil return nil
@@ -681,8 +757,11 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e
case dataKind == reflect.Int: case dataKind == reflect.Int:
i := dataVal.Int() i := dataVal.Int()
if i < 0 && !d.config.WeaklyTypedInput { if i < 0 && !d.config.WeaklyTypedInput {
return fmt.Errorf("cannot parse '%s', %d overflows uint", return newDecodeError(name, &ParseError{
name, i) Expected: val,
Value: data,
Err: fmt.Errorf("%d overflows uint", i),
})
} }
val.SetUint(uint64(i)) val.SetUint(uint64(i))
case dataKind == reflect.Uint: case dataKind == reflect.Uint:
@@ -690,8 +769,11 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e
case dataKind == reflect.Float32: case dataKind == reflect.Float32:
f := dataVal.Float() f := dataVal.Float()
if f < 0 && !d.config.WeaklyTypedInput { if f < 0 && !d.config.WeaklyTypedInput {
return fmt.Errorf("cannot parse '%s', %f overflows uint", return newDecodeError(name, &ParseError{
name, f) Expected: val,
Value: data,
Err: fmt.Errorf("%f overflows uint", f),
})
} }
val.SetUint(uint64(f)) val.SetUint(uint64(f))
case dataKind == reflect.Bool && d.config.WeaklyTypedInput: case dataKind == reflect.Bool && d.config.WeaklyTypedInput:
@@ -710,20 +792,28 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e
if err == nil { if err == nil {
val.SetUint(i) val.SetUint(i)
} else { } else {
return fmt.Errorf("cannot parse '%s' as uint: %s", name, err) return newDecodeError(name, &ParseError{
Expected: val,
Value: data,
Err: err,
})
} }
case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number":
jn := data.(json.Number) jn := data.(json.Number)
i, err := strconv.ParseUint(string(jn), 0, 64) i, err := strconv.ParseUint(string(jn), 0, 64)
if err != nil { if err != nil {
return fmt.Errorf( return newDecodeError(name, &ParseError{
"error decoding json.Number into %s: %s", name, err) Expected: val,
Value: data,
Err: err,
})
} }
val.SetUint(i) val.SetUint(i)
default: default:
return fmt.Errorf( return newDecodeError(name, &UnconvertibleTypeError{
"'%s' expected type '%s', got unconvertible type '%s', value: '%v'", Expected: val,
name, val.Type(), dataVal.Type(), data) Value: data,
})
} }
return nil return nil
@@ -749,12 +839,17 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) e
} else if dataVal.String() == "" { } else if dataVal.String() == "" {
val.SetBool(false) val.SetBool(false)
} else { } else {
return fmt.Errorf("cannot parse '%s' as bool: %s", name, err) return newDecodeError(name, &ParseError{
Expected: val,
Value: data,
Err: err,
})
} }
default: default:
return fmt.Errorf( return newDecodeError(name, &UnconvertibleTypeError{
"'%s' expected type '%s', got unconvertible type '%s', value: '%v'", Expected: val,
name, val.Type(), dataVal.Type(), data) Value: data,
})
} }
return nil return nil
@@ -788,20 +883,28 @@ func (d *Decoder) decodeFloat(name string, data interface{}, val reflect.Value)
if err == nil { if err == nil {
val.SetFloat(f) val.SetFloat(f)
} else { } else {
return fmt.Errorf("cannot parse '%s' as float: %s", name, err) return newDecodeError(name, &ParseError{
Expected: val,
Value: data,
Err: err,
})
} }
case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number": case dataType.PkgPath() == "encoding/json" && dataType.Name() == "Number":
jn := data.(json.Number) jn := data.(json.Number)
i, err := jn.Float64() i, err := jn.Float64()
if err != nil { if err != nil {
return fmt.Errorf( return newDecodeError(name, &ParseError{
"error decoding json.Number into %s: %s", name, err) Expected: val,
Value: data,
Err: err,
})
} }
val.SetFloat(i) val.SetFloat(i)
default: default:
return fmt.Errorf( return newDecodeError(name, &UnconvertibleTypeError{
"'%s' expected type '%s', got unconvertible type '%s', value: '%v'", Expected: val,
name, val.Type(), dataVal.Type(), data) Value: data,
})
} }
return nil return nil
@@ -815,9 +918,10 @@ func (d *Decoder) decodeComplex(name string, data interface{}, val reflect.Value
case dataKind == reflect.Complex64: case dataKind == reflect.Complex64:
val.SetComplex(dataVal.Complex()) val.SetComplex(dataVal.Complex())
default: default:
return fmt.Errorf( return newDecodeError(name, &UnconvertibleTypeError{
"'%s' expected type '%s', got unconvertible type '%s', value: '%v'", Expected: val,
name, val.Type(), dataVal.Type(), data) Value: data,
})
} }
return nil return nil
@@ -861,7 +965,10 @@ func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) er
fallthrough fallthrough
default: default:
return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) return newDecodeError(name, &UnconvertibleTypeError{
Expected: val,
Value: data,
})
} }
} }
@@ -947,7 +1054,10 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
// to the map value. // to the map value.
v := dataVal.Field(i) v := dataVal.Field(i)
if !v.Type().AssignableTo(valMap.Type().Elem()) { if !v.Type().AssignableTo(valMap.Type().Elem()) {
return fmt.Errorf("cannot assign type '%s' to map value field of type '%s'", v.Type(), valMap.Type().Elem()) return newDecodeError(
name+"."+f.Name,
fmt.Errorf("cannot assign type %q to map value field of type %q", v.Type(), valMap.Type().Elem()),
)
} }
tagValue := f.Tag.Get(d.config.TagName) tagValue := f.Tag.Get(d.config.TagName)
@@ -972,8 +1082,13 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
continue continue
} }
// If "omitzero" is specified in the tag, it ignores zero values.
if strings.Index(tagValue[index+1:], "omitzero") != -1 && v.IsZero() {
continue
}
// If "squash" is specified in the tag, we squash the field down. // If "squash" is specified in the tag, we squash the field down.
squash = squash || strings.Index(tagValue[index+1:], "squash") != -1 squash = squash || strings.Contains(tagValue[index+1:], d.config.SquashTagOption)
if squash { if squash {
// When squashing, the embedded type can be a pointer to a struct. // When squashing, the embedded type can be a pointer to a struct.
if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct { if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct {
@@ -982,12 +1097,18 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re
// The final type must be a struct // The final type must be a struct
if v.Kind() != reflect.Struct { if v.Kind() != reflect.Struct {
return fmt.Errorf("cannot squash non-struct type '%s'", v.Type()) return newDecodeError(
name+"."+f.Name,
fmt.Errorf("cannot squash non-struct type %q", v.Type()),
)
} }
} else { } else {
if strings.Index(tagValue[index+1:], "remain") != -1 { if strings.Index(tagValue[index+1:], "remain") != -1 {
if v.Kind() != reflect.Map { if v.Kind() != reflect.Map {
return fmt.Errorf("error remain-tag field with invalid type: '%s'", v.Type()) return newDecodeError(
name+"."+f.Name,
fmt.Errorf("error remain-tag field with invalid type: %q", v.Type()),
)
} }
ptr := v.MapRange() ptr := v.MapRange()
@@ -1107,9 +1228,10 @@ func (d *Decoder) decodeFunc(name string, data interface{}, val reflect.Value) e
// into that. Then set the value of the pointer to this type. // into that. Then set the value of the pointer to this type.
dataVal := reflect.Indirect(reflect.ValueOf(data)) dataVal := reflect.Indirect(reflect.ValueOf(data))
if val.Type() != dataVal.Type() { if val.Type() != dataVal.Type() {
return fmt.Errorf( return newDecodeError(name, &UnconvertibleTypeError{
"'%s' expected type '%s', got unconvertible type '%s', value: '%v'", Expected: val,
name, val.Type(), dataVal.Type(), data) Value: data,
})
} }
val.Set(dataVal) val.Set(dataVal)
return nil return nil
@@ -1150,8 +1272,8 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
} }
} }
return fmt.Errorf( return newDecodeError(name,
"'%s': source data must be an array or slice, got %s", name, dataValKind) fmt.Errorf("source data must be an array or slice, got %s", dataValKind))
} }
// If the input value is nil, then don't allocate since empty != nil // If the input value is nil, then don't allocate since empty != nil
@@ -1218,13 +1340,13 @@ func (d *Decoder) decodeArray(name string, data interface{}, val reflect.Value)
} }
} }
return fmt.Errorf( return newDecodeError(name,
"'%s': source data must be an array or slice, got %s", name, dataValKind) fmt.Errorf("source data must be an array or slice, got %s", dataValKind))
} }
if dataVal.Len() > arrayType.Len() { if dataVal.Len() > arrayType.Len() {
return fmt.Errorf( return newDecodeError(name,
"'%s': expected source data to have length less or equal to %d, got %d", name, arrayType.Len(), dataVal.Len()) fmt.Errorf("expected source data to have length less or equal to %d, got %d", arrayType.Len(), dataVal.Len()))
} }
// Make a new array to hold our result, same size as the original data. // Make a new array to hold our result, same size as the original data.
@@ -1289,16 +1411,16 @@ func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value)
return result return result
default: default:
return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) return newDecodeError(name,
fmt.Errorf("expected a map or struct, got %q", dataValKind))
} }
} }
func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error { func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error {
dataValType := dataVal.Type() dataValType := dataVal.Type()
if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface {
return fmt.Errorf( return newDecodeError(name,
"'%s' needs a map with string keys, has '%s' keys", fmt.Errorf("needs a map with string keys, has %q keys", kind))
name, dataValType.Key().Kind())
} }
dataValKeys := make(map[reflect.Value]struct{}) dataValKeys := make(map[reflect.Value]struct{})
@@ -1351,7 +1473,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
// We always parse the tags cause we're looking for other tags too // We always parse the tags cause we're looking for other tags too
tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",") tagParts := strings.Split(fieldType.Tag.Get(d.config.TagName), ",")
for _, tag := range tagParts[1:] { for _, tag := range tagParts[1:] {
if tag == "squash" { if tag == d.config.SquashTagOption {
squash = true squash = true
break break
} }
@@ -1363,10 +1485,18 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
} }
if squash { if squash {
if fieldVal.Kind() != reflect.Struct { switch fieldVal.Kind() {
errs = append(errs, fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldVal.Kind())) case reflect.Struct:
} else {
structs = append(structs, fieldVal) structs = append(structs, fieldVal)
case reflect.Interface:
if !fieldVal.IsNil() {
structs = append(structs, fieldVal.Elem().Elem())
}
default:
errs = append(errs, newDecodeError(
name+"."+fieldType.Name,
fmt.Errorf("unsupported type for squash: %s", fieldVal.Kind()),
))
} }
continue continue
} }
@@ -1417,7 +1547,9 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
if !rawMapVal.IsValid() { if !rawMapVal.IsValid() {
// There was no matching key in the map for the value in // There was no matching key in the map for the value in
// the struct. Remember it for potential errors and metadata. // the struct. Remember it for potential errors and metadata.
targetValKeysUnused[fieldName] = struct{}{} if !(d.config.AllowUnsetPointer && fieldValue.Kind() == reflect.Ptr) {
targetValKeysUnused[fieldName] = struct{}{}
}
continue continue
} }
} }
@@ -1473,8 +1605,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
} }
sort.Strings(keys) sort.Strings(keys)
err := fmt.Errorf("'%s' has invalid keys: %s", name, strings.Join(keys, ", ")) errs = append(errs, newDecodeError(
errs = append(errs, err) name,
fmt.Errorf("has invalid keys: %s", strings.Join(keys, ", ")),
))
} }
if d.config.ErrorUnset && len(targetValKeysUnused) > 0 { if d.config.ErrorUnset && len(targetValKeysUnused) > 0 {
@@ -1484,8 +1618,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
} }
sort.Strings(keys) sort.Strings(keys)
err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", ")) errs = append(errs, newDecodeError(
errs = append(errs, err) name,
fmt.Errorf("has unset fields: %s", strings.Join(keys, ", ")),
))
} }
if err := errors.Join(errs...); err != nil { if err := errors.Join(errs...); err != nil {
+2 -2
View File
@@ -119,7 +119,7 @@ github.com/aws/smithy-go/transport/http/internal/io
# github.com/cenkalti/backoff/v4 v4.3.0 # github.com/cenkalti/backoff/v4 v4.3.0
## explicit; go 1.18 ## explicit; go 1.18
github.com/cenkalti/backoff/v4 github.com/cenkalti/backoff/v4
# github.com/compose-spec/compose-go/v2 v2.7.1 # github.com/compose-spec/compose-go/v2 v2.7.2-0.20250703132301-891fce532a51
## explicit; go 1.23 ## explicit; go 1.23
github.com/compose-spec/compose-go/v2/cli github.com/compose-spec/compose-go/v2/cli
github.com/compose-spec/compose-go/v2/consts github.com/compose-spec/compose-go/v2/consts
@@ -328,7 +328,7 @@ github.com/go-openapi/jsonreference/internal
# github.com/go-openapi/swag v0.23.0 # github.com/go-openapi/swag v0.23.0
## explicit; go 1.20 ## explicit; go 1.20
github.com/go-openapi/swag github.com/go-openapi/swag
# github.com/go-viper/mapstructure/v2 v2.0.0 # github.com/go-viper/mapstructure/v2 v2.3.0
## explicit; go 1.18 ## explicit; go 1.18
github.com/go-viper/mapstructure/v2 github.com/go-viper/mapstructure/v2
github.com/go-viper/mapstructure/v2/internal/errors github.com/go-viper/mapstructure/v2/internal/errors