agentic-team-templates 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/package.json +1 -1
- package/src/index.js +22 -2
- package/src/index.test.js +5 -0
- package/templates/cpp-expert/.cursorrules/concurrency.md +211 -0
- package/templates/cpp-expert/.cursorrules/error-handling.md +170 -0
- package/templates/cpp-expert/.cursorrules/memory-and-ownership.md +220 -0
- package/templates/cpp-expert/.cursorrules/modern-cpp.md +211 -0
- package/templates/cpp-expert/.cursorrules/overview.md +87 -0
- package/templates/cpp-expert/.cursorrules/performance.md +223 -0
- package/templates/cpp-expert/.cursorrules/testing.md +230 -0
- package/templates/cpp-expert/.cursorrules/tooling.md +312 -0
- package/templates/cpp-expert/CLAUDE.md +242 -0
- package/templates/csharp-expert/.cursorrules/aspnet-core.md +311 -0
- package/templates/csharp-expert/.cursorrules/async-patterns.md +206 -0
- package/templates/csharp-expert/.cursorrules/dependency-injection.md +206 -0
- package/templates/csharp-expert/.cursorrules/error-handling.md +235 -0
- package/templates/csharp-expert/.cursorrules/language-features.md +204 -0
- package/templates/csharp-expert/.cursorrules/overview.md +92 -0
- package/templates/csharp-expert/.cursorrules/performance.md +251 -0
- package/templates/csharp-expert/.cursorrules/testing.md +282 -0
- package/templates/csharp-expert/.cursorrules/tooling.md +254 -0
- package/templates/csharp-expert/CLAUDE.md +360 -0
- package/templates/java-expert/.cursorrules/concurrency.md +209 -0
- package/templates/java-expert/.cursorrules/error-handling.md +205 -0
- package/templates/java-expert/.cursorrules/modern-java.md +216 -0
- package/templates/java-expert/.cursorrules/overview.md +81 -0
- package/templates/java-expert/.cursorrules/performance.md +239 -0
- package/templates/java-expert/.cursorrules/persistence.md +262 -0
- package/templates/java-expert/.cursorrules/spring-boot.md +262 -0
- package/templates/java-expert/.cursorrules/testing.md +272 -0
- package/templates/java-expert/.cursorrules/tooling.md +301 -0
- package/templates/java-expert/CLAUDE.md +325 -0
- package/templates/javascript-expert/.cursorrules/overview.md +5 -3
- package/templates/javascript-expert/.cursorrules/typescript-deep-dive.md +348 -0
- package/templates/javascript-expert/CLAUDE.md +34 -3
- package/templates/kotlin-expert/.cursorrules/coroutines.md +237 -0
- package/templates/kotlin-expert/.cursorrules/error-handling.md +149 -0
- package/templates/kotlin-expert/.cursorrules/frameworks.md +227 -0
- package/templates/kotlin-expert/.cursorrules/language-features.md +231 -0
- package/templates/kotlin-expert/.cursorrules/overview.md +77 -0
- package/templates/kotlin-expert/.cursorrules/performance.md +185 -0
- package/templates/kotlin-expert/.cursorrules/testing.md +213 -0
- package/templates/kotlin-expert/.cursorrules/tooling.md +258 -0
- package/templates/kotlin-expert/CLAUDE.md +276 -0
- package/templates/swift-expert/.cursorrules/concurrency.md +230 -0
- package/templates/swift-expert/.cursorrules/error-handling.md +213 -0
- package/templates/swift-expert/.cursorrules/language-features.md +246 -0
- package/templates/swift-expert/.cursorrules/overview.md +88 -0
- package/templates/swift-expert/.cursorrules/performance.md +260 -0
- package/templates/swift-expert/.cursorrules/swiftui.md +260 -0
- package/templates/swift-expert/.cursorrules/testing.md +286 -0
- package/templates/swift-expert/.cursorrules/tooling.md +285 -0
- package/templates/swift-expert/CLAUDE.md +275 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Kotlin Performance
|
|
2
|
+
|
|
3
|
+
Profile first. Understand the JVM. Kotlin-specific optimizations matter.
|
|
4
|
+
|
|
5
|
+
## Profile Before Optimizing
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# JFR (Java Flight Recorder) — works with Kotlin on JVM
|
|
9
|
+
java -XX:StartFlightRecording=filename=recording.jfr,duration=60s -jar app.jar
|
|
10
|
+
|
|
11
|
+
# Kotlin-specific: check for unnecessary boxing, allocations
|
|
12
|
+
# Use JMH for micro-benchmarks
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Inline Functions
|
|
16
|
+
|
|
17
|
+
```kotlin
|
|
18
|
+
// inline eliminates lambda allocation overhead — use for hot paths
|
|
19
|
+
inline fun <T> measure(label: String, block: () -> T): T {
|
|
20
|
+
val start = System.nanoTime()
|
|
21
|
+
val result = block()
|
|
22
|
+
val elapsed = System.nanoTime() - start
|
|
23
|
+
logger.debug { "$label took ${elapsed / 1_000_000}ms" }
|
|
24
|
+
return result
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// crossinline — prevent non-local returns in inlined lambdas
|
|
28
|
+
inline fun transaction(crossinline block: () -> Unit) {
|
|
29
|
+
beginTransaction()
|
|
30
|
+
try {
|
|
31
|
+
block()
|
|
32
|
+
commit()
|
|
33
|
+
} catch (e: Exception) {
|
|
34
|
+
rollback()
|
|
35
|
+
throw e
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// reified — preserve generic type info at runtime (only with inline)
|
|
40
|
+
inline fun <reified T> parseJson(json: String): T {
|
|
41
|
+
return objectMapper.readValue(json, T::class.java)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Rules:
|
|
45
|
+
// - Inline small, frequently-called higher-order functions
|
|
46
|
+
// - Don't inline large function bodies (code bloat)
|
|
47
|
+
// - Standard library uses inline extensively: let, apply, also, run, with, map, filter
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Value Classes
|
|
51
|
+
|
|
52
|
+
```kotlin
|
|
53
|
+
// Zero overhead wrappers — no runtime allocation for the wrapper
|
|
54
|
+
@JvmInline
|
|
55
|
+
value class UserId(val value: String)
|
|
56
|
+
|
|
57
|
+
@JvmInline
|
|
58
|
+
value class Email(val value: String) {
|
|
59
|
+
init {
|
|
60
|
+
require(value.contains("@")) { "Invalid email" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// At runtime, UserId("abc") is just the String "abc"
|
|
65
|
+
// But at compile time, you can't pass UserId where Email is expected
|
|
66
|
+
fun findUser(id: UserId): User // Type-safe, zero overhead
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Sequences vs Collections
|
|
70
|
+
|
|
71
|
+
```kotlin
|
|
72
|
+
// Collections: eager, creates intermediate lists
|
|
73
|
+
val result = users
|
|
74
|
+
.filter { it.isActive } // Creates List<User>
|
|
75
|
+
.map { it.name } // Creates List<String>
|
|
76
|
+
.take(10) // Creates List<String>
|
|
77
|
+
|
|
78
|
+
// Sequences: lazy, no intermediate allocations
|
|
79
|
+
val result = users.asSequence()
|
|
80
|
+
.filter { it.isActive } // No intermediate list
|
|
81
|
+
.map { it.name } // No intermediate list
|
|
82
|
+
.take(10) // Stops after 10 matches
|
|
83
|
+
.toList() // Single terminal allocation
|
|
84
|
+
|
|
85
|
+
// Use sequences when:
|
|
86
|
+
// - Processing large collections (>1000 elements)
|
|
87
|
+
// - Chaining multiple operations
|
|
88
|
+
// - Using take/first (short-circuit)
|
|
89
|
+
|
|
90
|
+
// Use collections when:
|
|
91
|
+
// - Small datasets
|
|
92
|
+
// - Single operation
|
|
93
|
+
// - Need indexed access during processing
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Coroutine Performance
|
|
97
|
+
|
|
98
|
+
```kotlin
|
|
99
|
+
// Channel buffers for throughput
|
|
100
|
+
val channel = Channel<Event>(capacity = 64) // Buffered, not unlimited
|
|
101
|
+
|
|
102
|
+
// Fan-out for parallel processing
|
|
103
|
+
val workers = List(Runtime.getRuntime().availableProcessors()) {
|
|
104
|
+
launch(Dispatchers.Default) {
|
|
105
|
+
for (item in channel) process(item)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Avoid unnecessary suspension
|
|
110
|
+
// Bad: wrapping non-suspending code in withContext
|
|
111
|
+
suspend fun format(name: String): String = withContext(Dispatchers.Default) {
|
|
112
|
+
name.trim().lowercase() // This is instant — no need for withContext
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Good: only use withContext for actual blocking/CPU work
|
|
116
|
+
suspend fun hashPassword(password: String): String = withContext(Dispatchers.Default) {
|
|
117
|
+
BCrypt.hashpw(password, BCrypt.gensalt()) // Actually CPU-intensive
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Collection Optimization
|
|
122
|
+
|
|
123
|
+
```kotlin
|
|
124
|
+
// Pre-size collections
|
|
125
|
+
val map = HashMap<String, User>(expectedSize)
|
|
126
|
+
val list = ArrayList<User>(expectedSize)
|
|
127
|
+
|
|
128
|
+
// buildList/buildMap — single allocation
|
|
129
|
+
val items = buildList(expectedSize) {
|
|
130
|
+
for (item in source) {
|
|
131
|
+
if (item.isValid) add(transform(item))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Use appropriate collection types
|
|
136
|
+
val lookup = users.associateBy { it.id } // HashMap for O(1) lookup
|
|
137
|
+
val uniqueEmails = users.mapTo(HashSet()) { it.email } // HashSet for uniqueness
|
|
138
|
+
val sorted = users.sortedBy { it.name } // Single sort, not repeated
|
|
139
|
+
|
|
140
|
+
// Avoid: repeated list.contains() — use a Set
|
|
141
|
+
val activeIds = activeUsers.map { it.id }.toSet() // O(1) lookup
|
|
142
|
+
orders.filter { it.userId in activeIds } // Fast membership test
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## String Performance
|
|
146
|
+
|
|
147
|
+
```kotlin
|
|
148
|
+
// StringBuilder for complex concatenation
|
|
149
|
+
val result = buildString {
|
|
150
|
+
for (item in items) {
|
|
151
|
+
append(item.name)
|
|
152
|
+
append(": ")
|
|
153
|
+
appendLine(item.value)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// joinToString for simple cases
|
|
158
|
+
val csv = items.joinToString(",") { it.name }
|
|
159
|
+
|
|
160
|
+
// String templates are efficient — Kotlin compiles them to StringBuilder
|
|
161
|
+
val message = "User $name logged in from $ip" // Fine for most cases
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Anti-Patterns
|
|
165
|
+
|
|
166
|
+
```kotlin
|
|
167
|
+
// Never: premature optimization without profiling
|
|
168
|
+
// "I think this allocation is slow" — prove it with JFR or JMH
|
|
169
|
+
|
|
170
|
+
// Never: creating coroutines for non-suspending work
|
|
171
|
+
launch { val x = 1 + 1 } // Overhead of coroutine machinery for nothing
|
|
172
|
+
|
|
173
|
+
// Never: using reflection in hot paths
|
|
174
|
+
val prop = User::class.memberProperties.find { it.name == "email" }
|
|
175
|
+
// Use direct property access
|
|
176
|
+
|
|
177
|
+
// Never: unnecessary boxing
|
|
178
|
+
val numbers: List<Int> = listOf(1, 2, 3) // Boxed integers
|
|
179
|
+
// For performance-critical code: IntArray(3) { it + 1 }
|
|
180
|
+
|
|
181
|
+
// Never: mutable shared state in coroutines without synchronization
|
|
182
|
+
var counter = 0
|
|
183
|
+
repeat(1000) { launch { counter++ } } // Data race
|
|
184
|
+
// Use: AtomicInteger, Mutex, or single-writer pattern
|
|
185
|
+
```
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Kotlin Testing
|
|
2
|
+
|
|
3
|
+
Test behavior with expressive assertions. Coroutines require special test infrastructure.
|
|
4
|
+
|
|
5
|
+
## Framework Stack
|
|
6
|
+
|
|
7
|
+
| Tool | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| JUnit 5 | Test framework |
|
|
10
|
+
| Kotest | Kotlin-native testing (property-based, data-driven) |
|
|
11
|
+
| MockK | Kotlin-native mocking |
|
|
12
|
+
| kotlinx-coroutines-test | Coroutine testing |
|
|
13
|
+
| Testcontainers | Real databases/services |
|
|
14
|
+
| Ktor Test | HTTP endpoint testing |
|
|
15
|
+
| ArchUnit | Architecture tests |
|
|
16
|
+
|
|
17
|
+
## Unit Test Structure
|
|
18
|
+
|
|
19
|
+
```kotlin
|
|
20
|
+
class OrderServiceTest {
|
|
21
|
+
|
|
22
|
+
private val orderRepo = mockk<OrderRepository>()
|
|
23
|
+
private val inventoryClient = mockk<InventoryClient>()
|
|
24
|
+
private val sut = OrderService(orderRepo, inventoryClient)
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
fun `create with valid items returns success`() = runTest {
|
|
28
|
+
// Arrange
|
|
29
|
+
val request = CreateOrderRequest(
|
|
30
|
+
customerId = "customer-1",
|
|
31
|
+
items = listOf(OrderItemRequest("SKU-001", 2))
|
|
32
|
+
)
|
|
33
|
+
coEvery { inventoryClient.checkAvailability("SKU-001", 2) } returns true
|
|
34
|
+
coEvery { orderRepo.save(any()) } answers { firstArg() }
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
val result = sut.create(request)
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
assertThat(result).isInstanceOf(Result.Success::class.java)
|
|
41
|
+
val order = (result as Result.Success).value
|
|
42
|
+
assertThat(order.customerId).isEqualTo("customer-1")
|
|
43
|
+
assertThat(order.items).hasSize(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun `create with insufficient inventory returns failure`() = runTest {
|
|
48
|
+
val request = CreateOrderRequest(
|
|
49
|
+
customerId = "customer-1",
|
|
50
|
+
items = listOf(OrderItemRequest("SKU-001", 100))
|
|
51
|
+
)
|
|
52
|
+
coEvery { inventoryClient.checkAvailability("SKU-001", 100) } returns false
|
|
53
|
+
|
|
54
|
+
val result = sut.create(request)
|
|
55
|
+
|
|
56
|
+
assertThat(result).isInstanceOf(Result.Failure::class.java)
|
|
57
|
+
val error = (result as Result.Failure).error
|
|
58
|
+
assertThat(error).isInstanceOf(AppError.Validation::class.java)
|
|
59
|
+
|
|
60
|
+
coVerify(exactly = 0) { orderRepo.save(any()) }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Coroutine Testing
|
|
66
|
+
|
|
67
|
+
```kotlin
|
|
68
|
+
// runTest provides a TestScope with virtual time
|
|
69
|
+
@Test
|
|
70
|
+
fun `polling retries on failure`() = runTest {
|
|
71
|
+
var attempts = 0
|
|
72
|
+
coEvery { service.fetch() } answers {
|
|
73
|
+
attempts++
|
|
74
|
+
if (attempts < 3) throw IOException("Temporary failure")
|
|
75
|
+
else Data("success")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
val result = sut.fetchWithRetry(maxRetries = 3)
|
|
79
|
+
|
|
80
|
+
assertThat(result.value).isEqualTo("success")
|
|
81
|
+
assertThat(attempts).isEqualTo(3)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Testing flows
|
|
85
|
+
@Test
|
|
86
|
+
fun `user updates emit state changes`() = runTest {
|
|
87
|
+
val viewModel = UserViewModel(mockUserService)
|
|
88
|
+
|
|
89
|
+
viewModel.state.test {
|
|
90
|
+
assertThat(awaitItem()).isEqualTo(UiState.Loading)
|
|
91
|
+
|
|
92
|
+
viewModel.loadUser(UserId("123"))
|
|
93
|
+
|
|
94
|
+
assertThat(awaitItem()).isEqualTo(UiState.Loading)
|
|
95
|
+
assertThat(awaitItem()).isInstanceOf(UiState.Success::class.java)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// advanceTimeBy for testing delays
|
|
100
|
+
@Test
|
|
101
|
+
fun `cache expires after TTL`() = runTest {
|
|
102
|
+
cache.set("key", "value")
|
|
103
|
+
|
|
104
|
+
assertThat(cache.get("key")).isEqualTo("value")
|
|
105
|
+
|
|
106
|
+
advanceTimeBy(cacheTtl + 1.seconds)
|
|
107
|
+
|
|
108
|
+
assertThat(cache.get("key")).isNull()
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## MockK
|
|
113
|
+
|
|
114
|
+
```kotlin
|
|
115
|
+
// Suspend function mocking
|
|
116
|
+
coEvery { userRepo.findById(any()) } returns User(id = "1", name = "Alice")
|
|
117
|
+
coEvery { emailService.send(any(), any(), any()) } just Runs
|
|
118
|
+
|
|
119
|
+
// Verification
|
|
120
|
+
coVerify(exactly = 1) { emailService.send("alice@test.com", any(), any()) }
|
|
121
|
+
coVerify(ordering = Ordering.ORDERED) {
|
|
122
|
+
userRepo.save(any())
|
|
123
|
+
emailService.send(any(), any(), any())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Capturing
|
|
127
|
+
val slot = slot<User>()
|
|
128
|
+
coEvery { userRepo.save(capture(slot)) } answers { firstArg() }
|
|
129
|
+
|
|
130
|
+
sut.create(request)
|
|
131
|
+
|
|
132
|
+
assertThat(slot.captured.name).isEqualTo("Alice")
|
|
133
|
+
|
|
134
|
+
// Relaxed mocks — return defaults for all unmocked calls
|
|
135
|
+
val logger = mockk<Logger>(relaxed = true)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Ktor Testing
|
|
139
|
+
|
|
140
|
+
```kotlin
|
|
141
|
+
class UserRoutesTest {
|
|
142
|
+
|
|
143
|
+
@Test
|
|
144
|
+
fun `POST users returns created for valid request`() = testApplication {
|
|
145
|
+
application {
|
|
146
|
+
configureRouting()
|
|
147
|
+
configureSerialization()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
val response = client.post("/api/v1/users") {
|
|
151
|
+
contentType(ContentType.Application.Json)
|
|
152
|
+
setBody("""{"name": "Alice", "email": "alice@test.com"}""")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
assertEquals(HttpStatusCode.Created, response.status)
|
|
156
|
+
val user = response.body<UserResponse>()
|
|
157
|
+
assertEquals("Alice", user.name)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Test
|
|
161
|
+
fun `GET users id returns not found for missing user`() = testApplication {
|
|
162
|
+
application { configureRouting() }
|
|
163
|
+
|
|
164
|
+
val response = client.get("/api/v1/users/nonexistent")
|
|
165
|
+
|
|
166
|
+
assertEquals(HttpStatusCode.NotFound, response.status)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Data-Driven Tests (Kotest)
|
|
172
|
+
|
|
173
|
+
```kotlin
|
|
174
|
+
class SlugifyTest : FunSpec({
|
|
175
|
+
withData(
|
|
176
|
+
"Hello World" to "hello-world",
|
|
177
|
+
" spaces " to "spaces",
|
|
178
|
+
"Special!@#Chars" to "specialchars",
|
|
179
|
+
"" to "",
|
|
180
|
+
) { (input, expected) ->
|
|
181
|
+
slugify(input) shouldBe expected
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Property-based testing
|
|
186
|
+
class MoneyTest : FunSpec({
|
|
187
|
+
test("addition is commutative") {
|
|
188
|
+
checkAll(Arb.positiveLong(), Arb.positiveLong()) { a, b ->
|
|
189
|
+
Money(a, USD) + Money(b, USD) shouldBe Money(b, USD) + Money(a, USD)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Anti-Patterns
|
|
196
|
+
|
|
197
|
+
```kotlin
|
|
198
|
+
// Never: Thread.sleep in tests
|
|
199
|
+
Thread.sleep(5000) // Flaky and slow
|
|
200
|
+
// Use: runTest with advanceTimeBy, or Awaitility
|
|
201
|
+
|
|
202
|
+
// Never: testing internal implementation
|
|
203
|
+
verify { repo.save(any()) } // How, not what
|
|
204
|
+
// Test the observable outcome instead
|
|
205
|
+
|
|
206
|
+
// Never: shared mutable state between tests
|
|
207
|
+
companion object { val testUsers = mutableListOf<User>() }
|
|
208
|
+
// Use @BeforeEach to reset state
|
|
209
|
+
|
|
210
|
+
// Never: ignoring coroutine test infrastructure
|
|
211
|
+
@Test fun `test`() { runBlocking { sut.doWork() } }
|
|
212
|
+
// Use: runTest { } for proper virtual time and dispatcher control
|
|
213
|
+
```
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Kotlin Tooling and Build System
|
|
2
|
+
|
|
3
|
+
Gradle with Kotlin DSL. Static analysis. CI/CD. The full production pipeline.
|
|
4
|
+
|
|
5
|
+
## Gradle (Kotlin DSL)
|
|
6
|
+
|
|
7
|
+
```kotlin
|
|
8
|
+
// build.gradle.kts
|
|
9
|
+
plugins {
|
|
10
|
+
kotlin("jvm") version "2.1.0"
|
|
11
|
+
kotlin("plugin.serialization") version "2.1.0"
|
|
12
|
+
id("io.ktor.plugin") version "3.0.0" // For Ktor projects
|
|
13
|
+
id("io.gitlab.arturbosch.detekt") version "1.23.7"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
kotlin {
|
|
17
|
+
jvmToolchain(21)
|
|
18
|
+
compilerOptions {
|
|
19
|
+
allWarningsAsErrors = true
|
|
20
|
+
freeCompilerArgs.addAll(
|
|
21
|
+
"-Xjsr305=strict", // Strict null-safety for Java interop
|
|
22
|
+
"-Xcontext-receivers", // Enable context receivers (experimental)
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
dependencies {
|
|
28
|
+
// Ktor
|
|
29
|
+
implementation("io.ktor:ktor-server-core")
|
|
30
|
+
implementation("io.ktor:ktor-server-netty")
|
|
31
|
+
implementation("io.ktor:ktor-server-content-negotiation")
|
|
32
|
+
implementation("io.ktor:ktor-serialization-kotlinx-json")
|
|
33
|
+
|
|
34
|
+
// Coroutines
|
|
35
|
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
|
|
36
|
+
|
|
37
|
+
// DI
|
|
38
|
+
implementation("io.insert-koin:koin-ktor")
|
|
39
|
+
|
|
40
|
+
// Database
|
|
41
|
+
implementation("org.jetbrains.exposed:exposed-core")
|
|
42
|
+
implementation("org.jetbrains.exposed:exposed-jdbc")
|
|
43
|
+
|
|
44
|
+
// Testing
|
|
45
|
+
testImplementation(kotlin("test"))
|
|
46
|
+
testImplementation("io.ktor:ktor-server-test-host")
|
|
47
|
+
testImplementation("io.mockk:mockk")
|
|
48
|
+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test")
|
|
49
|
+
testImplementation("org.testcontainers:postgresql")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
tasks.test {
|
|
53
|
+
useJUnitPlatform()
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Essential Commands
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Build and test
|
|
61
|
+
./gradlew build # Full build + tests
|
|
62
|
+
./gradlew test # Tests only
|
|
63
|
+
./gradlew check # Tests + static analysis
|
|
64
|
+
|
|
65
|
+
# Run
|
|
66
|
+
./gradlew run # Run application
|
|
67
|
+
./gradlew buildFatJar # Ktor fat JAR
|
|
68
|
+
|
|
69
|
+
# Dependencies
|
|
70
|
+
./gradlew dependencies # Dependency tree
|
|
71
|
+
./gradlew dependencyUpdates # Check for updates
|
|
72
|
+
|
|
73
|
+
# Static analysis
|
|
74
|
+
./gradlew detekt # Detekt analysis
|
|
75
|
+
./gradlew ktlintCheck # Code formatting check
|
|
76
|
+
./gradlew ktlintFormat # Auto-format
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Detekt (Static Analysis)
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
# detekt.yml
|
|
83
|
+
complexity:
|
|
84
|
+
LongMethod:
|
|
85
|
+
threshold: 30
|
|
86
|
+
ComplexCondition:
|
|
87
|
+
threshold: 4
|
|
88
|
+
TooManyFunctions:
|
|
89
|
+
thresholdInFiles: 20
|
|
90
|
+
|
|
91
|
+
style:
|
|
92
|
+
ForbiddenComment:
|
|
93
|
+
values:
|
|
94
|
+
- "TODO:"
|
|
95
|
+
- "FIXME:"
|
|
96
|
+
- "HACK:"
|
|
97
|
+
allowedPatterns: "TODO\\(#\\d+\\)" # Allow TODO with issue number
|
|
98
|
+
MagicNumber:
|
|
99
|
+
active: true
|
|
100
|
+
ignoreNumbers:
|
|
101
|
+
- "-1"
|
|
102
|
+
- "0"
|
|
103
|
+
- "1"
|
|
104
|
+
- "2"
|
|
105
|
+
MaxLineLength:
|
|
106
|
+
maxLineLength: 120
|
|
107
|
+
|
|
108
|
+
exceptions:
|
|
109
|
+
TooGenericExceptionCaught:
|
|
110
|
+
active: true
|
|
111
|
+
exceptionNames:
|
|
112
|
+
- "Exception"
|
|
113
|
+
- "RuntimeException"
|
|
114
|
+
- "Throwable"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## kotlinx.serialization
|
|
118
|
+
|
|
119
|
+
```kotlin
|
|
120
|
+
// Type-safe serialization — no reflection
|
|
121
|
+
@Serializable
|
|
122
|
+
data class CreateUserRequest(
|
|
123
|
+
val name: String,
|
|
124
|
+
val email: String,
|
|
125
|
+
val role: UserRole = UserRole.USER
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@Serializable
|
|
129
|
+
enum class UserRole { USER, ADMIN, VIEWER }
|
|
130
|
+
|
|
131
|
+
// Custom serializer
|
|
132
|
+
@Serializable(with = InstantSerializer::class)
|
|
133
|
+
data class Event(val name: String, val occurredAt: Instant)
|
|
134
|
+
|
|
135
|
+
object InstantSerializer : KSerializer<Instant> {
|
|
136
|
+
override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
|
|
137
|
+
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
|
|
138
|
+
override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString())
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Docker
|
|
143
|
+
|
|
144
|
+
```dockerfile
|
|
145
|
+
FROM gradle:8-jdk21 AS build
|
|
146
|
+
WORKDIR /app
|
|
147
|
+
COPY build.gradle.kts settings.gradle.kts ./
|
|
148
|
+
COPY gradle/ gradle/
|
|
149
|
+
RUN gradle dependencies --no-daemon
|
|
150
|
+
|
|
151
|
+
COPY src/ src/
|
|
152
|
+
RUN gradle buildFatJar --no-daemon
|
|
153
|
+
|
|
154
|
+
FROM eclipse-temurin:21-jre-alpine
|
|
155
|
+
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
|
156
|
+
USER appuser
|
|
157
|
+
WORKDIR /app
|
|
158
|
+
COPY --from=build /app/build/libs/*-all.jar app.jar
|
|
159
|
+
EXPOSE 8080
|
|
160
|
+
|
|
161
|
+
ENV JAVA_OPTS="-XX:+UseContainerSupport \
|
|
162
|
+
-XX:MaxRAMPercentage=75.0 \
|
|
163
|
+
-XX:+UseZGC"
|
|
164
|
+
|
|
165
|
+
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## CI/CD (GitHub Actions)
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
name: CI
|
|
172
|
+
|
|
173
|
+
on:
|
|
174
|
+
push:
|
|
175
|
+
branches: [main]
|
|
176
|
+
pull_request:
|
|
177
|
+
branches: [main]
|
|
178
|
+
|
|
179
|
+
jobs:
|
|
180
|
+
build-and-test:
|
|
181
|
+
runs-on: ubuntu-latest
|
|
182
|
+
|
|
183
|
+
services:
|
|
184
|
+
postgres:
|
|
185
|
+
image: postgres:16-alpine
|
|
186
|
+
env:
|
|
187
|
+
POSTGRES_PASSWORD: test
|
|
188
|
+
POSTGRES_DB: testdb
|
|
189
|
+
ports:
|
|
190
|
+
- 5432:5432
|
|
191
|
+
|
|
192
|
+
steps:
|
|
193
|
+
- uses: actions/checkout@v4
|
|
194
|
+
|
|
195
|
+
- uses: actions/setup-java@v4
|
|
196
|
+
with:
|
|
197
|
+
distribution: temurin
|
|
198
|
+
java-version: 21
|
|
199
|
+
|
|
200
|
+
- uses: gradle/actions/setup-gradle@v4
|
|
201
|
+
|
|
202
|
+
- name: Build and test
|
|
203
|
+
run: ./gradlew check
|
|
204
|
+
env:
|
|
205
|
+
DATABASE_URL: jdbc:postgresql://localhost:5432/testdb
|
|
206
|
+
|
|
207
|
+
- name: Detekt
|
|
208
|
+
run: ./gradlew detekt
|
|
209
|
+
|
|
210
|
+
- name: Upload test results
|
|
211
|
+
if: always()
|
|
212
|
+
uses: actions/upload-artifact@v4
|
|
213
|
+
with:
|
|
214
|
+
name: test-results
|
|
215
|
+
path: build/reports/tests/
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Logging
|
|
219
|
+
|
|
220
|
+
```kotlin
|
|
221
|
+
// kotlin-logging (SLF4J wrapper)
|
|
222
|
+
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
223
|
+
|
|
224
|
+
private val logger = KotlinLogging.logger {}
|
|
225
|
+
|
|
226
|
+
// Lazy evaluation — message not constructed if level is disabled
|
|
227
|
+
logger.info { "Processing order ${order.id} with ${order.items.size} items" }
|
|
228
|
+
logger.error(exception) { "Failed to process order ${order.id}" }
|
|
229
|
+
|
|
230
|
+
// MDC for request context
|
|
231
|
+
MDC.put("requestId", requestId)
|
|
232
|
+
MDC.put("userId", userId)
|
|
233
|
+
try {
|
|
234
|
+
processRequest()
|
|
235
|
+
} finally {
|
|
236
|
+
MDC.clear()
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Anti-Patterns
|
|
241
|
+
|
|
242
|
+
```kotlin
|
|
243
|
+
// Never: build.gradle (Groovy) for Kotlin projects
|
|
244
|
+
// Use: build.gradle.kts (Kotlin DSL) — type-safe, IDE support
|
|
245
|
+
|
|
246
|
+
// Never: skipping detekt in CI
|
|
247
|
+
// Static analysis catches real bugs and code smells
|
|
248
|
+
|
|
249
|
+
// Never: Jackson for Kotlin (use kotlinx.serialization)
|
|
250
|
+
// Jackson requires reflection and kotlin-module. kotlinx.serialization is compile-time.
|
|
251
|
+
|
|
252
|
+
// Never: JUnit 4 assertions
|
|
253
|
+
assertEquals(expected, actual) // No message, confusing order
|
|
254
|
+
// Use: assertThat(actual).isEqualTo(expected) // AssertJ, readable
|
|
255
|
+
|
|
256
|
+
// Never: hardcoded versions scattered across modules
|
|
257
|
+
// Use: version catalogs (gradle/libs.versions.toml)
|
|
258
|
+
```
|