diff --git a/splitio/example/pubspec.lock b/splitio/example/pubspec.lock index a3269a9..8c2bff7 100644 --- a/splitio/example/pubspec.lock +++ b/splitio/example/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -79,18 +79,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -111,34 +111,34 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" plugin_platform_interface: dependency: transitive description: @@ -151,29 +151,29 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" splitio: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.2.0" + version: "1.0.0-rc.1" splitio_android: dependency: transitive description: path: "../../splitio_android" relative: true source: path - version: "0.2.0" + version: "0.3.0-rc.1" splitio_ios: dependency: transitive description: @@ -187,47 +187,47 @@ packages: path: "../../splitio_platform_interface" relative: true source: path - version: "1.5.0" + version: "2.0.0-rc.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.4" vector_math: dependency: transitive description: @@ -240,10 +240,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "15.0.0" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/splitio/lib/split_client.dart b/splitio/lib/split_client.dart index cb9b27f..883a9f4 100644 --- a/splitio/lib/split_client.dart +++ b/splitio/lib/split_client.dart @@ -18,7 +18,8 @@ abstract class SplitClient { /// /// Returns the evaluated treatment, the default treatment of this feature flag, or 'control'. Future getTreatment(String featureFlagName, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Performs and evaluation and returns a [SplitResult] object for the /// [featureFlagName] feature flag. This object contains the treatment alongside the @@ -37,7 +38,8 @@ abstract class SplitClient { /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. Future getTreatmentWithConfig(String featureFlagName, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations. Returns a [Map] in /// which the keys are feature flag names and the values are treatments. @@ -47,7 +49,8 @@ abstract class SplitClient { /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. Future> getTreatments(List featureFlagNames, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations by flag set. Returns a [Map] in /// which the keys are feature flag names and the values are treatments. @@ -57,7 +60,8 @@ abstract class SplitClient { /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. Future> getTreatmentsByFlagSet(String flagSet, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations by flag sets. Returns a [Map] in /// which the keys are feature flag names and the values are treatments. @@ -67,7 +71,8 @@ abstract class SplitClient { /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. Future> getTreatmentsByFlagSets(List flagSets, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations by flag set. Returns a [Map] in /// which the keys are feature flag names and the values are [SplitResult] objects. @@ -76,8 +81,10 @@ abstract class SplitClient { /// /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. - Future> getTreatmentsWithConfigByFlagSet(String flagSet, - [Map attributes = const {}]); + Future> getTreatmentsWithConfigByFlagSet( + String flagSet, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations by flag sets. Returns a [Map] in /// which the keys are feature flag names and the values are [SplitResult] objects. @@ -86,8 +93,10 @@ abstract class SplitClient { /// /// Optionally, a [Map] can be specified with the [attributes] parameter to /// take into account when evaluating. - Future> getTreatmentsWithConfigByFlagSets(List flagSets, - [Map attributes = const {}]); + Future> getTreatmentsWithConfigByFlagSets( + List flagSets, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Convenience method to perform multiple evaluations. Returns a [Map] in /// which the keys are feature flag names and the values are [SplitResult] objects. @@ -98,7 +107,8 @@ abstract class SplitClient { /// take into account when evaluating. Future> getTreatmentsWithConfig( List featureFlagNames, - [Map attributes = const {}]); + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]); /// Enqueue a new event to be sent to Split data collection services. /// @@ -186,79 +196,105 @@ class DefaultSplitClient implements SplitClient { @override Future getTreatment(String featureFlagName, - [Map attributes = const {}]) async { + [Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()]) async { return _platform.getTreatment( matchingKey: _matchingKey, bucketingKey: _bucketingKey, splitName: featureFlagName, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override Future getTreatmentWithConfig(String featureFlagName, - [Map attributes = const {}]) async { + [Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()]) async { return _platform.getTreatmentWithConfig( matchingKey: _matchingKey, bucketingKey: _bucketingKey, splitName: featureFlagName, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override Future> getTreatments(List featureFlagNames, - [Map attributes = const {}]) async { + [Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()]) async { return _platform.getTreatments( matchingKey: _matchingKey, bucketingKey: _bucketingKey, splitNames: featureFlagNames, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override Future> getTreatmentsWithConfig( List featureFlagNames, - [Map attributes = const {}]) async { + [Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()]) async { return _platform.getTreatmentsWithConfig( matchingKey: _matchingKey, bucketingKey: _bucketingKey, splitNames: featureFlagNames, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override - Future> getTreatmentsByFlagSet(String flagSet, [Map attributes = const {}]) { + Future> getTreatmentsByFlagSet(String flagSet, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]) { return _platform.getTreatmentsByFlagSet( matchingKey: _matchingKey, bucketingKey: _bucketingKey, flagSet: flagSet, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override - Future> getTreatmentsByFlagSets(List flagSets, [Map attributes = const {}]) { + Future> getTreatmentsByFlagSets(List flagSets, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]) { return _platform.getTreatmentsByFlagSets( matchingKey: _matchingKey, bucketingKey: _bucketingKey, flagSets: flagSets, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override - Future> getTreatmentsWithConfigByFlagSet(String flagSet, [Map attributes = const {}]) { + Future> getTreatmentsWithConfigByFlagSet( + String flagSet, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]) { return _platform.getTreatmentsWithConfigByFlagSet( matchingKey: _matchingKey, bucketingKey: _bucketingKey, flagSet: flagSet, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override - Future> getTreatmentsWithConfigByFlagSets(List flagSets, [Map attributes = const {}]) { + Future> getTreatmentsWithConfigByFlagSets( + List flagSets, + [Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()]) { return _platform.getTreatmentsWithConfigByFlagSets( matchingKey: _matchingKey, bucketingKey: _bucketingKey, flagSets: flagSets, - attributes: attributes); + attributes: attributes, + evaluationOptions: evaluationOptions); } @override diff --git a/splitio/pubspec.yaml b/splitio/pubspec.yaml index 73c522c..b771d85 100644 --- a/splitio/pubspec.yaml +++ b/splitio/pubspec.yaml @@ -1,7 +1,7 @@ publish_to: none # TODO name: splitio description: Official plugin for split.io, the platform for controlled rollouts, which serves features to your users via feature flags to manage your complete customer experience. -version: 0.2.0 +version: 1.0.0-rc.1 homepage: https://split.io/ repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio/ @@ -24,7 +24,7 @@ dependencies: path: ../splitio_android splitio_ios: # ^0.2.0 path: ../splitio_ios - splitio_platform_interface: # ^1.5.0 + splitio_platform_interface: # ^2.0.0-rc.1 path: ../splitio_platform_interface dev_dependencies: flutter_test: diff --git a/splitio/test/splitio_client_test.dart b/splitio/test/splitio_client_test.dart index de984a8..cac009a 100644 --- a/splitio/test/splitio_client_test.dart +++ b/splitio/test/splitio_client_test.dart @@ -21,6 +21,161 @@ void main() { _platform = SplitioPlatformStub(); }); + group('evaluationOptions tests', () { + test('getTreatment includes evaluationOptions when non-empty', () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatment('split', {}, eo); + + expect(_platform.methodName, 'getTreatment'); + expect(_platform.methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatmentWithConfig includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentWithConfig('split1', {}, eo); + + expect(_platform.methodName, 'getTreatmentWithConfig'); + expect(_platform.methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatments includes evaluationOptions when non-empty', () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatments(['split1', 'split2'], {}, eo); + + expect(_platform.methodName, 'getTreatments'); + expect(_platform.methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatmentsWithConfig includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentsWithConfig(['split1', 'split2'], {}, eo); + + expect(_platform.methodName, 'getTreatmentsWithConfig'); + expect(_platform.methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatmentsByFlagSet includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentsByFlagSet('set_1', {}, eo); + + expect(_platform.methodName, 'getTreatmentsByFlagSet'); + expect(_platform.methodArguments, { + 'flagSet': 'set_1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test('getTreatmentsByFlagSets includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentsByFlagSets(['set_1', 'set_2'], {}, eo); + + expect(_platform.methodName, 'getTreatmentsByFlagSets'); + expect(_platform.methodArguments, { + 'flagSets': ['set_1', 'set_2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test( + 'getTreatmentsWithConfigByFlagSet includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client.getTreatmentsWithConfigByFlagSet('set_1', {}, eo); + + expect(_platform.methodName, 'getTreatmentsWithConfigByFlagSet'); + expect(_platform.methodArguments, { + 'flagSet': 'set_1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + + test( + 'getTreatmentsWithConfigByFlagSets includes evaluationOptions when non-empty', + () async { + final client = _getClient(); + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + + await client + .getTreatmentsWithConfigByFlagSets(['set_1', 'set_2'], {}, eo); + + expect(_platform.methodName, 'getTreatmentsWithConfigByFlagSets'); + expect(_platform.methodArguments, { + 'flagSets': ['set_1', 'set_2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + } + }); + }); + }); + group('evaluation', () { test('getTreatment without attributes', () async { SplitClient client = _getClient(); @@ -235,7 +390,8 @@ void main() { test('getTreatmentsWithConfigByFlagSets with attributes', () async { SplitClient client = _getClient(); - client.getTreatmentsWithConfigByFlagSets(['set_1', 'set_2'], {'attr1': true}); + client.getTreatmentsWithConfigByFlagSets( + ['set_1', 'set_2'], {'attr1': true}); expect(_platform.methodName, 'getTreatmentsWithConfigByFlagSets'); expect(_platform.methodArguments, { diff --git a/splitio/test/splitio_platform_stub.dart b/splitio/test/splitio_platform_stub.dart index a996c97..001d7b3 100644 --- a/splitio/test/splitio_platform_stub.dart +++ b/splitio/test/splitio_platform_stub.dart @@ -1,5 +1,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:splitio_platform_interface/splitio_platform_interface.dart'; +import 'package:splitio_platform_interface/split_evaluation_options.dart'; class SplitioPlatformStub with MockPlatformInterfaceMixin @@ -89,7 +90,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatment'; methodArguments = { @@ -99,6 +101,10 @@ class SplitioPlatformStub 'attributes': attributes }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value(''); } @@ -107,7 +113,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentWithConfig'; methodArguments = { @@ -117,6 +124,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value(const SplitResult('on', null)); } @@ -125,7 +136,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatments'; methodArguments = { @@ -135,6 +147,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -143,7 +159,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsWithConfig'; methodArguments = { @@ -153,6 +170,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -161,7 +182,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsByFlagSet'; methodArguments = { @@ -171,6 +193,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -179,7 +205,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsByFlagSets'; methodArguments = { @@ -189,6 +216,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -197,7 +228,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsWithConfigByFlagSet'; methodArguments = { @@ -207,6 +239,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } @@ -215,7 +251,8 @@ class SplitioPlatformStub {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { methodName = 'getTreatmentsWithConfigByFlagSets'; methodArguments = { @@ -225,6 +262,10 @@ class SplitioPlatformStub 'attributes': attributes, }; + if (evaluationOptions.properties.isNotEmpty) { + methodArguments['evaluationOptions'] = evaluationOptions.toJson(); + } + return Future.value({}); } diff --git a/splitio_platform_interface/lib/method_channel_platform.dart b/splitio_platform_interface/lib/method_channel_platform.dart index d3fb11d..aaf0b2c 100644 --- a/splitio_platform_interface/lib/method_channel_platform.dart +++ b/splitio_platform_interface/lib/method_channel_platform.dart @@ -22,6 +22,19 @@ class MethodChannelPlatform extends SplitioPlatform { } } + Map _withEvalOptions( + String matchingKey, + String? bucketingKey, { + required Map base, + required EvaluationOptions evaluationOptions, + }) { + final args = Map.from(base); + if (evaluationOptions.properties.isNotEmpty) { + args['evaluationOptions'] = evaluationOptions.toJson(); + } + return _buildParameters(matchingKey, bucketingKey, args); + } + @override Future init( {required String apiKey, @@ -107,11 +120,19 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) async { - return await methodChannel.invokeMethod( - 'getTreatment', - _buildParameters(matchingKey, bucketingKey, - {'splitName': splitName, 'attributes': attributes})) ?? + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'splitName': splitName, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + return await methodChannel.invokeMethod('getTreatment', params) ?? _controlTreatment; } @@ -120,14 +141,23 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) async { - Map? treatment = (await methodChannel.invokeMapMethod( - 'getTreatmentWithConfig', - _buildParameters(matchingKey, bucketingKey, - {'splitName': splitName, 'attributes': attributes}))) - ?.entries - .first - .value; + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'splitName': splitName, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatment = + (await methodChannel.invokeMapMethod('getTreatmentWithConfig', params)) + ?.entries + .first + .value; if (treatment == null) { return _controlResult; } @@ -140,11 +170,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) async { - Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatments', - _buildParameters(matchingKey, bucketingKey, - {'splitName': splitNames, 'attributes': attributes})); + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'splitName': splitNames, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatments = + await methodChannel.invokeMapMethod('getTreatments', params); return treatments ?.map((key, value) => MapEntry(key, value)) ?? @@ -156,11 +195,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) async { - Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsWithConfig', - _buildParameters(matchingKey, bucketingKey, - {'splitName': splitNames, 'attributes': attributes})); + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'splitName': splitNames, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatments = + await methodChannel.invokeMapMethod('getTreatmentsWithConfig', params); return treatments?.map((key, value) => MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? @@ -172,11 +220,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) async { - Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsByFlagSet', - _buildParameters(matchingKey, bucketingKey, - {'flagSet': flagSet, 'attributes': attributes})); + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'flagSet': flagSet, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatments = + await methodChannel.invokeMapMethod('getTreatmentsByFlagSet', params); return treatments ?.map((key, value) => MapEntry(key, value)) ?? @@ -188,11 +245,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) async { - Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsByFlagSets', - _buildParameters(matchingKey, bucketingKey, - {'flagSets': flagSets, 'attributes': attributes})); + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'flagSets': flagSets, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); + Map? treatments = + await methodChannel.invokeMapMethod('getTreatmentsByFlagSets', params); return treatments ?.map((key, value) => MapEntry(key, value)) ?? @@ -204,11 +270,20 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) async { + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'flagSet': flagSet, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsWithConfigByFlagSet', - _buildParameters(matchingKey, bucketingKey, - {'flagSet': flagSet, 'attributes': attributes})); + 'getTreatmentsWithConfigByFlagSet', params); return treatments?.map((key, value) => MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? @@ -220,14 +295,23 @@ class MethodChannelPlatform extends SplitioPlatform { {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) async { + Map attributes = const {}, + EvaluationOptions evaluationOptions = + const EvaluationOptions.empty()}) async { + final params = _withEvalOptions( + matchingKey, + bucketingKey, + base: { + 'flagSets': flagSets, + 'attributes': attributes, + }, + evaluationOptions: evaluationOptions, + ); Map? treatments = await methodChannel.invokeMapMethod( - 'getTreatmentsWithConfigByFlagSets', - _buildParameters(matchingKey, bucketingKey, - {'flagSets': flagSets, 'attributes': attributes})); + 'getTreatmentsWithConfigByFlagSets', params); return treatments?.map((key, value) => - MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? + MapEntry(key, SplitResult(value['treatment'], value['config']))) ?? {}; } diff --git a/splitio_platform_interface/lib/split_evaluation_options.dart b/splitio_platform_interface/lib/split_evaluation_options.dart new file mode 100644 index 0000000..2a603a8 --- /dev/null +++ b/splitio_platform_interface/lib/split_evaluation_options.dart @@ -0,0 +1,20 @@ +import 'dart:collection'; + +class EvaluationOptions { + final Map _properties; + + Map get properties => UnmodifiableMapView(_properties); + + const EvaluationOptions.empty() : _properties = const {}; + + factory EvaluationOptions([Map properties = const {}]) { + return EvaluationOptions._( + Map.unmodifiable(Map.from(properties))); + } + + Map toJson() => { + 'properties': _properties, + }; + + const EvaluationOptions._(this._properties); +} diff --git a/splitio_platform_interface/lib/splitio_platform_interface.dart b/splitio_platform_interface/lib/splitio_platform_interface.dart index a367fdf..247c3a2 100644 --- a/splitio_platform_interface/lib/splitio_platform_interface.dart +++ b/splitio_platform_interface/lib/splitio_platform_interface.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:splitio_platform_interface/method_channel_platform.dart'; import 'package:splitio_platform_interface/split_configuration.dart'; +import 'package:splitio_platform_interface/split_evaluation_options.dart'; import 'package:splitio_platform_interface/split_impression.dart'; import 'package:splitio_platform_interface/split_result.dart'; import 'package:splitio_platform_interface/split_view.dart'; @@ -12,7 +13,9 @@ export 'package:splitio_platform_interface/impressions/impressions_method_call_h export 'package:splitio_platform_interface/method_call_handler.dart'; export 'package:splitio_platform_interface/method_channel_platform.dart'; export 'package:splitio_platform_interface/split_configuration.dart'; +export 'package:splitio_platform_interface/split_evaluation_options.dart'; export 'package:splitio_platform_interface/split_impression.dart'; +export 'package:splitio_platform_interface/split_prerequisite.dart'; export 'package:splitio_platform_interface/split_result.dart'; export 'package:splitio_platform_interface/split_view.dart'; export 'package:splitio_platform_interface/splitio_platform_interface.dart'; @@ -62,7 +65,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -70,7 +74,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required String splitName, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -78,7 +83,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -86,7 +92,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required List splitNames, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -94,7 +101,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -102,7 +110,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -110,7 +119,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required String flagSet, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } @@ -118,7 +128,8 @@ abstract class _ClientPlatform { {required String matchingKey, required String? bucketingKey, required List flagSets, - Map attributes = const {}}) { + Map attributes = const {}, + EvaluationOptions evaluationOptions = const EvaluationOptions.empty()}) { throw UnimplementedError(); } diff --git a/splitio_platform_interface/pubspec.yaml b/splitio_platform_interface/pubspec.yaml index 0200526..3465a95 100644 --- a/splitio_platform_interface/pubspec.yaml +++ b/splitio_platform_interface/pubspec.yaml @@ -2,7 +2,7 @@ name: splitio_platform_interface description: A common platform interface for the splitio plugin. # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.6.0-rc.1 +version: 2.0.0-rc.1 repository: https://github.com/splitio/flutter-sdk-plugin/tree/main/splitio_platform_interface environment: diff --git a/splitio_platform_interface/test/method_channel_platform_test.dart b/splitio_platform_interface/test/method_channel_platform_test.dart index 5dceddf..5c4b57c 100644 --- a/splitio_platform_interface/test/method_channel_platform_test.dart +++ b/splitio_platform_interface/test/method_channel_platform_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:splitio_platform_interface/method_channel_platform.dart'; import 'package:splitio_platform_interface/split_configuration.dart'; +import 'package:splitio_platform_interface/split_evaluation_options.dart'; void main() { const MethodChannel _channel = MethodChannel('splitio'); @@ -65,6 +66,184 @@ void main() { }); }); + group('evaluationOptions serialization', () { + test('getTreatment includes evaluationOptions when non-empty', () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatment( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split', + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatment'); + expect(methodArguments, { + 'splitName': 'split', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatmentWithConfig includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitName: 'split1', + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentWithConfig'); + expect(methodArguments, { + 'splitName': 'split1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatments includes evaluationOptions when non-empty', () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatments( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatments'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatmentsWithConfig includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsWithConfig( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + splitNames: ['split1', 'split2'], + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsWithConfig'); + expect(methodArguments, { + 'splitName': ['split1', 'split2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatmentsByFlagSet includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsByFlagSet( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + flagSet: 'set_1', + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsByFlagSet'); + expect(methodArguments, { + 'flagSet': 'set_1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test('getTreatmentsByFlagSets includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsByFlagSets( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + flagSets: ['set_1', 'set_2'], + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsByFlagSets'); + expect(methodArguments, { + 'flagSets': ['set_1', 'set_2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test( + 'getTreatmentsWithConfigByFlagSet includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsWithConfigByFlagSet( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + flagSet: 'set_1', + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsWithConfigByFlagSet'); + expect(methodArguments, { + 'flagSet': 'set_1', + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + + test( + 'getTreatmentsWithConfigByFlagSets includes evaluationOptions when non-empty', + () async { + final eo = EvaluationOptions({'x': 1, 'y': 'z'}); + await _platform.getTreatmentsWithConfigByFlagSets( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + flagSets: ['set_1', 'set_2'], + evaluationOptions: eo, + ); + + expect(methodName, 'getTreatmentsWithConfigByFlagSets'); + expect(methodArguments, { + 'flagSets': ['set_1', 'set_2'], + 'matchingKey': 'matching-key', + 'bucketingKey': 'bucketing-key', + 'attributes': {}, + 'evaluationOptions': { + 'properties': {'x': 1, 'y': 'z'} + }, + }); + }); + }); + group('evaluation', () { test('getTreatment without attributes', () async { _platform.getTreatment( @@ -509,7 +688,11 @@ void main() { 'apiKey': 'api-key', 'matchingKey': 'matching-key', 'bucketingKey': 'bucketing-key', - 'sdkConfiguration': {'logLevel': 'debug', 'streamingEnabled': false, 'readyTimeout' : 10}, + 'sdkConfiguration': { + 'logLevel': 'debug', + 'streamingEnabled': false, + 'readyTimeout': 10 + }, }); }); }); diff --git a/splitio_platform_interface/test/split_evaluation_options_test.dart b/splitio_platform_interface/test/split_evaluation_options_test.dart new file mode 100644 index 0000000..a473c33 --- /dev/null +++ b/splitio_platform_interface/test/split_evaluation_options_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitio_platform_interface/split_evaluation_options.dart'; + +void main() { + group('EvaluationOptions', () { + test('default constructor has empty properties and is unmodifiable', () { + final eo = EvaluationOptions(); + expect(eo.properties, isEmpty); + expect(() => eo.properties['x'] = 1, throwsA(isA())); + expect(() => eo.properties.remove('x'), throwsA(isA())); + expect(() => eo.properties.clear(), throwsA(isA())); + }); + + test('constructor with properties stores them', () { + final props = {'a': 1, 'b': true, 'c': 'x'}; + final eo = EvaluationOptions(props); + expect(eo.properties, equals(props)); + }); + + test('internal map is decoupled from external map', () { + final props = {'k': 42}; + final eo = EvaluationOptions(props); + // Not the same instance + expect(identical(eo.properties, props), isFalse); + // Mutating the original does not change eo.properties + props['k'] = 99; + expect(eo.properties['k'], 42); + }); + + test('properties getter is unmodifiable', () { + final eo = EvaluationOptions({'k': 1}); + expect(() => eo.properties['k'] = 2, throwsA(isA())); + expect(() => eo.properties.addAll({'z': 0}), + throwsA(isA())); + }); + + test('toJson returns a map with properties key', () { + final props = {'x': 1, 'y': 'z'}; + final eo = EvaluationOptions(props); + final json = eo.toJson(); + expect(json.keys, contains('properties')); + expect(json['properties'], equals(props)); + }); + + test('toJson returns empty properties for empty options', () { + const eo = EvaluationOptions.empty(); + final json = eo.toJson(); + expect(json, equals({'properties': const {}})); + }); + }); +}