connekt-script-writer について
Connekt Kotlin HTTP クライアントの利用者向けスクリプト作成スキルです。実行時にランナーがリクエストを選ぶ `.connekt.kts` の作成方法を、環境設定、レスポンス処理、複数リクエストのシナリオまで説明します。
主な機能
- 01Connekt Kotlin DSL によるリクエストスクリプト作成
- 02connekt.env.json から設定とシークレットを読み込み
- 03HTTP メソッド、ボディ、認証の設定
- 04レスポンス解析とアサーション作成
- 05useCase による複数リクエストのシナリオ整理
- 06スクリプトのインポートと Maven 依存関係の宣言
活用シーン
- 01環境設定を使った API リクエストスクリプトの作成
- 02レスポンスのテストと JSON データの抽出
- 03リソース作成後に検証するワークフローの構成
- 04OAuth2 リクエスト用の認証スクリプトの再利用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ amplicode/ spring-skills --skill connekt-script-writerインストール後、Claude Code で/connekt-script-writerを入力して読み込まれたか確認します。
詳細
Connekt Script Writer
Connekt は Kotlin スクリプトで操作する HTTP クライアントです。スクリプトには .connekt.kts 拡張子を使用し、Connekt DSL 全体をトップレベルで利用できます。ボイラープレートや main 関数は不要で、宣言とリクエストだけを記述します。
スクリプトを生成する際は、値をハードコードせず、常に connekt.env.json からベース URL とシークレットを val x: String by env で読み込んでください。スクリプトは .connekt.kts 拡張子で保存してください。
実行モデル
スクリプトはリクエストを登録するだけで、実行するリクエストはランナーが決定します。 スクリプトは通常のプログラムのように上から下へ実行されるわけではありません。つまり、次のようになります。
- リクエスト間に命令型コードを記述しないでください。
println、if/else、thenブロックまたはuseCaseブロックの外側での変数代入は使用できません。 - アサーションは
then { }ブロックまたはuseCase { }ブロック内でのみ使用できます。リクエスト間のトップレベルには記述できません。 - 他のリクエストの結果を URL に直接埋め込まないでください。
pathParamを使用すると、実行時にランナーが値を解決できます。 - トップレベルで使用できるもの:
val x by env、data class、configureClient、val x by oauth(...)、拡張関数(共通ヘッダー用のRequestBuilder拡張関数など)、リクエスト宣言(val x by GET/POST/...)、およびuseCaseブロック。
// ✅ CORRECT — pass result via pathParam
val petId by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido"}""")
} then {
decode<Long>("$.id")
}
GET("$host/api/pets/{petId}") {
pathParam("petId", petId)
} then {
assert(code == 200)
}
// ❌ WRONG — never interpolate request results in URL
GET("$host/api/pets/$petId") then {
assert(code == 200)
}
RequestBuilder の拡張関数は、共通の設定を適用するのに便利です。
fun RequestBuilder.withApiKey() {
header("X-Api-Key", apiKey)
header("X-Request-Id", java.util.UUID.randomUUID().toString())
}
GET("$host/api/pets") {
withApiKey()
}
スクリプトの構成
.connekt.kts ファイルは、慣例として次の順序で記述します。
// 1. File-level annotations (imports, dependencies)
@file:Import("shared_auth.connekt.kts")
@file:DependsOn("com.example:my-lib:1.0")
// 2. Third-party imports (if needed)
import org.assertj.core.api.Assertions.assertThat
// 3. Environment variables from connekt.env.json
val host: String by env
val apiKey: String by env
// 4. Data classes for typed deserialization
data class Pet(val id: Long, val name: String, val status: String)
// 5. Global client configuration
configureClient {
insecure() // for local dev only
}
// 6. OAuth setup (if needed)
val auth by oauth(
authorizeEndpoint = "...",
clientId = "...",
clientSecret = "...",
scope = "openid",
tokenEndpoint = "...",
redirectUri = "http://localhost:8080/callback"
)
// 7. HTTP requests
val pets by GET("$host/api/pets") then {
decode<List<Pet>>("$.content")
}
// 8. Use cases for grouping related requests
val result by useCase("Create and verify") {
val created by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido"}""")
} then {
decode<Pet>()
}
created
}
主な規約:
val x by envはconnekt.env.jsonから値を読み込みます。プロパティ名が検索キーになります。val response by GET(...)は実行を委譲し、結果をバインドします。then { ... }はレスポンス処理を連結します。ブロック内のthisは OkHttp のResponseです。val result by GET(...) then { expr }はthenブロックの戻り値を取得します。decode<T>(jsonPath)はレスポンスボディから JSON をデシリアライズします。useCase("name") { ... }はリクエストをまとめます。最後の式が戻り値になります。assertSoftly { assert(...) }は、報告前にすべてのアサーション失敗を収集します。- リクエスト間にコードを記述しないでください。アサーションとロジックは
thenまたはuseCaseの中だけに記述します。 - リクエストの結果は
pathParamで渡してください。URL 文字列に直接埋め込まないでください。
DSL リファレンス
HTTP メソッド
GET("$host/api/resource")
POST("$host/api/resource")
PUT("$host/api/resource")
PATCH("$host/api/resource")
DELETE("$host/api/resource")
HEAD("$host/api/resource")
OPTIONS("$host/api/resource")
TRACE("$host/api/resource")
各メソッドは省略可能な name パラメーターと設定ラムダを受け取ります。
GET("$host/api/pets", name = "List all pets") {
header("Accept", "application/json")
queryParam("status", "available")
}
リクエストの設定
ヘッダー:
GET("$host/api/resource") {
header("X-Api-Key", apiKey)
headers("Accept" to "application/json", "X-Request-Id" to "abc123")
contentType("application/json")
accept("application/json")
}
クエリパラメーター:
GET("$host/api/pets") {
queryParam("status", "available")
queryParam("limit", 20)
queryParams("sort" to "name", "order" to "asc")
}
パスパラメーター(URL 内では {name} プレースホルダーを使用します):
DELETE("$host/api/owners/{ownerId}/pets/{petId}") {
pathParam("ownerId", 1)
pathParam("petId", 5)
}
リクエストオプション:
GET("$host/old-url") {
noRedirect() // don't follow 3xx redirects
noCookies() // exclude cookies from this request
http2() // use HTTP/2 (h2c)
}
リクエストボディ
JSON ボディ:
POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido", "species": "dog"}""")
}
フォームデータ(Content-Type は自動的に設定されます):
POST("$host/api/login") {
formData {
field("username", "alice")
field("password", "secret")
}
}
マルチパート:
POST("$host/api/upload") {
multipart {
part(name = "metadata", contentType = "application/json") {
body("""{"description": "profile picture"}""")
}
file(name = "photo", fileName = "avatar.jpg", file = java.io.File("/tmp/avatar.jpg"))
}
}
バイト配列:
POST("$host/api/upload") {
header("Content-Type", "application/octet-stream")
body(java.io.File("/tmp/data.bin").readBytes())
}
認証
Basic と Bearer:
GET("$host/api/resource") { basicAuth("user", "pass") }
GET("$host/api/resource") { bearerAuth(myToken) }
OAuth2(認可コードフロー):
val auth by oauth(
authorizeEndpoint = "$authHost/oauth/authorize",
clientId = clientId,
clientSecret = clientSecret,
scope = "openid profile",
tokenEndpoint = "$authHost/oauth/token",
redirectUri = "http://localhost:8080/callback"
)
GET("$host/api/protected") {
bearerAuth(auth.accessToken)
}
OAuth2 はログイン用のブラウザーを開き、ローカルのコールバックサーバーを自動的に起動します。auth オブジェクトは accessToken と refreshToken を提供します。
Keycloak の簡易記法:
val auth by oauth(
KeycloakOAuthParameters(
serverBaseUrl = keycloakHost,
realm = "my-realm",
protocol = "openid-connect",
clientId = "my-client",
clientSecret = "secret",
scope = "openid",
callbackPort = 8080,
callbackPath = "/callback"
)
)
レスポンス処理
then ブロック内では、this は OkHttp の Response です。code、body、header("Name") などを利用できます。
検証と抽出:
GET("$host/api/pets") then {
assert(code == 200) { "Expected 200 but got $code" }
val text = body!!.string()
println(text)
}
decode<T>() による型付きデシリアライズ:
val pets by GET("$host/api/pets") then {
decode<List<Pet>>("$.content") // JSONPath extraction
}
val pet by GET("$host/api/pet/1") then {
decode<Pet>() // root object (default when no JSONPath given)
}
JSONPath による生の値へのアクセス:
GET("$host/api/stats") then {
val ctx = jsonPath()
val count = ctx.decode<Int>("$.totalCount")
val names = ctx.decode<List<String>>("$.items[*].name")
}
リクエストの連結 — pathParam を使って、あるリクエストの結果を次のリクエストに渡します:
val petId by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido"}""")
} then {
decode<Long>("$.id")
}
GET("$host/api/pets/{petId}") {
pathParam("petId", petId)
} then {
assert(code == 200)
}
ユースケース
useCase を使用する場合
useCaseは、結果を受け渡す複数のリクエストを含むシナリオ(例:リソースを作成し、その後に存在を確認する)の場合にのみ使用します。- 1 つの HTTP リクエストは(
thenブロックがあっても)決してuseCaseで囲みません。 - ユーザーがビジネスケースのように見えるシナリオ(複数のステップ)を説明した場合、スキルは「これは複数のステップからなるビジネスシナリオです。
useCaseで囲みますか?」と必ず確認し、コードを生成する前に回答を待つ必要があります。
単一の HTTP リクエストは then を使っても単独で記述し、useCase では囲みません。
val createdPet by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Rex"}""")
} then {
decode<Pet>()
}
関連する複数のリクエストは useCase でまとめます。最後の式が戻り値になります。
val payment by useCase("Place order and pay") {
val order by POST("$host/api/orders") {
contentType("application/json")
body("""{"productId": 42, "quantity": 1}""")
} then {
decode<Order>()
}
val payment by POST("$host/api/payments") {
contentType("application/json")
body("""{"orderId": ${order.id}, "amount": ${order.totalPrice}}""")
} then {
decode<Payment>()
}
payment
}
名前のない無名のユースケースも、同じように動作します。
useCase {
GET("$host/foo") then { assert(code == 200) }
GET("$host/bar") then { assert(code == 200) }
}
環境変数
プロパティ委譲を使用して connekt.env.json から読み込みます。プロパティ名が検索キーになります。
val host: String by env
val port: Int by env
connekt.env.json ファイル:
{
"env": {
"host": "http://localhost:8080",
"port": 8080
}
}
対応する型:String、Int、Long、Double、Boolean。キーが存在しない場合はエラーが発生します。
アサーション
Kotlin の assert(--kotlin-power-assert を使用すると Power Assert の診断情報が表示されます):
GET("$host/api/pets") then {
assert(code == 200) { "Expected 200 but got $code" }
}
AssertJ(インポートが必要です。非推奨のため、新しいスクリプトでは使用しないでください):
import org.assertj.core.api.Assertions.assertThat
GET("$host/api/pets") then {
assertThat(code).isEqualTo(200)
val pets = decode<List<Pet>>("$.content")
assertThat(pets).isNotEmpty
}
ソフトアサーション — 報告する前にすべての失敗を収集します。
GET("$host/api/users/1") then {
val user = decode<User>()
assertSoftly {
assert(user.name == "Alice")
assert(user.email.contains("@"))
assert(user.age > 0)
assert(user.active)
}
}
スクリプトのインポートと依存関係
別のスクリプトをインポートする場合(パスはインポート元のスクリプトからの相対パスです):
@file:Import("shared_auth.connekt.kts")
@file:Import("utils.connekt.kts")
インポートしたトップレベル宣言(val、関数、クラス)が使用可能になります。推移的なインポートも機能します。
外部 Maven 依存関係:
@file:DependsOn("com.example:my-lib:1.0.0")
SSL/TLS とクライアントの設定
// Global client config
configureClient {
insecure() // disable SSL verification (dev only!)
addX509Certificate(java.io.File("certs/my-ca.crt"))
addKeyStore(java.io.File("certs/truststore.jks"), "changeit")
readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
}
// Per-request override
GET("$host/api/slow-endpoint") {
configureClient {
readTimeout(120, java.util.concurrent.TimeUnit.SECONDS)
}
}
よくあるパターン
アサーションを含むシンプルな GET
val host: String by env
GET("$host/api/health") then {
assert(code == 200) { "Health check failed with status $code" }
println(body!!.string())
}
JSON ボディとレスポンス抽出を含む POST
val host: String by env
data class Pet(val id: Long, val name: String, val status: String)
val created by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido", "status": "available"}""")
} then {
assert(code == 201) { "Expected 201 but got $code" }
decode<Pet>()
}
CRUD ワークフロー
val host: String by env
data class Pet(val id: Long, val name: String, val status: String)
// Многошаговый бизнес-сценарий — оправданное применение useCase
val result by useCase("Pet CRUD") {
val pet by POST("$host/api/pets") {
contentType("application/json")
body("""{"name": "Fido", "status": "available"}""")
} then {
assert(code == 201)
decode<Pet>()
}
GET("$host/api/pets/{petId}") {
pathParam("petId", pet.id)
} then {
assert(code == 200)
assert(decode<Pet>().name == "Fido")
}
val updated by PUT("$host/api/pets/{petId}") {
pathParam("petId", pet.id)
contentType("application/json")
body("""{"name": "Rex", "status": "available"}""")
} then {
assert(code == 200)
decode<Pet>()
}
assert(updated.name == "Rex")
DELETE("$host/api/pets/{petId}") {
pathParam("petId", pet.id)
} then { assert(code == 204) }
GET("$host/api/pets/{petId}") {
pathParam("petId", pet.id)
} then { assert(code == 404) }
pet
}
OAuth2 認証済みリクエスト
val host: String by env
val authHost: String by env
val clientId: String by env
val clientSecret: String by env
val auth by oauth(
authorizeEndpoint = "$authHost/realms/my-realm/protocol/openid-connect/auth",
clientId = clientId,
clientSecret = clientSecret,
scope = "openid",
tokenEndpoint = "$authHost/realms/my-realm/protocol/openid-connect/token",
redirectUri = "http://localhost:8080/callback"
)
val owners by GET("$host/api/owners") {
bearerAuth(auth.accessToken)
queryParam("lastNameContains", "smith")
} then {
assert(code == 200)
decode<List<String>>("$.content[*].name")
}
フォームログイン後に API を呼び出す
val host: String by env
POST("$host/login") {
formData {
field("username", "admin")
field("password", "admin")
}
} then {
assert(code == 200 || code == 302) { "Login failed" }
}
// Session cookie is sent automatically
val dashboard by GET("$host/api/dashboard") then {
assert(code == 200)
decode<Map<String, Any>>()
}
ファイルアップロード(マルチパート)
val host: String by env
val uploadResult by POST("$host/api/files/upload") {
multipart {
part(name = "metadata", contentType = "application/json") {
body("""{"description": "Monthly report", "category": "reports"}""")
}
file(name = "document", fileName = "report.pdf", file = java.io.File("/tmp/report.pdf"))
}
} then {
assert(code == 201) { "Upload failed with status $code" }
decode<Map<String, Any>>()
}
ページネーションループ
val host: String by env
data class Event(val id: String, val summary: String)
data class EventsPage(val items: List<Event>, val nextPageToken: String?)
val allEvents by useCase("Fetch all events") {
val events = mutableListOf<Event>()
var nextPageToken: String? = null
do {
nextPageToken = GET("$host/api/events") {
bearerAuth("my-token")
queryParam("maxResults", 50)
if (nextPageToken != null) {
queryParam("pageToken", nextPageToken!!)
}
} then {
val page = decode<EventsPage>()
events.addAll(page.items)
page.nextPageToken
}
} while (nextPageToken != null)
events.toList()
}
@file:Import による複数スクリプトでの再利用
auth_setup.connekt.kts(共有):
val host: String by env
val clientId: String by env
val clientSecret: String by env
val auth by oauth(
authorizeEndpoint = "$host/oauth/authorize",
clientId = clientId,
clientSecret = clientSecret,
scope = "openid",
tokenEndpoint = "$host/oauth/token",
redirectUri = "http://localhost:8080/callback"
)
api_tests.connekt.kts(上記をインポート):
@file:Import("auth_setup.connekt.kts")
GET("$host/api/protected") {
bearerAuth(auth.accessToken)
} then {
assert(code == 200)
}
ソフトアサーションによるレスポンス検証
val host: String by env
data class User(val name: String, val email: String, val age: Int, val active: Boolean)
GET("$host/api/users/1") then {
assert(code == 200) { "Expected 200 but got $code" }
val user = decode<User>()
assertSoftly {
assert(user.name == "Alice")
assert(user.email.contains("@"))
assert(user.age > 0)
assert(user.active)
}
}
重要な注意事項
- デフォルトでは、
useCaseを使わず、単一のリクエストを生成してください。useCaseを使用するのは、リクエスト間でデータを受け渡す複数のステップがシナリオに明示的に含まれている場合に限ります。1 つのリクエストは(thenの有無にかかわらず)決してuseCaseで囲みません。このルールが最優先です。 - スクリプトはリクエストを登録するものであり、命令的に実行するものではありません。 トップレベルでリクエスト間にコードを記述しないでください。アサーションとロジックは
thenブロックまたはuseCaseブロック内にのみ記述してください。 - リクエストの結果を URL に埋め込まないでください(
"$host/api/pets/$petId")。URL に{petId}プレースホルダーを指定し、必ずpathParam("petId", petId)を使用してください。 RequestBuilderに対するトップレベルの拡張関数は、リクエスト設定(共通ヘッダーや認証など)の再利用に使用できます。- 非推奨の関数は使用しないでください:
vars、variable<T>()、doRead()、readString()、readInt()、readLong()、readBoolean()。すべてのレスポンス抽出にはdecode<T>()を使用してください。 - Cookie はセッションジャーで自動的に管理されます。
noCookies()を使用する場合を除き、手動で Cookie を扱う必要はありません。 - デフォルトでは、OkHttp は 3xx リダイレクトに従います。リダイレクトレスポンスを確認するには
noRedirect()を使用してください。 insecure()はすべての SSL 検証を無効にします。ローカル開発でのみ使用してください。