Skip to content

Commit b48075b

Browse files
committed
feat(cmp): Every/Some
1 parent d5f2d95 commit b48075b

File tree

3 files changed

+218
-4
lines changed

3 files changed

+218
-4
lines changed

cmp/cmp.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ package cmp
22

33
import (
44
"cmp"
5+
"errors"
6+
"fmt"
7+
"iter"
58
"reflect"
9+
10+
"github.com/go-json-experiment/json/jsontext"
611
)
712

813
func True() func(a bool) error {
@@ -149,3 +154,55 @@ func Len[V any, E int | func(int) error](e E) func(a V) error {
149154
return nil
150155
}
151156
}
157+
158+
func Every[V any](p func(V) error) func(seq iter.Seq[V]) error {
159+
return func(seq iter.Seq[V]) error {
160+
i := 0
161+
for item := range seq {
162+
if err := p(item); err != nil {
163+
return wrap(err, "elem", fmt.Sprintf("%d", i), item)
164+
}
165+
i++
166+
}
167+
return nil
168+
}
169+
}
170+
171+
func Some[V any](p func(V) error) func(seq iter.Seq[V]) error {
172+
return func(seq iter.Seq[V]) error {
173+
var lastErr error
174+
for item := range seq {
175+
if err := p(item); err == nil {
176+
return nil
177+
} else {
178+
lastErr = err
179+
}
180+
}
181+
return &ErrCheck{
182+
Topic: "elem",
183+
Err: fmt.Errorf("none of the elements satisfy the predicate (error: %w)", lastErr),
184+
}
185+
}
186+
}
187+
188+
func wrap(err error, topic string, tok string, actual any) error {
189+
if err == nil {
190+
return nil
191+
}
192+
193+
next := &ErrCheck{
194+
Topic: topic,
195+
Err: err,
196+
Actual: actual,
197+
}
198+
199+
if child, ok := errors.AsType[*ErrCheck](err); ok {
200+
next.Pointer = jsontext.Pointer("").AppendToken(tok) + child.Pointer
201+
next.Err = child.Err
202+
next.Topic = child.Topic
203+
} else {
204+
next.Pointer = jsontext.Pointer("").AppendToken(tok)
205+
}
206+
207+
return next
208+
}

