import 'package:equatable/equatable.dart'; /// Generic API response wrapper that handles the standard API response format /// /// All API responses follow this structure: /// ```json /// { /// "Value": T, /// "IsSuccess": bool, /// "IsFailure": bool, /// "Errors": List, /// "ErrorCodes": List /// } /// ``` /// /// Usage: /// ```dart /// final response = ApiResponse.fromJson( /// jsonData, /// (json) => User.fromJson(json), /// ); /// /// if (response.isSuccess && response.value != null) { /// // Handle success /// final user = response.value!; /// } else { /// // Handle error /// final errorMessage = response.errors.first; /// } /// ``` class ApiResponse extends Equatable { /// The actual data/payload of the response /// Can be null if the API call failed or returned no data final T? value; /// Indicates if the API call was successful final bool isSuccess; /// Indicates if the API call failed final bool isFailure; /// List of error messages if the call failed final List errors; /// List of error codes for programmatic error handling final List errorCodes; const ApiResponse({ this.value, required this.isSuccess, required this.isFailure, this.errors = const [], this.errorCodes = const [], }); /// Create an ApiResponse from JSON /// /// The [fromJsonT] function is used to deserialize the "Value" field. /// If null, the value is used as-is. /// /// Example: /// ```dart /// // For single object /// ApiResponse.fromJson(json, (j) => User.fromJson(j)) /// /// // For list of objects /// ApiResponse.fromJson( /// json, /// (j) => (j as List).map((e) => User.fromJson(e)).toList() /// ) /// /// // For primitive types or no conversion needed /// ApiResponse.fromJson(json, null) /// ``` factory ApiResponse.fromJson( Map json, T Function(dynamic)? fromJsonT, ) { return ApiResponse( value: json['Value'] != null && fromJsonT != null ? fromJsonT(json['Value']) : json['Value'] as T?, isSuccess: json['IsSuccess'] ?? false, isFailure: json['IsFailure'] ?? true, errors: json['Errors'] != null ? List.from(json['Errors']) : const [], errorCodes: json['ErrorCodes'] != null ? List.from(json['ErrorCodes']) : const [], ); } /// Create a successful response (useful for testing or manual creation) factory ApiResponse.success(T value) { return ApiResponse( value: value, isSuccess: true, isFailure: false, ); } /// Create a failed response (useful for testing or manual creation) factory ApiResponse.failure({ required List errors, List? errorCodes, }) { return ApiResponse( isSuccess: false, isFailure: true, errors: errors, errorCodes: errorCodes ?? const [], ); } /// Check if response has data bool get hasValue => value != null; /// Get the first error message if available String? get firstError => errors.isNotEmpty ? errors.first : null; /// Get the first error code if available String? get firstErrorCode => errorCodes.isNotEmpty ? errorCodes.first : null; /// Get a combined error message from all errors String get combinedErrorMessage { if (errors.isEmpty) return 'An unknown error occurred'; return errors.join(', '); } /// Convert to a map (useful for serialization or debugging) Map toJson(Object? Function(T)? toJsonT) { return { 'Value': value != null && toJsonT != null ? toJsonT(value as T) : value, 'IsSuccess': isSuccess, 'IsFailure': isFailure, 'Errors': errors, 'ErrorCodes': errorCodes, }; } /// Create a copy with modified fields ApiResponse copyWith({ T? value, bool? isSuccess, bool? isFailure, List? errors, List? errorCodes, }) { return ApiResponse( value: value ?? this.value, isSuccess: isSuccess ?? this.isSuccess, isFailure: isFailure ?? this.isFailure, errors: errors ?? this.errors, errorCodes: errorCodes ?? this.errorCodes, ); } @override List get props => [value, isSuccess, isFailure, errors, errorCodes]; @override String toString() { if (isSuccess) { return 'ApiResponse.success(value: $value)'; } else { return 'ApiResponse.failure(errors: $errors, errorCodes: $errorCodes)'; } } } /// Extension to convert ApiResponse to nullable value easily extension ApiResponseExtension on ApiResponse { /// Get value if success, otherwise return null T? get valueOrNull => isSuccess ? value : null; /// Get value if success, otherwise throw exception with error message T get valueOrThrow { if (isSuccess && value != null) { return value!; } throw Exception(combinedErrorMessage); } } /// Specialized API response for list data with pagination class PaginatedApiResponse extends ApiResponse> { /// Current page number final int currentPage; /// Total number of pages final int totalPages; /// Total number of items final int totalItems; /// Number of items per page final int pageSize; /// Whether there is a next page bool get hasNextPage => currentPage < totalPages; /// Whether there is a previous page bool get hasPreviousPage => currentPage > 1; const PaginatedApiResponse({ super.value, required super.isSuccess, required super.isFailure, super.errors, super.errorCodes, required this.currentPage, required this.totalPages, required this.totalItems, required this.pageSize, }); /// Create a PaginatedApiResponse from JSON factory PaginatedApiResponse.fromJson( Map json, List Function(dynamic) fromJsonList, ) { final apiResponse = ApiResponse>.fromJson(json, fromJsonList); return PaginatedApiResponse( value: apiResponse.value, isSuccess: apiResponse.isSuccess, isFailure: apiResponse.isFailure, errors: apiResponse.errors, errorCodes: apiResponse.errorCodes, currentPage: json['CurrentPage'] ?? 1, totalPages: json['TotalPages'] ?? 1, totalItems: json['TotalItems'] ?? 0, pageSize: json['PageSize'] ?? 20, ); } @override List get props => [ ...super.props, currentPage, totalPages, totalItems, pageSize, ]; }