Skip to content

Commit

Permalink
Merge pull request #19 from Lukinhasssss/feature/video-gateway
Browse files Browse the repository at this point in the history
Automated PR from feature/video-gateway
  • Loading branch information
Lukinhasssss committed May 28, 2024
2 parents 059a20b + d469972 commit 01218a3
Show file tree
Hide file tree
Showing 8 changed files with 909 additions and 14 deletions.
2 changes: 1 addition & 1 deletion config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ style:
maxChainedCalls: 5
MaxLineLength:
active: true
maxLineLength: 120
maxLineLength: 140
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
Expand Down
44 changes: 32 additions & 12 deletions domain/src/test/kotlin/com/lukinhasssss/catalogo/domain/Fixture.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.lukinhasssss.catalogo.domain.utils.InstantUtils.now
import com.lukinhasssss.catalogo.domain.video.Rating
import com.lukinhasssss.catalogo.domain.video.Video
import net.datafaker.Faker
import java.time.Year

object Fixture {

Expand Down Expand Up @@ -43,10 +42,10 @@ object Fixture {
fun systemDesign() = Video.with(
id = IdUtils.uuid(),
title = "System Design",
description = "System Design description",
launchedAt = Year.now().value,
description = "O video mais assistido",
launchedAt = 2024,
duration = 60.0,
rating = Rating.L.name,
rating = Rating.AGE_16.name,
opened = true,
published = true,
createdAt = now().toString(),
Expand All @@ -56,16 +55,37 @@ object Fixture {
thumbnailHalf = "https://thumbnail-half.com",
trailer = "https://trailer.com",
video = "https://video.com",
categories = setOf(IdUtils.uuid()),
genres = setOf(IdUtils.uuid()),
castMembers = setOf(IdUtils.uuid())
categories = setOf("aulas"),
genres = setOf("systemdesign"),
castMembers = setOf("nami")
)

fun cleanCode() = Video.with(
id = IdUtils.uuid(),
title = "Clean Code",
description = "Clean Code description",
launchedAt = Year.now().value,
description = "Como escrever um codigo limpo",
launchedAt = 2025,
duration = 60.0,
rating = Rating.AGE_10.name,
opened = true,
published = true,
createdAt = now().toString(),
updatedAt = now().toString(),
banner = "https://banner.com",
thumbnail = "https://thumbnail.com",
thumbnailHalf = "https://thumbnail-half.com",
trailer = "https://trailer.com",
video = "https://video.com",
categories = setOf("lives"),
genres = setOf("cleancode"),
castMembers = setOf("zoro")
)

fun golang() = Video.with(
id = IdUtils.uuid(),
title = "Golang",
description = "Linguagem de programacao que surgiu em 2009",
launchedAt = 2077,
duration = 60.0,
rating = Rating.L.name,
opened = true,
Expand All @@ -77,9 +97,9 @@ object Fixture {
thumbnailHalf = "https://thumbnail-half.com",
trailer = "https://trailer.com",
video = "https://video.com",
categories = setOf(IdUtils.uuid()),
genres = setOf(IdUtils.uuid()),
castMembers = setOf(IdUtils.uuid())
categories = setOf("meeting"),
genres = setOf("golang"),
castMembers = setOf("luffy")
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.lukinhasssss.catalogo.infrastructure.video

import com.lukinhasssss.catalogo.domain.pagination.Pagination
import com.lukinhasssss.catalogo.domain.video.Video
import com.lukinhasssss.catalogo.domain.video.VideoGateway
import com.lukinhasssss.catalogo.domain.video.VideoSearchQuery
import com.lukinhasssss.catalogo.infrastructure.video.VideoQueryBuilder.Companion.containingCastMembers
import com.lukinhasssss.catalogo.infrastructure.video.VideoQueryBuilder.Companion.containingCategories
import com.lukinhasssss.catalogo.infrastructure.video.VideoQueryBuilder.Companion.containingGenres
import com.lukinhasssss.catalogo.infrastructure.video.VideoQueryBuilder.Companion.launchedAtEquals
import com.lukinhasssss.catalogo.infrastructure.video.VideoQueryBuilder.Companion.onlyPublished
import com.lukinhasssss.catalogo.infrastructure.video.VideoQueryBuilder.Companion.ratingEquals
import com.lukinhasssss.catalogo.infrastructure.video.VideoQueryBuilder.Companion.titleOrDescriptionContaining
import com.lukinhasssss.catalogo.infrastructure.video.persistence.VideoDocument
import com.lukinhasssss.catalogo.infrastructure.video.persistence.VideoRepository
import org.springframework.context.annotation.Profile
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.data.domain.Sort.Direction
import org.springframework.data.elasticsearch.client.elc.NativeQuery
import org.springframework.data.elasticsearch.core.SearchOperations
import org.springframework.data.elasticsearch.core.search
import org.springframework.stereotype.Component

@Component
@Profile("!development")
class VideoElasticsearchGateway(
private val videoRepository: VideoRepository,
private val searchOperations: SearchOperations
) : VideoGateway {

private companion object {
const val TITLE_PROP = "title"
const val KEYWORD = ".keyword"
}

override fun save(video: Video): Video {
videoRepository.save(VideoDocument.from(video))
return video
}

override fun findById(id: String): Video? =
if (id.isBlank()) null else videoRepository.findById(id).map { it.toVideo() }.orElse(null)

override fun findAll(aQuery: VideoSearchQuery): Pagination<Video> = with(aQuery) {
val aQueryBuilder = VideoQueryBuilder(
onlyPublished(),
containingCategories(categories),
containingCastMembers(castMembers),
containingGenres(genres),
launchedAtEquals(launchedAt),
ratingEquals(rating),
titleOrDescriptionContaining(terms)
).build()

val query = NativeQuery.builder()
.withQuery(aQueryBuilder)
.withPageable(PageRequest.of(page, perPage, Sort.by(Direction.fromString(direction), buildSort(sort))))
.build()

val result = searchOperations.search<VideoDocument>(query)

val total = result.totalHits
val videos = result.map { it.content.toVideo() }.toList()

Pagination(
currentPage = page,
perPage = perPage,
total = total,
items = videos
)
}

override fun deleteById(id: String) = if (id.isBlank()) Unit else videoRepository.deleteById(id)

private fun buildSort(sort: String) = if (TITLE_PROP.equals(sort, ignoreCase = true)) sort.plus(KEYWORD) else sort
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import com.lukinhasssss.catalogo.domain.pagination.Pagination
import com.lukinhasssss.catalogo.domain.video.Video
import com.lukinhasssss.catalogo.domain.video.VideoGateway
import com.lukinhasssss.catalogo.domain.video.VideoSearchQuery
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component

// @Profile("development")
@Component
@Profile("development")
class VideoInMemoryGateway(
private val db: MutableMap<String, Video> = mutableMapOf()
) : VideoGateway {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.lukinhasssss.catalogo.infrastructure.video

import co.elastic.clients.elasticsearch._types.FieldValue
import co.elastic.clients.elasticsearch._types.query_dsl.Query
import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders
import com.lukinhasssss.catalogo.infrastructure.video.VideoQueryBuilder.Option

class VideoQueryBuilder(vararg opts: Option) {
private val must: MutableList<Query> = mutableListOf()

init {
opts.forEach { it.invoke(this) }
}

fun must(aQuery: Query): VideoQueryBuilder {
must.add(aQuery)
return this
}

fun build(): Query = QueryBuilders.bool { b -> b.must(must) }

fun interface Option : (VideoQueryBuilder) -> Unit

companion object {
private val NOOP: Option = Option { }

fun onlyPublished(): Option = Option { b -> b.must(QueryBuilders.term { t -> t.field("published").value(true) }) }

fun containingCategories(categories: Set<String>): Option {
if (categories.isEmpty()) {
return NOOP
}

return Option { b -> b.must(QueryBuilders.terms { t -> t.field("categories").terms { it.value(fieldValues(categories)) } }) }
}

fun containingCastMembers(members: Set<String>): Option {
if (members.isEmpty()) {
return NOOP
}

return Option { b -> b.must(QueryBuilders.terms { t -> t.field("cast_members").terms { it.value(fieldValues(members)) } }) }
}

fun containingGenres(genres: Set<String>): Option {
if (genres.isEmpty()) {
return NOOP
}

return Option { b -> b.must(QueryBuilders.terms { t -> t.field("genres").terms { it.value(fieldValues(genres)) } }) }
}

fun launchedAtEquals(launchedAt: Int?): Option {
if (launchedAt == null) {
return NOOP
}

return Option { b -> b.must(QueryBuilders.term { t -> t.field("launched_at").value(launchedAt.toLong()) }) }
}

fun ratingEquals(rating: String?): Option {
if (rating.isNullOrBlank()) {
return NOOP
}

return Option { b -> b.must(QueryBuilders.term { t -> t.field("rating").value(rating) }) }
}

fun titleOrDescriptionContaining(terms: String?): Option {
if (terms.isNullOrBlank()) {
return NOOP
}

return Option { b -> b.must(QueryBuilders.queryString { q -> q.fields("title", "description").query("*$terms*") }) }
}

private fun fieldValues(ids: Set<String>): List<FieldValue> = ids.map { FieldValue.of(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.lukinhasssss.catalogo.infrastructure.video.persistence

import com.lukinhasssss.catalogo.domain.video.Video
import org.springframework.data.annotation.Id
import org.springframework.data.elasticsearch.annotations.Document
import org.springframework.data.elasticsearch.annotations.Field
import org.springframework.data.elasticsearch.annotations.FieldType
import org.springframework.data.elasticsearch.annotations.InnerField
import org.springframework.data.elasticsearch.annotations.MultiField

@Document(indexName = "videos")
data class VideoDocument(

@Id
val id: String,

@MultiField(
mainField = Field(type = FieldType.Text, name = "title"),
otherFields = [ InnerField(suffix = "keyword", type = FieldType.Keyword) ]
)
val title: String,

@Field(type = FieldType.Text, name = "description")
val description: String,

@Field(type = FieldType.Integer, name = "launched_at")
val launchedAt: Int,

@Field(type = FieldType.Double, name = "duration")
val duration: Double,

@Field(type = FieldType.Keyword, name = "rating")
val rating: String,

@Field(type = FieldType.Boolean, name = "opened")
val opened: Boolean,

@Field(type = FieldType.Boolean, name = "published")
val published: Boolean,

@Field(type = FieldType.Keyword, name = "banner")
val banner: String? = null,

@Field(type = FieldType.Keyword, name = "thumbnail")
val thumbnail: String? = null,

@Field(type = FieldType.Keyword, name = "thumbnail_half")
val thumbnailHalf: String? = null,

@Field(type = FieldType.Keyword, name = "trailer")
val trailer: String? = null,

@Field(type = FieldType.Keyword, name = "video")
val video: String? = null,

@Field(type = FieldType.Keyword, name = "categories")
val categories: Set<String> = setOf(),

@Field(type = FieldType.Keyword, name = "cast_members")
val castMembers: Set<String> = setOf(),

@Field(type = FieldType.Keyword, name = "genres")
val genres: Set<String> = setOf(),

@Field(type = FieldType.Date, name = "created_at")
val createdAt: String,

@Field(type = FieldType.Date, name = "updated_at")
val updatedAt: String
) {

companion object {
fun from(aVideo: Video) = with(aVideo) {
VideoDocument(
id = id,
title = title,
description = description,
launchedAt = launchedAt.value,
duration = duration,
rating = rating.name,
opened = opened,
published = published,
banner = banner,
thumbnail = thumbnail,
thumbnailHalf = thumbnailHalf,
trailer = trailer,
video = video,
categories = categories,
castMembers = castMembers,
genres = genres,
createdAt = createdAt.toString(),
updatedAt = updatedAt.toString()
)
}
}

fun toVideo() = Video.with(
id = id,
title = title,
description = description,
launchedAt = launchedAt,
duration = duration,
rating = rating,
opened = opened,
published = published,
banner = banner,
thumbnail = thumbnail,
thumbnailHalf = thumbnailHalf,
trailer = trailer,
video = video,
categories = categories,
castMembers = castMembers,
genres = genres,
createdAt = createdAt,
updatedAt = updatedAt
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.lukinhasssss.catalogo.infrastructure.video.persistence

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository

interface VideoRepository : ElasticsearchRepository<VideoDocument, String>
Loading

0 comments on commit 01218a3

Please sign in to comment.