goviper-go

Idiomatic way to conditionally unmarshal a viper config (toml) into structs


I'm new to Go and would like to know how to solve the following in an idiomatic fashion:

I am using viper to load config files into the program. I chose the toml format because I want to have a config file which can specify several different formats of required input: for instance the Alpha provider requires an apikey, while the Beta provider requires username and password.

[alpha]
apikey = "123"
domain = "example.com"

# [beta]
# username = ""
# password = ""
# domain = ""
type ProviderService interface {                                 
    PrintDomain()                                                
}                                                                
                                                                 
type Provider struct {                                           
    Alpha `mapstructure:"alpha"`                                 
    Beta  `mapstructure:"beta"`                                                     
}                                                                
                                                                 
type Alpha struct {                                              
    Apikey string `mapstructure:"apikey"`                        
    Domain string `mapstructure:"domain"`                        
}                                                                
                                                                 
type Beta struct {                                               
    Username string `mapstructure:"username"`                    
    Password string `mapstructure:"password"`                    
    Domain   string `mapstructure:"domain"`                      
}                                                                
                                                                 
func main() {                                                    
    provider := loadConfig()                                     
    fmt.Printf("%+v\n", provider)                                
    // provider.DoAThing()  # <==== Want to do this; currently results in "ambiguous selector provider.DoAThing"                              
}                                                                
                                                                 
func (a Alpha) DoAThing() {                                   
    fmt.Println("domain", a.Domain)                              
}                                                                
func (b Beta) DoAThing() {                                    
    fmt.Println("domain", b.Domain)                              
}                                                                
                                                                 
func loadConfig() (p Provider) {                                 
    viper.AddConfigPath("./")                                    
    viper.SetConfigName("config")                                
    viper.SetConfigType("toml")                                  
                                                                 
    err := viper.ReadInConfig()                                  
    if err != nil {                                              
        panic(fmt.Errorf("Fatal error config file: %w \n", err)) 
    }                                                            
                                                                 
    err = viper.Unmarshal(&p)                                    
    if err != nil {                                              
        log.Fatal("unable to decode into struct", err)           
    }                                                            
                                                                 
    return p                                                     
}

The code above results in {Alpha:{Apikey:123 Domain:example.com} Beta:{Username: Password: Domain:}} where the empty/unused struct is still present.

Ultimately I want the ProviderService interface to be provider agnostic so I can simply call provider.PrintDomain() instead of provider.Alpha.PrintDomain() and have the code littered with if/else statements. I am also open to other ways of structuring the code to achieve this outcome.

Thanks in advance!


Solution

  • This is what I ended up with that allows me to conditionally load a struct based on what is specified in the toml file, and still use an interface to treat it as struct agnostic.

    If anyone has any tips on refactoring this to be more idiomatic, please let me know!

    type ProviderService interface {
        DoAThing()
    }
    
    type Alpha struct {
        Apikey string `mapstructure:"apikey"`
        Domain string `mapstructure:"domain"`
    }
    
    type Beta struct {
        Username string `mapstructure:"username"`
        Password string `mapstructure:"password"`
        Domain   string `mapstructure:"domain"`
    }
    
    func main() {
        provider := loadConfig()
        if provider == nil {
            log.Fatal("unable to parse config file")
        }
    
        provider.DoAThing()
    }
    
    func (a Alpha) DoAThing() {
        fmt.Println("domain", a.Domain)
    }
    func (b Beta) DoAThing() {
        fmt.Println("domain", b.Domain)
    }
    
    func loadConfig() ProviderService {
        viper.AddConfigPath("./")
        viper.SetConfigName("config")
        viper.SetConfigType("toml")
    
        err := viper.ReadInConfig()
        if err != nil {
            panic(fmt.Errorf("Fatal error config file: %w \n", err))
        }
    
        var a Alpha
        _ = viper.UnmarshalKey("alpha", &a)
        if a != (Alpha{}) {
            return a
        }
    
        var b Beta
        _ = viper.UnmarshalKey("beta", &b)
        if b != (Beta{}) {
            return b
        }
    
        return nil
    }