Skip to content

Commit 7acd0aa

Browse files
feat(policy): enhance management by add deep copy functionality and integrating collection package
1 parent 2c7f0cf commit 7acd0aa

9 files changed

+2229
-27
lines changed

lib/src/core/memory_policy_storage.dart

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:flutter_policy_engine/src/core/interfaces/i_policy_storage.dart'
1111
///
1212
/// **Note:** All data stored in this implementation will be lost when the
1313
/// application is terminated or the object is garbage collected.
14+
// ignore: must_be_immutable
1415
class MemoryPolicyStorage implements IPolicyStorage {
1516
/// Internal storage for policies using a Map with String keys and dynamic values.
1617
///
@@ -34,7 +35,7 @@ class MemoryPolicyStorage implements IPolicyStorage {
3435
/// ```
3536
@override
3637
Future<Map<String, dynamic>> loadPolicies() async {
37-
return Map.from(_policies);
38+
return _deepCopy(_policies);
3839
}
3940

4041
/// Saves the provided policies to memory storage.
@@ -54,7 +55,7 @@ class MemoryPolicyStorage implements IPolicyStorage {
5455
/// ```
5556
@override
5657
Future<void> savePolicies(Map<String, dynamic> policies) async {
57-
_policies = Map.from(policies);
58+
_policies = _deepCopy(policies);
5859
}
5960

6061
/// Removes all stored policies from memory.
@@ -72,4 +73,24 @@ class MemoryPolicyStorage implements IPolicyStorage {
7273
Future<void> clearPolicies() async {
7374
_policies.clear();
7475
}
76+
77+
/// Creates a deep copy of the policies map.
78+
Map<String, dynamic> _deepCopy(Map<String, dynamic> source) {
79+
final result = <String, dynamic>{};
80+
for (final entry in source.entries) {
81+
result[entry.key] = _deepCopyValue(entry.value);
82+
}
83+
return result;
84+
}
85+
86+
/// Creates a deep copy of a single value.
87+
dynamic _deepCopyValue(dynamic value) {
88+
if (value is Map) {
89+
return _deepCopy(Map<String, dynamic>.from(value));
90+
} else if (value is List) {
91+
return value.map((item) => _deepCopyValue(item)).toList();
92+
} else {
93+
return value;
94+
}
95+
}
7596
}

lib/src/core/policy_manager.dart

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,22 +79,68 @@ class PolicyManager extends ChangeNotifier {
7979
operation: 'policy_manager_initialize',
8080
);
8181

82-
final policyMap = jsonPolicies.map(
83-
(key, value) => MapEntry(
84-
key,
85-
Policy(
82+
// Create a map of valid policies, skipping invalid ones
83+
final validPolicies = <String, Map<String, dynamic>>{};
84+
85+
for (final entry in jsonPolicies.entries) {
86+
final key = entry.key;
87+
final value = entry.value;
88+
89+
try {
90+
if (value == null) {
91+
LogHandler.warning(
92+
'Skipping null policy value',
93+
context: {'role': key},
94+
operation: 'policy_validation_skip',
95+
);
96+
continue;
97+
}
98+
99+
if (value is! List) {
100+
LogHandler.warning(
101+
'Skipping invalid policy value type',
102+
context: {
103+
'role': key,
104+
'expected_type': 'List',
105+
'actual_type': value.runtimeType.toString(),
106+
},
107+
operation: 'policy_validation_skip',
108+
);
109+
continue;
110+
}
111+
112+
if (value.any((item) => item is! String)) {
113+
LogHandler.warning(
114+
'Skipping policy with non-string content items',
115+
context: {'role': key},
116+
operation: 'policy_validation_skip',
117+
);
118+
continue;
119+
}
120+
121+
// Create the policy and add to valid policies
122+
final policy = Policy(
86123
roleName: key,
87-
allowedContent: value as List<String>,
88-
).toJson(),
89-
),
90-
);
124+
allowedContent: value.cast<String>(),
125+
);
126+
validPolicies[key] = policy.toJson();
127+
} catch (e, stackTrace) {
128+
LogHandler.error(
129+
'Failed to process policy',
130+
error: e,
131+
stackTrace: stackTrace,
132+
context: {'role': key},
133+
operation: 'policy_processing_error',
134+
);
135+
// Continue with other policies
136+
}
137+
}
91138

92139
_policies = JsonHandler.parseMap(
93-
policyMap,
140+
validPolicies,
94141
(json) => Policy.fromJson(json),
95142
context: 'policy_manager',
96-
allowPartialSuccess:
97-
true, // Allow partial success for graceful degradation
143+
allowPartialSuccess: true,
98144
);
99145

100146
// Only create evaluator if we have at least some policies

lib/src/models/policy.dart

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import 'package:meta/meta.dart';
1+
import 'package:flutter/foundation.dart';
2+
import 'package:collection/collection.dart';
23

34
/// Represents a policy that defines content access permissions for a specific role.
45
///
@@ -91,19 +92,29 @@ class Policy {
9192
/// the same [allowedContent] (regardless of order). The [metadata] is not
9293
/// considered in the equality comparison.
9394
@override
94-
bool operator ==(Object other) =>
95-
identical(this, other) ||
96-
other is Policy &&
97-
roleName == other.roleName &&
98-
allowedContent.length == other.allowedContent.length &&
99-
allowedContent
100-
.every((content) => other.allowedContent.contains(content));
95+
bool operator ==(Object other) {
96+
if (identical(this, other)) return true;
97+
if (other is! Policy) return false;
98+
99+
if (roleName != other.roleName) return false;
100+
if (allowedContent.length != other.allowedContent.length) return false;
101+
102+
// Sort both lists to ensure order-independent comparison (same as hashCode)
103+
final sortedThis = List<String>.from(allowedContent)..sort();
104+
final sortedOther = List<String>.from(other.allowedContent)..sort();
105+
106+
return listEquals(sortedThis, sortedOther);
107+
}
101108

102109
/// Returns the hash code for this policy.
103110
///
104111
/// The hash code is based on the [roleName] and [allowedContent] fields.
112+
/// The allowedContent is sorted to ensure consistent hash codes regardless of order.
105113
@override
106-
int get hashCode => roleName.hashCode ^ allowedContent.hashCode;
114+
int get hashCode {
115+
final sortedContent = List<String>.from(allowedContent)..sort();
116+
return Object.hash(roleName, const ListEquality().hash(sortedContent));
117+
}
107118

108119
/// Returns a string representation of this policy.
109120
///
@@ -130,11 +141,28 @@ class Policy {
130141
/// };
131142
/// final policy = Policy.fromJson(json);
132143
/// ```
133-
factory Policy.fromJson(Map<String, dynamic> json) => Policy(
134-
roleName: json['roleName'] as String,
135-
allowedContent: json['allowedContent'] as List<String>,
136-
metadata: json['metadata'] as Map<String, dynamic>,
137-
);
144+
factory Policy.fromJson(Map<String, dynamic> json) {
145+
final roleName = json['roleName'];
146+
final allowedContent = json['allowedContent'];
147+
final metadata = json['metadata'];
148+
149+
if (roleName == null || roleName is! String) {
150+
throw ArgumentError('roleName must be a non-null string');
151+
}
152+
if (allowedContent == null || allowedContent is! List) {
153+
throw ArgumentError('allowedContent must be a non-null list');
154+
}
155+
if (allowedContent.any((item) => item is! String)) {
156+
throw ArgumentError('All allowedContent items must be strings');
157+
}
158+
159+
return Policy(
160+
roleName: roleName,
161+
allowedContent: allowedContent.cast<String>(),
162+
metadata:
163+
metadata is Map<String, dynamic> ? metadata : <String, dynamic>{},
164+
);
165+
}
138166

139167
/// Converts this policy to a JSON map.
140168
///

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ environment:
88
flutter: ">=1.17.0"
99

1010
dependencies:
11+
collection: ^1.19.1
1112
flutter:
1213
sdk: flutter
1314
meta: ^1.12.0

0 commit comments

Comments
 (0)