Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/skills/resolve-coderabbit-review/learned-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
| 동적 라우트 파라미터 하드코딩 → await params로 동적 해석 | 1 | #61 | 2026-05-14 |
| `<a>` 태그 대신 Next.js `<Link>` 사용 | 1 | #61 | 2026-05-14 |
| 문서 코드 샘플에 필수 import 힌트 추가 (DX 개선) | 1 | #61 | 2026-05-14 |
| CI path filter에 실행 도구 파일 누락 방지 | 1 | #88 | 2026-08-10 |
| GitHub Actions checkout credential persistence 비활성화 | 1 | #88 | 2026-08-10 |
| 문서 dependency 예시의 미정의 버전 변수 제거 | 1 | #88 | 2026-08-10 |

## 거절 패턴 (반복적으로 거절한 코멘트 유형)

Expand Down
54 changes: 54 additions & 0 deletions .github/workflows/kmp-core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: KMP Core

on:
pull_request:
paths:
- "dari-core/**"
- "dari/build.gradle.kts"
- "dari-noop/build.gradle.kts"
- "gradle/**"
- "gradlew"
- "build.gradle.kts"
- ".github/workflows/kmp-core.yml"
push:
branches: [main]
paths:
- "dari-core/**"
- "dari/build.gradle.kts"
- "dari-noop/build.gradle.kts"
- "gradle/**"
- "gradlew"
- "build.gradle.kts"
- ".github/workflows/kmp-core.yml"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

permissions:
contents: read

