timsoftdz_core
Enterprise-grade core library for Dart & Flutter.
Production-ready utilities following Clean Architecture principles β zero heavy framework dependencies, full null safety, >90 % test coverage.
β¨ Feature Overview
| Module | What you get |
|---|---|
| Result<T> | Success / Failure discriminated union β no exceptions in your control flow |
| Exceptions | Typed hierarchy β ValidationException, NetworkException, StorageException, etc. |
| Logger | 7 levels, ANSI console, JSON pipeline, filter DSL, lazy evaluation |
| HTTP Client | Interceptor chain, jitter-backoff retry, CancelToken, full testability |
| Storage | Async key-value, read-through cache, optional encryption plugin |
| Strings | Unicode-safe, camelCase/snake/kebab/pascal, masking, Levenshtein |
| Validation | Email, phone, password, IBAN, IPv4/6, NIN, Luhn, hex colour |
| DateTime | Zero-intl formatting, relative time (AR / EN / FR) |
| DzPhone | Algerian phone normaliser, carrier detection |
π Architecture
timsoftdz_core
βββ core/
β βββ result.dart β Result<T>, Success<T>, Failure<T>
β βββ app_exception.dart β AppException hierarchy (10 types)
β βββ error_codes.dart β Machine-readable string constants
β
βββ logger/
β βββ logger.dart β TimLogger contract + SilentLogger + CompositeLogger + TaggedLogger
β βββ log_level.dart β LogLevel enum (traceβ¦fatal) with ordering operators
β βββ log_entry.dart β Immutable structured log record
β βββ log_filter.dart β LogFilter DSL (&, |, ~)
β βββ console_logger.dart β ANSI-coloured terminal output, lazy evaluation
β βββ json_logger.dart β NDJSON output for log pipelines
β βββ filtered_logger.dart β Decorator applying LogFilter
β
βββ network/
β βββ http_client_contract.dart β TimHttpClientContract (abstract interface)
β βββ http_client.dart β TimHttpClient (package:http impl)
β βββ http_response.dart β TimHttpResponse model
β βββ retry_policy.dart β RetryPolicy + ConstantBackoff / Exponential / JitteredExponential
β βββ cancel_token.dart β Cooperative request cancellation
β
βββ storage/
β βββ storage_contract.dart β TimStorage abstract key-value contract
β βββ memory_storage.dart β In-memory (tests, session cache)
β βββ file_storage.dart β JSON file (atomic write-then-rename)
β βββ cached_storage.dart β Read-through / write-through cache + TTL
β βββ storage_encryption_codec.dart β Plugin interface + Base64 + NoOp codecs
β
βββ strings/
β βββ string_helpers.dart β TimStringX, TimNullableStringX, TimStringUtils
β
βββ validation/
β βββ validator.dart β Validator (static helpers)
β βββ dz_phone.dart β DzPhone (Algerian-specific)
β
βββ datetime/
βββ date_formatter.dart β Pattern-based formatting (no intl dependency)
βββ relative_time.dart β RelativeTime + RelativeLocale + DateTimeRelativeX
π Quick Start
# pubspec.yaml
dependencies:
timsoftdz_core: ^1.0.0
import 'package:timsoftdz_core/timsoftdz_core.dart';
π Core API
Result<T>
Every operation that can fail returns Result<T> β never throw, never catch
in your business logic.
// Construction
final Result<int> ok = const Success(42);
final Result<int> fail = Failure(const AppException('oops'));
// Pattern matching
final label = ok.fold(
onSuccess: (v) => 'Value: $v',
onFailure: (e) => 'Error: ${e.message}',
);
// Transformation
final doubled = ok.map((v) => v * 2); // Success(84)
final chained = ok.flatMap((v) => Success(v + 1)); // Success(43)
// Async
final asyncResult = await Result.runAsync(() async => fetchData());
// Guard (sync try/catch)
final parsed = Result.guard(() => int.parse(input));
// Recovery
final safe = fail.recover((_) => 0); // Success(0)
final safeWith = fail.recoverWith((_) => ok); // Success(42)
// Side effects
asyncResult
.onSuccess((v) => log.info('Got $v'))
.onFailure((e) => log.error(e.message));
// Async chaining
final result = await fetchUser(id)
.thenMap((u) => u.displayName)
.thenFlatMap((name) => fetchAvatar(name));
Exception Hierarchy
AppException
βββ ValidationException // invalid user input
βββ NetworkException // HTTP / connectivity
β βββ TimeoutException
β βββ CancelledException
β βββ UnauthorizedException (401)
β βββ ForbiddenException (403)
β βββ NotFoundException (404)
β βββ ConflictException (409)
β βββ RateLimitedException (429)
βββ StorageException // read/write failures
βββ ParseException // JSON / format errors
// Catch any library exception safely
try {
final r = await client.get('/users');
} on AppException catch (e) {
print('${e.code}: ${e.message}');
if (e is NetworkException) print('status: ${e.statusCode}');
}
// Machine-readable codes (never hardcode strings)
throw ValidationException('Email invalid', code: ErrorCode.invalidEmail);
πͺ΅ Logger
// Console logger with ANSI colours
final log = ConsoleLogger(
minLevel: LogLevel.debug,
showTimestamp: true,
);
log.trace('Entering method X', tag: 'Repo');
log.debug('Query executed', tag: 'DB');
log.info('Server started', tag: 'App');
log.success('Payment processed', tag: 'Billing');
log.warning('Rate limit 80 %', tag: 'Monitor');
log.error('Request failed', error: e, stackTrace: s);
log.fatal('DB unreachable β terminating');
// Tagged logger β auto-attaches tag
class AuthService {
final _log = TaggedLogger(ConsoleLogger(), 'AuthService');
void signIn(String email) {
_log.info('Signing in $email'); // β [AuthService] Signing in β¦
}
}
// Filtered logger β only forward warning and above
final filtered = FilteredLogger(
delegate: ConsoleLogger(),
filter: const MinLevelFilter(LogLevel.warning),
);
// Compose filters
final f = const MinLevelFilter(LogLevel.error) | const TagFilter('Payment');
// JSON logger β NDJSON for ELK / Loki / Datadog
final file = File('app.log').openWrite(mode: FileMode.append);
final jsonLog = JsonLogger(sink: file, minLevel: LogLevel.info);
jsonLog.info('Order placed', tag: 'Shop'); // {"timestamp":"β¦","level":"INFO","tag":"Shop","message":"Order placed"}
await jsonLog.close();
// Fan-out to multiple sinks
final multi = CompositeLogger([ConsoleLogger(), jsonLog]);
π HTTP Client
// Create once, inject everywhere (program against the interface)
final TimHttpClientContract client = TimHttpClient(
baseUrl: 'https://api.timsoft.dz',
defaultHeaders: {'Authorization': 'Bearer $token'},
timeout: const Duration(seconds: 15),
retryPolicy: RetryPolicy.standard, // 3 attempts, jitter backoff
onRequest: (method, url, headers) async {
headers['X-Request-Id'] = TimStringUtils.randomAlphanumeric(16);
},
onError: (e) async {
if (e is UnauthorizedException) await refreshToken();
return e;
},
);
// All methods return Result<TimHttpResponse>
final result = await client.get('/users', query: {'page': '1'});
result
.onSuccess((r) => print(r.bodyAsMap()))
.onFailure((e) => print('${e.code}: ${e.message}'));
// Cancellation
final token = CancelToken();
unawaited(client.post('/orders', body: order, cancelToken: token));
token.cancel('User pressed back');
// Retry policies
final aggressive = RetryPolicy(
maxAttempts: 5,
backoff: const JitteredExponentialBackoff(
base: Duration(seconds: 1),
maxDelay: Duration(seconds: 60),
),
);
client.close(); // release socket connections
π¦ Storage
// Contract-driven β swap implementations without changing business logic
final TimStorage store = CachedStorage(
FileStorage('./config/app.json'),
ttl: const Duration(minutes: 30),
);
// Write
await store.set('theme', 'dark');
await store.setAll({'lang': 'ar', 'uid': '42'});
await store.setInt('retries', 3);
await store.setBool('onboarded', value: true);
// Read
final theme = (await store.getOrDefault('theme', 'light')).getOrElse('light');
final uid = (await store.get('uid')).valueOrNull;
final count = (await store.count()).getOrElse(0);
// Cache management
if (store is CachedStorage) {
store.invalidate('theme'); // evict from cache
print(store.cacheSize); // number of live entries
}
// Encryption plugin
final secure = FileStorage('./prefs.json', codec: const Base64StorageCodec());
// Testing helpers
final mock = MemoryStorage({'token': 'test-token'});
final failing = AlwaysFailStorage('disk full');
π€ String Utilities
// Case conversions
'helloWorld'.toSnakeCase() // 'hello_world'
'hello_world'.toCamelCase() // 'helloWorld'
'hello_world'.toPascalCase() // 'HelloWorld'
'MyClass'.toKebabCase() // 'my-class'
'hello world'.titleCase() // 'Hello World'
// Slugify (URL-safe, Arabic-aware)
'Hello World 2026!'.slugify() // 'hello-world-2026'
'Ω
Ψ±ΨΨ¨Ψ§ Ψ¨Ψ§ΩΨΉΨ§ΩΩ
'.slugify() // 'Ω
Ψ±ΨΨ¨Ψ§-Ψ¨Ψ§ΩΨΉΨ§ΩΩ
'
// Truncation
'Hello World'.truncate(5) // 'Helloβ¦'
'Hello beautiful world'.truncateWords(12) // 'Helloβ¦'
// Masking / privacy
'1234567890'.mask(start: 3, end: 7) // '123****890'
'user@timsoft.dz'.maskEmail() // 'us***@timsoft.dz'
'0551234567'.maskPhone() // '055*****67'
// Character predicates
'12345'.isDigitsOnly // true
'Hello'.isAlpha // true
'ABC123'.isAlphanumeric // true
// Initials
'John Doe'.initials() // 'JD'
TimStringUtils.initials('Ahmed Ben Salah', max: 2) // 'AB'
// Unicode-safe reverse
'hi π'.unicodeReversed // 'π ih'
'racecar'.isPalindrome // true
// Static utilities
TimStringUtils.levenshtein('kitten', 'sitting') // 3
TimStringUtils.similarity('dart', 'dart') // 1.0
TimStringUtils.pluralise(3, 'item') // '3 items'
TimStringUtils.pluralise(0, 'box', 'boxes') // '0 boxes'
TimStringUtils.wordWrap('Hello world', 5) // 'Hello\nworld'
TimStringUtils.randomAlphanumeric(12) // 'aB3xZ9mQ7Kp1'
// Nullable extensions
String? name;
name.orEmpty // ''
name.isNullOrBlank // true
name.orElse('N/A') // 'N/A'
β Validation
// Email
Validator.isEmail('user@timsoft.dz') // true
Validator.validateEmail('user@x.com') // Result<String>
// Phone
Validator.isPhone('+12025551234') // true (international)
Validator.isAlgerianPhone('0551234567') // true
// Password
Validator.passwordStrength('P@ssword1') // 4 (Strong)
Validator.validatePassword('Weak1', minLength: 8) // Failure(tooShort)
// IBAN (MOD-97 checksum)
Validator.isIBAN('GB29NWBK60161331926819') // true
Validator.isIBAN('DZ5900600002100111999000') // true (Algeria)
// IPv4 / IPv6
Validator.isIPv4('192.168.1.1') // true
Validator.isIPv6('::1') // true
Validator.isIPv6('2001:db8::1') // true
Validator.isIP('::1') // true (either version)
// Credit card β Luhn
Validator.isValidCreditCard('4532015112830366') // true (test Visa)
// Algerian NIN
Validator.isAlgerianNIN('123456789012345678') // true (18 digits)
// Helpers
Validator.isSlug('hello-world') // true
Validator.isHexColor('#1A2B3C') // true
Validator.isJsonString('{"k":"v"}') // true
π DateTime
final now = DateTime(2026, 3, 15, 14, 30, 5);
DateFormatter.isoDate(now) // '2026-03-15'
DateFormatter.isoTime(now) // '14:30:05'
DateFormatter.shortDate(now) // '15/03/2026'
DateFormatter.longDate(now) // '15 Mar 2026'
DateFormatter.time12(now) // '02:30 PM'
DateFormatter.iso8601(now) // '2026-03-15T14:30:05'
DateFormatter.format(now, 'dd-MM-yyyy HH:mm') // '15-03-2026 14:30'
// Relative time
final past = DateTime.now().subtract(const Duration(minutes: 5));
RelativeTime.from(past, locale: RelativeLocale.en) // '5 minutes ago'
RelativeTime.from(past, locale: RelativeLocale.ar) // 'ΩΨ¨Ω 5 Ψ―ΩΨ§Ψ¦Ω'
RelativeTime.from(past, locale: RelativeLocale.fr) // 'il y a 5 minutes'
// Extension method
past.relativeTime() // '5 minutes ago' (default: en)
π± DzPhone β Algerian Phone
DzPhone.isValid('0551234567') // true
DzPhone.isValid('+213771234567') // true
DzPhone.normalize('0551234567') // '+213551234567'
DzPhone.carrier('0551234567') // 'Ooredoo'
DzPhone.carrier('0661234567') // 'Mobilis'
DzPhone.carrier('0771234567') // 'Djezzy'
DzPhone.toLocal('+213551234567') // '0551234567'
DzPhone.toDisplay('0551234567') // '055 123 45 67'
π§ͺ Testing
The package is designed to be fully testable:
// Replace real client with a mock (no network)
class MockHttpClient implements TimHttpClientContract {
@override
Future<Result<TimHttpResponse>> get(String path, {/* β¦ */}) async {
return Success(TimHttpResponse(
statusCode: 200,
body: '{"ok":true}',
headers: {},
));
}
// β¦
}
// Silence all logs in tests
final logger = SilentLogger();
// Pre-populate storage for tests
final store = MemoryStorage({'token': 'test-token', 'uid': '1'});
// Simulate storage failures
final failingStore = AlwaysFailStorage('disk full');
π Test Coverage
The package targets >90 % line coverage:
| Module | Tests |
|---|---|
core/result.dart |
40+ cases β Success, Failure, async, fold, recover, swap |
core/app_exception.dart |
All 10 exception types + fromError |
logger/ |
LogLevel ordering, LogEntry, LogFilter DSL, all logger types |
network/ |
CancelToken, RetryPolicy, backoff strategies, TimHttpResponse |
storage/ |
MemoryStorage, FileStorage, CachedStorage, codecs, AlwaysFail |
strings/ |
50+ edge cases across all extension methods and static utils |
validation/ |
All validators including IBAN, IPv6, Luhn, DzPhone |
datetime/ |
DateFormatter, RelativeTime (all 3 locales) |
Run tests:
dart test # all tests
dart test test/core_result_test.dart # specific file
π pub.dev Scores (targets)
| Dimension | Score | Details |
|---|---|---|
| Likes | β | Community signal |
| Pub points | 130 | Null safety, docs, tests, analysis |
| Popularity | β | Download count |
| Overall | 160+ | Target for v1.0 |
Score improvements over 0.x:
- β Full API documentation on every public member
- β
example/main.dartinexample/folder - β
No analyzer warnings (
dart analyze --fatal-infos) - β
analysis_options.yamlusinglints ^4.0.0 - β
Correct
CHANGELOG.mdformat - β
homepage,repository,issue_tracker,documentationin pubspec - β
topicslist for discoverability
πΊοΈ Roadmap β v1.1+
| Feature | Status |
|---|---|
SecureStorage adapter (flutter_secure_storage integration) |
Planned |
FileLogger concrete implementation |
Planned |
RemoteLogger (HTTP log sink) |
Planned |
Validator.validateIBAN() returning Result<String> |
Planned |
TimHttpClient β Dio adapter |
Planned |
Internationalized ValidationException messages |
Planned |
π License
MIT β see LICENSE.
π€ Author
TIMSoftDZ
π timsoftdz.com Β·
π github.com/TIMSoftDZ Β·
π¦ pub.dev/publishers/timsoftdz.com