# API Request Architecture Documentation ## Overview This document explains how to create and handle JSON-based API requests in the Translator application. The architecture has been refactored to use a unified, type-safe template system that ensures consistency and maintainability. ## Architecture Components ### 1. JsonHelper **Location**: `app/src/main/java/eu/gaudian/translator/utils/JsonHelper.kt` The JsonHelper provides unified JSON parsing, validation, and error handling for all API responses. **Key Methods**: ```kotlin // Parse JSON with comprehensive error handling fun parseJson(json: String, serializer: KSerializer, serviceName: String): Result // Validate required fields exist in JSON fun validateRequiredFields(json: String, requiredFields: List): Boolean // Extract specific field value fun extractField(json: String, fieldName: String): String? // Clean and validate JSON string fun cleanAndValidateJson(json: String): String ``` ### 2. ApiRequestHandler **Location**: `app/src/main/java/eu/gaudian/translator/utils/ApiRequestHandler.kt` The single entry point for all API calls. This is the ONLY component that should call `ApiManager.getCompletion()`. **Key Methods**: ```kotlin // Preferred method - use templates suspend fun executeRequest(template: ApiRequestTemplate): Result // Legacy method - deprecated suspend fun executeRequest(prompt: String, serializer: KSerializer, modelType: ModelType): Result // Text-only requests suspend fun executeTextRequest(prompt: String, modelType: ModelType): Result ``` ### 3. ApiRequestTemplates **Location**: `app/src/main/java/eu/gaudian/translator/utils/ApiRequestTemplates.kt` Type-safe request templates that define the structure and validation for different API operations. ## Creating New JSON API Requests ### Step 1: Define Your Response Data Class Create a serializable data class for your API response: ```kotlin @kotlinx.serialization.Serializable data class MyApiResponse( val requiredField: String, val optionalField: Int? = null, val items: List = emptyList() ) @kotlinx.serialization.Serializable data class MyItem( val id: String, val name: String, val value: Double ) ``` ### Step 2: Create a Request Template Extend `BaseApiRequestTemplate` for type-safe requests: ```kotlin class MyApiRequest( private val parameter1: String, private val parameter2: Int, private val language: String ) : BaseApiRequestTemplate() { override val responseSerializer = MyApiResponse.serializer() override val modelType = ModelType.TRANSLATION // Choose appropriate type override val serviceName = "MyService" override val requiredFields = listOf("requiredField") // Fields that must exist init { promptBuilder.basePrompt = "Perform my API operation with $parameter1 and $parameter2" addDetail("Language: $language") addDetail("Parameter 2 value: $parameter2") withJsonResponse("a JSON object with 'requiredField' (string), 'optionalField' (integer), and 'items' array") } } ``` ### Step 3: Use the Template in Your Service ```kotlin class MyService(context: Context) { private val apiRequestHandler = ApiRequestHandler(ApiManager(context), context) suspend fun performMyOperation(param1: String, param2: Int, language: String): Result { return try { Log.i("MyService", "Performing operation with $param1, $param2 in $language") val template = MyApiRequest( parameter1 = param1, parameter2 = param2, language = language ) val result = apiRequestHandler.executeRequest(template) result.map { response -> Log.i("MyService", "Successfully completed operation") response }.onFailure { exception -> Log.e("MyService", "Failed to perform operation", exception) } } catch (e: Exception) { Log.e("MyService", "Unexpected error in performMyOperation", e) Result.failure(e) } } } ``` ## Available Model Types Choose the appropriate `ModelType` for your request: - `ModelType.TRANSLATION` - For translation-related requests - `ModelType.DICTIONARY` - For dictionary and vocabulary requests - `ModelType.VOCABULARY` - For vocabulary generation and analysis - `ModelType.EXERCISE` - For exercise generation - `ModelType.CORRECTION` - For text correction ## JSON Response Structure Guidelines ### 1. Always Define Required Fields ```kotlin override val requiredFields = listOf("id", "name", "data") ``` ### 2. Use Clear Field Names ```kotlin // Good data class Response(val userId: String, val userName: String, val isActive: Boolean) // Avoid data class Response(val uid: String, val nm: String, val active: Boolean) ``` ### 3. Include Type Information in Prompts ```kotlin withJsonResponse("a JSON object with 'userId' (string), 'userName' (string), 'isActive' (boolean), and 'createdAt' (timestamp)") ``` ## Error Handling Best Practices ### 1. Always Wrap in Try-Catch ```kotlin suspend fun myMethod(): Result = withContext(Dispatchers.IO) { try { // Your logic here result.map { response -> // Success handling response }.onFailure { exception -> // Failure logging Log.e("MyService", "Operation failed", exception) } } catch (e: Exception) { Log.e("MyService", "Unexpected error", e) Result.failure(e) } } ``` ### 2. Log at Appropriate Levels ```kotlin Log.i("ServiceName", "Starting operation with parameters: $param1, $param2") // Info level Log.d("ServiceName", "Generated prompt: $prompt") // Debug level Log.e("ServiceName", "Operation failed", exception) // Error level ``` ### 3. Validate Responses ```kotlin // The template automatically validates required fields // But you can add custom validation if needed result.map { response -> if (response.items.isEmpty()) { Log.w("MyService", "API returned empty items array") } response } ``` ## Testing Your API Requests ### 1. Unit Test Your Template ```kotlin @Test fun `MyApiRequest should build correct prompt`() { val template = MyApiRequest("test", 42, "English") val prompt = template.buildPrompt() assertTrue(prompt.contains("test")) assertTrue(prompt.contains("42")) assertTrue(prompt.contains("English")) assertTrue(prompt.contains("JSON object")) } ``` ### 2. Test JSON Parsing ```kotlin @Test fun `JsonHelper should parse valid response`() { val json = """{"requiredField": "value", "optionalField": 123}""" val result = jsonHelper.validateRequiredFields(json, listOf("requiredField")) assertTrue(result) } ``` ### 3. Integration Test ```kotlin @Test fun `end to end API request should work`() = runTest { // Mock the API manager every { apiManager.getCompletion( prompt = any(), callback = any(), modelType = ModelType.TRANSLATION ) } answers { val callback = thirdArg<(String?) -> Unit>() callback("""{"requiredField": "test", "optionalField": 123}""") } val template = MyApiRequest("test", 42, "English") val result = apiRequestHandler.executeRequest(template) assertTrue(result.isSuccess) assertEquals("test", result.getOrNull()?.requiredField) } ``` ## Migration from Legacy Code ### Before (Legacy): ```kotlin val prompt = PromptBuilder("Do something") .addDetail("Parameter: $param") .withJsonResponse("JSON format") .build() apiRequestHandler.executeRequest(prompt, MyResponse.serializer(), ModelType.TRANSLATION) ``` ### After (New Architecture): ```kotlin val template = MyApiRequest(param) apiRequestHandler.executeRequest(template) ``` ## Common Patterns ### 1. Language-Specific Requests ```kotlin class LanguageSpecificRequest( private val text: String, private val sourceLanguage: String, private val targetLanguage: String ) : BaseApiRequestTemplate() { override val serviceName = "TranslationService" init { promptBuilder.basePrompt = "Translate from $sourceLanguage to $targetLanguage: '$text'" withJsonResponse("a JSON object with 'translatedText' containing the translation") } } ``` ### 2. Batch Processing Requests ```kotlin class BatchProcessingRequest( private val items: List, private val operation: String ) : BaseApiRequestTemplate() { override val serviceName = "BatchService" init { val itemsJson = Json.encodeToString(items) promptBuilder.basePrompt = "Perform $operation on these items: $itemsJson" withJsonResponse("a JSON object with 'results' array containing processed items") } } ``` ### 3. Configuration-Based Requests ```kotlin class ConfigurableRequest( private val baseText: String, private val customInstructions: String, private val outputFormat: String ) : BaseApiRequestTemplate() { override val serviceName = "ConfigurableService" init { promptBuilder.basePrompt = baseText addDetail(customInstructions) withJsonResponse(outputFormat) } } ``` ## Troubleshooting ### Common Issues and Solutions 1. **"Unresolved reference" errors** - Make sure you're calling the correct method (private methods stay within their class) - Check imports are correct 2. **JSON parsing failures** - Use `jsonHelper.validateRequiredFields()` to check structure first - Check that your response data class matches the actual JSON structure - Look at logs for detailed parsing error messages 3. **Template not working** - Verify all required fields are listed in `requiredFields` - Check that the prompt clearly explains the expected JSON format - Ensure the serializer matches your data class 4. **API request timeouts** - Check network connectivity - Verify the API manager is properly initialized - Look at API logs for detailed error information ## Best Practices Summary 1. **Always use templates** for new API requests 2. **Define required fields** to catch malformed responses early 3. **Log comprehensively** at appropriate levels 4. **Handle errors gracefully** with proper Result types 5. **Write tests** for both templates and parsing logic 6. **Keep prompts clear** and specific about JSON structure 7. **Use descriptive service names** for better logging 8. **Validate inputs** before making API requests ## Future Considerations When extending the architecture: 1. **Reuse existing templates** when possible 2. **Follow naming conventions** consistently 3. **Document custom logic** in comments 4. **Add comprehensive tests** for new functionality 5. **Consider backward compatibility** when changing existing APIs This architecture is designed to be easily extensible while maintaining type safety and consistency across all API operations.