Files
Polly/docs/API_REQUEST_ARCHITECTURE.md
2026-02-13 00:15:36 +01:00

11 KiB

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:

// Parse JSON with comprehensive error handling
fun <T> parseJson(json: String, serializer: KSerializer<T>, serviceName: String): Result<T>

// Validate required fields exist in JSON
fun validateRequiredFields(json: String, requiredFields: List<String>): 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:

// Preferred method - use templates
suspend fun <T> executeRequest(template: ApiRequestTemplate<T>): Result<T>

// Legacy method - deprecated
suspend fun <T> executeRequest(prompt: String, serializer: KSerializer<T>, modelType: ModelType): Result<T>

// Text-only requests
suspend fun executeTextRequest(prompt: String, modelType: ModelType): Result<String>

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:

@kotlinx.serialization.Serializable
data class MyApiResponse(
    val requiredField: String,
    val optionalField: Int? = null,
    val items: List<MyItem> = emptyList()
)

@kotlinx.serialization.Serializable
data class MyItem(
    val id: String,
    val name: String,
    val value: Double
)

Step 2: Create a Request Template

Extend BaseApiRequestTemplate<T> for type-safe requests:

class MyApiRequest(
    private val parameter1: String,
    private val parameter2: Int,
    private val language: String
) : BaseApiRequestTemplate<MyApiResponse>() {
    
    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

class MyService(context: Context) {
    private val apiRequestHandler = ApiRequestHandler(ApiManager(context), context)
    
    suspend fun performMyOperation(param1: String, param2: Int, language: String): Result<MyApiResponse> {
        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

override val requiredFields = listOf("id", "name", "data")

2. Use Clear Field Names

// 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

withJsonResponse("a JSON object with 'userId' (string), 'userName' (string), 'isActive' (boolean), and 'createdAt' (timestamp)")

Error Handling Best Practices

1. Always Wrap in Try-Catch

suspend fun myMethod(): Result<MyType> = 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

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

// 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

@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

@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

@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):

val prompt = PromptBuilder("Do something")
    .addDetail("Parameter: $param")
    .withJsonResponse("JSON format")
    .build()

apiRequestHandler.executeRequest(prompt, MyResponse.serializer(), ModelType.TRANSLATION)

After (New Architecture):

val template = MyApiRequest(param)
apiRequestHandler.executeRequest(template)

Common Patterns

1. Language-Specific Requests

class LanguageSpecificRequest(
    private val text: String,
    private val sourceLanguage: String,
    private val targetLanguage: String
) : BaseApiRequestTemplate<TranslationResponse>() {
    
    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

class BatchProcessingRequest(
    private val items: List<String>,
    private val operation: String
) : BaseApiRequestTemplate<BatchResponse>() {
    
    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

class ConfigurableRequest(
    private val baseText: String,
    private val customInstructions: String,
    private val outputFormat: String
) : BaseApiRequestTemplate<ConfigurableResponse>() {
    
    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.