Skip to content

Commit 7ae271a

Browse files
Add httpassert ExtractRegexTo
1 parent 27b9107 commit 7ae271a

File tree

2 files changed

+111
-51
lines changed

2 files changed

+111
-51
lines changed

pkg/httpassert/extracter.go

Lines changed: 109 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package httpassert
77
import (
88
"fmt"
99
"reflect"
10+
"regexp"
1011
"testing"
1112

1213
"github.com/stretchr/testify/assert"
@@ -21,68 +22,127 @@ type Extractor func(t *testing.T, actual any) any
2122
// request.Expect().JsonPath("$.data.id", httpassert.ExtractTo(&id))
2223
func ExtractTo(ptr any) Extractor {
2324
return func(t *testing.T, actual any) any {
24-
targetVal := reflect.ValueOf(ptr)
25-
if targetVal.Kind() != reflect.Ptr || targetVal.IsNil() {
26-
assert.Fail(t, "ExtractTo requires a non-nil pointer")
25+
return extractInto(t, ptr, actual, nil)
26+
}
27+
}
28+
29+
func ExtractRegexTo(value string, ptr any) Extractor {
30+
return func(t *testing.T, actual any) any {
31+
return extractInto(t, ptr, actual, func(t *testing.T, v any, dstType reflect.Type) (any, bool) {
32+
s, ok := v.(string)
33+
if !ok {
34+
assert.Fail(t, fmt.Sprintf("ExtractRegexTo expects actual to be string, got %T", v))
35+
return nil, false
36+
}
37+
38+
re := regexp.MustCompile(value)
39+
40+
m := re.FindStringSubmatch(s)
41+
if m == nil {
42+
assert.Fail(t, fmt.Sprintf("ExtractRegexTo no match for %q in %q", re.String(), s))
43+
return nil, false
44+
}
45+
46+
// m[0] full match, m[1:] groups
47+
groups := m[1:]
48+
if len(groups) == 0 {
49+
groups = []string{m[0]}
50+
}
51+
52+
if dstType.Kind() == reflect.Slice {
53+
return groups, true
54+
}
55+
return groups[0], true
56+
})
57+
}
58+
}
59+
60+
func extractInto(
61+
t *testing.T,
62+
ptr any,
63+
actual any,
64+
preprocess func(t *testing.T, actual any, dstType reflect.Type) (any, bool),
65+
) any {
66+
target := reflect.ValueOf(ptr)
67+
if target.Kind() != reflect.Ptr || target.IsNil() {
68+
assert.Fail(t, "ExtractTo requires a non-nil pointer")
69+
return nil
70+
}
71+
if actual == nil {
72+
assert.Fail(t, "ExtractTo actual value is nil")
73+
return nil
74+
}
75+
76+
dst := target.Elem()
77+
dstType := dst.Type()
78+
79+
if preprocess != nil {
80+
var ok bool
81+
actual, ok = preprocess(t, actual, dstType)
82+
if !ok {
2783
return nil
2884
}
29-
3085
if actual == nil {
3186
assert.Fail(t, "ExtractTo actual value is nil")
3287
return nil
3388
}
89+
}
3490

35-
outVal := reflect.ValueOf(actual)
36-
targetType := targetVal.Elem().Type()
91+
src := reflect.ValueOf(actual)
3792

38-
// Direct assign / convert for simple values (string, int, etc.)
39-
if outVal.Type().AssignableTo(targetType) {
40-
targetVal.Elem().Set(outVal)
41-
return ptr
42-
}
93+
// unwrap interface{}
94+
if src.IsValid() && src.Kind() == reflect.Interface && !src.IsNil() {
95+
src = reflect.ValueOf(src.Interface())
96+
}
4397

44-
if outVal.Type().ConvertibleTo(targetType) {
45-
targetVal.Elem().Set(outVal.Convert(targetType))
46-
return ptr
47-
}
98+
// direct assign / convert
99+
if src.IsValid() && src.Type().AssignableTo(dstType) {
100+
dst.Set(src)
101+
return ptr
102+
}
103+
if src.IsValid() && src.Type().ConvertibleTo(dstType) {
104+
dst.Set(src.Convert(dstType))
105+
return ptr
106+
}
48107

49-
// Special handling for slices, e.g. []interface{} -> []string
50-
if outVal.Kind() == reflect.Slice && targetType.Kind() == reflect.Slice {
51-
elemType := targetType.Elem()
52-
n := outVal.Len()
53-
dst := reflect.MakeSlice(targetType, n, n)
54-
55-
for i := 0; i < n; i++ {
56-
src := outVal.Index(i)
57-
58-
// If it's interface{}, unwrap to the underlying concrete value.
59-
if src.Kind() == reflect.Interface && !src.IsNil() {
60-
src = reflect.ValueOf(src.Interface())
61-
}
62-
63-
if src.Type().AssignableTo(elemType) {
64-
dst.Index(i).Set(src)
65-
continue
66-
}
67-
68-
if src.Type().ConvertibleTo(elemType) {
69-
dst.Index(i).Set(src.Convert(elemType))
70-
continue
71-
}
72-
73-
assert.Fail(t,
74-
fmt.Sprintf("ExtractTo slice element type mismatch at index %d: cannot assign %v to %v",
75-
i, src.Type(), elemType))
76-
return nil
108+
// slice handling: e.g. []interface{} -> []string
109+
if src.IsValid() && src.Kind() == reflect.Slice && dstType.Kind() == reflect.Slice {
110+
elemType := dstType.Elem()
111+
n := src.Len()
112+
out := reflect.MakeSlice(dstType, n, n)
113+
114+
for i := 0; i < n; i++ {
115+
s := src.Index(i)
116+
117+
// unwrap interface{} elements
118+
if s.Kind() == reflect.Interface && !s.IsNil() {
119+
s = reflect.ValueOf(s.Interface())
120+
}
121+
122+
if s.Type().AssignableTo(elemType) {
123+
out.Index(i).Set(s)
124+
continue
125+
}
126+
if s.Type().ConvertibleTo(elemType) {
127+
out.Index(i).Set(s.Convert(elemType))
128+
continue
77129
}
78130

79-
targetVal.Elem().Set(dst)
80-
return ptr
131+
assert.Fail(t, fmt.Sprintf(
132+
"ExtractTo slice element type mismatch at index %d: cannot assign %v to %v",
133+
i, s.Type(), elemType,
134+
))
135+
return nil
81136
}
82137

83-
assert.Fail(t,
84-
fmt.Sprintf("ExtractTo type mismatch: cannot assign %v to %v",
85-
outVal.Type(), targetType))
86-
return nil
138+
dst.Set(out)
139+
return ptr
140+
}
141+
142+
srcType := any("<invalid>")
143+
if src.IsValid() {
144+
srcType = src.Type()
87145
}
146+
assert.Fail(t, fmt.Sprintf("ExtractTo type mismatch: cannot assign %v to %v", srcType, dstType))
147+
return nil
88148
}

pkg/httpassert/response_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ func TestResponse(t *testing.T) {
164164
var value string
165165
m.Get("/api").
166166
Expect().
167-
Header("Content-Type", ExtractTo(&value))
168-
assert.Equal(t, "application/json", value)
167+
Header("Content-Type", ExtractRegexTo(`application/(.*)`, &value))
168+
assert.Equal(t, "json", value)
169169
})
170170

171171
t.Run("use matcher", func(t *testing.T) {

0 commit comments

Comments
 (0)