Merge branch 'development/6/implement-full-crud-for-locations-and-labels' into main, accepting all changes from the feature branch
This commit is contained in:
@@ -62,6 +62,9 @@ dependencies {
|
||||
implementation(Libs.hiltAndroid)
|
||||
kapt(Libs.hiltCompiler)
|
||||
|
||||
// [DEPENDENCY] Logging
|
||||
implementation(Libs.timber)
|
||||
|
||||
// [DEPENDENCY] Testing
|
||||
testImplementation(Libs.junit)
|
||||
androidTestImplementation(Libs.extJunit)
|
||||
|
||||
@@ -1,74 +1,97 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api
|
||||
// [FILE] HomeboxApiService.kt
|
||||
// [SEMANTICS] data, api, retrofit
|
||||
package com.homebox.lens.data.api
|
||||
|
||||
import com.homebox.lens.data.api.dto.GroupStatisticsDto
|
||||
import com.homebox.lens.data.api.dto.ItemCreateDto
|
||||
import com.homebox.lens.data.api.dto.ItemOutDto
|
||||
import com.homebox.lens.data.api.dto.ItemSummaryDto
|
||||
import com.homebox.lens.data.api.dto.ItemUpdateDto
|
||||
import com.homebox.lens.data.api.dto.LabelCreateDto
|
||||
import com.homebox.lens.data.api.dto.LabelOutDto
|
||||
import com.homebox.lens.data.api.dto.LabelSummaryDto
|
||||
import com.homebox.lens.data.api.dto.LocationOutCountDto
|
||||
import com.homebox.lens.data.api.dto.LoginFormDto
|
||||
import com.homebox.lens.data.api.dto.PaginationResultDto
|
||||
import com.homebox.lens.data.api.dto.TokenResponseDto
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.dto.*
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
import retrofit2.http.*
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Interface('HomeboxApiService')]
|
||||
/**
|
||||
* [ENTITY: Interface('HomeboxApiService')]
|
||||
* [PURPOSE] Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
|
||||
* @summary Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
|
||||
*/
|
||||
interface HomeboxApiService {
|
||||
|
||||
// [ENDPOINT] Auth
|
||||
// [ENTITY: ApiEndpoint('login')]
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("v1/users/login")
|
||||
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
|
||||
// [END_ENTITY: ApiEndpoint('login')]
|
||||
|
||||
// [ENDPOINT] Items
|
||||
// [ENTITY: ApiEndpoint('getItems')]
|
||||
@GET("v1/items")
|
||||
suspend fun getItems(
|
||||
@Query("q") query: String? = null,
|
||||
@Query("page") page: Int? = null,
|
||||
@Query("pageSize") pageSize: Int? = null
|
||||
): PaginationResultDto<ItemSummaryDto>
|
||||
// [END_ENTITY: ApiEndpoint('getItems')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('createItem')]
|
||||
@POST("v1/items")
|
||||
suspend fun createItem(@Body item: ItemCreateDto): ItemSummaryDto
|
||||
// [END_ENTITY: ApiEndpoint('createItem')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('getItem')]
|
||||
@GET("v1/items/{id}")
|
||||
suspend fun getItem(@Path("id") itemId: String): ItemOutDto
|
||||
// [END_ENTITY: ApiEndpoint('getItem')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('updateItem')]
|
||||
@PUT("v1/items/{id}")
|
||||
suspend fun updateItem(@Path("id") itemId: String, @Body item: ItemUpdateDto): ItemOutDto
|
||||
// [END_ENTITY: ApiEndpoint('updateItem')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('deleteItem')]
|
||||
@DELETE("v1/items/{id}")
|
||||
suspend fun deleteItem(@Path("id") itemId: String): Response<Unit>
|
||||
// [END_ENTITY: ApiEndpoint('deleteItem')]
|
||||
|
||||
// [ENDPOINT] Locations
|
||||
// [ENTITY: ApiEndpoint('getLocations')]
|
||||
@GET("v1/locations")
|
||||
suspend fun getLocations(): List<LocationOutCountDto>
|
||||
// [END_ENTITY: ApiEndpoint('getLocations')]
|
||||
|
||||
// [ENDPOINT] Labels
|
||||
// [ENTITY: ApiEndpoint('getLabels')]
|
||||
@GET("v1/labels")
|
||||
suspend fun getLabels(): List<LabelOutDto>
|
||||
// [END_ENTITY: ApiEndpoint('getLabels')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('createLabel')]
|
||||
@POST("v1/labels")
|
||||
suspend fun createLabel(@Body newLabel: LabelCreateDto): LabelSummaryDto
|
||||
// [END_ENTITY: ApiEndpoint('createLabel')]
|
||||
|
||||
// [ENDPOINT] Statistics
|
||||
// [ENTITY: ApiEndpoint('updateLabel')]
|
||||
@PUT("v1/labels/{id}")
|
||||
suspend fun updateLabel(@Path("id") labelId: String, @Body label: LabelUpdateDto): LabelOutDto
|
||||
// [END_ENTITY: ApiEndpoint('updateLabel')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('deleteLabel')]
|
||||
@DELETE("v1/labels/{id}")
|
||||
suspend fun deleteLabel(@Path("id") labelId: String): Response<Unit>
|
||||
|
||||
// [ENTITY: ApiEndpoint('createLocation')]
|
||||
@POST("v1/locations")
|
||||
suspend fun createLocation(@Body newLocation: LocationCreateDto): LocationOutDto
|
||||
// [END_ENTITY: ApiEndpoint('createLocation')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('updateLocation')]
|
||||
@PUT("v1/locations/{id}")
|
||||
suspend fun updateLocation(@Path("id") locationId: String, @Body location: LocationUpdateDto): LocationOutDto
|
||||
// [END_ENTITY: ApiEndpoint('updateLocation')]
|
||||
|
||||
// [ENTITY: ApiEndpoint('deleteLocation')]
|
||||
@DELETE("v1/locations/{id}")
|
||||
suspend fun deleteLocation(@Path("id") locationId: String): Response<Unit>
|
||||
|
||||
// [ENTITY: ApiEndpoint('getStatistics')]
|
||||
@GET("v1/groups/statistics")
|
||||
suspend fun getStatistics(): GroupStatisticsDto
|
||||
// [END_ENTITY: ApiEndpoint('getStatistics')]
|
||||
}
|
||||
// [END_FILE_HomeboxApiService.kt]
|
||||
// [END_ENTITY: Interface('HomeboxApiService')]
|
||||
// [END_FILE_HomeboxApiService.kt]
|
||||
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.CustomField
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('CustomFieldDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для кастомного поля.
|
||||
* @summary DTO для кастомного поля.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomFieldDto(
|
||||
@@ -20,10 +20,12 @@ data class CustomFieldDto(
|
||||
@Json(name = "value") val value: String,
|
||||
@Json(name = "type") val type: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('CustomFieldDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('CustomField')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из CustomFieldDto в доменную модель CustomField.
|
||||
* @summary Маппер из CustomFieldDto в доменную модель CustomField.
|
||||
*/
|
||||
fun CustomFieldDto.toDomain(): CustomField {
|
||||
return CustomField(
|
||||
@@ -32,3 +34,4 @@ fun CustomFieldDto.toDomain(): CustomField {
|
||||
type = this.type
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,14 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.GroupStatistics
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('GroupStatisticsDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для статистики.
|
||||
* [COHERENCE_NOTE] Этот DTO был исправлен, чтобы точно соответствовать JSON-ответу от сервера.
|
||||
* Поля `items`, `labels`, `locations`, `totalValue` были заменены на `totalItems`, `totalLabels`,
|
||||
* `totalLocations`, `totalItemPrice` и т.д., чтобы устранить ошибку парсинга `JsonDataException`.
|
||||
* @summary DTO для статистики.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GroupStatisticsDto(
|
||||
@@ -23,19 +20,17 @@ data class GroupStatisticsDto(
|
||||
@Json(name = "totalLabels") val totalLabels: Int,
|
||||
@Json(name = "totalLocations") val totalLocations: Int,
|
||||
@Json(name = "totalItemPrice") val totalItemPrice: Double,
|
||||
// [FIX] Добавляем недостающие поля, которые присутствуют в JSON, но отсутствовали в DTO.
|
||||
// Делаем их nullable на случай, если API перестанет их присылать в будущем.
|
||||
@Json(name = "totalUsers") val totalUsers: Int? = null,
|
||||
@Json(name = "totalWithWarranty") val totalWithWarranty: Int? = null
|
||||
)
|
||||
// [END_ENTITY: DataClass('GroupStatisticsDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('GroupStatistics')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
|
||||
* [COHERENCE_NOTE] Маппер обновлен для использования правильных полей из исправленного DTO.
|
||||
* @summary Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
|
||||
*/
|
||||
fun GroupStatisticsDto.toDomain(): GroupStatistics {
|
||||
// [ACTION] Маппим данные из DTO в доменную модель.
|
||||
return GroupStatistics(
|
||||
items = this.totalItems,
|
||||
labels = this.totalLabels,
|
||||
@@ -43,4 +38,5 @@ fun GroupStatisticsDto.toDomain(): GroupStatistics {
|
||||
totalValue = this.totalItemPrice
|
||||
)
|
||||
}
|
||||
// [END_FILE_GroupStatisticsDto.kt]
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_GroupStatisticsDto.kt]
|
||||
|
||||
@@ -8,14 +8,14 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.Image
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ImageDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для изображения.
|
||||
* @property id Уникальный идентификатор.
|
||||
* @property path Путь к файлу.
|
||||
* @property isPrimary Является ли основным.
|
||||
* @summary DTO для изображения.
|
||||
* @param id Уникальный идентификатор.
|
||||
* @param path Путь к файлу.
|
||||
* @param isPrimary Является ли основным.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ImageDto(
|
||||
@@ -23,10 +23,12 @@ data class ImageDto(
|
||||
@Json(name = "path") val path: String,
|
||||
@Json(name = "isPrimary") val isPrimary: Boolean
|
||||
)
|
||||
// [END_ENTITY: DataClass('ImageDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('Image')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из ImageDto в доменную модель Image.
|
||||
* @summary Маппер из ImageDto в доменную модель Image.
|
||||
*/
|
||||
fun ImageDto.toDomain(): Image {
|
||||
return Image(
|
||||
@@ -35,3 +37,4 @@ fun ImageDto.toDomain(): Image {
|
||||
isPrimary = this.isPrimary
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemAttachment
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemAttachmentDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для вложения.
|
||||
* @summary DTO для вложения.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemAttachmentDto(
|
||||
@@ -23,10 +23,12 @@ data class ItemAttachmentDto(
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemAttachmentDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemAttachment')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
|
||||
* @summary Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
|
||||
*/
|
||||
fun ItemAttachmentDto.toDomain(): ItemAttachment {
|
||||
return ItemAttachment(
|
||||
@@ -38,3 +40,4 @@ fun ItemAttachmentDto.toDomain(): ItemAttachment {
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemCreate
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemCreateDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для создания вещи.
|
||||
* @summary DTO для создания вещи.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemCreateDto(
|
||||
@@ -30,10 +30,12 @@ data class ItemCreateDto(
|
||||
@Json(name = "parentId") val parentId: String?,
|
||||
@Json(name = "labelIds") val labelIds: List<String>?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemCreateDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из доменной модели ItemCreate в ItemCreateDto.
|
||||
* @summary Маппер из доменной модели ItemCreate в ItemCreateDto.
|
||||
*/
|
||||
fun ItemCreate.toDto(): ItemCreateDto {
|
||||
return ItemCreateDto(
|
||||
@@ -52,3 +54,4 @@ fun ItemCreate.toDto(): ItemCreateDto {
|
||||
labelIds = this.labelIds
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
@@ -1,16 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] ItemDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('ItemOut')]
|
||||
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
|
||||
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LabelOutDto')]
|
||||
/**
|
||||
* [ENTITY: DataClass('ItemOut')]
|
||||
* [PURPOSE] DTO для полной информации о вещи (GET /v1/items/{id}).
|
||||
* @summary DTO для полной информации о вещи (GET /v1/items/{id}).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemOut(
|
||||
@@ -23,10 +26,12 @@ data class ItemOut(
|
||||
@Json(name = "value") val value: BigDecimal?,
|
||||
@Json(name = "createdAt") val createdAt: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemOut')]
|
||||
|
||||
// [ENTITY: DataClass('ItemSummary')]
|
||||
// [RELATION: DataClass('ItemSummary')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
|
||||
/**
|
||||
* [ENTITY: DataClass('ItemSummary')]
|
||||
* [PURPOSE] DTO для краткой информации о вещи в списках (GET /v1/items).
|
||||
* @summary DTO для краткой информации о вещи в списках (GET /v1/items).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemSummary(
|
||||
@@ -36,10 +41,11 @@ data class ItemSummary(
|
||||
@Json(name = "location") val location: LocationOut?,
|
||||
@Json(name = "createdAt") val createdAt: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemSummary')]
|
||||
|
||||
// [ENTITY: DataClass('ItemCreate')]
|
||||
/**
|
||||
* [ENTITY: DataClass('ItemCreate')]
|
||||
* [PURPOSE] DTO для создания новой вещи (POST /v1/items).
|
||||
* @summary DTO для создания новой вещи (POST /v1/items).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemCreate(
|
||||
@@ -49,10 +55,11 @@ data class ItemCreate(
|
||||
@Json(name = "labelIds") val labelIds: List<String>?,
|
||||
@Json(name = "value") val value: BigDecimal?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemCreate')]
|
||||
|
||||
// [ENTITY: DataClass('ItemUpdate')]
|
||||
/**
|
||||
* [ENTITY: DataClass('ItemUpdate')]
|
||||
* [PURPOSE] DTO для обновления вещи (PUT /v1/items/{id}).
|
||||
* @summary DTO для обновления вещи (PUT /v1/items/{id}).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemUpdate(
|
||||
@@ -62,5 +69,6 @@ data class ItemUpdate(
|
||||
@Json(name = "labelIds") val labelIds: List<String>?,
|
||||
@Json(name = "value") val value: BigDecimal?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemUpdate')]
|
||||
|
||||
// [END_FILE_ItemDto.kt]
|
||||
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemOut
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemOutDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для полной модели вещи.
|
||||
* @summary DTO для полной модели вещи.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemOutDto(
|
||||
@@ -39,10 +39,12 @@ data class ItemOutDto(
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemOutDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemOut')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из ItemOutDto в доменную модель ItemOut.
|
||||
* @summary Маппер из ItemOutDto в доменную модель ItemOut.
|
||||
*/
|
||||
fun ItemOutDto.toDomain(): ItemOut {
|
||||
return ItemOut(
|
||||
@@ -70,3 +72,4 @@ fun ItemOutDto.toDomain(): ItemOut {
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemSummary
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemSummaryDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для сокращенной модели вещи.
|
||||
* @summary DTO для сокращенной модели вещи.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemSummaryDto(
|
||||
@@ -27,10 +27,12 @@ data class ItemSummaryDto(
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemSummaryDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из ItemSummaryDto в доменную модель ItemSummary.
|
||||
* @summary Маппер из ItemSummaryDto в доменную модель ItemSummary.
|
||||
*/
|
||||
fun ItemSummaryDto.toDomain(): ItemSummary {
|
||||
return ItemSummary(
|
||||
@@ -46,3 +48,4 @@ fun ItemSummaryDto.toDomain(): ItemSummary {
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.ItemUpdate
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('ItemUpdateDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для обновления вещи.
|
||||
* @summary DTO для обновления вещи.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ItemUpdateDto(
|
||||
@@ -31,10 +31,12 @@ data class ItemUpdateDto(
|
||||
@Json(name = "parentId") val parentId: String?,
|
||||
@Json(name = "labelIds") val labelIds: List<String>?
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemUpdateDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemUpdateDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из доменной модели ItemUpdate в ItemUpdateDto.
|
||||
* @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto.
|
||||
*/
|
||||
fun ItemUpdate.toDto(): ItemUpdateDto {
|
||||
return ItemUpdateDto(
|
||||
@@ -54,3 +56,4 @@ fun ItemUpdate.toDto(): ItemUpdateDto {
|
||||
labelIds = this.labelIds
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
@@ -3,21 +3,23 @@
|
||||
// [SEMANTICS] data_transfer_object, label, create, api
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LabelCreateDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для тела запроса на создание метки (POST /v1/labels).
|
||||
* @property name Название метки.
|
||||
* @property color Цвет метки в формате HEX (например, "#FF0000").
|
||||
* @property description Описание метки.
|
||||
* @coherence_note Структура этого класса точно соответствует схеме `repo.LabelCreate` из OpenAPI.
|
||||
* @summary DTO для тела запроса на создание метки (POST /v1/labels).
|
||||
* @param name Название метки.
|
||||
* @param color Цвет метки в формате HEX (например, "#FF0000").
|
||||
* @param description Описание метки.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelCreateDto(
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "color") val color: String?,
|
||||
@Json(name = "description") val description: String? = null // Описание не используется в приложении, но может быть в API
|
||||
@Json(name = "description") val description: String? = null // [AI_NOTE]: Описание не используется в приложении, но может быть в API
|
||||
)
|
||||
// [END_FILE_LabelCreateDto.kt]
|
||||
// [END_ENTITY: DataClass('LabelCreateDto')]
|
||||
// [END_FILE_LabelCreateDto.kt]
|
||||
|
||||
@@ -8,44 +8,38 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('LabelOutDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для метки.
|
||||
* [COHERENCE_NOTE] Поле `isArchived` сделано nullable (`Boolean?`),
|
||||
* так как оно отсутствует в JSON-ответе от сервера. Это исправляет ошибку парсинга
|
||||
* `JsonDataException: Required value 'isArchived' missing`.
|
||||
* @summary DTO для метки.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelOutDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
// [COHERENCE_NOTE] Поле `color` может быть null или отсутствовать, делаем его nullable для безопасности.
|
||||
@Json(name = "color") val color: String?,
|
||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
||||
@Json(name = "isArchived") val isArchived: Boolean?,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
// [COHERENCE_NOTE] Поле `description` также может отсутствовать, добавляем его как nullable.
|
||||
@Json(name = "description") val description: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelOutDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из LabelOutDto в доменную модель LabelOut.
|
||||
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
|
||||
* и предоставления non-nullable значений по умолчанию для доменной модели.
|
||||
* @summary Маппер из LabelOutDto в доменную модель LabelOut.
|
||||
*/
|
||||
fun LabelOutDto.toDomain(): LabelOut {
|
||||
return LabelOut(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию.
|
||||
color = this.color ?: "", // Пустая строка как дефолтный цвет
|
||||
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
|
||||
color = this.color ?: "",
|
||||
isArchived = this.isArchived ?: false,
|
||||
createdAt = this.createdAt,
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_FILE_LabelOutDto.kt]
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_LabelOutDto.kt]
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
// [SEMANTICS] data_transfer_object, label, summary, api, mapper
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.LabelSummary
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LabelSummaryDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для ответа от API при создании метки.
|
||||
* @coherence_note Структура этого класса точно соответствует схеме `repo.LabelSummary` из OpenAPI.
|
||||
* @summary DTO для ответа от API при создании метки.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelSummaryDto(
|
||||
@@ -21,9 +22,11 @@ data class LabelSummaryDto(
|
||||
@Json(name = "createdAt") val createdAt: String?,
|
||||
@Json(name = "updatedAt") val updatedAt: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelSummaryDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelSummary')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Маппер из DTO в доменную модель.
|
||||
* @return Объект доменной модели [LabelSummary].
|
||||
* @sideeffect Отбрасывает поля, ненужные доменному слою (`color`, `description`, etc.),
|
||||
@@ -35,4 +38,5 @@ fun LabelSummaryDto.toDomain(): LabelSummary {
|
||||
name = this.name
|
||||
)
|
||||
}
|
||||
// [END_FILE_LabelSummaryDto.kt]
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_LabelSummaryDto.kt]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LabelUpdateDto.kt
|
||||
// [SEMANTICS] data_transfer_object, label, update
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LabelUpdate
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LabelUpdateDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelUpdateDto(
|
||||
@Json(name = "name")
|
||||
val name: String?,
|
||||
@Json(name = "color")
|
||||
val color: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelUpdateDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
|
||||
fun LabelUpdate.toDto(): LabelUpdateDto {
|
||||
return LabelUpdateDto(
|
||||
name = this.name,
|
||||
color = this.color
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
// [END_FILE_LabelUpdateDto.kt]
|
||||
@@ -0,0 +1,22 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LocationCreateDto.kt
|
||||
// [SEMANTICS] data_transfer_object, location, create
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LocationCreateDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationCreateDto(
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
@Json(name = "color")
|
||||
val color: String?,
|
||||
@Json(name = "description")
|
||||
val description: String? // Assuming description can be null for creation
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationCreateDto')]
|
||||
// [END_FILE_LocationCreateDto.kt]
|
||||
@@ -1,25 +1,27 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LocationDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, location
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('LocationOut')]
|
||||
/**
|
||||
* [ENTITY: DataClass('LocationOut')]
|
||||
* [PURPOSE] DTO для информации о местоположении.
|
||||
* @summary DTO для информации о местоположении.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationOut(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationOut')]
|
||||
|
||||
// [ENTITY: DataClass('LocationOutCount')]
|
||||
/**
|
||||
* [ENTITY: DataClass('LocationOutCount')]
|
||||
* [PURPOSE] DTO для информации о местоположении со счетчиком вещей.
|
||||
* @summary DTO для информации о местоположении со счетчиком вещей.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationOutCount(
|
||||
@@ -27,5 +29,6 @@ data class LocationOutCount(
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "itemCount") val itemCount: Int
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationOutCount')]
|
||||
|
||||
// [END_FILE_LocationDto.kt]
|
||||
// [END_FILE_LocationDto.kt]
|
||||
@@ -8,47 +8,40 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('LocationOutCountDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для местоположения со счетчиком.
|
||||
* [COHERENCE_NOTE] Поля `color` и `isArchived` сделаны nullable (`String?`, `Boolean?`),
|
||||
* так как они отсутствуют в JSON-ответе от сервера. Это исправляет ошибку парсинга
|
||||
* `JsonDataException: Required value '...' missing`.
|
||||
* @summary DTO для местоположения со счетчиком.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationOutCountDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
||||
@Json(name = "color") val color: String?,
|
||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
||||
@Json(name = "isArchived") val isArchived: Boolean?,
|
||||
@Json(name = "itemCount") val itemCount: Int,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
// [COHERENCE_NOTE] Поле `description` также может отсутствовать или быть null,
|
||||
// поэтому его тоже безопасно сделать nullable.
|
||||
@Json(name = "description") val description: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationOutCountDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOutCount')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из LocationOutCountDto в доменную модель LocationOutCount.
|
||||
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
|
||||
* и предоставления non-nullable значений по умолчанию для доменной модели.
|
||||
* @summary Маппер из LocationOutCountDto в доменную модель LocationOutCount.
|
||||
*/
|
||||
fun LocationOutCountDto.toDomain(): LocationOutCount {
|
||||
return LocationOutCount(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию, если поле null.
|
||||
color = this.color ?: "", // Пустая строка как дефолтный цвет
|
||||
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
|
||||
color = this.color ?: "",
|
||||
isArchived = this.isArchived ?: false,
|
||||
itemCount = this.itemCount,
|
||||
createdAt = this.createdAt,
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_FILE_LocationOutCountDto.kt]
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_LocationOutCountDto.kt]
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LocationOutDto.kt
|
||||
// [SEMANTICS] data_transfer_object, location
|
||||
|
||||
// [SEMANTICS] data_transfer_object, location, output
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LocationOut
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для местоположения.
|
||||
*/
|
||||
// [ENTITY: DataClass('LocationOutDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationOutDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "color") val color: String,
|
||||
@Json(name = "isArchived") val isArchived: Boolean,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
@Json(name = "id")
|
||||
val id: String,
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
@Json(name = "color")
|
||||
val color: String,
|
||||
@Json(name = "isArchived")
|
||||
val isArchived: Boolean,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: String,
|
||||
@Json(name = "updatedAt")
|
||||
val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationOutDto')]
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из LocationOutDto в доменную модель LocationOut.
|
||||
*/
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOut')]
|
||||
fun LocationOutDto.toDomain(): LocationOut {
|
||||
return LocationOut(
|
||||
id = this.id,
|
||||
@@ -38,3 +39,5 @@ fun LocationOutDto.toDomain(): LocationOut {
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_LocationOutDto.kt]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LocationUpdateDto.kt
|
||||
// [SEMANTICS] data_transfer_object, location, update
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.LocationUpdate
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LocationUpdateDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationUpdateDto(
|
||||
@Json(name = "name")
|
||||
val name: String?,
|
||||
@Json(name = "color")
|
||||
val color: String?
|
||||
)
|
||||
// [END_ENTITY: DataClass('LocationUpdateDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LocationUpdateDto')]
|
||||
fun LocationUpdate.toDto(): LocationUpdateDto {
|
||||
return LocationUpdateDto(
|
||||
name = this.name,
|
||||
color = this.color
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
// [END_FILE_LocationUpdateDto.kt]
|
||||
@@ -1,15 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LoginFormDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, login
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('LoginFormDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LoginFormDto(
|
||||
@Json(name = "username") val username: String,
|
||||
@Json(name = "password") val password: String,
|
||||
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
|
||||
)
|
||||
// [END_FILE_LoginFormDto.kt]
|
||||
// [END_ENTITY: DataClass('LoginFormDto')]
|
||||
// [END_FILE_LoginFormDto.kt]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.MaintenanceEntry
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('MaintenanceEntryDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для записи об обслуживании.
|
||||
* @summary DTO для записи об обслуживании.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MaintenanceEntryDto(
|
||||
@@ -25,10 +25,12 @@ data class MaintenanceEntryDto(
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String
|
||||
)
|
||||
// [END_ENTITY: DataClass('MaintenanceEntryDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('MaintenanceEntry')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
|
||||
* @summary Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
|
||||
*/
|
||||
fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
|
||||
return MaintenanceEntry(
|
||||
@@ -42,3 +44,4 @@ fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -1,15 +1,16 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] PaginationDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, pagination
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('PaginationResult')]
|
||||
/**
|
||||
* [ENTITY: DataClass('PaginationResult')]
|
||||
* [PURPOSE] DTO для пагинированных результатов от API.
|
||||
* @summary DTO для пагинированных результатов от API.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PaginationResult<T>(
|
||||
@@ -19,5 +20,6 @@ data class PaginationResult<T>(
|
||||
@Json(name = "total") val total: Int,
|
||||
@Json(name = "pageSize") val pageSize: Int
|
||||
)
|
||||
// [END_ENTITY: DataClass('PaginationResult')]
|
||||
|
||||
// [END_FILE_PaginationDto.kt]
|
||||
// [END_FILE_PaginationDto.kt]
|
||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.homebox.lens.domain.model.PaginationResult
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: DataClass('PaginationResultDto')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для постраничных результатов.
|
||||
* @summary DTO для постраничных результатов.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PaginationResultDto<T>(
|
||||
@@ -21,10 +21,12 @@ data class PaginationResultDto<T>(
|
||||
@Json(name = "pageSize") val pageSize: Int,
|
||||
@Json(name = "total") val total: Int
|
||||
)
|
||||
// [END_ENTITY: DataClass('PaginationResultDto')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('PaginationResult')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из PaginationResultDto в доменную модель PaginationResult.
|
||||
* @summary Маппер из PaginationResultDto в доменную модель PaginationResult.
|
||||
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
|
||||
*/
|
||||
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> {
|
||||
@@ -35,3 +37,4 @@ fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResul
|
||||
total = this.total
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -1,16 +1,17 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] StatisticsDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, statistics
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('GroupStatistics')]
|
||||
/**
|
||||
* [ENTITY: DataClass('GroupStatistics')]
|
||||
* [PURPOSE] DTO для статистической информации.
|
||||
* @summary DTO для статистической информации.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GroupStatistics(
|
||||
@@ -19,5 +20,6 @@ data class GroupStatistics(
|
||||
@Json(name = "locations") val locations: Int,
|
||||
@Json(name = "labels") val labels: Int
|
||||
)
|
||||
// [END_ENTITY: DataClass('GroupStatistics')]
|
||||
|
||||
// [END_FILE_StatisticsDto.kt]
|
||||
// [END_FILE_StatisticsDto.kt]
|
||||
@@ -1,15 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] TokenResponseDto.kt
|
||||
|
||||
// [SEMANTICS] data, dto, api, token
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
// [IMPORTS]
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: DataClass('TokenResponseDto')]
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TokenResponseDto(
|
||||
@Json(name = "token") val token: String,
|
||||
@Json(name = "attachmentToken") val attachmentToken: String,
|
||||
@Json(name = "expiresAt") val expiresAt: String
|
||||
)
|
||||
// [END_FILE_TokenResponseDto.kt]
|
||||
// [END_ENTITY: DataClass('TokenResponseDto')]
|
||||
// [END_FILE_TokenResponseDto.kt]
|
||||
@@ -4,26 +4,27 @@
|
||||
|
||||
package com.homebox.lens.data.api.mapper
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.dto.TokenResponseDto
|
||||
import com.homebox.lens.domain.model.TokenResponse
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('TokenResponse')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [HELPER] Преобразует DTO-объект токена в доменную модель.
|
||||
* @summary Преобразует DTO-объект токена в доменную модель.
|
||||
* @receiver [TokenResponseDto] объект из слоя данных.
|
||||
* @return [TokenResponse] объект для доменного слоя.
|
||||
* @throws IllegalArgumentException если токен в DTO пустой.
|
||||
*/
|
||||
fun TokenResponseDto.toDomain(): TokenResponse {
|
||||
// [PRECONDITION] DTO должен содержать валидные данные для маппинга.
|
||||
require(this.token.isNotBlank()) { "[PRECONDITION_FAILED] DTO token is blank, cannot map to domain model." }
|
||||
require(this.token.isNotBlank()) { "DTO token is blank, cannot map to domain model." }
|
||||
|
||||
// [ACTION]
|
||||
val domainModel = TokenResponse(token = this.token)
|
||||
|
||||
// [POSTCONDITION] Проверяем, что инвариант доменной модели соблюден.
|
||||
check(domainModel.token.isNotBlank()) { "[POSTCONDITION_FAILED] Domain model token is blank after mapping." }
|
||||
check(domainModel.token.isNotBlank()) { "Domain model token is blank after mapping." }
|
||||
|
||||
return domainModel
|
||||
}
|
||||
// [END_FILE_TokenMapper.kt]
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
// [END_FILE_TokenMapper.kt]
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db
|
||||
// [FILE] Converters.kt
|
||||
|
||||
// [SEMANTICS] data, database, room, converter
|
||||
package com.homebox.lens.data.db
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.TypeConverter
|
||||
import java.math.BigDecimal
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Class('Converters')]
|
||||
/**
|
||||
* [ENTITY: Class('Converters')]
|
||||
* [PURPOSE] Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
|
||||
* @summary Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
|
||||
*/
|
||||
class Converters {
|
||||
// [ENTITY: Function('fromString')]
|
||||
@TypeConverter
|
||||
fun fromString(value: String?): BigDecimal? {
|
||||
return value?.let { BigDecimal(it) }
|
||||
}
|
||||
// [END_ENTITY: Function('fromString')]
|
||||
|
||||
// [ENTITY: Function('bigDecimalToString')]
|
||||
@TypeConverter
|
||||
fun bigDecimalToString(bigDecimal: BigDecimal?): String? {
|
||||
return bigDecimal?.toPlainString()
|
||||
}
|
||||
// [END_ENTITY: Function('bigDecimalToString')]
|
||||
}
|
||||
// [END_ENTITY: Class('Converters')]
|
||||
|
||||
// [END_FILE_Converters.kt]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db
|
||||
// [FILE] HomeboxDatabase.kt
|
||||
|
||||
// [SEMANTICS] data, database, room
|
||||
package com.homebox.lens.data.db
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
@@ -10,11 +11,11 @@ import com.homebox.lens.data.db.dao.ItemDao
|
||||
import com.homebox.lens.data.db.dao.LabelDao
|
||||
import com.homebox.lens.data.db.dao.LocationDao
|
||||
import com.homebox.lens.data.db.entity.*
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Database('HomeboxDatabase')]
|
||||
/**
|
||||
* [ENTITY: RoomDatabase('HomeboxDatabase')]
|
||||
* [PURPOSE] Основной класс для работы с локальной базой данных Room.
|
||||
* @summary Основной класс для работы с локальной базой данных Room.
|
||||
*/
|
||||
@Database(
|
||||
entities = [
|
||||
@@ -37,5 +38,6 @@ abstract class HomeboxDatabase : RoomDatabase() {
|
||||
const val DATABASE_NAME = "homebox_lens_db"
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Database('HomeboxDatabase')]
|
||||
|
||||
// [END_FILE_HomeboxDatabase.kt]
|
||||
// [END_FILE_HomeboxDatabase.kt]
|
||||
@@ -1,45 +1,61 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.dao
|
||||
// [FILE] ItemDao.kt
|
||||
|
||||
// [SEMANTICS] data, database, dao, item
|
||||
package com.homebox.lens.data.db.dao
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.*
|
||||
import com.homebox.lens.data.db.entity.ItemEntity
|
||||
import com.homebox.lens.data.db.entity.ItemLabelCrossRef
|
||||
import com.homebox.lens.data.db.entity.ItemWithLabels
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Interface('ItemDao')]
|
||||
/**
|
||||
* [ENTITY: RoomDao('ItemDao')]
|
||||
* [PURPOSE] Предоставляет методы для работы с 'items' в локальной БД.
|
||||
* @summary Предоставляет методы для работы с 'items' в локальной БД.
|
||||
*/
|
||||
@Dao
|
||||
interface ItemDao {
|
||||
|
||||
// [ENTITY: Function('getRecentlyAddedItems')]
|
||||
@Transaction
|
||||
@Query("SELECT * FROM items ORDER BY createdAt DESC LIMIT :limit")
|
||||
fun getRecentlyAddedItems(limit: Int): Flow<List<ItemWithLabels>>
|
||||
// [END_ENTITY: Function('getRecentlyAddedItems')]
|
||||
|
||||
// [ENTITY: Function('getItems')]
|
||||
@Transaction
|
||||
@Query("SELECT * FROM items")
|
||||
suspend fun getItems(): List<ItemWithLabels>
|
||||
// [END_ENTITY: Function('getItems')]
|
||||
|
||||
// [ENTITY: Function('getItem')]
|
||||
@Transaction
|
||||
@Query("SELECT * FROM items WHERE id = :itemId")
|
||||
suspend fun getItem(itemId: String): ItemWithLabels?
|
||||
// [END_ENTITY: Function('getItem')]
|
||||
|
||||
// [ENTITY: Function('insertItems')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertItems(items: List<ItemEntity>)
|
||||
// [END_ENTITY: Function('insertItems')]
|
||||
|
||||
// [ENTITY: Function('insertItem')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertItem(item: ItemEntity)
|
||||
// [END_ENTITY: Function('insertItem')]
|
||||
|
||||
// [ENTITY: Function('deleteItem')]
|
||||
@Query("DELETE FROM items WHERE id = :itemId")
|
||||
suspend fun deleteItem(itemId: String)
|
||||
// [END_ENTITY: Function('deleteItem')]
|
||||
|
||||
// [ENTITY: Function('insertItemLabelCrossRefs')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertItemLabelCrossRefs(crossRefs: List<ItemLabelCrossRef>)
|
||||
// [END_ENTITY: Function('insertItemLabelCrossRefs')]
|
||||
}
|
||||
// [END_ENTITY: Interface('ItemDao')]
|
||||
|
||||
// [END_FILE_ItemDao.kt]
|
||||
// [END_FILE_ItemDao.kt]
|
||||
@@ -1,27 +1,33 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.dao
|
||||
// [FILE] LabelDao.kt
|
||||
|
||||
// [SEMANTICS] data, database, dao, label
|
||||
package com.homebox.lens.data.db.dao
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.homebox.lens.data.db.entity.LabelEntity
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Interface('LabelDao')]
|
||||
/**
|
||||
* [ENTITY: RoomDao('LabelDao')]
|
||||
* [PURPOSE] Предоставляет методы для работы с 'labels' в локальной БД.
|
||||
* @summary Предоставляет методы для работы с 'labels' в локальной БД.
|
||||
*/
|
||||
@Dao
|
||||
interface LabelDao {
|
||||
|
||||
// [ENTITY: Function('getLabels')]
|
||||
@Query("SELECT * FROM labels")
|
||||
suspend fun getLabels(): List<LabelEntity>
|
||||
// [END_ENTITY: Function('getLabels')]
|
||||
|
||||
// [ENTITY: Function('insertLabels')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertLabels(labels: List<LabelEntity>)
|
||||
// [END_ENTITY: Function('insertLabels')]
|
||||
}
|
||||
// [END_ENTITY: Interface('LabelDao')]
|
||||
|
||||
// [END_FILE_LabelDao.kt]
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.dao
|
||||
// [FILE] LocationDao.kt
|
||||
|
||||
// [SEMANTICS] data, database, dao, location
|
||||
package com.homebox.lens.data.db.dao
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.homebox.lens.data.db.entity.LocationEntity
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Interface('LocationDao')]
|
||||
/**
|
||||
* [ENTITY: RoomDao('LocationDao')]
|
||||
* [PURPOSE] Предоставляет методы для работы с 'locations' в локальной БД.
|
||||
* @summary Предоставляет методы для работы с 'locations' в локальной БД.
|
||||
*/
|
||||
@Dao
|
||||
interface LocationDao {
|
||||
|
||||
// [ENTITY: Function('getLocations')]
|
||||
@Query("SELECT * FROM locations")
|
||||
suspend fun getLocations(): List<LocationEntity>
|
||||
// [END_ENTITY: Function('getLocations')]
|
||||
|
||||
// [ENTITY: Function('insertLocations')]
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertLocations(locations: List<LocationEntity>)
|
||||
// [END_ENTITY: Function('insertLocations')]
|
||||
}
|
||||
// [END_ENTITY: Interface('LocationDao')]
|
||||
|
||||
// [END_FILE_LocationDao.kt]
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] ItemEntity.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, item
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.math.BigDecimal
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DatabaseTable('ItemEntity')]
|
||||
/**
|
||||
* [ENTITY: RoomEntity('ItemEntity')]
|
||||
* [PURPOSE] Представляет собой строку в таблице 'items' в локальной БД.
|
||||
* @summary Представляет собой строку в таблице 'items' в локальной БД.
|
||||
*/
|
||||
@Entity(tableName = "items")
|
||||
data class ItemEntity(
|
||||
@@ -22,5 +23,6 @@ data class ItemEntity(
|
||||
val value: BigDecimal?,
|
||||
val createdAt: String?
|
||||
)
|
||||
// [END_ENTITY: DatabaseTable('ItemEntity')]
|
||||
|
||||
// [END_FILE_ItemEntity.kt]
|
||||
// [END_FILE_ItemEntity.kt]
|
||||
@@ -1,15 +1,16 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] ItemLabelCrossRef.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, relation
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DatabaseTable('ItemLabelCrossRef')]
|
||||
/**
|
||||
* [ENTITY: RoomEntity('ItemLabelCrossRef')]
|
||||
* [PURPOSE] Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
|
||||
* @summary Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
|
||||
*/
|
||||
@Entity(
|
||||
primaryKeys = ["itemId", "labelId"],
|
||||
@@ -19,5 +20,6 @@ data class ItemLabelCrossRef(
|
||||
val itemId: String,
|
||||
val labelId: String
|
||||
)
|
||||
// [END_ENTITY: DatabaseTable('ItemLabelCrossRef')]
|
||||
|
||||
// [END_FILE_ItemLabelCrossRef.kt]
|
||||
// [END_FILE_ItemLabelCrossRef.kt]
|
||||
@@ -1,16 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] ItemWithLabels.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, relation
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DataClass('ItemWithLabels')]
|
||||
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('ItemEntity')]
|
||||
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('LabelEntity')]
|
||||
/**
|
||||
* [ENTITY: Pojo('ItemWithLabels')]
|
||||
* [PURPOSE] POJO для получения ItemEntity вместе со связанными LabelEntity.
|
||||
* @summary POJO для получения ItemEntity вместе со связанными LabelEntity.
|
||||
*/
|
||||
data class ItemWithLabels(
|
||||
@Embedded val item: ItemEntity,
|
||||
@@ -25,5 +28,6 @@ data class ItemWithLabels(
|
||||
)
|
||||
val labels: List<LabelEntity>
|
||||
)
|
||||
// [END_ENTITY: DataClass('ItemWithLabels')]
|
||||
|
||||
// [END_FILE_ItemWithLabels.kt]
|
||||
// [END_FILE_ItemWithLabels.kt]
|
||||
@@ -1,20 +1,22 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] LabelEntity.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, label
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DatabaseTable('LabelEntity')]
|
||||
/**
|
||||
* [ENTITY: RoomEntity('LabelEntity')]
|
||||
* [PURPOSE] Представляет собой строку в таблице 'labels' в локальной БД.
|
||||
* @summary Представляет собой строку в таблице 'labels' в локальной БД.
|
||||
*/
|
||||
@Entity(tableName = "labels")
|
||||
data class LabelEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val name: String
|
||||
)
|
||||
// [END_ENTITY: DatabaseTable('LabelEntity')]
|
||||
|
||||
// [END_FILE_LabelEntity.kt]
|
||||
// [END_FILE_LabelEntity.kt]
|
||||
@@ -1,20 +1,22 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] LocationEntity.kt
|
||||
|
||||
// [SEMANTICS] data, database, entity, location
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: DatabaseTable('LocationEntity')]
|
||||
/**
|
||||
* [ENTITY: RoomEntity('LocationEntity')]
|
||||
* [PURPOSE] Представляет собой строку в таблице 'locations' в локальной БД.
|
||||
* @summary Представляет собой строку в таблице 'locations' в локальной БД.
|
||||
*/
|
||||
@Entity(tableName = "locations")
|
||||
data class LocationEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val name: String
|
||||
)
|
||||
// [END_ENTITY: DatabaseTable('LocationEntity')]
|
||||
|
||||
// [END_FILE_LocationEntity.kt]
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||
// [FILE] Mapper.kt
|
||||
|
||||
// [SEMANTICS] data, database, mapper
|
||||
package com.homebox.lens.data.db.entity
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.Image
|
||||
import com.homebox.lens.domain.model.ItemSummary
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
import com.homebox.lens.domain.model.LocationOut
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
|
||||
*
|
||||
* [COHERENCE_NOTE] Так как сущности БД содержат только подмножество полей доменной модели,
|
||||
* недостающие поля заполняются значениями по умолчанию (false, 0.0, пустые строки) или null.
|
||||
* Это компромисс для обеспечения компиляции и базовой функциональности.
|
||||
* @summary Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
|
||||
*/
|
||||
fun ItemWithLabels.toDomain(): ItemSummary {
|
||||
return ItemSummary(
|
||||
id = this.item.id,
|
||||
name = this.item.name,
|
||||
// Предполагаем, что `image` в БД - это URL. Создаем объект Image или null.
|
||||
image = this.item.image?.let { Image(id = "", path = it, isPrimary = true) },
|
||||
// `location` в ItemEntity - это только ID. Создаем базовый LocationOut.
|
||||
location = this.item.locationId?.let { LocationOut(id = it, name = "", color = "", isArchived = false, createdAt = "", updatedAt = "") },
|
||||
labels = this.labels.map { it.toDomain() },
|
||||
// Заполняем недостающие поля значениями по умолчанию.
|
||||
assetId = null,
|
||||
isArchived = false,
|
||||
value = this.item.value?.toDouble() ?: 0.0,
|
||||
@@ -33,21 +29,21 @@ fun ItemWithLabels.toDomain(): ItemSummary {
|
||||
updatedAt = ""
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
|
||||
// [ENTITY: Function('toDomain')]
|
||||
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
|
||||
*
|
||||
* [COHERENCE_NOTE] Заполняет недостающие поля значениями по умолчанию.
|
||||
* @summary Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
|
||||
*/
|
||||
fun LabelEntity.toDomain(): LabelOut {
|
||||
return LabelOut(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
// Заполняем недостающие поля значениями по умолчанию.
|
||||
color = "#CCCCCC", // Серый цвет по умолчанию
|
||||
color = "#CCCCCC",
|
||||
isArchived = false,
|
||||
createdAt = "",
|
||||
updatedAt = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('toDomain')]
|
||||
@@ -1,7 +1,8 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] ApiModule.kt
|
||||
// [PURPOSE] Предоставляет синглтон-зависимости для работы с сетью, включая OkHttpClient, Retrofit и ApiService.
|
||||
// [SEMANTICS] di, hilt, networking
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
@@ -17,41 +18,34 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import timber.log.Timber
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Module('ApiModule')]
|
||||
/**
|
||||
* [ENTITY: Module('ApiModule')]
|
||||
* [CONTRACT]
|
||||
* Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
|
||||
* @summary Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
|
||||
* необходимых для сетевого взаимодействия.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ApiModule {
|
||||
|
||||
// [HELPER] Временный базовый URL для API. В будущем должен стать динамическим.
|
||||
private const val BASE_URL = "https://homebox.bebesh.ru/api/"
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT]
|
||||
* Предоставляет сконфигурированный OkHttpClient.
|
||||
* @param credentialsRepositoryProvider Провайдер репозитория для доступа к токену авторизации.
|
||||
* Используется Provider<T> для предотвращения циклов зависимостей.
|
||||
* @return Синглтон-экземпляр OkHttpClient с настроенными перехватчиками.
|
||||
*/
|
||||
// [ENTITY: Function('provideOkHttpClient')]
|
||||
// [RELATION: Function('provideOkHttpClient')] -> [PROVIDES] -> [Framework('OkHttpClient')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(
|
||||
credentialsRepositoryProvider: Provider<CredentialsRepository>
|
||||
): OkHttpClient {
|
||||
// [ACTION] Создаем перехватчик для логирования.
|
||||
Timber.d("[DEBUG][PROVIDER][providing_okhttp_client] Providing OkHttpClient.")
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
|
||||
// [ACTION] Создаем перехватчик для добавления заголовка 'Accept'.
|
||||
val acceptHeaderInterceptor = Interceptor { chain ->
|
||||
val request = chain.request().newBuilder()
|
||||
.header("Accept", "application/json")
|
||||
@@ -59,77 +53,71 @@ object ApiModule {
|
||||
chain.proceed(request)
|
||||
}
|
||||
|
||||
// [CORE-LOGIC] Создаем перехватчик для добавления токена авторизации.
|
||||
val authInterceptor = Interceptor { chain ->
|
||||
// [HELPER] Получаем токен из репозитория.
|
||||
// runBlocking здесь допустим, т.к. чтение из SharedPreferences - быстрая I/O операция,
|
||||
// а интерфейс Interceptor'а является синхронным.
|
||||
val token = runBlocking { credentialsRepositoryProvider.get().getToken() }
|
||||
|
||||
val requestBuilder = chain.request().newBuilder()
|
||||
|
||||
// [ACTION] Если токен существует, добавляем его в заголовок.
|
||||
if (token != null) {
|
||||
// Сервер ожидает заголовок "Authorization: Bearer <token>"
|
||||
// Предполагается, что `token` уже содержит префикс "Bearer ".
|
||||
requestBuilder.addHeader("Authorization", token)
|
||||
}
|
||||
|
||||
chain.proceed(requestBuilder.build())
|
||||
}
|
||||
|
||||
// [ACTION] Собираем OkHttpClient с правильным порядком перехватчиков.
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(acceptHeaderInterceptor)
|
||||
.addInterceptor(authInterceptor) // Добавляем перехватчик для токена
|
||||
.addInterceptor(loggingInterceptor) // Логирование должно идти последним, чтобы видеть финальный запрос
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build()
|
||||
}
|
||||
// [END_ENTITY: Function('provideOkHttpClient')]
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет экземпляр Moshi для парсинга JSON.
|
||||
*/
|
||||
// [ENTITY: Function('provideMoshi')]
|
||||
// [RELATION: Function('provideMoshi')] -> [PROVIDES] -> [Framework('Moshi')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoshi(): Moshi {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_moshi] Providing Moshi.")
|
||||
return Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
// [END_ENTITY: Function('provideMoshi')]
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет фабрику конвертеров для Retrofit.
|
||||
*/
|
||||
// [ENTITY: Function('provideMoshiConverterFactory')]
|
||||
// [RELATION: Function('provideMoshiConverterFactory')] -> [PROVIDES] -> [Framework('MoshiConverterFactory')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_moshi_converter] Providing MoshiConverterFactory.")
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
// [END_ENTITY: Function('provideMoshiConverterFactory')]
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет сконфигурированный экземпляр Retrofit.
|
||||
*/
|
||||
// [ENTITY: Function('provideRetrofit')]
|
||||
// [RELATION: Function('provideRetrofit')] -> [PROVIDES] -> [Framework('Retrofit')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_retrofit] Providing Retrofit.")
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(moshiConverterFactory)
|
||||
.build()
|
||||
}
|
||||
// [END_ENTITY: Function('provideRetrofit')]
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет реализацию интерфейса HomeboxApiService.
|
||||
*/
|
||||
// [ENTITY: Function('provideHomeboxApiService')]
|
||||
// [RELATION: Function('provideHomeboxApiService')] -> [PROVIDES] -> [Interface('HomeboxApiService')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_api_service] Providing HomeboxApiService.")
|
||||
return retrofit.create(HomeboxApiService::class.java)
|
||||
}
|
||||
// [END_ENTITY: Function('provideHomeboxApiService')]
|
||||
}
|
||||
// [END_ENTITY: Module('ApiModule')]
|
||||
// [END_FILE_ApiModule.kt]
|
||||
@@ -1,8 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] DatabaseModule.kt
|
||||
|
||||
// [SEMANTICS] di, hilt, database
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
// [IMPORTS]
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.homebox.lens.data.db.HomeboxDatabase
|
||||
@@ -11,40 +12,50 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Module('DatabaseModule')]
|
||||
/**
|
||||
* [MODULE: DaggerHilt('DatabaseModule')]
|
||||
* [PURPOSE] Предоставляет зависимости для работы с базой данных Room.
|
||||
* @summary Предоставляет зависимости для работы с базой данных Room.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
// [PROVIDER]
|
||||
// [ENTITY: Function('provideHomeboxDatabase')]
|
||||
// [RELATION: Function('provideHomeboxDatabase')] -> [PROVIDES] -> [Database('HomeboxDatabase')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHomeboxDatabase(@ApplicationContext context: Context): HomeboxDatabase {
|
||||
// [ACTION] Build Room database instance
|
||||
Timber.d("[DEBUG][PROVIDER][providing_database] Providing HomeboxDatabase.")
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
HomeboxDatabase::class.java,
|
||||
HomeboxDatabase.DATABASE_NAME
|
||||
).build()
|
||||
}
|
||||
// [END_ENTITY: Function('provideHomeboxDatabase')]
|
||||
|
||||
// [PROVIDER]
|
||||
// [ENTITY: Function('provideItemDao')]
|
||||
// [RELATION: Function('provideItemDao')] -> [PROVIDES] -> [Interface('ItemDao')]
|
||||
@Provides
|
||||
fun provideItemDao(database: HomeboxDatabase) = database.itemDao()
|
||||
// [END_ENTITY: Function('provideItemDao')]
|
||||
|
||||
// [PROVIDER]
|
||||
// [ENTITY: Function('provideLabelDao')]
|
||||
// [RELATION: Function('provideLabelDao')] -> [PROVIDES] -> [Interface('LabelDao')]
|
||||
@Provides
|
||||
fun provideLabelDao(database: HomeboxDatabase) = database.labelDao()
|
||||
// [END_ENTITY: Function('provideLabelDao')]
|
||||
|
||||
// [PROVIDER]
|
||||
// [ENTITY: Function('provideLocationDao')]
|
||||
// [RELATION: Function('provideLocationDao')] -> [PROVIDES] -> [Interface('LocationDao')]
|
||||
@Provides
|
||||
fun provideLocationDao(database: HomeboxDatabase) = database.locationDao()
|
||||
// [END_ENTITY: Function('provideLocationDao')]
|
||||
}
|
||||
// [END_ENTITY: Module('DatabaseModule')]
|
||||
|
||||
// [END_FILE_DatabaseModule.kt]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.repository.AuthRepositoryImpl
|
||||
import com.homebox.lens.data.repository.CredentialsRepositoryImpl
|
||||
import com.homebox.lens.data.repository.ItemRepositoryImpl
|
||||
@@ -15,47 +16,52 @@ import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Module('RepositoryModule')]
|
||||
/**
|
||||
* [ENTITY: Module('RepositoryModule')]
|
||||
* [CONTRACT]
|
||||
* Hilt-модуль для предоставления реализаций репозиториев.
|
||||
* Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
|
||||
* @summary Hilt-модуль для предоставления реализаций репозиториев.
|
||||
* @description Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class RepositoryModule {
|
||||
|
||||
// [ENTITY: Function('bindItemRepository')]
|
||||
// [RELATION: Function('bindItemRepository')] -> [PROVIDES] -> [Interface('ItemRepository')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Связывает интерфейс ItemRepository с его реализацией.
|
||||
* @summary Связывает интерфейс ItemRepository с его реализацией.
|
||||
*/
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindItemRepository(
|
||||
itemRepositoryImpl: ItemRepositoryImpl
|
||||
): ItemRepository
|
||||
// [END_ENTITY: Function('bindItemRepository')]
|
||||
|
||||
// [ENTITY: Function('bindCredentialsRepository')]
|
||||
// [RELATION: Function('bindCredentialsRepository')] -> [PROVIDES] -> [Interface('CredentialsRepository')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Связывает интерфейс CredentialsRepository с его реализацией.
|
||||
* @summary Связывает интерфейс CredentialsRepository с его реализацией.
|
||||
*/
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindCredentialsRepository(
|
||||
credentialsRepositoryImpl: CredentialsRepositoryImpl
|
||||
): CredentialsRepository
|
||||
// [END_ENTITY: Function('bindCredentialsRepository')]
|
||||
|
||||
// [ENTITY: Function('bindAuthRepository')]
|
||||
// [RELATION: Function('bindAuthRepository')] -> [PROVIDES] -> [Interface('AuthRepository')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [FIX] Связывает интерфейс AuthRepository с его реализацией.
|
||||
* Это исправляет ошибку "could not be resolved", так как теперь Hilt знает,
|
||||
* какую конкретную реализацию предоставить, когда запрашивается AuthRepository.
|
||||
* @summary Связывает интерфейс AuthRepository с его реализацией.
|
||||
*/
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindAuthRepository(
|
||||
authRepositoryImpl: AuthRepositoryImpl
|
||||
): AuthRepository
|
||||
// [END_ENTITY: Function('bindAuthRepository')]
|
||||
}
|
||||
// [END_ENTITY: Module('RepositoryModule')]
|
||||
// [END_FILE_RepositoryModule.kt]
|
||||
@@ -1,8 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] StorageModule.kt
|
||||
|
||||
// [SEMANTICS] di, hilt, storage
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
// [IMPORTS]
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.data.repository.EncryptedPreferencesWrapper
|
||||
@@ -12,30 +13,39 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Module('StorageModule')]
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object StorageModule {
|
||||
|
||||
private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs" // No longer secret
|
||||
private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs"
|
||||
|
||||
// [ACTION] Provide a standard, unencrypted SharedPreferences instance.
|
||||
// [ENTITY: Function('provideSharedPreferences')]
|
||||
// [RELATION: Function('provideSharedPreferences')] -> [PROVIDES] -> [Framework('SharedPreferences')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_shared_preferences] Providing SharedPreferences.")
|
||||
return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
// [END_ENTITY: Function('provideSharedPreferences')]
|
||||
|
||||
// [ACTION] Provide our new EncryptedPreferencesWrapper as the main entry point for secure storage.
|
||||
// Hilt will automatically provide SharedPreferences and CryptoManager to its constructor.
|
||||
// [ENTITY: Function('provideEncryptedPreferencesWrapper')]
|
||||
// [RELATION: Function('provideEncryptedPreferencesWrapper')] -> [PROVIDES] -> [Class('EncryptedPreferencesWrapper')]
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEncryptedPreferencesWrapper(
|
||||
sharedPreferences: SharedPreferences,
|
||||
cryptoManager: CryptoManager
|
||||
): EncryptedPreferencesWrapper {
|
||||
Timber.d("[DEBUG][PROVIDER][providing_encrypted_prefs_wrapper] Providing EncryptedPreferencesWrapper.")
|
||||
return EncryptedPreferencesWrapper(sharedPreferences, cryptoManager)
|
||||
}
|
||||
// [END_ENTITY: Function('provideEncryptedPreferencesWrapper')]
|
||||
}
|
||||
// [END_ENTITY: Module('StorageModule')]
|
||||
// [END_FILE_StorageModule.kt]
|
||||
@@ -20,17 +20,20 @@ import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Class('AuthRepositoryImpl')]
|
||||
// [RELATION: Class('AuthRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('AuthRepository')]
|
||||
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
|
||||
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('OkHttpClient')]
|
||||
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('MoshiConverterFactory')]
|
||||
/**
|
||||
* [ENTITY: Class('AuthRepositoryImpl')]
|
||||
* [CONTRACT]
|
||||
* Реализация репозитория для управления аутентификацией.
|
||||
* @summary Реализация репозитория для управления аутентификацией.
|
||||
* @param encryptedPrefs Защищенное хранилище для токена.
|
||||
* @param okHttpClient Общий OkHttp клиент для переиспользования.
|
||||
* @param moshiConverterFactory Общий конвертер Moshi для переиспользования.
|
||||
* [COHERENCE_NOTE] Реализация метода login теперь включает логику создания временного Retrofit-клиента
|
||||
* "на лету", используя URL сервера из credentials. Эта логика была перенесена из ItemRepositoryImpl.
|
||||
*/
|
||||
class AuthRepositoryImpl @Inject constructor(
|
||||
private val encryptedPrefs: SharedPreferences,
|
||||
@@ -42,47 +45,53 @@ class AuthRepositoryImpl @Inject constructor(
|
||||
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
||||
}
|
||||
|
||||
// [ENTITY: Function('login')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Реализует вход пользователя. Создает временный API сервис для выполнения запроса
|
||||
* @summary Реализует вход пользователя. Создает временный API сервис для выполнения запроса
|
||||
* на указанный пользователем URL сервера.
|
||||
* @param credentials Учетные данные пользователя, включая URL сервера.
|
||||
* @return [Result] с доменной моделью [TokenResponse] при успехе или [Exception] при ошибке.
|
||||
*/
|
||||
override suspend fun login(credentials: Credentials): Result<TokenResponse> {
|
||||
// [PRECONDITION]
|
||||
require(credentials.serverUrl.isNotBlank()) { "[PRECONDITION_FAILED] Server URL cannot be blank." }
|
||||
require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." }
|
||||
|
||||
// [CORE-LOGIC]
|
||||
return withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
// [ACTION] Создаем временный Retrofit клиент с URL, указанным пользователем.
|
||||
Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}")
|
||||
val tempApiService = Retrofit.Builder()
|
||||
.baseUrl(credentials.serverUrl)
|
||||
.client(okHttpClient) // Переиспользуем существующий OkHttp клиент
|
||||
.addConverterFactory(moshiConverterFactory) // и конвертер
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(moshiConverterFactory)
|
||||
.build()
|
||||
.create(HomeboxApiService::class.java)
|
||||
|
||||
// [ACTION] Создаем DTO и выполняем запрос.
|
||||
val loginForm = LoginFormDto(credentials.username, credentials.password)
|
||||
Timber.d("[DEBUG][ACTION][performing_login] Performing login request.")
|
||||
val tokenResponseDto = tempApiService.login(loginForm)
|
||||
|
||||
// [ACTION] Маппим результат в доменную модель.
|
||||
Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.")
|
||||
tokenResponseDto.toDomain()
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('login')]
|
||||
|
||||
// [ENTITY: Function('saveToken')]
|
||||
override suspend fun saveToken(token: String) {
|
||||
require(token.isNotBlank()) { "[PRECONDITION_FAILED] Token cannot be blank." }
|
||||
require(token.isNotBlank()) { "Token cannot be blank." }
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.")
|
||||
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('saveToken')]
|
||||
|
||||
// [ENTITY: Function('getToken')]
|
||||
override fun getToken(): Flow<String?> = flow {
|
||||
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
|
||||
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
// [END_ENTITY: Function('getToken')]
|
||||
}
|
||||
// [END_FILE_AuthRepositoryImpl.kt]
|
||||
// [END_ENTITY: Class('AuthRepositoryImpl')]
|
||||
// [END_FILE_AuthRepositoryImpl.kt]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] CredentialsRepositoryImpl.kt
|
||||
// [PURPOSE] Имплементация репозитория для управления учетными данными и токенами доступа.
|
||||
// [SEMANTICS] data, repository, credentials, security
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
@@ -11,13 +12,16 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Class('CredentialsRepositoryImpl')]
|
||||
// [RELATION: Class('CredentialsRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('CredentialsRepository')]
|
||||
// [RELATION: Class('CredentialsRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
|
||||
/**
|
||||
* [ENTITY: Class('CredentialsRepositoryImpl')]
|
||||
* [CONTRACT]
|
||||
* Реализует репозиторий для управления учетными данными пользователя.
|
||||
* Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
|
||||
* @summary Реализует репозиторий для управления учетными данными пользователя.
|
||||
* @description Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
|
||||
* @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt.
|
||||
* @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`.
|
||||
*/
|
||||
@@ -25,7 +29,6 @@ class CredentialsRepositoryImpl @Inject constructor(
|
||||
private val encryptedPrefs: SharedPreferences
|
||||
) : CredentialsRepository {
|
||||
|
||||
// [CONSTANTS_KEYS] Ключи для хранения данных в SharedPreferences.
|
||||
companion object {
|
||||
private const val KEY_SERVER_URL = "key_server_url"
|
||||
private const val KEY_USERNAME = "key_username"
|
||||
@@ -33,15 +36,15 @@ class CredentialsRepositoryImpl @Inject constructor(
|
||||
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
||||
}
|
||||
|
||||
// [ENTITY: Function('saveCredentials')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Сохраняет основные учетные данные пользователя.
|
||||
* @summary Сохраняет основные учетные данные пользователя.
|
||||
* @param credentials Объект с учетными данными для сохранения.
|
||||
* @sideeffect Перезаписывает существующие учетные данные в SharedPreferences.
|
||||
*/
|
||||
override suspend fun saveCredentials(credentials: Credentials) {
|
||||
// [ACTION] Выполняем запись в SharedPreferences в фоновом потоке.
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.d("[DEBUG][ACTION][saving_credentials] Saving user credentials.")
|
||||
encryptedPrefs.edit()
|
||||
.putString(KEY_SERVER_URL, credentials.serverUrl)
|
||||
.putString(KEY_USERNAME, credentials.username)
|
||||
@@ -49,51 +52,57 @@ class CredentialsRepositoryImpl @Inject constructor(
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('saveCredentials')]
|
||||
|
||||
// [ENTITY: Function('getCredentials')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Извлекает сохраненные учетные данные пользователя в виде потока.
|
||||
* @summary Извлекает сохраненные учетные данные пользователя в виде потока.
|
||||
* @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют.
|
||||
*/
|
||||
override fun getCredentials(): Flow<Credentials?> = flow {
|
||||
// [CORE-LOGIC] Читаем данные из SharedPreferences.
|
||||
Timber.d("[DEBUG][ACTION][getting_credentials] Getting user credentials.")
|
||||
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
|
||||
val username = encryptedPrefs.getString(KEY_USERNAME, null)
|
||||
val password = encryptedPrefs.getString(KEY_PASSWORD, null)
|
||||
|
||||
// [ACTION] Эммитим результат.
|
||||
if (serverUrl != null && username != null && password != null) {
|
||||
Timber.d("[DEBUG][SUCCESS][credentials_found] Found and emitting credentials.")
|
||||
emit(Credentials(serverUrl, username, password))
|
||||
} else {
|
||||
Timber.d("[DEBUG][FALLBACK][no_credentials] No credentials found, emitting null.")
|
||||
emit(null)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO) // [ACTION] Указываем, что Flow должен выполняться в фоновом потоке.
|
||||
}.flowOn(Dispatchers.IO)
|
||||
// [END_ENTITY: Function('getCredentials')]
|
||||
|
||||
// [ENTITY: Function('saveToken')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Сохраняет токен авторизации.
|
||||
* @summary Сохраняет токен авторизации.
|
||||
* @param token Токен для сохранения.
|
||||
* @sideeffect Перезаписывает существующий токен в SharedPreferences.
|
||||
*/
|
||||
override suspend fun saveToken(token: String) {
|
||||
// [ACTION] Выполняем запись токена в фоновом потоке.
|
||||
withContext(Dispatchers.IO) {
|
||||
Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.")
|
||||
encryptedPrefs.edit()
|
||||
.putString(KEY_AUTH_TOKEN, token)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('saveToken')]
|
||||
|
||||
// [ENTITY: Function('getToken')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Извлекает сохраненный токен авторизации.
|
||||
* @summary Извлекает сохраненный токен авторизации.
|
||||
* @return Строка с токеном или null, если он не найден.
|
||||
*/
|
||||
override suspend fun getToken(): String? {
|
||||
// [ACTION] Выполняем чтение токена в фоновом потоке.
|
||||
return withContext(Dispatchers.IO) {
|
||||
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
|
||||
encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('getToken')]
|
||||
}
|
||||
// [END_FILE_CredentialsRepositoryImpl.kt]
|
||||
// [END_ENTITY: Class('CredentialsRepositoryImpl')]
|
||||
// [END_FILE_CredentialsRepositoryImpl.kt]
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] EncryptedPreferencesWrapper.kt
|
||||
// [PURPOSE] A wrapper around SharedPreferences to provide on-the-fly encryption/decryption.
|
||||
|
||||
// [SEMANTICS] data, security, preferences
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.data.security.CryptoManager
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Class('EncryptedPreferencesWrapper')]
|
||||
// [RELATION: Class('EncryptedPreferencesWrapper')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
|
||||
// [RELATION: Class('EncryptedPreferencesWrapper')] -> [DEPENDS_ON] -> [Class('CryptoManager')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Provides a simplified and secure interface for storing and retrieving sensitive string data.
|
||||
* It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance.
|
||||
* @summary Provides a simplified and secure interface for storing and retrieving sensitive string data.
|
||||
* @description It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance.
|
||||
* @param sharedPreferences The underlying standard SharedPreferences instance to store encrypted data.
|
||||
* @param cryptoManager The manager responsible for all cryptographic operations.
|
||||
*/
|
||||
@@ -23,44 +27,58 @@ class EncryptedPreferencesWrapper @Inject constructor(
|
||||
private val cryptoManager: CryptoManager
|
||||
) {
|
||||
|
||||
// [ENTITY: Function('getString')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Retrieves a decrypted string value for a given key.
|
||||
* @summary Retrieves a decrypted string value for a given key.
|
||||
* @param key The key for the preference.
|
||||
* @param defaultValue The value to return if the key is not found or decryption fails.
|
||||
* @return The decrypted string, or the defaultValue.
|
||||
* @sideeffect Reads from SharedPreferences.
|
||||
*/
|
||||
fun getString(key: String, defaultValue: String?): String? {
|
||||
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue
|
||||
Timber.d("[DEBUG][ENTRYPOINT][getting_string] Attempting to get string for key: %s", key)
|
||||
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue.also {
|
||||
Timber.d("[DEBUG][FALLBACK][no_value_found] No value for key %s, returning default.", key)
|
||||
}
|
||||
return try {
|
||||
Timber.d("[DEBUG][ACTION][decoding_value] Decoding Base64 value.")
|
||||
val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT)
|
||||
Timber.d("[DEBUG][ACTION][decrypting_value] Decrypting value with CryptoManager.")
|
||||
val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes))
|
||||
String(decryptedBytes, Charset.defaultCharset())
|
||||
String(decryptedBytes, Charset.defaultCharset()).also {
|
||||
Timber.d("[DEBUG][SUCCESS][decryption_complete] Successfully decrypted value for key: %s", key)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log the error, maybe clear the invalid preference
|
||||
Timber.e(e, "[ERROR][EXCEPTION][decryption_failed] Failed to decrypt value for key: %s", key)
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('getString')]
|
||||
|
||||
// [ENTITY: Function('putString')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Encrypts and saves a string value for a given key.
|
||||
* @summary Encrypts and saves a string value for a given key.
|
||||
* @param key The key for the preference.
|
||||
* @param value The string value to encrypt and save.
|
||||
* @sideeffect Modifies the underlying SharedPreferences file.
|
||||
*/
|
||||
fun putString(key: String, value: String) {
|
||||
Timber.d("[DEBUG][ENTRYPOINT][putting_string] Attempting to put string for key: %s", key)
|
||||
try {
|
||||
Timber.d("[DEBUG][ACTION][encrypting_value] Encrypting value with CryptoManager.")
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream)
|
||||
val encryptedBytes = outputStream.toByteArray()
|
||||
Timber.d("[DEBUG][ACTION][encoding_value] Encoding encrypted value to Base64.")
|
||||
val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT)
|
||||
Timber.d("[DEBUG][ACTION][writing_to_prefs] Writing encrypted value to SharedPreferences.")
|
||||
sharedPreferences.edit().putString(key, encryptedValue).apply()
|
||||
Timber.d("[DEBUG][SUCCESS][encryption_complete] Successfully encrypted and saved value for key: %s", key)
|
||||
} catch (e: Exception) {
|
||||
// Log the error
|
||||
Timber.e(e, "[ERROR][EXCEPTION][encryption_failed] Failed to encrypt and save value for key: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
// [COHERENCE_NOTE] Add other methods like getInt, putInt etc. as needed, following the same pattern.
|
||||
// [END_ENTITY: Function('putString')]
|
||||
}
|
||||
// [END_ENTITY: Class('EncryptedPreferencesWrapper')]
|
||||
// [END_FILE_EncryptedPreferencesWrapper.kt]
|
||||
@@ -2,11 +2,16 @@
|
||||
// [FILE] ItemRepositoryImpl.kt
|
||||
// [SEMANTICS] data_repository, implementation, items, labels
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.data.api.dto.LabelCreateDto
|
||||
import com.homebox.lens.data.api.dto.toDomain
|
||||
import com.homebox.lens.data.api.dto.toDto
|
||||
import com.homebox.lens.data.api.dto.LocationCreateDto
|
||||
import com.homebox.lens.data.api.dto.LocationUpdateDto
|
||||
import com.homebox.lens.data.api.dto.LabelUpdateDto
|
||||
import com.homebox.lens.data.api.dto.LocationOutDto
|
||||
import com.homebox.lens.data.db.dao.ItemDao
|
||||
import com.homebox.lens.data.db.entity.toDomain
|
||||
import com.homebox.lens.domain.model.*
|
||||
@@ -15,108 +20,138 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
[CONTRACT]
|
||||
Реализация репозитория для работы с данными о вещах.
|
||||
@param apiService Сервис для взаимодействия с Homebox API.
|
||||
@param itemDao DAO для доступа к локальной базе данных.
|
||||
*/
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Repository('ItemRepositoryImpl')]
|
||||
// [RELATION: Repository('ItemRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('ItemRepository')]
|
||||
// [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [ApiEndpoint('HomeboxApiService')]
|
||||
// [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [DatabaseTable('ItemDao')]
|
||||
@Singleton
|
||||
class ItemRepositoryImpl @Inject constructor(
|
||||
private val apiService: HomeboxApiService,
|
||||
private val itemDao: ItemDao
|
||||
) : ItemRepository {
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.createItem
|
||||
*/
|
||||
|
||||
// [ENTITY: Function('createItem')]
|
||||
// [RELATION: Function('createItem')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
|
||||
val itemDto = newItemData.toDto()
|
||||
val resultDto = apiService.createItem(itemDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.getItemDetails
|
||||
*/
|
||||
// [END_ENTITY: Function('createItem')]
|
||||
|
||||
// [ENTITY: Function('getItemDetails')]
|
||||
// [RELATION: Function('getItemDetails')] -> [RETURNS] -> [DataClass('ItemOut')]
|
||||
override suspend fun getItemDetails(itemId: String): ItemOut {
|
||||
val resultDto = apiService.getItem(itemId)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.updateItem
|
||||
*/
|
||||
// [END_ENTITY: Function('getItemDetails')]
|
||||
|
||||
// [ENTITY: Function('updateItem')]
|
||||
// [RELATION: Function('updateItem')] -> [RETURNS] -> [DataClass('ItemOut')]
|
||||
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
|
||||
val itemDto = item.toDto()
|
||||
val resultDto = apiService.updateItem(itemId, itemDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.deleteItem
|
||||
*/
|
||||
// [END_ENTITY: Function('updateItem')]
|
||||
|
||||
// [ENTITY: Function('deleteItem')]
|
||||
override suspend fun deleteItem(itemId: String) {
|
||||
apiService.deleteItem(itemId)
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.syncInventory
|
||||
*/
|
||||
// [END_ENTITY: Function('deleteItem')]
|
||||
|
||||
// [ENTITY: Function('syncInventory')]
|
||||
// [RELATION: Function('syncInventory')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')]
|
||||
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
|
||||
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
|
||||
return resultDto.toDomain { it.toDomain() }
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.getStatistics
|
||||
*/
|
||||
// [END_ENTITY: Function('syncInventory')]
|
||||
|
||||
// [ENTITY: Function('getStatistics')]
|
||||
// [RELATION: Function('getStatistics')] -> [RETURNS] -> [DataClass('GroupStatistics')]
|
||||
override suspend fun getStatistics(): GroupStatistics {
|
||||
val resultDto = apiService.getStatistics()
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.getAllLocations
|
||||
*/
|
||||
// [END_ENTITY: Function('getStatistics')]
|
||||
|
||||
// [ENTITY: Function('getAllLocations')]
|
||||
// [RELATION: Function('getAllLocations')] -> [RETURNS] -> [DataStructure('List<LocationOutCount>')]
|
||||
override suspend fun getAllLocations(): List<LocationOutCount> {
|
||||
val resultDto = apiService.getLocations()
|
||||
return resultDto.map { it.toDomain() }
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.getAllLabels
|
||||
*/
|
||||
// [END_ENTITY: Function('getAllLocations')]
|
||||
|
||||
// [ENTITY: Function('getAllLabels')]
|
||||
// [RELATION: Function('getAllLabels')] -> [RETURNS] -> [DataStructure('List<LabelOut>')]
|
||||
override suspend fun getAllLabels(): List<LabelOut> {
|
||||
val resultDto = apiService.getLabels()
|
||||
return resultDto.map { it.toDomain() }
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.createLabel
|
||||
*/
|
||||
// [END_ENTITY: Function('getAllLabels')]
|
||||
|
||||
// [ENTITY: Function('createLabel')]
|
||||
// [RELATION: Function('createLabel')] -> [RETURNS] -> [DataClass('LabelSummary')]
|
||||
override suspend fun createLabel(newLabelData: LabelCreate): LabelSummary {
|
||||
// [DATA-FLOW] Convert domain model to DTO for the API call.
|
||||
val labelCreateDto = newLabelData.toDto()
|
||||
// [ACTION] Call the API service.
|
||||
val resultDto = apiService.createLabel(labelCreateDto)
|
||||
// [DATA-FLOW] Convert the resulting DTO back to a domain model.
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.searchItems
|
||||
*/
|
||||
// [END_ENTITY: Function('createLabel')]
|
||||
|
||||
override suspend fun updateLabel(labelId: String, labelData: LabelUpdate): LabelOut {
|
||||
val labelDto = labelData.toDto()
|
||||
val resultDto = apiService.updateLabel(labelId, labelDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun deleteLabel(labelId: String) {
|
||||
apiService.deleteLabel(labelId)
|
||||
}
|
||||
|
||||
override suspend fun createLocation(newLocationData: LocationCreate): LocationOut {
|
||||
val locationDto = newLocationData.toDto()
|
||||
val resultDto = apiService.createLocation(locationDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun updateLocation(locationId: String, locationData: LocationUpdate): LocationOut {
|
||||
val locationDto = locationData.toDto()
|
||||
val resultDto = apiService.updateLocation(locationId, locationDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
override suspend fun deleteLocation(locationId: String) {
|
||||
apiService.deleteLocation(locationId)
|
||||
}
|
||||
|
||||
// [ENTITY: Function('searchItems')]
|
||||
// [RELATION: Function('searchItems')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')]
|
||||
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
|
||||
val resultDto = apiService.getItems(query = query)
|
||||
return resultDto.toDomain { it.toDomain() }
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.getRecentlyAddedItems
|
||||
*/
|
||||
// [END_ENTITY: Function('searchItems')]
|
||||
|
||||
// [ENTITY: Function('getRecentlyAddedItems')]
|
||||
// [RELATION: Function('getRecentlyAddedItems')] -> [RETURNS] -> [DataStructure('Flow<List<ItemSummary>>')]
|
||||
override fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> {
|
||||
return itemDao.getRecentlyAddedItems(limit).map { entities ->
|
||||
entities.map { it.toDomain() }
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('getRecentlyAddedItems')]
|
||||
}
|
||||
// [HELPER] Mapper function for LabelCreate
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Маппер из доменной модели LabelCreate в DTO LabelCreateDto.
|
||||
@return DTO-объект [LabelCreateDto].
|
||||
*/
|
||||
// [END_ENTITY: Repository('ItemRepositoryImpl')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelCreateDto')]
|
||||
private fun LabelCreate.toDto(): LabelCreateDto {
|
||||
return LabelCreateDto(
|
||||
name = this.name,
|
||||
@@ -124,4 +159,27 @@ private fun LabelCreate.toDto(): LabelCreateDto {
|
||||
description = null // Description is not part of the domain model for creation.
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LocationCreateDto')]
|
||||
private fun LocationCreate.toDto(): LocationCreateDto {
|
||||
return LocationCreateDto(
|
||||
name = this.name,
|
||||
color = this.color,
|
||||
description = null // Description is not part of the domain model for creation.
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
|
||||
// [ENTITY: Function('toDto')]
|
||||
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelUpdateDto')]
|
||||
private fun LabelUpdate.toDto(): LabelUpdateDto {
|
||||
return LabelUpdateDto(
|
||||
name = this.name,
|
||||
color = this.color
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('toDto')]
|
||||
|
||||
// [END_FILE_ItemRepositoryImpl.kt]
|
||||
@@ -1,13 +1,14 @@
|
||||
// [PACKAGE] com.homebox.lens.data.security
|
||||
// [FILE] CryptoManager.kt
|
||||
// [PURPOSE] Handles all cryptographic operations using AndroidKeyStore.
|
||||
|
||||
// [SEMANTICS] data, security, cryptography
|
||||
package com.homebox.lens.data.security
|
||||
|
||||
// [IMPORTS]
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import androidx.annotation.RequiresApi
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.KeyStore
|
||||
@@ -17,11 +18,12 @@ import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Class('CryptoManager')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* A manager for handling encryption and decryption using the Android Keystore system.
|
||||
* This class ensures that cryptographic keys are stored securely.
|
||||
* @summary A manager for handling encryption and decryption using the Android Keystore system.
|
||||
* @description This class ensures that cryptographic keys are stored securely.
|
||||
* It is designed to be a Singleton provided by Hilt.
|
||||
* @invariant The underlying SecretKey must be valid within the AndroidKeyStore.
|
||||
*/
|
||||
@@ -29,7 +31,6 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class CryptoManager @Inject constructor() {
|
||||
|
||||
// [ЯКОРЬ] Настройки для шифрования
|
||||
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
|
||||
load(null)
|
||||
}
|
||||
@@ -45,7 +46,6 @@ class CryptoManager @Inject constructor() {
|
||||
}
|
||||
}
|
||||
|
||||
// [CORE-LOGIC] Получение или создание ключа
|
||||
private fun getKey(): SecretKey {
|
||||
val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry
|
||||
return existingKey?.secretKey ?: createKey()
|
||||
@@ -67,8 +67,15 @@ class CryptoManager @Inject constructor() {
|
||||
}.generateKey()
|
||||
}
|
||||
|
||||
// [ACTION] Шифрование потока данных
|
||||
// [ENTITY: Function('encrypt')]
|
||||
/**
|
||||
* @summary Encrypts a byte array and writes it to an output stream.
|
||||
* @param bytes The byte array to encrypt.
|
||||
* @param outputStream The stream to write the encrypted data to.
|
||||
* @return The encrypted byte array.
|
||||
*/
|
||||
fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray {
|
||||
Timber.d("[DEBUG][ACTION][encrypting_data] Encrypting data.")
|
||||
val cipher = encryptCipher
|
||||
val encryptedBytes = cipher.doFinal(bytes)
|
||||
outputStream.use {
|
||||
@@ -79,9 +86,16 @@ class CryptoManager @Inject constructor() {
|
||||
}
|
||||
return encryptedBytes
|
||||
}
|
||||
// [END_ENTITY: Function('encrypt')]
|
||||
|
||||
// [ACTION] Дешифрование потока данных
|
||||
// [ENTITY: Function('decrypt')]
|
||||
/**
|
||||
* @summary Decrypts a byte array from an input stream.
|
||||
* @param inputStream The stream to read the encrypted data from.
|
||||
* @return The decrypted byte array.
|
||||
*/
|
||||
fun decrypt(inputStream: InputStream): ByteArray {
|
||||
Timber.d("[DEBUG][ACTION][decrypting_data] Decrypting data.")
|
||||
return inputStream.use {
|
||||
val ivSize = it.read()
|
||||
val iv = ByteArray(ivSize)
|
||||
@@ -94,6 +108,7 @@ class CryptoManager @Inject constructor() {
|
||||
getDecryptCipherForIv(iv).doFinal(encryptedBytes)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('decrypt')]
|
||||
|
||||
companion object {
|
||||
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||
@@ -103,4 +118,5 @@ class CryptoManager @Inject constructor() {
|
||||
private const val ALIAS = "homebox_lens_secret_key"
|
||||
}
|
||||
}
|
||||
// [END_FILE_CryptoManager.kt]
|
||||
// [END_ENTITY: Class('CryptoManager')]
|
||||
// [END_FILE_CryptoManager.kt]
|
||||
|
||||
Reference in New Issue
Block a user