How to design classes for X number of config files which needs to be read individually in memory?

Issue

I am working with lot of config files. I need to read all those individual config file in their own struct and then make one giant Config struct which holds all other individual config struct in it.

Let’s suppose if I am working with 3 config files.

  • ClientConfig deals with one config file.
  • DataMapConfig deals with second config file.
  • ProcessDataConfig deals with third config file.

I created separate class for each of those individual config file and have separate Readxxxxx method in them to read their own individual config and return struct back. Below is my config.go file which is called via Init method from main function after passing path and logger.

config.go

package config

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "github.com/david/internal/utilities"
)

type Config struct {
    ClientMapConfigs   ClientConfig
    DataMapConfigs     DataMapConfig
    ProcessDataConfigs ProcessDataConfig
}

func Init(path string, logger log.Logger) (*Config, error) {
    var err error
    clientConfig, err := ReadClientMapConfig(path, logger)
    dataMapConfig, err := ReadDataMapConfig(path, logger)
    processDataConfig, err := ReadProcessDataConfig(path, logger)
    if err != nil {
        return nil, err
    }
    return &Config{
        ClientMapConfigs:       *clientConfig,
        DataMapConfigs:         *dataMapConfig,
        ProcessDataConfigs:     *processDataConfig,
    }, nil
}

clientconfig.go

package config

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "github.com/david/internal/utilities"
)

type ClientConfig struct {
  .....
  .....
}

const (
    ClientConfigFile = "clientConfigMap.json"
)

func ReadClientMapConfig(path string, logger log.Logger) (*ClientConfig, error) {
  files, err := utilities.FindFiles(path, ClientConfigFile)
  // read all the files
  // do some validation on all those files
  // deserialize them into ClientConfig struct
  // return clientconfig object back
}

datamapconfig.go

Similar style I have for datamapconfig too. Exactly replica of clientconfig.go file but operating on different config file name and will return DataMapConfig struct back.

processdataConfig.go

Same thing as clientconfig.go file. Only difference is it will operate on different config file and return ProcessDataConfig struct back.

Problem Statement

I am looking for ideas where this above design can be improved? Is there any better way to do this in golang? Can we use interface or anything else which can improve the above design?

If I have let’s say 10 different files instead of 3, then do I need to keep doing above same thing for remaining 7 files? If yes, then the code will look ugly. Any suggestions or ideas will greatly help me.

Update

Everything looks good but I have few questions as I am confuse on how can I achieve those with your current suggestion. On majority of my configs, your suggestion is perfect but there are two cases on two different configs where I am confuse on how to do it.

  • Case 1 After deserializing json into original struct which matches json format, I make another different struct after massaging that data and then I return that struct back.
  • Case 2 All my configs have one file but there are few configs which have multiple files in them and the number isn’t fixed. So I pass regex file name and then I find all the files starting with that regex and then loop over all those files one by one. After deserializing each json file, I start populating another object and keep populating it until all files have been deserialized and then make a new struct with those objects and then return it.

Example of above scenarios:

Sample case 1

package config

import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "github.com/david/internal/utilities"
)

type CustomerManifest struct {
  CustomerManifest map[int64]Site
}

type CustomerConfigs struct {
  CustomerConfigurations []Site `json:"customerConfigurations"`
}

type Site struct {
  ....
  ....
}

const (
  CustomerConfigFile = "abc.json"
)

func ReadCustomerConfig(path string, logger log.Logger) (*CustomerManifest, error) {
  // I try to find all the files with my below utility method.
  // Work with single file name and also with regex name
  files, err := utilities.FindFiles(path, CustomerConfigFile)
  if err != nil {
    return nil, err
  }
  var customerConfig CustomerConfigs
  // there is only file for this config so loop will run once
  for _, file := range files {
    body, err := ioutil.ReadFile(file)
    if err != nil {
      return nil, err
    }

    err = json.Unmarshal(body, &customerConfig)
    if err != nil {
      return nil, err
    }
  }

  customerConfigIndex := BuildIndex(customerConfig, logger)
  return &CustomerManifest{CustomerManifest: customerConfigIndex}, nil
}

func BuildIndex(customerConfig CustomerConfigs, logger log.Logger) map[int64]Site {
  ...
  ...
}

As you can see above in sample case 1, I am making CustomerManifest struct from CustomerConfigs struct and then return it instead of returning CustomerConfigs directly.

Sample case 2

package config

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
  "github.com/david/internal/utilities"
)

type StateManifest struct {
    NotionTemplates       NotionTemplates
    NotionIndex           map[int64]NotionTemplates
}

type NotionMapConfigs struct {
    NotionTemplates      []NotionTemplates      `json:"notionTemplates"`
  ...
}

