api: initial commit

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2021-06-03 00:32:17 +05:30
parent 29c374859b
commit fcfcbfbf92
No known key found for this signature in database
GPG Key ID: 366D7BBAD1031E80
17 changed files with 273 additions and 11 deletions

View File

@ -11,8 +11,8 @@ version = "1.0"
repositories { google() }
dependencies {
implementation(project(":common"))
implementation("androidx.activity:activity-compose:1.3.0-alpha08")
implementation(projects.common)
implementation("androidx.activity:activity-compose:1.3.0-beta01")
}
android {

1
api/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

23
api/build.gradle.kts Normal file
View File

@ -0,0 +1,23 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
id("com.google.devtools.ksp") version "1.5.10-1.0.0-beta01"
}
dependencies {
api(libs.thirdparty.retrofit.lib)
ksp(libs.thirdparty.moshix.ksp)
implementation(libs.thirdparty.moshi.lib)
implementation(libs.thirdparty.retrofit.moshiConverter) { exclude(group = "com.squareup.moshi") }
testImplementation(libs.kotlin.coroutines.core)
testImplementation(libs.testing.kotlintest.junit)
testImplementation(libs.testing.mockWebServer)
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
languageVersion = "1.5"
}
}

View File

@ -0,0 +1,22 @@
package dev.msfjarvis.lobsters.data.api
import dev.msfjarvis.lobsters.model.LobstersPost
import dev.msfjarvis.lobsters.model.LobstersPostDetails
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
/** Simple interface defining an API for lobste.rs */
interface LobstersApi {
@GET("hottest.json") suspend fun getHottestPosts(@Query("page") page: Int): List<LobstersPost>
@GET("newest.json") suspend fun getNewestPosts(@Query("page") page: Int): List<LobstersPost>
@GET("s/{postId}.json")
suspend fun getPostDetails(@Path("postId") postId: String): LobstersPostDetails
companion object {
const val BASE_URL = "https://lobste.rs"
}
}

View File

@ -0,0 +1,20 @@
package dev.msfjarvis.lobsters.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class Comment(
@Json(name = "short_id") val shortId: String,
@Json(name = "short_id_url") val shortIdUrl: String,
@Json(name = "created_at") val createdAt: String,
@Json(name = "updated_at") val updatedAt: String,
@Json(name = "is_deleted") val isDeleted: Boolean,
@Json(name = "is_moderated") val isModerated: Boolean,
val score: Long,
val flags: Long,
val comment: String,
val url: String,
@Json(name = "indent_level") val indentLevel: Long,
@Json(name = "commenting_user") val user: User,
)

View File

@ -0,0 +1,10 @@
package dev.msfjarvis.lobsters.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class KeybaseSignature(
@Json(name = "kb_username") val kbUsername: String,
@Json(name = "sig_hash") val sigHash: String,
)

View File

@ -0,0 +1,20 @@
package dev.msfjarvis.lobsters.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class LobstersPost(
@Json(name = "short_id") val shortId: String,
@Json(name = "short_id_url") val shortIdUrl: String,
@Json(name = "created_at") val createdAt: String,
val title: String,
val url: String,
val score: Long,
val flags: Long,
@Json(name = "comment_count") val commentCount: Long,
val description: String,
@Json(name = "comments_url") val commentsUrl: String,
@Json(name = "submitter_user") val submitter: User,
val tags: List<String>,
)

View File

@ -0,0 +1,21 @@
package dev.msfjarvis.lobsters.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class LobstersPostDetails(
@Json(name = "short_id") val shortId: String,
@Json(name = "short_id_url") val shortIdUrl: String,
@Json(name = "created_at") val createdAt: String,
val title: String,
val url: String,
val score: Long,
val flags: Long,
@Json(name = "comment_count") val commentCount: Long,
val description: String,
@Json(name = "comments_url") val commentsUrl: String,
@Json(name = "submitter_user") val submitter: User,
val tags: List<String>,
val comments: List<Comment>,
)

View File

@ -0,0 +1,19 @@
package dev.msfjarvis.lobsters.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class User(
val username: String,
@Json(name = "created_at") val createdAt: String,
@Json(name = "is_admin") val isAdmin: Boolean,
val about: String,
@Json(name = "is_moderator") val isModerator: Boolean,
val karma: Long = 0,
@Json(name = "avatar_url") val avatarUrl: String,
@Json(name = "invited_by_user") val invitedByUser: String,
@Json(name = "github_username") val githubUsername: String? = null,
@Json(name = "twitter_username") val twitterUsername: String? = null,
@Json(name = "keybase_signatures") val keybaseSignatures: List<KeybaseSignature> = emptyList(),
)

View File

