mirror of
https://github.com/AdrianKuta/KahootQuiz.git
synced 2025-09-14 17:24:21 +02:00
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:
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@@ -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
8
.idea/gradle.xml
generated
@@ -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>
|
||||
|
@@ -37,7 +37,7 @@ class MainActivity : ComponentActivity() {
|
||||
var quizId by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
quizId = getQuizUseCase().id
|
||||
quizId = getQuizUseCase().id.value
|
||||
}
|
||||
|
||||
Greeting(
|
||||
|
@@ -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).
|
@@ -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?,
|
||||
|
@@ -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>?
|
||||
)
|
@@ -0,0 +1,5 @@
|
||||
package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
// Minimal channel info
|
||||
|
||||
data class Channel(val id: String?)
|
@@ -0,0 +1,7 @@
|
||||
package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
data class Choice(
|
||||
val answer: String?,
|
||||
val correct: Boolean?,
|
||||
val languageInfo: LanguageInfo?
|
||||
)
|
@@ -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?
|
||||
)
|
@@ -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>?
|
||||
)
|
@@ -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?
|
||||
)
|
@@ -0,0 +1,9 @@
|
||||
package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
// Crop descriptor
|
||||
|
||||
data class Crop(
|
||||
val origin: Point?,
|
||||
val target: Point?,
|
||||
val circular: Boolean?
|
||||
)
|
@@ -0,0 +1,8 @@
|
||||
package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
// Color extracted from cover image
|
||||
|
||||
data class ExtractedColor(
|
||||
val swatch: String?,
|
||||
val rgbHex: String?
|
||||
)
|
@@ -0,0 +1,8 @@
|
||||
package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
// Featured list membership
|
||||
|
||||
data class FeaturedListMembership(
|
||||
val list: String?,
|
||||
val addedAt: Long?
|
||||
)
|
@@ -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
|
||||
)
|
@@ -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?
|
||||
)
|
@@ -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?
|
||||
)
|
@@ -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
|
||||
)
|
@@ -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?
|
||||
)
|
@@ -0,0 +1,8 @@
|
||||
package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
// Geometry helpers
|
||||
|
||||
data class Point(
|
||||
val x: Int?,
|
||||
val y: Int?
|
||||
)
|
@@ -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?
|
||||
)
|
@@ -0,0 +1,3 @@
|
||||
package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
// Moved: data classes have been split into separate files.
|
@@ -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?
|
||||
)
|
@@ -0,0 +1,5 @@
|
||||
package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
data class QuizId(
|
||||
val value: String,
|
||||
)
|
@@ -0,0 +1,9 @@
|
||||
package dev.adriankuta.kahootquiz.domain.models
|
||||
|
||||
// Version metadata
|
||||
|
||||
data class VersionMetadata(
|
||||
val version: Int?,
|
||||
val created: Long?,
|
||||
val creator: String?
|
||||
)
|
@@ -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?
|
||||
)
|
@@ -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
|
||||
)
|
||||
|
Reference in New Issue
Block a user