const (
  // there are many files starting with "state-", it's not fixed number
    StateConfigFile = "state-*.json"
)

func ReadStateConfig(path string, logger log.Logger) (*StateManifest, error) {
  // I try to find all the files with my below utility method.
  // Work with single file name and also with regex name
    files, err := utilities.FindFiles(path, StateConfigFile)
    if err != nil {
        return nil, err
    }
    var defaultTemp NotionTemplates
    var idx = map[int64]NotionTemplates{}

  // there are lot of config files for this config so loop will run multiple times
    for _, file := range files {
        var notionMapConfig NotionMapConfigs
        body, err := ioutil.ReadFile(file)
        if err != nil {
            return nil, err
        }
        err = json.Unmarshal(body, &notionMapConfig)
        if err != nil {
            return nil, err
        }

        for _, tt := range notionMapConfig.NotionTemplates {
            if tt.IsProcess {
                defaultTemp = tt
            } else if tt.ClientId > 0 {
                idx[tt.ClientId] = tt
            }
        }
    }

    stateManifest := StateManifest{
        NotionTemplates:       defaultTemp,
        NotionIndex:           idx,
    }
    return &stateManifest, nil
}

As you can see above in my both the cases, I am making another different struct after deserializing is done and then I return that struct back but as of now in your current suggestion I think I won’t be able to do this generically because for each config I do different type of massaging and then return those struct back. Is there any way to achieve above functionality with your current suggestion? Basically for each config if I want to do some massaging, then I should be able to do it and return new modified struct back but for some cases if I don’t want to do any massaging then I can return direct deserialize json struct back. Can this be done generically?

Since there are config which has multiple files in them so that is why I was using my utilities.FindFiles method to give me all files basis on file name or regex name and then I loop over all those files to either return original struct back or return new struct back after massaging original struct data.

Solution

You can use a common function to load all the configuration files.

Assume you have config structures:

type Config1 struct {...}
type Config2 struct {...}
type Config3 struct {...}

You define configuration validators for those who need it:

func (c Config1) Validate() error {...}
func (c Config2) Validate() error {...}

Note that these implement a Validatable interface:

type Validatable interface {
   Validate() error
}

There is one config type that includes all these configurations:

type Config struct {
   C1 Config1
   C2 Config2
   C3 Config3
   ...
}

Then, you can define a simple configuration loader function:

func LoadConfig(fname string, out interface{}) error {
    data, err:=ioutil.ReadFile(fname)
    if err!=nil {
       return err
    }
    if err:=json.Unmarshal(data,out); err!=nil {
       return err
    }
    // Validate the config if necessary
    if validator, ok:=out.(Validatable); ok {
       if err:=validator.Validate(); err!=nil {
         return err
       }
    }
    return nil
}

Then, you can load the files:

var c Config
if err:=LoadConfig("file1",&c.C1); err!=nil {
   return err
}
if err:=LoadConfig("file2",&c.C2); err!=nil {
   return err
}
...

If there are multiple files loading different parts of the same struct, you can do:

LoadConfig("file1",&c.C3)
LoadConfig("file2",&c.C3)
...

You can simplify this further by defining a slice:

type cfgInfo struct {
   fileName string
   getCfg func(*Config) interface{}
}

var configs=[]cfgInfo {
   {
     fileName: "file1",
     getCfg: func(c *Config) interface{} {return &c.C1},
   },
   {
     fileName: "file2",
     getCfg: func(c *Config) interface{} {return &c.C2},
   },
   {
     fileName: "file3",
     getCfg: func(c *Config) interface{} {return &c.C3},
   },
   ...
}

func loadConfigs(cfg *Config) error {
   for _,f:=range configs {
     if err:=loadConfig(f.fileName,f.getCfg(cfg)); err!=nil {
         return err
     }
   }
  return nil
}

Then, loadConfigs would load all the configuration files into cfg.

func main() {
   var cfg Config
   if err:=loadConfigs(&cfg); err!=nil {
      panic(err)
   }
   ...
}

Any configuration that doesn’t match this pattern can be dealt with using LoadConfig:

var customConfig1 CustomConfigStruct1
if err:=LoadConfig("customConfigFile1",&customConfig1); err!=nil {
   panic(err)
}
cfg.CustomConfig1 = processCustomConfig1(customConfig1)

var customConfig2 CustomConfigStruct2
if err:=LoadConfig("customConfigFile2",&customConfig2); err!=nil {
   panic(err)
}
cfg.CustomConfig2 = processCustomConfig2(customConfig2)

Answered By – Burak Serdar

Answer Checked By – Clifford M. (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.