Skip to content

Commit

Permalink
Add query for loading kotlin weekly entries by url.
Browse files Browse the repository at this point in the history
  • Loading branch information
ychescale9 committed Dec 25, 2023
1 parent 3751df7 commit f0c0cfc
Show file tree
Hide file tree
Showing 12 changed files with 1,380 additions and 7 deletions.
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ dependencies {
implementation(libs.ktor.serialization.xml)
implementation(libs.apacheCommonsText)
implementation(libs.apacheCommonsLang3)
implementation(libs.apacheCommonsNet)
implementation(libs.caffeine)
implementation(libs.scrapeit)
implementation(libs.jsoup)
implementation(libs.xalan)

testImplementation(libs.spring.boot.starter.test)
testImplementation(kotlin("test"))
Expand Down
8 changes: 8 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ dgsCodegen = "6.1.1"
detekt = "1.23.4"
apacheCommonsText = "1.11.0"
apacheCommonsLang3 = "3.14.0"
apacheCommonsNet = "3.10.0"
caffeine = "3.1.8"
scrapeit = "1.3.0-alpha.2"
jsoup = "1.17.1"
xalan = "2.7.3"
toolchainsResolver = "0.7.0"

[plugins]
Expand All @@ -35,4 +39,8 @@ dgs-bom = { module = "com.netflix.graphql.dgs:graphql-dgs-platform-dependencies"
dgs-starter = { module = "com.netflix.graphql.dgs:graphql-dgs-webflux-starter"}
apacheCommonsText = { module = "org.apache.commons:commons-text", version.ref = "apacheCommonsText" }
apacheCommonsLang3 = { module = "org.apache.commons:commons-lang3", version.ref = "apacheCommonsLang3" }
apacheCommonsNet = { module = "commons-net:commons-net", version.ref = "apacheCommonsNet" }
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" }
scrapeit = { module = "it.skrape:skrapeit", version.ref = "scrapeit" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
xalan = { module = "xalan:xalan", version.ref = "xalan" }
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package io.github.reactivecircus.kstreamlined.backend

import io.github.reactivecircus.kstreamlined.backend.client.ClientConfigs
import io.github.reactivecircus.kstreamlined.backend.client.FeedClient
import io.github.reactivecircus.kstreamlined.backend.client.KotlinWeeklyIssueClient
import io.github.reactivecircus.kstreamlined.backend.client.RealFeedClient
import io.github.reactivecircus.kstreamlined.backend.client.RealKotlinWeeklyIssueClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.cio.CIO
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
Expand All @@ -12,13 +15,30 @@ import org.springframework.context.annotation.Configuration
class KSConfiguration {

@Bean
fun feedClient(clientConfigs: ClientConfigs): FeedClient {
fun feedClient(
engine: HttpClientEngine,
clientConfigs: ClientConfigs,
): FeedClient {
return RealFeedClient(
engine = CIO.create(),
engine = engine,
clientConfigs = clientConfigs,
)
}

@Bean
fun kotlinWeeklyIssueClient(
engine: HttpClientEngine
): KotlinWeeklyIssueClient {
return RealKotlinWeeklyIssueClient(
engine = engine,
)
}

@Bean
fun httpClientEngine(): HttpClientEngine {
return CIO.create()
}

@Bean
fun clientConfigs(
@Value("\${ks.kotlin-blog-feed-url}") kotlinBlogFeedUrl: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.github.reactivecircus.kstreamlined.backend.client

import io.github.reactivecircus.kstreamlined.backend.schema.generated.types.KotlinWeeklyIssueEntry
import io.github.reactivecircus.kstreamlined.backend.schema.generated.types.KotlinWeeklyIssueEntryType
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import it.skrape.core.htmlDocument
import it.skrape.selects.eachText
import it.skrape.selects.html5.a
import it.skrape.selects.html5.div
import it.skrape.selects.html5.span

interface KotlinWeeklyIssueClient {
suspend fun loadKotlinWeeklyIssue(url: String): List<KotlinWeeklyIssueEntry>
}

class RealKotlinWeeklyIssueClient(
engine: HttpClientEngine,
) : KotlinWeeklyIssueClient {

private val httpClient = HttpClient(engine) {
expectSuccess = true
install(HttpTimeout) {
connectTimeoutMillis = HttpTimeoutMillis
requestTimeoutMillis = HttpTimeoutMillis
}
}

override suspend fun loadKotlinWeeklyIssue(url: String): List<KotlinWeeklyIssueEntry> {
return buildList {
htmlDocument(httpClient.get(url).bodyAsText()) {
div {
withAttribute = "style" to "overflow: hidden;"
findAll {
forEach { section ->
val type = section.div {
findFirst { text }
}.uppercase().let {
KotlinWeeklyIssueEntryType.entries.find { type -> it == type.name } ?: return@forEach
}

val titleWithLinkPairs = mutableListOf<Pair<String, String>>()
val summaries = mutableListOf<String>()
val sources = mutableListOf<String>()

section.div {
findSecond {
a {
findAll {
forEach {
it.eachHref.first().let { url ->
if (it.text != url) {
titleWithLinkPairs.add(it.text to url)
} else {
sources.add(url)
}
}
}
}
}
span {
withAttribute = "style" to "font-size:14px"
findAll {
eachText.forEach {
summaries.add(it)
}
}
}
}
}

titleWithLinkPairs.forEachIndexed { index, pair ->
add(
KotlinWeeklyIssueEntry(
type = type,
title = pair.first,
url = pair.second,
summary = summaries[index],
source = sources[index],
)
)
}
}
}
}
}
}
}

companion object {
private const val HttpTimeoutMillis = 10_000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import java.time.Duration

@DgsComponent
class FeedEntryDataFetcher(
private val feedClient: FeedClient,
private val client: FeedClient,
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
private val kotlinBlogCacheContext = object : CacheContext<KotlinBlogItem> {
Expand Down Expand Up @@ -71,19 +71,19 @@ class FeedEntryDataFetcher(
async(coroutineDispatcher) {
when (source) {
FeedSourceKey.KOTLIN_BLOG -> with(kotlinBlogCacheContext) {
feedClient.loadKotlinBlogFeed().map { it.toKotlinBlogEntry() }
client.loadKotlinBlogFeed().map { it.toKotlinBlogEntry() }
}

FeedSourceKey.KOTLIN_YOUTUBE_CHANNEL -> with(kotlinYouTubeCacheContext) {
feedClient.loadKotlinYouTubeFeed().map { it.toKotlinYouTubeEntry() }
client.loadKotlinYouTubeFeed().map { it.toKotlinYouTubeEntry() }
}

FeedSourceKey.TALKING_KOTLIN_PODCAST -> with(talkingKotlinCacheContext) {
feedClient.loadTalkingKotlinFeed().map { it.toTalkingKotlinEntry() }
client.loadTalkingKotlinFeed().map { it.toTalkingKotlinEntry() }
}

FeedSourceKey.KOTLIN_WEEKLY -> with(kotlinWeeklyCacheContext) {
feedClient.loadKotlinWeeklyFeed().map { it.toKotlinWeeklyEntry() }
client.loadKotlinWeeklyFeed().map { it.toKotlinWeeklyEntry() }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.github.reactivecircus.kstreamlined.backend.datafetcher

import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.netflix.graphql.dgs.DgsComponent
import com.netflix.graphql.dgs.DgsQuery
import com.netflix.graphql.dgs.InputArgument
import io.github.reactivecircus.kstreamlined.backend.client.KotlinWeeklyIssueClient
import io.github.reactivecircus.kstreamlined.backend.schema.generated.DgsConstants
import io.github.reactivecircus.kstreamlined.backend.schema.generated.types.KotlinWeeklyIssueEntry
import java.time.Duration

@DgsComponent
class KotlinWeeklyIssueDataFetcher(
private val client: KotlinWeeklyIssueClient
) {
private val cache: Cache<String, List<KotlinWeeklyIssueEntry>> = Caffeine
.newBuilder()
.expireAfterAccess(Duration.ofHours(1))
.build()

@DgsQuery(field = DgsConstants.QUERY.KotlinWeeklyIssue)
suspend fun kotlinWeeklyIssue(@InputArgument url: String): List<KotlinWeeklyIssueEntry> {
return cache.getIfPresent(url) ?: client.loadKotlinWeeklyIssue(url).also {
cache.put(url, it)
}
}
}
28 changes: 28 additions & 0 deletions src/main/resources/schema/kstreamlined.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ type Query {
feedEntries(filters: [FeedSourceKey!] = null): [FeedEntry!]!
"Returns list of all available feed sources."
feedSources: [FeedSource!]!
"Returns list of entries for a Kotlin Weekly issue."
kotlinWeeklyIssue(url: String!): [KotlinWeeklyIssueEntry]!
}

type FeedSource {
Expand Down Expand Up @@ -96,5 +98,31 @@ type KotlinWeekly implements FeedEntry {
contentUrl: String!
}

type KotlinWeeklyIssueEntry {
"Title of the issue entry."
title: String!
"Summary of the issue entry."
summary: String!
"Url of the issue entry."
url: String!
"Url of the issue entry source."
source: String!
"Type of the issue entry."
type: KotlinWeeklyIssueEntryType!
}

enum KotlinWeeklyIssueEntryType {
"Announcements."
ANNOUNCEMENTS
"Articles."
ARTICLES
"Android."
ANDROID
"Videos."
VIDEOS
"Libraries."
LIBRARIES
}

"ISO 8601 instant."
scalar Instant
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.github.reactivecircus.kstreamlined.backend

import io.github.reactivecircus.kstreamlined.backend.client.FakeFeedClient
import io.github.reactivecircus.kstreamlined.backend.client.FakeKotlinWeeklyIssueClient
import io.github.reactivecircus.kstreamlined.backend.client.FeedClient
import io.github.reactivecircus.kstreamlined.backend.client.KotlinWeeklyIssueClient
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

Expand All @@ -12,4 +14,9 @@ class TestKSConfiguration {
fun feedClient(): FeedClient {
return FakeFeedClient
}

@Bean
fun kotlinWeeklyIssueClient(): KotlinWeeklyIssueClient {
return FakeKotlinWeeklyIssueClient
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.github.reactivecircus.kstreamlined.backend.client

import io.github.reactivecircus.kstreamlined.backend.schema.generated.types.KotlinWeeklyIssueEntry
import io.github.reactivecircus.kstreamlined.backend.schema.generated.types.KotlinWeeklyIssueEntryType

object FakeKotlinWeeklyIssueClient : KotlinWeeklyIssueClient {

var nextKotlinWeeklyIssueResponse: () -> List<KotlinWeeklyIssueEntry> = {
DummyKotlinWeeklyIssueEntries
}

override suspend fun loadKotlinWeeklyIssue(url: String): List<KotlinWeeklyIssueEntry> {
return nextKotlinWeeklyIssueResponse()
}
}

val DummyKotlinWeeklyIssueEntries = listOf(
KotlinWeeklyIssueEntry(
title = "Amper Update – December 2023",
summary = "Last month JetBrains introduced Amper, a tool to improve the project configuration user experience. Marton Braun gives us an update about its state in December 2023.",
url = "https://blog.jetbrains.com/amper/2023/12/amper-update-december-2023/",
source = "blog.jetbrains.com",
type = KotlinWeeklyIssueEntryType.ANNOUNCEMENTS,
),
KotlinWeeklyIssueEntry(
title = "How to Use the Cucumber Framework to Test Application Use Cases",
summary = "Matthias Schenk writes today about Cucumber, a framework that can be used in application development to verify the correct behavior of the application.",
url = "https://towardsdev.com/how-to-use-the-cucumber-framework-to-test-application-use-cases-48b4f21ee0d0",
source = "towardsdev.com",
type = KotlinWeeklyIssueEntryType.ARTICLES,
),
KotlinWeeklyIssueEntry(
title = "Using launcher and themed icons in Android Studio, the manual way",
summary = "Marlon Lòpez describes how we can use the launcher and the themed icons in Android Studio.",
url = "https://dev.to/marlonlom/using-launcher-and-themed-icons-in-android-studio-the-manual-way-1h2a",
source = "dev.to",
type = KotlinWeeklyIssueEntryType.ANDROID,
),
KotlinWeeklyIssueEntry(
title = "Setting Sail with Compose Multiplatform by Isuru Rajapakse",
summary = "Isuru Rajapakse talks at the DevFest Sri Lanka about Compose Multiplatform.",
url = "https://www.youtube.com/watch?v=sG60644C47I",
source = "www.youtube.com",
type = KotlinWeeklyIssueEntryType.VIDEOS,
),
KotlinWeeklyIssueEntry(
title = "Kim - Kotlin Image Metadata",
summary = "Kim is a Kotlin image metadata manipulation library for Kotlin Multiplatform.",
url = "https://github.com/Ashampoo/kim",
source = "github.com",
type = KotlinWeeklyIssueEntryType.LIBRARIES,
),
)
Loading

0 comments on commit f0c0cfc

Please sign in to comment.