Skip to content

Commit b56f0e2

Browse files
authored
Merge pull request #238 from splitio/spec-proxy
Spec proxy
2 parents 9432408 + a66ed77 commit b56f0e2

File tree

18 files changed

+251
-132
lines changed

18 files changed

+251
-132
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
6.8.1 (Nov 10, 2025)
2+
- Fixed to use an old proxy with 1.3 spec.
3+
14
6.8.0 (Sep 26, 2025)
25
- Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK.
36
- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules.

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/splitio/go-client/v6
33
go 1.18
44

55
require (
6-
github.com/splitio/go-split-commons/v7 v7.0.1-0.20250930213118-b0b22c397fc4
7-
github.com/splitio/go-toolkit/v5 v5.4.1-0.20250930172659-38274b802d99
6+
github.com/splitio/go-split-commons/v8 v8.0.0
7+
github.com/splitio/go-toolkit/v5 v5.4.1
88
github.com/stretchr/testify v1.11.1
99
)
1010

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
1818
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1919
github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc=
2020
github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
21-
github.com/splitio/go-split-commons/v7 v7.0.1-0.20250930213118-b0b22c397fc4 h1:OK9LLRmEpLghXM5paCrR9zFXTuYTdoiP2P2apwW3C9E=
22-
github.com/splitio/go-split-commons/v7 v7.0.1-0.20250930213118-b0b22c397fc4/go.mod h1:Lsj2n1zm88laFRu+JhlNeXW0x1ndtjQ1H21rLhRFfOs=
23-
github.com/splitio/go-toolkit/v5 v5.4.1-0.20250930172659-38274b802d99 h1:rQo355F9JbdyTMz2X5MU+FeRvkT6rvD1n+GnXdJr33A=
24-
github.com/splitio/go-toolkit/v5 v5.4.1-0.20250930172659-38274b802d99/go.mod h1:SifzysrOVDbzMcOE8zjX02+FG5az4FrR3Us/i5SeStw=
21+
github.com/splitio/go-split-commons/v8 v8.0.0 h1:wLk5eT6WU2LfxtaWG3ZHlTbNMGWP2eYsZTb1o+tFpkI=
22+
github.com/splitio/go-split-commons/v8 v8.0.0/go.mod h1:vgRGPn0s4RC9/zp1nIn4KeeIEj/K3iXE2fxYQbCk/WI=
23+
github.com/splitio/go-toolkit/v5 v5.4.1 h1:srTyvDBJZMUcJ/KiiQDMyjCuELVgTBh2TGRVn0sOXEE=
24+
github.com/splitio/go-toolkit/v5 v5.4.1/go.mod h1:SifzysrOVDbzMcOE8zjX02+FG5az4FrR3Us/i5SeStw=
2525
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
2626
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
2727
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=

splitio/client/client.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import (
1111
"github.com/splitio/go-client/v6/splitio/conf"
1212
impressionlistener "github.com/splitio/go-client/v6/splitio/impressionListener"
1313

14-
"github.com/splitio/go-split-commons/v7/dtos"
15-
"github.com/splitio/go-split-commons/v7/engine/evaluator"
16-
"github.com/splitio/go-split-commons/v7/engine/evaluator/impressionlabels"
17-
"github.com/splitio/go-split-commons/v7/flagsets"
18-
"github.com/splitio/go-split-commons/v7/provisional"
19-
"github.com/splitio/go-split-commons/v7/storage"
20-
"github.com/splitio/go-split-commons/v7/telemetry"
14+
"github.com/splitio/go-split-commons/v8/dtos"
15+
"github.com/splitio/go-split-commons/v8/engine/evaluator"
16+
"github.com/splitio/go-split-commons/v8/engine/evaluator/impressionlabels"
17+
"github.com/splitio/go-split-commons/v8/flagsets"
18+
"github.com/splitio/go-split-commons/v8/provisional"
19+
"github.com/splitio/go-split-commons/v8/storage"
20+
"github.com/splitio/go-split-commons/v8/telemetry"
2121
"github.com/splitio/go-toolkit/v5/logging"
2222
)
2323

splitio/client/client_test.go

