Skip to content

Commit 3f64157

Browse files
Implement factory.client and client.getTreatment methods
1 parent eb71ff5 commit 3f64157

File tree

4 files changed

+196
-9
lines changed

4 files changed

+196
-9
lines changed

splitio_web/lib/splitio_web.dart

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class SplitioWeb extends SplitioPlatform {
2525
String? _trafficType;
2626
bool _impressionListener = false;
2727

28+
final Map<String, JS_IBrowserClient> _clients = {};
29+
2830
@override
2931
Future<void> init({
3032
required String apiKey,
@@ -107,7 +109,8 @@ class SplitioWeb extends SplitioPlatform {
107109
}.toJS;
108110

109111
script.onerror = (Event event) {
110-
completer.completeError(Exception('Failed to load Split SDK'));
112+
completer.completeError(
113+
Exception('Failed to load Split SDK, with error: $event'));
111114
}.toJS;
112115

113116
document.head!.appendChild(script);
@@ -311,4 +314,103 @@ class SplitioWeb extends SplitioPlatform {
311314
}
312315
return matchingKey.toJS;
313316
}
317+
318+
static String _buildKeyString(String matchingKey, String? bucketingKey) {
319+
return bucketingKey == null ? matchingKey : '${matchingKey}_$bucketingKey';
320+
}
321+
322+
@override
323+
Future<void> getClient({
324+
required String matchingKey,
325+
required String? bucketingKey,
326+
}) async {
327+
await this._initFuture;
328+
329+
final key = _buildKeyString(matchingKey, bucketingKey);
330+
331+
if (_clients.containsKey(key)) {
332+
return;
333+
}
334+
335+
final client = this._factory.client.callAsFunction(
336+
null, _buildKey(matchingKey, bucketingKey)) as JS_IBrowserClient;
337+
338+
_clients[key] = client;
339+
}
340+
341+
Future<JS_IBrowserClient> _getClient({
342+
required String matchingKey,
343+
required String? bucketingKey,
344+
}) async {
345+
await getClient(matchingKey: matchingKey, bucketingKey: bucketingKey);
346+
347+
final key = _buildKeyString(matchingKey, bucketingKey);
348+
349+
return _clients[key]!;
350+
}
351+
352+
JSAny? _convertValue(dynamic value, bool attributes) {
353+
if (value is bool) return value.toJS;
354+
if (value is num) return value.toJS; // covers int + double
355+
if (value is String) return value.toJS;
356+
357+
// properties do not support lists and sets
358+
if (attributes) {
359+
if (value is List) return value.jsify();
360+
if (value is Set) return value.jsify();
361+
}
362+
363+
return null;
364+
}
365+
366+
JSObject _convertMap(Map<String, dynamic> dartMap, bool areAttributes) {
367+
final jsMap = JSObject();
368+
369+
dartMap.forEach((key, value) {
370+
final jsValue = _convertValue(value, areAttributes);
371+
372+
if (jsValue != null) {
373+
jsMap.setProperty(key.toJS, jsValue);
374+
} else {
375+
this._factory.settings.log.warn.callAsFunction(
376+
null,
377+
'Invalid ${areAttributes ? 'attribute' : 'property'} value: $value, for key: $key, will be ignored'
378+
.toJS);
379+
}
380+
});
381+
382+
return jsMap;
383+
}
384+
385+
JSObject _convertEvaluationOptions(EvaluationOptions evaluationOptions) {
386+
final jsEvalOptions = JSObject();
387+
388+
if (evaluationOptions.properties.isNotEmpty) {
389+
jsEvalOptions.setProperty(
390+
'properties'.toJS, _convertMap(evaluationOptions.properties, false));
391+
}
392+
393+
return jsEvalOptions;
394+
}
395+
396+
@override
397+
Future<String> getTreatment({
398+
required String matchingKey,
399+
required String? bucketingKey,
400+
required String splitName,
401+
Map<String, dynamic> attributes = const {},
402+
EvaluationOptions evaluationOptions = const EvaluationOptions.empty(),
403+
}) async {
404+
final client = await _getClient(
405+
matchingKey: matchingKey,
406+
bucketingKey: bucketingKey,
407+
);
408+
409+
final result = client.getTreatment.callAsFunction(
410+
null,
411+
splitName.toJS,
412+
_convertMap(attributes, true),
413+
_convertEvaluationOptions(evaluationOptions)) as JSString;
414+
return result.toDart;
415+
}
314416
}

splitio_web/lib/src/js_interop.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@ extension type JS_ISettings._(JSObject _) implements JSObject {
1010
external JS_Logger log;
1111
}
1212

13+
@JS()
14+
extension type JS_IBrowserClient._(JSObject _) implements JSObject {
15+
external JSFunction getTreatment;
16+
}
17+
1318
@JS()
1419
extension type JS_IBrowserSDK._(JSObject _) implements JSObject {
20+
external JSFunction client;
1521
external JS_ISettings settings;
1622
}
1723

splitio_web/test/splitio_web_test.dart

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,105 @@ extension on web.Window {
1616
}
1717

1818
void main() {
19-
final List<({String methodName, List<dynamic> methodArguments})> calls = [];
19+
final List<({String methodName, List<JSAny?> methodArguments})> calls = [];
20+
21+
final mockClient = JSObject();
22+
mockClient['getTreatment'] =
23+
(JSAny? flagName, JSAny? attributes, JSAny? evaluationOptions) {
24+
calls.add((
25+
methodName: 'getTreatment',
26+
methodArguments: [flagName, attributes, evaluationOptions]
27+
));
28+
return 'on'.toJS;
29+
}.toJS;
2030

2131
final mockLog = JSObject();
2232
mockLog['warn'] = (JSAny? arg1) {
2333
calls.add((methodName: 'warn', methodArguments: [arg1]));
2434
}.toJS;
35+
2536
final mockSettings = JSObject();
2637
mockSettings['log'] = mockLog;
2738

2839
final mockFactory = JSObject();
2940
mockFactory['settings'] = mockSettings;
41+
mockFactory['client'] = (JSAny? splitKey) {
42+
calls.add((methodName: 'client', methodArguments: [splitKey]));
43+
return mockClient;
44+
}.toJS;
3045

3146
final mockSplitio = JSObject();
3247
mockSplitio['SplitFactory'] = (JSAny? arg1) {
3348
calls.add((methodName: 'SplitFactory', methodArguments: [arg1]));
3449
return mockFactory;
3550
}.toJS;
3651

52+
SplitioWeb _platform = SplitioWeb();
53+
3754
setUp(() {
38-
(web.window as JSObject).setProperty('splitio'.toJS, mockSplitio);
55+
(web.window as JSObject)['splitio'] = mockSplitio;
56+
57+
_platform.init(
58+
apiKey: 'apiKey',
59+
matchingKey: 'matching-key',
60+
bucketingKey: 'bucketing-key');
61+
});
62+
63+
group('evaluation', () {
64+
test('getTreatment without attributes', () async {
65+
final result = await _platform.getTreatment(
66+
matchingKey: 'matching-key',
67+
bucketingKey: 'bucketing-key',
68+
splitName: 'split');
69+
70+
expect(result, 'on');
71+
expect(calls.last.methodName, 'getTreatment');
72+
expect(calls.last.methodArguments.map(jsAnyToDart), ['split', {}, {}]);
73+
});
74+
75+
test('getTreatment with attributes', () async {
76+
final result = await _platform.getTreatment(
77+
matchingKey: 'matching-key',
78+
bucketingKey: 'bucketing-key',
79+
splitName: 'split',
80+
attributes: {
81+
'attrBool': true,
82+
'attrString': 'value',
83+
'attrInt': 1,
84+
'attrDouble': 1.1,
85+
'attrList': ['value1', 100, false],
86+
'attrSet': {'value3', 100, true},
87+
'attrNull': null, // ignored
88+
'attrInvalid': {'value5': true} // ignored
89+
});
90+
91+
expect(result, 'on');
92+
expect(calls.last.methodName, 'getTreatment');
93+
expect(calls.last.methodArguments.map(jsAnyToDart), [
94+
'split',
95+
{
96+
'attrBool': true,
97+
'attrString': 'value',
98+
'attrInt': 1,
99+
'attrDouble': 1.1,
100+
'attrList': ['value1', 100, false],
101+
'attrSet': ['value3', 100, true]
102+
},
103+
{}
104+
]);
105+
106+
// assert warnings
107+
expect(calls[calls.length - 2].methodName, 'warn');
108+
expect(
109+
jsAnyToDart(calls[calls.length - 2].methodArguments[0]),
110+
equals(
111+
'Invalid attribute value: {value5: true}, for key: attrInvalid, will be ignored'));
112+
expect(calls[calls.length - 3].methodName, 'warn');
113+
expect(
114+
jsAnyToDart(calls[calls.length - 3].methodArguments[0]),
115+
equals(
116+
'Invalid attribute value: null, for key: attrNull, will be ignored'));
117+
});
39118
});
40119

41120
group('initialization', () {
@@ -47,7 +126,7 @@ void main() {
47126

48127
expect(calls.last.methodName, 'SplitFactory');
49128
expect(
50-
jsObjectToMap(calls.last.methodArguments[0]),
129+
jsAnyToDart(calls.last.methodArguments[0]),
51130
equals({
52131
'core': {
53132
'authorizationKey': 'api-key',
@@ -66,7 +145,7 @@ void main() {
66145

67146
expect(calls.last.methodName, 'SplitFactory');
68147
expect(
69-
jsObjectToMap(calls.last.methodArguments[0]),
148+
jsAnyToDart(calls.last.methodArguments[0]),
70149
equals({
71150
'core': {
72151
'authorizationKey': 'api-key',
@@ -89,7 +168,7 @@ void main() {
89168

90169
expect(calls.last.methodName, 'SplitFactory');
91170
expect(
92-
jsObjectToMap(calls.last.methodArguments[0]),
171+
jsAnyToDart(calls.last.methodArguments[0]),
93172
equals({
94173
'core': {
95174
'authorizationKey': 'api-key',
@@ -152,7 +231,7 @@ void main() {
152231

153232
expect(calls[calls.length - 5].methodName, 'SplitFactory');
154233
expect(
155-
jsObjectToMap(calls[calls.length - 5].methodArguments[0]),
234+
jsAnyToDart(calls[calls.length - 5].methodArguments[0]),
156235
equals({
157236
'core': {
158237
'authorizationKey': 'api-key',
@@ -241,7 +320,7 @@ void main() {
241320

242321
expect(calls.last.methodName, 'SplitFactory');
243322
expect(
244-
jsObjectToMap(calls.last.methodArguments[0]),
323+
jsAnyToDart(calls.last.methodArguments[0]),
245324
equals({
246325
'core': {
247326
'authorizationKey': 'api-key',

splitio_web/test/utils/js_interop_test_utils.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ dynamic jsAnyToDart(JSAny? value) {
2525
} else if (value is JSString) {
2626
return value.toDart;
2727
} else if (value is JSNumber) {
28-
return value.toDartInt;
28+
return value.toDartDouble;
2929
} else if (value is JSBoolean) {
3030
return value.toDart;
3131
} else {

0 commit comments

Comments
 (0)