369 lines
11 KiB
Markdown
369 lines
11 KiB
Markdown
# 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 <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**:
|
|
```kotlin
|
|
// 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:
|
|
|
|
```kotlin
|
|
@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:
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
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
|
|
```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<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
|
|
```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<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
|
|
```kotlin
|
|
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
|
|
```kotlin
|
|
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.
|