Lines changed: 151 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,26 @@ import (
1717
"github.com/splitio/go-client/v6/splitio"
1818
"github.com/splitio/go-client/v6/splitio/conf"
1919
impressionlistener "github.com/splitio/go-client/v6/splitio/impressionListener"
20-
21-
commonsCfg "github.com/splitio/go-split-commons/v7/conf"
22-
"github.com/splitio/go-split-commons/v7/dtos"
23-
"github.com/splitio/go-split-commons/v7/engine/evaluator"
24-
"github.com/splitio/go-split-commons/v7/engine/evaluator/impressionlabels"
25-
evaluatorMock "github.com/splitio/go-split-commons/v7/engine/evaluator/mocks"
26-
"github.com/splitio/go-split-commons/v7/healthcheck/application"
27-
"github.com/splitio/go-split-commons/v7/provisional"
28-
"github.com/splitio/go-split-commons/v7/provisional/strategy"
29-
authMocks "github.com/splitio/go-split-commons/v7/service/mocks"
30-
"github.com/splitio/go-split-commons/v7/storage"
31-
"github.com/splitio/go-split-commons/v7/storage/inmemory"
32-
"github.com/splitio/go-split-commons/v7/storage/inmemory/mutexqueue"
33-
"github.com/splitio/go-split-commons/v7/storage/mocks"
34-
"github.com/splitio/go-split-commons/v7/storage/redis"
35-
"github.com/splitio/go-split-commons/v7/synchronizer"
36-
syncMock "github.com/splitio/go-split-commons/v7/synchronizer/mocks"
37-
"github.com/splitio/go-split-commons/v7/telemetry"
38-
"github.com/splitio/go-split-commons/v7/util"
20+
"github.com/stretchr/testify/assert"
21+
22+
commonsCfg "github.com/splitio/go-split-commons/v8/conf"
23+
"github.com/splitio/go-split-commons/v8/dtos"
24+
"github.com/splitio/go-split-commons/v8/engine/evaluator"
25+
"github.com/splitio/go-split-commons/v8/engine/evaluator/impressionlabels"
26+
evaluatorMock "github.com/splitio/go-split-commons/v8/engine/evaluator/mocks"
27+
"github.com/splitio/go-split-commons/v8/healthcheck/application"
28+
"github.com/splitio/go-split-commons/v8/provisional"
29+
"github.com/splitio/go-split-commons/v8/provisional/strategy"
30+
authMocks "github.com/splitio/go-split-commons/v8/service/mocks"
31+
"github.com/splitio/go-split-commons/v8/storage"
32+
"github.com/splitio/go-split-commons/v8/storage/inmemory"
33+
"github.com/splitio/go-split-commons/v8/storage/inmemory/mutexqueue"
34+
"github.com/splitio/go-split-commons/v8/storage/mocks"
35+
"github.com/splitio/go-split-commons/v8/storage/redis"
36+
"github.com/splitio/go-split-commons/v8/synchronizer"
37+
syncMock "github.com/splitio/go-split-commons/v8/synchronizer/mocks"
38+
"github.com/splitio/go-split-commons/v8/telemetry"
39+
"github.com/splitio/go-split-commons/v8/util"
3940

4041
"github.com/splitio/go-toolkit/v5/datastructures/set"
4142
"github.com/splitio/go-toolkit/v5/logging"
@@ -1005,26 +1006,30 @@ func TestBlockUntilReadyInMemoryOk(t *testing.T) {
10051006
mockedSplit3 := dtos.SplitDTO{Name: "split3", Killed: true, Status: "INACTIVE"}
10061007

10071008
sdkServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1008-
time.Sleep(3 * time.Second)
1009-
if r.URL.Path != "/splitChanges" || r.Method != "GET" {
1010-
t.Error("Invalid request. Should be GET to /splitChanges")
1011-
}
1009+
switch r.URL.Path {
1010+
case "/version":
1011+
w.WriteHeader(http.StatusOK)
1012+
case "/splitChanges":
1013+
time.Sleep(3 * time.Second)
1014+
splitChanges := dtos.RuleChangesDTO{
1015+
FeatureFlags: dtos.FeatureFlagsDTO{
1016+
Splits: []dtos.SplitDTO{mockedSplit1, mockedSplit2, mockedSplit3},
1017+
Since: 3,
1018+
Till: 3,
1019+
},
1020+
}
10121021

1013-
splitChanges := dtos.SplitChangesDTO{
1014-
FeatureFlags: dtos.FeatureFlagsDTO{
1015-
Splits: []dtos.SplitDTO{mockedSplit1, mockedSplit2, mockedSplit3},
1016-
Since: 3,
1017-
Till: 3,
1018-
},
1019-
}
1022+
raw, err := json.Marshal(splitChanges)
1023+
if err != nil {
1024+
t.Error("Error building json")
1025+
return
1026+
}
10201027

1021-
raw, err := json.Marshal(splitChanges)
1022-
if err != nil {
1023-
t.Error("Error building json")
1024-
return
1028+
w.Write(raw)
1029+
default:
1030+
t.Error("Unexpected path")
10251031
}
10261032

1027-
w.Write(raw)
10281033
}))
10291034
defer sdkServer.Close()
10301035

