timsoftdz_core

pub.dev Dart SDK License: MIT style: lint

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.dart in example/ folder
  • βœ… No analyzer warnings (dart analyze --fatal-infos)
  • βœ… analysis_options.yaml using lints ^4.0.0
  • βœ… Correct CHANGELOG.md format
  • βœ… homepage, repository, issue_tracker, documentation in pubspec
  • βœ… topics list 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

Libraries

timsoftdz_core