Skip to content

Instantly share code, notes, and snippets.

@keilin-anz
Last active November 15, 2021 08:25
Show Gist options
  • Save keilin-anz/50c91eb7bc1abcb65db4fc6217cc6c8b to your computer and use it in GitHub Desktop.
Save keilin-anz/50c91eb7bc1abcb65db4fc6217cc6c8b to your computer and use it in GitHub Desktop.
Pair helper for JSON and YAML

Pair

A helper for that - admittedly very rare - YAML/JSON scenario where you may want a single key->value pair represented as a map, but don't want to have map[string]interface{} strewn throughout your code

see example.go

Why?

My use case was I wanted to represent environment variables in a config file, BUT there were caveats to the easy methods:

  • if I used Environment map[string]interface{} their order would be randomized, precluding logical self-referencing env vars
    environment:
      foo: bar
      baz: bingle
  • if I used Environment []map[string]interface{} you get an edge case where one can have too many entries in an env var
    environment:
    - foo: bar
    - baz: bingle
      chicken: soup   # this could easily be entered by accident, but is an error and would require a lot of excess checking
    • also notable that the use of map[string]interface{} would be highly ambiguous, even with aliasing
  • a common solution seen in YAML formats is to just curtail the above conundra and parse env vars separately:
    environment:
    - foo=bar
    - baz=bingle
    But I've always kind of hated that, so - whisky in hand - sought alternatives

By going admittedly overboard and building this type, we get:

  • type safety in application code
  • useful marshal/unmarshal checking in a single place
  • vaguely self-descriptive application code

*[conundra]: ie. conundrums - whilst there's a fairly scathing remark about people who say "conundra" here I still use it https://english.stackexchange.com/a/556467

package pair
import (
"encoding/json"
"fmt"
"time"
"gopkg.in/yaml.v2"
)
type Pair struct {
Key string
Value string
}
func (p Pair) interimMap() map[string]interface{} {
return map[string]interface{}{
p.Key: p.Value,
}
}
func (p *Pair) fromInterimMap(interimMap map[string]interface{}) error {
if len(interimMap) < 1 {
return fmt.Errorf("pair is empty")
}
if len(interimMap) > 1 {
return fmt.Errorf("pair has more than one pair (%d items)", len(interimMap))
}
// NOTE: we assume the first key/value pair we can get is valid
// the alternative is to use reflection
for k, v := range interimMap {
fmt.Printf("%s=>%v\n", k, v)
p.Key = k
p.Value = v.(string)
//break
}
return nil
}
func (p Pair) MarshalYAML() (interface{}, error) {
return p.interimMap(), nil
}
func (p *Pair) UnmarshalYAML(unmarshal func(interface{}) error) error {
interimMap := map[string]interface{}{}
if err := unmarshal(&interimMap); err != nil {
return err
}
fmt.Println(interimMap)
return p.fromInterimMap(interimMap)
}
func (p Pair) MarshalJSON() ([]byte, error) {
interimMap := p.interimMap()
return json.Marshal(interimMap)
}
func (p *Pair) UnmarshalJSON(buf []byte) error {
interim := make(map[string]interface{})
if err := json.Unmarshal(buf, &interim); err != nil {
return err
}
return p.fromInterimMap(interim)
}
package main
import (
"fmt"
"time"
"encoding/json"
"gopkg.in/yaml.v2"
"./pair" // invalid reference (just demonstrating)
)
type ExampleConfig struct {
Environment []Pair `json:"environment" yaml:"environment"`
}
//
// Basic env var config example
//
func configExample() {
configInput := `
environment:
- FOO: bar
- BAZ: bingle
`
var exampleConfig ExampleConfig
err = yaml.Unmarshal([]byte(configInput), &exampleConfig)
if err != nil {
fmt.Printf("yaml unmarshal error:", err)
}
fmt.Println(exampleConfig)
}
//
// Example JSON unmarshal(marshal(x))
//
func jsonExample() Pair {
var p = Pair{
Key: "foo",
Value: "bar",
}
jsonOutput, err := json.Marshal(p)
if err != nil {
fmt.Printf("json marshal error:", err)
}
fmt.Println(string(jsonOutput))
var p2 Pair
err = json.Unmarshal([]byte(jsonOutput), &p2)
if err != nil {
fmt.Printf("json unmarshal error:", err)
}
fmt.Println(p2)
return p
}
func main() {
p := jsonExample()
//
// Example JSON unmarshal(marshal(x))
//
yamlOutput, err := yaml.Marshal(p)
if err != nil {
fmt.Printf("yaml marshal error:", err)
}
fmt.Println(string(yamlOutput))
err = yaml.Unmarshal([]byte(yamlOutput), &p2)
if err != nil {
fmt.Printf("yaml unmarshal error:", err)
}
fmt.Println(p2)
configExample()
//
// The following intentionally errors
//
fmt.Println("the next bit will error")
configInput = `
environment:
- FOO: bar
BAZ: bingle # too many values for a pair
- Fozzle: wizzle
`
err = yaml.Unmarshal([]byte(configInput), &exampleConfig)
if err != nil {
fmt.Printf("yaml unmarshal error:", err)
}
fmt.Println(exampleConfig)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment