From 313f7fdac7e15683290800a78b4af038372879fe Mon Sep 17 00:00:00 2001 From: Michal Moczulski Date: Mon, 17 Jan 2022 21:01:39 +0100 Subject: [PATCH 1/9] Add Kotlin 1.6.10 --- build.gradle | 4 ++++ ok2curl/build.gradle | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 61aed67..7ed03ed 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,17 @@ buildscript { + ext.kotlin_version = '1.6.10' + repositories { google() mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } + dependencies { classpath 'com.android.tools.build:gradle:7.0.4' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/ok2curl/build.gradle b/ok2curl/build.gradle index 7e6a881..6fe6583 100644 --- a/ok2curl/build.gradle +++ b/ok2curl/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'java' +apply plugin: 'kotlin' apply plugin: 'maven-publish' ext { @@ -12,8 +12,20 @@ apply from: "${rootProject.projectDir}/scripts/publish-module.gradle" targetCompatibility = '1.8' sourceCompatibility = '1.8' +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.3' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:2.23.0' From 35550e0dfbe9490a1ba70dd5d23dcb6a46a178ee Mon Sep 17 00:00:00 2001 From: Michal Moczulski Date: Mon, 31 Jan 2022 21:29:08 +0100 Subject: [PATCH 2/9] Convert library code to Kotlin --- .../com/moczul/ok2curl/CommandComponent.kt | 16 ++ .../ok2curl/ConfigurableCurlBuilder.java | 86 -------- .../java/com/moczul/ok2curl/Configuration.kt | 12 ++ .../java/com/moczul/ok2curl/CurlBuilder.java | 153 -------------- .../moczul/ok2curl/CurlCommandGenerator.kt | 111 ++++++++++ .../com/moczul/ok2curl/CurlInterceptor.java | 111 ---------- .../com/moczul/ok2curl/CurlInterceptor.kt | 23 +++ .../src/main/java/com/moczul/ok2curl/Flags.kt | 65 ++++++ .../main/java/com/moczul/ok2curl/Header.java | 39 ---- .../main/java/com/moczul/ok2curl/Header.kt | 3 + .../java/com/moczul/ok2curl/LimitedSink.java | 44 ---- .../java/com/moczul/ok2curl/LimitedSink.kt | 42 ++++ .../main/java/com/moczul/ok2curl/Options.java | 68 ------- .../java/com/moczul/ok2curl/StringUtil.java | 23 --- .../com/moczul/ok2curl/logger/Loggable.java | 6 - .../java/com/moczul/ok2curl/logger/Logger.kt | 5 + ...{HeaderModifier.java => HeaderModifier.kt} | 13 +- .../com/moczul/ok2curl/CookieHandlerTest.java | 130 ------------ .../com/moczul/ok2curl/CookieHandlerTest.kt | 119 +++++++++++ .../com/moczul/ok2curl/CurlBuilderTest.java | 123 ------------ .../ok2curl/CurlCommandGeneratorTest.kt | 190 ++++++++++++++++++ .../test/java/com/moczul/ok2curl/FlagsTest.kt | 65 ++++++ .../com/moczul/ok2curl/LimitedSinkTest.java | 72 ------- .../com/moczul/ok2curl/LimitedSinkTest.kt | 67 ++++++ .../java/com/moczul/ok2curl/OptionsTest.java | 58 ------ .../com/moczul/ok2curl/util/FakeLogger.java | 11 - .../ok2curl/util/HeaderModifierTest.java | 84 -------- .../moczul/ok2curl/util/HeaderModifierTest.kt | 89 ++++++++ .../java/com/moczul/sample/AndroidLogger.java | 8 +- 29 files changed, 818 insertions(+), 1018 deletions(-) create mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/CommandComponent.kt delete mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/ConfigurableCurlBuilder.java create mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/Configuration.kt delete mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/CurlBuilder.java create mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/CurlCommandGenerator.kt delete mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.java create mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.kt create mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/Flags.kt delete mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/Header.java create mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/Header.kt delete mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.java create mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt delete mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/Options.java delete mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/StringUtil.java delete mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/logger/Loggable.java create mode 100644 ok2curl/src/main/java/com/moczul/ok2curl/logger/Logger.kt rename ok2curl/src/main/java/com/moczul/ok2curl/modifier/{HeaderModifier.java => HeaderModifier.kt} (65%) delete mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.java create mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.kt delete mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/CurlBuilderTest.java create mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/CurlCommandGeneratorTest.kt create mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/FlagsTest.kt delete mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/LimitedSinkTest.java create mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/LimitedSinkTest.kt delete mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/OptionsTest.java delete mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/util/FakeLogger.java delete mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/util/HeaderModifierTest.java create mode 100644 ok2curl/src/test/java/com/moczul/ok2curl/util/HeaderModifierTest.kt diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/CommandComponent.kt b/ok2curl/src/main/java/com/moczul/ok2curl/CommandComponent.kt new file mode 100644 index 0000000..dadc7c8 --- /dev/null +++ b/ok2curl/src/main/java/com/moczul/ok2curl/CommandComponent.kt @@ -0,0 +1,16 @@ +package com.moczul.ok2curl + +sealed class CommandComponent { + + object Curl : CommandComponent() + object Flags : CommandComponent() + object Method : CommandComponent() + object Header : CommandComponent() + object Body : CommandComponent() + object Url : CommandComponent() + + companion object { + @JvmField + val DEFAULT = listOf(Curl, Flags, Method, Header, Body, Url) + } +} \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/ConfigurableCurlBuilder.java b/ok2curl/src/main/java/com/moczul/ok2curl/ConfigurableCurlBuilder.java deleted file mode 100644 index b1a575f..0000000 --- a/ok2curl/src/main/java/com/moczul/ok2curl/ConfigurableCurlBuilder.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.moczul.ok2curl; - -import com.moczul.ok2curl.modifier.HeaderModifier; - -import java.util.ArrayList; -import java.util.List; - -import okhttp3.Request; - -import static com.moczul.ok2curl.StringUtil.join; - -public class ConfigurableCurlBuilder extends CurlBuilder { - - public static final int PART_CURL = 0; - public static final int PART_OPTIONS = 1; - public static final int PART_METHOD = 2; - public static final int PART_HEADERS = 3; - public static final int PART_CONTENT_TYPE = 4; - public static final int PART_BODY = 5; - public static final int PART_URL = 6; - protected static final int[] DEFAULT_PARTS_ORDER = new int[]{PART_CURL, PART_OPTIONS, PART_METHOD, PART_HEADERS, PART_CONTENT_TYPE, PART_BODY, PART_URL}; - - protected final int[] partsOrder; - - public ConfigurableCurlBuilder(Request request, long limit, List headerModifiers, Options options, String delimiter) { - this(request, limit, headerModifiers, options, delimiter, DEFAULT_PARTS_ORDER); - } - - public ConfigurableCurlBuilder(Request request, long limit, List headerModifiers, Options options, String delimiter, int[] partsOrder) { - super(request, limit, headerModifiers, options, delimiter); - this.partsOrder = partsOrder; - } - - @Override - public String build() { - List parts = new ArrayList<>(); - for (int part : partsOrder) { - switch (part) { - case PART_CURL: addCurl(parts); break; - case PART_OPTIONS: addOptions(parts); break; - case PART_METHOD: addMethod(parts); break; - case PART_HEADERS: addHeaders(parts); break; - case PART_CONTENT_TYPE: addContentType(parts); break; - case PART_BODY: addBody(parts); break; - case PART_URL: addUrl(parts); break; - } - } - - return join(delimiter, parts); - } - - private void addCurl(List parts) { - parts.add("curl"); - } - - private void addOptions(List parts) { - parts.addAll(options); - } - - private void addMethod(List parts) { - parts.add(String.format(FORMAT_METHOD, method.toUpperCase())); - } - - private void addHeaders(List parts) { - for (Header header : headers) { - final String headerPart = String.format(FORMAT_HEADER, header.name(), header.value()); - parts.add(headerPart); - } - } - - private void addContentType(List parts) { - if (contentType != null && !containsName(CONTENT_TYPE, headers)) { - parts.add(String.format(FORMAT_HEADER, CONTENT_TYPE, contentType)); - } - } - - private void addBody(List parts) { - if (body != null) { - parts.add(String.format(FORMAT_BODY, body)); - } - } - - private void addUrl(List parts) { - parts.add(String.format(FORMAT_URL, url)); - } -} diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/Configuration.kt b/ok2curl/src/main/java/com/moczul/ok2curl/Configuration.kt new file mode 100644 index 0000000..51430bc --- /dev/null +++ b/ok2curl/src/main/java/com/moczul/ok2curl/Configuration.kt @@ -0,0 +1,12 @@ +package com.moczul.ok2curl + + +import com.moczul.ok2curl.modifier.HeaderModifier + +class Configuration @JvmOverloads constructor( + val headerModifiers: List = emptyList(), + val components: List = CommandComponent.DEFAULT, + val flags: Flags = Flags.EMPTY, + val limit: Long = 1024L * 1024L, + val delimiter: String = " " +) \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/CurlBuilder.java b/ok2curl/src/main/java/com/moczul/ok2curl/CurlBuilder.java deleted file mode 100644 index 9bb98cf..0000000 --- a/ok2curl/src/main/java/com/moczul/ok2curl/CurlBuilder.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.moczul.ok2curl; - -import com.moczul.ok2curl.modifier.HeaderModifier; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okio.Buffer; -import okio.BufferedSink; -import okio.Okio; - -import static com.moczul.ok2curl.StringUtil.join; - -public class CurlBuilder { - - protected static final String FORMAT_HEADER = "-H \"%1$s:%2$s\""; - protected static final String FORMAT_METHOD = "-X %1$s"; - protected static final String FORMAT_BODY = "-d '%1$s'"; - protected static final String FORMAT_URL = "\"%1$s\""; - protected static final String CONTENT_TYPE = "Content-Type"; - - protected final String url; - protected final String method; - protected final String contentType; - protected final String body; - protected final List options; - protected final List
headers; - protected final String delimiter; - - public CurlBuilder(Request request) { - this(request, -1L, Collections.emptyList(), Options.EMPTY); - } - - public CurlBuilder(Request request, long limit, List headerModifiers, Options options) { - this(request, limit, headerModifiers, options, " "); - } - - public CurlBuilder(Request request, long limit, List headerModifiers, Options options, String delimiter) { - this.url = request.url().toString(); - this.method = request.method(); - this.options = Collections.unmodifiableList(options.list()); - this.delimiter = delimiter; - final RequestBody body = request.body(); - if (body != null) { - this.contentType = getContentType(body); - this.body = getBodyAsString(body, limit); - } else { - this.contentType = null; - this.body = null; - } - - final Headers headers = request.headers(); - final List
modifiableHeaders = new LinkedList<>(); - for (int i = 0; i < headers.size(); i++) { - final Header header = new Header(headers.name(i), headers.value(i)); - final Header modifiedHeader = modifyHeader(header, headerModifiers); - if (modifiedHeader != null) { - modifiableHeaders.add(modifiedHeader); - } - } - this.headers = Collections.unmodifiableList(modifiableHeaders); - } - - private Header modifyHeader(Header header, List headerModifiers) { - for (HeaderModifier modifier : headerModifiers) { - if (modifier.matches(header)) { - return modifier.modify(header); - } - } - - return header; - } - - private String getContentType(RequestBody body) { - final MediaType mediaType = body.contentType(); - if (mediaType != null) { - return mediaType.toString(); - } - - return null; - } - - private String getBodyAsString(RequestBody body, long limit) { - try { - final Buffer sink = new Buffer(); - - final MediaType mediaType = body.contentType(); - final Charset charset = getCharset(mediaType); - - if (limit > 0) { - final BufferedSink buffer = Okio.buffer(new LimitedSink(sink, limit)); - body.writeTo(buffer); - buffer.flush(); - } else { - body.writeTo(sink); - } - - return sink.readString(charset); - } catch (IOException e) { - return "Error while reading body: " + e.toString(); - } - } - - private Charset getCharset(MediaType mediaType) { - if (mediaType != null) { - return mediaType.charset(Charset.defaultCharset()); - } - - return Charset.defaultCharset(); - } - - public String build() { - List parts = new ArrayList<>(); - parts.add("curl"); - parts.addAll(options); - parts.add(String.format(FORMAT_METHOD, method.toUpperCase())); - - for (Header header : headers) { - final String headerPart = String.format(FORMAT_HEADER, header.name(), header.value()); - parts.add(headerPart); - } - - if (contentType != null && !containsName(CONTENT_TYPE, headers)) { - parts.add(String.format(FORMAT_HEADER, CONTENT_TYPE, contentType)); - } - - if (body != null) { - parts.add(String.format(FORMAT_BODY, body)); - } - - parts.add(String.format(FORMAT_URL, url)); - - return join(delimiter, parts); - } - - protected boolean containsName(String name, List
headers) { - for (Header header : headers) { - if (header.name().equals(name)) { - return true; - } - } - - return false; - } -} diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/CurlCommandGenerator.kt b/ok2curl/src/main/java/com/moczul/ok2curl/CurlCommandGenerator.kt new file mode 100644 index 0000000..e7c85f7 --- /dev/null +++ b/ok2curl/src/main/java/com/moczul/ok2curl/CurlCommandGenerator.kt @@ -0,0 +1,111 @@ +package com.moczul.ok2curl + +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody +import okio.Buffer +import okio.buffer +import java.io.IOException +import java.nio.charset.Charset +import java.util.Locale + +class CurlCommandGenerator(private val configuration: Configuration) { + + fun generate(request: Request): String { + return configuration.components + .flatMap { generateCommandComponent(it, request) } + .joinToString(separator = configuration.delimiter) + } + + private fun generateCommandComponent( + commandComponent: CommandComponent, + request: Request + ): List { + return when (commandComponent) { + CommandComponent.Curl -> listOf("curl") + CommandComponent.Url -> generateUrl(request.url) + CommandComponent.Flags -> generateFlags() + CommandComponent.Body -> generateBody(request.body) + CommandComponent.Method -> generateMethod(request.method) + CommandComponent.Header -> generateHeaders(request.headers, request.body) + } + } + + private fun generateBody(body: RequestBody?): List { + return if (body != null) { + val bodyString = getBodyAsString(body) + listOf(bodyString) + } else { + emptyList() + } + } + + private fun getBodyAsString(body: RequestBody) = try { + val sink = Buffer() + val mediaType = body.contentType() + val charset: Charset = getCharset(mediaType) + if (configuration.limit > 0) { + val buffer = LimitedSink(sink, configuration.limit).buffer() + body.writeTo(buffer) + buffer.flush() + } else { + body.writeTo(sink) + } + FORMAT_BODY.format(sink.readString(charset)) + } catch (e: IOException) { + "Error while reading body: $e" + } + + private fun generateHeaders(headers: Headers, body: RequestBody?): List { + return headers + .map { Header(name = it.first, value = it.second) } + .mapNotNull { header -> modifyHeader(header) } + .applyContentTypeHeader(body) + .map { header -> FORMAT_HEADER.format(header.name, header.value) } + } + + + private fun modifyHeader(header: Header): Header? { + val modifier = configuration.headerModifiers.find { it.matches(header) } + return if (modifier != null) { + modifier.modify(header) + } else { + header + } + } + + private fun generateMethod(method: String): List { + return listOf(FORMAT_METHOD.format(method.uppercase(Locale.getDefault()))) + } + + private fun generateFlags(): List = configuration.flags.list() + + private fun generateUrl(url: HttpUrl): List = listOf(FORMAT_URL.format(url.toString())) + + private fun getCharset(mediaType: MediaType?): Charset { + val default = Charset.defaultCharset() + return mediaType?.charset(default) ?: default + } + + + private fun List
.applyContentTypeHeader(body: RequestBody?): List
{ + val contentTypeHeader = find { it.name.equals(CONTENT_TYPE, ignoreCase = false) } + val contentType = body?.contentType()?.toString() + + return if (contentTypeHeader == null && contentType != null) { + this + listOf(Header(CONTENT_TYPE, contentType)) + } else { + this + } + } + + private companion object { + const val FORMAT_HEADER = "-H \"%1\$s:%2\$s\"" + const val FORMAT_METHOD = "-X %1\$s" + const val FORMAT_BODY = "-d '%1\$s'" + const val FORMAT_URL = "\"%1\$s\"" + const val CONTENT_TYPE = "Content-Type" + } +} \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.java b/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.java deleted file mode 100644 index 0b157db..0000000 --- a/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.moczul.ok2curl; - -import com.moczul.ok2curl.logger.Loggable; -import com.moczul.ok2curl.modifier.HeaderModifier; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; - -public class CurlInterceptor implements Interceptor { - - private static final long DEFAULT_LIMIT = 1024L * 1024L; - private static final String DEFAULT_DELIMITER = " "; - - protected final Loggable logger; - protected final long limit; - protected final List headerModifiers = new ArrayList<>(); - protected final Options options; - protected final String delimiter; - - /** - * Interceptor responsible for printing curl logs - * - * Logs are pushed to stdout with 1MB limit - * - * @param logger output of logging - */ - public CurlInterceptor(Loggable logger) { - this(logger, DEFAULT_LIMIT, Collections.emptyList(), Options.EMPTY, DEFAULT_DELIMITER); - } - - /** - * Interceptor responsible for printing curl logs - * - * Logs are pushed to stdout with 1MB limit - * - * @param logger output of logging - * @param options list of curl options - */ - public CurlInterceptor(Loggable logger, Options options) { - this(logger, DEFAULT_LIMIT, Collections.emptyList(), options, DEFAULT_DELIMITER); - } - - /** - * Interceptor responsible for printing curl logs - * - * @param logger output of logging - * @param headerModifiers list of header modifiers - */ - public CurlInterceptor(Loggable logger, List headerModifiers) { - this(logger, DEFAULT_LIMIT, headerModifiers, Options.EMPTY, DEFAULT_DELIMITER); - } - - /** - * Interceptor responsible for printing curl logs - * - * @param logger output of logging - * @param limit limit maximal bytes logged, if negative - non limited - */ - public CurlInterceptor(Loggable logger, long limit) { - this(logger, limit, Collections.emptyList(), Options.EMPTY, DEFAULT_DELIMITER); - } - - /** - * Interceptor responsible for printing curl logs - * @param logger output of logging - * @param limit limit maximal bytes logged, if negative - non limited - * @param headerModifiers list of header modifiers - * @param options list of curl options - */ - public CurlInterceptor(Loggable logger, long limit, List headerModifiers, Options options) { - this(logger, limit, headerModifiers, options, DEFAULT_DELIMITER); - } - - /** - * Interceptor responsible for printing curl logs - * @param logger output of logging - * @param limit limit maximal bytes logged, if negative - non limited - * @param headerModifiers list of header modifiers - * @param options list of curl options - * @param delimiter string delimiter - */ - public CurlInterceptor(Loggable logger, long limit, List headerModifiers, Options options, String delimiter) { - this.logger = logger; - this.limit = limit; - this.headerModifiers.addAll(headerModifiers); - this.options = options; - this.delimiter = delimiter; - } - - @Override - public Response intercept(Chain chain) throws IOException { - final Request request = chain.request(); - - final Request copy = request.newBuilder().build(); - final String curl = getCurlBuilder(copy).build(); - - logger.log(curl); - - return chain.proceed(request); - } - - protected CurlBuilder getCurlBuilder(Request copy) { - return new CurlBuilder(copy, limit, headerModifiers, options, delimiter); - } -} diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.kt b/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.kt new file mode 100644 index 0000000..5334095 --- /dev/null +++ b/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.kt @@ -0,0 +1,23 @@ +package com.moczul.ok2curl + +import com.moczul.ok2curl.logger.Logger +import okhttp3.Interceptor +import okhttp3.Response + +class CurlInterceptor @JvmOverloads constructor( + private val logger: Logger, + configuration: Configuration = Configuration() +) : Interceptor { + + private val curlGenerator = CurlCommandGenerator(configuration) + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + val curl = curlGenerator.generate(request) + + logger.log(curl) + + return chain.proceed(request) + } +} \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/Flags.kt b/ok2curl/src/main/java/com/moczul/ok2curl/Flags.kt new file mode 100644 index 0000000..63946d9 --- /dev/null +++ b/ok2curl/src/main/java/com/moczul/ok2curl/Flags.kt @@ -0,0 +1,65 @@ +package com.moczul.ok2curl + +import java.util.* +import kotlin.collections.ArrayList +import kotlin.collections.HashSet + +class Flags private constructor(options: Collection) { + + private val options: List + + init { + this.options = ArrayList(options) + } + + fun list(): List { + return options + } + + class Builder { + private val options: MutableSet = HashSet() + fun insecure(): Builder { + options.add("--insecure") + return this + } + + fun maxTime(seconds: Int): Builder { + options.add(String.format(Locale.getDefault(), "--max-time %d", seconds)) + return this + } + + fun connectTimeout(seconds: Int): Builder { + options.add(String.format(Locale.getDefault(), "--connect-timeout %d", seconds)) + return this + } + + fun retry(num: Int): Builder { + options.add(String.format(Locale.getDefault(), "--retry %d", num)) + return this + } + + fun compressed(): Builder { + options.add("--compressed") + return this + } + + fun location(): Builder { + options.add("--location") + return this + } + + fun build(): Flags { + return Flags(options) + } + } + + companion object { + @JvmField + val EMPTY = Flags(emptyList()) + + @JvmStatic + fun builder(): Builder { + return Builder() + } + } +} \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/Header.java b/ok2curl/src/main/java/com/moczul/ok2curl/Header.java deleted file mode 100644 index 2b9a825..0000000 --- a/ok2curl/src/main/java/com/moczul/ok2curl/Header.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.moczul.ok2curl; - -public class Header { - - private final String name; - private final String value; - - public Header(String name, String value) { - this.name = name; - this.value = value; - } - - public String name() { - return name; - } - - public String value() { - return value; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Header header = (Header) o; - - if (name != null ? !name.equals(header.name) : header.name != null) return false; - return value != null ? value.equals(header.value) : header.value == null; - - } - - @Override - public int hashCode() { - int result = name != null ? name.hashCode() : 0; - result = 31 * result + (value != null ? value.hashCode() : 0); - return result; - } -} diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/Header.kt b/ok2curl/src/main/java/com/moczul/ok2curl/Header.kt new file mode 100644 index 0000000..975f456 --- /dev/null +++ b/ok2curl/src/main/java/com/moczul/ok2curl/Header.kt @@ -0,0 +1,3 @@ +package com.moczul.ok2curl + +data class Header( val name: String, val value: String) \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.java b/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.java deleted file mode 100644 index a0d3218..0000000 --- a/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.moczul.ok2curl; - -import java.io.IOException; - -import okio.Buffer; -import okio.Sink; -import okio.Timeout; - -public class LimitedSink implements Sink { - - private final Buffer limited; - private long total; - - public LimitedSink(Buffer limited, long limit) { - if (limited == null) throw new NullPointerException("limited can not be null"); - if (limit <= 0) throw new IllegalArgumentException("limit has to be grater than 0"); - this.limited = limited; - total = limit; - } - - @Override - public void write(Buffer source, long byteCount) throws IOException { - if (total > 0) { - long toWrite = Math.min(total, byteCount); - limited.write(source, toWrite); - total -= toWrite; - } - } - - @Override - public void flush() throws IOException { - limited.flush(); - } - - @Override - public Timeout timeout() { - return Timeout.NONE; - } - - @Override - public void close() throws IOException { - limited.close(); - } -} diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt b/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt new file mode 100644 index 0000000..3a4c2c7 --- /dev/null +++ b/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt @@ -0,0 +1,42 @@ +package com.moczul.ok2curl + +import okio.Buffer +import okio.Sink +import okio.Timeout +import java.io.IOException +import kotlin.math.min + +class LimitedSink(limited: Buffer, limit: Long) : Sink { + + private val limited: Buffer + private var total: Long + + init { + require(limit > 0) { "limit has to be grater than 0" } + this.limited = limited + total = limit + } + + @Throws(IOException::class) + override fun write(source: Buffer, byteCount: Long) { + if (total > 0) { + val toWrite = min(total, byteCount) + limited.write(source, toWrite) + total -= toWrite + } + } + + @Throws(IOException::class) + override fun flush() { + limited.flush() + } + + override fun timeout(): Timeout { + return Timeout.NONE + } + + @Throws(IOException::class) + override fun close() { + limited.close() + } +} \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/Options.java b/ok2curl/src/main/java/com/moczul/ok2curl/Options.java deleted file mode 100644 index bbc1c62..0000000 --- a/ok2curl/src/main/java/com/moczul/ok2curl/Options.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.moczul.ok2curl; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -public class Options { - - public static final Options EMPTY = new Options(Collections.emptyList()); - - private final List options; - - public static Builder builder() { - return new Builder(); - } - - private Options(Collection options) { - this.options = new ArrayList<>(options); - } - - public List list() { - return options; - } - - public static class Builder { - - private final Set options = new HashSet<>(); - - public Builder insecure() { - options.add("--insecure"); - return this; - } - - public Builder maxTime(int seconds) { - options.add(String.format(Locale.getDefault(), "--max-time %d", seconds)); - return this; - } - - public Builder connectTimeout(int seconds) { - options.add(String.format(Locale.getDefault(), "--connect-timeout %d", seconds)); - return this; - } - - public Builder retry(int num) { - options.add(String.format(Locale.getDefault(), "--retry %d", num)); - return this; - } - - public Builder compressed() { - options.add("--compressed"); - return this; - } - - public Builder location() { - options.add("--location"); - return this; - } - - public Options build() { - return new Options(options); - } - } - -} diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/StringUtil.java b/ok2curl/src/main/java/com/moczul/ok2curl/StringUtil.java deleted file mode 100644 index 450432f..0000000 --- a/ok2curl/src/main/java/com/moczul/ok2curl/StringUtil.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.moczul.ok2curl; - -public class StringUtil { - - /** - * Returns a string containing the tokens joined by delimiters. - * @param tokens an array objects to be joined. Strings will be formed from - * the objects by calling object.toString(). - */ - public static String join(CharSequence delimiter, Iterable tokens) { - StringBuilder sb = new StringBuilder(); - boolean firstTime = true; - for (Object token: tokens) { - if (firstTime) { - firstTime = false; - } else { - sb.append(delimiter); - } - sb.append(token); - } - return sb.toString(); - } -} diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/logger/Loggable.java b/ok2curl/src/main/java/com/moczul/ok2curl/logger/Loggable.java deleted file mode 100644 index 9082b75..0000000 --- a/ok2curl/src/main/java/com/moczul/ok2curl/logger/Loggable.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.moczul.ok2curl.logger; - -public interface Loggable { - - void log(String message); -} diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/logger/Logger.kt b/ok2curl/src/main/java/com/moczul/ok2curl/logger/Logger.kt new file mode 100644 index 0000000..d2cea17 --- /dev/null +++ b/ok2curl/src/main/java/com/moczul/ok2curl/logger/Logger.kt @@ -0,0 +1,5 @@ +package com.moczul.ok2curl.logger + +interface Logger { + fun log(message: String) +} \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/modifier/HeaderModifier.java b/ok2curl/src/main/java/com/moczul/ok2curl/modifier/HeaderModifier.kt similarity index 65% rename from ok2curl/src/main/java/com/moczul/ok2curl/modifier/HeaderModifier.java rename to ok2curl/src/main/java/com/moczul/ok2curl/modifier/HeaderModifier.kt index a53886f..29cec21 100644 --- a/ok2curl/src/main/java/com/moczul/ok2curl/modifier/HeaderModifier.java +++ b/ok2curl/src/main/java/com/moczul/ok2curl/modifier/HeaderModifier.kt @@ -1,21 +1,20 @@ -package com.moczul.ok2curl.modifier; +package com.moczul.ok2curl.modifier -import com.moczul.ok2curl.Header; +import com.moczul.ok2curl.Header /** * HeaderModifier allow for changing header name/value before creating curl log */ -public interface HeaderModifier { - +interface HeaderModifier { /** * @param header the header to check * @return true if header should be modified and false otherwise. */ - boolean matches(Header header); + fun matches(header: Header): Boolean /** * @param header the header to modify * @return modified header or null to omit header in curl log */ - Header modify(Header header); -} + fun modify(header: Header): Header? +} \ No newline at end of file diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.java b/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.java deleted file mode 100644 index b4da299..0000000 --- a/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.moczul.ok2curl; - -import com.moczul.ok2curl.util.FakeLogger; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -import okhttp3.Cookie; -import okhttp3.CookieJar; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okio.BufferedSink; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.contains; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -public class CookieHandlerTest { - - private FakeLogger logger; - private MockWebServer server; - private String url; - - @Before - public void setUpOkHttp() throws IOException { - logger = mock(FakeLogger.class); - - server = new MockWebServer(); - server.start(); - server.enqueue(new MockResponse().setBody("ok")); - url = server.url("/test").toString(); - } - - @After - public void tearDown() throws IOException { - server.shutdown(); - } - - @Test - public void testIfRequestBodyThrowsException_returnInfoAboutError() throws Exception { - final Request request = new Request.Builder().url(url).patch(new RequestBody() { - @Override - public MediaType contentType() { - return MediaType.parse("application/json"); - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - throw new IOException("exception"); - } - }).build(); - - OkHttpClient okHttpClient = new OkHttpClient.Builder() - .addInterceptor(new CurlInterceptor(logger)) - .cookieJar(getCookieJar()) - .build(); - - try { - okHttpClient.newCall(request).execute(); - } catch (IOException ignore) {} - - final ArgumentCaptor stringArgumentCaptor = ArgumentCaptor.forClass(String.class); - verify(logger).log(stringArgumentCaptor.capture()); - assertTrue(stringArgumentCaptor.getValue().contains("Error")); - } - - @Test - public void testApplicationInterceptor() throws IOException { - final Request request = new Request.Builder().url(url).build(); - - OkHttpClient okHttpClient = new OkHttpClient.Builder() - .addInterceptor(new CurlInterceptor(logger)) - .cookieJar(getCookieJar()) - .build(); - - okHttpClient.newCall(request).execute(); - - final String expectedCurl = String.format("curl -X GET \"%s\"", url); - verify(logger).log(expectedCurl); - } - - @Test - public void testNetworkInterceptor() throws IOException { - final Request request = new Request.Builder().url(url).build(); - - OkHttpClient okHttpClient = new OkHttpClient.Builder() - .addNetworkInterceptor(new CurlInterceptor(logger)) - .cookieJar(getCookieJar()) - .build(); - - okHttpClient.newCall(request).execute(); - - verify(logger).log(contains("-H \"Cookie:foo=bar; banana=rama\"")); - } - - private CookieJar getCookieJar() { - return new CookieJar() { - @Override - public void saveFromResponse(HttpUrl url, List cookies) { - } - - @Override - public List loadForRequest(HttpUrl url) { - Cookie foo = new Cookie.Builder() - .name("foo") - .value("bar") - .domain(url.host()) - .build(); - Cookie banana = new Cookie.Builder() - .name("banana") - .value("rama") - .domain(url.host()) - .build(); - return Arrays.asList(foo, banana); - } - }; - } -} diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.kt b/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.kt new file mode 100644 index 0000000..5bbe1cc --- /dev/null +++ b/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.kt @@ -0,0 +1,119 @@ +import com.moczul.ok2curl.CurlInterceptor +import com.moczul.ok2curl.logger.Logger +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okio.BufferedSink +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.contains +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import java.io.IOException + +class CookieHandlerTest { + + private val logger: Logger = mock(Logger::class.java) + private lateinit var server: MockWebServer + private lateinit var url: String + + @Before + fun setUpOkHttp() { + server = MockWebServer() + server.start() + server.enqueue(MockResponse().setBody("ok")) + url = server.url("/test").toString() + } + + @After + fun tearDown() { + server.shutdown() + } + + @Test + fun `should intercept failed request and log error information`() { + // given + val request: Request = Request.Builder().url(url).patch(object : RequestBody() { + override fun contentType(): MediaType? { + return "application/json".toMediaTypeOrNull() + } + + override fun writeTo(sink: BufferedSink) { + throw IOException("exception") + } + }).build() + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(CurlInterceptor(logger)) + .cookieJar(createCookieJar()) + .build() + + // when + try { + okHttpClient.newCall(request).execute() + } catch (ignore: IOException) { + } + + // then + verify(logger).log(contains("Error")) + } + + @Test + fun `should intercept request with application interceptor`() { + // given + val request: Request = Request.Builder().url(url).build() + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(CurlInterceptor(logger)) + .cookieJar(createCookieJar()) + .build() + + // when + okHttpClient.newCall(request).execute() + + // then + val expectedCurl = String.format("curl -X GET \"%s\"", url) + verify(logger).log(expectedCurl) + } + + @Test + fun `should intercept request with network interceptor`() { + // given + val request: Request = Request.Builder().url(url).build() + val okHttpClient = OkHttpClient.Builder() + .addNetworkInterceptor(CurlInterceptor(logger)) + .cookieJar(createCookieJar()) + .build() + + // when + okHttpClient.newCall(request).execute() + + // then + verify(logger).log(contains("-H \"Cookie:foo=bar; banana=rama\"")) + } + + private fun createCookieJar(): CookieJar { + return object : CookieJar { + override fun saveFromResponse(url: HttpUrl, cookies: List) {} + override fun loadForRequest(url: HttpUrl): List { + val foo: Cookie = Cookie.Builder() + .name("foo") + .value("bar") + .domain(url.host) + .build() + val banana: Cookie = Cookie.Builder() + .name("banana") + .value("rama") + .domain(url.host) + .build() + return listOf(foo, banana) + } + } + } +} \ No newline at end of file diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/CurlBuilderTest.java b/ok2curl/src/test/java/com/moczul/ok2curl/CurlBuilderTest.java deleted file mode 100644 index 3d2c318..0000000 --- a/ok2curl/src/test/java/com/moczul/ok2curl/CurlBuilderTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.moczul.ok2curl; - -import com.moczul.ok2curl.modifier.HeaderModifier; - -import org.junit.Test; - -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -import okhttp3.CacheControl; -import okhttp3.FormBody; -import okhttp3.Request; -import okhttp3.RequestBody; - -import static org.junit.Assert.assertEquals; - -public class CurlBuilderTest { - - @Test - public void getRequestHasCorrectCommand() { - final Request request = new Request.Builder().url("http://example.com/").build(); - - final String command = new CurlBuilder(request).build(); - - assertEquals("curl -X GET \"http://example.com/\"", command); - } - - @Test - public void getRequestHasCorrectHeader() { - final Request request = new Request.Builder() - .url("http://example.com/") - .header("Accept", "application/json") - .build(); - - final String command = new CurlBuilder(request).build(); - - assertEquals("curl -X GET -H \"Accept:application/json\" \"http://example.com/\"", command); - } - - @Test - public void getRequestHasCorrectCacheHeader() { - final CacheControl cache = new CacheControl.Builder() - .maxAge(1, TimeUnit.DAYS) - .onlyIfCached() - .build(); - - final Request request = new Request.Builder() - .url("http://example.com/") - .cacheControl(cache) - .build(); - - final String command = new CurlBuilder(request).build(); - - assertEquals("curl -X GET -H \"Cache-Control:max-age=86400, only-if-cached\" \"http://example.com/\"", command); - } - - @Test - public void postRequestHasCorrectPostData() { - final Request request = new Request.Builder().url("http://example.com/").post(body()).build(); - - final String command = new CurlBuilder(request).build(); - - final String expected = "curl -X POST -H \"Content-Type:application/x-www-form-urlencoded\" -d 'key1=value1' \"http://example.com/\""; - assertEquals(expected, command); - } - - @Test - public void postRequestBodyWithNullMediaType() { - final RequestBody body = RequestBody.create(null, "StringBody"); - final Request request = new Request.Builder().url("http://example.com/").post(body).build(); - - final String command = new CurlBuilder(request).build(); - - final String expected = "curl -X POST -d 'StringBody' \"http://example.com/\""; - assertEquals(expected, command); - } - - @Test - public void multipleHeadersWithTheSameNameShouldBeAddedToCurlCommand() { - final Request request = new Request.Builder() - .url("http://example.com/") - .addHeader("Cookie", "FIRST=foo") - .addHeader("Cookie", "SECOND=bar") - .build(); - - final String command = new CurlBuilder(request).build(); - - assertEquals("curl -X GET -H \"Cookie:FIRST=foo\" -H \"Cookie:SECOND=bar\" \"http://example.com/\"", command); - } - - @Test - public void getRequestContainsInsecureOption() { - final Request request = new Request.Builder().url("http://example.com/").build(); - final Options options = Options.builder().insecure().build(); - - final String command = new CurlBuilder(request, 1024, Collections.emptyList(), options).build(); - - assertEquals("curl --insecure -X GET \"http://example.com/\"", command); - } - - @Test - public void getRequestContainsConnectTimeoutOptions() { - final Request request = new Request.Builder().url("http://example.com/").build(); - final Options options = Options.builder().connectTimeout(120).build(); - - final String command = new CurlBuilder(request, 1024, Collections.emptyList(), options).build(); - - assertEquals("curl --connect-timeout 120 -X GET \"http://example.com/\"", command); - } - - @Test - public void getRequesWithDelimiter() { - final Request request = new Request.Builder().url("http://example.com/").build(); - - final String command = new CurlBuilder(request, -1L, Collections.emptyList(), Options.EMPTY, " \\\n").build(); - - assertEquals("curl \\\n-X GET \\\n\"http://example.com/\"", command); - } - - private RequestBody body() { - return new FormBody.Builder().add("key1", "value1").build(); - } -} diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/CurlCommandGeneratorTest.kt b/ok2curl/src/test/java/com/moczul/ok2curl/CurlCommandGeneratorTest.kt new file mode 100644 index 0000000..880bd69 --- /dev/null +++ b/ok2curl/src/test/java/com/moczul/ok2curl/CurlCommandGeneratorTest.kt @@ -0,0 +1,190 @@ +package com.moczul.ok2curl.util + +import com.moczul.ok2curl.CommandComponent +import com.moczul.ok2curl.Configuration +import com.moczul.ok2curl.CurlCommandGenerator +import com.moczul.ok2curl.Flags +import okhttp3.CacheControl +import okhttp3.FormBody +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.concurrent.TimeUnit + + +class CurlCommandGeneratorTest { + + private val configuration = Configuration() + + @Test + fun `should generate simple GET command`() { + // given + val curlGenerator = CurlCommandGenerator(configuration) + val request: Request = Request.Builder().url("http://example.com/").build() + + // when + val command = curlGenerator.generate(request) + + // then + assertEquals("curl -X GET \"http://example.com/\"", command) + } + + @Test + fun `should generate get command with headers`() { + // given + val curlGenerator = CurlCommandGenerator(configuration) + val request: Request = Request.Builder() + .url("http://example.com/") + .header("Accept", "application/json") + .build() + + // when + val command = curlGenerator.generate(request) + + // then + assertEquals("curl -X GET -H \"Accept:application/json\" \"http://example.com/\"", command) + } + + @Test + fun `should generate GET command with cache-control header`() { + // given + val curlGenerator = CurlCommandGenerator(configuration) + val request: Request = Request.Builder() + .url("http://example.com/") + .cacheControl(oneDayCache()) + .build() + + // when + val command = curlGenerator.generate(request) + + // then + assertEquals( + "curl -X GET -H \"Cache-Control:max-age=86400, only-if-cached\" \"http://example.com/\"", + command + ) + } + + @Test + fun `should generate POST command with body`() { + // given + val curlGenerator = CurlCommandGenerator(configuration) + val request: Request = Request.Builder().url("http://example.com/").post(body()).build() + + // when + val command = curlGenerator.generate(request) + + // then + val expected = + "curl -X POST -H \"Content-Type:application/x-www-form-urlencoded\" -d 'key1=value1' \"http://example.com/\"" + assertEquals(expected, command) + } + + @Test + fun postRequestBodyWithNullMediaType() { + // given + val curlGenerator = CurlCommandGenerator(configuration) + val body = "StringBody".toRequestBody(contentType = null) + val request: Request = Request.Builder().url("http://example.com/").post(body).build() + + // when + val command = curlGenerator.generate(request) + + // then + val expected = "curl -X POST -d 'StringBody' \"http://example.com/\"" + assertEquals(expected, command) + } + + @Test + fun `should generate GET command with mutli-value header`() { + // given + val curlGenerator = CurlCommandGenerator(configuration) + val request: Request = Request.Builder() + .url("http://example.com/") + .addHeader("Cookie", "FIRST=foo") + .addHeader("Cookie", "SECOND=bar") + .build() + + // when + val command = curlGenerator.generate(request) + + // when + assertEquals( + "curl -X GET -H \"Cookie:FIRST=foo\" -H \"Cookie:SECOND=bar\" \"http://example.com/\"", + command + ) + } + + @Test + fun `should generate GET command with insecure flag`() { + // given + val insecureConfig = Configuration(flags = Flags.builder().insecure().build()) + val curlGenerator = CurlCommandGenerator(insecureConfig) + val request: Request = Request.Builder().url("http://example.com/").build() + + // when + val command = curlGenerator.generate(request) + + // then + assertEquals("curl --insecure -X GET \"http://example.com/\"", command) + } + + @Test + fun `should generate GET command with connect-timeout flag`() { + // given + val timeoutConfig = Configuration(flags = Flags.builder().connectTimeout(120).build()) + val curlGenerator = CurlCommandGenerator(timeoutConfig) + val request: Request = Request.Builder().url("http://example.com/").build() + + // when + val command = curlGenerator.generate(request) + + // †hen + assertEquals("curl --connect-timeout 120 -X GET \"http://example.com/\"", command) + } + + @Test + fun `should generate GET command using specific delimiter`() { + // given + val delimiterConfig = Configuration(delimiter = " \\\n") + val curlGenerator = CurlCommandGenerator(delimiterConfig) + val request: Request = Request.Builder().url("http://example.com/").build() + + // when + val command = curlGenerator.generate(request) + + // then + assertEquals("curl \\\n-X GET \\\n\"http://example.com/\"", command) + } + + @Test + fun `should generate POST command with specified components`() { + // given + val simpleConfig = Configuration( + components = listOf( + CommandComponent.Curl, + CommandComponent.Method, + CommandComponent.Url + ) + ) + val curlGenerator = CurlCommandGenerator(simpleConfig) + val request: Request = Request.Builder() + .url("https://github.com") + .cacheControl(oneDayCache()) + .post(body()) + .build() + + // when + val command = curlGenerator.generate(request) + + // then + assertEquals("curl -X POST \"https://github.com/\"", command) + } + + private companion object { + private fun body() = FormBody.Builder().add("key1", "value1").build() + + private fun oneDayCache() = + CacheControl.Builder().maxAge(1, TimeUnit.DAYS).onlyIfCached().build() + } +} \ No newline at end of file diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/FlagsTest.kt b/ok2curl/src/test/java/com/moczul/ok2curl/FlagsTest.kt new file mode 100644 index 0000000..cd1d77b --- /dev/null +++ b/ok2curl/src/test/java/com/moczul/ok2curl/FlagsTest.kt @@ -0,0 +1,65 @@ +package com.moczul.ok2curl.util + +import com.moczul.ok2curl.Flags +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.hasItem +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test + +class FlagsTest { + + @Test + fun `should return empty options for default builder`() { + // when + val flags = Flags.builder().build() + + // when + assertThat(flags.list().size, `is`(0)) + } + + @Test + fun `should correctly handle options parameters`() { + // when + val flags = Flags.builder() + .maxTime(120) + .connectTimeout(60) + .retry(3) + .build() + + // then + assertThat(flags.list().size, `is`(3)) + assertThat>(flags.list(), hasItem("--max-time 120")) + assertThat>(flags.list(), hasItem("--connect-timeout 60")) + assertThat>(flags.list(), hasItem("--retry 3")) + } + + @Test + fun `should return correct list of parameters`() { + // when + val flags = Flags.builder() + .insecure() + .compressed() + .location() + .build() + + // then + assertThat(flags.list().size, `is`(3)) + assertThat>(flags.list(), hasItem("--insecure")) + assertThat>(flags.list(), hasItem("--compressed")) + assertThat>(flags.list(), hasItem("--location")) + } + + @Test + fun `should ignore duplicated parameters`() { + // when + val flags = Flags.builder() + .insecure() + .insecure() + .insecure() + .build() + + // then + assertThat(flags.list().size, `is`(1)) + assertThat>(flags.list(), hasItem("--insecure")) + } +} \ No newline at end of file diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/LimitedSinkTest.java b/ok2curl/src/test/java/com/moczul/ok2curl/LimitedSinkTest.java deleted file mode 100644 index 7f82327..0000000 --- a/ok2curl/src/test/java/com/moczul/ok2curl/LimitedSinkTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.moczul.ok2curl; - -import org.junit.Test; - -import java.nio.charset.Charset; - -import okio.Buffer; -import okio.BufferedSink; -import okio.ByteString; -import okio.Okio; - -import static org.junit.Assert.*; - -public class LimitedSinkTest { - - @Test(expected = IllegalArgumentException.class) - public void testWhenLimitIs0_throwIllegalArgumentException() throws Exception { - new LimitedSink(new Buffer(), 0L); - } - - @Test(expected = IllegalArgumentException.class) - public void testWhenLimitIsNegative_throwIllegalArgumentException() throws Exception { - new LimitedSink(new Buffer(), -1L); - } - - @Test(expected = NullPointerException.class) - public void testWhenBufferIsNull_throwNullPointerException() throws Exception { - new LimitedSink(null, 10L); - } - - @Test - public void testWhenLongResult_writeOnlyLimitedBytes() throws Exception { - final Buffer limited = new Buffer(); - final BufferedSink source = Okio.buffer(new LimitedSink(limited, 10L)); - - source - .write(ByteString.encodeUtf8("0123456789012345678901234567890123456789")); // 40B - source.flush(); - - assertEquals("0123456789", limited.readString(Charset.forName("UTF-8"))); - } - - @Test - public void testWhenLongResultWrittenInParts_writeOnlyLimitedBytes() throws Exception { - final Buffer limited = new Buffer(); - final BufferedSink source = Okio.buffer(new LimitedSink(limited, 10L)); - - source - .write(ByteString.encodeUtf8("01234")); // 5B - source.flush(); - source - .write(ByteString.encodeUtf8("5678901234")); // 10B - source.flush(); - source - .write(ByteString.encodeUtf8("5678901234567890123456789")); // 25B - source.flush(); - - assertEquals("0123456789", limited.readString(Charset.forName("UTF-8"))); - } - - @Test - public void testWhenShorterResult_writeOnlyThatData() throws Exception { - final Buffer limited = new Buffer(); - final BufferedSink source = Okio.buffer(new LimitedSink(limited, 10L)); - - source - .write(ByteString.encodeUtf8("01234")); // 5B - source.flush(); - - assertEquals("01234", limited.readString(Charset.forName("UTF-8"))); - } -} diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/LimitedSinkTest.kt b/ok2curl/src/test/java/com/moczul/ok2curl/LimitedSinkTest.kt new file mode 100644 index 0000000..254b5fb --- /dev/null +++ b/ok2curl/src/test/java/com/moczul/ok2curl/LimitedSinkTest.kt @@ -0,0 +1,67 @@ +package com.moczul.ok2curl + +import okio.Buffer +import okio.ByteString.Companion.encodeUtf8 +import okio.buffer +import org.junit.Assert.assertEquals +import org.junit.Test +import java.nio.charset.Charset + +class LimitedSinkTest { + + @Test(expected = IllegalArgumentException::class) + fun `should throw exception when limit is 0`() { + LimitedSink(Buffer(), 0L) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw exception when limit is negative`() { + LimitedSink(Buffer(), -1L) + } + + @Test + fun `should write to buffer only limited amount of bytes`() { + // given + val limited = Buffer() + val source = LimitedSink(limited, 10L).buffer() + + // when + source.write("0123456789012345678901234567890123456789".encodeUtf8()) // 40B + source.flush() + + // then + assertEquals("0123456789", limited.readString(Charset.forName("UTF-8"))) + } + + @Test + fun `should write to buffer only limited amount of bytes when written in parts`() { + // given + val limited = Buffer() + val source = LimitedSink(limited, 10L).buffer() + + // when + source.write("01234".encodeUtf8()) // 5B + source.flush() + source.write("5678901234".encodeUtf8()) // 10B + source.flush() + source.write("5678901234567890123456789".encodeUtf8()) // 25B + source.flush() + + // then + assertEquals("0123456789", limited.readString(Charset.forName("UTF-8"))) + } + + @Test + fun `should write all bytes when limit is bigger than input`() { + // given + val limited = Buffer() + val source = LimitedSink(limited, 10L).buffer() + + // when + source.write("01234".encodeUtf8()) // 5B + source.flush() + + // then + assertEquals("01234", limited.readString(Charset.forName("UTF-8"))) + } +} \ No newline at end of file diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/OptionsTest.java b/ok2curl/src/test/java/com/moczul/ok2curl/OptionsTest.java deleted file mode 100644 index c9fae6c..0000000 --- a/ok2curl/src/test/java/com/moczul/ok2curl/OptionsTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.moczul.ok2curl; - -import org.junit.Test; - -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -public class OptionsTest { - - @Test - public void defaultBuilderShouldReturnEmptyOptions() { - final Options options = Options.builder().build(); - - assertThat(options.list().size(), is(0)); - } - - @Test - public void shouldCorrectlyHandleOptionsParameters() { - final Options options = Options - .builder() - .maxTime(120) - .connectTimeout(60) - .retry(3) - .build(); - - assertThat(options.list().size(), is(3)); - assertThat(options.list(), hasItem("--max-time 120")); - assertThat(options.list(), hasItem("--connect-timeout 60")); - assertThat(options.list(), hasItem("--retry 3")); - } - - @Test - public void shouldReturnCorrectListOfParameters() { - final Options options = Options.builder() - .insecure() - .compressed() - .location() - .build(); - - assertThat(options.list().size(), is(3)); - assertThat(options.list(), hasItem("--insecure")); - assertThat(options.list(), hasItem("--compressed")); - assertThat(options.list(), hasItem("--location")); - } - - @Test - public void shouldIgnoreDuplicatedParameters() { - final Options options = Options.builder() - .insecure() - .insecure() - .insecure() - .build(); - - assertThat(options.list().size(), is(1)); - assertThat(options.list(), hasItem("--insecure")); - } -} diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/util/FakeLogger.java b/ok2curl/src/test/java/com/moczul/ok2curl/util/FakeLogger.java deleted file mode 100644 index 18d46e6..0000000 --- a/ok2curl/src/test/java/com/moczul/ok2curl/util/FakeLogger.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.moczul.ok2curl.util; - -import com.moczul.ok2curl.logger.Loggable; - -public class FakeLogger implements Loggable { - - @Override - public void log(String message) { - System.out.println(message); - } -} diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/util/HeaderModifierTest.java b/ok2curl/src/test/java/com/moczul/ok2curl/util/HeaderModifierTest.java deleted file mode 100644 index 7e09977..0000000 --- a/ok2curl/src/test/java/com/moczul/ok2curl/util/HeaderModifierTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.moczul.ok2curl.util; - -import com.moczul.ok2curl.CurlBuilder; -import com.moczul.ok2curl.Header; -import com.moczul.ok2curl.Options; -import com.moczul.ok2curl.modifier.HeaderModifier; - -import org.junit.Test; - -import java.util.Collections; -import java.util.List; - -import okhttp3.Request; - -import static junit.framework.Assert.assertEquals; - -public class HeaderModifierTest { - - private final HeaderModifier cookieHeaderModifier = new HeaderModifier() { - - @Override - public boolean matches(Header header) { - return "Cookie".equals(header.name()); - } - - @Override - public Header modify(Header header) { - return new Header(header.name(), "modifiedCookieValue"); - } - }; - - private final HeaderModifier nullHeaderModifier = new HeaderModifier() { - - @Override - public boolean matches(Header header) { - return true; - } - - @Override - public Header modify(Header header) { - return null; - } - }; - - @Test - public void curlCommand_shouldContains_modifiedHeader() { - final Request request = new Request.Builder() - .url("http://example.com/") - .header("Cookie", "FIRST=foo") - .build(); - - final List modifiers = Collections.singletonList(cookieHeaderModifier); - final String command = new CurlBuilder(request, -1L, modifiers, Options.EMPTY).build(); - - assertEquals("curl -X GET -H \"Cookie:modifiedCookieValue\" \"http://example.com/\"", command); - } - - @Test - public void curlCommand_shouldNotBeModified_ifDoesNotContainMatchingHeader() { - final Request request = new Request.Builder() - .url("http://example.com/") - .header("Accept", "application/json") - .build(); - final List modifiers = Collections.singletonList(cookieHeaderModifier); - - final String command = new CurlBuilder(request, -1L, modifiers, Options.EMPTY).build(); - - assertEquals("curl -X GET -H \"Accept:application/json\" \"http://example.com/\"", command); - } - - @Test - public void curlCommand_shouldNotContainsAnyHeaders_forNullHeaderModifier() { - final Request request = new Request.Builder() - .url("http://example.com/") - .header("Cookie", "FIRST=foo") - .header("Accept", "application/json") - .build(); - - final List modifiers = Collections.singletonList(nullHeaderModifier); - final String command = new CurlBuilder(request, -1L, modifiers, Options.EMPTY).build(); - - assertEquals(command, "curl -X GET \"http://example.com/\"", command); - } -} diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/util/HeaderModifierTest.kt b/ok2curl/src/test/java/com/moczul/ok2curl/util/HeaderModifierTest.kt new file mode 100644 index 0000000..a1ed6a5 --- /dev/null +++ b/ok2curl/src/test/java/com/moczul/ok2curl/util/HeaderModifierTest.kt @@ -0,0 +1,89 @@ +package com.moczul.ok2curl.util + + +import com.moczul.ok2curl.Configuration +import com.moczul.ok2curl.CurlCommandGenerator +import com.moczul.ok2curl.Header +import com.moczul.ok2curl.modifier.HeaderModifier +import okhttp3.Request +import org.junit.Assert.assertEquals +import org.junit.Test + +class HeaderModifierTest { + private val cookieHeaderModifier: HeaderModifier = object : HeaderModifier { + override fun matches(header: Header): Boolean { + return "Cookie" == header.name + } + + override fun modify(header: Header): Header { + return Header(header.name, "modifiedCookieValue") + } + } + private val nullHeaderModifier: HeaderModifier = object : HeaderModifier { + override fun matches(header: Header): Boolean { + return true + } + + override fun modify(header: Header): Header? { + return null + } + } + + @Test + fun `should generate GET command with modified header`() { + // given + val config = Configuration(headerModifiers = listOf(cookieHeaderModifier)) + val curlGenerator = CurlCommandGenerator(configuration = config) + val request: Request = Request.Builder() + .url("http://example.com/") + .header("Cookie", "FIRST=foo") + .build() + + // when + val command = curlGenerator.generate(request) + + // then + assertEquals( + "curl -X GET -H \"Cookie:modifiedCookieValue\" \"http://example.com/\"", + command + ) + } + + @Test + fun `should generate GET command with unmodified header`() { + // given + val config = Configuration(headerModifiers = listOf(cookieHeaderModifier)) + val curlGenerator = CurlCommandGenerator(configuration = config) + val request: Request = Request.Builder() + .url("http://example.com/") + .header("Accept", "application/json") + .build() + + // when + val command = curlGenerator.generate(request) + + // then + assertEquals( + "curl -X GET -H \"Accept:application/json\" \"http://example.com/\"", + command + ) + } + + @Test + fun `should generate GET command without any headers`() { + // given + val config = Configuration(headerModifiers = listOf(nullHeaderModifier)) + val curlGenerator = CurlCommandGenerator(configuration = config) + val request: Request = Request.Builder() + .url("http://example.com/") + .header("Cookie", "FIRST=foo") + .header("Accept", "application/json") + .build() + + // when + val command = curlGenerator.generate(request) + + // then + assertEquals(command, "curl -X GET \"http://example.com/\"", command) + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/moczul/sample/AndroidLogger.java b/sample/src/main/java/com/moczul/sample/AndroidLogger.java index 7fd440f..971d75d 100644 --- a/sample/src/main/java/com/moczul/sample/AndroidLogger.java +++ b/sample/src/main/java/com/moczul/sample/AndroidLogger.java @@ -2,14 +2,16 @@ import android.util.Log; -import com.moczul.ok2curl.logger.Loggable; +import androidx.annotation.NonNull; -public class AndroidLogger implements Loggable { +import com.moczul.ok2curl.logger.Logger; + +public class AndroidLogger implements Logger { public static final String TAG = "Ok2Curl"; @Override - public void log(String message) { + public void log(@NonNull String message) { Log.v(TAG, message); } } From 7dd972dcb0630c84c548b880e23f7b12578355aa Mon Sep 17 00:00:00 2001 From: Michal Moczulski Date: Wed, 2 Feb 2022 20:23:24 +0100 Subject: [PATCH 3/9] Convert sample to Kotlin --- sample/build.gradle | 16 ++++- sample/src/main/AndroidManifest.xml | 2 +- .../java/com/moczul/sample/AndroidLogger.java | 17 ----- .../java/com/moczul/sample/AndroidLogger.kt | 15 ++++ .../java/com/moczul/sample/MainActivity.java | 68 ------------------- .../java/com/moczul/sample/MainActivity.kt | 55 +++++++++++++++ .../com/moczul/sample/RequestFactory.java | 58 ---------------- .../java/com/moczul/sample/RequestFactory.kt | 54 +++++++++++++++ .../com/moczul/sample/RequestService.java | 48 ------------- .../java/com/moczul/sample/RequestService.kt | 36 ++++++++++ .../moczul/sample/modifier/Base64Decoder.java | 11 --- .../moczul/sample/modifier/Base64Decoder.kt | 11 +++ .../BasicAuthorizationHeaderModifier.java | 26 ------- .../BasicAuthorizationHeaderModifier.kt | 16 +++++ sample/src/main/res/values/strings.xml | 2 +- .../com/moczul/sample/ExampleUnitTest.java | 15 ---- .../BasicAuthorizationHeaderModifierTest.java | 45 ------------ .../BasicAuthorizationHeaderModifierTest.kt | 48 +++++++++++++ 18 files changed, 251 insertions(+), 292 deletions(-) delete mode 100644 sample/src/main/java/com/moczul/sample/AndroidLogger.java create mode 100644 sample/src/main/java/com/moczul/sample/AndroidLogger.kt delete mode 100644 sample/src/main/java/com/moczul/sample/MainActivity.java create mode 100644 sample/src/main/java/com/moczul/sample/MainActivity.kt delete mode 100644 sample/src/main/java/com/moczul/sample/RequestFactory.java create mode 100644 sample/src/main/java/com/moczul/sample/RequestFactory.kt delete mode 100644 sample/src/main/java/com/moczul/sample/RequestService.java create mode 100644 sample/src/main/java/com/moczul/sample/RequestService.kt delete mode 100644 sample/src/main/java/com/moczul/sample/modifier/Base64Decoder.java create mode 100644 sample/src/main/java/com/moczul/sample/modifier/Base64Decoder.kt delete mode 100644 sample/src/main/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifier.java create mode 100644 sample/src/main/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifier.kt delete mode 100644 sample/src/test/java/com/moczul/sample/ExampleUnitTest.java delete mode 100644 sample/src/test/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifierTest.java create mode 100644 sample/src/test/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifierTest.kt diff --git a/sample/build.gradle b/sample/build.gradle index 1ec253c..11bed1f 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,10 +1,11 @@ apply plugin: 'com.android.application' +apply plugin: 'org.jetbrains.kotlin.android' android { compileSdkVersion 31 defaultConfig { applicationId "com.moczul.sample" - minSdkVersion 14 + minSdkVersion 16 targetSdkVersion 31 versionCode 1 versionName "1.0" @@ -21,6 +22,12 @@ android { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } + buildFeatures { + viewBinding true + } + kotlinOptions { + jvmTarget = '1.8' + } } repositories { @@ -46,7 +53,12 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.4.0' implementation 'com.squareup.okhttp3:okhttp:4.9.3' + implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:2.23.0' + testImplementation 'org.mockito:mockito-core:4.3.1' + testImplementation 'org.mockito:mockito-inline:4.3.1' } diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 5496032..0fc1104 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -23,4 +23,4 @@ - + \ No newline at end of file diff --git a/sample/src/main/java/com/moczul/sample/AndroidLogger.java b/sample/src/main/java/com/moczul/sample/AndroidLogger.java deleted file mode 100644 index 971d75d..0000000 --- a/sample/src/main/java/com/moczul/sample/AndroidLogger.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.moczul.sample; - -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.moczul.ok2curl.logger.Logger; - -public class AndroidLogger implements Logger { - - public static final String TAG = "Ok2Curl"; - - @Override - public void log(@NonNull String message) { - Log.v(TAG, message); - } -} diff --git a/sample/src/main/java/com/moczul/sample/AndroidLogger.kt b/sample/src/main/java/com/moczul/sample/AndroidLogger.kt new file mode 100644 index 0000000..adc773c --- /dev/null +++ b/sample/src/main/java/com/moczul/sample/AndroidLogger.kt @@ -0,0 +1,15 @@ +package com.moczul.sample + +import android.util.Log +import com.moczul.ok2curl.logger.Logger + +class AndroidLogger : Logger { + + override fun log(message: String) { + Log.v(TAG, message) + } + + companion object { + const val TAG = "Ok2Curl" + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/moczul/sample/MainActivity.java b/sample/src/main/java/com/moczul/sample/MainActivity.java deleted file mode 100644 index 463c234..0000000 --- a/sample/src/main/java/com/moczul/sample/MainActivity.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.moczul.sample; - -import android.content.Intent; -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import android.view.View; -import android.widget.TextView; - -import com.moczul.ok2curl.CurlBuilder; -import com.moczul.ok2curl.Options; -import com.moczul.ok2curl.modifier.HeaderModifier; -import com.moczul.sample.modifier.Base64Decoder; -import com.moczul.sample.modifier.BasicAuthorizationHeaderModifier; - -import java.util.Collections; -import java.util.List; - -public class MainActivity extends AppCompatActivity implements View.OnClickListener { - - private TextView curlLog; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - curlLog = findViewById(R.id.curl_log); - - findViewById(R.id.get_request).setOnClickListener(this); - findViewById(R.id.post_request).setOnClickListener(this); - findViewById(R.id.get_request_modified).setOnClickListener(this); - } - - private void sendRequest(String type) { - final Intent intent = new Intent(this, RequestService.class); - intent.putExtra(RequestService.REQUEST_TYPE, type); - - startService(intent); - } - - private void displayCurlLog(String type) { - final BasicAuthorizationHeaderModifier modifier = new BasicAuthorizationHeaderModifier(new Base64Decoder()); - final List modifiers = Collections.singletonList(modifier); - - final String curl = new CurlBuilder(RequestFactory.getRequest(type), -1L, modifiers, Options.EMPTY).build(); - curlLog.setText(curl); - } - - @Override - public void onClick(View v) { - switch (v.getId()) { - case R.id.get_request: - sendRequest(RequestFactory.TYPE_GET); - displayCurlLog(RequestFactory.TYPE_GET); - break; - case R.id.post_request: - sendRequest(RequestFactory.TYPE_POST); - displayCurlLog(RequestFactory.TYPE_POST); - break; - case R.id.get_request_modified: - sendRequest(RequestFactory.TYPE_GET_MODIFIED); - displayCurlLog(RequestFactory.TYPE_GET_MODIFIED); - break; - default: - throw new IllegalArgumentException("Invalid view id"); - } - } -} diff --git a/sample/src/main/java/com/moczul/sample/MainActivity.kt b/sample/src/main/java/com/moczul/sample/MainActivity.kt new file mode 100644 index 0000000..f7b3651 --- /dev/null +++ b/sample/src/main/java/com/moczul/sample/MainActivity.kt @@ -0,0 +1,55 @@ +package com.moczul.sample + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.moczul.ok2curl.Configuration +import com.moczul.ok2curl.CurlCommandGenerator +import com.moczul.ok2curl.modifier.HeaderModifier +import com.moczul.sample.modifier.Base64Decoder +import com.moczul.sample.modifier.BasicAuthorizationHeaderModifier + +class MainActivity : AppCompatActivity() { + + private lateinit var curlLog: TextView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + curlLog = findViewById(R.id.curl_log) + + findViewById(R.id.get_request).setOnClickListener { + performRequest(RequestFactory.TYPE_GET) + } + + findViewById(R.id.post_request).setOnClickListener { + performRequest(RequestFactory.TYPE_POST) + } + + findViewById(R.id.get_request_modified).setOnClickListener { + performRequest(RequestFactory.TYPE_GET_MODIFIED) + } + } + + private fun performRequest(type: String) { + sendRequest(type) + displayCurlLog(type) + } + + private fun sendRequest(type: String) { + val intent = Intent(this, RequestService::class.java) + intent.putExtra(RequestService.REQUEST_TYPE, type) + startService(intent) + } + + private fun displayCurlLog(type: String) { + val modifier = BasicAuthorizationHeaderModifier(Base64Decoder()) + val modifiers: List = listOf(modifier) + val commandGenerator = CurlCommandGenerator(Configuration(modifiers)) + val curl: String = commandGenerator.generate(RequestFactory.getRequest(type)) + curlLog.text = curl + } + +} \ No newline at end of file diff --git a/sample/src/main/java/com/moczul/sample/RequestFactory.java b/sample/src/main/java/com/moczul/sample/RequestFactory.java deleted file mode 100644 index b6dfd77..0000000 --- a/sample/src/main/java/com/moczul/sample/RequestFactory.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.moczul.sample; - -import okhttp3.CacheControl; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; - -public class RequestFactory { - - public static final String TYPE_GET = "type_get"; - public static final String TYPE_POST = "type_post"; - public static final String TYPE_GET_MODIFIED = "type_get_modified"; - - private static final String NEW_REPO_BODY = "{" + - " \"name\": \"Hello-World\"," + - " \"description\": \"This is your first repository\"," + - " \"homepage\": \"https://github.com\"," + - " \"private\": false," + - " \"has_issues\": true," + - " \"has_wiki\": true," + - " \"has_downloads\": true" + - "}"; - - public static Request sampleGetRequest() { - return new Request.Builder() - .url("https://api.github.com/repos/vmg/redcarpet/issues?state=closed") - .cacheControl(CacheControl.FORCE_CACHE) - .build(); - } - - public static Request samplePostRequest() { - return new Request.Builder() - .url("https://api.github.com/user/repos") - .post(RequestBody.create(MediaType.parse("application/json"), NEW_REPO_BODY)) - .build(); - } - - public static Request modifiedGetRequest() { - return new Request.Builder() - .url("https://api.github.com/repos/vmg/redcarpet/issues?state=closed") - .header("Authorization", "Basic bWFjaWVrOnRham5laGFzbG8xMjM=") - .build(); - } - - public static Request getRequest(String type) { - switch (type) { - case TYPE_GET: - return sampleGetRequest(); - case TYPE_POST: - return samplePostRequest(); - case TYPE_GET_MODIFIED: - return modifiedGetRequest(); - default: - throw new IllegalArgumentException("Invalid request type"); - } - } - -} diff --git a/sample/src/main/java/com/moczul/sample/RequestFactory.kt b/sample/src/main/java/com/moczul/sample/RequestFactory.kt new file mode 100644 index 0000000..b51769d --- /dev/null +++ b/sample/src/main/java/com/moczul/sample/RequestFactory.kt @@ -0,0 +1,54 @@ +package com.moczul.sample + +import okhttp3.CacheControl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +object RequestFactory { + + const val TYPE_GET = "type_get" + const val TYPE_POST = "type_post" + const val TYPE_GET_MODIFIED = "type_get_modified" + + private const val NEW_REPO_BODY = "{" + + " \"name\": \"Hello-World\"," + + " \"description\": \"This is your first repository\"," + + " \"homepage\": \"https://github.com\"," + + " \"private\": false," + + " \"has_issues\": true," + + " \"has_wiki\": true," + + " \"has_downloads\": true" + + "}" + + private fun sampleGetRequest(): Request { + return Request.Builder() + .url("https://api.github.com/repos/vmg/redcarpet/issues?state=closed") + .cacheControl(CacheControl.FORCE_CACHE) + .build() + } + + private fun samplePostRequest(): Request { + return Request.Builder() + .url("https://api.github.com/user/repos") + .post(NEW_REPO_BODY.toRequestBody("application/json".toMediaType())) + .build() + } + + private fun modifiedGetRequest(): Request { + return Request.Builder() + .url("https://api.github.com/repos/vmg/redcarpet/issues?state=closed") + .header("Authorization", "Basic bWFjaWVrOnRham5laGFzbG8xMjM=") + .build() + } + + @JvmStatic + fun getRequest(type: String?): Request { + return when (type) { + TYPE_GET -> sampleGetRequest() + TYPE_POST -> samplePostRequest() + TYPE_GET_MODIFIED -> modifiedGetRequest() + else -> throw IllegalArgumentException("Invalid request type") + } + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/moczul/sample/RequestService.java b/sample/src/main/java/com/moczul/sample/RequestService.java deleted file mode 100644 index 35ff8f7..0000000 --- a/sample/src/main/java/com/moczul/sample/RequestService.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.moczul.sample; - -import android.app.IntentService; -import android.content.Intent; - -import com.moczul.ok2curl.CurlInterceptor; -import com.moczul.ok2curl.modifier.HeaderModifier; -import com.moczul.sample.modifier.Base64Decoder; -import com.moczul.sample.modifier.BasicAuthorizationHeaderModifier; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -import okhttp3.OkHttpClient; -import okhttp3.Request; - -public class RequestService extends IntentService { - - public static final String REQUEST_TYPE = "request_type"; - - public RequestService() { - super("RequestService"); - } - - @Override - protected void onHandleIntent(Intent intent) { - final BasicAuthorizationHeaderModifier modifier = new BasicAuthorizationHeaderModifier(new Base64Decoder()); - final List modifiers = Collections.singletonList(modifier); - - final CurlInterceptor curlInterceptor = new CurlInterceptor(new AndroidLogger(), modifiers); - - final OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(curlInterceptor) - .build(); - - final String requestType = intent.getStringExtra(REQUEST_TYPE); - final Request request = RequestFactory.getRequest(requestType); - - try { - // This call will be logged via logcat - client.newCall(request).execute(); - } catch (IOException e) { - e.printStackTrace(); - } - } - -} diff --git a/sample/src/main/java/com/moczul/sample/RequestService.kt b/sample/src/main/java/com/moczul/sample/RequestService.kt new file mode 100644 index 0000000..adcc2de --- /dev/null +++ b/sample/src/main/java/com/moczul/sample/RequestService.kt @@ -0,0 +1,36 @@ +package com.moczul.sample + +import android.app.IntentService +import android.content.Intent +import com.moczul.ok2curl.Configuration +import com.moczul.ok2curl.CurlInterceptor +import com.moczul.sample.modifier.Base64Decoder +import com.moczul.sample.modifier.BasicAuthorizationHeaderModifier +import okhttp3.OkHttpClient +import java.io.IOException + +class RequestService : IntentService("RequestService") { + + override fun onHandleIntent(intent: Intent?) { + val modifier = BasicAuthorizationHeaderModifier(Base64Decoder()) + val config = Configuration(headerModifiers = listOf(modifier)) + val curlInterceptor = CurlInterceptor(AndroidLogger(), config) + + val client = OkHttpClient.Builder() + .addInterceptor(curlInterceptor) + .build() + val requestType = intent?.getStringExtra(REQUEST_TYPE) + val request = RequestFactory.getRequest(requestType) + + try { + // This call will be logged via logcat + client.newCall(request).execute() + } catch (e: IOException) { + e.printStackTrace() + } + } + + companion object { + const val REQUEST_TYPE = "request_type" + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/moczul/sample/modifier/Base64Decoder.java b/sample/src/main/java/com/moczul/sample/modifier/Base64Decoder.java deleted file mode 100644 index 2c5f154..0000000 --- a/sample/src/main/java/com/moczul/sample/modifier/Base64Decoder.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.moczul.sample.modifier; - -import android.util.Base64; - -public class Base64Decoder { - - public String decode(String value) { - final byte[] decodedBytes = Base64.decode(value.getBytes(), Base64.DEFAULT); - return new String(decodedBytes); - } -} diff --git a/sample/src/main/java/com/moczul/sample/modifier/Base64Decoder.kt b/sample/src/main/java/com/moczul/sample/modifier/Base64Decoder.kt new file mode 100644 index 0000000..3b11a59 --- /dev/null +++ b/sample/src/main/java/com/moczul/sample/modifier/Base64Decoder.kt @@ -0,0 +1,11 @@ +package com.moczul.sample.modifier + +import android.util.Base64 + +class Base64Decoder { + + fun decode(value: String): String { + val decodedBytes = Base64.decode(value.toByteArray(), Base64.DEFAULT) + return String(decodedBytes) + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifier.java b/sample/src/main/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifier.java deleted file mode 100644 index 86a18da..0000000 --- a/sample/src/main/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifier.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.moczul.sample.modifier; - -import com.moczul.ok2curl.Header; -import com.moczul.ok2curl.modifier.HeaderModifier; - -public class BasicAuthorizationHeaderModifier implements HeaderModifier { - - private final Base64Decoder base64Decoder; - - public BasicAuthorizationHeaderModifier(Base64Decoder base64Decoder) { - this.base64Decoder = base64Decoder; - } - - @Override - public boolean matches(Header header) { - return "Authorization".equals(header.name()) - && header.value().startsWith("Basic"); - } - - @Override - public Header modify(Header header) { - final String valueToDecode = header.value().replace("Basic", "").trim(); - final String decodedHeaderValue = base64Decoder.decode(valueToDecode); - return new Header(header.name(), decodedHeaderValue); - } -} diff --git a/sample/src/main/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifier.kt b/sample/src/main/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifier.kt new file mode 100644 index 0000000..968e53c --- /dev/null +++ b/sample/src/main/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifier.kt @@ -0,0 +1,16 @@ +package com.moczul.sample.modifier + +import com.moczul.ok2curl.Header +import com.moczul.ok2curl.modifier.HeaderModifier + +class BasicAuthorizationHeaderModifier(private val base64Decoder: Base64Decoder) : HeaderModifier { + override fun matches(header: Header): Boolean { + return "Authorization" == header.name && header.value.startsWith("Basic") + } + + override fun modify(header: Header): Header { + val valueToDecode = header.value.replace("Basic", "").trim() + val decodedHeaderValue = base64Decoder.decode(valueToDecode) + return Header(name = header.name, value = decodedHeaderValue) + } +} \ No newline at end of file diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 6cb9953..737b75f 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - Sample + Ok2Curl Sample Sample GET request Sample POST request Get request with modified header diff --git a/sample/src/test/java/com/moczul/sample/ExampleUnitTest.java b/sample/src/test/java/com/moczul/sample/ExampleUnitTest.java deleted file mode 100644 index 251832f..0000000 --- a/sample/src/test/java/com/moczul/sample/ExampleUnitTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.moczul.sample; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * To work on unit tests, switch the Test Artifact in the Build Variants view. - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} diff --git a/sample/src/test/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifierTest.java b/sample/src/test/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifierTest.java deleted file mode 100644 index c40b17d..0000000 --- a/sample/src/test/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifierTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.moczul.sample.modifier; - -import com.moczul.ok2curl.Header; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class BasicAuthorizationHeaderModifierTest { - @Mock - Base64Decoder base64Decoder; - @InjectMocks - BasicAuthorizationHeaderModifier headerModifier; - - private final Header basicAuthHeader = new Header("Authorization", "Basic bWFjaWVrOnRham5laGFzbG8xMjM="); - - @Test - public void basicAuthorizationHeader_shouldMatch_modifier() throws Exception { - assertThat(headerModifier.matches(basicAuthHeader), is(true)); - } - - @Test - public void nonBasicAuthorizationHeader_shouldNotMatch_modifier() throws Exception { - final Header header = new Header("Authorization", "Non basic header value"); - - assertThat(headerModifier.matches(header), is(false)); - } - - @Test - public void modifier_shouldReturnHeader_withDecodedValue() throws Exception { - when(base64Decoder.decode("bWFjaWVrOnRham5laGFzbG8xMjM=")).thenReturn("maciej:tajnehaslo123"); - - final Header modifiedHeader = headerModifier.modify(basicAuthHeader); - - assertThat(modifiedHeader.name(), is("Authorization")); - assertThat(modifiedHeader.value(), is("maciej:tajnehaslo123")); - } -} diff --git a/sample/src/test/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifierTest.kt b/sample/src/test/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifierTest.kt new file mode 100644 index 0000000..ae5692e --- /dev/null +++ b/sample/src/test/java/com/moczul/sample/modifier/BasicAuthorizationHeaderModifierTest.kt @@ -0,0 +1,48 @@ +package com.moczul.sample.modifier + +import com.moczul.ok2curl.Header +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class BasicAuthorizationHeaderModifierTest { + + @Mock + private lateinit var base64Decoder: Base64Decoder + + @InjectMocks + private lateinit var headerModifier: BasicAuthorizationHeaderModifier + + + @Test + fun basicAuthorizationHeader_shouldMatch_modifier() { + val header = Header("Authorization", "Basic bWFjaWVrOnRham5laGFzbG8xMjM=") + + assertTrue(headerModifier.matches(header)) + } + + @Test + fun nonBasicAuthorizationHeader_shouldNotMatch_modifier() { + val header = Header("Authorization", "Non basic header value") + + assertFalse(headerModifier.matches(header)) + } + + @Test + fun modifier_shouldReturnHeader_withDecodedValue() { + `when`(base64Decoder.decode("bWFjaWVrOnRham5laGFzbG8xMjM=")).thenReturn("maciej:tajnehaslo123") + val header = Header("Authorization", "Basic bWFjaWVrOnRham5laGFzbG8xMjM=") + + val modifiedHeader = headerModifier.modify(header) + + val expectedHeader = Header(name = "Authorization", value = "maciej:tajnehaslo123") + assertEquals(expectedHeader, modifiedHeader) + } +} \ No newline at end of file From c2882afda097eecb59b652244b060c3ab6e5cdf6 Mon Sep 17 00:00:00 2001 From: Michal Moczulski Date: Tue, 8 Feb 2022 21:42:25 +0100 Subject: [PATCH 4/9] Code cleanup --- .../java/com/moczul/ok2curl/Configuration.kt | 1 - .../moczul/ok2curl/CurlCommandGenerator.kt | 8 ++++---- .../com/moczul/ok2curl/CurlInterceptor.kt | 1 - .../src/main/java/com/moczul/ok2curl/Flags.kt | 16 +++++---------- .../main/java/com/moczul/ok2curl/Header.kt | 2 +- .../java/com/moczul/ok2curl/LimitedSink.kt | 2 +- .../com/moczul/ok2curl/CookieHandlerTest.kt | 3 ++- .../ok2curl/CurlCommandGeneratorTest.kt | 10 +++------- .../test/java/com/moczul/ok2curl/FlagsTest.kt | 20 +++++++------------ sample/build.gradle | 7 ------- 10 files changed, 23 insertions(+), 47 deletions(-) diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/Configuration.kt b/ok2curl/src/main/java/com/moczul/ok2curl/Configuration.kt index 51430bc..5f1e01a 100644 --- a/ok2curl/src/main/java/com/moczul/ok2curl/Configuration.kt +++ b/ok2curl/src/main/java/com/moczul/ok2curl/Configuration.kt @@ -1,6 +1,5 @@ package com.moczul.ok2curl - import com.moczul.ok2curl.modifier.HeaderModifier class Configuration @JvmOverloads constructor( diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/CurlCommandGenerator.kt b/ok2curl/src/main/java/com/moczul/ok2curl/CurlCommandGenerator.kt index e7c85f7..1e8266c 100644 --- a/ok2curl/src/main/java/com/moczul/ok2curl/CurlCommandGenerator.kt +++ b/ok2curl/src/main/java/com/moczul/ok2curl/CurlCommandGenerator.kt @@ -24,10 +24,10 @@ class CurlCommandGenerator(private val configuration: Configuration) { request: Request ): List { return when (commandComponent) { - CommandComponent.Curl -> listOf("curl") - CommandComponent.Url -> generateUrl(request.url) - CommandComponent.Flags -> generateFlags() - CommandComponent.Body -> generateBody(request.body) + CommandComponent.Curl -> listOf("curl") + CommandComponent.Url -> generateUrl(request.url) + CommandComponent.Flags -> generateFlags() + CommandComponent.Body -> generateBody(request.body) CommandComponent.Method -> generateMethod(request.method) CommandComponent.Header -> generateHeaders(request.headers, request.body) } diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.kt b/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.kt index 5334095..0fde228 100644 --- a/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.kt +++ b/ok2curl/src/main/java/com/moczul/ok2curl/CurlInterceptor.kt @@ -15,7 +15,6 @@ class CurlInterceptor @JvmOverloads constructor( val request = chain.request() val curl = curlGenerator.generate(request) - logger.log(curl) return chain.proceed(request) diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/Flags.kt b/ok2curl/src/main/java/com/moczul/ok2curl/Flags.kt index 63946d9..043abfc 100644 --- a/ok2curl/src/main/java/com/moczul/ok2curl/Flags.kt +++ b/ok2curl/src/main/java/com/moczul/ok2curl/Flags.kt @@ -1,15 +1,13 @@ package com.moczul.ok2curl -import java.util.* -import kotlin.collections.ArrayList -import kotlin.collections.HashSet +import java.util.Locale class Flags private constructor(options: Collection) { private val options: List init { - this.options = ArrayList(options) + this.options = options.toList() } fun list(): List { @@ -17,7 +15,7 @@ class Flags private constructor(options: Collection) { } class Builder { - private val options: MutableSet = HashSet() + private val options: MutableSet = mutableSetOf() fun insecure(): Builder { options.add("--insecure") return this @@ -48,9 +46,7 @@ class Flags private constructor(options: Collection) { return this } - fun build(): Flags { - return Flags(options) - } + fun build() = Flags(options) } companion object { @@ -58,8 +54,6 @@ class Flags private constructor(options: Collection) { val EMPTY = Flags(emptyList()) @JvmStatic - fun builder(): Builder { - return Builder() - } + fun builder() = Builder() } } \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/Header.kt b/ok2curl/src/main/java/com/moczul/ok2curl/Header.kt index 975f456..882b856 100644 --- a/ok2curl/src/main/java/com/moczul/ok2curl/Header.kt +++ b/ok2curl/src/main/java/com/moczul/ok2curl/Header.kt @@ -1,3 +1,3 @@ package com.moczul.ok2curl -data class Header( val name: String, val value: String) \ No newline at end of file +data class Header(val name: String, val value: String) \ No newline at end of file diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt b/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt index 3a4c2c7..ba8b917 100644 --- a/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt +++ b/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt @@ -14,7 +14,7 @@ class LimitedSink(limited: Buffer, limit: Long) : Sink { init { require(limit > 0) { "limit has to be grater than 0" } this.limited = limited - total = limit + this.total = limit } @Throws(IOException::class) diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.kt b/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.kt index 5bbe1cc..d0460a6 100644 --- a/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.kt +++ b/ok2curl/src/test/java/com/moczul/ok2curl/CookieHandlerTest.kt @@ -1,4 +1,5 @@ -import com.moczul.ok2curl.CurlInterceptor +package com.moczul.ok2curl + import com.moczul.ok2curl.logger.Logger import okhttp3.Cookie import okhttp3.CookieJar diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/CurlCommandGeneratorTest.kt b/ok2curl/src/test/java/com/moczul/ok2curl/CurlCommandGeneratorTest.kt index 880bd69..946c7f1 100644 --- a/ok2curl/src/test/java/com/moczul/ok2curl/CurlCommandGeneratorTest.kt +++ b/ok2curl/src/test/java/com/moczul/ok2curl/CurlCommandGeneratorTest.kt @@ -1,9 +1,5 @@ -package com.moczul.ok2curl.util +package com.moczul.ok2curl -import com.moczul.ok2curl.CommandComponent -import com.moczul.ok2curl.Configuration -import com.moczul.ok2curl.CurlCommandGenerator -import com.moczul.ok2curl.Flags import okhttp3.CacheControl import okhttp3.FormBody import okhttp3.Request @@ -182,9 +178,9 @@ class CurlCommandGeneratorTest { } private companion object { - private fun body() = FormBody.Builder().add("key1", "value1").build() + fun body() = FormBody.Builder().add("key1", "value1").build() - private fun oneDayCache() = + fun oneDayCache() = CacheControl.Builder().maxAge(1, TimeUnit.DAYS).onlyIfCached().build() } } \ No newline at end of file diff --git a/ok2curl/src/test/java/com/moczul/ok2curl/FlagsTest.kt b/ok2curl/src/test/java/com/moczul/ok2curl/FlagsTest.kt index cd1d77b..ceefb01 100644 --- a/ok2curl/src/test/java/com/moczul/ok2curl/FlagsTest.kt +++ b/ok2curl/src/test/java/com/moczul/ok2curl/FlagsTest.kt @@ -1,9 +1,8 @@ -package com.moczul.ok2curl.util +package com.moczul.ok2curl -import com.moczul.ok2curl.Flags import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.CoreMatchers.hasItem import org.hamcrest.MatcherAssert.assertThat +import org.junit.Assert.assertEquals import org.junit.Test class FlagsTest { @@ -27,10 +26,8 @@ class FlagsTest { .build() // then - assertThat(flags.list().size, `is`(3)) - assertThat>(flags.list(), hasItem("--max-time 120")) - assertThat>(flags.list(), hasItem("--connect-timeout 60")) - assertThat>(flags.list(), hasItem("--retry 3")) + val expectedFlags = listOf("--max-time 120", "--connect-timeout 60", "--retry 3") + assertEquals(expectedFlags, flags.list()) } @Test @@ -43,10 +40,8 @@ class FlagsTest { .build() // then - assertThat(flags.list().size, `is`(3)) - assertThat>(flags.list(), hasItem("--insecure")) - assertThat>(flags.list(), hasItem("--compressed")) - assertThat>(flags.list(), hasItem("--location")) + val expectedFlags = listOf("--insecure", "--compressed", "--location") + assertEquals(expectedFlags, flags.list()) } @Test @@ -59,7 +54,6 @@ class FlagsTest { .build() // then - assertThat(flags.list().size, `is`(1)) - assertThat>(flags.list(), hasItem("--insecure")) + assertEquals(listOf("--insecure"), flags.list()) } } \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index 11bed1f..636c8e5 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -22,12 +22,6 @@ android { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } - buildFeatures { - viewBinding true - } - kotlinOptions { - jvmTarget = '1.8' - } } repositories { @@ -59,6 +53,5 @@ dependencies { implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.3.1' testImplementation 'org.mockito:mockito-inline:4.3.1' } From 4b8a12b338cde3dec1980f41c8b1c8ff2f8691b9 Mon Sep 17 00:00:00 2001 From: Michal Moczulski Date: Sun, 10 Apr 2022 11:18:44 +0200 Subject: [PATCH 5/9] Bump Gradle and libs dependencies, bump compileSdk --- build.gradle | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 55190 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 53 ++++++++++++++--------- gradlew.bat | 43 ++++++++++-------- sample/build.gradle | 14 +++--- 6 files changed, 67 insertions(+), 49 deletions(-) diff --git a/build.gradle b/build.gradle index 7ed03ed..a982cc9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.6.20' repositories { google() @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.4' + classpath 'com.android.tools.build:gradle:7.1.2' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' classpath 'io.github.gradle-nexus:publish-plugin:1.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 87b738cbd051603d91cc39de6cb000dd98fe6b02..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 26193 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

$|c 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∨fd|u2@Kuk2 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 F_djzcFxCJ7 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f80bbf..aa991fc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index af6708f..4f906e0 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,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 @@ -109,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 @@ -138,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 @@ -159,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 0f8d593..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@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 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, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ 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" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe 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. @@ -35,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% @@ -45,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/sample/build.gradle b/sample/build.gradle index 636c8e5..0846645 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -2,11 +2,11 @@ apply plugin: 'com.android.application' apply plugin: 'org.jetbrains.kotlin.android' android { - compileSdkVersion 31 + compileSdkVersion 32 defaultConfig { applicationId "com.moczul.sample" minSdkVersion 16 - targetSdkVersion 31 + targetSdkVersion 32 versionCode 1 versionName "1.0" } @@ -45,12 +45,12 @@ dependencies { * } */ - implementation 'androidx.appcompat:appcompat:1.4.0' + implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.squareup.okhttp3:okhttp:4.9.3' - implementation 'com.google.android.material:material:1.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' - implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' + implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2' + implementation 'androidx.navigation:navigation-ui-ktx:2.4.2' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:4.3.1' From 23fe43d5ad0a11b5edcfc0cf27160bc5ba6f1192 Mon Sep 17 00:00:00 2001 From: Michal Moczulski Date: Sun, 10 Apr 2022 11:19:22 +0200 Subject: [PATCH 6/9] Remove extra workflow step --- .github/workflows/push.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 91ee3e3..5cc98ad 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -11,7 +11,5 @@ jobs: uses: actions/setup-java@v1 with: java-version: '11' - - name: Grant execute permission for gradlew - run: chmod +x gradlew - name: Build with Gradle run: ./gradlew clean build \ No newline at end of file From fd10dd7683f98c847cd4693fb51ba331a4d72fcc Mon Sep 17 00:00:00 2001 From: Michal Moczulski Date: Sun, 10 Apr 2022 11:20:47 +0200 Subject: [PATCH 7/9] Bump library version --- README.md | 2 +- ok2curl/build.gradle | 2 +- sample/build.gradle | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 023d945..76d0851 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ repositories { } dependencies { - implementation 'com.github.mrmike:ok2curl:0.7.0' + implementation 'com.github.mrmike:ok2curl:0.8.0' } ``` diff --git a/ok2curl/build.gradle b/ok2curl/build.gradle index f492330..0422dd4 100644 --- a/ok2curl/build.gradle +++ b/ok2curl/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'maven-publish' ext { PUBLISH_GROUP_ID = 'com.github.mrmike' - PUBLISH_VERSION = '0.7.1' + PUBLISH_VERSION = '0.8.0' PUBLISH_ARTIFACT_ID = 'ok2curl' } diff --git a/sample/build.gradle b/sample/build.gradle index 0846645..2e37db0 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -33,14 +33,14 @@ dependencies { implementation project(':ok2curl') /** - * implementation 'com.github.mrmike:ok2curl:0.7.0' + * implementation 'com.github.mrmike:ok2curl:0.8.0' */ /** * Ok2Curl is packed with the latest version of OkHttp. If you already have OkHttp declared in * your dependencies or want to use different version simply add exclude module. * - * implementation ('com.github.mrmike:ok2curl:0.7.0') { + * implementation ('com.github.mrmike:ok2curl:0.8.0') { * exclude module: 'okhttp' * } */ From 895b0c541bbd8561f1c661656919089b29f66f72 Mon Sep 17 00:00:00 2001 From: Michal Moczulski Date: Sun, 10 Apr 2022 11:44:26 +0200 Subject: [PATCH 8/9] Make LimitedSink internal --- ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt b/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt index ba8b917..f3d8a4f 100644 --- a/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt +++ b/ok2curl/src/main/java/com/moczul/ok2curl/LimitedSink.kt @@ -6,7 +6,7 @@ import okio.Timeout import java.io.IOException import kotlin.math.min -class LimitedSink(limited: Buffer, limit: Long) : Sink { +internal class LimitedSink(limited: Buffer, limit: Long) : Sink { private val limited: Buffer private var total: Long From 443891d430786815420d9e943b1b459fc34bbfab Mon Sep 17 00:00:00 2001 From: Michal Moczulski Date: Sun, 10 Apr 2022 11:59:21 +0200 Subject: [PATCH 9/9] Update readme --- README.md | 88 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 76d0851..bad289d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Ok2Curl [![Commit check](https://github.com/mrmike/Ok2Curl/actions/workflows/push.yml/badge.svg)](https://github.com/mrmike/Ok2Curl/actions/workflows/push.yml) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Ok2Curl-green.svg?style=flat)](https://android-arsenal.com/details/1/2653) [![Release](https://jitpack.io/v/mrmike/Ok2Curl.svg)](https://jitpack.io/#mrmike/Ok2Curl) -Convert OkHttp requests into curl logs. +Simply way to transform OkHttp requests into curl logs. ## Usage Add library to project dependencies. Library is hosted on Maven Central. @@ -15,15 +15,14 @@ dependencies { ``` To start logging requests with Ok2Curl add interceptor to OkHttp client. -```java -OkHttpClient okHttp = new OkHttpClient.Builder() - .addInterceptor(new CurlInterceptor(new Loggable() { - @Override - public void log(String message) { - Log.v("Ok2Curl", message); - } - })) - .build(); +```kotlin +val client = OkHttpClient.Builder() + .addInterceptor(CurlInterceptor(object : Logger { + override fun log(message: String) { + Log.v("Ok2Curl", message) + } + })) + .build() ``` ## Result @@ -36,34 +35,67 @@ curl -X GET -H "Cache-Control:max-stale=2147483647, only-if-cached" https://api. ## Network interceptors By default Ok2Curl uses application interceptors from OkHttp which is adequate for most cases. But sometimes you may want to use network interceptor e.g. to log Cookies set via [CookieHandler](http://docs.oracle.com/javase/6/docs/api/java/net/CookieHandler.html). In such a case add interceptor the same way as below: -``` -OkHttpClient okHttp = new OkHttpClient.Builder() - .addNetworkInterceptor(new CurlInterceptor()) - .build(); +```kotlin +val client = OkHttpClient.Builder() + .addNetworkInterceptor(CurlInterceptor(logger)) + .build() ``` To get know more about Interceptor in OkHttp take a look here: https://github.com/square/okhttp/wiki/Interceptors +## Configuration + +`CurlInterceptor` accepts an optional configuration object. With `Configuration` you can specify various options like: +* header modifiers - custom logic for modifying header values +* components - list of required command components +* flags - optional curl flags +* limit - bytes limit for body +* delimiter for command components + +```kotlin +class Configuration( + val headerModifiers: List = emptyList(), + val components: List = CommandComponent.DEFAULT, + val flags: Flags = Flags.EMPTY, + val limit: Long = 1024L * 1024L, + val delimiter: String = " " +) +``` + ## Header modifiers -Ok2Curl allows you to modify any header before creating curl command. All you have to do is create your own modifier that implements [HeaderModifier](https://github.com/mrmike/Ok2Curl/blob/master/ok2curl/src/main/java/com/moczul/ok2curl/modifier/HeaderModifier.java) -and add this modifier to CurlInterceptor. See [sample](https://github.com/mrmike/Ok2Curl/blob/master/sample/src/main/java/com/moczul/sample/RequestService.java) for reference. +Ok2Curl allows you to modify any header before creating curl command. All you have to do is create your own modifier that implements [HeaderModifier](https://github.com/mrmike/Ok2Curl/blob/master/ok2curl/src/main/java/com/moczul/ok2curl/modifier/HeaderModifier.kt) +and add this modifier to CurlInterceptor. See [sample](https://github.com/mrmike/Ok2Curl/blob/master/sample/src/main/java/com/moczul/sample/RequestService.kt) for reference. +``` +val modifier = BasicAuthorizationHeaderModifier(Base64Decoder()) +val configuration = Configuration(headerModifiers = listOf(modifier)) +val curlInterceptor = CurlInterceptor(AndroidLogger(), configuration) ``` -final BasicAuthorizationHeaderModifier modifier = new BasicAuthorizationHeaderModifier(new Base64Decoder()); -final List modifiers = Collections.singletonList(modifier); -final CurlInterceptor curlInterceptor = new CurlInterceptor(new AndroidLogger(), modifiers); +## Command Components +With Ok2Curl configuration you can specify a list of components for curl command. For instance, +you can skip header, body, and flag components. +```kotlin +val components = listOf(Curl, Method, Url) +val configuration = Configuration(components = components) +val curlInterceptor = CurlInterceptor(AndroidLogger(), configuration) ``` -## Options -Ok2Curl supports basic Curl options. In order to use options use the following code: +As a result, CurlInterceptor will receive given simplified command +```shell +// Headers, body and flags are skipped +curl -X GET https://api.github.com/repos/vmg/redcarpet/issues?state=closed ``` -final Options options = Options.builder() - .insecure() - .connectTimeout(120) - .retry(5) - .build(); -final CurlInterceptor interceptor = new CurlInterceptor(logger, options); +## Flags +Ok2Curl supports basic Curl options. To use options use the following code: +```kotlin +val flags = Flags.builder() + .insecure() + .connectTimeout(seconds = 120) + .retry(5) + .build() +val configuration = Configuration(flags = flags) +val curlInterceptor = CurlInterceptor(AndroidLogger(), configuration) ``` Since now every Curl command will start with `curl --insecure --connect-timeout 120 --retry 5...` @@ -75,7 +107,7 @@ Since now every Curl command will start with `curl --insecure --connect-timeout * --compressed * --location -If would like to support any new options please feel free to open PR. Full list of curl options is +If would like to support any new options please feel free to open PR. A full list of curl options is available [here](https://curl.haxx.se/docs/manpage.html).