From a3292890055352ac4c62b11436e71bc7f06157a8 Mon Sep 17 00:00:00 2001 From: Paul Thompson Date: Wed, 23 Dec 2020 16:29:42 +1100 Subject: [PATCH] Request data --- .../main/kotlin/net/devslash/BodyProviders.kt | 12 +-- api/src/main/kotlin/net/devslash/CookieJar.kt | 2 +- .../main/kotlin/net/devslash/Definitions.kt | 48 ++++++----- api/src/main/kotlin/net/devslash/FetchDsl.kt | 53 ++++++------ .../kotlin/net/devslash/SessionManager.kt | 4 +- .../devslash/err/RetryOnTransitiveError.kt | 6 +- .../src/jmh/kotlin/net/devslash/Bencho.kt | 1 - build.gradle.kts | 9 +- .../net/devslash/examples/PipeExample.kt | 21 +++-- .../net/devslash/data/FileDataSupplier.kt | 4 +- .../net/devslash/data/ListDataSupplier.kt | 37 ++++++--- .../net/devslash/data/ModifiableSupplier.kt | 10 +-- .../net/devslash/err/DeadLetterQueue.kt | 6 +- .../kotlin/net/devslash/outputs/AppendFile.kt | 21 +++-- .../net/devslash/outputs/DebugOutput.kt | 2 +- .../net/devslash/outputs/DefaultOutput.kt | 4 +- .../kotlin/net/devslash/outputs/StdOut.kt | 2 +- .../kotlin/net/devslash/outputs/WriteFile.kt | 11 ++- .../main/kotlin/net/devslash/pipes/Pipe.kt | 18 ++-- .../net/devslash/pipes/ResettablePipe.kt | 20 ++--- .../main/kotlin/net/devslash/post/Filter.kt | 2 +- .../kotlin/net/devslash/post/LogResponse.kt | 4 +- .../kotlin/net/devslash/pre/LogRequest.kt | 2 +- .../src/main/kotlin/net/devslash/pre/Once.kt | 4 +- .../main/kotlin/net/devslash/pre/SkipIf.kt | 4 +- .../kotlin/net/devslash/BodyProviderTest.kt | 16 ++-- .../test/kotlin/net/devslash/CookieJarTest.kt | 15 ++-- .../net/devslash/HttpSessionManagerTest.kt | 2 +- .../kotlin/net/devslash/it/HttpBounceTest.kt | 46 ++++++++--- .../net/devslash/outputs/AppendFileTest.kt | 6 +- .../kotlin/net/devslash/pipes/PipeTest.kt | 24 +++--- .../net/devslash/pipes/ResettablePipeTest.kt | 22 ++--- .../test/kotlin/net/devslash/pre/OnceTest.kt | 27 +++--- .../kotlin/net/devslash/pre/SkipIfTest.kt | 2 +- .../kotlin/net/devslash/util/TestUtils.kt | 25 +----- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 37 ++++----- gradlew.bat | 27 ++---- service/build.gradle.kts | 6 ++ .../kotlin/net/devslash/GenericRequestData.kt | 28 +++++++ .../main/kotlin/net/devslash/HttpDriver.kt | 52 ++++++------ .../kotlin/net/devslash/HttpSessionManager.kt | 77 +++++++++++------- .../net/devslash/ListBasedRequestData.kt | 36 ++++---- .../kotlin/net/devslash/ListStringAcceptor.kt | 35 ++++++++ .../src/main/kotlin/net/devslash/Providers.kt | 21 +++-- .../main/kotlin/net/devslash/RateLimiter.kt | 2 +- .../src/main/kotlin/net/devslash/Runner.kt | 18 ++-- .../main/kotlin/net/devslash/UrlProviders.kt | 10 ++- .../kotlin/net/devslash/DefaultHeaderTest.kt | 4 +- .../src/test/kotlin/net/devslash/DelayTest.kt | 19 ++++- .../net/devslash/ListBasedRequestDataTest.kt | 2 +- .../src/test/kotlin/net/devslash/TestUtils.kt | 40 +++++++++ .../kotlin/net/devslash/UrlProvidersTest.kt | 2 - 54 files changed, 538 insertions(+), 373 deletions(-) create mode 100644 service/src/main/kotlin/net/devslash/GenericRequestData.kt create mode 100644 service/src/main/kotlin/net/devslash/ListStringAcceptor.kt rename {extensions => service}/src/test/kotlin/net/devslash/DelayTest.kt (58%) create mode 100644 service/src/test/kotlin/net/devslash/TestUtils.kt rename {extensions => service}/src/test/kotlin/net/devslash/UrlProvidersTest.kt (87%) diff --git a/api/src/main/kotlin/net/devslash/BodyProviders.kt b/api/src/main/kotlin/net/devslash/BodyProviders.kt index 3d919fa..0066529 100644 --- a/api/src/main/kotlin/net/devslash/BodyProviders.kt +++ b/api/src/main/kotlin/net/devslash/BodyProviders.kt @@ -1,7 +1,7 @@ package net.devslash -class BasicBodyProvider(private val body: String, val data: RequestData) : BodyProvider { +class BasicBodyProvider(private val body: String, val data: RequestData) : BodyProvider { fun get(): String { var copy = "" + body data.getReplacements().forEach { (key, value) -> copy = copy.replace(key, value) } @@ -9,11 +9,13 @@ class BasicBodyProvider(private val body: String, val data: RequestData) : BodyP } } -class FormBody(private val body: Map>, - private val data: RequestData) : BodyProvider { +class FormBody( + private val body: Map>, + private val data: RequestData +) : BodyProvider { fun get(): Map> { return body.map { - val entries = it.value.map { it.asReplaceableValue().get(data) } + val entries = it.value.map { v -> data.accept(v) } it.key to entries }.toMap() } @@ -25,7 +27,7 @@ class JsonBody(private val any: Any) : BodyProvider { } } -fun getBodyProvider(call: Call, data: RequestData): BodyProvider { +fun getBodyProvider(call: Call, data: RequestData): BodyProvider { if (call.body == null) { return EmptyBodyProvider } diff --git a/api/src/main/kotlin/net/devslash/CookieJar.kt b/api/src/main/kotlin/net/devslash/CookieJar.kt index 07a6efd..4d97721 100644 --- a/api/src/main/kotlin/net/devslash/CookieJar.kt +++ b/api/src/main/kotlin/net/devslash/CookieJar.kt @@ -6,7 +6,7 @@ import java.util.concurrent.ConcurrentHashMap class CookieJar : SimpleBeforeHook, SimpleAfterHook { private val cookies = ConcurrentHashMap(mutableMapOf>()) - override fun accept(req: HttpRequest, data: RequestData) { + override fun accept(req: HttpRequest, data: RequestData) { val basicURl = URL(req.url) val filteredUrl = "${basicURl.protocol}://${basicURl.host}" diff --git a/api/src/main/kotlin/net/devslash/Definitions.kt b/api/src/main/kotlin/net/devslash/Definitions.kt index 2d9fe26..ea98977 100644 --- a/api/src/main/kotlin/net/devslash/Definitions.kt +++ b/api/src/main/kotlin/net/devslash/Definitions.kt @@ -4,26 +4,26 @@ import kotlinx.coroutines.channels.Channel sealed class Value data class StrValue(val value: String) : Value() -data class ProvidedValue(val lambda: (RequestData) -> String) : Value() +data class ProvidedValue(val lambda: (RequestData) -> String) : Value() interface BodyProvider -data class Session(val calls: List, val concurrency: Int = 100, val delay: Long?, val rateOptions: RateLimitOptions) +data class Session(val calls: List>, val concurrency: Int = 100, val delay: Long?, val rateOptions: RateLimitOptions) -data class Call(val url: String, +data class Call(val url: String, val headers: Map>?, val cookieJar: String?, val type: HttpMethod, - val dataSupplier: RequestDataSupplier?, - val body: HttpBody?, + val dataSupplier: RequestDataSupplier?, + val body: HttpBody?, val onError: OnError?, val beforeHooks: List, val afterHooks: List) -interface RequestDataSupplier { +interface RequestDataSupplier { /** * Request data should be a closure that is safe to call on a per-request basis */ - suspend fun getDataForRequest(): RequestData? + suspend fun getDataForRequest(): RequestData? fun init() { // By default this is empty, but implementors can be assured that on a per-call basis, this @@ -32,29 +32,33 @@ interface RequestDataSupplier { } interface OutputFormat { - fun accept(resp: HttpResponse, rep: RequestData): ByteArray? + fun accept(resp: HttpResponse, rep: RequestData): ByteArray? } -interface RequestData { +interface RequestData { + @Deprecated("Instead, please migrate to utilising Get") fun getReplacements(): Map + fun get(): T + fun accept(v: String): String } interface BasicOutput : FullDataAfterHook -data class HttpBody(val value: String?, +data class HttpBody(val value: String?, val formData: Map>?, val jsonObject: Any?, - val lazyJsonObject: ((RequestData) -> Any)?) + val lazyJsonObject: ((RequestData) -> Any)?) interface ReplaceableValue { fun get(data: V): T } -fun String.asReplaceableValue() = object : ReplaceableValue { - override fun get(data: RequestData): String { +@Deprecated("Please use accept in request data") +fun String.asReplaceableValue() = object : ReplaceableValue>> { + override fun get(data: RequestData>): String { val replacements = data.getReplacements() var copy = "" + this@asReplaceableValue - replacements.forEach { key, value -> copy = copy.replace(key, value) } + replacements.forEach { (key, value) -> copy = copy.replace(key, value) } return copy } } @@ -62,24 +66,24 @@ fun String.asReplaceableValue() = object : ReplaceableValue interface BeforeHook fun (() -> Unit).toPreHook() = object : SimpleBeforeHook { - override fun accept(req: HttpRequest, data: RequestData) { + override fun accept(req: HttpRequest, data: RequestData) { this@toPreHook() } } interface SessionPersistingBeforeHook : BeforeHook { - suspend fun accept(sessionManager: SessionManager, + suspend fun accept(sessionManager: SessionManager, cookieJar: CookieJar, req: HttpRequest, - data: RequestData) + data: RequestData) } -interface SkipBeforeHook : BeforeHook { - fun skip(requestData: RequestData): Boolean +interface SkipBeforeHook : BeforeHook { + fun skip(requestData: RequestData): Boolean } interface SimpleBeforeHook : BeforeHook { - fun accept(req: HttpRequest, data: RequestData) + fun accept(req: HttpRequest, data: RequestData) } class Envelope(private val message: T, private val maxRetries: Int = 3) { @@ -115,8 +119,8 @@ interface ChainReceivingResponseHook : AfterHook { fun accept(resp: HttpResponse) } -interface FullDataAfterHook : AfterHook { - fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) +interface FullDataAfterHook: AfterHook { + fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) } sealed class HttpResult diff --git a/api/src/main/kotlin/net/devslash/FetchDsl.kt b/api/src/main/kotlin/net/devslash/FetchDsl.kt index f782c6e..e82bc48 100644 --- a/api/src/main/kotlin/net/devslash/FetchDsl.kt +++ b/api/src/main/kotlin/net/devslash/FetchDsl.kt @@ -25,13 +25,14 @@ class UnaryAddBuilder { data class RateLimitOptions(val enabled: Boolean, val count: Int, val duration: Duration) @FetchDSL -open class CallBuilder(private val url: String) { +open class CallBuilder(private val url: String) { + private var cookieJar: String? = null - var data: RequestDataSupplier? = null - var body: HttpBody? = null + var data: RequestDataSupplier? = null + var body: HttpBody? = null var type: HttpMethod = HttpMethod.GET var headers: Map>? = null - var onError: OnError? = RetryOnTransitiveError() + var onError: OnError? = RetryOnTransitiveError() private var preHooksList = mutableListOf() private var postHooksList = mutableListOf() @@ -44,8 +45,8 @@ open class CallBuilder(private val url: String) { postHooksList.addAll(UnaryAddBuilder().apply(block).build()) } - fun body(block: BodyBuilder.() -> Unit) { - body = BodyBuilder().apply(block).build() + fun body(block: BodyBuilder.() -> Unit) { + body = BodyBuilder().apply(block).build() } private fun mapHeaders(m: Map>?): Map>? { @@ -62,7 +63,7 @@ open class CallBuilder(private val url: String) { } } - fun build(): Call { + fun build(): Call { val localHeaders = headers if (localHeaders == null || !localHeaders.contains("User-Agent")) { val set = mutableMapOf>() @@ -72,27 +73,29 @@ open class CallBuilder(private val url: String) { set["User-Agent"] = listOf("FetchDSL (Apache-HttpAsyncClient + Kotlin, version not set)") headers = set } - return Call(url, mapHeaders(headers), cookieJar, type, data, body, - onError, preHooksList, postHooksList) + return Call( + url, mapHeaders(headers), cookieJar, type, data, body, + onError, preHooksList, postHooksList + ) } } @FetchDSL -class BodyBuilder { +class BodyBuilder { var value: String? = null var formParams: Map>? = null var jsonObject: Any? = null - var lazyJsonObject: ((RequestData) -> Any)? = null + var lazyJsonObject: ((RequestData) -> Any)? = null - fun build(): HttpBody = HttpBody(value, formParams, jsonObject, lazyJsonObject) + fun build(): HttpBody = HttpBody(value, formParams, jsonObject, lazyJsonObject) } @FetchDSL class MultiCallBuilder { - private var calls = mutableListOf() + private var calls = mutableListOf>() - fun call(url: String, block: CallBuilder.() -> Unit = {}) { - calls.add(CallBuilder(url).apply(block).build()) + fun call(url: String, block: CallBuilder<*>.() -> Unit = {}) { + calls.add(CallBuilder(url).apply(block).build()) } fun calls() = calls @@ -100,8 +103,8 @@ class MultiCallBuilder { @FetchDSL class SessionBuilder { - private var calls = mutableListOf() - private val chained = mutableListOf>() + private var calls = mutableListOf>() + private val chained = mutableListOf>>() var concurrency = 20 var delay: Long? = null @@ -113,18 +116,14 @@ class SessionBuilder { rateOptions = RateLimitOptions(true, count, duration) } - fun call(url: String, block: CallBuilder.() -> Unit = {}) { - calls.add(CallBuilder(url).apply(block).build()) + @JvmName("nonStringCall") + fun call(url: String, block: CallBuilder.() -> Unit = {}) { + calls.add(CallBuilder(url).apply(block).build()) } - // TODO: Re-enable when chaining is stable -// fun chained(block: MultiCallBuilder.(prev: Previous?) -> Unit = {}) { -// if (chained.isNotEmpty()) { -// val line = chained.last().line -// -// } -// chained.add(MultiCallBuilder().apply(block).calls()) -// } + fun call(url: String, block: CallBuilder>.() -> Unit = {}) { + calls.add(CallBuilder>(url).apply(block).build()) + } fun build(): Session = Session(calls, concurrency, delay, rateOptions) } diff --git a/api/src/main/kotlin/net/devslash/SessionManager.kt b/api/src/main/kotlin/net/devslash/SessionManager.kt index 4459b65..fcaf5f6 100644 --- a/api/src/main/kotlin/net/devslash/SessionManager.kt +++ b/api/src/main/kotlin/net/devslash/SessionManager.kt @@ -1,6 +1,6 @@ package net.devslash interface SessionManager { - fun call(call: Call, jar: CookieJar) - fun call(call: Call) + fun call(call: Call, jar: CookieJar) + fun call(call: Call) } diff --git a/api/src/main/kotlin/net/devslash/err/RetryOnTransitiveError.kt b/api/src/main/kotlin/net/devslash/err/RetryOnTransitiveError.kt index d895e86..9e55342 100644 --- a/api/src/main/kotlin/net/devslash/err/RetryOnTransitiveError.kt +++ b/api/src/main/kotlin/net/devslash/err/RetryOnTransitiveError.kt @@ -7,9 +7,9 @@ import net.devslash.HttpRequest import net.devslash.RequestData import java.net.SocketTimeoutException -class RetryOnTransitiveError : ChannelReceiving> { - override suspend fun accept(channel: Channel>>, - envelope: Envelope>, +class RetryOnTransitiveError : ChannelReceiving>> { + override suspend fun accept(channel: Channel>>>, + envelope: Envelope>>, e: Exception) { if (!envelope.shouldProceed()) { // fail after a few failures diff --git a/benchmarks/src/jmh/kotlin/net/devslash/Bencho.kt b/benchmarks/src/jmh/kotlin/net/devslash/Bencho.kt index 3f08325..e2c3693 100644 --- a/benchmarks/src/jmh/kotlin/net/devslash/Bencho.kt +++ b/benchmarks/src/jmh/kotlin/net/devslash/Bencho.kt @@ -1,6 +1,5 @@ package net.devslash -import net.devslash.data.ListDataSupplier import org.openjdk.jmh.annotations.* import java.util.concurrent.TimeUnit diff --git a/build.gradle.kts b/build.gradle.kts index 651882d..2eafa78 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,7 @@ buildscript { plugins { base `maven-publish` - kotlin("jvm") version "1.3.50" apply false + kotlin("jvm") version "1.4.20" apply false id("com.jfrog.bintray") version "1.8.4" apply false } repositories { @@ -51,13 +51,6 @@ subprojects { } } -// tasks.withType(Test::class).configureEach { -// useJUnitPlatform() -// testLogging { -// events("passed", "skipped", "failed") -// } -// } - configure { user = project.findProperty("bintrayUser") as String? ?: System.getenv("BINTRAY_USER") key = project.findProperty("bintrayApiKey") as String? ?: System.getenv("BINTRAY_API_KEY") diff --git a/examples/src/main/kotlin/net/devslash/examples/PipeExample.kt b/examples/src/main/kotlin/net/devslash/examples/PipeExample.kt index e053235..e27f953 100644 --- a/examples/src/main/kotlin/net/devslash/examples/PipeExample.kt +++ b/examples/src/main/kotlin/net/devslash/examples/PipeExample.kt @@ -1,20 +1,18 @@ package net.devslash.examples -import io.ktor.application.call -import io.ktor.response.respondText -import io.ktor.routing.get -import io.ktor.routing.routing -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import net.devslash.data.FileDataSupplier -import net.devslash.outputs.WriteFile +import io.ktor.application.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import net.devslash.data.ListDataSupplier import net.devslash.pipes.ResettablePipe import net.devslash.runHttp import java.net.ServerSocket import java.util.concurrent.TimeUnit fun main() { - val pipe = ResettablePipe({ r, _ -> listOf(String(r.body)) }) + val pipe = ResettablePipe { r, _ -> listOf(String(r.body)) } val port = ServerSocket(0).use { it.localPort } val server = embeddedServer(Netty, port) { routing { @@ -27,10 +25,11 @@ fun main() { val address = "http://localhost:$port" runHttp { call(address) { - data = FileDataSupplier(this.javaClass.getResource("/in.log").path) + // Ok this asserting that it passesa round List types as its big object. therefore this should + data = ListDataSupplier(listOf("1")) after { +pipe - +WriteFile("!1!") +// +WriteFileeFile("!1!") } } call(address) { diff --git a/extensions/src/main/kotlin/net/devslash/data/FileDataSupplier.kt b/extensions/src/main/kotlin/net/devslash/data/FileDataSupplier.kt index 49c1cd5..1717108 100644 --- a/extensions/src/main/kotlin/net/devslash/data/FileDataSupplier.kt +++ b/extensions/src/main/kotlin/net/devslash/data/FileDataSupplier.kt @@ -6,11 +6,11 @@ import net.devslash.RequestDataSupplier import java.io.File import java.util.concurrent.atomic.AtomicInteger -class FileDataSupplier(val name: String, private val split: String = " ") : RequestDataSupplier { +class FileDataSupplier(val name: String, private val split: String = " ") : RequestDataSupplier> { private val sourceFile = File(name).readLines() private val line = AtomicInteger(0) - override suspend fun getDataForRequest(): RequestData? { + override suspend fun getDataForRequest(): RequestData>? { val ourLine = sourceFile.getOrNull(line.getAndIncrement())?.split(split) return if (ourLine == null) null else ListBasedRequestData(ourLine) } diff --git a/extensions/src/main/kotlin/net/devslash/data/ListDataSupplier.kt b/extensions/src/main/kotlin/net/devslash/data/ListDataSupplier.kt index 31074b9..0a15342 100644 --- a/extensions/src/main/kotlin/net/devslash/data/ListDataSupplier.kt +++ b/extensions/src/main/kotlin/net/devslash/data/ListDataSupplier.kt @@ -1,31 +1,46 @@ package net.devslash.data -import net.devslash.ListBasedRequestData +import net.devslash.GenericRequestData import net.devslash.RequestData import net.devslash.RequestDataSupplier import java.util.concurrent.atomic.AtomicInteger -class ListDataSupplier : RequestDataSupplier { +// This aims to create multiple request data pieces. Therefore out can be any type +class ListDataSupplier> : RequestDataSupplier { private val line = AtomicInteger(0) - private val list: Lazy> - private val transform: (T) -> List + private val list: Lazy + private val transform: (InType) -> Out - constructor(list: List, - transform: (T) -> List = { listOf("$it") }) { + companion object { + @JvmName("invoke1") + operator fun invoke(list: List): ListDataSupplier, String, List> { + return ListDataSupplier(list, { listOf(it) }) + } + + operator fun invoke(list: List): ListDataSupplier> { + return ListDataSupplier(list, {it}) + } + } + + constructor( + list: L, + transform: (InType) -> Out + ) { this.list = lazy { list } this.transform = transform - } - constructor(list: Lazy>, - transform: (T) -> List = { listOf("$it") }) { + constructor( + list: Lazy, + transform: (InType) -> Out + ) { this.list = list this.transform = transform } - override suspend fun getDataForRequest(): RequestData? { + override suspend fun getDataForRequest(): RequestData? { val index = line.getAndIncrement() val obj = list.value.getOrNull(index) ?: return null - return ListBasedRequestData(transform(obj)) + return GenericRequestData(transform(obj)) } } diff --git a/extensions/src/main/kotlin/net/devslash/data/ModifiableSupplier.kt b/extensions/src/main/kotlin/net/devslash/data/ModifiableSupplier.kt index b19d98b..d645da2 100644 --- a/extensions/src/main/kotlin/net/devslash/data/ModifiableSupplier.kt +++ b/extensions/src/main/kotlin/net/devslash/data/ModifiableSupplier.kt @@ -17,16 +17,16 @@ import java.util.concurrent.atomic.AtomicInteger * During this time between the last from the delegate, and the final request going through the * [getDataForRequest] method will block the calling thread as it's unsure of its response. */ -class ModifiableSupplier(private val delegate: RequestDataSupplier) : RequestDataSupplier, SimpleAfterHook { +class ModifiableSupplier(private val delegate: RequestDataSupplier) : RequestDataSupplier, SimpleAfterHook { - private val modifiedQueue: ConcurrentLinkedQueue = ConcurrentLinkedQueue() + private val modifiedQueue: ConcurrentLinkedQueue> = ConcurrentLinkedQueue() private val sentRequests = AtomicInteger(0) private val receivedResponses = AtomicInteger(0) - fun add(data: RequestData) = modifiedQueue.add(data) + fun add(data: RequestData) = modifiedQueue.add(data) - override suspend fun getDataForRequest(): RequestData? { + override suspend fun getDataForRequest(): RequestData? { val data = delegate.getDataForRequest() if (data != null) { // we'll send a request, thus count. @@ -45,7 +45,7 @@ class ModifiableSupplier(private val delegate: RequestDataSupplier) : RequestDat /** * In this case, there was no obvious candidate. Thus we want to wait until a request comes */ - private suspend fun waitOrNull(): RequestData? { + private suspend fun waitOrNull(): RequestData? { while (sentRequests.get() > receivedResponses.get()) { // Attempt to find one, if you don't get it then block until there's some output returned val result = modifiedQueue.poll() diff --git a/extensions/src/main/kotlin/net/devslash/err/DeadLetterQueue.kt b/extensions/src/main/kotlin/net/devslash/err/DeadLetterQueue.kt index 3614d7a..fce2ecc 100644 --- a/extensions/src/main/kotlin/net/devslash/err/DeadLetterQueue.kt +++ b/extensions/src/main/kotlin/net/devslash/err/DeadLetterQueue.kt @@ -8,15 +8,15 @@ import net.devslash.RequestData import java.io.File class DeadLetterQueue(filename: String, - private val split: String = " ") : ChannelReceiving> { + private val split: String = " ") : ChannelReceiving>> { private val file = File(filename) init { file.createNewFile() } - override suspend fun accept(channel: Channel>>, - envelope: Envelope>, + override suspend fun accept(channel: Channel>>>, + envelope: Envelope>>, e: Exception) { val data = envelope.get().second.getReplacements() var builder = "" diff --git a/extensions/src/main/kotlin/net/devslash/outputs/AppendFile.kt b/extensions/src/main/kotlin/net/devslash/outputs/AppendFile.kt index 530211d..6a8dda1 100644 --- a/extensions/src/main/kotlin/net/devslash/outputs/AppendFile.kt +++ b/extensions/src/main/kotlin/net/devslash/outputs/AppendFile.kt @@ -5,24 +5,29 @@ import java.io.BufferedOutputStream import java.io.FileOutputStream import java.io.OutputStream -class AppendFile(private val fileName: String, - private val out: OutputFormat = DefaultOutput()) : BasicOutput { +class AppendFile( + private val fileName: String, + private val out: OutputFormat = DefaultOutput() +) : BasicOutput { private val lock = Object() private var memoizedFile: OutputStream? = null init { - if (fileName.contains(Regex("!\\d+!"))) { + memoizedFile = if (fileName.contains(Regex("!\\d+!"))) { // then it's non memoizable - memoizedFile = null + null } else { - memoizedFile = BufferedOutputStream(FileOutputStream(fileName, true)) + BufferedOutputStream(FileOutputStream(fileName, true)) } } - override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { - val f = memoizedFile ?: BufferedOutputStream(FileOutputStream(fileName.asReplaceableValue().get( - data), true)) + override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { + val f = memoizedFile ?: BufferedOutputStream( + FileOutputStream( + data.accept(fileName), true + ) + ) val output = out.accept(resp, data) if (output != null) { diff --git a/extensions/src/main/kotlin/net/devslash/outputs/DebugOutput.kt b/extensions/src/main/kotlin/net/devslash/outputs/DebugOutput.kt index cc1b9ca..58ad63f 100644 --- a/extensions/src/main/kotlin/net/devslash/outputs/DebugOutput.kt +++ b/extensions/src/main/kotlin/net/devslash/outputs/DebugOutput.kt @@ -5,7 +5,7 @@ import net.devslash.OutputFormat import net.devslash.RequestData class DebugOutput : OutputFormat { - override fun accept(resp: HttpResponse, rep: RequestData): ByteArray? { + override fun accept(resp: HttpResponse, rep: RequestData): ByteArray? { return """ ---------------- url: ${resp.url} diff --git a/extensions/src/main/kotlin/net/devslash/outputs/DefaultOutput.kt b/extensions/src/main/kotlin/net/devslash/outputs/DefaultOutput.kt index a31101f..73fa3ee 100644 --- a/extensions/src/main/kotlin/net/devslash/outputs/DefaultOutput.kt +++ b/extensions/src/main/kotlin/net/devslash/outputs/DefaultOutput.kt @@ -4,8 +4,8 @@ import net.devslash.HttpResponse import net.devslash.OutputFormat import net.devslash.RequestData -class DefaultOutput : OutputFormat { - override fun accept(resp: HttpResponse, rep: RequestData): ByteArray? { +class DefaultOutput: OutputFormat { + override fun accept(resp: HttpResponse, rep: RequestData): ByteArray { return resp.body } } diff --git a/extensions/src/main/kotlin/net/devslash/outputs/StdOut.kt b/extensions/src/main/kotlin/net/devslash/outputs/StdOut.kt index 379254f..e5e3133 100644 --- a/extensions/src/main/kotlin/net/devslash/outputs/StdOut.kt +++ b/extensions/src/main/kotlin/net/devslash/outputs/StdOut.kt @@ -4,7 +4,7 @@ import net.devslash.* import java.io.PrintStream class StdOut(private val output: PrintStream = System.out, private val format: OutputFormat = DefaultOutput()) : BasicOutput { - override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { + override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { format.accept(resp, data)?.let { output.println(String(it)) } diff --git a/extensions/src/main/kotlin/net/devslash/outputs/WriteFile.kt b/extensions/src/main/kotlin/net/devslash/outputs/WriteFile.kt index e71ee6d..fc826f3 100644 --- a/extensions/src/main/kotlin/net/devslash/outputs/WriteFile.kt +++ b/extensions/src/main/kotlin/net/devslash/outputs/WriteFile.kt @@ -4,13 +4,15 @@ import net.devslash.* import java.io.File -class WriteFile(private val fileName: String, - private val out: OutputFormat = DefaultOutput()) : BasicOutput { +class WriteFile( + private val fileName: String, + private val out: OutputFormat = DefaultOutput() +) : BasicOutput { private val lock = Object() - override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { + override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { synchronized(lock) { - val f = File(fileName.asReplaceableValue().get(data)) + val f = File(data.accept(fileName)) val output = out.accept(resp, data) if (output != null) { f.writeBytes(output) @@ -18,3 +20,4 @@ class WriteFile(private val fileName: String, } } } + diff --git a/extensions/src/main/kotlin/net/devslash/pipes/Pipe.kt b/extensions/src/main/kotlin/net/devslash/pipes/Pipe.kt index 363a6dd..8f1e673 100644 --- a/extensions/src/main/kotlin/net/devslash/pipes/Pipe.kt +++ b/extensions/src/main/kotlin/net/devslash/pipes/Pipe.kt @@ -3,27 +3,35 @@ package net.devslash.pipes import net.devslash.* import java.util.concurrent.ConcurrentLinkedDeque -class Pipe(val acceptor: (HttpResponse, RequestData) -> List, private val split: String? = null) : BasicOutput, RequestDataSupplier { +class Pipe(val acceptor: (HttpResponse, RequestData) -> List, private val split: String? = null) : BasicOutput, RequestDataSupplier { private val storage = ConcurrentLinkedDeque() - override suspend fun getDataForRequest(): RequestData? { + override suspend fun getDataForRequest(): RequestData? { val currentValue = storage.poll() ?: return null val line = if (split != null) { currentValue.split(split) } else listOf(currentValue) - return object : RequestData { + return object : RequestData { override fun getReplacements(): Map { return line.mapIndexed { index, string -> "!" + (index + 1) + "!" to string }.toMap() } + + override fun get(): T { + TODO("Not yet implemented") + } + + override fun accept(v: String): String { + TODO("Not yet implemented") + } } } - override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { - val newResults = acceptor(resp, data) + override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { + val newResults = acceptor(resp, data as RequestData) storage.addAll(newResults) } } diff --git a/extensions/src/main/kotlin/net/devslash/pipes/ResettablePipe.kt b/extensions/src/main/kotlin/net/devslash/pipes/ResettablePipe.kt index 6809d3b..a2011fc 100644 --- a/extensions/src/main/kotlin/net/devslash/pipes/ResettablePipe.kt +++ b/extensions/src/main/kotlin/net/devslash/pipes/ResettablePipe.kt @@ -3,28 +3,24 @@ package net.devslash.pipes import net.devslash.* import java.util.concurrent.atomic.AtomicInteger -class ResettablePipe(val acceptor: (HttpResponse, RequestData) -> List, - private val split: String? = null) : BasicOutput, RequestDataSupplier { +class ResettablePipe( + val acceptor: (HttpResponse, RequestData) -> List +) : BasicOutput, RequestDataSupplier> { private val index = AtomicInteger(0) - private val storage = mutableListOf() + private val storage = mutableListOf() - override suspend fun getDataForRequest(): RequestData? { + override suspend fun getDataForRequest(): RequestData>? { val currentValue = storage.getOrNull(index.getAndIncrement()) ?: return null - - val line = if (split != null) { - currentValue.split(split) - } else listOf(currentValue) - - return ListBasedRequestData(line) + return ListBasedRequestData(listOf(currentValue)) } override fun init() { reset() } - override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { - val newResults = acceptor(resp, data) + override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { + val newResults = acceptor(resp, data as RequestData) storage.addAll(newResults) } diff --git a/extensions/src/main/kotlin/net/devslash/post/Filter.kt b/extensions/src/main/kotlin/net/devslash/post/Filter.kt index 4012176..085300d 100644 --- a/extensions/src/main/kotlin/net/devslash/post/Filter.kt +++ b/extensions/src/main/kotlin/net/devslash/post/Filter.kt @@ -13,7 +13,7 @@ class FilterBuilder { class Filter(private val pred: (HttpResponse) -> Boolean, private val builder: FilterBuilder.() -> Unit) : FullDataAfterHook { - override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { + override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { val current = FilterBuilder().apply(builder) if (pred(resp)) { current.posts.forEach { diff --git a/extensions/src/main/kotlin/net/devslash/post/LogResponse.kt b/extensions/src/main/kotlin/net/devslash/post/LogResponse.kt index 9f07262..bcdf929 100644 --- a/extensions/src/main/kotlin/net/devslash/post/LogResponse.kt +++ b/extensions/src/main/kotlin/net/devslash/post/LogResponse.kt @@ -3,13 +3,13 @@ package net.devslash.post import net.devslash.* class DefaultResponseFormat : OutputFormat { - override fun accept(resp: HttpResponse, rep: RequestData): ByteArray? { + override fun accept(resp: HttpResponse, rep: RequestData): ByteArray? { return "Resp ${resp.url} -> ${resp.statusCode}".toByteArray() } } class LogResponse(private val format: OutputFormat = DefaultResponseFormat()) : FullDataAfterHook { - override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { + override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { format.accept(resp, data)?.let { println(String(it)) } diff --git a/extensions/src/main/kotlin/net/devslash/pre/LogRequest.kt b/extensions/src/main/kotlin/net/devslash/pre/LogRequest.kt index 3f2d3b9..5c58917 100644 --- a/extensions/src/main/kotlin/net/devslash/pre/LogRequest.kt +++ b/extensions/src/main/kotlin/net/devslash/pre/LogRequest.kt @@ -5,7 +5,7 @@ import net.devslash.RequestData import net.devslash.SimpleBeforeHook class LogRequest : SimpleBeforeHook { - override fun accept(req: HttpRequest, data: RequestData) { + override fun accept(req: HttpRequest, data: RequestData) { println("Requesting to ${req.url}") } } diff --git a/extensions/src/main/kotlin/net/devslash/pre/Once.kt b/extensions/src/main/kotlin/net/devslash/pre/Once.kt index 784d350..cae3ab0 100644 --- a/extensions/src/main/kotlin/net/devslash/pre/Once.kt +++ b/extensions/src/main/kotlin/net/devslash/pre/Once.kt @@ -13,10 +13,10 @@ class Once(private val before: BeforeHook) : SessionPersistingBeforeHook { private val flag = AtomicBoolean(false) - override suspend fun accept(sessionManager: SessionManager, + override suspend fun accept(sessionManager: SessionManager, cookieJar: CookieJar, req: HttpRequest, - data: RequestData) { + data: RequestData) { if (flag.compareAndSet(false, true)) { val methods: KClass = before::class diff --git a/extensions/src/main/kotlin/net/devslash/pre/SkipIf.kt b/extensions/src/main/kotlin/net/devslash/pre/SkipIf.kt index bf5b502..e4f3f19 100644 --- a/extensions/src/main/kotlin/net/devslash/pre/SkipIf.kt +++ b/extensions/src/main/kotlin/net/devslash/pre/SkipIf.kt @@ -3,8 +3,8 @@ package net.devslash.pre import net.devslash.RequestData import net.devslash.SkipBeforeHook -class SkipIf(private val predicate: (RequestData) -> Boolean): SkipBeforeHook { - override fun skip(requestData: RequestData): Boolean { +class SkipIf(private val predicate: (RequestData) -> Boolean): SkipBeforeHook { + override fun skip(requestData: RequestData): Boolean { return predicate(requestData) } } diff --git a/extensions/src/test/kotlin/net/devslash/BodyProviderTest.kt b/extensions/src/test/kotlin/net/devslash/BodyProviderTest.kt index 8f642c9..d588c5e 100644 --- a/extensions/src/test/kotlin/net/devslash/BodyProviderTest.kt +++ b/extensions/src/test/kotlin/net/devslash/BodyProviderTest.kt @@ -1,7 +1,6 @@ package net.devslash import net.devslash.util.getCall -import net.devslash.util.requestDataFromList import org.junit.Assert.assertEquals import org.junit.Test @@ -9,7 +8,7 @@ internal class BodyProviderTest { @Test fun testEmpty() { val provider = getBodyProvider( - getCall(), requestDataFromList(listOf("b", "d")) + getCall(), ListBasedRequestData(listOf("b", "d")) ) assertEquals(EmptyBodyProvider::class, provider::class) @@ -18,26 +17,27 @@ internal class BodyProviderTest { @Test fun testWithBody() { val provider = getBodyProvider( - getCall(HttpBody(null, mapOf("a" to listOf("b"), "c" to listOf("d")), null, null)), requestDataFromList() + getCall(HttpBody(null, mapOf("a" to listOf("b"), "c" to listOf("d")), null, null)), ListBasedRequestData( + listOf()) ) - assertEquals(mapOf("a" to listOf("b"), "c" to listOf("d")), (provider as FormBody).get()) + assertEquals(mapOf("a" to listOf("b"), "c" to listOf("d")), (provider as FormBody).get()) } @Test fun testBodyWithReplaceableValues() { val provider = getBodyProvider( - getCall(HttpBody("a=!1!&c=!2!", null, null, null)), requestDataFromList(listOf("b", "d")) + getCall(HttpBody("a=!1!&c=!2!", null, null, null)), ListBasedRequestData(listOf("b", "d")) ) - assertEquals("a=b&c=d", (provider as BasicBodyProvider).get()) + assertEquals("a=b&c=d", (provider as BasicBodyProvider).get()) } @Test fun testParamsWithReplacement() { val provider = getBodyProvider( getCall(HttpBody(null, mapOf("a" to listOf("!1!"), "c" to listOf("!2!")), null, null)), - requestDataFromList(listOf("b", "d")) + ListBasedRequestData(listOf("b", "d")) ) - assertEquals(mapOf("a" to listOf("b"), "c" to listOf("d")), (provider as FormBody).get()) + assertEquals(mapOf("a" to listOf("b"), "c" to listOf("d")), (provider as FormBody).get()) } } diff --git a/extensions/src/test/kotlin/net/devslash/CookieJarTest.kt b/extensions/src/test/kotlin/net/devslash/CookieJarTest.kt index ebd39e8..39a85a4 100644 --- a/extensions/src/test/kotlin/net/devslash/CookieJarTest.kt +++ b/extensions/src/test/kotlin/net/devslash/CookieJarTest.kt @@ -1,6 +1,5 @@ package net.devslash -import net.devslash.util.requestDataFromList import org.hamcrest.CoreMatchers.`is` import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat @@ -16,7 +15,7 @@ internal class CookieJarTest { @Test fun testSingleCookieSet() { jar.accept(responseWithHeaders(mapOf("Set-Cookie" to listOf("A=B")))) - jar.accept(standardRequest, requestDataFromList(listOf())) + jar.accept(standardRequest, ListBasedRequestData(listOf())) assertThat(standardRequest.headers["Cookie"], equalTo(listOf("A=B"))) } @@ -25,7 +24,7 @@ internal class CookieJarTest { fun testMultipleCaseCookieSet() { jar.accept( responseWithHeaders(mapOf("set-Cookie" to listOf("A=B"), "SET-COOKIE" to listOf("C=D")))) - jar.accept(standardRequest, requestDataFromList(listOf())) + jar.accept(standardRequest, ListBasedRequestData(listOf())) assertThat(standardRequest.headers["Cookie"], equalTo(listOf("A=B; C=D"))) @@ -34,7 +33,7 @@ internal class CookieJarTest { @Test fun testSetMultipleOfTheSameKey() { jar.accept( responseWithHeaders(mapOf("set-Cookie" to listOf("A=B"), "SET-COOKIE" to listOf("A=D")))) - jar.accept(standardRequest, requestDataFromList(listOf())) + jar.accept(standardRequest, ListBasedRequestData(listOf())) assertThat(standardRequest.headers["Cookie"], equalTo(listOf("A=D"))) @@ -45,8 +44,8 @@ internal class CookieJarTest { jar.accept(responseWithHeaders(mapOf("Set-Cookie" to listOf("A=B")))) val otherSizeRequest = HttpRequest(HttpMethod.GET, "https://differentDomain.com", EmptyBodyProvider) - jar.accept(otherSizeRequest, requestDataFromList(listOf())) - jar.accept(standardRequest, requestDataFromList(listOf())) + jar.accept(otherSizeRequest, ListBasedRequestData(listOf())) + jar.accept(standardRequest, ListBasedRequestData(listOf())) assertThat(otherSizeRequest.headers["Cookie"], `is`(nullValue())) assertThat(standardRequest.headers["Cookie"], equalTo(listOf("A=B"))) @@ -57,8 +56,8 @@ internal class CookieJarTest { jar.accept(responseWithHeaders(mapOf("Set-Cookie" to listOf("A=B")))) val httpRequest = HttpRequest(HttpMethod.GET, "http://example.com", EmptyBodyProvider) - jar.accept(httpRequest, requestDataFromList(listOf())) - jar.accept(standardRequest, requestDataFromList(listOf("A=B"))) + jar.accept(httpRequest, ListBasedRequestData(listOf())) + jar.accept(standardRequest, ListBasedRequestData(listOf("A=B"))) } private fun responseWithHeaders(headers: Map>, diff --git a/extensions/src/test/kotlin/net/devslash/HttpSessionManagerTest.kt b/extensions/src/test/kotlin/net/devslash/HttpSessionManagerTest.kt index 2c806ca..83c23c0 100644 --- a/extensions/src/test/kotlin/net/devslash/HttpSessionManagerTest.kt +++ b/extensions/src/test/kotlin/net/devslash/HttpSessionManagerTest.kt @@ -47,7 +47,7 @@ internal class HttpSessionManagerTest : ServerTest() { call(address) { after { +object : BasicOutput { - override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { + override fun accept(req: HttpRequest, resp: HttpResponse, data: RequestData) { cookie = resp.headers["set-cookie"]!![0] body = String(resp.body) } diff --git a/extensions/src/test/kotlin/net/devslash/it/HttpBounceTest.kt b/extensions/src/test/kotlin/net/devslash/it/HttpBounceTest.kt index ccf5f86..47c676c 100644 --- a/extensions/src/test/kotlin/net/devslash/it/HttpBounceTest.kt +++ b/extensions/src/test/kotlin/net/devslash/it/HttpBounceTest.kt @@ -1,15 +1,17 @@ package net.devslash.it -import io.ktor.application.call -import io.ktor.http.Headers -import io.ktor.request.receiveText -import io.ktor.response.respondText -import io.ktor.routing.get -import io.ktor.routing.post -import io.ktor.routing.routing -import io.ktor.server.engine.ApplicationEngine +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.request.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.server.engine.* import kotlinx.coroutines.runBlocking import net.devslash.* +import net.devslash.HttpMethod +import net.devslash.data.Capture +import net.devslash.data.ListDataSupplier +import net.devslash.post.LogResponse import net.devslash.pre.SkipIf import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -43,6 +45,27 @@ class HttpBounceTest : ServerTest() { assertEquals("RESULT", bodyResult) } + @Test + fun testDataClassBasedCall() { + data class TClass(@Capture("!2!") val i: Int, val a: Boolean) + runWith { + routing { + get("/2") { + call.respond(HttpStatusCode.OK, "Nice") + } + } + } + + runHttp { + call("$address/!2!") { + data = ListDataSupplier(listOf(TClass(2, true))) {it} + after { + +LogResponse() + } + } + } + } + @Test fun testWithBodyParams() { var sentBody = "" @@ -82,8 +105,9 @@ class HttpBounceTest : ServerTest() { } runHttp { - call(address) { - this.headers = mapOf("A" to listOf(ProvidedValue { r -> "!1!".asReplaceableValue().get(r) + "."}), "C" to listOf("D")) + call>(address) { + this.headers = + mapOf("A" to listOf(ProvidedValue> { it.get()["!1!"]!! + "." }), "C" to listOf("D")) data = SingleUseDataSupplier(mapOf("!1!" to "Hi")) } } @@ -106,7 +130,7 @@ class HttpBounceTest : ServerTest() { runHttp { call(address) { before { - +SkipIf { true } + +SkipIf { true } } } } diff --git a/extensions/src/test/kotlin/net/devslash/outputs/AppendFileTest.kt b/extensions/src/test/kotlin/net/devslash/outputs/AppendFileTest.kt index 0a20190..dc116a6 100644 --- a/extensions/src/test/kotlin/net/devslash/outputs/AppendFileTest.kt +++ b/extensions/src/test/kotlin/net/devslash/outputs/AppendFileTest.kt @@ -14,7 +14,7 @@ internal class AppendFileTest { @Rule @JvmField - public val tmpDir = TemporaryFolder() + val tmpDir = TemporaryFolder() @Test fun testSimpleAppendTest() { @@ -23,10 +23,10 @@ internal class AppendFileTest { appender.accept(getBasicRequest(), getResponseWithBody("abc".toByteArray()), - ListBasedRequestData()) + ListBasedRequestData(listOf())) appender.accept(getBasicRequest(), getResponseWithBody("def".toByteArray()), - ListBasedRequestData()) + ListBasedRequestData(listOf())) assertThat(file.readText(), equalTo("abc\ndef\n")) } diff --git a/extensions/src/test/kotlin/net/devslash/pipes/PipeTest.kt b/extensions/src/test/kotlin/net/devslash/pipes/PipeTest.kt index 7e309b8..c3e6242 100644 --- a/extensions/src/test/kotlin/net/devslash/pipes/PipeTest.kt +++ b/extensions/src/test/kotlin/net/devslash/pipes/PipeTest.kt @@ -2,8 +2,8 @@ package net.devslash.pipes import kotlinx.coroutines.runBlocking import net.devslash.HttpResponse +import net.devslash.ListBasedRequestData import net.devslash.util.getBasicRequest -import net.devslash.util.requestDataFromList import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat import org.junit.Test @@ -13,19 +13,19 @@ internal class PipeTest { @Test fun testPipeStartsEmpty() = runBlocking { - val pipe = Pipe({ _, _ -> listOf("A", "B") }, null) + val pipe = Pipe({ _, _ -> listOf("A", "B") }, null) assertThat(pipe.getDataForRequest(), nullValue()) } @Test fun testPipeSingleCase() = runBlocking { - val pipe = Pipe({ r, _ -> listOf(String(r.body)) }, null) + val pipe = Pipe({ r, _ -> listOf(String(r.body)) }, null) pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), "result".toByteArray()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) val data = pipe.getDataForRequest()!! @@ -36,11 +36,11 @@ internal class PipeTest { @Test fun testPipeSplitsCorrectly() = runBlocking { - val pipe = Pipe({ _, _ -> listOf("a b c") }, " ") + val pipe = Pipe({ _, _ -> listOf("a b c") }, " ") pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), byteArrayOf()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) val data = pipe.getDataForRequest()!! @@ -51,11 +51,11 @@ internal class PipeTest { @Test fun testPipeCanReturnMultipleResults() = runBlocking { val vals = listOf("a", "b", "c") - val pipe = Pipe({ _, _ -> vals }, " ") + val pipe = Pipe({ _, _ -> vals }, " ") pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), byteArrayOf()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) vals.forEach { @@ -65,21 +65,21 @@ internal class PipeTest { @Test fun testPipeAcceptsMultipleAndReturnsInOrder() = runBlocking { - val pipe = Pipe({ r, _ -> listOf(String(r.body)) }, " ") + val pipe = Pipe({ r, _ -> listOf(String(r.body)) }, " ") pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), "a".toByteArray()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), "b".toByteArray()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), "c".toByteArray()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) val values = listOf("a", "b", "c") diff --git a/extensions/src/test/kotlin/net/devslash/pipes/ResettablePipeTest.kt b/extensions/src/test/kotlin/net/devslash/pipes/ResettablePipeTest.kt index 79143ab..3fc1ef5 100644 --- a/extensions/src/test/kotlin/net/devslash/pipes/ResettablePipeTest.kt +++ b/extensions/src/test/kotlin/net/devslash/pipes/ResettablePipeTest.kt @@ -2,8 +2,8 @@ package net.devslash.pipes import kotlinx.coroutines.runBlocking import net.devslash.HttpResponse +import net.devslash.ListBasedRequestData import net.devslash.util.getBasicRequest -import net.devslash.util.requestDataFromList import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat import org.junit.Test @@ -13,19 +13,19 @@ internal class ResettablePipeTest { @Test fun testPipeStartsEmpty() = runBlocking{ - val pipe = ResettablePipe({ _, _ -> listOf("A", "B") }, null) + val pipe = ResettablePipe { _, _ -> listOf("A", "B") } assertThat(pipe.getDataForRequest(), nullValue()) } @Test fun testPipeSingleCase() = runBlocking { - val pipe = ResettablePipe({ r, _ -> listOf(String(r.body)) }, null) + val pipe = ResettablePipe({ r, _ -> listOf(String(r.body)) }) pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), "result".toByteArray()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) val data = pipe.getDataForRequest()!! @@ -36,17 +36,17 @@ internal class ResettablePipeTest { // now reset pipe.reset() val nextData = pipe.getDataForRequest()!! - assertThat(data, equalTo(nextData)) + assertThat(data.get(), equalTo(nextData.get())) } @Test fun testPipeCanReturnMultipleResults() = runBlocking { val vals = listOf("a", "b", "c") - val pipe = Pipe({ _, _ -> vals }, " ") + val pipe = Pipe({ _, _ -> vals }, " ") pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), byteArrayOf()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) vals.forEach { @@ -56,21 +56,21 @@ internal class ResettablePipeTest { @Test fun testPipeAcceptsMultipleAndReturnsInOrder() = runBlocking { - val pipe = Pipe({ r, _ -> listOf(String(r.body)) }, " ") + val pipe = Pipe({ r, _ -> listOf(String(r.body)) }, " ") pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), "a".toByteArray()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), "b".toByteArray()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) pipe.accept( getBasicRequest(), HttpResponse(URL("http://"), 200, mapOf(), "c".toByteArray()), - requestDataFromList(listOf()) + ListBasedRequestData(listOf()) ) val values = listOf("a", "b", "c") diff --git a/extensions/src/test/kotlin/net/devslash/pre/OnceTest.kt b/extensions/src/test/kotlin/net/devslash/pre/OnceTest.kt index d121788..e65a383 100644 --- a/extensions/src/test/kotlin/net/devslash/pre/OnceTest.kt +++ b/extensions/src/test/kotlin/net/devslash/pre/OnceTest.kt @@ -1,23 +1,24 @@ package net.devslash.pre +import io.mockk.mockk import kotlinx.coroutines.runBlocking import net.devslash.* import net.devslash.util.getBasicRequest import net.devslash.util.getCookieJar -import net.devslash.util.getSessionManager -import net.devslash.util.requestDataFromList import org.junit.Assert.assertEquals import org.junit.Assert.fail import org.junit.Test internal class OnceTest { + private val sManager = mockk() + @Test fun testLambda() = runBlocking { var count = 0 val o = Once({ count++; Unit }.toPreHook()) - o.accept(getSessionManager(), getCookieJar(), getBasicRequest(), requestDataFromList(listOf())) + o.accept(sManager, getCookieJar(), getBasicRequest(), ListBasedRequestData(listOf())) assertEquals(1, count) } @@ -33,8 +34,8 @@ internal class OnceTest { } }) - o.accept(getSessionManager(), getCookieJar(), getBasicRequest(), requestDataFromList(listOf())) - o.accept(getSessionManager(), getCookieJar(), getBasicRequest(), requestDataFromList(listOf())) + o.accept(sManager, getCookieJar(), getBasicRequest(), ListBasedRequestData(listOf())) + o.accept(sManager, getCookieJar(), getBasicRequest(), ListBasedRequestData(listOf())) assertEquals(1, count) } @@ -50,10 +51,10 @@ internal class OnceTest { }) try { - o.accept(getSessionManager(), + o.accept(sManager, getCookieJar(), getBasicRequest(), - requestDataFromList(listOf())) + ListBasedRequestData(listOf())) fail("Should have an exception") } catch (e: InvalidHookException) { // ignore @@ -65,15 +66,17 @@ internal class OnceTest { fun testWorksWithComplexHook() = runBlocking { var count = 0 val o = Once(object : SessionPersistingBeforeHook { - override suspend fun accept(sessionManager: SessionManager, - cookieJar: CookieJar, - req: HttpRequest, - data: RequestData) { + override suspend fun accept( + sessionManager: SessionManager, + cookieJar: CookieJar, + req: HttpRequest, + data: RequestData + ) { count++ } }) - o.accept(getSessionManager(), getCookieJar(), getBasicRequest(), requestDataFromList(listOf())) + o.accept(sManager, getCookieJar(), getBasicRequest(), ListBasedRequestData(listOf())) assertEquals(1, count) } diff --git a/extensions/src/test/kotlin/net/devslash/pre/SkipIfTest.kt b/extensions/src/test/kotlin/net/devslash/pre/SkipIfTest.kt index 37a9718..37b4e54 100644 --- a/extensions/src/test/kotlin/net/devslash/pre/SkipIfTest.kt +++ b/extensions/src/test/kotlin/net/devslash/pre/SkipIfTest.kt @@ -8,7 +8,7 @@ internal class SkipIfTest { @Test fun testSkipIf() { - val skip = SkipIf { true } + val skip = SkipIf> { true } val result = skip.skip(ListBasedRequestData(listOf())) assertTrue(result) } diff --git a/extensions/src/test/kotlin/net/devslash/util/TestUtils.kt b/extensions/src/test/kotlin/net/devslash/util/TestUtils.kt index 75f4c44..7533553 100644 --- a/extensions/src/test/kotlin/net/devslash/util/TestUtils.kt +++ b/extensions/src/test/kotlin/net/devslash/util/TestUtils.kt @@ -1,23 +1,8 @@ package net.devslash.util -import io.ktor.client.engine.apache.Apache import net.devslash.* import java.net.URL -fun requestDataFromList(listOf: List? = null): RequestData { - return object : RequestData { - override fun getReplacements(): Map { - if (listOf != null) { - return listOf.mapIndexed { i, p -> - "!${i + 1}!" to p - }.toMap() - } - - return mapOf() - } - } -} - fun getBasicRequest() : HttpRequest { return HttpRequest(HttpMethod.GET, "https://example.com", EmptyBodyProvider) } @@ -26,14 +11,6 @@ fun getCookieJar(): CookieJar { return CookieJar() } -fun getSessionManager(): SessionManager { - return HttpSessionManager(HttpDriver(ConfigBuilder().build()), getSession()) -} - -fun getSession(): Session { - return SessionBuilder().build() -} - fun getResponseWithBody(body: ByteArray) : HttpResponse { return HttpResponse(URL("http://example.com"), 200, mapOf(), body) } @@ -42,7 +19,7 @@ fun getResponse(): HttpResponse { return getResponseWithBody("Body".toByteArray()) } -fun getCall(sup: HttpBody? = null, url: String = "https://example.com") = CallBuilder( +fun getCall(sup: HttpBody? = null, url: String = "https://example.com") = CallBuilder( url ).apply { body = sup diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 23334 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 WDWbsF$p{Gi@ZUasP|4|kdH)CXgbPdn delta 19998 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 43a22ac..4d9ca16 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu Jul 04 19:09:21 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/gradlew b/gradlew index b0d6d0a..4f906e0 100755 --- a/gradlew +++ b/gradlew @@ -7,7 +7,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -82,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -125,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -154,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 15e1ee3..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -5,7 +5,7 @@ @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem -@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/service/build.gradle.kts b/service/build.gradle.kts index 7b83ec1..f961b83 100644 --- a/service/build.gradle.kts +++ b/service/build.gradle.kts @@ -2,6 +2,11 @@ val kotlinVersion: String by project val ktorVersion: String by project val junitVersion: String by project val ktorNettyVersion: String by project +val mockkVersion = "1.9.3" + +kotlin { + explicitApi() +} dependencies { implementation(kotlin("stdlib", kotlinVersion)) @@ -17,6 +22,7 @@ dependencies { implementation("org.apache.httpcomponents:httpclient:4.5") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2") + testImplementation("io.mockk:mockk:$mockkVersion") testImplementation("junit:junit:$junitVersion") testImplementation(project(":test-utils")) testImplementation("io.ktor:ktor-client-mock-jvm:$ktorVersion") diff --git a/service/src/main/kotlin/net/devslash/GenericRequestData.kt b/service/src/main/kotlin/net/devslash/GenericRequestData.kt new file mode 100644 index 0000000..35ec95d --- /dev/null +++ b/service/src/main/kotlin/net/devslash/GenericRequestData.kt @@ -0,0 +1,28 @@ +package net.devslash + +import net.devslash.data.Acceptor +import net.devslash.data.DataClassAcceptor + +/** + * This is a one-shot. Not a data supplier. Therefore this simply has to accept a list and provide it in subsequent + * calls + */ +public class GenericRequestData(private val obj: T) : RequestData { + + override fun get(): T = obj + private val acceptor: Acceptor = visit(obj) + + private fun visit(obj: T): Acceptor { + return when (obj) { + else -> DataClassAcceptor(obj) + } + } + + public override fun getReplacements(): Map { + throw NotImplementedError() + } + + override fun accept(v: String): String { + return acceptor.accept(v) + } +} diff --git a/service/src/main/kotlin/net/devslash/HttpDriver.kt b/service/src/main/kotlin/net/devslash/HttpDriver.kt index 71f7d3a..5e5d7be 100644 --- a/service/src/main/kotlin/net/devslash/HttpDriver.kt +++ b/service/src/main/kotlin/net/devslash/HttpDriver.kt @@ -1,27 +1,20 @@ package net.devslash import com.fasterxml.jackson.databind.ObjectMapper -import io.ktor.client.HttpClient -import io.ktor.client.call.call -import io.ktor.client.engine.apache.Apache -import io.ktor.client.request.forms.FormDataContent -import io.ktor.client.request.headers -import io.ktor.content.ByteArrayContent -import io.ktor.content.TextContent -import io.ktor.http.ContentType -import io.ktor.http.Headers -import io.ktor.http.Parameters -import io.ktor.util.cio.toByteArray +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.apache.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.content.* +import io.ktor.http.* +import io.ktor.util.cio.* import java.net.URL -import kotlin.collections.List -import kotlin.collections.Map import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.forEach -import kotlin.collections.mutableMapOf import kotlin.collections.set -class HttpDriver(config: Config) : AutoCloseable { +internal class HttpDriver(config: Config) : AutoCloseable { private val client = HttpClient(Apache) { engine { @@ -47,14 +40,17 @@ class HttpDriver(config: Config) : AutoCloseable { } when (req.body) { is JsonBody -> { - body = TextContent(mapper.writeValueAsString((req.body as JsonBody).get()), - ContentType.Application.Json) + val text = (req.body as JsonBody).get() + body = TextContent( + mapper.writeValueAsString(text), + ContentType.Application.Json + ) } - is BasicBodyProvider -> { - body = ByteArrayContent((req.body as BasicBodyProvider).get().toByteArray()) + is BasicBodyProvider<*> -> { + body = ByteArrayContent((req.body as BasicBodyProvider<*>).get().toByteArray()) } - is FormBody -> body = FormDataContent(Parameters.build { - val prov = req.body as FormBody + is FormBody<*> -> body = FormDataContent(Parameters.build { + val prov = req.body as FormBody<*> prov.get().forEach { (key, value) -> value.forEach { append(key, it) @@ -81,12 +77,14 @@ class HttpDriver(config: Config) : AutoCloseable { } } - suspend fun mapResponse(request: io.ktor.client.response.HttpResponse): HttpResponse { + internal suspend fun mapResponse(request: io.ktor.client.response.HttpResponse): HttpResponse { val response = request.call.response - return HttpResponse(URL(request.call.request.url.toString()), - response.status.value, - mapHeaders(response.headers), - response.content.toByteArray()) + return HttpResponse( + URL(request.call.request.url.toString()), + response.status.value, + mapHeaders(response.headers), + response.content.toByteArray() + ) } private fun mapHeaders(headers: Headers): Map> { diff --git a/service/src/main/kotlin/net/devslash/HttpSessionManager.kt b/service/src/main/kotlin/net/devslash/HttpSessionManager.kt index 3812485..a928082 100644 --- a/service/src/main/kotlin/net/devslash/HttpSessionManager.kt +++ b/service/src/main/kotlin/net/devslash/HttpSessionManager.kt @@ -7,9 +7,9 @@ import java.util.concurrent.Executors import java.util.concurrent.Semaphore import kotlin.coroutines.CoroutineContext -typealias Contents = Pair +private typealias Contents = Pair> -class HttpSessionManager(val engine: HttpDriver, private val session: Session) : SessionManager { +internal class HttpSessionManager(val engine: HttpDriver, private val session: Session) : SessionManager { private val semaphore = Semaphore(0) private var lastCall = 0L @@ -24,38 +24,48 @@ class HttpSessionManager(val engine: HttpDriver, private val session: Session) : } } - private suspend fun produceHttp(call: Call, - jar: CookieJar, channel: Channel>) { + private suspend fun produceHttp( + call: Call, + jar: CookieJar, channel: Channel>> + ) { val dataSupplier = handleNoSupplier(call.dataSupplier) while (true) { - val data = dataSupplier.getDataForRequest() ?: break + // This is _relatively_ safe because the type T is captured in the build of the call. Someone using the DSL + // can't stuff this up + @Suppress("UNCHECKED_CAST") + val data = dataSupplier.getDataForRequest() as RequestData? ?: break val req = mapHttpRequest(call, data) // Call the prerequesites val preRequest = call.beforeHooks.toMutableList() preRequest.add(jar) val shouldSkip = - preRequest.filter { it is SkipBeforeHook }.any { (it as SkipBeforeHook).skip(data) } + preRequest.filter { it is SkipBeforeHook<*> }.any { (it as SkipBeforeHook).skip(data) } if (shouldSkip) continue preRequest.forEach { when (it) { is SimpleBeforeHook -> it.accept(req, data) - is SessionPersistingBeforeHook -> it.accept(sessionManager, jar, req, data) + is SessionPersistingBeforeHook -> it.accept( + sessionManager, + jar, + req, + data + ) } } call.headers?.forEach { entry -> entry.value.forEach { val s = when (it) { is StrValue -> it.value - is ProvidedValue -> it.lambda(data) + is ProvidedValue<*> -> (it as ProvidedValue).lambda(data) } req.addHeader(entry.key, s) } } - if(channel.offer(Envelope(Pair(req, data)))) { + if (channel.offer(Envelope(Pair(req, data)))) { continue } else { channel.send(Envelope(Pair(req, data))) @@ -64,14 +74,15 @@ class HttpSessionManager(val engine: HttpDriver, private val session: Session) : channel.close() } - override fun call(call: Call) = call(call, CookieJar()) + override fun call(call: Call) = call(call, CookieJar()) - override fun call(call: Call, jar: CookieJar) = runBlocking { + + override fun call(call: Call, jar: CookieJar) = runBlocking { // Okay, so in here we're going to do the one to many calls we have to to get this to run. - val channel: Channel>> = - Channel(session.concurrency * 2) + val channel: Channel>>> = + Channel(session.concurrency * 2) val produceThreadPool = System.getProperty("PRODUCE_THREAD_POOL_SIZE")?.toInt() - ?: Runtime.getRuntime().availableProcessors() + ?: Runtime.getRuntime().availableProcessors() val produceExecutor = Executors.newFixedThreadPool(produceThreadPool) val produceDispatcher = produceExecutor.asCoroutineDispatcher() launch(produceDispatcher) { produceHttp(call, jar, channel) } @@ -99,11 +110,13 @@ class HttpSessionManager(val engine: HttpDriver, private val session: Session) : val jobs = mutableListOf() val concurrency = if (hasDelay) 1 else session.concurrency repeat(concurrency) { - jobs += launchHttpProcessor(call, - limiter, - afterRequest, - channel, - dispatcher) + jobs += launchHttpProcessor( + call, + limiter, + afterRequest, + channel, + dispatcher + ) } jobs.joinAll() produceExecutor.shutdownNow() @@ -111,11 +124,13 @@ class HttpSessionManager(val engine: HttpDriver, private val session: Session) : Unit } - private fun CoroutineScope.launchHttpProcessor(call: Call, - rateLimiter: AcquiringRateLimiter, - afterRequest: List, - channel: Channel>>, - dispatcher: CoroutineContext) = launch(dispatcher) { + private fun CoroutineScope.launchHttpProcessor( + call: Call, + rateLimiter: AcquiringRateLimiter, + afterRequest: List, + channel: Channel>>>, + dispatcher: CoroutineContext + ) = launch(dispatcher) { for (next in channel) { // ensure that this is a valid request if (next.shouldProceed()) { @@ -140,14 +155,16 @@ class HttpSessionManager(val engine: HttpDriver, private val session: Session) : } } - private suspend fun handleFailure(call: Call, - channel: Channel>>, - next: Envelope>, - request: Failure) { + private suspend fun handleFailure( + call: Call, + channel: Channel>>>, + next: Envelope>>, + request: Failure + ) { call.onError?.let { when (it) { is ChannelReceiving<*> -> { - (it as ChannelReceiving).accept(channel, next, request.err) + (it as ChannelReceiving>).accept(channel, next, request.err) } else -> { throw request.err @@ -156,7 +173,7 @@ class HttpSessionManager(val engine: HttpDriver, private val session: Session) : } } - private fun mapHttpRequest(call: Call, data: RequestData): HttpRequest { + private fun mapHttpRequest(call: Call, data: RequestData): HttpRequest { val url = getUrlProvider(call, data) val body = getBodyProvider(call, data) val currentUrl = url.get() diff --git a/service/src/main/kotlin/net/devslash/ListBasedRequestData.kt b/service/src/main/kotlin/net/devslash/ListBasedRequestData.kt index 331e5f3..caa1913 100644 --- a/service/src/main/kotlin/net/devslash/ListBasedRequestData.kt +++ b/service/src/main/kotlin/net/devslash/ListBasedRequestData.kt @@ -1,24 +1,30 @@ package net.devslash -class ListBasedRequestData(private val parts: List = listOf()) : RequestData { - override fun getReplacements(): Map { - return parts.mapIndexed { index, string -> - "!" + (index + 1) + "!" to string - }.toMap() - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false +/** + * This is a one-shot. Not a data supplier. Therefore this simply has to accept a list and provide it in subsequent + * calls + */ +public class ListBasedRequestData> private constructor( + private val parts: T, +) : RequestData { - other as ListBasedRequestData - if (parts != other.parts) return false + public companion object { + public operator fun invoke(list: List): ListBasedRequestData> { + return ListBasedRequestData(list); + } + } - return true + override fun getReplacements(): Map { + return parts.mapIndexed { index, obj -> + "!" + (index + 1) + "!" to obj.toString() + }.toMap() } - override fun hashCode(): Int { - return parts.hashCode() + override fun get(): T = parts + override fun accept(v: String): String { + var x = v + parts.forEachIndexed { index, e -> x = x.replace("!${index + 1}!", e.toString()) } + return x } } diff --git a/service/src/main/kotlin/net/devslash/ListStringAcceptor.kt b/service/src/main/kotlin/net/devslash/ListStringAcceptor.kt new file mode 100644 index 0000000..a17b35b --- /dev/null +++ b/service/src/main/kotlin/net/devslash/ListStringAcceptor.kt @@ -0,0 +1,35 @@ +package net.devslash.data + +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +public fun interface Acceptor { + public fun accept(v: String): String +} + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +public annotation class Capture(val replace: String) + +public val NoOpAcceptor: Acceptor = Acceptor { it } + +private abstract class TypeReference : Comparable> { + val type: Type = + (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] + + override fun compareTo(other: TypeReference) = 0 +} + +public fun DataClassAcceptor(v: T): Acceptor { + val type = v!!::class.java + val fieldPairs = type.declaredFields.filter { it.getAnnotation(Capture::class.java) != null } + .map { it to it.getDeclaredAnnotation(Capture::class.java).replace } + return Acceptor { + var ret = it + fieldPairs.forEach { pair -> + val backer = v!!::class.members.find { it.name == pair.first.name }!! + ret = it.replace(pair.second, backer.call(v).toString()) + } + ret + } +} \ No newline at end of file diff --git a/service/src/main/kotlin/net/devslash/Providers.kt b/service/src/main/kotlin/net/devslash/Providers.kt index acf59b2..fac657a 100644 --- a/service/src/main/kotlin/net/devslash/Providers.kt +++ b/service/src/main/kotlin/net/devslash/Providers.kt @@ -2,11 +2,11 @@ package net.devslash import java.util.concurrent.atomic.AtomicBoolean -interface URLProvider { +internal interface URLProvider { fun get(): String } -fun handleNoSupplier(data: RequestDataSupplier?): RequestDataSupplier { +internal fun handleNoSupplier(data: RequestDataSupplier<*>?): RequestDataSupplier<*> { if (data != null) { data.init() return data @@ -16,15 +16,26 @@ fun handleNoSupplier(data: RequestDataSupplier?): RequestDataSupplier { return SingleUseDataSupplier() } -class SingleUseDataSupplier(private val supply: Map = mapOf()) : RequestDataSupplier { +public class SingleUseDataSupplier(private val supply: Map = mapOf()) : RequestDataSupplier> { private val first = AtomicBoolean(true) - override suspend fun getDataForRequest(): RequestData? { + override suspend fun getDataForRequest(): RequestData>? { if (first.compareAndSet(true, false)) { - return object : RequestData { + return object : RequestData> { override fun getReplacements(): Map { return supply } + + override fun get(): Map { + return supply + } + + override fun accept(v: String): String { + supply.forEach { (key, value) -> + v.replace(key, value) + } + return v + } } } return null diff --git a/service/src/main/kotlin/net/devslash/RateLimiter.kt b/service/src/main/kotlin/net/devslash/RateLimiter.kt index 8be6c51..e1042ce 100644 --- a/service/src/main/kotlin/net/devslash/RateLimiter.kt +++ b/service/src/main/kotlin/net/devslash/RateLimiter.kt @@ -15,7 +15,7 @@ import kotlin.math.max * This means that the rate limiter effectively has no memory of the past, beyond the call before. This means that if * there is inconsistency with how fast requests are performed, then the rate limit may be over-restrictive. */ -class AcquiringRateLimiter(private val rateLimitOptions: RateLimitOptions, private val clock: Clock = Clock.systemUTC()) { +internal class AcquiringRateLimiter(private val rateLimitOptions: RateLimitOptions, private val clock: Clock = Clock.systemUTC()) { private var lastRelease = Instant.ofEpochMilli(0) // This equals how many milliseconds it takes to release a ticket private val qps = rateLimitOptions.duration.toMillis() / max(1, rateLimitOptions.count) diff --git a/service/src/main/kotlin/net/devslash/Runner.kt b/service/src/main/kotlin/net/devslash/Runner.kt index aed5485..ac0d049 100644 --- a/service/src/main/kotlin/net/devslash/Runner.kt +++ b/service/src/main/kotlin/net/devslash/Runner.kt @@ -5,36 +5,36 @@ package net.devslash * Use `0` to specify infinite. * Negative value mean to use the system's default value. */ -class ConfigBuilder { +public class ConfigBuilder { /** * Max time between TCP packets - default 10 seconds. */ - var followRedirects: Boolean = false + public var followRedirects: Boolean = false /** * Max time to establish an HTTP connection - default 10 seconds. */ - var socketTimeout: Int = 10_000 + public var socketTimeout: Int = 10_000 /** * Max time to establish an HTTP connection - default 10 seconds. */ - var connectTimeout: Int = 20_000 + public var connectTimeout: Int = 20_000 /** * Max time for the connection manager to start a request - 20 seconds. */ - var connectionRequestTimeout = 20_000 + public var connectionRequestTimeout: Int = 20_000 - fun build(): Config { + internal fun build(): Config { return Config(followRedirects, socketTimeout, connectTimeout, connectionRequestTimeout) } } -data class Config(val followRedirects: Boolean, val socketTimeout: Int, val connectTimeout: Int, val connectionRequestTimeout: Int) +internal data class Config(val followRedirects: Boolean, val socketTimeout: Int, val connectTimeout: Int, val connectionRequestTimeout: Int) -fun runHttp(block: SessionBuilder.() -> Unit) { +public fun runHttp(block: SessionBuilder.() -> Unit) { return runHttp({}, block) } -fun runHttp(config: ConfigBuilder.() -> Unit, block: SessionBuilder.() -> Unit) { +public fun runHttp(config: ConfigBuilder.() -> Unit, block: SessionBuilder.() -> Unit) { return runHttp(HttpDriver(ConfigBuilder().apply(config).build()), block) } diff --git a/service/src/main/kotlin/net/devslash/UrlProviders.kt b/service/src/main/kotlin/net/devslash/UrlProviders.kt index 0f630df..b8545d6 100644 --- a/service/src/main/kotlin/net/devslash/UrlProviders.kt +++ b/service/src/main/kotlin/net/devslash/UrlProviders.kt @@ -1,12 +1,14 @@ package net.devslash -private class OverwrittenUrlProvider(private val url: String, - private val replacements: RequestData) : URLProvider { +private class OverwrittenUrlProvider( + private val url: String, + private val replacements: RequestData +) : URLProvider { override fun get(): String { - return url.asReplaceableValue().get(replacements) + return replacements.accept(url) } } -fun getUrlProvider(call: Call, data: RequestData): URLProvider { +internal fun getUrlProvider(call: Call, data: RequestData): URLProvider { return OverwrittenUrlProvider(call.url, data) } diff --git a/service/src/test/kotlin/net/devslash/DefaultHeaderTest.kt b/service/src/test/kotlin/net/devslash/DefaultHeaderTest.kt index ab45d61..8d83962 100644 --- a/service/src/test/kotlin/net/devslash/DefaultHeaderTest.kt +++ b/service/src/test/kotlin/net/devslash/DefaultHeaderTest.kt @@ -8,7 +8,7 @@ class DefaultHeaderTest { @Test fun testIfNoHeaderSetThenUserAgentExists() { - val call = CallBuilder("http://example.com").build() + val call = CallBuilder("http://example.com").build() assertThat(call.headers, equalTo(mapOf>("User-Agent" to listOf(StrValue("FetchDSL (Apache-HttpAsyncClient + Kotlin, version not set)"))))) @@ -16,7 +16,7 @@ class DefaultHeaderTest { @Test fun testIfUserAgentSetItIsNotOverwritten() { - val call = CallBuilder("http://example.com").apply { + val call = CallBuilder("http://example.com").apply { headers = mapOf>("User-Agent" to listOf("OVERRIDE")) }.build() diff --git a/extensions/src/test/kotlin/net/devslash/DelayTest.kt b/service/src/test/kotlin/net/devslash/DelayTest.kt similarity index 58% rename from extensions/src/test/kotlin/net/devslash/DelayTest.kt rename to service/src/test/kotlin/net/devslash/DelayTest.kt index 2c0a9fb..83a4cfe 100644 --- a/extensions/src/test/kotlin/net/devslash/DelayTest.kt +++ b/service/src/test/kotlin/net/devslash/DelayTest.kt @@ -3,10 +3,14 @@ package net.devslash import io.ktor.client.response.HttpResponse import io.mockk.coEvery import io.mockk.mockk -import net.devslash.data.ListDataSupplier -import net.devslash.util.getResponseWithBody import org.junit.Assert.assertTrue import org.junit.Test +import java.net.URL +import java.util.concurrent.atomic.AtomicInteger + +fun getResponseWithBody(body: ByteArray): net.devslash.HttpResponse { + return HttpResponse(URL("http://example.com"), 200, mapOf(), body) +} class DelayTest { @@ -25,7 +29,16 @@ class DelayTest { HttpSessionManager(engine, SessionBuilder().apply { delay = 30 call("http://example.org") { - data = ListDataSupplier(listOf(1, 2)) + data = object : RequestDataSupplier> { + val count = AtomicInteger(0) + override suspend fun getDataForRequest(): RequestData>? { + if (count.incrementAndGet() > 2) { + return null + } + return ListBasedRequestData(listOf()) + } + + } } }.build()).run() diff --git a/service/src/test/kotlin/net/devslash/ListBasedRequestDataTest.kt b/service/src/test/kotlin/net/devslash/ListBasedRequestDataTest.kt index c17da32..adcb24c 100644 --- a/service/src/test/kotlin/net/devslash/ListBasedRequestDataTest.kt +++ b/service/src/test/kotlin/net/devslash/ListBasedRequestDataTest.kt @@ -8,7 +8,7 @@ internal class ListBasedRequestDataTest { @Test fun testEmptyRequestData() { - val data = ListBasedRequestData(emptyList()) + val data = ListBasedRequestData(emptyList()) assertThat(data.getReplacements(), equalTo(emptyMap())) } diff --git a/service/src/test/kotlin/net/devslash/TestUtils.kt b/service/src/test/kotlin/net/devslash/TestUtils.kt new file mode 100644 index 0000000..ef2a91d --- /dev/null +++ b/service/src/test/kotlin/net/devslash/TestUtils.kt @@ -0,0 +1,40 @@ +package net.devslash + +fun requestDataFromList(listOf: List? = null): RequestData { + return object : RequestData { + override fun getReplacements(): Map { + if (listOf != null) { + return listOf.mapIndexed { i, p -> + "!${i + 1}!" to p + }.toMap() + } + + return mapOf() + } + + override fun get(): String { + TODO("Not yet implemented") + } + + override fun accept(v: String): String { + val replacements = getReplacements() + var copy = v + replacements.forEach { (key, value) -> copy = copy.replace(key, value) } + return copy + } + } +} + +fun getBasicRequest(): HttpRequest { + return HttpRequest(HttpMethod.GET, "https://example.com", EmptyBodyProvider) +} + +fun getCookieJar(): CookieJar { + return CookieJar() +} + +internal fun getCall(sup: HttpBody? = null, url: String = "https://example.com") = CallBuilder( + url +).apply { + body = sup +}.build() diff --git a/extensions/src/test/kotlin/net/devslash/UrlProvidersTest.kt b/service/src/test/kotlin/net/devslash/UrlProvidersTest.kt similarity index 87% rename from extensions/src/test/kotlin/net/devslash/UrlProvidersTest.kt rename to service/src/test/kotlin/net/devslash/UrlProvidersTest.kt index cd35b94..6b91a70 100644 --- a/extensions/src/test/kotlin/net/devslash/UrlProvidersTest.kt +++ b/service/src/test/kotlin/net/devslash/UrlProvidersTest.kt @@ -1,7 +1,5 @@ package net.devslash -import net.devslash.util.getCall -import net.devslash.util.requestDataFromList import org.junit.Assert.assertEquals import org.junit.Test