Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting specific indexes and sub-properties of a specific index in slices #192

Merged
merged 2 commits into from
Jan 29, 2025
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
111 changes: 69 additions & 42 deletions pkg/config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"gopkg.in/yaml.v3"

"github.com/chia-network/go-chia-libs/pkg/types"
"github.com/chia-network/go-chia-libs/pkg/util"
)

// FillValuesFromEnvironment reads environment variables starting with `chia.` and edits the config based on the config path
Expand Down Expand Up @@ -177,63 +178,89 @@ func setFieldByPath(v reflect.Value, path []string, value any) error {
fieldValue.SetMapIndex(mapKey, mapValue)
return nil
}
} else if fieldValue.Kind() == reflect.Slice && util.IsNumericInt(path[1]) {
sliceKey, err := strconv.Atoi(path[1])
if err != nil {
return fmt.Errorf("unable to parse slice index as int: %w", err)
}

if sliceKey >= fieldValue.Len() {
// Set a zero value and then call back to this function to go the other path
zeroSliceValue := reflect.Zero(fieldValue.Type().Elem())
fieldValue.Set(reflect.Append(fieldValue, zeroSliceValue))
}

sliceValue := fieldValue.Index(sliceKey)

if !sliceValue.IsValid() {
return fmt.Errorf("invalid slice value")
}
if len(path) < 3 {
// This is the case where we're setting the index directly to a single value (not a sub-value in a struct, etc)
return doValueSet(sliceValue, path[1:], value)
}
return setFieldByPath(sliceValue, path[2:], value)
} else {
return setFieldByPath(fieldValue, path[1:], value)
}
}

if !fieldValue.CanSet() {
return fmt.Errorf("cannot set field %s", path[0])
}
return doValueSet(fieldValue, path, value)
}
}

// Special Cases
if fieldValue.Type() == reflect.TypeOf(types.Uint128{}) {
strValue, ok := value.(string)
if !ok {
return fmt.Errorf("expected string for Uint128 field, got %T", value)
}
bigIntValue := new(big.Int)
_, ok = bigIntValue.SetString(strValue, 10)
if !ok {
return fmt.Errorf("invalid string for big.Int: %s", strValue)
}
fieldValue.Set(reflect.ValueOf(types.Uint128FromBig(bigIntValue)))
return nil
}
return nil
}

// Handle YAML (and therefore JSON) parsing for passing in entire structs/maps
// This is particularly useful if you want to pass in a whole blob of network constants at once
if fieldValue.Kind() == reflect.Struct || fieldValue.Kind() == reflect.Map || fieldValue.Kind() == reflect.Slice {
if strValue, ok := value.(string); ok {
yamlData := []byte(strValue)
if err := yaml.Unmarshal(yamlData, fieldValue.Addr().Interface()); err != nil {
return fmt.Errorf("failed to unmarshal yaml into field: %w", err)
}
// If we successfully replaced by doing yaml parsing into the field, then we should not try anything else
return nil
}
}
func doValueSet(fieldValue reflect.Value, path []string, value any) error {
if !fieldValue.CanSet() {
return fmt.Errorf("cannot set field %s", path[0])
}

val := reflect.ValueOf(value)
// Special Cases
if fieldValue.Type() == reflect.TypeOf(types.Uint128{}) {
strValue, ok := value.(string)
if !ok {
return fmt.Errorf("expected string for Uint128 field, got %T", value)
}
bigIntValue := new(big.Int)
_, ok = bigIntValue.SetString(strValue, 10)
if !ok {
return fmt.Errorf("invalid string for big.Int: %s", strValue)
}
fieldValue.Set(reflect.ValueOf(types.Uint128FromBig(bigIntValue)))
return nil
}

if fieldValue.Type() != val.Type() {
if val.Type().ConvertibleTo(fieldValue.Type()) {
val = val.Convert(fieldValue.Type())
} else {
convertedVal, err := convertValue(value, fieldValue.Type())
if err != nil {
return err
}
val = reflect.ValueOf(convertedVal)
}
// Handle YAML (and therefore JSON) parsing for passing in entire structs/maps
// This is particularly useful if you want to pass in a whole blob of network constants at once
if fieldValue.Kind() == reflect.Struct || fieldValue.Kind() == reflect.Map || fieldValue.Kind() == reflect.Slice {
if strValue, ok := value.(string); ok {
yamlData := []byte(strValue)
if err := yaml.Unmarshal(yamlData, fieldValue.Addr().Interface()); err != nil {
return fmt.Errorf("failed to unmarshal yaml into field: %w", err)
}
// If we successfully replaced by doing yaml parsing into the field, then we should not try anything else
return nil
}
}

fieldValue.Set(val)
val := reflect.ValueOf(value)

return nil
if fieldValue.Type() != val.Type() {
if val.Type().ConvertibleTo(fieldValue.Type()) {
val = val.Convert(fieldValue.Type())
} else {
convertedVal, err := convertValue(value, fieldValue.Type())
if err != nil {
return err
}
val = reflect.ValueOf(convertedVal)
}
}

fieldValue.Set(val)

return nil
}

Expand Down
41 changes: 41 additions & 0 deletions pkg/config/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,47 @@ func TestChiaConfig_SetFieldByPath_Lists(t *testing.T) {
}, defaultConfig.FullNode.FullNodePeers)
}