@@ -1150,7 +1155,7 @@ func TestBlockUntilReadyInMemoryOk(t *testing.T) {
11501155

11511156
err = client.BlockUntilReady(2)
11521157
if err != nil {
1153-
t.Error("Wrong message error")
1158+
t.Error("Wrong message error", err.Error())
11541159
}
11551160

11561161
if !client.factory.IsReady() || !manager.factory.IsReady() {
@@ -2443,7 +2448,7 @@ func TestTelemetryMemory(t *testing.T) {
24432448

24442449
sdkServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24452450
time.Sleep(100 * time.Millisecond)
2446-
splitChanges := dtos.SplitChangesDTO{
2451+
splitChanges := dtos.RuleChangesDTO{
24472452
FeatureFlags: dtos.FeatureFlagsDTO{
24482453
Splits: []dtos.SplitDTO{
24492454
{Name: "split1", Killed: true, Status: "ACTIVE", DefaultTreatment: "on"},
@@ -3111,6 +3116,115 @@ func TestUnsupportedandSemverMatcherRedis(t *testing.T) {
31113116
}
31123117
}
31133118

3119+
var splitRuleBased = &dtos.SplitDTO{
3120+
Algo: 2,
3121+
ChangeNumber: 1494593336752,
3122+
DefaultTreatment: "off",
3123+
Killed: false,
3124+
Name: "rbsplit",
3125+
Seed: -1992295819,
3126+
Status: "ACTIVE",
3127+
TrafficAllocation: 100,
3128+
TrafficAllocationSeed: -285565213,
3129+
TrafficTypeName: "user",
3130+
Configurations: map[string]string{"on": "{\"color\": \"blue\",\"size\": 13}"},
3131+
Conditions: []dtos.ConditionDTO{
3132+
{
3133+
ConditionType: "ROLLOUT",
3134+
Label: "default rule",
3135+
MatcherGroup: dtos.MatcherGroupDTO{
3136+
Combiner: "AND",
3137+
Matchers: []dtos.MatcherDTO{
3138+
{
3139+
KeySelector: &dtos.KeySelectorDTO{
3140+
TrafficType: "user",
3141+
},
3142+
MatcherType: "IN_RULE_BASED_SEGMENT",
3143+
UserDefinedSegment: &dtos.UserDefinedSegmentMatcherDataDTO{
3144+
SegmentName: "rbsegment1",
3145+
},
3146+
Negate: false,
3147+
},
3148+
},
3149+
},
3150+
Partitions: []dtos.PartitionDTO{
3151+
{
3152+
Size: 100,
3153+
Treatment: "on",
3154+
},
3155+
{
3156+
Size: 0,
3157+
Treatment: "off",
3158+
},
3159+
},
3160+
},
3161+
},
3162+
}
3163+
3164+
var rbsegment1 = &dtos.RuleBasedSegmentDTO{
3165+
Name: "rbsegment1",
3166+
Conditions: []dtos.RuleBasedConditionDTO{
3167+
{
3168+
MatcherGroup: dtos.MatcherGroupDTO{
3169+
Combiner: "AND",
3170+
Matchers: []dtos.MatcherDTO{
3171+
{
3172+
KeySelector: &dtos.KeySelectorDTO{
3173+
TrafficType: "user",
3174+
Attribute: &attribute,
3175+
},
3176+
MatcherType: "EQUAL_TO_SEMVER",
3177+
String: &semver,
3178+
Whitelist: nil,
3179+
Negate: false,
3180+
},
3181+
},
3182+
},
3183+
},
3184+
},
3185+
TrafficTypeName: "user",
3186+
}
3187+
3188+
func TestRuleBasedSegmentRedis(t *testing.T) {
3189+
redisConfig := &commonsCfg.RedisConfig{
3190+
Host: "localhost",
3191+
Port: 6379,
3192+
Password: "",
3193+
Prefix: "test-prefix-rulebased",
3194+
}
3195+
3196+
prefixedClient, _ := redis.NewRedisClient(redisConfig, logging.NewLogger(&logging.LoggerOptions{}))
3197+
// Clean redis
3198+
defer func() {
3199+
keys, _ := prefixedClient.Keys("test-prefix-rulebased*")
3200+
for _, k := range keys {
3201+
prefixedClient.Del(k)
3202+
}
3203+
}()
3204+
raw, _ := json.Marshal(*splitRuleBased)
3205+
prefixedClient.Set("SPLITIO.split.rbsplit", raw, 0)
3206+
rbraw, _ := json.Marshal(*rbsegment1)
3207+
prefixedClient.Set("SPLITIO.rbsegment.rbsegment1", rbraw, 0)
3208+
3209+
impTest := &ImpressionListenerTest{}
3210+
cfg := conf.Default()
3211+
cfg.LabelsEnabled = true
3212+
cfg.Advanced.ImpressionListener = impTest
3213+
cfg.ImpressionsMode = commonsCfg.ImpressionsModeOptimized
3214+
cfg.OperationMode = conf.RedisConsumer
3215+
cfg.Redis = *redisConfig
3216+
3217+
factory, _ := NewSplitFactory("test", cfg)
3218+
client := factory.Client()
3219+
3220+
// Calls treatments to generate one valid impression
3221+
attributes := make(map[string]interface{})
3222+
attributes["version"] = "3.4.5"
3223+
evaluation := client.Treatment("user1", "rbsplit", attributes)
3224+
assert.Equal(t, "on", evaluation, "evaluation for rbsplit should be on")
3225+
client.Destroy()
3226+
}
3227+
31143228
func TestPrerequisites(t *testing.T) {
31153229
var isDestroyCalled = false
31163230
var splitsMock, _ = ioutil.ReadFile("../../testdata/splits_mock_5.json")

splitio/client/factory.go

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,30 @@ import (
1515
impressionlistener "github.com/splitio/go-client/v6/splitio/impressionListener"
1616
"github.com/splitio/go-client/v6/splitio/impressions"
1717

18-
config "github.com/splitio/go-split-commons/v7/conf"
19-
"github.com/splitio/go-split-commons/v7/dtos"
20-
"github.com/splitio/go-split-commons/v7/engine"
21-
"github.com/splitio/go-split-commons/v7/engine/evaluator"
22-
"github.com/splitio/go-split-commons/v7/engine/grammar"
23-
"github.com/splitio/go-split-commons/v7/flagsets"
24-
"github.com/splitio/go-split-commons/v7/healthcheck/application"
25-
"github.com/splitio/go-split-commons/v7/provisional"
26-
"github.com/splitio/go-split-commons/v7/provisional/strategy"
27-
"github.com/splitio/go-split-commons/v7/service/api"
28-
"github.com/splitio/go-split-commons/v7/service/api/specs"
29-
"github.com/splitio/go-split-commons/v7/service/local"
30-
"github.com/splitio/go-split-commons/v7/storage"
31-
"github.com/splitio/go-split-commons/v7/storage/inmemory"
32-
"github.com/splitio/go-split-commons/v7/storage/inmemory/mutexmap"
33-
"github.com/splitio/go-split-commons/v7/storage/inmemory/mutexqueue"
34-
"github.com/splitio/go-split-commons/v7/storage/mocks"
35-
"github.com/splitio/go-split-commons/v7/storage/redis"
36-
"github.com/splitio/go-split-commons/v7/synchronizer"
37-
"github.com/splitio/go-split-commons/v7/synchronizer/worker/event"
38-
"github.com/splitio/go-split-commons/v7/synchronizer/worker/segment"
39-
"github.com/splitio/go-split-commons/v7/synchronizer/worker/split"
40-
"github.com/splitio/go-split-commons/v7/tasks"
41-
"github.com/splitio/go-split-commons/v7/telemetry"
18+
config "github.com/splitio/go-split-commons/v8/conf"
19+
"github.com/splitio/go-split-commons/v8/dtos"
20+
"github.com/splitio/go-split-commons/v8/engine"
21+
"github.com/splitio/go-split-commons/v8/engine/evaluator"
22+
"github.com/splitio/go-split-commons/v8/engine/grammar"
23+
"github.com/splitio/go-split-commons/v8/flagsets"
24+
"github.com/splitio/go-split-commons/v8/healthcheck/application"
25+
"github.com/splitio/go-split-commons/v8/provisional"
26+
"github.com/splitio/go-split-commons/v8/provisional/strategy"
27+
"github.com/splitio/go-split-commons/v8/service/api"
28+
"github.com/splitio/go-split-commons/v8/service/api/specs"
29+
"github.com/splitio/go-split-commons/v8/service/local"
30+
"github.com/splitio/go-split-commons/v8/storage"
31+
"github.com/splitio/go-split-commons/v8/storage/inmemory"
32+
"github.com/splitio/go-split-commons/v8/storage/inmemory/mutexmap"
33+
"github.com/splitio/go-split-commons/v8/storage/inmemory/mutexqueue"
34+
"github.com/splitio/go-split-commons/v8/storage/mocks"
35+
"github.com/splitio/go-split-commons/v8/storage/redis"
36+
"github.com/splitio/go-split-commons/v8/synchronizer"
37+
"github.com/splitio/go-split-commons/v8/synchronizer/worker/event"
38+
"github.com/splitio/go-split-commons/v8/synchronizer/worker/segment"
39+
"github.com/splitio/go-split-commons/v8/synchronizer/worker/split"
40+
"github.com/splitio/go-split-commons/v8/tasks"
41+
"github.com/splitio/go-split-commons/v8/telemetry"
4242
"github.com/splitio/go-toolkit/v5/logging"
4343
)
4444

@@ -124,7 +124,7 @@ func (f *SplitFactory) IsReady() bool {
124124

125125
// initializates tasks for in-memory mode
126126
func (f *SplitFactory) initializationManager(readyChannel chan int, flagSetsInvalid int64) {
127-
go f.syncManager.StartBGSyng(readyChannel, f.cfg.Advanced.RetryEnabled, func() {
127+
go f.syncManager.StartBGSync(readyChannel, f.cfg.Advanced.RetryEnabled, func() {
128128
f.broadcastReadiness(sdkStatusReady, make([]string, 0), flagSetsInvalid)
129129
})
130130
}
@@ -296,10 +296,11 @@ func setupInMemoryFactory(
296296

297297
splitAPI := api.NewSplitAPI(apikey, advanced, logger, metadata)
298298

299+
isProxy := splitAPI.SplitFetcher.IsProxy()
299300
evaluator := evaluator.NewEvaluator(splitsStorage, segmentsStorage, ruleBasedSegmentStorage, nil, engine.NewEngine(logger), logger, cfg.Advanced.FeatureFlagRules, cfg.Advanced.RuleBasedSegmentRules)
300301
ruleBuilder := grammar.NewRuleBuilder(segmentsStorage, ruleBasedSegmentStorage, nil, cfg.Advanced.FeatureFlagRules, cfg.Advanced.RuleBasedSegmentRules, logger, evaluator)
301302
workers := synchronizer.Workers{
302-
SplitUpdater: split.NewSplitUpdater(splitsStorage, ruleBasedSegmentStorage, splitAPI.SplitFetcher, logger, telemetryStorage, dummyHC, flagSetFilter, ruleBuilder),
303+
SplitUpdater: split.NewSplitUpdater(splitsStorage, ruleBasedSegmentStorage, splitAPI.SplitFetcher, logger, telemetryStorage, dummyHC, flagSetFilter, ruleBuilder, isProxy, advanced.FlagsSpecVersion),
303304
SegmentUpdater: segment.NewSegmentUpdater(splitsStorage, segmentsStorage, ruleBasedSegmentStorage, splitAPI.SegmentFetcher, logger, telemetryStorage, dummyHC),
304305
EventRecorder: event.NewEventRecorderSingle(eventsStorage, splitAPI.EventRecorder, logger, metadata, telemetryStorage),
305306
TelemetryRecorder: telemetry.NewTelemetrySynchronizer(telemetryStorage, splitAPI.TelemetryRecorder, splitsStorage, segmentsStorage, logger, metadata, telemetryStorage),
@@ -404,6 +405,7 @@ func setupRedisFactory(apikey string, cfg *conf.SplitSdkConfig, logger logging.L
404405
storages := sdkStorages{
405406
splits: redis.NewSplitStorage(redisClient, logger, flagSetFilter),
406407
segments: redis.NewSegmentStorage(redisClient, logger),
408+
ruleBasedSegments: redis.NewRuleBasedStorage(redisClient, logger),
407409
impressionsConsumer: impressionStorage,
408410
impressions: impressionStorage,
409411
events: redis.NewEventsStorage(redisClient, metadata, logger),

splitio/client/factory_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package client
33
import (
44
"testing"
55

6-
"github.com/splitio/go-split-commons/v7/flagsets"
6+
"github.com/splitio/go-split-commons/v8/flagsets"
77
"github.com/splitio/go-toolkit/v5/logging/mocks"
88

99
"github.com/stretchr/testify/assert"

splitio/client/input_validator.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88
"strconv"
99
"strings"
1010

11-
"github.com/splitio/go-split-commons/v7/engine/evaluator/impressionlabels"
12-
"github.com/splitio/go-split-commons/v7/storage"
11+
"github.com/splitio/go-split-commons/v8/engine/evaluator/impressionlabels"
12+
"github.com/splitio/go-split-commons/v8/storage"
1313
"github.com/splitio/go-toolkit/v5/datastructures/set"
1414
"github.com/splitio/go-toolkit/v5/logging"
1515
)

0 commit comments

Comments
 (0)