Ace
Ace

Reputation: 493

Golang Format Text Template based on variable

I am trying to use text/template to generate ini-like configurations on the fly, where the original data is provided in yaml format.

I want the resulting output to be different depending on where the request is coming from.

Please consider the following broken code:

package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
    "os"
    "text/template"
)

var yamlData = `
# comment
---
States:
- StateName: California
  DateEstablished: September 9, 1850
  Cities:
  - CityName: Los Angeles
    Population: 4 Million
  - CityName: Santa Barbara
    Population: 92 Thousand
  - CityName: San Jose
    Population: 1 Million
- StateName: Washington
  DateEstablished: November 11, 1889
  Cities:
  - CityName: Seattle
    Population: 724 Thousand
    Climate: wet
  - CityName: Spokane
    Population: 217 Thousand
    Climate: dry
  - CityName: Scappoose
    Population: 7
`

const reportTemplate string = `{{ range . }}
# {{ if .CityName == requestingCityName }}
# {{ .CityName }}
[RequestingCity]
Population = {{ .Population }}
{{ if .Climate }}Climate = {{ .Climate }}{{ end }}
{{ end }}
# {{ if .CityName != requestingCityName }}
# {{ .CityName }}
[City]
Population = {{ .Population }}
{{ if .Climate }}Climate = {{ .Climate }}{{ end }}
{{ end }}
{{ end }}
`

type dataStruct struct {
    States []struct {
        StateName       string `yaml:"StateName"`
        DateEstablished string `yaml:"DateEstablished"`
        Cities          []struct {
            CityName   string `yaml:"CityName"`
            Population string `yaml:"Population"`
            Climate    string `yaml:"Climate,omitempty"`
        } `yaml:"Cities"`
    } `yaml:"States"`
}

func (d *dataStruct) readData(data []byte) *dataStruct {
    yaml.Unmarshal(data, d)
    return d
}

func main() {

    var report dataStruct
    report.readData([]byte(yamlData))
    t := template.Must(template.New("new").Parse(reportTemplate))

    requestingStateName := "Washington"
    requestingCityName := "Seattle"

    for i := range report.States {
        if report.States[i].StateName == requestingStateName {
            x := report.States[i].Cities
            fmt.Println(t.Execute(os.Stdout, x))

        }
    }
}

Most of this code "works" the way i would expect it to, but the part i am having problems with is how to make the template.

I want to be able to change the value for requestingCityName so that the output will change like so:

if requestingCityName == "Scappoose" then the output would be like so:


# Scappoose
[RequestingCity]
Population = 7

# Spokane
[City]
Population = 217 Thousand
Climate = dry

# Seattle
[City]
Population = 724 Thousand
Climate = wet

or if requestingCityName == "Seattle" then the output would be like so:


# Seattle
[RequestingCity]
Population = 724 Thousand
Climate = wet

# Spokane
[City]
Population = 217 Thousand
Climate = dry

# Scappoose
[City]
Population = 7

How would make the template so that I can achieve the desired behaviour?

Upvotes: 3

Views: 1313

Answers (2)

Fabian
Fabian

Reputation: 5348

You can do the if-condition inside the template with eq, see Compare strings in templates. To negate an if you can write if not <a> eq <b>.

I assumed you don't care about order. If you care you can sort it beforehand so at the top is the requested city and everything else below. Else you have to split the request-city-printing up and output outside of the range the requested, then filter out the requested once you range over all cities.

The most clean is probably to go with a separate attribute inside the data struct where the requesting city is held and filter that one out of cities beforehand.

Oh and if you haven't added the requested city to the data struct, then do that beforehand. It's probably easiest to separate by embedding the yaml data struct by embedding it inside a struct named DataRendering which also has the requested city attribute.

Upvotes: 1

James Henstridge
James Henstridge

Reputation: 43919

One simple solution would be to pass a different data object to Template.Execute. Something like:

type templateData struct {
    requestingCityName string
    cities             []citiesStruct // or whatever you name the struct
}
...
fmt.Println(t.Execute(os.Stdout, templateData{requestingCityName, x}))

This solution would require you to update your template to work with the new context struct (i.e. that the old cities array is now .cities rather than .), but it gives you access to .requestingCityName.

Upvotes: 4

Related Questions