Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,10 @@ func Parse(tag string) (*Tags, error) {
qvalue := tag[:i+1]
tag = tag[i+1:]

value, err := strconv.Unquote(qvalue)
res, err := splitWithEscapedComma(qvalue)
if err != nil {
return nil, errTagValueSyntax
}

res := strings.Split(value, ",")
name := res[0]
options := res[1:]
if len(options) == 0 {
Expand All @@ -124,6 +122,38 @@ func Parse(tag string) (*Tags, error) {
}, nil
}

// splitWithEscapedComma splits the input on unescaped ',' locations.
// This allows having options contain regular expressions with ',' in them
// strconv.Unquote() considers strings with '\\,' to be syntactically wrong
// so unquoting is done after '\\,' is no longer an issue
func splitWithEscapedComma(value string) ([]string, error) {
res := strings.Split(value[1:len(value)-1], ",")
ret := make([]string, len(res))
out := 0
for _, r := range res {
l := len(r)
t := r
wasEscape := false
if l > 1 && t[l-1] == '\\' {
wasEscape = true
t = t[0:l-2] + ","
}
ret[out] = ret[out] + t
if !wasEscape {
out++
}
}
for i := 0; i < out; i++ {
var err error
ret[i], err = strconv.Unquote(fmt.Sprintf("\"%s\"", ret[i]))
if err != nil {
return nil, err
}
}

return ret[0:out], 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
Expand Down Expand Up @@ -273,16 +303,17 @@ func (t *Tag) HasOption(opt string) bool {
// 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)
optCopy := make([]string, 0, len(t.Options)+1)
optCopy = append(optCopy, t.Name)
for _, v := range t.Options {
optCopy = append(optCopy, strings.ReplaceAll(v, ",", "\\,"))
}
return t.Name
return strings.Join(optCopy, ",")
}

// String reassembles the tag into a valid tag field representation
func (t *Tag) String() string {
return fmt.Sprintf(`%s:%q`, t.Key, t.Value())
return fmt.Sprintf("%s:%q", t.Key, t.Value())
}

// GoString implements the fmt.GoStringer interface
Expand Down
16 changes: 16 additions & 0 deletions tags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,22 @@ func TestParse(t *testing.T) {
},
},
},
{
name: "tag with options containing escaped commas",
tag: `jsonschema:"required,maxLength=1024,minLength=1,pattern=^[A-Za-z]([A-Za-z0-9_\\,:]*[A-Za-z0-9_])?$,description=some long description\\, that has both spaces and escaped comma characters"`,
exp: []*Tag{
{
Key: "jsonschema",
Name: "required",
Options: []string{
"maxLength=1024",
"minLength=1",
"pattern=^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$",
"description=some long description, that has both spaces and escaped comma characters",
},
},
},
},
}

for _, ts := range test {
Expand Down