@ -0,0 +1,89 @@
package dev.msfjarvis.lobsters.data.api
import com.squareup.moshi.Moshi
import dev.msfjarvis.lobsters.util.TestUtils
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail
import kotlinx.coroutines.runBlocking
import mockwebserver3.Dispatcher
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import mockwebserver3.RecordedRequest
import okhttp3.OkHttpClient
import org.junit.AfterClass
import org.junit.BeforeClass
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create
class LobstersApiTest {
companion object {
private val webServer = MockWebServer()
private val moshi = Moshi.Builder().build()
private val okHttp = OkHttpClient.Builder().build()
private val retrofit =
Retrofit.Builder()
.client(okHttp)
.baseUrl("http://localhost:8080/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
private val apiClient = retrofit.create<LobstersApi>()
@JvmStatic
@BeforeClass
fun setUp() {
webServer.start(8080)
webServer.dispatcher =
object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
val path = requireNotNull(request.path)
return when {
path.startsWith("/hottest") ->
MockResponse().setBody(TestUtils.getJson("hottest.json")).setResponseCode(200)
path.startsWith("/s/") ->
MockResponse()
.setBody(TestUtils.getJson("post_details_d9ucpe.json"))
.setResponseCode(200)
else -> fail("'$path' unexpected")
}
}
}
}
@JvmStatic
@AfterClass
fun tearDown() {
webServer.shutdown()
}
}
@Test
fun `api gets correct number of items`() = runBlocking {
val posts = apiClient.getHottestPosts(1)
assertEquals(25, posts.size)
}
@Test
fun `no moderator posts in test data`() = runBlocking {
val posts = apiClient.getHottestPosts(1)
val moderatorPosts = posts.asSequence().filter { it.submitter.isModerator }.toSet()
assertTrue(moderatorPosts.isEmpty())
}
@Test
fun `posts with no urls`() = runBlocking {
val posts = apiClient.getHottestPosts(1)
val commentsOnlyPosts = posts.asSequence().filter { it.url.isEmpty() }.toSet()
assertEquals(2, commentsOnlyPosts.size)
}
@Test
fun `post details with comments`() = runBlocking {
val postDetails = apiClient.getPostDetails("d9ucpe")
assertEquals(7, postDetails.commentCount)
assertEquals(7, postDetails.comments.size)
}
}

View File

@ -0,0 +1,12 @@
package dev.msfjarvis.lobsters.util
import java.io.File
object TestUtils {
fun getJson(path: String): String {
// Load the JSON response
val uri = javaClass.classLoader.getResource(path)
val file = File(uri.path)
return String(file.readBytes())
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,8 @@
buildscript {
repositories {
gradlePluginPortal()
jcenter()
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10")
@ -18,21 +17,19 @@ version = "1.0"
allprojects {
repositories {
jcenter()
mavenCentral()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
google()
}
apply(plugin = "com.diffplug.spotless")
configure<com.diffplug.gradle.spotless.SpotlessExtension> {
kotlin {
target("**/*.kt")
targetExclude("**/build/**")
ktlint().userData(mapOf("indent_size" to "2", "continuation_indent_size" to "2"))
ktfmt().googleStyle()
}
kotlinGradle {
target("*.gradle.kts")
ktlint().userData(mapOf("indent_size" to "2", "continuation_indent_size" to "2"))
ktfmt().googleStyle()
}
format("xml") {

View File

@ -15,7 +15,7 @@ kotlin {
sourceSets {
val jvmMain by getting {
dependencies {
implementation(project(":common"))
implementation(projects.common)
implementation(compose.desktop.currentOs)
}
}

21
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,21 @@
[versions]
coroutines = "1.5.0"
kotlin = "1.5.10"
moshix = "0.11.2"
retrofit = "2.9.0"
[libraries]
kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlin-coroutines-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutines" }
thirdparty-moshi-lib = "com.squareup.moshi:moshi:1.12.0"
thirdparty-moshix-ksp = { module = "dev.zacsweers.moshix:moshi-ksp", version.ref = "moshix" }
thirdparty-moshix-metadatareflect = { module = "dev.zacsweers.moshix:moshi-metadata-reflect", version.ref = "moshix" }
thirdparty-retrofit-lib = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
thirdparty-retrofit-moshiConverter = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
testing-kotlintest-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
testing-mockWebServer = "com.squareup.okhttp3:mockwebserver3-junit4:5.0.0-alpha.2"

View File

@ -1,17 +1,22 @@
pluginManagement {
repositories {
google()
jcenter()
gradlePluginPortal()
mavenCentral()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
gradlePluginPortal()
}
}
rootProject.name = "Claw"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
enableFeaturePreview("VERSION_CATALOGS")
include(":android")
include(":desktop")
include(":api")
include(":common")
include(":desktop")