refactor: removed unused api validator
This commit is contained in:
@@ -147,7 +147,7 @@ See the dedicated documentation: [docs/localization.md](docs/localization.md)
|
||||
|
||||
| Conduit App | Open‑WebUI | Notes |
|
||||
| --- | --- | --- |
|
||||
| 1.x | 0.3.x+ | Uses OpenAPI schema at `assets/openapi.json` |
|
||||
| 1.x | 0.3.x+ | OpenAPI validation removed in 1.1+ (no bundled schema) |
|
||||
|
||||
## Docs
|
||||
|
||||
|
||||
22618
assets/openapi.json
22618
assets/openapi.json
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ import '../models/model.dart';
|
||||
import '../models/conversation.dart';
|
||||
import '../models/chat_message.dart';
|
||||
import '../auth/api_auth_interceptor.dart';
|
||||
import '../validation/validation_interceptor.dart';
|
||||
import '../error/api_error_interceptor.dart';
|
||||
// Tool-call details are parsed in the UI layer to render collapsible blocks
|
||||
import 'persistent_streaming_service.dart';
|
||||
@@ -66,16 +65,7 @@ class ApiService {
|
||||
// 1. Auth interceptor (must be first to add auth headers)
|
||||
_dio.interceptors.add(_authInterceptor);
|
||||
|
||||
// 2. Validation interceptor (validates requests/responses against OpenAPI schema)
|
||||
// Disable for now to ensure parameters aren't being filtered
|
||||
final validationInterceptor = ValidationInterceptor(
|
||||
enableRequestValidation: false, // Disabled to preserve all parameters
|
||||
enableResponseValidation: false, // Disabled for SSE streams
|
||||
throwOnValidationError: false,
|
||||
logValidationResults: kDebugMode,
|
||||
);
|
||||
// Comment out to disable completely
|
||||
// _dio.interceptors.add(validationInterceptor);
|
||||
// 2. Validation interceptor removed (no schema loading/logging)
|
||||
|
||||
// 3. Error handling interceptor (transforms errors to standardized format)
|
||||
_dio.interceptors.add(
|
||||
@@ -99,10 +89,7 @@ class ApiService {
|
||||
// We now use custom interceptors with secure logging via DebugLogger
|
||||
}
|
||||
|
||||
// Initialize validation interceptor asynchronously
|
||||
validationInterceptor.initialize().catchError((error) {
|
||||
// Handle validation initialization errors silently
|
||||
});
|
||||
// Validation interceptor fully removed
|
||||
}
|
||||
|
||||
void updateAuthToken(String token) {
|
||||
|
||||
@@ -1,416 +0,0 @@
|
||||
import 'schema_registry.dart';
|
||||
import 'validation_result.dart';
|
||||
import 'field_mapper.dart';
|
||||
import '../utils/debug_logger.dart';
|
||||
|
||||
/// Comprehensive API request and response validator
|
||||
/// Validates against OpenAPI specification schemas
|
||||
class ApiValidator {
|
||||
static final ApiValidator _instance = ApiValidator._internal();
|
||||
factory ApiValidator() => _instance;
|
||||
ApiValidator._internal();
|
||||
|
||||
final SchemaRegistry _schemaRegistry = SchemaRegistry();
|
||||
final FieldMapper _fieldMapper = FieldMapper();
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
bool get isInitialized => _initialized;
|
||||
|
||||
/// Initialize validator with OpenAPI schemas
|
||||
Future<void> initialize() async {
|
||||
if (_initialized) return;
|
||||
|
||||
try {
|
||||
await _schemaRegistry.loadSchemas();
|
||||
_initialized = true;
|
||||
DebugLogger.validation('Successfully initialized with schemas');
|
||||
} catch (e) {
|
||||
DebugLogger.error('Failed to initialize', e);
|
||||
// Continue without validation if schemas can't be loaded
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate request payload before sending to API
|
||||
ValidationResult validateRequest(
|
||||
dynamic data,
|
||||
String endpoint, {
|
||||
String method = 'GET',
|
||||
}) {
|
||||
if (!_initialized) {
|
||||
return ValidationResult.warning(
|
||||
'Validator not initialized - skipping validation',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final schema = _schemaRegistry.getRequestSchema(endpoint, method);
|
||||
if (schema == null) {
|
||||
return ValidationResult.warning(
|
||||
'No schema found for $method $endpoint',
|
||||
);
|
||||
}
|
||||
|
||||
// Transform field names for API (camelCase -> snake_case)
|
||||
final transformedData = _fieldMapper.toApiFormat(data);
|
||||
|
||||
// Validate against schema
|
||||
return _validateAgainstSchema(transformedData, schema, 'request');
|
||||
} catch (e) {
|
||||
return ValidationResult.error('Request validation failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate response payload after receiving from API
|
||||
ValidationResult validateResponse(
|
||||
dynamic data,
|
||||
String endpoint, {
|
||||
String method = 'GET',
|
||||
int? statusCode,
|
||||
}) {
|
||||
if (!_initialized) {
|
||||
return ValidationResult.warning(
|
||||
'Validator not initialized - skipping validation',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final schema = _schemaRegistry.getResponseSchema(
|
||||
endpoint,
|
||||
method,
|
||||
statusCode,
|
||||
);
|
||||
if (schema == null) {
|
||||
return ValidationResult.warning(
|
||||
'No schema found for $method $endpoint response',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate against schema first
|
||||
final validationResult = _validateAgainstSchema(data, schema, 'response');
|
||||
if (!validationResult.isValid) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
// Transform field names from API (snake_case -> camelCase)
|
||||
final transformedData = _fieldMapper.fromApiFormat(data);
|
||||
|
||||
return ValidationResult.success(
|
||||
'Response validated successfully',
|
||||
data: transformedData,
|
||||
);
|
||||
} catch (e) {
|
||||
return ValidationResult.error('Response validation failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate data against a specific schema
|
||||
ValidationResult _validateAgainstSchema(
|
||||
dynamic data,
|
||||
Map<String, dynamic> schema,
|
||||
String context,
|
||||
) {
|
||||
final errors = <String>[];
|
||||
final warnings = <String>[];
|
||||
|
||||
try {
|
||||
_validateValue(data, schema, '', errors, warnings);
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
return ValidationResult.error(
|
||||
'Schema validation failed for $context',
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
if (warnings.isNotEmpty) {
|
||||
return ValidationResult.warning(
|
||||
'Schema validation passed with warnings for $context',
|
||||
warnings: warnings,
|
||||
);
|
||||
}
|
||||
|
||||
return ValidationResult.success('Schema validation passed for $context');
|
||||
} catch (e) {
|
||||
return ValidationResult.error('Schema validation error for $context: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively validate a value against schema
|
||||
void _validateValue(
|
||||
dynamic value,
|
||||
Map<String, dynamic> schema,
|
||||
String path,
|
||||
List<String> errors,
|
||||
List<String> warnings,
|
||||
) {
|
||||
final type = schema['type'] as String?;
|
||||
final required = schema['required'] as List<dynamic>? ?? [];
|
||||
|
||||
// Handle null values
|
||||
if (value == null) {
|
||||
if (required.isNotEmpty && path.isNotEmpty) {
|
||||
errors.add('Required field missing: $path');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Type validation
|
||||
switch (type) {
|
||||
case 'object':
|
||||
_validateObject(value, schema, path, errors, warnings);
|
||||
break;
|
||||
case 'array':
|
||||
_validateArray(value, schema, path, errors, warnings);
|
||||
break;
|
||||
case 'string':
|
||||
_validateString(value, schema, path, errors, warnings);
|
||||
break;
|
||||
case 'number':
|
||||
case 'integer':
|
||||
_validateNumber(value, schema, path, errors, warnings);
|
||||
break;
|
||||
case 'boolean':
|
||||
_validateBoolean(value, schema, path, errors, warnings);
|
||||
break;
|
||||
default:
|
||||
// Unknown type - add warning but don't fail
|
||||
warnings.add('Unknown schema type "$type" at $path');
|
||||
}
|
||||
}
|
||||
|
||||
void _validateObject(
|
||||
dynamic value,
|
||||
Map<String, dynamic> schema,
|
||||
String path,
|
||||
List<String> errors,
|
||||
List<String> warnings,
|
||||
) {
|
||||
if (value is! Map) {
|
||||
errors.add('Expected object at $path, got ${value.runtimeType}');
|
||||
return;
|
||||
}
|
||||
|
||||
final valueMap = value as Map<String, dynamic>;
|
||||
final properties = schema['properties'] as Map<String, dynamic>? ?? {};
|
||||
final required = (schema['required'] as List<dynamic>? ?? [])
|
||||
.cast<String>();
|
||||
|
||||
// Check required fields
|
||||
for (final requiredField in required) {
|
||||
if (!valueMap.containsKey(requiredField)) {
|
||||
errors.add(
|
||||
'Required field missing: ${path.isEmpty ? '' : '$path.'}$requiredField',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate each property
|
||||
for (final entry in valueMap.entries) {
|
||||
final fieldName = entry.key;
|
||||
final fieldValue = entry.value;
|
||||
final fieldPath = path.isEmpty ? fieldName : '$path.$fieldName';
|
||||
|
||||
if (properties.containsKey(fieldName)) {
|
||||
_validateValue(
|
||||
fieldValue,
|
||||
properties[fieldName],
|
||||
fieldPath,
|
||||
errors,
|
||||
warnings,
|
||||
);
|
||||
} else {
|
||||
// Additional property - warn but don't error
|
||||
warnings.add('Additional property found: $fieldPath');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _validateArray(
|
||||
dynamic value,
|
||||
Map<String, dynamic> schema,
|
||||
String path,
|
||||
List<String> errors,
|
||||
List<String> warnings,
|
||||
) {
|
||||
if (value is! List) {
|
||||
errors.add('Expected array at $path, got ${value.runtimeType}');
|
||||
return;
|
||||
}
|
||||
|
||||
final array = value;
|
||||
final items = schema['items'] as Map<String, dynamic>?;
|
||||
final minItems = schema['minItems'] as int?;
|
||||
final maxItems = schema['maxItems'] as int?;
|
||||
|
||||
// Validate array constraints
|
||||
if (minItems != null && array.length < minItems) {
|
||||
errors.add(
|
||||
'Array at $path has ${array.length} items, minimum is $minItems',
|
||||
);
|
||||
}
|
||||
|
||||
if (maxItems != null && array.length > maxItems) {
|
||||
errors.add(
|
||||
'Array at $path has ${array.length} items, maximum is $maxItems',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate each item
|
||||
if (items != null) {
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
_validateValue(array[i], items, '$path[$i]', errors, warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _validateString(
|
||||
dynamic value,
|
||||
Map<String, dynamic> schema,
|
||||
String path,
|
||||
List<String> errors,
|
||||
List<String> warnings,
|
||||
) {
|
||||
if (value is! String) {
|
||||
errors.add('Expected string at $path, got ${value.runtimeType}');
|
||||
return;
|
||||
}
|
||||
|
||||
final string = value;
|
||||
final minLength = schema['minLength'] as int?;
|
||||
final maxLength = schema['maxLength'] as int?;
|
||||
final pattern = schema['pattern'] as String?;
|
||||
final format = schema['format'] as String?;
|
||||
|
||||
if (minLength != null && string.length < minLength) {
|
||||
errors.add(
|
||||
'String at $path is ${string.length} chars, minimum is $minLength',
|
||||
);
|
||||
}
|
||||
|
||||
if (maxLength != null && string.length > maxLength) {
|
||||
errors.add(
|
||||
'String at $path is ${string.length} chars, maximum is $maxLength',
|
||||
);
|
||||
}
|
||||
|
||||
if (pattern != null) {
|
||||
try {
|
||||
final regex = RegExp(pattern);
|
||||
if (!regex.hasMatch(string)) {
|
||||
errors.add('String at $path does not match pattern: $pattern');
|
||||
}
|
||||
} catch (e) {
|
||||
warnings.add('Invalid regex pattern at $path: $pattern');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate common formats
|
||||
if (format != null) {
|
||||
_validateStringFormat(string, format, path, errors, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
void _validateStringFormat(
|
||||
String value,
|
||||
String format,
|
||||
String path,
|
||||
List<String> errors,
|
||||
List<String> warnings,
|
||||
) {
|
||||
switch (format) {
|
||||
case 'email':
|
||||
final emailRegex = RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$');
|
||||
if (!emailRegex.hasMatch(value)) {
|
||||
errors.add('Invalid email format at $path: $value');
|
||||
}
|
||||
break;
|
||||
case 'uri':
|
||||
case 'url':
|
||||
try {
|
||||
Uri.parse(value);
|
||||
} catch (e) {
|
||||
errors.add('Invalid URL format at $path: $value');
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
try {
|
||||
DateTime.parse(value);
|
||||
} catch (e) {
|
||||
errors.add('Invalid date format at $path: $value');
|
||||
}
|
||||
break;
|
||||
case 'date-time':
|
||||
try {
|
||||
DateTime.parse(value);
|
||||
} catch (e) {
|
||||
errors.add('Invalid datetime format at $path: $value');
|
||||
}
|
||||
break;
|
||||
case 'uuid':
|
||||
final uuidRegex = RegExp(
|
||||
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
if (!uuidRegex.hasMatch(value)) {
|
||||
errors.add('Invalid UUID format at $path: $value');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
warnings.add('Unknown string format "$format" at $path');
|
||||
}
|
||||
}
|
||||
|
||||
void _validateNumber(
|
||||
dynamic value,
|
||||
Map<String, dynamic> schema,
|
||||
String path,
|
||||
List<String> errors,
|
||||
List<String> warnings,
|
||||
) {
|
||||
if (value is! num) {
|
||||
errors.add('Expected number at $path, got ${value.runtimeType}');
|
||||
return;
|
||||
}
|
||||
|
||||
final number = value;
|
||||
final minimum = schema['minimum'] as num?;
|
||||
final maximum = schema['maximum'] as num?;
|
||||
final multipleOf = schema['multipleOf'] as num?;
|
||||
|
||||
if (minimum != null && number < minimum) {
|
||||
errors.add('Number at $path is $number, minimum is $minimum');
|
||||
}
|
||||
|
||||
if (maximum != null && number > maximum) {
|
||||
errors.add('Number at $path is $number, maximum is $maximum');
|
||||
}
|
||||
|
||||
if (multipleOf != null && number % multipleOf != 0) {
|
||||
errors.add('Number at $path ($number) is not a multiple of $multipleOf');
|
||||
}
|
||||
}
|
||||
|
||||
void _validateBoolean(
|
||||
dynamic value,
|
||||
Map<String, dynamic> schema,
|
||||
String path,
|
||||
List<String> errors,
|
||||
List<String> warnings,
|
||||
) {
|
||||
if (value is! bool) {
|
||||
errors.add('Expected boolean at $path, got ${value.runtimeType}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform and validate data for API consumption
|
||||
Map<String, dynamic> transformForApi(Map<String, dynamic> data) {
|
||||
return _fieldMapper.toApiFormat(data);
|
||||
}
|
||||
|
||||
/// Transform and validate data from API response
|
||||
Map<String, dynamic> transformFromApi(Map<String, dynamic> data) {
|
||||
return _fieldMapper.fromApiFormat(data);
|
||||
}
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
import '../utils/debug_logger.dart';
|
||||
|
||||
/// Handles field name transformations between API and client formats
|
||||
/// Converts between snake_case (API) and camelCase (client)
|
||||
class FieldMapper {
|
||||
static final FieldMapper _instance = FieldMapper._internal();
|
||||
factory FieldMapper() => _instance;
|
||||
FieldMapper._internal();
|
||||
|
||||
// Cache for converted field names to improve performance
|
||||
final Map<String, String> _toCamelCaseCache = {};
|
||||
final Map<String, String> _toSnakeCaseCache = {};
|
||||
|
||||
// Special field mappings that don't follow standard conversion rules
|
||||
static const Map<String, String> _specialApiToClient = {
|
||||
'created_at': 'createdAt',
|
||||
'updated_at': 'updatedAt',
|
||||
'user_id': 'userId',
|
||||
'chat_id': 'chatId',
|
||||
'message_id': 'messageId',
|
||||
'session_id': 'sessionId',
|
||||
'folder_id': 'folderId',
|
||||
'share_id': 'shareId',
|
||||
'model_id': 'modelId',
|
||||
'tool_id': 'toolId',
|
||||
'function_id': 'functionId',
|
||||
'file_id': 'fileId',
|
||||
'knowledge_base_id': 'knowledgeBaseId',
|
||||
'channel_id': 'channelId',
|
||||
'note_id': 'noteId',
|
||||
'prompt_id': 'promptId',
|
||||
'memory_id': 'memoryId',
|
||||
'is_private': 'isPrivate',
|
||||
'is_enabled': 'isEnabled',
|
||||
'is_active': 'isActive',
|
||||
'is_archived': 'isArchived',
|
||||
'is_pinned': 'isPinned',
|
||||
'api_key': 'apiKey',
|
||||
'access_token': 'accessToken',
|
||||
'refresh_token': 'refreshToken',
|
||||
'content_type': 'contentType',
|
||||
'file_size': 'fileSize',
|
||||
'file_type': 'fileType',
|
||||
'mime_type': 'mimeType',
|
||||
// OpenWebUI chat message fields - keep in camelCase
|
||||
'parentId': 'parentId',
|
||||
'childrenIds': 'childrenIds',
|
||||
'currentId': 'currentId',
|
||||
'modelName': 'modelName',
|
||||
'modelIdx': 'modelIdx',
|
||||
};
|
||||
|
||||
static const Map<String, String> _specialClientToApi = {
|
||||
'createdAt': 'created_at',
|
||||
'updatedAt': 'updated_at',
|
||||
'userId': 'user_id',
|
||||
'chatId': 'chat_id',
|
||||
'messageId': 'message_id',
|
||||
'sessionId': 'session_id',
|
||||
'folderId': 'folder_id',
|
||||
'shareId': 'share_id',
|
||||
'modelId': 'model_id',
|
||||
'toolId': 'tool_id',
|
||||
'functionId': 'function_id',
|
||||
'fileId': 'file_id',
|
||||
'knowledgeBaseId': 'knowledge_base_id',
|
||||
'channelId': 'channel_id',
|
||||
'noteId': 'note_id',
|
||||
'promptId': 'prompt_id',
|
||||
'memoryId': 'memory_id',
|
||||
'isPrivate': 'is_private',
|
||||
'isEnabled': 'is_enabled',
|
||||
'isActive': 'is_active',
|
||||
'isArchived': 'is_archived',
|
||||
'isPinned': 'is_pinned',
|
||||
'apiKey': 'api_key',
|
||||
'accessToken': 'access_token',
|
||||
'refreshToken': 'refresh_token',
|
||||
'contentType': 'content_type',
|
||||
'fileSize': 'file_size',
|
||||
'fileType': 'file_type',
|
||||
'mimeType': 'mime_type',
|
||||
// OpenWebUI chat message fields - keep in camelCase
|
||||
'parentId': 'parentId',
|
||||
'childrenIds': 'childrenIds',
|
||||
'currentId': 'currentId',
|
||||
'modelName': 'modelName',
|
||||
'modelIdx': 'modelIdx',
|
||||
};
|
||||
|
||||
/// Transform data from client format (camelCase) to API format (snake_case)
|
||||
dynamic toApiFormat(dynamic data) {
|
||||
if (data == null) return null;
|
||||
|
||||
if (data is Map<String, dynamic>) {
|
||||
return _transformMap(data, _toSnakeCase);
|
||||
} else if (data is List) {
|
||||
return data.map((item) => toApiFormat(item)).toList();
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform data from API format (snake_case) to client format (camelCase)
|
||||
dynamic fromApiFormat(dynamic data) {
|
||||
if (data == null) return null;
|
||||
|
||||
if (data is Map<String, dynamic>) {
|
||||
return _transformMap(data, _toCamelCase);
|
||||
} else if (data is List) {
|
||||
return data.map((item) => fromApiFormat(item)).toList();
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform a map using the provided key transformation function
|
||||
Map<String, dynamic> _transformMap(
|
||||
Map<String, dynamic> map,
|
||||
String Function(String) keyTransform,
|
||||
) {
|
||||
final transformed = <String, dynamic>{};
|
||||
|
||||
for (final entry in map.entries) {
|
||||
final transformedKey = keyTransform(entry.key);
|
||||
dynamic transformedValue = entry.value;
|
||||
|
||||
// Recursively transform nested objects and arrays
|
||||
if (transformedValue is Map<String, dynamic>) {
|
||||
transformedValue = _transformMap(transformedValue, keyTransform);
|
||||
} else if (transformedValue is List) {
|
||||
transformedValue = transformedValue.map((item) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
return _transformMap(item, keyTransform);
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
transformed[transformedKey] = transformedValue;
|
||||
}
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
/// Convert snake_case to camelCase
|
||||
String _toCamelCase(String snakeCase) {
|
||||
// Check cache first
|
||||
if (_toCamelCaseCache.containsKey(snakeCase)) {
|
||||
return _toCamelCaseCache[snakeCase]!;
|
||||
}
|
||||
|
||||
// Check special mappings
|
||||
if (_specialApiToClient.containsKey(snakeCase)) {
|
||||
final result = _specialApiToClient[snakeCase]!;
|
||||
_toCamelCaseCache[snakeCase] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Standard conversion
|
||||
if (!snakeCase.contains('_')) {
|
||||
_toCamelCaseCache[snakeCase] = snakeCase;
|
||||
return snakeCase;
|
||||
}
|
||||
|
||||
final words = snakeCase.split('_');
|
||||
final result =
|
||||
words.first + words.skip(1).map((word) => _capitalize(word)).join('');
|
||||
|
||||
_toCamelCaseCache[snakeCase] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Convert camelCase to snake_case
|
||||
String _toSnakeCase(String camelCase) {
|
||||
// Check cache first
|
||||
if (_toSnakeCaseCache.containsKey(camelCase)) {
|
||||
return _toSnakeCaseCache[camelCase]!;
|
||||
}
|
||||
|
||||
// Check special mappings
|
||||
if (_specialClientToApi.containsKey(camelCase)) {
|
||||
final result = _specialClientToApi[camelCase]!;
|
||||
_toSnakeCaseCache[camelCase] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Standard conversion
|
||||
final result = camelCase.replaceAllMapped(
|
||||
RegExp(r'[A-Z]'),
|
||||
(match) => '_${match.group(0)!.toLowerCase()}',
|
||||
);
|
||||
|
||||
_toSnakeCaseCache[camelCase] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Capitalize first letter of a word
|
||||
String _capitalize(String word) {
|
||||
if (word.isEmpty) return word;
|
||||
return word[0].toUpperCase() + word.substring(1).toLowerCase();
|
||||
}
|
||||
|
||||
/// Convert a single field name from snake_case to camelCase
|
||||
String fieldToCamelCase(String snakeCase) {
|
||||
return _toCamelCase(snakeCase);
|
||||
}
|
||||
|
||||
/// Convert a single field name from camelCase to snake_case
|
||||
String fieldToSnakeCase(String camelCase) {
|
||||
return _toSnakeCase(camelCase);
|
||||
}
|
||||
|
||||
/// Get all cached transformations for debugging
|
||||
Map<String, dynamic> getCacheInfo() {
|
||||
return {
|
||||
'toCamelCacheSize': _toCamelCaseCache.length,
|
||||
'toSnakeCacheSize': _toSnakeCaseCache.length,
|
||||
'specialMappingsCount': _specialApiToClient.length,
|
||||
};
|
||||
}
|
||||
|
||||
/// Clear transformation caches
|
||||
void clearCache() {
|
||||
_toCamelCaseCache.clear();
|
||||
_toSnakeCaseCache.clear();
|
||||
DebugLogger.validation('Cleared transformation caches');
|
||||
}
|
||||
|
||||
/// Add custom field mapping
|
||||
void addCustomMapping(String apiField, String clientField) {
|
||||
_specialApiToClient[apiField] = clientField;
|
||||
_specialClientToApi[clientField] = apiField;
|
||||
|
||||
// Clear relevant cache entries
|
||||
_toCamelCaseCache.remove(apiField);
|
||||
_toSnakeCaseCache.remove(clientField);
|
||||
|
||||
DebugLogger.validation('Added custom mapping: $apiField <-> $clientField');
|
||||
}
|
||||
|
||||
/// Validate that field transformations are reversible
|
||||
bool validateTransformations() {
|
||||
final errors = <String>[];
|
||||
|
||||
// Test special mappings
|
||||
for (final entry in _specialApiToClient.entries) {
|
||||
final apiField = entry.key;
|
||||
final clientField = entry.value;
|
||||
|
||||
// Test API -> Client -> API
|
||||
final backToApi = _toSnakeCase(clientField);
|
||||
if (backToApi != apiField) {
|
||||
errors.add(
|
||||
'$apiField -> $clientField -> $backToApi (should be $apiField)',
|
||||
);
|
||||
}
|
||||
|
||||
// Test Client -> API -> Client
|
||||
final backToClient = _toCamelCase(apiField);
|
||||
if (backToClient != clientField) {
|
||||
errors.add(
|
||||
'$clientField -> $apiField -> $backToClient (should be $clientField)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
DebugLogger.error('Transformation validation errors:');
|
||||
for (final error in errors) {
|
||||
DebugLogger.error(' $error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
DebugLogger.validation('All transformations validated successfully');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../utils/debug_logger.dart';
|
||||
|
||||
/// Registry for OpenAPI schemas
|
||||
/// Loads and provides access to request/response schemas for validation
|
||||
class SchemaRegistry {
|
||||
static final SchemaRegistry _instance = SchemaRegistry._internal();
|
||||
factory SchemaRegistry() => _instance;
|
||||
SchemaRegistry._internal();
|
||||
|
||||
Map<String, dynamic>? _openApiSpec;
|
||||
final Map<String, Map<String, dynamic>> _requestSchemaCache = {};
|
||||
final Map<String, Map<String, dynamic>> _responseSchemaCache = {};
|
||||
|
||||
bool get isLoaded => _openApiSpec != null;
|
||||
|
||||
/// Load schemas from OpenAPI specification
|
||||
Future<void> loadSchemas() async {
|
||||
try {
|
||||
DebugLogger.validation('Loading OpenAPI specification...');
|
||||
|
||||
// Try to load from assets first, then from file system as fallback
|
||||
String openApiContent;
|
||||
try {
|
||||
openApiContent = await rootBundle.loadString('assets/openapi.json');
|
||||
} catch (e) {
|
||||
DebugLogger.warning(
|
||||
'Could not load from assets, trying file system...',
|
||||
);
|
||||
// Fallback - in a real app you might load from network or local file
|
||||
throw Exception('OpenAPI specification not found in assets');
|
||||
}
|
||||
|
||||
_openApiSpec = jsonDecode(openApiContent) as Map<String, dynamic>;
|
||||
|
||||
DebugLogger.validation(
|
||||
'Successfully loaded OpenAPI spec with ${_getPaths().length} paths',
|
||||
);
|
||||
|
||||
// Pre-process and cache commonly used schemas
|
||||
await _buildSchemaCache();
|
||||
} catch (e) {
|
||||
DebugLogger.error('Failed to load schemas', e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get request schema for endpoint and method
|
||||
Map<String, dynamic>? getRequestSchema(String endpoint, String method) {
|
||||
if (!isLoaded) return null;
|
||||
|
||||
final cacheKey = '${method.toUpperCase()}:$endpoint:request';
|
||||
if (_requestSchemaCache.containsKey(cacheKey)) {
|
||||
return _requestSchemaCache[cacheKey];
|
||||
}
|
||||
|
||||
try {
|
||||
final pathItem = _findPathItem(endpoint);
|
||||
if (pathItem == null) return null;
|
||||
|
||||
final operation = pathItem[method.toLowerCase()] as Map<String, dynamic>?;
|
||||
if (operation == null) return null;
|
||||
|
||||
final requestBody = operation['requestBody'] as Map<String, dynamic>?;
|
||||
if (requestBody == null) return null;
|
||||
|
||||
final content = requestBody['content'] as Map<String, dynamic>?;
|
||||
if (content == null) return null;
|
||||
|
||||
// Try to find JSON content type
|
||||
final jsonContent =
|
||||
content['application/json'] as Map<String, dynamic>? ??
|
||||
content.values.first as Map<String, dynamic>?;
|
||||
|
||||
if (jsonContent == null) return null;
|
||||
|
||||
final schema = _resolveSchema(
|
||||
jsonContent['schema'] as Map<String, dynamic>?,
|
||||
);
|
||||
|
||||
if (schema != null) {
|
||||
_requestSchemaCache[cacheKey] = schema;
|
||||
}
|
||||
|
||||
return schema;
|
||||
} catch (e) {
|
||||
DebugLogger.error(
|
||||
'Error getting request schema for $method $endpoint',
|
||||
e,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get response schema for endpoint, method, and status code
|
||||
Map<String, dynamic>? getResponseSchema(
|
||||
String endpoint,
|
||||
String method,
|
||||
int? statusCode,
|
||||
) {
|
||||
if (!isLoaded) return null;
|
||||
|
||||
final code = statusCode?.toString() ?? '200';
|
||||
final cacheKey = '${method.toUpperCase()}:$endpoint:response:$code';
|
||||
|
||||
if (_responseSchemaCache.containsKey(cacheKey)) {
|
||||
return _responseSchemaCache[cacheKey];
|
||||
}
|
||||
|
||||
try {
|
||||
final pathItem = _findPathItem(endpoint);
|
||||
if (pathItem == null) return null;
|
||||
|
||||
final operation = pathItem[method.toLowerCase()] as Map<String, dynamic>?;
|
||||
if (operation == null) return null;
|
||||
|
||||
final responses = operation['responses'] as Map<String, dynamic>?;
|
||||
if (responses == null) return null;
|
||||
|
||||
// Try to find the specific status code, or fall back to 'default' or '200'
|
||||
final response =
|
||||
responses[code] as Map<String, dynamic>? ??
|
||||
responses['default'] as Map<String, dynamic>? ??
|
||||
responses['200'] as Map<String, dynamic>?;
|
||||
|
||||
if (response == null) return null;
|
||||
|
||||
final content = response['content'] as Map<String, dynamic>?;
|
||||
if (content == null) return null;
|
||||
|
||||
// Try to find JSON content type
|
||||
final jsonContent =
|
||||
content['application/json'] as Map<String, dynamic>? ??
|
||||
content.values.first as Map<String, dynamic>?;
|
||||
|
||||
if (jsonContent == null) return null;
|
||||
|
||||
final schema = _resolveSchema(
|
||||
jsonContent['schema'] as Map<String, dynamic>?,
|
||||
);
|
||||
|
||||
if (schema != null) {
|
||||
_responseSchemaCache[cacheKey] = schema;
|
||||
}
|
||||
|
||||
return schema;
|
||||
} catch (e) {
|
||||
DebugLogger.error(
|
||||
'Error getting response schema for $method $endpoint ($code)',
|
||||
e,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Find path item that matches the given endpoint
|
||||
Map<String, dynamic>? _findPathItem(String endpoint) {
|
||||
final paths = _getPaths();
|
||||
|
||||
// Try exact match first
|
||||
if (paths.containsKey(endpoint)) {
|
||||
return paths[endpoint] as Map<String, dynamic>?;
|
||||
}
|
||||
|
||||
// Try to find parameterized routes
|
||||
for (final pathPattern in paths.keys) {
|
||||
if (_matchesPathPattern(endpoint, pathPattern)) {
|
||||
return paths[pathPattern] as Map<String, dynamic>?;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check if endpoint matches a path pattern with parameters
|
||||
bool _matchesPathPattern(String endpoint, String pattern) {
|
||||
// Convert OpenAPI path parameters {id} to regex
|
||||
final regexPattern = pattern.replaceAllMapped(
|
||||
RegExp(r'\{([^}]+)\}'),
|
||||
(match) => r'([^/]+)',
|
||||
);
|
||||
|
||||
final regex = RegExp('^$regexPattern\$');
|
||||
return regex.hasMatch(endpoint);
|
||||
}
|
||||
|
||||
/// Get paths from OpenAPI spec
|
||||
Map<String, dynamic> _getPaths() {
|
||||
return _openApiSpec?['paths'] as Map<String, dynamic>? ?? {};
|
||||
}
|
||||
|
||||
/// Resolve schema references ($ref)
|
||||
Map<String, dynamic>? _resolveSchema(Map<String, dynamic>? schema) {
|
||||
if (schema == null) return null;
|
||||
|
||||
// Handle $ref
|
||||
final ref = schema['\$ref'] as String?;
|
||||
if (ref != null) {
|
||||
return _resolveReference(ref);
|
||||
}
|
||||
|
||||
// Handle allOf, oneOf, anyOf
|
||||
if (schema.containsKey('allOf')) {
|
||||
return _mergeAllOfSchemas(schema['allOf'] as List);
|
||||
}
|
||||
|
||||
if (schema.containsKey('oneOf') || schema.containsKey('anyOf')) {
|
||||
// For now, just take the first schema in oneOf/anyOf
|
||||
final schemas = (schema['oneOf'] ?? schema['anyOf']) as List;
|
||||
if (schemas.isNotEmpty) {
|
||||
return _resolveSchema(schemas.first as Map<String, dynamic>?);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively resolve nested schemas
|
||||
final resolved = Map<String, dynamic>.from(schema);
|
||||
|
||||
if (resolved.containsKey('properties')) {
|
||||
final properties = resolved['properties'] as Map<String, dynamic>;
|
||||
final resolvedProperties = <String, dynamic>{};
|
||||
|
||||
for (final entry in properties.entries) {
|
||||
resolvedProperties[entry.key] = _resolveSchema(
|
||||
entry.value as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
resolved['properties'] = resolvedProperties;
|
||||
}
|
||||
|
||||
if (resolved.containsKey('items')) {
|
||||
resolved['items'] = _resolveSchema(
|
||||
resolved['items'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/// Resolve $ref reference
|
||||
Map<String, dynamic>? _resolveReference(String ref) {
|
||||
if (!ref.startsWith('#/')) {
|
||||
DebugLogger.warning('External references not supported: $ref');
|
||||
return null;
|
||||
}
|
||||
|
||||
final path = ref.substring(2).split('/');
|
||||
dynamic current = _openApiSpec;
|
||||
|
||||
for (final segment in path) {
|
||||
if (current is Map<String, dynamic> && current.containsKey(segment)) {
|
||||
current = current[segment];
|
||||
} else {
|
||||
DebugLogger.warning('Could not resolve reference: $ref');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return _resolveSchema(current as Map<String, dynamic>?);
|
||||
}
|
||||
|
||||
/// Merge allOf schemas
|
||||
Map<String, dynamic> _mergeAllOfSchemas(List schemas) {
|
||||
final merged = <String, dynamic>{};
|
||||
final mergedProperties = <String, dynamic>{};
|
||||
final mergedRequired = <String>[];
|
||||
|
||||
for (final schema in schemas) {
|
||||
final resolvedSchema = _resolveSchema(schema as Map<String, dynamic>?);
|
||||
if (resolvedSchema == null) continue;
|
||||
|
||||
// Merge top-level properties
|
||||
merged.addAll(resolvedSchema);
|
||||
|
||||
// Merge properties
|
||||
if (resolvedSchema.containsKey('properties')) {
|
||||
mergedProperties.addAll(
|
||||
resolvedSchema['properties'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
// Merge required fields
|
||||
if (resolvedSchema.containsKey('required')) {
|
||||
mergedRequired.addAll(
|
||||
(resolvedSchema['required'] as List).cast<String>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mergedProperties.isNotEmpty) {
|
||||
merged['properties'] = mergedProperties;
|
||||
}
|
||||
|
||||
if (mergedRequired.isNotEmpty) {
|
||||
merged['required'] = mergedRequired;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/// Pre-build cache of commonly used schemas
|
||||
Future<void> _buildSchemaCache() async {
|
||||
if (!isLoaded) return;
|
||||
|
||||
final paths = _getPaths();
|
||||
int cachedCount = 0;
|
||||
|
||||
for (final pathEntry in paths.entries) {
|
||||
final path = pathEntry.key;
|
||||
final pathItem = pathEntry.value as Map<String, dynamic>;
|
||||
|
||||
for (final method in ['get', 'post', 'put', 'delete', 'patch']) {
|
||||
if (pathItem.containsKey(method)) {
|
||||
// Cache request schema
|
||||
getRequestSchema(path, method);
|
||||
|
||||
// Cache common response schemas
|
||||
getResponseSchema(path, method, 200);
|
||||
getResponseSchema(path, method, 201);
|
||||
getResponseSchema(path, method, 400);
|
||||
getResponseSchema(path, method, 401);
|
||||
getResponseSchema(path, method, 403);
|
||||
getResponseSchema(path, method, 404);
|
||||
getResponseSchema(path, method, 422);
|
||||
getResponseSchema(path, method, 500);
|
||||
|
||||
cachedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DebugLogger.validation('Pre-cached schemas for $cachedCount operations');
|
||||
}
|
||||
|
||||
/// Get all available endpoints
|
||||
List<String> getAvailableEndpoints() {
|
||||
if (!isLoaded) return [];
|
||||
return _getPaths().keys.toList();
|
||||
}
|
||||
|
||||
/// Get available methods for an endpoint
|
||||
List<String> getAvailableMethods(String endpoint) {
|
||||
final pathItem = _findPathItem(endpoint);
|
||||
if (pathItem == null) return [];
|
||||
|
||||
return pathItem.keys
|
||||
.where(
|
||||
(key) => [
|
||||
'get',
|
||||
'post',
|
||||
'put',
|
||||
'delete',
|
||||
'patch',
|
||||
'head',
|
||||
'options',
|
||||
].contains(key),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Clear all caches
|
||||
void clearCache() {
|
||||
_requestSchemaCache.clear();
|
||||
_responseSchemaCache.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'api_validator.dart';
|
||||
import 'validation_result.dart';
|
||||
import '../utils/debug_logger.dart';
|
||||
|
||||
/// Dio interceptor for automatic API validation
|
||||
/// Validates requests and responses against OpenAPI schemas
|
||||
class ValidationInterceptor extends Interceptor {
|
||||
final ApiValidator _validator = ApiValidator();
|
||||
final bool enableRequestValidation;
|
||||
final bool enableResponseValidation;
|
||||
final bool throwOnValidationError;
|
||||
final bool logValidationResults;
|
||||
|
||||
ValidationInterceptor({
|
||||
this.enableRequestValidation = true,
|
||||
this.enableResponseValidation = true,
|
||||
this.throwOnValidationError = false,
|
||||
this.logValidationResults = true,
|
||||
});
|
||||
|
||||
/// Initialize the validator
|
||||
Future<void> initialize() async {
|
||||
await _validator.initialize();
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (enableRequestValidation && options.data != null) {
|
||||
try {
|
||||
final result = _validator.validateRequest(
|
||||
options.data,
|
||||
options.path,
|
||||
method: options.method,
|
||||
);
|
||||
|
||||
if (logValidationResults) {
|
||||
_logValidationResult(result, 'REQUEST', options.path, options.method);
|
||||
}
|
||||
|
||||
if (!result.isValid && throwOnValidationError) {
|
||||
throw ValidationException(result);
|
||||
}
|
||||
|
||||
// Transform data if validation succeeded
|
||||
// Temporarily disabled to preserve background_tasks and session_id parameters
|
||||
// if (result.isValid && options.data is Map<String, dynamic>) {
|
||||
// options.data = _validator.transformForApi(
|
||||
// options.data as Map<String, dynamic>,
|
||||
// );
|
||||
// }
|
||||
} catch (e) {
|
||||
if (e is ValidationException) {
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: options,
|
||||
error: e,
|
||||
type: DioExceptionType.unknown,
|
||||
message: 'Request validation failed: ${e.result.message}',
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
DebugLogger.error('Request validation error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
if (enableResponseValidation && response.data != null) {
|
||||
try {
|
||||
final result = _validator.validateResponse(
|
||||
response.data,
|
||||
response.requestOptions.path,
|
||||
method: response.requestOptions.method,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
|
||||
if (logValidationResults) {
|
||||
_logValidationResult(
|
||||
result,
|
||||
'RESPONSE',
|
||||
response.requestOptions.path,
|
||||
response.requestOptions.method,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
if (!result.isValid && throwOnValidationError) {
|
||||
throw ValidationException(result);
|
||||
}
|
||||
|
||||
// Transform data if validation succeeded and data is available
|
||||
if (result.isValid && result.data != null) {
|
||||
response.data = result.data;
|
||||
} else if (result.isValid && response.data is Map<String, dynamic>) {
|
||||
response.data = _validator.transformFromApi(
|
||||
response.data as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
// Store validation result in response for debugging
|
||||
if (kDebugMode) {
|
||||
response.extra['validationResult'] = result;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is ValidationException) {
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
error: e,
|
||||
type: DioExceptionType.unknown,
|
||||
message: 'Response validation failed: ${e.result.message}',
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
DebugLogger.error('Response validation error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
// Try to validate error responses too
|
||||
if (enableResponseValidation && err.response?.data != null) {
|
||||
try {
|
||||
final result = _validator.validateResponse(
|
||||
err.response!.data,
|
||||
err.requestOptions.path,
|
||||
method: err.requestOptions.method,
|
||||
statusCode: err.response!.statusCode,
|
||||
);
|
||||
|
||||
if (logValidationResults) {
|
||||
_logValidationResult(
|
||||
result,
|
||||
'ERROR_RESPONSE',
|
||||
err.requestOptions.path,
|
||||
err.requestOptions.method,
|
||||
statusCode: err.response!.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
// Transform error response data
|
||||
if (result.isValid && result.data != null) {
|
||||
err.response!.data = result.data;
|
||||
} else if (result.isValid &&
|
||||
err.response!.data is Map<String, dynamic>) {
|
||||
err.response!.data = _validator.transformFromApi(
|
||||
err.response!.data as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
// Store validation result for debugging
|
||||
if (kDebugMode) {
|
||||
err.response!.extra['validationResult'] = result;
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLogger.error('Error response validation failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
/// Log validation results in a structured format
|
||||
void _logValidationResult(
|
||||
ValidationResult result,
|
||||
String type,
|
||||
String path,
|
||||
String method, {
|
||||
int? statusCode,
|
||||
}) {
|
||||
if (!logValidationResults) return;
|
||||
|
||||
final statusText = statusCode != null ? ' ($statusCode)' : '';
|
||||
final icon = result.isValid ? '✅' : '❌';
|
||||
|
||||
DebugLogger.validation(
|
||||
'$icon Validation $type: ${method.toUpperCase()} $path$statusText - ${result.status.name}',
|
||||
);
|
||||
|
||||
if (result.hasErrors) {
|
||||
DebugLogger.error(' Errors: ${result.errors.join(', ')}');
|
||||
}
|
||||
|
||||
if (result.hasWarnings) {
|
||||
DebugLogger.warning(' Warnings: ${result.warnings.join(', ')}');
|
||||
}
|
||||
|
||||
if (result.message.isNotEmpty &&
|
||||
result.status != ValidationStatus.success) {
|
||||
DebugLogger.info(' Message: ${result.message}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get validation statistics
|
||||
Map<String, dynamic> getStats() {
|
||||
return {
|
||||
'requestValidationEnabled': enableRequestValidation,
|
||||
'responseValidationEnabled': enableResponseValidation,
|
||||
'throwOnError': throwOnValidationError,
|
||||
'loggingEnabled': logValidationResults,
|
||||
'validatorInitialized': _validator.isInitialized,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/// Result of API validation operations
|
||||
class ValidationResult {
|
||||
const ValidationResult._({
|
||||
required this.isValid,
|
||||
required this.status,
|
||||
required this.message,
|
||||
this.errors = const [],
|
||||
this.warnings = const [],
|
||||
this.data,
|
||||
});
|
||||
|
||||
const ValidationResult.success(
|
||||
String message, {
|
||||
dynamic data,
|
||||
List<String> warnings = const [],
|
||||
}) : this._(
|
||||
isValid: true,
|
||||
status: ValidationStatus.success,
|
||||
message: message,
|
||||
warnings: warnings,
|
||||
data: data,
|
||||
);
|
||||
|
||||
const ValidationResult.warning(
|
||||
String message, {
|
||||
List<String> warnings = const [],
|
||||
dynamic data,
|
||||
}) : this._(
|
||||
isValid: true,
|
||||
status: ValidationStatus.warning,
|
||||
message: message,
|
||||
warnings: warnings,
|
||||
data: data,
|
||||
);
|
||||
|
||||
const ValidationResult.error(
|
||||
String message, {
|
||||
List<String> errors = const [],
|
||||
List<String> warnings = const [],
|
||||
}) : this._(
|
||||
isValid: false,
|
||||
status: ValidationStatus.error,
|
||||
message: message,
|
||||
errors: errors,
|
||||
warnings: warnings,
|
||||
);
|
||||
|
||||
final bool isValid;
|
||||
final ValidationStatus status;
|
||||
final String message;
|
||||
final List<String> errors;
|
||||
final List<String> warnings;
|
||||
final dynamic data;
|
||||
|
||||
bool get hasWarnings => warnings.isNotEmpty;
|
||||
bool get hasErrors => errors.isNotEmpty;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.write('ValidationResult(');
|
||||
buffer.write('status: $status, ');
|
||||
buffer.write('message: $message');
|
||||
|
||||
if (hasErrors) {
|
||||
buffer.write(', errors: ${errors.length}');
|
||||
}
|
||||
|
||||
if (hasWarnings) {
|
||||
buffer.write(', warnings: ${warnings.length}');
|
||||
}
|
||||
|
||||
buffer.write(')');
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Convert to a detailed map for logging/debugging
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'isValid': isValid,
|
||||
'status': status.name,
|
||||
'message': message,
|
||||
'errors': errors,
|
||||
'warnings': warnings,
|
||||
'hasData': data != null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum ValidationStatus { success, warning, error }
|
||||
|
||||
/// Exception thrown when validation fails critically
|
||||
class ValidationException implements Exception {
|
||||
const ValidationException(this.result);
|
||||
|
||||
final ValidationResult result;
|
||||
|
||||
@override
|
||||
String toString() => 'ValidationException: ${result.message}';
|
||||
}
|
||||
@@ -80,7 +80,6 @@ flutter:
|
||||
|
||||
assets:
|
||||
- assets/icons/
|
||||
- assets/openapi.json
|
||||
|
||||
flutter_native_splash:
|
||||
# Background color (Conduit dark theme)
|
||||
|
||||
Reference in New Issue
Block a user