Skip to content
Merged
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
127 changes: 127 additions & 0 deletions pkg/codingcontext/param_map_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package codingcontext

import (
"strings"
"testing"
)

Expand Down Expand Up @@ -95,3 +96,129 @@ func TestParams_SetMultiple(t *testing.T) {
t.Errorf("Params[key2] = %q, want %q", p["key2"], "value2")
}
}

func TestParseParams(t *testing.T) {
tests := []struct {
name string
input string
expected Params
wantError bool
errorMsg string
}{
{
name: "empty string",
input: "",
expected: Params{},
wantError: false,
},
{
name: "single quoted key=value",
input: `key="value"`,
expected: Params{"key": "value"},
wantError: false,
},
{
name: "multiple quoted key=value pairs",
input: `key1="value1" key2="value2" key3="value3"`,
expected: Params{"key1": "value1", "key2": "value2", "key3": "value3"},
wantError: false,
},
{
name: "double-quoted value with spaces",
input: `key1="value with spaces" key2="value2"`,
expected: Params{"key1": "value with spaces", "key2": "value2"},
wantError: false,
},
{
name: "escaped double quotes",
input: `key1="value with \"escaped\" quotes"`,
expected: Params{"key1": `value with "escaped" quotes`},
wantError: false,
},
{
name: "value with equals sign in quotes",
input: `key1="value=with=equals" key2="normal"`,
expected: Params{"key1": "value=with=equals", "key2": "normal"},
wantError: false,
},
{
name: "empty quoted value",
input: `key1="" key2="value2"`,
expected: Params{"key1": "", "key2": "value2"},
wantError: false,
},
{
name: "whitespace around equals",
input: `key1 = "value1" key2="value2"`,
expected: Params{"key1": "value1", "key2": "value2"},
wantError: false,
},
{
name: "quoted value with spaces and equals",
input: `key1="value with spaces and = signs"`,
expected: Params{"key1": "value with spaces and = signs"},
wantError: false,
},
{
name: "unquoted value - error",
input: `key1=value1`,
wantError: true,
errorMsg: "unquoted value",
},
{
name: "mixed quoted and unquoted - error",
input: `key1="quoted value" key2=unquoted`,
wantError: true,
errorMsg: "unquoted value",
},
{
name: "unclosed quote - error",
input: `key1="value with spaces`,
wantError: true,
errorMsg: "unclosed quote",
},
{
name: "missing value after equals - error",
input: `key1= key2="value2"`,
wantError: true,
errorMsg: "unquoted value",
},
{
name: "single quote not supported - error",
input: `key1='value'`,
wantError: true,
errorMsg: "unquoted value",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParseParams(tt.input)

if (err != nil) != tt.wantError {
t.Errorf("ParseParams() error = %v, wantError %v", err, tt.wantError)
return
}

if tt.wantError {
if err != nil && tt.errorMsg != "" {
if !strings.Contains(err.Error(), tt.errorMsg) {
t.Errorf("ParseParams() error = %v, want error containing %q", err, tt.errorMsg)
}
}
return
}

if len(result) != len(tt.expected) {
t.Errorf("ParseParams() length = %d, want %d", len(result), len(tt.expected))
return
}

for k, v := range tt.expected {
if result[k] != v {
t.Errorf("ParseParams()[%q] = %q, want %q", k, result[k], v)
}
}
})
}
}
82 changes: 82 additions & 0 deletions pkg/codingcontext/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,85 @@ func (p *Params) Set(value string) error {
(*p)[kv[0]] = kv[1]
return nil
}

// ParseParams parses a string containing key=value pairs separated by spaces.
// Values must be quoted with double quotes, and quotes can be escaped.
// Unquoted values are treated as an error.
// Examples:
// - `key1="value1" key2="value2"`
// - `key1="value with spaces" key2="value2"`
// - `key1="value with \"escaped\" quotes"`
func ParseParams(s string) (Params, error) {
params := make(Params)
if s == "" {
return params, nil
}

s = strings.TrimSpace(s)
var i int
for i < len(s) {
// Skip whitespace
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
i++
}
if i >= len(s) {
break
}

// Find the key (until '=')
keyStart := i
for i < len(s) && s[i] != '=' {
i++
}
if i >= len(s) {
break
}
key := strings.TrimSpace(s[keyStart:i])
if key == "" {
i++
continue
}

// Skip '='
i++

// Skip whitespace after '='
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
i++
}
if i >= len(s) {
return nil, fmt.Errorf("missing quoted value for key %q", key)
}

// Values must be quoted
if s[i] != '"' {
return nil, fmt.Errorf("unquoted value for key %q: values must be double-quoted", key)
}

// Parse the double-quoted value
var value strings.Builder
i++ // skip opening quote
quoted := false
for i < len(s) {
if s[i] == '\\' && i+1 < len(s) && s[i+1] == '"' {
value.WriteByte('"')
i += 2
} else if s[i] == '"' {
i++ // skip closing quote
quoted = true
break
} else {
value.WriteByte(s[i])
i++
}
}

if !quoted {
return nil, fmt.Errorf("unclosed quote for key %q", key)
}

params[key] = value.String()
}

return params, nil
}
Loading