mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-08-22 20:47:54 +02:00
315 lines
6.5 KiB
Go
Vendored
315 lines
6.5 KiB
Go
Vendored
package structtag
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
errTagSyntax = errors.New("bad syntax for struct tag pair")
|
|
errTagKeySyntax = errors.New("bad syntax for struct tag key")
|
|
errTagValueSyntax = errors.New("bad syntax for struct tag value")
|
|
|
|
errKeyNotSet = errors.New("tag key does not exist")
|
|
errTagNotExist = errors.New("tag does not exist")
|
|
errTagKeyMismatch = errors.New("mismatch between key and tag.key")
|
|
)
|
|
|
|
// Tags represent a set of tags from a single struct field
|
|
type Tags struct {
|
|
tags []*Tag
|
|
}
|
|
|
|
// Tag defines a single struct's string literal tag
|
|
type Tag struct {
|
|
// Key is the tag key, such as json, xml, etc..
|
|
// i.e: `json:"foo,omitempty". Here key is: "json"
|
|
Key string
|
|
|
|
// Name is a part of the value
|
|
// i.e: `json:"foo,omitempty". Here name is: "foo"
|
|
Name string
|
|
|
|
// Options is a part of the value. It contains a slice of tag options i.e:
|
|
// `json:"foo,omitempty". Here options is: ["omitempty"]
|
|
Options []string
|
|
}
|
|
|
|
// Parse parses a single struct field tag and returns the set of tags.
|
|
func Parse(tag string) (*Tags, error) {
|
|
var tags []*Tag
|
|
|
|
hasTag := tag != ""
|
|
|
|
// NOTE(arslan) following code is from reflect and vet package with some
|
|
// modifications to collect all necessary information and extend it with
|
|
// usable methods
|
|
for tag != "" {
|
|
// Skip leading space.
|
|
i := 0
|
|
for i < len(tag) && tag[i] == ' ' {
|
|
i++
|
|
}
|
|
tag = tag[i:]
|
|
if tag == "" {
|
|
break
|
|
}
|
|
|
|
// Scan to colon. A space, a quote or a control character is a syntax
|
|
// error. Strictly speaking, control chars include the range [0x7f,
|
|
// 0x9f], not just [0x00, 0x1f], but in practice, we ignore the
|
|
// multi-byte control characters as it is simpler to inspect the tag's
|
|
// bytes than the tag's runes.
|
|
i = 0
|
|
for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f {
|
|
i++
|
|
}
|
|
|
|
if i == 0 {
|
|
return nil, errTagKeySyntax
|
|
}
|
|
if i+1 >= len(tag) || tag[i] != ':' {
|
|
return nil, errTagSyntax
|
|
}
|
|
if tag[i+1] != '"' {
|
|
return nil, errTagValueSyntax
|
|
}
|
|
|
|
key := string(tag[:i])
|
|
tag = tag[i+1:]
|
|
|
|
// Scan quoted string to find value.
|
|
i = 1
|
|
for i < len(tag) && tag[i] != '"' {
|
|
if tag[i] == '\\' {
|
|
i++
|
|
}
|
|
i++
|
|
}
|
|
if i >= len(tag) {
|
|
return nil, errTagValueSyntax
|
|
}
|
|
|
|
qvalue := string(tag[:i+1])
|
|
tag = tag[i+1:]
|
|
|
|
value, err := strconv.Unquote(qvalue)
|
|
if err != nil {
|
|
return nil, errTagValueSyntax
|
|
}
|
|
|
|
res := strings.Split(value, ",")
|
|
name := res[0]
|
|
options := res[1:]
|
|
if len(options) == 0 {
|
|
options = nil
|
|
}
|
|
|
|
tags = append(tags, &Tag{
|
|
Key: key,
|
|
Name: name,
|
|
Options: options,
|
|
})
|
|
}
|
|
|
|
if hasTag && len(tags) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return &Tags{
|
|
tags: tags,
|
|
}, nil
|
|
}
|
|
|
|
// Get returns the tag associated with the given key. If the key is present
|
|
// in the tag the value (which may be empty) is returned. Otherwise the
|
|
// returned value will be the empty string. The ok return value reports whether
|
|
// the tag exists or not (which the return value is nil).
|
|
func (t *Tags) Get(key string) (*Tag, error) {
|
|
for _, tag := range t.tags {
|
|
if tag.Key == key {
|
|
return tag, nil
|
|
}
|
|
}
|
|
|
|
return nil, errTagNotExist
|
|
}
|
|
|
|
// Set sets the given tag. If the tag key already exists it'll override it
|
|
func (t *Tags) Set(tag *Tag) error {
|
|
if tag.Key == "" {
|
|
return errKeyNotSet
|
|
}
|
|
|
|
added := false
|
|
for i, tg := range t.tags {
|
|
if tg.Key == tag.Key {
|
|
added = true
|
|
t.tags[i] = tag
|
|
}
|
|
}
|
|
|
|
if !added {
|
|
// this means this is a new tag, add it
|
|
t.tags = append(t.tags, tag)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddOptions adds the given option for the given key. If the option already
|
|
// exists it doesn't add it again.
|
|
func (t *Tags) AddOptions(key string, options ...string) {
|
|
for i, tag := range t.tags {
|
|
if tag.Key != key {
|
|
continue
|
|
}
|
|
|
|
for _, opt := range options {
|
|
if !tag.HasOption(opt) {
|
|
tag.Options = append(tag.Options, opt)
|
|
}
|
|
}
|
|
|
|
t.tags[i] = tag
|
|
}
|
|
}
|
|
|
|
// DeleteOptions deletes the given options for the given key
|
|
func (t *Tags) DeleteOptions(key string, options ...string) {
|
|
hasOption := func(option string) bool {
|
|
for _, opt := range options {
|
|
if opt == option {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
for i, tag := range t.tags {
|
|
if tag.Key != key {
|
|
continue
|
|
}
|
|
|
|
var updated []string
|
|
for _, opt := range tag.Options {
|
|
if !hasOption(opt) {
|
|
updated = append(updated, opt)
|
|
}
|
|
}
|
|
|
|
tag.Options = updated
|
|
t.tags[i] = tag
|
|
}
|
|
}
|
|
|
|
// Delete deletes the tag for the given keys
|
|
func (t *Tags) Delete(keys ...string) {
|
|
hasKey := func(key string) bool {
|
|
for _, k := range keys {
|
|
if k == key {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var updated []*Tag
|
|
for _, tag := range t.tags {
|
|
if !hasKey(tag.Key) {
|
|
updated = append(updated, tag)
|
|
}
|
|
}
|
|
|
|
t.tags = updated
|
|
}
|
|
|
|
// Tags returns a slice of tags. The order is the original tag order unless it
|
|
// was changed.
|
|
func (t *Tags) Tags() []*Tag {
|
|
return t.tags
|
|
}
|
|
|
|
// Tags returns a slice of tags. The order is the original tag order unless it
|
|
// was changed.
|
|
func (t *Tags) Keys() []string {
|
|
var keys []string
|
|
for _, tag := range t.tags {
|
|
keys = append(keys, tag.Key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// String reassembles the tags into a valid literal tag field representation
|
|
func (t *Tags) String() string {
|
|
tags := t.Tags()
|
|
if len(tags) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
for i, tag := range t.Tags() {
|
|
buf.WriteString(tag.String())
|
|
if i != len(tags)-1 {
|
|
buf.WriteString(" ")
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// HasOption returns true if the given option is available in options
|
|
func (t *Tag) HasOption(opt string) bool {
|
|
for _, tagOpt := range t.Options {
|
|
if tagOpt == opt {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Value returns the raw value of the tag, i.e. if the tag is
|
|
// `json:"foo,omitempty", the Value is "foo,omitempty"
|
|
func (t *Tag) Value() string {
|
|
options := strings.Join(t.Options, ",")
|
|
if options != "" {
|
|
return fmt.Sprintf(`%s,%s`, t.Name, options)
|
|
}
|
|
return t.Name
|
|
}
|
|
|
|
// String reassembles the tag into a valid tag field representation
|
|
func (t *Tag) String() string {
|
|
return fmt.Sprintf(`%s:%q`, t.Key, t.Value())
|
|
}
|
|
|
|
// GoString implements the fmt.GoStringer interface
|
|
func (t *Tag) GoString() string {
|
|
template := `{
|
|
Key: '%s',
|
|
Name: '%s',
|
|
Option: '%s',
|
|
}`
|
|
|
|
if t.Options == nil {
|
|
return fmt.Sprintf(template, t.Key, t.Name, "nil")
|
|
}
|
|
|
|
options := strings.Join(t.Options, ",")
|
|
return fmt.Sprintf(template, t.Key, t.Name, options)
|
|
}
|
|
|
|
func (t *Tags) Len() int {
|
|
return len(t.tags)
|
|
}
|
|
|
|
func (t *Tags) Less(i int, j int) bool {
|
|
return t.tags[i].Key < t.tags[j].Key
|
|
}
|
|
|
|
func (t *Tags) Swap(i int, j int) {
|
|
t.tags[i], t.tags[j] = t.tags[j], t.tags[i]
|
|
}
|