7

I have the following code which parses YAML files and needs to match values from one struct external and update the internal struct's type property.

For example, this is the yaml file(translated to bin for simplicity) and content which is parsed correctly

package main

import (
    "fmt"
    "gopkg.in/yaml.v2"
    "log"
)

//internal config model for parsing

type InternalModel struct {
    models []Model2 `yaml:"models"`
}

type Model2 struct {
    Name   string `yaml:"name"`
    Type   string `yaml:"type"`
    Target string `yaml:"target"`
}

var internal_config = []byte(`
 models:
   - name: myapp
     type: app1
     target: ./

   - name: myapp2
     type: app2
     target: ./
`)

type ExternalConfig struct {
    Landscape Zone `yaml:"Landscape"`
}

type Zone struct {
    Zone   string  `yaml:"zone"`
    Models []Model `yaml:"models"`
}

type Model struct {
    AppType     string `yaml:"app-type"`
    ServiceType string `yaml:"service-type"`
}


var external_config = []byte(`
Landscape:
  zone: zone1
  models:
    - app-type: app1
      service-type: GCP
    - app-type: app2
      service-type: AMAZON
  zone: zone2
  models:
    - app-type: app3
      service-type: AZURE
    - app-type: app4Í
      service-type: HEROKU
`)

func main() {

    // This is the internal config which needs updated

    internalConfiglYaml := InternalModel{}

    err := yaml.Unmarshal([]byte(internal_config), &internalConfiglYaml)
    if err != nil {
        log.Fatalf("error in model internalConfiglYaml: %v", err)
    }
    //fmt.Printf("%+v\n", internalConfiglYaml)

    //--------------------------Second config file-----------------------//
    //This is the external config yaml

    extConfigYaml := ExternalConfig{}

    err = yaml.Unmarshal([]byte(external_config), &extConfigYaml)
    if err != nil {
        log.Fatalf("error in model extConfigYaml: %v", err)
    }
    fmt.Printf("%+v\n", extConfigYaml)

    landscape := "zone1"

    modifiedConfig := ConvertTypes(internalConfiglYaml, extConfigYaml, landscape)

    fmt.Printf("%+v\n", modifiedConfig)

}

func ConvertTypes(int_cfg InternalModel, ext_config ExternalConfig, landscape string) (out_cfg InternalModel) {

    for _, module := range int_cfg.models {

        switch module.Type {
        case "app1":
            //here I hard-coded the value "GCP" but it should come from the yaml struct after parsing
            module.Type = "GCP" // should be something like ext_config.models.service-type when the key in the struct
        case "app2":
            //here I hard-coded the value "AMAZON" but it should come from the yaml struct after parsing
            module.Type = "AMAZON"

        }
    }

    return int_cfg
}

//At the end what I need to do is to get the internal yaml file to be changed to the following struct
//The changes are when the type=app-type I need to modify the type in the internal config, here its GCP and ruby

//internal_config_after_changes := []byte(`
//
//
//models:
// - name: myapp
//   type: GCP
//   target: ./
//
// - name: myapp2
//   type: AMAZON
//   target: ./
//
//
//`)

At the end what I need to do is to get the internal yaml file to be changed to the struct above internal_config_after_changes The changes are when the type=app-type I need to modify the type value in the internal_config, here from app1 to GCP and app2 to amazon

The problem is with the second loop which I should use to iterate on the external_config and the matching values, I'm not sure how to combine them both with efficient way...

Himanshu
  • 12,071
  • 7
  • 46
  • 61
  • I think that you "didn't explain myself well" because I have re-read your question and if I didn't answer it, then I have no idea what you want to know. – Zan Lynx Aug 12 '18 at 18:37
  • @ZanLynx please see my update, is it more clear now? –  Aug 12 '18 at 18:42
  • @JhonD so actually you want to update the values of intconfig according to the type you have defined in the switch right. – Himanshu Aug 13 '18 at 20:07
  • @Himanshu - yes exactly, it should be at the end like `internal_config_after_changes` and should get the value from `external_config` yaml file content when there is a match of the values like `app1` –  Aug 13 '18 at 20:19
  • @JhonD check the answer with change inside `ConvertType` function with slice indexing to fetch the underlying array. – Himanshu Aug 13 '18 at 20:27

4 Answers4

2

Golang FAQ described regarding pointers to maps and slice:

