Skip to content

Commit 37efc42

Browse files
committed
Add listable
1 parent 2c92339 commit 37efc42

6 files changed

Lines changed: 227 additions & 20 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package duration
1+
package types
22

33
import (
44
"encoding/json"

infra/conf/cfgcommon/duration/duration_test.go renamed to infra/conf/cfgcommon/types/duration_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
package duration_test
1+
package types_test
22

33
import (
44
"encoding/json"
55
"testing"
66
"time"
77

8-
"github.com/xtls/xray-core/infra/conf/cfgcommon/duration"
8+
"github.com/xtls/xray-core/infra/conf/cfgcommon/types"
99
)
1010

1111
type testWithDuration struct {
12-
Duration duration.Duration
12+
Duration types.Duration
1313
}
1414

1515
func TestDurationJSON(t *testing.T) {
1616
expected := &testWithDuration{
17-
Duration: duration.Duration(time.Hour),
17+
Duration: types.Duration(time.Hour),
1818
}
1919
data, err := json.Marshal(expected)
2020
if err != nil {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package types
2+
3+
import (
4+
"encoding/json"
5+
"reflect"
6+
"slices"
7+
"strings"
8+
)
9+
10+
// Listable allows a field to be unmarshalled from a single object or a list of objects.
11+
// If the json input is a single object, it will be stored as a slice with one element.
12+
// If the json input is null or empty or a single empty object, it will be nil.
13+
type Listable[T any] []T
14+
15+
func (l *Listable[T]) UnmarshalJSON(data []byte) error {
16+
var v T
17+
if len(data) != 0 && !slices.Equal(data, []byte("null")) && data[0] != '[' {
18+
if err := json.Unmarshal(data, &v); err == nil {
19+
// make the list nil if the single value is the zero value
20+
var zero T
21+
if reflect.DeepEqual(v, zero) {
22+
return nil
23+
}
24+
*l = []T{v}
25+
return err
26+
}
27+
}
28+
return json.Unmarshal(data, (*[]T)(l))
29+
}
30+
31+
// ListableSimpleString is like Listable[string], but able to separate by `~`
32+
type ListableSimpleString []string
33+
34+
func (l *ListableSimpleString) UnmarshalJSON(data []byte) error {
35+
var v string
36+
if len(data) != 0 && !slices.Equal(data, []byte("null")) && data[0] != '[' {
37+
if err := json.Unmarshal(data, &v); err == nil {
38+
if v == "" {
39+
// make the list nil if the single value is empty string
40+
return nil
41+
}
42+
*l = strings.Split(v, "~")
43+
return nil
44+
}
45+
}
46+
return json.Unmarshal(data, (*[]string)(l))
47+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package types_test
2+
3+
import (
4+
"encoding/json"
5+
"slices"
6+
"testing"
7+
8+
"github.com/xtls/xray-core/infra/conf/cfgcommon/types"
9+
)
10+
11+
type TestGroup[T any] struct {
12+
name string
13+
input string
14+
expected []T
15+
}
16+
17+
// intentionally to be so chaos
18+
var rawJson = `{
19+
"field":
20+
["value1",
21+
"value2", "value3"
22+
]
23+
}`
24+
25+
func TestListableUnmarshal(t *testing.T) {
26+
type TestStruct struct {
27+
Field types.Listable[string] `json:"field"`
28+
}
29+
30+
tests := []TestGroup[string]{
31+
{
32+
name: "SingleString",
33+
input: `{"field": "hello"}`,
34+
expected: []string{"hello"},
35+
},
36+
{
37+
name: "ArrayString",
38+
input: `{"field": ["value1", "value2", "value3"]}`,
39+
expected: []string{"value1", "value2", "value3"},
40+
},
41+
{
42+
name: "ComplexArray",
43+
input: rawJson,
44+
expected: []string{"value1", "value2", "value3"},
45+
},
46+
{
47+
name: "SingleStringWithSpace",
48+
input: `{"field": "hello" }`,
49+
expected: []string{"hello"},
50+
},
51+
{
52+
name: "ArrayWithSpace",
53+
input: `{"field": [ "a", "b" ] }`,
54+
expected: []string{"a", "b"},
55+
},
56+
{
57+
name: "SingleEmptyString",
58+
input: `{"field": ""}`,
59+
expected: nil,
60+
},
61+
{
62+
name: "Null",
63+
input: `{"field": null}`,
64+
expected: nil,
65+
},
66+
{
67+
name: "Missing (default)",
68+
input: `{}`,
69+
expected: nil,
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
var ts TestStruct
76+
err := json.Unmarshal([]byte(tt.input), &ts)
77+
if err != nil {
78+
t.Fatalf("Unmarshal failed: %v", err)
79+
}
80+
if !slices.Equal([]string(ts.Field), tt.expected) {
81+
t.Errorf("Expected %v, got %v", tt.expected, ts.Field)
82+
}
83+
})
84+
}
85+
}
86+
87+
func TestListableInt(t *testing.T) {
88+
tests := []TestGroup[int]{
89+
{
90+
name: "SingleInt",
91+
input: `123`,
92+
expected: []int{123},
93+
},
94+
{
95+
name: "ArrayInt",
96+
input: `[1, 2]`,
97+
expected: []int{1, 2},
98+
},
99+
{
100+
name: "Null",
101+
input: `null`,
102+
expected: nil,
103+
},
104+
}
105+
106+
for _, tt := range tests {
107+
t.Run(tt.name, func(t *testing.T) {
108+
var l types.Listable[int]
109+
err := json.Unmarshal([]byte(tt.input), &l)
110+
if err != nil {
111+
t.Fatalf("Unmarshal failed: %v", err)
112+
}
113+
if !slices.Equal([]int(l), tt.expected) {
114+
t.Errorf("Expected %v, got %v", tt.expected, l)
115+
}
116+
})
117+
}
118+
}
119+
120+
func TestListableSimpleString(t *testing.T) {
121+
type TestStruct struct {
122+
Field types.ListableSimpleString `json:"field"`
123+
}
124+
125+
tests := []TestGroup[string]{
126+
{
127+
name: "SingleString",
128+
input: `{"field": "singleValue"}`,
129+
expected: []string{"singleValue"},
130+
},
131+
{
132+
name: "ArrayString",
133+
input: `{"field": ["value1", "value2", "value3"]}`,
134+
expected: []string{"value1", "value2", "value3"},
135+
},
136+
{
137+
name: "SingleEmptyString",
138+
input: `{"field": ""}`,
139+
expected: nil,
140+
},
141+
{
142+
name: "WaveSplit",
143+
input: `{"field": "value1~value2~value3"}`,
144+
expected: []string{"value1", "value2", "value3"},
145+
},
146+
}
147+
for _, tt := range tests {
148+
t.Run(tt.name, func(t *testing.T) {
149+
var ts TestStruct
150+
err := json.Unmarshal([]byte(tt.input), &ts)
151+
if err != nil {
152+
t.Fatalf("Unmarshal failed: %v", err)
153+
}
154+
if !slices.Equal([]string(ts.Field), tt.expected) {
155+
t.Errorf("Expected %v, got %v", tt.expected, ts.Field)
156+
}
157+
})
158+
}
159+
}

infra/conf/observatory.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import (
66
"github.com/xtls/xray-core/app/observatory"
77
"github.com/xtls/xray-core/app/observatory/burst"
88
"github.com/xtls/xray-core/common/errors"
9-
"github.com/xtls/xray-core/infra/conf/cfgcommon/duration"
9+
"github.com/xtls/xray-core/infra/conf/cfgcommon/types"
1010
)
1111

1212
type ObservatoryConfig struct {
13-
SubjectSelector []string `json:"subjectSelector"`
14-
ProbeURL string `json:"probeURL"`
15-
ProbeInterval duration.Duration `json:"probeInterval"`
16-
EnableConcurrency bool `json:"enableConcurrency"`
13+
SubjectSelector []string `json:"subjectSelector"`
14+
ProbeURL string `json:"probeURL"`
15+
ProbeInterval types.Duration `json:"probeInterval"`
16+
EnableConcurrency bool `json:"enableConcurrency"`
1717
}
1818

1919
func (o *ObservatoryConfig) Build() (proto.Message, error) {

infra/conf/router_strategy.go

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

33
import (
4-
"google.golang.org/protobuf/proto"
54
"strings"
65

6+
"google.golang.org/protobuf/proto"
7+
78
"github.com/xtls/xray-core/app/observatory/burst"
89
"github.com/xtls/xray-core/app/router"
9-
"github.com/xtls/xray-core/infra/conf/cfgcommon/duration"
10+
"github.com/xtls/xray-core/infra/conf/cfgcommon/types"
1011
)
1112

1213
const (
@@ -36,23 +37,23 @@ type strategyLeastLoadConfig struct {
3637
// weight settings
3738
Costs []*router.StrategyWeight `json:"costs,omitempty"`
3839
// ping rtt baselines
39-
Baselines []duration.Duration `json:"baselines,omitempty"`
40+
Baselines []types.Duration `json:"baselines,omitempty"`
4041
// expected nodes count to select
4142
Expected int32 `json:"expected,omitempty"`
4243
// max acceptable rtt, filter away high delay nodes. default 0
43-
MaxRTT duration.Duration `json:"maxRTT,omitempty"`
44+
MaxRTT types.Duration `json:"maxRTT,omitempty"`
4445
// acceptable failure rate
4546
Tolerance float64 `json:"tolerance,omitempty"`
4647
}
4748

4849
// healthCheckSettings holds settings for health Checker
4950
type healthCheckSettings struct {
50-
Destination string `json:"destination"`
51-
Connectivity string `json:"connectivity"`
52-
Interval duration.Duration `json:"interval"`
53-
SamplingCount int `json:"sampling"`
54-
Timeout duration.Duration `json:"timeout"`
55-
HttpMethod string `json:"httpMethod"`
51+
Destination string `json:"destination"`
52+
Connectivity string `json:"connectivity"`
53+
Interval types.Duration `json:"interval"`
54+
SamplingCount int `json:"sampling"`
55+
Timeout types.Duration `json:"timeout"`
56+
HttpMethod string `json:"httpMethod"`
5657
}
5758

5859
func (h healthCheckSettings) Build() (proto.Message, error) {

0 commit comments

Comments
 (0)