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

Parsing different structures depending on a field #125

Closed
rasky opened this issue Mar 8, 2016 · 5 comments
Closed

Parsing different structures depending on a field #125

rasky opened this issue Mar 8, 2016 · 5 comments

Comments

@rasky
Copy link

rasky commented Mar 8, 2016

Hi, I would like to parse a TOML made like this:

[connection]
type = "http"

    [connection.config]
    url = "https://foobar.com/api"
    forcessl = True

or:

[connection]
type = "tcp"

    [connection.config]
    ip = "12.12.12.12"
    port = 1234

so basically the config table changes its keys depending on connection.type. It looks like this library allows me to define a structure like this:

type Connection struct {
    Type string
    Config interface{}
}

and then the config table is read into a map[string]interface{}. Instead, I would be able to decode those configuration keys into their own structures, such as:

type ConfigHttp struct {
    Url string
    ForceSsl bool
}

type ConfigIp struct {
    Host string
    Port int
}

So, I have two questions:

  1. Is it possible to first parse the config file, and later, once the Type has been parsed, further decode the map[string]interface{} into the actual structure? I couldn't find an API to do this.
  2. Alternatively, I tried parsing the file two times (which would be acceptable). The first time, the map[string]interface{} is generated, but I ignore it. I then instantiate the correct configuration structure and put it into the config field, eg: conn.Config = ConfigHttp{} and proceed to run a second decoding, but unfortunately it seems to be ignored by the library, because the field's content is discarded and overwritten with a new map[string]interface{} instance. BTW this behavior doesn't match what I would expect; for instance, when I use encoding/json to decode a JSON into a structure that contains an interface{} with a reference to an actual object, the library "sees through" the interface{} and decodes into the actual structure.

Would you accept a patch that implements the roadmap presented in option 2? That is, if an interface{} field is non-nil and is a struct, decodes into it rather than just overwriting it with the map[string]interface{} instance?

@cespare
Copy link
Collaborator

cespare commented Mar 8, 2016

Use toml.Primitive to delay encoding instead of interface{}. (This is like json.RawMessage.)

package main

import (
    "fmt"
    "log"

    "github.com/BurntSushi/toml"
)

type ConfigHTTP struct {
    URL      string
    ForceSSL bool
}

type ConfigIP struct {
    Host string
    Port int
}

const input0 = `
[connection]
type = "http"
    [connection.config]
    url = "https://foobar.com/api"
    forcessl = true
`

const input1 = `
[connection]
type = "tcp"
    [connection.config]
        host = "12.12.12.12"
    port = 1234
`

type connection struct {
    Connection struct {
        Type   string
        Config toml.Primitive
    }
}

func parseConfig(s string) (interface{}, error) { // or return a more specific interface
    var conn connection
    md, err := toml.Decode(s, &conn)
    if err != nil {
        return nil, err
    }
    var conf interface{}
    switch conn.Connection.Type {
    case "http":
        conf = new(ConfigHTTP)
    case "tcp":
        conf = new(ConfigIP)
    default:
        return nil, fmt.Errorf("unknown type %q", conn.Connection.Type)
    }
    if err := md.PrimitiveDecode(conn.Connection.Config, conf); err != nil {
        return nil, err
    }
    return conf, nil
}

func main() {
    for _, s := range []string{input0, input1} {
        conf, err := parseConfig(s)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%#v\n", conf)
    }
}
&main.ConfigHTTP{URL:"https://foobar.com/api", ForceSSL:true}
&main.ConfigIP{Host:"12.12.12.12", Port:1234}

@cespare cespare closed this as completed Mar 8, 2016
@rasky
Copy link
Author

rasky commented Mar 8, 2016

Thanks, I'm trying this approach now. I found out that, if the config section is not present, PrimitiveDecode() exits with an error: Type mismatch for main.ConfigHTTP. Expected map but found '<nil>'.. You can try by changing one of your test inputs like this:

const input0 = `
[connection]
type = "http"
`

Is this behavior expected?

@cespare
Copy link
Collaborator

cespare commented Mar 9, 2016

Interesting, that should work. I filed #126.

In the meantime, you can work around this by using a *Primitive and explicitly checking the nil case yourself:

http://play.golang.org/p/fKiiL7a-uc

rasky added a commit to rasky/toml that referenced this issue Mar 9, 2016
Not needed anymore after workaround was proposed:
BurntSushi#125

This reverts commit cb8d1d1.
cespare added a commit that referenced this issue Mar 9, 2016
@cespare
Copy link
Collaborator

cespare commented Mar 9, 2016

Hey @rasky, I also fixed the empty Primitive case so now my original code works if you don't have any config section.

@rasky
Copy link
Author

rasky commented Mar 9, 2016

Thanks! BTW I suggest you add this or a similar example of delayed encoding to the README

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants