feat: Expand domain models and implement full DTO to domain mapping

This commit significantly expands the domain models to fully represent the quiz structure and implements the complete mapping logic in `QuizMapper` to convert `QuizResponseDto` and its nested DTOs to their corresponding domain models.

Key changes:

- **Domain Layer (`domain` module):**
    - Introduced a `QuizId` value class for type safety.
    - Added comprehensive domain models for all aspects of a quiz, including:
        - `Quiz`: Updated to include all fields from the DTO.
        - `Question`, `Choice`, `Video`, `ImageMetadata`, `MediaItem`, `ChoiceRange`: For question details.
        - `CoverMetadata`, `ExtractedColor`, `Crop`, `Point`: For cover image information.
        - `ContentTags`: For curriculum and generated codes.
        - `Metadata`, `Access`, `FeaturedListMembership`, `LastEdit`, `VersionMetadata`: For quiz metadata.
        - `LanguageInfo`, `Channel`: Common reusable models.
    - Organized domain models into separate files for better maintainability (e.g., `Question.kt`, `CoverMetadata.kt`).
    - Added a placeholder `QuestionModels.kt` to indicate that models were moved.
- **Data Layer (`model:data` module):**
    - Implemented a complete `toDomainModel()` extension function in `QuizMapper.kt` to map all fields from `QuizResponseDto` and its nested DTOs (like `CoverMetadataDto`, `QuestionDto`, etc.) to the new domain models.
    - This includes mapping for lists and nullable fields.
- **App Module (`app` module):**
    - Updated `MainActivity` to access `quizId.value` due to `QuizId` being a value class.
- **Network Layer (`core:network` module):**
    - Changed `QuizResponseDto.uuid` to be non-nullable (`String`) as it's essential for mapping to `QuizId`.
    - Removed `QuizResponse.kt` as DTOs were previously split into individual files.
This commit is contained in:
GitHub Actions Bot
2025-09-03 12:45:53 +02:00
parent 293b7f75b9
commit fd0678c1fd
27 changed files with 417 additions and 18 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-01T13:50:36.976965Z">
<DropdownSelection timestamp="2025-09-02T20:25:37.879520Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/adriankuta/.android/avd/Pixel_8_Pro.avd" />
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/adriankuta/.android/avd/Pixel_9_Pro.avd" />
</handle>
</Target>
</DropdownSelection>

8
.idea/gradle.xml generated
View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
@@ -24,6 +25,13 @@
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/build-logic" />
<option value="$PROJECT_DIR$/build-logic/convention" />
<option value="$PROJECT_DIR$/core" />
<option value="$PROJECT_DIR$/core/network" />
<option value="$PROJECT_DIR$/domain" />
<option value="$PROJECT_DIR$/model" />
<option value="$PROJECT_DIR$/model/data" />
<option value="$PROJECT_DIR$/ui" />
<option value="$PROJECT_DIR$/ui/quiz" />
</set>
</option>
</GradleProjectSettings>

View File

