Using a TextUnmarshaler in a map value doesn't work

This issue has been created since 2021-11-27.

Could you explain how to apply this

func (d *duration) UnmarshalText(text []byte) (err error) {
	d.Duration, err = time.ParseDuration(string(text))
	return err
}

to

type httpclient struct {
    Timeouts map[string]string
}

to be able to write in the config like this

[httpclient.timeouts]
client = "60s"
2XX = "50ms"
3XX = "1000ms"
4XX = "2000ms"
5XX = "5000ms"

?

arp242 wrote this answer on 2021-11-27

Did you try map[string]duration? I think that should work, but I didn't try it.

GarryGaller wrote this answer on 2021-11-27

Yeah, I tried that.
But then all key type becomes duration type, which is incompatible with time.Duration for passing values to functions like time.Sleep, etc.

P.S. In the standard argument parser string command somehow made it possible to pass a string and get time.Duration...

arp242 wrote this answer on 2021-11-27

The key is a string, not a duration(?) And you can pass duration.Duration to time.Sleep().

It would be helpful if you could more clearly describe what exactly isn't working with some example.

GarryGaller wrote this answer on 2021-11-28

If you create a minimal example, there is an error: type mismatch for main.duration: expected table but found string
(and my code didn't compile at all for a slightly different reason)

package main

import (
    "github.com/BurntSushi/toml"
    "time"
    "fmt"
    "log"
)

type Config struct {
    HttpClient  httpclient
}
type httpclient struct {
    Timeouts   map[string]duration
}

type duration struct {
	time.Duration
} 

func (d *duration) UnmarshalText(text []byte) (err error) {
	d.Duration, err = time.ParseDuration(string(text))
	return err
}


func main() {
    var conf Config
    if _, err := toml.DecodeFile("toml_config.toml", &conf); err != nil {
         log.Fatal(err)
    } else {
        fmt.Printf("%v\n", conf)
        fmt.Printf("%v\n", conf.HttpClient.Timeouts)
    }
}

config_toml.toml

[httpclient.timeouts]
client = "60s"

Output error:

arp242 wrote this answer on 2021-11-28

Right; here's a bit of a clearer example:

type duration struct{ time.Duration }

func (d *duration) UnmarshalText(text []byte) (err error) {
	d.Duration, err = time.ParseDuration(string(text))
	return err
}

func main() {
	t := `
		[httpclient.timeouts]
		client = "60s"
		2XX = "50ms"
		3XX = "1000ms"
		4XX = "2000ms"
		5XX = "5000ms"
	`

	{
		var conf struct {
			HttpClient struct{ Timeouts map[string]string }
		}
		_, err := toml.Decode(t, &conf)

		// {{map[2XX:50ms 3XX:1000ms 4XX:2000ms 5XX:5000ms client:60s]}}
		fmt.Printf("%v\n%v\n\n", err, conf)
	}

	{
		var conf struct {
			HttpClient struct{ Timeouts map[string]duration }
		}
		_, err := toml.Decode(t, &conf)
		// toml: type mismatch for main.duration: expected table but found string
		fmt.Printf("%v\n%v\n", err, conf)
	}
}

Outputs:

<nil>
{{map[2XX:50ms 3XX:1000ms 4XX:2000ms 5XX:5000ms client:60s]}}

toml: type mismatch for main.duration: expected table but found string
{{map[]}}

The map[string]string works, but map[string]duration doesn't with a rather odd error.

Might be related to #195 (?) Curiously, it does work if you use UnmarshalTOML:

type duration struct{ time.Duration }

func (d *duration) UnmarshalTOML(text interface{}) (err error) {
    d.Duration, err = time.ParseDuration(text.(string))
    return err
}

Which outputs the expected:

<nil>
{{map[2XX:50ms 3XX:1000ms 4XX:2000ms 5XX:5000ms client:60s]}}

<nil>
{{map[2XX:50ms 3XX:1s 4XX:2s 5XX:5s client:1m0s]}}

So you can use that, although UnmarshalText should also work.

GarryGaller wrote this answer on 2021-11-28

So you can use that, although UnmarshalText should also work.

Yes, UnmarshalTOML works for a small example. But only as long as we don't start applying values from map, or assign new values like time.Duration to map.
Here are the errors from my actual application.
Many functions turn out not to be ready to accept the custom type duration.

engine\engine.go:152:27: cannot use delayTime5XX (type config.duration) as type time.Duration in argument to time.Sleep
engine\engine.go:225:31: cannot use delayTime5XX (type config.duration) as type time.Duration in argument to time.Sleep
engine\engine.go:261:23: cannot use delayTime2XX (type config.duration) as type time.Duration in argument to time.Sleep
engine\engine.go:304:26: invalid operation: goTimeOut > 0 (mismatched types config.duration and int)
engine\engine.go:305:27: cannot use goTimeOut (type config.duration) as type time.Duration in argument to time.Sleep
engine\handlers.go:198:27: cannot use requestTimeOut (type config.duration) as type time.Duration in argument to context.WithTimeout
engine\handlers.go:324:27: cannot use requestTimeOut (type config.duration) as type time.Duration in argument to context.WithTimeout
engine\handlers.go:475:27: cannot use requestTimeOut (type config.duration) as type time.Duration in argument to context.WithTimeout
engine\handlers.go:869:27: cannot use requestTimeOut (type config.duration) as type time.Duration in argument to context.WithTimeout
engine\sessionid.go:118:47: cannot use t.config.HttpClient.Timeouts["token_ttl"] (type config.duration) as type time.Duration in argument to token.TTL```

We need to somehow avoid creating a new type instead of time.Duration...
GarryGaller wrote this answer on 2021-11-28

Yes, of course, it will work if you write it that way:
time.Sleep(conf.HttpClient.Timeouts["client"].Duration)

but I wouldn't want to rewrite everything...

arp242 wrote this answer on 2021-11-28

You can use the .Duration field, e.g. with the example I posted above:

n := time.Now().Round(time.Second)
for _, d := range conf.HttpClient.Timeouts {
    fmt.Println(n, n.Add(d.Duration))
}

Shows:

2021-11-28 14:50:54 +0100 CET 2021-11-28 14:50:55 +0100 CET
2021-11-28 14:50:54 +0100 CET 2021-11-28 14:50:56 +0100 CET
2021-11-28 14:50:54 +0100 CET 2021-11-28 14:50:59 +0100 CET
2021-11-28 14:50:54 +0100 CET 2021-11-28 14:51:54 +0100 CET
2021-11-28 14:50:54 +0100 CET 2021-11-28 14:50:54.05 +0100 CET

Or alternatively, you can make Timeouts a custom map[string]Duration type with an unmarshal.

GarryGaller wrote this answer on 2021-11-28

OK, I used to pass my custom type .Duration field to the functions waiting for time.Duration.
Since there's no other way out.
But it's not very convenient.
Anyway, thank you for the clarification.

arp242 wrote this answer on 2021-11-28

Like I said, you can use a custom map type; I actually do something similar over here: https://github.com/arp242/z18n/blob/master/finder/finder.go#L61

Here's an example for your durations, which does what you want if I understand correctly:

type timeouts map[string]time.Duration

func (t *timeouts) UnmarshalTOML(m interface{}) error {
	mm, ok := m.(map[string]interface{})
	if !ok {
		return fmt.Errorf("wrong type: %T", m)
	}

	tt := *t
	if tt == nil {
		tt = make(map[string]time.Duration)
	}

	for k, v := range mm {
		vv, ok := v.(string)
		if !ok {
			return fmt.Errorf("wrong type for key %q: %T", k, v)
		}
		d, err := time.ParseDuration(vv)
		if err != nil {
			return err
		}
		tt[k] = d
	}
	*t = tt

	return nil
}

func main() {
	t := `
		[httpclient.timeouts]
		client = "60s"
		2XX = "50ms"
		3XX = "1000ms"
		4XX = "2000ms"
		5XX = "5000ms"
	`

	var conf struct {
		HttpClient struct{ Timeouts timeouts }
	}
	_, err := toml.Decode(t, &conf)
	fmt.Printf("%v\n%v\n", err, conf)

	n := time.Now().Round(time.Second)
	for _, d := range conf.HttpClient.Timeouts {
		fmt.Println(n, n.Add(d))
	}
}
GarryGaller wrote this answer on 2021-12-03

@arp242
Thanks a lot :-)
That's exactly what I wanted but couldn't come up with.

arp242 wrote this answer on 2022-05-27

The underlying issue of map values not working should be fixed now.

More Details About Repo
Owner Name BurntSushi
Repo Name toml
Full Name BurntSushi/toml
Language Go
Created Date 2013-02-26
Updated Date 2023-03-20
Star Count 4154
Watcher Count 84
Fork Count 518
Issue Count 15

YOU MAY BE INTERESTED

Issue Title Created Date Comment Count Updated Date
Chromebrew installation command does nothing 5 2023-02-12 2023-02-22
Obsolete tool-specific hack in 'state1' and friends? 4 2022-08-31 2023-02-12
AllowClientCertificate doesn't work when hosting on IIS with certificate authentication enabled 13 2021-12-09 2023-02-26
有希望支持pixi吗?用canvas发热量太高了 3 2021-06-08 2023-03-10
Size attribute in resulting dot file causes output to be clipped 8 2021-01-21 2023-03-02
Error on module installaton vue 3 with vue cli 0 2021-01-21 2023-02-28
KeyError: 'address 139644515901800 not present' 0 2021-05-26 2023-02-23
script fails (undefined is not an object) 4 2021-03-14 2023-02-15
reprocess BrainSeq Phase 1 data 11 2021-12-09 2023-02-23
GUI + Ipython Integrate Terminal Very Slow 2 2021-10-25 2023-03-07
3.0: libsemanage uses libsepol non-public symbols. 18 2020-02-22 2023-02-13
Use Circle CI container parallelization to speed up building/deployment 7 2020-06-25 2023-02-28
Table Summary is not working (for 508 tagging) 1 2022-03-11 2023-02-10
TyperError with prepro.py, "float() argument must be a string or number, not 'map' " 2 2017-10-30 2023-02-21
Error generating android package 3 2021-09-13 2023-02-23
Can't open search modal via keyboard when caps locked 2 2022-10-03 2023-02-22
The ''Normal' execution method locks tables for MySQL dumps 2 2022-04-06 2023-02-22
Login page not working @^6 5 2022-07-27 2023-02-12
Statevector draw with latex: 2 2023-02-24 2023-02-28
[Bug] [FEM] opening result histogram twice fails 0 2022-06-06 2023-02-05
Project predefined scales do not override global scales in labels and 2D map view widgets 0 2022-12-29 2023-02-17
notcurses-info: first "capability" line duplicated 2 2022-04-06 2023-03-12
[LOGS Enhancement] Materialised integer column result in 0. 1 2023-03-17 2023-03-14
Investigate update of Patternfly to be compatible with OS Console 5 2022-03-23 2023-02-10
Investigate moving some extension scripts to the end of <body> 6 2021-08-25 2022-06-28
55 - Union to Intersection 0 2022-09-06 2023-02-10
[bitnami/kafka] Offline partitions while kafka rolling update 7 2022-10-24 2023-02-04
cSpell in vscode 0 2021-12-28 2022-01-18
Returning match with custom error type give return type mismatch 0 2023-02-25 2023-02-26
Adding toggle switch for Light/Dark mode 2 2022-02-04 2023-02-12
Skeleton2D SkeletonModification2DCCIDK stops working when reloading scene 1 2023-02-12 2023-03-03
UNITLESS_ABS2 not accepting mixed types after #723 2 2022-01-30 2023-02-28
[BUG] updateListingInventory clears linked product variation images 10 2021-08-25 2023-03-15
When I use websocket server receive this string, the last } missing, I can't find the root cause. could you please check 0 2022-04-25 2023-02-28
support composer like notations 5 2016-06-10 2023-01-09
What does "for the current environment" mean 1 2022-01-13 2023-03-03
‘docker pull’ should cache the layer it downloaded and verified 2 2022-10-17 2023-02-19
Redis can delete incorrect keys 1 2021-12-15 2023-01-31
Fitness Function problem 2 2022-12-02 2023-02-16
Search "terms"? Searching for multiple words? 1 2022-11-01 2023-02-21
Get.width doesn't work in splash screen for release 0 2023-02-04 2023-03-06
LSP crashes when using mixins and the `on` clause 2 2022-05-31 2023-03-03
Buzzfeed URL changed 0 2017-05-27 2023-02-25
Add Tests for Flyout and Popup Launch 0 2023-01-18 2023-02-08
cute sound v2.0 problems 0 2022-05-31 2023-02-25
Send empty packet in cute_net seems broken 1 2022-05-30 2023-02-20
[linux] How setup datadog agent with python virtual-env ? 1 2019-01-15 2022-12-06
Prometheus autodiscovery customization 0 2021-06-21 2023-02-04
LoadAverage() Where is this used? 12 2022-07-03 2023-03-15
cross compile on aix fails 2 2022-02-02 2023-03-15