Map and slice values behave like pointers: they are descriptors that contain pointers to the underlying map or slice data. Copying a map or slice value doesn't copy the data it points to. Copying an interface value makes a copy of the thing stored in the interface value. If the interface value holds a struct, copying the interface value makes a copy of the struct. If the interface value holds a pointer, copying the interface value makes a copy of the pointer, but again not the data it points to.

On iterating through the slice of model inside ConvertType you are actually creating a copy of []Models slice whose value.Type is not changing the value of original struct due that reason.

for _, module := range int_cfg.models{}

Above code snippet is creating a copy of int_cfg.models{}.

Index the slice model to point to the exact underlying array of slice Model to change the value as:

package main

import (
    "fmt"
    "log"
    "strings"

    "gopkg.in/yaml.v2"
)

//internal config model for parsing

type InternalModel struct {
    Models []Model2 `yaml:"models"`
}

type Model2 struct {
    Name   string `yaml:"name"`
    Type   string `yaml:"type"`
    Target string `yaml:"target"`
}

var internal_config = []byte(`
  models:
    - name: myapp
      type: app1
      target: ./

    - name: myapp2
      type: app2
      target: ./
`)

type ExternalConfig struct {
    Landscape []Zone `yaml:"Landscape"`
}

type Zone struct {
    Zone   string  `yaml:"zone"`
    Models []Model `yaml:"models"`
}

type Model struct {
    AppType     string `yaml:"app-type"`
    ServiceType string `yaml:"service-type"`
}

var external_config = []byte(`
Landscape:
  - zone: zone1
    models:
     - app-type: app1
       service-type: GCP
     - app-type: app2
       service-type: AMAZON
  - zone: zone2
    models:
     - app-type: app3
       service-type: AZURE
     - app-type: app4Í
       service-type: HEROKU
`)

func main() {

    //This is the internal config which needs to be update

    internalConfiglYaml := InternalModel{}

    err := yaml.Unmarshal(internal_config, &internalConfiglYaml)
    if err != nil {
        log.Fatalf("error in model internalConfiglYaml: %v", err)
    }
    fmt.Printf("%+v\n", internalConfiglYaml)

    //--------------------------Second config file-----------------------//
    //This is the external config yaml

    extConfigYaml := ExternalConfig{}
    // var response interface{}

    err = yaml.Unmarshal(external_config, &extConfigYaml)
    if err != nil {
        log.Fatalf("error in model extConfigYaml: %v", err)
    }
    fmt.Printf("%+v\n", extConfigYaml)

    landscape := "zone1"

    modifiedConfig := ConvertTypes(&internalConfiglYaml, extConfigYaml, landscape)

    fmt.Printf("%+v\n", modifiedConfig)

}

// ConvertTypes for changing the intConfig struct types
func ConvertTypes(int_cfg *InternalModel, ext_config ExternalConfig, landscape string) (out_cfg *InternalModel) {

    for _, module := range ext_config.Landscape {
        if module.Zone == landscape {
            for i, value := range module.Models {
                switch strings.Compare(value.AppType, int_cfg.Models[i].Type) {
                case 0:
                    //here I hard-coded the value "GCP" but it should come from the yaml struct after parsing
                    int_cfg.Models[i].Type = value.ServiceType // should be something like ext_config.models.service-type when the key in the struct
                default:
                }
            }
        }
    }

    return int_cfg
}

If you check above code snippet, you will also recognize that I have changed the struct.

type InternalModel struct {
    models []Model2 `yaml:"models"`
}

to first letter uppercase to make it exportable as:

type InternalModel struct {
    Models []Model2 `yaml:"models"`
}

Because of the struct InternalModel is unexportable field model was unable to parse the provided internal_config yaml which leads to empty []slice data after unmarshal yaml.

One more thing that I have noticed is you are converting bytes into bytes again. There is no need for that.

err := yaml.Unmarshal([]byte(internal_config), &internalConfiglYaml)

So I have changed it to just:

err := yaml.Unmarshal(internal_config, &internalConfiglYaml)

Since internal_config is already declared as byte using []byte in global variable.