jobs:
verify:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: gradle/actions/setup-gradle@v4
- name: Verify shared contracts and Android consumers
run: >-
./gradlew
-PskipPublicationSigning
:dari-core:jvmTest
:dari-core:testAndroidHostTest
:dari-core:compileKotlinIosX64
:dari-core:compileKotlinIosArm64
:dari-core:compileKotlinIosSimulatorArm64
:dari-core:compileTestKotlinIosX64
:dari-core:compileTestKotlinIosArm64
:dari-core:compileTestKotlinIosSimulatorArm64
:dari-core:ktlintCheck
:dari-core:publishToMavenLocal
:dari:assemble
:dari-noop:assemble
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,22 @@ class MyApp : Application() {

```
dari/
├── dari-core/ ← Shared types (DariConfig, MessageEntry, DariInterceptor, etc.)
├── dari-core/ ← KMP contracts for Android, JVM, and iOS
├── dari/ ← Debug library (interceptor, UI, notifications)
├── dari-noop/ ← Release no-op (same API surface, zero overhead)
└── sample/ ← Sample app with WebView bridge demo
```

`dari-core` can be added to `commonMain` in a Kotlin Multiplatform project:

```kotlin
commonMain.dependencies {
implementation("io.github.easyhooon:dari-core:<version>")
}
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

This shares message models and interceptor contracts across Android, JVM, and iOS targets. The inspector runtime, UI, persistence, notifications, and WebView integration in `dari` remain Android-only.

## Sample App

The `sample/` module contains a working WebView demo with realistic bridge scenarios: fetching app/device info, showing toast messages, haptic feedback, sharing via native share sheet, clipboard access, opening app settings, and requesting camera permission. Run the sample app, tap the buttons, and observe:
Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import org.jlleitschuh.gradle.ktlint.KtlintExtension
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.android.kotlin.multiplatform.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.kotlin.serialization) apply false
Expand Down
46 changes: 32 additions & 14 deletions dari-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import com.vanniktech.maven.publish.SonatypeHost
import org.gradle.plugins.signing.Sign
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.kotlin.multiplatform.library)
alias(libs.plugins.vanniktech.maven.publish)
}

android {
namespace = "com.easyhooon.dari.core"
compileSdk = 36
tasks.withType<KotlinCompile>().configureEach {
compilerOptions.jvmTarget.set(JvmTarget.JVM_11)
}

defaultConfig {
minSdk = 26
tasks.withType<Sign>().configureEach {
onlyIf("publication signing is enabled") {
!providers.gradleProperty("skipPublicationSigning").isPresent
}
}

kotlin {
android {
namespace = "com.easyhooon.dari.core"
compileSdk = 36
minSdk = 26

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
withJava()
withHostTestBuilder {}
}
}

dependencies {
testImplementation(libs.junit)
jvm()

iosX64()
iosArm64()
iosSimulatorArm64()

sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}

mavenPublishing {
Expand Down Expand Up @@ -58,6 +76,6 @@ mavenPublishing {
}
}

publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
publishToMavenCentral()
signAllPublications()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.easyhooon.dari

internal actual fun currentTimeMillis(): Long = System.currentTimeMillis()
3 changes: 3 additions & 0 deletions dari-core/src/commonMain/kotlin/com/easyhooon/dari/Clock.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.easyhooon.dari

internal expect fun currentTimeMillis(): Long
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ data class MessageEntry(
val requestDataTruncated: Boolean = false,
val responseDataTruncated: Boolean = false,
val status: MessageStatus = MessageStatus.IN_PROGRESS,
val requestTimestamp: Long = System.currentTimeMillis(),
val requestTimestamp: Long = currentTimeMillis(),
val responseTimestamp: Long? = null,
val requestPayloadMetadata: MessagePayloadMetadata? = null,
val responsePayloadMetadata: MessagePayloadMetadata? = null,
Expand Down Expand Up @@ -109,4 +109,4 @@ data class MessageEntry(
}
}

private fun String?.utf8Size(): Int = this?.toByteArray(Charsets.UTF_8)?.size ?: 0
private fun String?.utf8Size(): Int = this?.encodeToByteArray()?.size ?: 0
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package com.easyhooon.dari
enum class MessageStatus {
/** Request has been sent but no response received yet (e.g., waiting for permission dialog) */
IN_PROGRESS,

/** Response received successfully */
SUCCESS,

/** Response received with an error */
ERROR,
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.easyhooon.dari

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertThrows
import org.junit.Test
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
Expand All @@ -12,7 +12,7 @@ import kotlin.time.Duration.Companion.seconds
class DariConfigTest {

@Test
fun `default retention period is null (disabled)`() {
fun `default retention period is null when disabled`() {
assertNull(DariConfig().retentionPeriod)
}

Expand All @@ -30,22 +30,22 @@ class DariConfigTest {

@Test
fun `zero retention period throws`() {
assertThrows(IllegalArgumentException::class.java) {
assertFailsWith<IllegalArgumentException> {
DariConfig(retentionPeriod = Duration.ZERO)
}
}

@Test
fun `negative retention period throws`() {
assertThrows(IllegalArgumentException::class.java) {
assertFailsWith<IllegalArgumentException> {
DariConfig(retentionPeriod = (-1).seconds)
}
}

@Test
fun `other validation still applies`() {
// Sanity check that retentionPeriod was added without breaking maxContentLength validation.
assertThrows(IllegalArgumentException::class.java) {
assertFailsWith<IllegalArgumentException> {
DariConfig(maxContentLength = 0)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.easyhooon.dari

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class MessageEntryTest {
@Test
fun `default timestamp uses platform clock`() {
val before = currentTimeMillis()
val entry = MessageEntry(handlerName = "handler", direction = MessageDirection.WEB_TO_APP)
val after = currentTimeMillis()

assertTrue(entry.requestTimestamp in before..after)
}

@Test
fun `payload size is calculated in UTF-8 bytes`() {
val entry = MessageEntry(
handlerName = "handler",
direction = MessageDirection.APP_TO_WEB,
requestData = "Dari 다리",
responseData = "ok",
)

assertEquals(11, entry.requestSizeBytes)
assertEquals(2, entry.responseSizeBytes)
assertEquals(13, entry.totalSizeBytes)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.easyhooon.dari

import com.easyhooon.dari.interceptor.DariInterceptor
import kotlin.test.Test
import kotlin.test.assertEquals

class PublicApiSmokeTest {
@Test
fun `legacy positional message constructor remains available`() {
val entry = MessageEntry(
1L,
"request-id",
"handler",
MessageDirection.WEB_TO_APP,
"request",
"response",
MessageStatus.SUCCESS,
100L,
120L,
)

assertEquals(20L, entry.durationMs)
}

@Test
fun `interceptor contract remains implementable from common code`() {
val interceptor: DariInterceptor = RecordingInterceptor()

interceptor.onAppToWebRequest("handler", "request-id", "payload")

assertEquals("handler", (interceptor as RecordingInterceptor).handlerName)
}

private class RecordingInterceptor : DariInterceptor {
var handlerName: String? = null

override fun onWebToAppRequest(
handlerName: String,
requestId: String?,
requestData: String?,
fireAndForget: Boolean?,
) = Unit

override fun onWebToAppResponse(
handlerName: String,
requestId: String?,
responseData: String?,
isSuccess: Boolean,
) = Unit

override fun onAppToWebRequest(
handlerName: String,
requestId: String?,
data: String?,
fireAndForget: Boolean?,
) {
this.handlerName = handlerName
}

override fun onAppToWebResponse(requestId: String?, isSuccess: Boolean, responseData: String?) = Unit
}
}
6 changes: 6 additions & 0 deletions dari-core/src/iosMain/kotlin/com/easyhooon/dari/Clock.ios.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.easyhooon.dari

import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970

internal actual fun currentTimeMillis(): Long = (NSDate().timeIntervalSince1970 * 1_000.0).toLong()
3 changes: 3 additions & 0 deletions dari-core/src/jvmMain/kotlin/com/easyhooon/dari/Clock.jvm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.easyhooon.dari

internal actual fun currentTimeMillis(): Long = System.currentTimeMillis()
4 changes: 1 addition & 3 deletions dari-noop/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import com.vanniktech.maven.publish.SonatypeHost

plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.vanniktech.maven.publish)
Expand Down Expand Up @@ -63,6 +61,6 @@ mavenPublishing {
}
}

publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
publishToMavenCentral()
signAllPublications()
}
4 changes: 1 addition & 3 deletions dari/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import com.vanniktech.maven.publish.SonatypeHost

plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
Expand Down Expand Up @@ -111,6 +109,6 @@ mavenPublishing {
}
}

publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
publishToMavenCentral()
signAllPublications()
}
Loading
Loading