tags API

tags

package

API reference for the tags package.

S
struct

Parser

Parser provides generic struct tag parsing with configurable syntax.

pkg/tags/parser.go:15-24
type Parser struct

Example

p := tags.NewParser("my-tag", tags.WithPairDelimiter(";"))
data := p.Parse("key:val; option:a,b")

Methods

Parse
Method

Parse extracts key-value pairs from a tag string.

Parameters

tag string

Returns

map[string][]string
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
Method

ParseStruct extracts tag metadata from all fields of a struct.

Parameters

v any

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
Method

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

prefix string
startIndex int

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
T
type

Option

Option configures a Parser.

pkg/tags/parser.go:27-27
type Option func(*Parser)
F
function

WithIncludeUntagged

WithIncludeUntagged configures the parser to include fields even if the tag is missing.

Returns

pkg/tags/parser.go:30-32
func WithIncludeUntagged() Option

{
	return func(p *Parser) { p.includeUntagged = true }
}
F
function

WithPairDelimiter

WithPairDelimiter sets the delimiter between key:value pairs (default “;”).

Parameters

d
string

Returns

pkg/tags/parser.go:35-37
func WithPairDelimiter(d string) Option

{
	return func(p *Parser) { p.pairDelimiter = d }
}
F
function

WithKVSeparator

WithKVSeparator sets the key-value separator (default “:”).

Parameters

s
string

Returns

pkg/tags/parser.go:40-42
func WithKVSeparator(s string) Option

{
	return func(p *Parser) { p.kvSeparator = s }
}
F
function

WithValueDelimiter

WithValueDelimiter sets the delimiter for multiple values (default “,”).

Parameters

d
string

Returns

pkg/tags/parser.go:45-47
func WithValueDelimiter(d string) Option

{
	return func(p *Parser) { p.valueDelim = d }
}
F
function

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

pkg/tags/parser.go:52-54
func WithNested() Option

{
	return func(p *Parser) { p.nested = true }
}
F
function

NewParser

NewParser creates a Parser for the given tag name.

Parameters

tagName
string
opts
...Option

Returns

pkg/tags/parser.go:57-69
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
}
S
struct

FieldMeta

FieldMeta holds parsed tag metadata for a struct field.

pkg/tags/parser.go:101-108
type FieldMeta struct

Methods

Get
Method

Get returns the first value for a key, or empty string if not found.

Parameters

key string

Returns

string
func (FieldMeta) Get(key string) string
{
	if vals, ok := m.Tags[key]; ok && len(vals) > 0 {
		return vals[0]
	}
	return ""
}
GetAll
Method

GetAll returns all values for a key.

Parameters

key string

Returns

[]string
func (FieldMeta) GetAll(key string) []string
{
	return m.Tags[key]
}
Has
Method

Has checks if a key exists in the tag.

Parameters

key string

Returns

bool
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
F
function

TestParser_Parse

Parameters

pkg/tags/parser_test.go:8-90
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])
					}
				}
			}
		})
	}
}
F
function

TestParser_ParseStruct

Parameters

pkg/tags/parser_test.go:92-122
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)
	}
}
F
function

TestParser_ParseType

Parameters

pkg/tags/parser_test.go:124-139
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")
	}
}
F
function

TestFieldMeta_Methods

Parameters

pkg/tags/parser_test.go:141-170
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")
	}
}