Himanshu
  • 12,071
  • 7
  • 46
  • 61
  • Sorry but I dont understand how you solve the issue ? the properties values which needs to be modified is still hard-coded like `GCP` and `amazon` which should come from the external config when there is a match –  Aug 13 '18 at 20:34
  • see my comments inside the switch, I've hard-coded the value but it should come from the `extrnal_config` yaml file when there is a match –  Aug 13 '18 at 20:36
  • @JhonD I have solved the issue in which you are unable to change the value of internalConfig struct. from app1 to GCP – Himanshu Aug 13 '18 at 20:36
  • @JhonD I am updating my code by looping over external config. Just wait for a while. – Himanshu Aug 13 '18 at 20:37
  • Sure this is the real issue here :) , take your time, I need to leave the office and connect again in few hours, Thanks. please it should be efficient as much as possible –  Aug 13 '18 at 20:39
  • @JhonD I will resolve it no issue, just tell me in external config model array `Models:[{AppType:app3 ServiceType:AZURE} {AppType:app4Í ServiceType:HEROKU}]` with which type you wants to check in switch to change the value. `AppType` or `ServiceType`. – Himanshu Aug 13 '18 at 20:40
  • @JhonD I have edited my answer to loop though `extrnal_config` yaml to get the `AppType` in switch to change the value of `internal_config`. Please check the code. – Himanshu Aug 13 '18 at 20:49
  • well, it shouldnt be `int_cfg.Models[i].Type = "GCP"` , the 'GCP` value should come from `extrnal_config ` –  Aug 13 '18 at 20:52
  • Sorry I really need to leave now for few hours, see that this `int_cfg.Models[i].Type = "GCP"` should changed to something like `int_cfg.Models[i].Type = ext_config.models.app-type` –  Aug 13 '18 at 20:54
  • i'll ping you in few hours :) I need to leave, see my last comment, it will be great if you can add some unit test that we can verify , i'll check the secnrio and i'll close the question with bounty . bye for now and thank you –  Aug 13 '18 at 20:55
  • HI thanks 1+ , I've checked it and first 1. we should update for `zone1` as this is the value for the landscape 2. is should be the `service-type` value but this not a problem I just change the name 3. it will be great if you can provide a `unit test` for the convert function that we can validate all the use cases according to the `in` and `out` ? –  Aug 14 '18 at 08:15
  • Done :) thank you for the quick response and support –  Aug 21 '18 at 06:16
2

If you're looking for how to apply the app-type to service-type mapping from the ExternalConfig, I suggest converting the array into a map and using it to look up the mapping:

func ConvertTypes(intCfg *InternalModel, extCfg ExternalConfig, landscape string) {
    // Make an app-type to service-type map.
    appToSvc := make(map[string]string, len(extCfg.Landscape.Models))
    for _, m := range extCfg.Landscape.Models {
        appToSvc[m.AppType] = m.ServiceType
    }

    // For each InternalModel model, check if there's an app-type to service-type
    // mapping. If so, use the service-type instead of the app-type.
    for i, model := range intCfg.Models {
        if t, ok := appToSvc[model.Name]; ok {
            intCfg.Models[i].Type = t
        }
    }
}

I've also fixed the pass-by-pointer and range loop assignment problems as suggested by @Himanshu and @Zan Lynx.

Matt D.
  • 896
  • 6
  • 5
  • HI, can you please elaborate on `fixed the pass-by-pointer and range loop assignment problems` , ? why it's bad using it like @Himanshu suggested –  Aug 20 '18 at 06:40
1

The first thing to consider is that you are working with two structures that are defined at run-time, thus you will not be able to use a single loop to match them. The next thing to consider is that your InternalConfig is the one least likely to change and should thus be the first structure to parse, where after the ExternalConfig is parsed and matched with it.

From reading your question, comments and the provided answers it also seems that there are a number of assumptions on the board which few have alluded to. I will attempt to address these if possible.

So let's give it a go:

package main

import (
    // import standard library packages first (alphabetically)
    "fmt"
    "log"

    // import third party packages next (also alphabetically)
    "gopkg.in/yaml.v2"

    // finally import your own packages here
)

// InternalConfig
//   - Let's call it InternalConfig so as to ensure consistent naming standards
type InternalConfig struct {
    Models []Imodel `yaml:"models"`
    // Exported to ensure access to external packages (such as yaml)
}

// Imodel - The internal model struct
type Imodel struct {
    Name   string `yaml:"name"`
    Type   string `yaml:"type"`
    Target string `yaml:"target"`
}

var iCfgb = []byte(`
models:
   - name: myapp
     type: app1
     target: ./

   - name: myapp2
     type: app2
     target: ./
`)

type ExternalConfig struct {
    Landscape Zone `yaml:"Landscape"`
}

