Go struct tags for marshaling and unmarshaling map[string]string
MIT License
A Go package for marshaling and unmarshaling map[string]string
with struct tags.
Requires go version 1.13+ for errors.As
and errors.Is
go get github.com/chanced/labeler
package main
import (
"fmt"
"github.com/chanced/labeler"
)
// The simplest implementation uses a container for labels,
// denoted with a tag marked as `label:"*"`
type Struct struct {
Labels map[string]string `label:"*"`
Field string `label:"field"`
}
func main() {
labels := map[string]string{"field": "val"}
v := Struct{}
labeler.Unmarshal(labels, &v)
result, err := labeler.Marshal(&v)
_ = err
fmt.Println("v.Field: ", v.Field)
for key, value := range labels {
fmt.Printf("labels[%s]: %s result[%s]: %s\n", key, labels[key], key, result[key])
}
}
interface{}
defined
I am developing a project on Google Cloud Platform. Resources on GCP can have
labels, which I use in a number of ways to classify and organize with. The gRPC
client responses come with a GetLabels()map[string]string
method. This project
was built as a means to interpret map[string]string
into a well-defined struct
.
The package has worked really well for config values coming in from Environ & elsewhere.
interface{}
definedBoth Marshal and Unmarshal accept v interface{}
, the value to marshal from or unmarshal
into. v
must be pointer to a struct
or a type
that implements
labeler.MarshalWithOpts
, labeler.Marshal
, labeler.UnmarshalWithOpts
, or
labeler.Unmarshal
respectively.
labeler is fairly flexible when it comes to what all you can tag. It supports the following types:
Interface / Type | Signature | Usage |
---|---|---|
labeler.MarshalerWithOpts |
MarshalLabels(o labeler.Options) map[string]string |
Marshal |
labeler.Marshaler |
MarshalLabels() map[string]string |
Marshal |
fmt.Stringer |
String() string |
Marshal |
encoding.TextMarshaler |
MarshalText() (text []byte, err error) |
Marshal |
labeler.UnmarshalerWithOpts |
UnmarshalLabels(v map[string]string, opts Options) error |
Unmarshal |
labeler.Unmarshaler |
UnmarshalLabels(l map[string]string) error |
Unmarshal |
labeler.Stringee |
FromString(s string) error |
Unmarshal |
encoding.TextUnmarshaler |
UnmarshalText(text []byte) error |
Unmarshal |
struct |
can either implement any of the above interfaces or have fields with tags. Supports n level of nesting |
Both |
basic types |
string , bool , int , int64 , int32 , int16 , int8 , float64 , float32 , uint , uint64 , uint32 , uint16 , uint8 , complex128 , complex64 , |
Both |
time |
time.Time , time.Duration
|
Both |
pointer | pointer to any of the above | Both |
slices & arrays | slices / arrays composed of any type above | Both |
When unmarshaling, labeler needs a way to persist labels, regardless of whether or not they
have been assigned to tagged fields. By default, labeler will retain all labels unless
Options.KeepLabels
is set to false
(See Options). This is to ensure
data integrity for labels that have not been unmarshaled into fields.
Your available choices for v
are:
Interface / Type | Signature |
---|---|
labeler.UnmarshalerWithOpts |
UnmarshalLabels(v map[string]string, opts Options) error |
labeler.Unmarshaler |
UnmarshalLabels(l map[string]string) error |
labeler.Labelee |
SetLabels(labels map[string]string) |
labeler.StrictLabelee |
SetLabels(labels map[string]string) error |
labeler.GenericLabelee |
SetLabels(labels map[string]string, tag string) error |
struct with a container field |
A struct with a field marked as being the container using Options.ContainerToken on any level of v or a field with the name matching Options.ContainerField that is any type above or an accessible map[string]string . (See Options). |
While marshaling, labeler will prioritize field values over those stored in your label
container. This means that values in the map[string]string
will be overridden
if there is a key with a matching tag.
labeler ignores the case of keys by default, but this is configurable. (See Options)
For Unmarshal
, you also need to pass input interface{}
which provides a means of
accessing the labels map[string]string
.
input
must satisfy one of the following:
Interface / Type | Signature | Example |
---|---|---|
labeler.Labeled |
GetLabels() map[string]string |
example |
labeler.GenericallyLabeled |
GetLabels(tag string) map[string]string |
example |
map[string]string |
Any type derived from map[string]string
|
example |
If you need to change any Option, consider creating an instance of labeler as there will be a bit
of extra pre-processing for each call to Marshal
and Unmarshal
otherwise.
environ := labeler.NewLabeler(OptTag("env"))
// then use environ.Unmarshal(in, v) and environ.Marshal(v)
_ = environ
package main
import (
"fmt"
"github.com/chanced/labeler"
)
type ExampleInput struct {
Labels map[string]string
}
func (e ExampleInput) GetLabels() map[string]string {
return e.Labels
}
type NestedExample struct {
Field string `label:"nested_field"`
}
type Example struct {
Name string `label:"name"`
Duration time.Duration `label:"duration"`
Time time.Time `label:"time, format: 01/02/2006 03:04PM"`
Dedupe string `label:"dedupe, discard"`
CaSe string `label:"CaSe, casesensitive"`
FloatWithFormat float64 `label:"FloatWithFormat, format:b"`
Float64 float64 `label:"float64"`
Float32 float32 `label:"float32"`
Int int `label:"int"`
Int64 int64 `label:"int64"`
Int32 int32 `label:"int32"`
Int16 int16 `label:"int16"`
Int8 int8 `label:"int8"`
Bool bool `label:"bool"`
Uint uint `label:"uint"`
Uint64 uint64 `label:"uint64"`
Uint32 uint32 `label:"uint32"`
Uint16 uint16 `label:"uint16"`
Uint8 uint8 `label:"uint8"`
StrSlice []string `label:"str_slice"`
IntArray [8]int `label:"int_array"`
Nested NestedExample
Labels map[string]string // SetLabels is used instead of the container
}
func (e *Example) SetLabels(l map[string]string) {
e.Labels = l
}
func (e *Example) GetLabels() map[string]string {
return e.Labels
}
func main() {
labels := map[string]string{
"name": "Bart",
"imp": "important field",
"enum": "ValueB",
"int": "123456789",
"int64": "1234567890",
"int32": "12345",
"int16": "123",
"int8": "1",
"bool": "true",
"duration": "1s",
"float64": "1.1234567890",
"float32": "1.123",
"time": "09/26/2020 10:10PM",
"uint": "1234",
"uint64": "1234567890",
"uint32": "1234567",
"uint16": "123",
"uint8": "1",
"FloatWithFormat": "123.234823484",
"dedupe": "Will be removed from the Labels after field value is set",
"case": "value should not be set due to not matching case",
"nested_field": "nested value",
"int_array": "0,1,2,3,4,5,6,7"
"str_slice": "red,blue,green"
}
input := ExampleInput {
Labels: labels,
}
v := &Example{}
err := labeler.Unmarshal(input, v)
if err != nil {
var pErr *labeler.ParsingError
select {
case errors.As(err, &pErr):
for _, fieldErr := range pErr.Errors {
// fieldErr has the field's Name (string) and Tag (labeler.Tag)
// as well as Err, the underlying Error which unwraps
}
case errors.Is(err, ErrInvalidInput):
// bad input
}
}
l, err := labeler.Unmarshal(v)
if err != nil {
//handle err
}
fmt.Println(l) // map[string]string
}
If you don't want to use accessors and mutators on your input and value, you can opt instead for a container tag that is of map[string]string
or implements the appropriate interface
to retrieve and set labels. You can also pass in a map[string]string
directly as your input for Unmarshal.
The container token is configurable with the ContainerToken
option. See Options for more info.
package main
import (
"fmt"
"github.com/chanced/labeler"
)
type Example2 struct {
Name string `label:"name"`
Defaulted string `label:"defaulted, default: my default value"` // spaces are trimmed
Labels map[string]string `label:"*"`
}
func main() {
l := map[string]string { name: "Homer" }
v := &Example2{}
err := labeler.Unmarshal(l, v)
if err != nil {
// handle err
_ = err
}
fmt.Println("Name is: ", v.Name)
fmt.Println(len(v.Labels), " Labels")
labels, err := labeler.Marshal(v, l)
if err != nil {
_ = err
}
}
The only important bit is that Color
implements String() string
and
FromString(s string) error
. Color
could have also implemented
UnmarshalText(text []byte) error
and MarshalText() (text []byte, err error)
.
package main
type Color int
const (
ColorUnknown Color = iota
ColorRed
ColorBlue
)
type Example3 struct {
Color Color `label:"color"`
Labels map[string]string `label:"*"`
}
var colorMapToStr map[Color]string = map[Color]string{
ColorUnknown: "Unknown",
ColorBlue: "Blue",
ColorRed: "Red",
}
func getColorMapFromStr() map[string]Color {
m := make(map[string]Color)
for k, v := range colorMapToStr {
m[v] = k
}
return m
}
var colormMapFromStr map[string]Color = getColorMapFromStr()
func (c Color) String() string {
return colorMapToStr[my]
}
func (c *Color) FromString(s string) error {
if v, ok := colorMapFromStr[s]; ok {
*my = v
return nil
}
return errors.New("Invalid value")
}
type Example3 struct {
}
Say you have multiple sources of labels and you want to unmarshal them into the same struct
. This is achievable by setting the option Tag
! See Options for more info.
import (
"github.com/chanced/labeler"
)
type Example4 struct {
Name string `property:"name"`
Color string `attribute:"color"`
Characteristics map[string]string
Attributes map[string]string
}
func (e *Example4) GetLabels(t string){
switch t {
case "property":
return e.Characteristics
case "attribute":
return e.Attributes
}
}
func (e *Example4) SetLabels(l map[string]string, t string) error{
switch t {
case "property":
e.Characteristics = l
case "attribute":
e.Attributes = l
}
}
func main() {
properties := map[string]string { name: "Homer" }
attributes := map[string]string { color: "Yellow" }
v := &Example4{}
err := labeler.Unmarshal(v, properties, OptTag("property"))
if err != nil {
_ = err
}
err := labeler.Unmarshal(v, attributes, OptTag("attribute"))
if err != nil {
_ = err
}
}
Option | Default | Details | Option func
|
---|---|---|---|
Tag |
"label" |
Tag is the name of the tag to lookup. This is especially handy if you have multiple sources of labels |
OptTag(t string) |
Separator |
"," |
Seperates the tag attributes. Configurable incase you have a tag that contains commas. | OptSeparator(v string) |
Split |
"," |
String used to split and join arrays and slices | OptSplit(v string) |
ContainerField |
"" |
ContainerField determines the field to set and retrieve the labels in the form of map[string]string . If ContainerField is set, labeler will assume that GetLabels and SetLabels should not be utilized. To set the ContainerField of a nested field, use dot notation (Root.Labels ). ContainerField is not required if input implements the appropriate interface to retrieve and set labels. |
OptContainerField(s string) |
ContainerToken |
"*" |
Used in place of the ContainerField option, indicating the container field via tag instead. It must derive from map[string]string . This option is only required if you do not wish to implement mutator/accessor interfaces. This can also be used to set some options such as TimeFormat , FloatFormat , ComplexFormat , CaseSensitive , IntBase , UintBase using the appropriate tokens. |
OptContainerToken(v string) |
AssignmentStr |
":" |
Used to assign values. This is in the event that a default value needs to contain ":"
|
OptAssignmentStr(v string) |
KeepLabels |
true |
Indicates whether or not labels that have been assigned to values are kept in the labels map[string]string when unmarshaling. |
OptKeepLabels() OptDiscardLabels()
|
IgnoreCase |
true |
If true , label keys are matched regardless of case. Setting this to false makes all keys case sensitive. This can be overridden at the field level. |
OptCaseSensitive() |
OmitEmpty |
true |
Determines whether or not to set zero-value when marshaling and unmarshaling. |
OptOmitEmpty() OptIncludeEmpty()
|
TimeFormat |
"" |
Default format / layout to use when formatting time.Time . Field level formats can be provided with either format (configurable) or timeformat (configurable) |
OptTimeFormat(v string) |
IntBase |
10 |
default base while parsing int , int64 , int32 , int16 , int8
|
OptIntBase(b int) |
UintBase |
10 |
default base while parsing uint , uint64 , uint32 , uint16 , uint8
|
OptUintBase(b int) |
ComplexFormat |
'f' |
Default format to use when formatting complex values. |
OptComplexFormat(f byte) |
FloatFormat |
'f' |
Default format to use when formatting float values. |
OptFloatFormat(f byte) |
Option | Default | Details | Option func
|
---|---|---|---|
KeepToken |
"keep" |
Token used to set KeepLabels to true
|
OptKeepToken(v string) |
DiscardToken |
"discard" |
Token used to set KeepLabels to false
|
OptDiscardToken(v string) |
DefaultToken |
"default" |
Token to provide a default value if one is not set. | OptDefaultToken(v string) |
SplitToken |
"split" |
Token used to set Split to v
|
OptSplitToken(v string) |
CaseSensitiveToken |
"casesensitive" |
Token used to set IgnoreCase to false
|
OptCaseSensitiveToken(v string) |
IgnoreCaseToken |
"ignorecase" |
Token used to determine whether or not to ignore case of the field's (or all fields if on container) key | OptIgnoreCaseToken(v string) |
OmitEmptyToken |
"omitempty" |
Token used to determine whether or not to assign empty / zero-value labels | OptOmitEmptyToken(v string) |
IncludeEmptyToken |
"includeempty" |
Token used to determine whether or not to assign empty / zero-value labels | OptIncludeEmptyToken(v string) |
FormatToken |
"format" |
Token to set the field-level formatting for time and float . |
OptFormatToken(v string) |
TimeFormatToken |
"timeformat" |
Token used to set TimeFormat . FormatToken can be used on non-container fields instead. |
OptTimeFormatToken(v string) |
FloatFormatToken |
"floatformat" |
Token used to set FloatFormat . FormatToken can be used on non-container fields instead. |
OptFloatFormatToken(v string) |
ComplexFormatToken |
"complexformat" |
Token used to set ComplexFormat . FormatToken can be used on non-container fields instead. |
OptComplexFormatToken(v string) |
BaseToken |
"base" |
sets the token for parsing base of int , int64 , int32 , int16 , int8 , uint , uint64 , uint32 , uint16 , and uint8 at the field level |
OptBaseToken(v string) |
IntBaseToken |
"intbase" |
sets the token for parsing base of int , int64 , int32 , int16 , int8 , at the container or field level |
OptIntBaseToken(v string) |
UintBaseToken |
"uintbase" |
sets the token for parsing base of uint , uint64 , uint32 , uint16 , uint8 , at the container or field level |
OptUintBaseToken(v string) |
If you run into any issues or have any questions, please do submit a ticket.
Do with it as you wish.