tags
packageAPI reference for the tags
package.
Imports
(4)Parser
Parser provides generic struct tag parsing with configurable syntax.
type Parser struct
Example
p := tags.NewParser("my-tag", tags.WithPairDelimiter(";"))
data := p.Parse("key:val; option:a,b")
Methods
Parse extracts key-value pairs from a tag string.
Parameters
Returns
func (*Parser) Parse(tag string) map[string][]string
{
result := make(map[string][]string)
for part := range strings.SplitSeq(tag, p.pairDelimiter) {
part = strings.TrimSpace(part)
if part == "" {
continue
}
key, value, found := strings.Cut(part, p.kvSeparator)
key = strings.TrimSpace(key)
if !found {
result[key] = nil
continue
}
value = strings.TrimSpace(value)
var values []string
for v := range strings.SplitSeq(value, p.valueDelim) {
values = append(values, strings.TrimSpace(v))
}
result[key] = values
}
return result
}
ParseStruct extracts tag metadata from all fields of a struct.
Parameters
Returns
func (*Parser) ParseStruct(v any) []FieldMeta
{
val := reflect.ValueOf(v)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil
}
return p.ParseType(val.Type())
}
ParseType extracts tag metadata from a reflect.Type.
Parameters
Returns
func (*Parser) ParseType(typ reflect.Type) []FieldMeta
{
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
if typ.Kind() != reflect.Struct {
return nil
}
p.mu.RLock()
if cached, ok := p.cache[typ]; ok {
p.mu.RUnlock()
return cached
}
p.mu.RUnlock()
p.mu.Lock()
defer p.mu.Unlock()
// Double check logic
if cached, ok := p.cache[typ]; ok {
return cached
}
fields := p.parseTypeRecursive(typ, "", 0)
p.cache[typ] = fields
return fields
}
Parameters
Returns
func (*Parser) parseTypeRecursive(typ reflect.Type, prefix string, startIndex int) []FieldMeta
{
var fields []FieldMeta
index := startIndex
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get(p.tagName)
hasTag := tag != ""
shouldRecurse := p.nested && (field.Type.Kind() == reflect.Struct || (field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct))
if !hasTag && !p.includeUntagged && !shouldRecurse {
continue
}
fieldName := field.Name
if prefix != "" {
fieldName = prefix + "." + field.Name
}
var parsedTags map[string][]string
if tag != "" {
parsedTags = p.Parse(tag)
}
if hasTag || p.includeUntagged {
fields = append(fields, FieldMeta{
Name: fieldName,
Index: index,
Type: field.Type,
Tags: parsedTags,
RawTag: tag,
IsExported: field.IsExported(),
})
}
index++
if shouldRecurse {
var nestedType reflect.Type
if field.Type.Kind() == reflect.Struct {
nestedType = field.Type
} else if field.Type.Kind() == reflect.Ptr {
nestedType = field.Type.Elem()
}
nested := p.parseTypeRecursive(nestedType, fieldName, index)
fields = append(fields, nested...)
index += len(nested)
}
}
return fields
}
Fields
| Name | Type | Description |
|---|---|---|
| tagName | string | |
| pairDelimiter | string | |
| kvSeparator | string | |
| valueDelim | string | |
| includeUntagged | bool | |
| nested | bool | |
| cache | map[reflect.Type][]FieldMeta | |
| mu | sync.RWMutex |
Option
Option configures a Parser.
type Option func(*Parser)
WithIncludeUntagged
WithIncludeUntagged configures the parser to include fields even if the tag is missing.
Returns
func WithIncludeUntagged() Option
{
return func(p *Parser) { p.includeUntagged = true }
}
Uses
WithPairDelimiter
WithPairDelimiter sets the delimiter between key:value pairs (default “;”).
Parameters
Returns
func WithPairDelimiter(d string) Option
{
return func(p *Parser) { p.pairDelimiter = d }
}
Uses
WithKVSeparator
WithKVSeparator sets the key-value separator (default “:”).
Parameters
Returns
func WithKVSeparator(s string) Option
{
return func(p *Parser) { p.kvSeparator = s }
}
Uses
WithValueDelimiter
WithValueDelimiter sets the delimiter for multiple values (default “,”).
Parameters
Returns
func WithValueDelimiter(d string) Option
{
return func(p *Parser) { p.valueDelim = d }
}
Uses
WithNested
WithNested enables recursive parsing of nested and embedded struct fields.
When enabled, fields of struct type are recursed into with their tag prefix
set to the parent field name followed by a dot.
Returns
func WithNested() Option
{
return func(p *Parser) { p.nested = true }
}
Uses
NewParser
NewParser creates a Parser for the given tag name.
Parameters
Returns
func NewParser(tagName string, opts ...Option) *Parser
{
p := &Parser{
tagName: tagName,
pairDelimiter: ";",
kvSeparator: ":",
valueDelim: ",",
cache: make(map[reflect.Type][]FieldMeta),
}
for _, opt := range opts {
opt(p)
}
return p
}
FieldMeta
FieldMeta holds parsed tag metadata for a struct field.
type FieldMeta struct
Methods
Get returns the first value for a key, or empty string if not found.
Parameters
Returns
func (FieldMeta) Get(key string) string
{
if vals, ok := m.Tags[key]; ok && len(vals) > 0 {
return vals[0]
}
return ""
}
GetAll returns all values for a key.
Parameters
Returns
func (FieldMeta) GetAll(key string) []string
{
return m.Tags[key]
}
Has checks if a key exists in the tag.
Parameters
Returns
func (FieldMeta) Has(key string) bool
{
_, ok := m.Tags[key]
return ok
}
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | |
| Index | int | |
| Type | reflect.Type | |
| Tags | map[string][]string | |
| RawTag | string | |
| IsExported | bool |
TestParser_Parse
Parameters
func TestParser_Parse(t *testing.T)
{
tests := []struct {
name string
tagName string
opts []Option
input string
expected map[string][]string
}{
{
name: "guard style",
tagName: "guard",
input: "role:owner; read:admin,user; delete:admin",
expected: map[string][]string{
"role": {"owner"},
"read": {"admin", "user"},
"delete": {"admin"},
},
},
{
name: "fsm style",
tagName: "fsm",
input: "initial:draft; draft->paid; paid->shipped",
expected: map[string][]string{
"initial": {"draft"},
"draft->paid": nil,
"paid->shipped": nil,
},
},
{
name: "conf style",
tagName: "conf",
opts: []Option{WithPairDelimiter(",")},
input: "env:PORT,flag:port,default:8080",
expected: map[string][]string{
"env": {"PORT"},
"flag": {"port"},
"default": {"8080"},
},
},
{
name: "empty tag",
tagName: "test",
input: "",
expected: map[string][]string{},
},
{
name: "key without value",
tagName: "test",
input: "required; optional",
expected: map[string][]string{
"required": nil,
"optional": nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewParser(tt.tagName, tt.opts...)
result := p.Parse(tt.input)
if len(result) != len(tt.expected) {
t.Errorf("got %d keys, want %d", len(result), len(tt.expected))
}
for k, want := range tt.expected {
got, ok := result[k]
if !ok {
t.Errorf("missing key %q", k)
continue
}
if len(got) != len(want) {
t.Errorf("key %q: got %v, want %v", k, got, want)
}
for i := range want {
if i >= len(got) || got[i] != want[i] {
t.Errorf("key %q value %d: got %q, want %q", k, i, got[i], want[i])
}
}
}
})
}
}
TestParser_ParseStruct
Parameters
func TestParser_ParseStruct(t *testing.T)
{
type TestStruct struct {
Untagged string
Role string `guard:"role:owner"`
Perms string `guard:"read:admin,user; write:owner"`
}
p := NewParser("guard")
fields := p.ParseStruct(&TestStruct{})
if len(fields) != 2 {
t.Fatalf("got %d fields, want 2", len(fields))
}
if fields[0].Name != "Role" {
t.Errorf("first field name: got %q, want %q", fields[0].Name, "Role")
}
if fields[0].Get("role") != "owner" {
t.Errorf("Role field 'role' tag: got %q, want %q", fields[0].Get("role"), "owner")
}
if !fields[1].Has("read") {
t.Error("Perms field should have 'read' key")
}
perms := fields[1].GetAll("read")
if len(perms) != 2 || perms[0] != "admin" || perms[1] != "user" {
t.Errorf("Perms 'read' values: got %v, want [admin user]", perms)
}
}
TestParser_ParseType
Parameters
func TestParser_ParseType(t *testing.T)
{
type TestStruct struct {
Status string `fsm:"initial:draft; draft->paid"`
}
p := NewParser("fsm")
fields := p.ParseType(reflect.TypeOf(TestStruct{}))
if len(fields) != 1 {
t.Fatalf("got %d fields, want 1", len(fields))
}
if fields[0].Get("initial") != "draft" {
t.Errorf("initial: got %q, want %q", fields[0].Get("initial"), "draft")
}
}
TestFieldMeta_Methods
Parameters
func TestFieldMeta_Methods(t *testing.T)
{
meta := FieldMeta{
Tags: map[string][]string{
"single": {"value"},
"multiple": {"a", "b", "c"},
"empty": nil,
},
}
if meta.Get("single") != "value" {
t.Errorf("Get single: got %q, want %q", meta.Get("single"), "value")
}
if meta.Get("missing") != "" {
t.Errorf("Get missing: got %q, want empty", meta.Get("missing"))
}
all := meta.GetAll("multiple")
if len(all) != 3 {
t.Errorf("GetAll multiple: got %d values, want 3", len(all))
}
if !meta.Has("empty") {
t.Error("Has empty: should be true")
}
if meta.Has("missing") {
t.Error("Has missing: should be false")
}
}