type Zone struct {
    Zone   string  `yaml:"zone"`
    Models []Emodel `yaml:"models"`
}

type Emodel struct {
    AppType     string `yaml:"app-type"`
    ServiceType string `yaml:"service-type"`
}


var eCfgb = []byte(`
Landscape:
  zone: zone1
  models:
    - app-type: app1
      service-type: GCP
    - app-type: app2
      service-type: AMAZON
  zone: zone2
  models:
    - app-type: app3
      service-type: AZURE
    - app-type: app4Í
      service-type: HEROKU
`)

function main() {
    iCfgYaml := InternalConfig{}
    // don't need []byte(iCfgb) as iCfgb is already a byte slice
    // We are also using a shorthand if statement as we will only use err in the
    // if scope, this is because our config is passed by reference and not value
    if err := yaml.Unmarshal(iCfgb, &iCfgYaml); err != nil {
        log.Fatalf("error in model InternalConfig: %v", err)
    }

    eCfgYaml := ExternalConfig{}
    if err := yaml.Unmarshal(eCfgb, &eCfgYaml); err != nil {
        log.Fatalf("error in model ExternalConfig: %v", err)
    }

    // From here there are clearly some assumptions made, here is my take:
    //   1. I do not see any purpose in the landscape variable, the value "zone1"
    //      can be obtained from eCfgYaml.Landscape.Zone;
    //   2. From the question it seems you want to update the InternalConfig
    //      values with values from the ExternalConfig. However, from the code you
    //      created a modified copy. I will assume this was for debugging purposes
    //      and skip the creating of another copy;

    // I have renamed ConvertTypes to UpdateInternalState to accurately reflect
    // the behavior of the function. It now returns an error instead of a copy of
    // InternalConfig (any state altering functions should most likely return an
    // error). It receives a pointer of InternalConfig and a value copy of
    // ExternalConfig. This is because we will be altering the state of
    // InternalConfig
    if err := UpdateInternalState(&iCfgYaml, eCfgConfig); err != nil {
        log.Fatalf("Error updating internal config: %v", err)
    }

    // Assume the print is for debugging, thus I denote it with DBG. Makes it easy
    // to find and remove from production code.
    fmt.Printf("DBG: %+v\n", iCfgYaml)
}

func UpdateInternalState(iCfg *InternalConfig, eCfg ExternalConfig) error {
    // Firstly, we need to track the internal config models. We will create an
    // index lookup map (ilm) for it.
    ilm := make(map[string]int)

    // Now we iterate through our internal models and update the index:
    for i, v := range iCfg.Models {
        ilm[v] = i
    }

    // Now we iterate through the external config and update the internal config
    // state accordingly:
    for _, em := range eCfg.Landscape.Models {
        // We find the index of the specified app type in the index lookup
        // We use a shorthand if statement as we will only use i and ok within
        // the scope of the if (if you need it outside you can't use shorthand)
        // If there is no match nothing happens, but if found it will update to
        // the corresponding specified value in the external config.
        if i, ok := ilm[em.AppType]; ok {
            iCfg.Models[i].Type = em.ServiceType
        }
    }

    // At any point within this function you are able to do more robust error
    // checking, such as ensuring the index lookup map must have a corresponding
    // entry, and return any errors encountered.
    return nil
}
Zanicar
  • 100
  • 3
0

This question is definitely a duplicate but I can't find one to link right now.

Your problem is that the for loop is creating a copy of each struct. Writing into element which is just a copy of xtr[i] never changes the original xtr[i]. What you do to fix it is to use the slice index instead.

Change

for _, element := range xtr {
    switch element.vType {
    case "app1":
        //user1 value is coming from the &y parsed data of the yaml
        element.vType = "user1"
    case "app2":
         //user2 value is coming from the &y parsed data
        element.vType = "user2"

    }
}

Into

for i := range xtr {
    switch xtr[i].vType {
    case "app1":
        //user1 value is coming from the &y parsed data of the yaml
        xtr[i].vType = "user1"
    case "app2":
         //user2 value is coming from the &y parsed data
        xtr[i].vType = "user2"

    }
}
Zan Lynx
  • 53,022
  • 10
  • 79
  • 131
  • Thanks! I've two structs, how do I match the values from the yaml `parsed `(byte) to the `xtr` struct ? the `user1` & `user2` should come from the `&y` parsed data and should be filled just when the value `app1` is match in `both` structs –  Aug 12 '18 at 18:40