migrate to gitea
This commit is contained in:
368
docs/API_REQUEST_ARCHITECTURE.md
Normal file
368
docs/API_REQUEST_ARCHITECTURE.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user