cmp/cmp_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package cmp_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"maps"
7+
"slices"
8+
"testing"
9+
10+
"github.com/octohelm/x/cmp"
11+
. "github.com/octohelm/x/testing/v2"
12+
)
13+
14+
type User struct {
15+
ID int
16+
Name string
17+
Tags []string
18+
}
19+
20+
func TestCmp(t *testing.T) {
21+
t.Run("原子比较 (True, False, Eq, Neq)", func(t *testing.T) {
22+
Then(t, "布尔校验",
23+
Expect(true, Be(cmp.True())),
24+
Expect(false, Be(cmp.False())),
25+
)
26+
27+
Then(t, "等值校验",
28+
Expect(100, Be(cmp.Eq(100))),
29+
Expect("go", Be(cmp.Neq("java"))),
30+
)
31+
})
32+
33+
t.Run("数值区间 (Gt, Gte, Lt, Lte)", func(t *testing.T) {
34+
val := 10
35+
Then(t, "区间断言",
36+
Expect(val, Be(cmp.Gt(5))),
37+
Expect(val, Be(cmp.Gte(10))),
38+
Expect(val, Be(cmp.Lt(20))),
39+
Expect(val, Be(cmp.Lte(10))),
40+
)
41+
})
42+
43+
t.Run("状态校验 (Nil, NotNil, Zero, NotZero)", func(t *testing.T) {
44+
var ptr *int
45+
var s []int
46+
47+
Then(t, "空指针与空容器",
48+
Expect(ptr, Be(cmp.Nil[*int]())),
49+
Expect(s, Be(cmp.Nil[[]int]())),
50+
Expect(1, Be(cmp.NotNil[int]())),
51+
)
52+
53+
Then(t, "零值状态",
54+
Expect(0, Be(cmp.Zero[int]())),
55+
Expect(User{}, Be(cmp.Zero[User]())),
56+
Expect("hello", Be(cmp.NotZero[string]())),
57+
)
58+
})
59+
60+
t.Run("容器长度 (Len)", func(t *testing.T) {
61+
list := []int{1, 2, 3}
62+
dict := map[string]int{"a": 1}
63+
64+
Then(t, "支持 int 或谓词函数",
65+
Expect(list, Be(cmp.Len[[]int](3))),
66+
Expect(dict, Be(cmp.Len[map[string]int](cmp.Gt(0)))),
67+
Expect("golang", Be(cmp.Len[string](cmp.Lte(10)))),
68+
)
69+
})
70+
71+
t.Run("迭代器校验 (Every)", func(t *testing.T) {
72+
t.Run("校验 Slice 元素", func(t *testing.T) {
73+
nums := []int{2, 4, 6, 8}
74+
// 配合 slices.Values 转换为 iter.Seq[int]
75+
Then(t, "所有元素必须为偶数",
76+
Expect(slices.Values(nums), Be(cmp.Every(func(v int) error {
77+
if v%2 != 0 {
78+
return fmt.Errorf("must be even")
79+
}
80+
return nil
81+
}))),
82+
)
83+
})
84+
85+
t.Run("校验 Map 键值", func(t *testing.T) {
86+
m := map[string]int{"a": 10, "b": 20}
87+
88+
Then(t, "校验所有 Value",
89+
Expect(maps.Values(m), Be(cmp.Every(cmp.Gte(10)))),
90+
)
91+
92+
Then(t, "校验所有 Key",
93+
Expect(maps.Keys(m), Be(cmp.Every(cmp.NotZero[string]()))),
94+
)
95+
})
96+
})
97+
98+
t.Run("迭代器校验 (Some)", func(t *testing.T) {
99+
nums := []int{1, 3, 5, 8, 9}
100+
101+
Then(t, "存在满足条件的元素",
102+
// 集合里至少有一个偶数 (8)
103+
Expect(slices.Values(nums), Be(cmp.Some(func(v int) error {
104+
if v%2 == 0 {
105+
return nil
106+
}
107+
return fmt.Errorf("not even")
108+
}))),
109+
)
110+
111+
Then(t, "配合内建谓词",
112+
Expect(slices.Values(nums), Be(cmp.Some(cmp.Eq(5)))),
113+
)
114+
})
115+
116+
t.Run("综合复杂场景", func(t *testing.T) {
117+
users := []User{
118+
{ID: 1, Name: "Alice", Tags: []string{"admin", "staff"}},
119+
{ID: 2, Name: "Bob", Tags: []string{"staff"}},
120+
}
121+
122+
Then(t, "深度嵌套校验",
123+
Expect(
124+
slices.Values(users),
125+
Be(cmp.Every(func(u User) error {
126+
if err := cmp.Gt(0)(u.ID); err != nil {
127+
return err
128+
}
129+
return cmp.Some(cmp.Eq("staff"))(
130+
slices.Values(u.Tags),
131+
)
132+
}))),
133+
)
134+
})
135+
136+
t.Run("错误结构验证", func(t *testing.T) {
137+
nums := []int{1, 2, 3}
138+
err := cmp.Every(cmp.Eq(1))(slices.Values(nums))
139+
if err != nil {
140+
e, ok := errors.AsType[*cmp.ErrCheck](err)
141+
142+
Then(t, "应返回 ErrCheck 类型且 Topic 为 elem",
143+
Expect(ok, Be(cmp.True())),
144+
Expect(e.Topic, Be(cmp.Eq("elem"))),
145+
Expect(e.Err, Be(cmp.NotNil[error]())),
146+
)
147+
}
148+
})
149+
}

cmp/errors.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package cmp
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
6+
"github.com/go-json-experiment/json/jsontext"
7+
)
48

59
type ErrCondition struct {
610
Op string
@@ -22,11 +26,15 @@ func (e *ErrState) Error() string {
2226
}
2327

2428
type ErrCheck struct {
25-
Topic string
26-
Err error
27-
Actual any
29+
Topic string
30+
Err error
31+
Actual any
32+
Pointer jsontext.Pointer
2833
}
2934

3035
func (e *ErrCheck) Error() string {
36+
if len(e.Pointer) > 0 {
37+
return fmt.Sprintf("%s: %s check failed: %v", e.Pointer, e.Topic, e.Err)
38+
}
3139
return fmt.Sprintf("%s check failed: %v", e.Topic, e.Err)
3240
}

0 commit comments

Comments
 (0)