Skip to content

Commit ef8d40f

Browse files
authored
feat: add ParseParams function with quote handling (#123)
* feat: add ParseParams function with quote handling Implement ParseParams function that correctly parses key=value pairs with support for: - Double and single quoted values - Escaped quotes within quoted values - Empty values - Whitespace handling - Multiple pairs in a single string * refactor: remove single-quote support from ParseParams Only double-quotes are now supported for quoted values. Single quotes are treated as regular characters in unquoted values. * refactor: require quoted values in ParseParams ParseParams now requires all values to be double-quoted. Unquoted values are treated as an error. Function signature changed to return (Params, error) to support error handling.
1 parent 4c030f9 commit ef8d40f

File tree

2 files changed

+209
-0
lines changed

2 files changed

+209
-0
lines changed

pkg/codingcontext/param_map_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package codingcontext
22

33
import (
4+
"strings"
45
"testing"
56
)
67

@@ -95,3 +96,129 @@ func TestParams_SetMultiple(t *testing.T) {
9596
t.Errorf("Params[key2] = %q, want %q", p["key2"], "value2")
9697
}
9798
}
99+
100+
func TestParseParams(t *testing.T) {
101+
tests := []struct {
102+
name string
103+
input string
104+
expected Params
105+
wantError bool
106+
errorMsg string
107+
}{
108+
{
109+
name: "empty string",
110+
input: "",
111+
expected: Params{},
112+
wantError: false,
113+
},
114+
{
115+
name: "single quoted key=value",
116+
input: `key="value"`,
117+
expected: Params{"key": "value"},
118+
wantError: false,
119+
},
120+
{
121+
name: "multiple quoted key=value pairs",
122+
input: `key1="value1" key2="value2" key3="value3"`,
123+
expected: Params{"key1": "value1", "key2": "value2", "key3": "value3"},
124+
wantError: false,
125+
},
126+
{
127+
name: "double-quoted value with spaces",
128+
input: `key1="value with spaces" key2="value2"`,
129+
expected: Params{"key1": "value with spaces", "key2": "value2"},
130+
wantError: false,
131+
},
132+
{
133+
name: "escaped double quotes",
134+
input: `key1="value with \"escaped\" quotes"`,
135+
expected: Params{"key1": `value with "escaped" quotes`},
136+
wantError: false,
137+
},
138+
{
139+
name: "value with equals sign in quotes",
140+
input: `key1="value=with=equals" key2="normal"`,
141+
expected: Params{"key1": "value=with=equals", "key2": "normal"},
142+
wantError: false,
143+
},
144+
{
145+
name: "empty quoted value",
146+
input: `key1="" key2="value2"`,
147+
expected: Params{"key1": "", "key2": "value2"},
148+
wantError: false,
149+
},
150+
{
151+
name: "whitespace around equals",
152+
input: `key1 = "value1" key2="value2"`,
153+
expected: Params{"key1": "value1", "key2": "value2"},
154+
wantError: false,
155+
},
156+
{
157+
name: "quoted value with spaces and equals",
158+
input: `key1="value with spaces and = signs"`,
159+
expected: Params{"key1": "value with spaces and = signs"},
160+
wantError: false,
161+
},
162+
{
163+
name: "unquoted value - error",
164+
input: `key1=value1`,
165+
wantError: true,
166+
errorMsg: "unquoted value",
167+
},
168+
{
169+
name: "mixed quoted and unquoted - error",
170+
input: `key1="quoted value" key2=unquoted`,
171+
wantError: true,
172+
errorMsg: "unquoted value",
173+
},
174+
{
175+
name: "unclosed quote - error",
176+
input: `key1="value with spaces`,
177+
wantError: true,
178+
errorMsg: "unclosed quote",
179+
},
180+
{
181+
name: "missing value after equals - error",
182+
input: `key1= key2="value2"`,
183+
wantError: true,
184+
errorMsg: "unquoted value",
185+
},
186+
{
187+
name: "single quote not supported - error",
188+
input: `key1='value'`,
189+
wantError: true,
190+
errorMsg: "unquoted value",
191+
},
192+
}
193+
194+
for _, tt := range tests {
195+
t.Run(tt.name, func(t *testing.T) {
196+
result, err := ParseParams(tt.input)
197+
198+
if (err != nil) != tt.wantError {
199+
t.Errorf("ParseParams() error = %v, wantError %v", err, tt.wantError)
200+
return
201+
}
202+
203+
if tt.wantError {
204+
if err != nil && tt.errorMsg != "" {
205+
if !strings.Contains(err.Error(), tt.errorMsg) {
206+
t.Errorf("ParseParams() error = %v, want error containing %q", err, tt.errorMsg)
207+
}
208+
}
209+
return
210+
}
211+
212+
if len(result) != len(tt.expected) {
213+
t.Errorf("ParseParams() length = %d, want %d", len(result), len(tt.expected))
214+
return
215+
}
216+
217+
for k, v := range tt.expected {
218+
if result[k] != v {
219+
t.Errorf("ParseParams()[%q] = %q, want %q", k, result[k], v)
220+
}
221+
}
222+
})
223+
}
224+
}

pkg/codingcontext/params.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,85 @@ func (p *Params) Set(value string) error {
2525
(*p)[kv[0]] = kv[1]
2626
return nil
2727
}
28+
29+
// ParseParams parses a string containing key=value pairs separated by spaces.
30+
// Values must be quoted with double quotes, and quotes can be escaped.
31+
// Unquoted values are treated as an error.
32+
// Examples:
33+
// - `key1="value1" key2="value2"`
34+
// - `key1="value with spaces" key2="value2"`
35+
// - `key1="value with \"escaped\" quotes"`
36+
func ParseParams(s string) (Params, error) {
37+
params := make(Params)
38+
if s == "" {
39+
return params, nil
40+
}
41+
42+
s = strings.TrimSpace(s)
43+
var i int
44+
for i < len(s) {
45+
// Skip whitespace
46+
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
47+
i++
48+
}
49+
if i >= len(s) {
50+
break
51+
}
52+
53+
// Find the key (until '=')
54+
keyStart := i
55+
for i < len(s) && s[i] != '=' {
56+
i++
57+
}
58+
if i >= len(s) {
59+
break
60+
}
61+
key := strings.TrimSpace(s[keyStart:i])
62+
if key == "" {
63+
i++
64+
continue
65+
}
66+
67+
// Skip '='
68+
i++
69+
70+
// Skip whitespace after '='
71+
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
72+
i++
73+
}
74+
if i >= len(s) {
75+
return nil, fmt.Errorf("missing quoted value for key %q", key)
76+
}
77+
78+
// Values must be quoted
79+
if s[i] != '"' {
80+
return nil, fmt.Errorf("unquoted value for key %q: values must be double-quoted", key)
81+
}
82+
83+
// Parse the double-quoted value
84+
var value strings.Builder
85+
i++ // skip opening quote
86+
quoted := false
87+
for i < len(s) {
88+
if s[i] == '\\' && i+1 < len(s) && s[i+1] == '"' {
89+
value.WriteByte('"')
90+
i += 2
91+
} else if s[i] == '"' {
92+
i++ // skip closing quote
93+
quoted = true
94+
break
95+
} else {
96+
value.WriteByte(s[i])
97+
i++
98+
}
99+
}
100+
101+
if !quoted {
102+
return nil, fmt.Errorf("unclosed quote for key %q", key)
103+
}
104+
105+
params[key] = value.String()
106+
}
107+
108+
return params, nil
109+
}

0 commit comments

Comments
 (0)