timsoftdz_core 1.0.1
timsoftdz_core: ^1.0.1 copied to clipboard
Enterprise-grade core library for Dart & Flutter — Result pattern, unified exception hierarchy, structured logging with pipelines, interceptor-based HTTP client, contract-driven key-value storage, Uni [...]
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