@@ -37,7 +37,7 @@ class MainActivity : ComponentActivity() {
var quizId by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
quizId = getQuizUseCase().id
quizId = getQuizUseCase().id.value
}
Greeting(

View File

@@ -1,10 +0,0 @@
package dev.adriankuta.kahootquiz.core.network.models
// This file used to contain all DTOs in one place.
// The DTOs have been split into separate files for maintainability:
// - QuizResponseDto.kt
// - CommonDtos.kt
// - CoverDtos.kt
// - QuestionDtos.kt
// - MetadataDtos.kt
// Keeping this file as a placeholder to avoid breaking any imports by file path (package remains the same).

View File

@@ -4,7 +4,7 @@ import com.google.gson.annotations.SerializedName
// Root response for a Kahoot quiz details (network layer DTO)
data class QuizResponseDto(
val uuid: String?,
val uuid: String,
val language: String?,
val creator: String?,
@SerializedName("creator_username") val creatorUsername: String?,

View File

@@ -0,0 +1,9 @@
package dev.adriankuta.kahootquiz.domain.models
// Access settings
data class Access(
val groupRead: List<String>?,
val folderGroupIds: List<String>?,
val features: List<String>?
)

View File

@@ -0,0 +1,5 @@
package dev.adriankuta.kahootquiz.domain.models
// Minimal channel info
data class Channel(val id: String?)

View File

@@ -0,0 +1,7 @@
package dev.adriankuta.kahootquiz.domain.models
data class Choice(
val answer: String?,
val correct: Boolean?,
val languageInfo: LanguageInfo?
)

View File

@@ -0,0 +1,11 @@
package dev.adriankuta.kahootquiz.domain.models
// Slider range for "slider" question type
data class ChoiceRange(
val start: Int?,
val end: Int?,
val step: Int?,
val correct: Int?,
val tolerance: Int?
)

View File

@@ -0,0 +1,8 @@
package dev.adriankuta.kahootquiz.domain.models
// Content tags domain model
data class ContentTags(
val curriculumCodes: List<String>?,
val generatedCurriculumCodes: List<String>?
)

View File

@@ -0,0 +1,17 @@
package dev.adriankuta.kahootquiz.domain.models
// Cover metadata and related domain models
data class CoverMetadata(
val id: String?,
val altText: String?,
val contentType: String?,
val origin: String?,
val externalRef: String?,
val resources: String?,
val width: Int?,
val height: Int?,
val extractedColors: List<ExtractedColor>?,
val blurhash: String?,
val crop: Crop?
)

View File

@@ -0,0 +1,9 @@
package dev.adriankuta.kahootquiz.domain.models
// Crop descriptor
data class Crop(
val origin: Point?,
val target: Point?,
val circular: Boolean?
)

View File

@@ -0,0 +1,8 @@
package dev.adriankuta.kahootquiz.domain.models
// Color extracted from cover image
data class ExtractedColor(
val swatch: String?,
val rgbHex: String?
)

View File

@@ -0,0 +1,8 @@
package dev.adriankuta.kahootquiz.domain.models
// Featured list membership
data class FeaturedListMembership(
val list: String?,
val addedAt: Long?
)

View File

@@ -0,0 +1,16 @@
package dev.adriankuta.kahootquiz.domain.models
// Image metadata appearing in multiple places
data class ImageMetadata(
val id: String?,
val altText: String? = null,
val contentType: String?,
val origin: String? = null,
val externalRef: String? = null,
val resources: String? = null,
val width: Int? = null,
val height: Int? = null,
val effects: List<String>? = null,
val crop: Crop? = null
)

View File

@@ -0,0 +1,9 @@
package dev.adriankuta.kahootquiz.domain.models
// Common simple models reused across the domain layer
data class LanguageInfo(
val language: String?,
val lastUpdatedOn: Long?,
val readAloudSupported: Boolean?
)

View File

@@ -0,0 +1,9 @@
package dev.adriankuta.kahootquiz.domain.models
// Last edit information
data class LastEdit(
val editorUserId: String?,
val editorUsername: String?,
val editTimestamp: Long?
)

View File

@@ -0,0 +1,18 @@
package dev.adriankuta.kahootquiz.domain.models
// Generic media item on question
data class MediaItem(
val type: String?,
val zIndex: Int?,
val isColorOnly: Boolean?,
val id: String?,
val altText: String? = null,
val contentType: String?,
val origin: String? = null,
val externalRef: String? = null,
val resources: String? = null,
val width: Int? = null,
val height: Int? = null,
val crop: Crop? = null
)

View File

@@ -0,0 +1,11 @@
package dev.adriankuta.kahootquiz.domain.models
// Metadata section domain models
data class Metadata(
val access: Access?,
val duplicationProtection: Boolean?,
val featuredListMemberships: List<FeaturedListMembership>?,
val lastEdit: LastEdit?,
val versionMetadata: VersionMetadata?
)

View File

@@ -0,0 +1,8 @@
package dev.adriankuta.kahootquiz.domain.models
// Geometry helpers
data class Point(
val x: Int?,
val y: Int?
)

View File

@@ -0,0 +1,23 @@
package dev.adriankuta.kahootquiz.domain.models
import kotlin.time.Duration
// Question and choice related domain models
data class Question(
val type: String?,
val question: String?,
val time: Duration?,
val points: Boolean?,
val pointsMultiplier: Int?,
val choices: List<Choice>?,
val layout: String?,
val image: String?,
val imageMetadata: ImageMetadata?,
val resources: String?,
val video: Video?,
val questionFormat: Int?,
val languageInfo: LanguageInfo?,
val media: List<MediaItem>?,
val choiceRange: ChoiceRange?
)

View File

@@ -0,0 +1,3 @@
package dev.adriankuta.kahootquiz.domain.models
// Moved: data classes have been split into separate files.

View File

@@ -1,5 +1,35 @@
package dev.adriankuta.kahootquiz.domain.models
// Domain model representing a Quiz and its nested data
data class Quiz(
val id: String
val id: QuizId,
val language: String?,
val creator: String?,
val creatorUsername: String?,
val compatibilityLevel: Int?,
val creatorPrimaryUsage: String?,
val folderId: String?,
val themeId: String?,
val visibility: Int?,
val audience: String?,
val title: String?,
val description: String?,
val quizType: String?,
val cover: String?,
val coverMetadata: CoverMetadata?,
val questions: List<Question>,
val contentTags: ContentTags?,
val metadata: Metadata?,
val resources: String?,
val slug: String?,
val languageInfo: LanguageInfo?,
val inventoryItemIds: List<String>,
val channels: List<Channel>,
val isValid: Boolean?,
val playAsGuest: Boolean?,
val hasRestrictedContent: Boolean?,
val type: String?,
val created: Long?,
val modified: Long?
)

View File

@@ -0,0 +1,5 @@
package dev.adriankuta.kahootquiz.domain.models
data class QuizId(
val value: String,
)

View File

@@ -0,0 +1,9 @@
package dev.adriankuta.kahootquiz.domain.models
// Version metadata
data class VersionMetadata(
val version: Int?,
val created: Long?,
val creator: String?
)

View File

@@ -0,0 +1,9 @@
package dev.adriankuta.kahootquiz.domain.models
data class Video(
val id: String? = null,
val startTime: Int?,
val endTime: Int?,
val service: String?,
val fullUrl: String?
)

View File

@@ -1,7 +1,176 @@
package dev.adriankuta.kahootquiz.model.data.mappers
import dev.adriankuta.kahootquiz.core.network.models.QuizResponseDto
import dev.adriankuta.kahootquiz.domain.models.Quiz
import dev.adriankuta.kahootquiz.core.network.models.*
import dev.adriankuta.kahootquiz.domain.models.*
internal fun QuizResponseDto.toDomainModel(): Quiz =
Quiz(this.uuid ?: "")
Quiz(
id = QuizId(uuid),
language = language,
creator = creator,
creatorUsername = creatorUsername,
compatibilityLevel = compatibilityLevel,
creatorPrimaryUsage = creatorPrimaryUsage,
folderId = folderId,
themeId = themeId,
visibility = visibility,
audience = audience,
title = title,
description = description,
quizType = quizType,
cover = cover,
coverMetadata = coverMetadata?.toDomain(),
questions = questions?.map { it.toDomain() } ?: emptyList(),
contentTags = contentTags?.toDomain(),
metadata = metadata?.toDomain(),
resources = resources,
slug = slug,
languageInfo = languageInfo?.toDomain(),
inventoryItemIds = inventoryItemIds ?: emptyList(),
channels = channels?.map { it.toDomain() } ?: emptyList(),
isValid = isValid,
playAsGuest = playAsGuest,
hasRestrictedContent = hasRestrictedContent,
type = type,
created = created,
modified = modified
)
private fun CoverMetadataDto.toDomain(): CoverMetadata = CoverMetadata(
id = id,
altText = altText,
contentType = contentType,
origin = origin,
externalRef = externalRef,
resources = resources,
width = width,
height = height,
extractedColors = extractedColors?.map { it.toDomain() },
blurhash = blurhash,
crop = crop?.toDomain()
)
private fun ExtractedColorDto.toDomain(): ExtractedColor = ExtractedColor(
swatch = swatch,
rgbHex = rgbHex
)
private fun CropDto.toDomain(): Crop = Crop(
origin = origin?.toDomain(),
target = target?.toDomain(),
circular = circular
)
private fun PointDto.toDomain(): Point = Point(x = x, y = y)
private fun ContentTagsDto.toDomain(): ContentTags = ContentTags(
curriculumCodes = curriculumCodes,
generatedCurriculumCodes = generatedCurriculumCodes
)
private fun MetadataDto.toDomain(): Metadata = Metadata(
access = access?.toDomain(),
duplicationProtection = duplicationProtection,
featuredListMemberships = featuredListMemberships?.map { it.toDomain() },
lastEdit = lastEdit?.toDomain(),
versionMetadata = versionMetadata?.toDomain()
)
private fun AccessDto.toDomain(): Access = Access(
groupRead = groupRead,
folderGroupIds = folderGroupIds,
features = features
)
private fun FeaturedListMembershipDto.toDomain(): FeaturedListMembership = FeaturedListMembership(
list = list,
addedAt = addedAt
)
private fun LastEditDto.toDomain(): LastEdit = LastEdit(
editorUserId = editorUserId,
editorUsername = editorUsername,
editTimestamp = editTimestamp
)
private fun VersionMetadataDto.toDomain(): VersionMetadata = VersionMetadata(
version = version,
created = created,
creator = creator
)
private fun LanguageInfoDto.toDomain(): LanguageInfo = LanguageInfo(
language = language,
lastUpdatedOn = lastUpdatedOn,
readAloudSupported = readAloudSupported
)
private fun ChannelDto.toDomain(): Channel = Channel(id = id)
private fun QuestionDto.toDomain(): Question = Question(
type = type,
question = question,
time = time,
points = points,
pointsMultiplier = pointsMultiplier,
choices = choices?.map { it.toDomain() },
layout = layout,
image = image,
imageMetadata = imageMetadata?.toDomain(),
resources = resources,
video = video?.toDomain(),
questionFormat = questionFormat,
languageInfo = languageInfo?.toDomain(),
media = media?.map { it.toDomain() },
choiceRange = choiceRange?.toDomain()
)
private fun ChoiceDto.toDomain(): Choice = Choice(
answer = answer,
correct = correct,
languageInfo = languageInfo?.toDomain()
)
private fun VideoDto.toDomain(): Video = Video(
id = id,
startTime = startTime,
endTime = endTime,
service = service,
fullUrl = fullUrl
)
private fun ImageMetadataDto.toDomain(): ImageMetadata = ImageMetadata(
id = id,
altText = altText,
contentType = contentType,
origin = origin,
externalRef = externalRef,
resources = resources,
width = width,
height = height,
effects = effects,
crop = crop?.toDomain()
)
private fun MediaItemDto.toDomain(): MediaItem = MediaItem(
type = type,
zIndex = zIndex,
isColorOnly = isColorOnly,
id = id,
altText = altText,
contentType = contentType,
origin = origin,
externalRef = externalRef,
resources = resources,
width = width,
height = height,
crop = crop?.toDomain()
)
private fun ChoiceRangeDto.toDomain(): ChoiceRange = ChoiceRange(
start = start,
end = end,
step = step,
correct = correct,
tolerance = tolerance
)