Basic integration test confirming that dackka can be invoked from the command line Bug: 161384734 Test: ./gradlew test Change-Id: Ife1d82c34d2eb8a8c282da667a692c13a5b15e40
diff --git a/integration-tests/cli/build.gradle.kts b/integration-tests/cli/build.gradle.kts new file mode 100644 index 0000000..e7a1ebc --- /dev/null +++ b/integration-tests/cli/build.gradle.kts
@@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +// This project confirms that Dokka can be invoked as a .jar from the command line + +val generatedDir = project.file("${buildDir}/docs") + +configurations.create("runnerJar") + +dependencies.add( + "runnerJar", + project.dependencies.project( + mapOf( + "path" to ":", + "configuration" to "shadow" + ) + ) +) +tasks.register<Copy>("generateConfig") { + from("src/test/resources") + into("${project.buildDir}/resources") + filter({ text -> + // Dackka requires absolute paths, so expand "$projectDir" into its value + text.replace("\$projectDir", "$projectDir") + }) +} + +tasks.register<JavaExec>("run") { + description = "Ensures that dackka can be invoked as a .jar from the command line" + dependsOn(project.configurations.getByName("runnerJar")) + dependsOn("generateConfig") + classpath = files({ + project.configurations.getByName("runnerJar").resolvedConfiguration.files + }) + args = listOf("${project.buildDir}/resources/config.json") + doFirst { + generatedDir.deleteRecursively() + } +} + +tasks.register<Task>("verifyRun") { + dependsOn("run") + doLast { + val expectedPaths = listOf("index.html", + "androidx.paging/Pager.html", + "androidx.paging/PagingDataKt.html", + "androidx.paging/PagingData.html") + for (relativePath in expectedPaths) { + val path = "${generatedDir}/$relativePath" + if (!file(path).exists()) { + throw GradleException("Failed to create " + path) + } + } + } +} +tasks.register<Task>("test") { + dependsOn("verifyRun") +}
diff --git a/integration-tests/cli/src/test/resources/config.json b/integration-tests/cli/src/test/resources/config.json new file mode 100644 index 0000000..9317d95 --- /dev/null +++ b/integration-tests/cli/src/test/resources/config.json
@@ -0,0 +1,27 @@ +{ + "format": "javadoc", + "outputDir": "build/docs", + "sourceSets": [ + { + "moduleDisplayName": "Sample", + "externalDocumentationLinks": [ + { + "url": "https://kotlinlang.org/api/latest/jvm/stdlib/", + "packageListUrl": "https://kotlinlang.org/api/latest/jvm/stdlib/package-list" + }, + { + "url": "https://docs.oracle.com/javase/8/docs/api/", + "packageListUrl": "https://docs.oracle.com/javase/8/docs/api/package-list" + } + ], + "sourceSetID": { + "moduleName": "sample", + "sourceSetName": "main" + }, + "sourceRoots": [ + {"path":"$projectDir/src/testData"} + ] + } + ] +} +
diff --git a/integration-tests/cli/src/testData/androidx/paging/CombinedLoadStates.kt b/integration-tests/cli/src/testData/androidx/paging/CombinedLoadStates.kt new file mode 100644 index 0000000..a77b3e2 --- /dev/null +++ b/integration-tests/cli/src/testData/androidx/paging/CombinedLoadStates.kt
@@ -0,0 +1,56 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +package androidx.paging + +/** + * Collection of pagination [LoadState]s for both a [PagingSource], and [RemoteMediator]. + */ +data class CombinedLoadStates( + /** + * [LoadStates] corresponding to loads from a [PagingSource]. + */ + val source: LoadStates, + + /** + * [LoadStates] corresponding to loads from a [RemoteMediator], or `null` if RemoteMediator + * not present. + */ + val mediator: LoadStates? = null +) { + val refresh: LoadState = (mediator ?: source).refresh + val prepend: LoadState = (mediator ?: source).prepend + val append: LoadState = (mediator ?: source).append + + inline fun forEach(op: (LoadType, Boolean, LoadState) -> Unit) { + source.forEach { type, state -> + op(type, false, state) + } + mediator?.forEach { type, state -> + op(type, true, state) + } + } + + internal companion object { + val IDLE_SOURCE = CombinedLoadStates( + source = LoadStates.IDLE + ) + val IDLE_MEDIATOR = CombinedLoadStates( + source = LoadStates.IDLE, + mediator = LoadStates.IDLE + ) + } +}
diff --git a/integration-tests/cli/src/testData/androidx/paging/Pager.kt b/integration-tests/cli/src/testData/androidx/paging/Pager.kt new file mode 100644 index 0000000..049ef37 --- /dev/null +++ b/integration-tests/cli/src/testData/androidx/paging/Pager.kt
@@ -0,0 +1,58 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +package androidx.paging + +import kotlinx.coroutines.flow.Flow + +/** + * Primary entry point into Paging; constructor for a reactive stream of [PagingData]. + * + * Each [PagingData] represents a snapshot of the backing paginated data. Updates to the backing + * dataset should be represented by a new instance of [PagingData]. + * + * [PagingSource.invalidate] and calls to [AsyncPagingDataDiffer.refresh] or + * [PagingDataAdapter.refresh] will notify [Pager] that the backing dataset has been updated and a + * new [PagingData] / [PagingSource] pair will be generated to represent an updated snapshot. + * + * [PagingData] can be transformed to alter data as it loads, and presented in a `RecyclerView` via + * `AsyncPagingDataDiffer` or `PagingDataAdapter`. + * + * LiveData support is available as an extension property provided by the + * `androidx.paging:paging-runtime` artifact. + * + * RxJava support is available as extension properties provided by the + * `androidx.paging:paging-rxjava2` artifact. + */ +class Pager<Key : Any, Value : Any> +@JvmOverloads constructor( + config: PagingConfig, + initialKey: Key? = null, + remoteMediator: RemoteMediator<Key, Value>? = null, + pagingSourceFactory: () -> PagingSource<Key, Value> +) { + /** + * A cold [Flow] of [PagingData], which emits new instances of [PagingData] once they become + * invalidated by [PagingSource.invalidate] or calls to [AsyncPagingDataDiffer.refresh] or + * [PagingDataAdapter.refresh]. + */ + val flow: Flow<PagingData<Value>> = PageFetcher( + pagingSourceFactory, + initialKey, + config, + remoteMediator + ).flow +}
diff --git a/integration-tests/cli/src/testData/androidx/paging/PagingData.kt b/integration-tests/cli/src/testData/androidx/paging/PagingData.kt new file mode 100644 index 0000000..c73c34b --- /dev/null +++ b/integration-tests/cli/src/testData/androidx/paging/PagingData.kt
@@ -0,0 +1,293 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * 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 + * + * http://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. + */ + +package androidx.paging + +import androidx.annotation.CheckResult +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** + * Container for Paged data from a single generation of loads. + * + * Each refresh of data (generally either pushed by local storage, or pulled from the network) + * will have a separate corresponding [PagingData]. + */ +class PagingData<T : Any> internal constructor( + internal val flow: Flow<PageEvent<T>>, + internal val receiver: UiReceiver +) { + /** + * Returns a [PagingData] containing the result of applying the given [transform] to each + * element, as it is loaded. + * + * @see map + */ + @JvmName("map") + @CheckResult + fun <R : Any> mapSync(transform: (T) -> R): PagingData<R> = transform { event -> + event.map { transform(it) } + } + + /** + * Returns a [PagingData] of all elements returned from applying the given [transform] + * to each element, as it is loaded. + * + * @see flatMap + */ + @JvmName("flatMap") + @CheckResult + fun <R : Any> flatMapSync(transform: (T) -> Iterable<R>): PagingData<R> = transform { event -> + event.flatMap { transform(it) } + } + + /** + * Returns a [PagingData] containing only elements matching the given [predicate] + * + * @see filter + */ + @JvmName("filter") + @CheckResult + fun filterSync(predicate: (T) -> Boolean): PagingData<T> = transform { event -> + event.filter { predicate(it) } + } + + /** + * Returns a [PagingData] containing each original element, with the passed header [item] added + * to the start of the list. + * + * The header [item] is added to a loaded page which marks the end of the data stream in the + * prepend direction by returning null in [PagingSource.LoadResult.Page.prevKey]. It will be + * removed if the first page in the list is dropped, which can happen in the case of loaded + * pages exceeding [PagedList.Config.maxSize]. + * + * Note: This operation is not idempotent, calling it multiple times will continually add + * more headers to the start of the list, which can be useful if multiple header items are + * required. + * + * @see [insertFooterItem] + */ + @CheckResult + fun insertHeaderItem(item: T) = insertSeparators { before, _ -> + if (before == null) item else null + } + + /** + * Returns a [PagingData] containing each original element, with the passed footer [item] added + * to the end of the list. + * + * The footer [item] is added to a loaded page which marks the end of the data stream in the + * append direction, either by returning null in [PagingSource.LoadResult.Page.nextKey]. It + * will be removed if the first page in the list is dropped, which can happen in the case of + * loaded* pages exceeding [PagedList.Config.maxSize]. + * + * Note: This operation is not idempotent, calling it multiple times will continually add + * more footers to the end of the list, which can be useful if multiple footer items are + * required. + * + * @see [insertHeaderItem] + */ + @CheckResult + fun insertFooterItem(item: T) = insertSeparators { _, after -> + if (after == null) item else null + } + + companion object { + internal val NOOP_RECEIVER = object : UiReceiver { + override fun addHint(hint: ViewportHint) {} + + override fun retry() {} + + override fun refresh() {} + } + + private val EMPTY = PagingData<Any>( + flow = flowOf(PageEvent.Insert.EMPTY_REFRESH_LOCAL), + receiver = NOOP_RECEIVER + ) + + @Suppress("UNCHECKED_CAST", "SyntheticAccessor") + @JvmStatic // Convenience for Java developers. + fun <T : Any> empty() = EMPTY as PagingData<T> + + // NOTE: samples in the doc below are manually imported from Java code in the samples + // project, since Java cannot be linked with @sample. + // DO NOT CHANGE THE BELOW COMMENT WITHOUT MAKING THE CORRESPONDING CHANGE IN `samples/` + /** + * Returns a [PagingData] containing each original element, with an optional separator + * generated by [generator], given the elements before and after (or null, in boundary + * conditions). + * + * Note that this transform is applied asynchronously, as pages are loaded. Potential + * separators between pages are only computed once both pages are loaded. + * + * **Kotlin callers should instead use the extension function [insertSeparators]** + * + * ``` + * /* + * * Create letter separators in an alphabetically sorted list. + * * + * * For example, if the input is: + * * "apple", "apricot", "banana", "carrot" + * * + * * The operator would output: + * * "A", "apple", "apricot", "B", "banana", "C", "carrot" + * */ + * pagingDataStream.map((pagingData) -> + * // map outer stream, so we can perform transformations on each paging generation + * PagingData.insertSeparators(pagingData, + * (@Nullable String before, @Nullable String after) -> { + * if (after != null && (before == null + * || before.charAt(0) != after.charAt(0))) { + * // separator - after is first item that starts with its first letter + * return Character.toString(Character.toUpperCase(after.charAt(0))); + * } else { + * // no separator - either end of list, or first + * // letters of items are the same + * return null; + * } + * })); + * + * /* + * * Create letter separators in an alphabetically sorted list of Items, with UiModel objects. + * * + * * For example, if the input is (each an `Item`): + * * "apple", "apricot", "banana", "carrot" + * * + * * The operator would output a list of UiModels corresponding to: + * * "A", "apple", "apricot", "B", "banana", "C", "carrot" + * */ + * pagingDataStream.map((itemPagingData) -> { + * // map outer stream, so we can perform transformations on each paging generation + * + * // first convert items in stream to UiModel.Item + * PagingData<UiModel.ItemModel> itemModelPagingData = + * itemPagingData.map(UiModel.ItemModel::new); + * + * // Now insert UiModel.Separators, which makes the PagingData of generic type UiModel + * return PagingData.insertSeparators( + * itemModelPagingData, + * (@Nullable UiModel.ItemModel before, @Nullable UiModel.ItemModel after) -> { + * if (after != null && (before == null + * || before.item.label.charAt(0) != after.item.label.charAt(0))) { + * // separator - after is first item that starts with its first letter + * return new UiModel.SeparatorModel( + * Character.toUpperCase(after.item.label.charAt(0))); + * } else { + * // no separator - either end of list, or first + * // letters of items are the same + * return null; + * } + * }); + * }); + * + * public class UiModel { + * static class ItemModel extends UiModel { + * public Item item; + * ItemModel(Item item) { + * this.item = item; + * } + * } + * static class SeparatorModel extends UiModel { + * public char character; + * SeparatorModel(char character) { + * this.character = character; + * } + * } + * } + */ + @JvmStatic + @CheckResult + fun <T : R, R : Any> insertSeparators( + pagingData: PagingData<T>, + generator: (T?, T?) -> R? + ): PagingData<R> { + return pagingData.insertSeparators { before, after -> generator(before, after) } + } + } +} + +private inline fun <T : Any, R : Any> PagingData<T>.transform( + crossinline transform: suspend (PageEvent<T>) -> PageEvent<R> +) = PagingData( + flow = flow.map { transform(it) }, + receiver = receiver +) + +/** + * Returns a [PagingData] containing the result of applying the given [transform] to each + * element, as it is loaded. + */ +@CheckResult +@JvmSynthetic +fun <T : Any, R : Any> PagingData<T>.map( + transform: suspend (T) -> R +): PagingData<R> = transform { it.map(transform) } + +/** + * Returns a [PagingData] of all elements returned from applying the given [transform] + * to each element, as it is loaded. + */ +@CheckResult +@JvmSynthetic +fun <T : Any, R : Any> PagingData<T>.flatMap( + transform: suspend (T) -> Iterable<R> +): PagingData<R> = transform { it.flatMap(transform) } + +/** + * Returns a [PagingData] containing only elements matching the given [predicate] + */ +@CheckResult +@JvmSynthetic +fun <T : Any> PagingData<T>.filter( + predicate: suspend (T) -> Boolean +): PagingData<T> = transform { it.filter(predicate) } + +/** + * Returns a [PagingData] containing each original element, with an optional separator + * generated by [generator], given the elements before and after (or null, in boundary + * conditions). + * + * Note that this transform is applied asynchronously, as pages are loaded. Potential + * separators between pages are only computed once both pages are loaded. + * + * @sample androidx.paging.samples.insertSeparatorsSample + * @sample androidx.paging.samples.insertSeparatorsUiModelSample + */ +@CheckResult +@JvmSynthetic +fun <T : R, R : Any> PagingData<T>.insertSeparators( + generator: suspend (T?, T?) -> R? +): PagingData<R> { + // This function must be an extension method, as it indirectly imposes a constraint on + // the type of T (because T extends R). Ideally it would be declared not be an + // extension, to make this method discoverable for Java callers, but we need to support + // the common UI model pattern for separators: + // class UiModel + // class ItemModel: UiModel + // class SeparatorModel: UiModel + return PagingData( + flow = flow.insertEventSeparators(generator), + receiver = receiver + ) +} + +internal interface UiReceiver { + fun addHint(hint: ViewportHint) + fun retry() + fun refresh() +}
diff --git a/settings.gradle.kts b/settings.gradle.kts index fa6e176..627b181 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts
@@ -30,3 +30,5 @@ maven("https://dl.bintray.com/kotlin/kotlin-dev") } } + +include(":integration-tests:cli")