func TestChiaConfig_SetFieldByPath_Lists_SingleItems(t *testing.T) {
defaultConfig, err := config.LoadDefaultConfig()
assert.NoError(t, err)
// Make assertions about the default state, to ensure the assumed initial values are correct
assert.Equal(t, []string{}, defaultConfig.Seeder.StaticPeers)
assert.Equal(t, []config.Peer{}, defaultConfig.FullNode.FullNodePeers)

err = defaultConfig.SetFieldByPath([]string{"seeder", "static_peers", "0"}, "test-host.chia.net")
assert.NoError(t, err)
assert.Equal(t, []string{"test-host.chia.net"}, defaultConfig.Seeder.StaticPeers)

err = defaultConfig.SetFieldByPath([]string{"full_node", "full_node_peers", "0", "host"}, "node-0-override.chia.net")
assert.NoError(t, err)
assert.Equal(t, "node-0-override.chia.net", defaultConfig.FullNode.FullNodePeers[0].Host)

defaultConfig.FullNode.FullNodePeers = []config.Peer{{Host: "testnode.example.com", Port: 1234}}
err = defaultConfig.SetFieldByPath([]string{"full_node", "full_node_peers", "0", "host"}, "node-0-override-2.chia.net")
assert.NoError(t, err)
assert.Equal(t, "node-0-override-2.chia.net", defaultConfig.FullNode.FullNodePeers[0].Host)
assert.Equal(t, uint16(1234), defaultConfig.FullNode.FullNodePeers[0].Port)

err = defaultConfig.SetFieldByPath([]string{"full_node", "full_node_peers", "0", "port"}, "8444")
assert.NoError(t, err)
assert.Equal(t, "node-0-override-2.chia.net", defaultConfig.FullNode.FullNodePeers[0].Host)
assert.Equal(t, uint16(8444), defaultConfig.FullNode.FullNodePeers[0].Port)

defaultConfig, err = config.LoadDefaultConfig()
assert.NoError(t, err)
// Make assertions about the default state, to ensure the assumed initial values are correct
assert.Equal(t, []string{}, defaultConfig.Seeder.StaticPeers)
assert.Equal(t, []config.Peer{}, defaultConfig.FullNode.FullNodePeers)

err = defaultConfig.SetFieldByPath([]string{"full_node", "full_node_peers", "0"}, config.Peer{
Host: "node-0-override-frompeer.chia.net",
Port: 9999,
})
assert.NoError(t, err)
assert.Equal(t, "node-0-override-frompeer.chia.net", defaultConfig.FullNode.FullNodePeers[0].Host)
assert.Equal(t, uint16(9999), defaultConfig.FullNode.FullNodePeers[0].Port)
}

func TestChiaConfig_FillValuesFromEnvironment(t *testing.T) {
defaultConfig, err := config.LoadDefaultConfig()
assert.NoError(t, err)
Expand Down
11 changes: 11 additions & 0 deletions pkg/util/number.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package util

import (
"strconv"
)

// IsNumericInt returns true if the given string represents an integer
func IsNumericInt(s string) bool {
_, err := strconv.Atoi(s)
return err == nil
}