Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.0
hooks:
- id: gitleaks
verbose: true
args: ['-c', 'config/gitleaks/.gitleaks.toml']
- repo: local
hooks:
- id: detekt
name: Detekt
entry: bash ./gradlew detektFull -Pandroidx.baselineprofile.skipgeneration
language: system
pass_filenames: false
verbose: true
66 changes: 66 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import org.jetbrains.gradle.ext.settings
import org.jetbrains.gradle.ext.taskTriggers

plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
Expand All @@ -10,6 +13,7 @@ plugins {
alias(libs.plugins.compose.stability.analyzer) apply false
alias(libs.plugins.detekt) apply false
alias(libs.plugins.dokka)
alias(libs.plugins.jetbrains.idea.ext)
alias(libs.plugins.kotest.multiplatform) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.multiplatform) apply false
Expand All @@ -30,4 +34,66 @@ tasks.register("detektFull") {
task.name == "detektMainAndroid" || task.name == "detektIosMainSourceSet"
}
})
}

subprojects {
pluginManager.withPlugin("org.jetbrains.dokka") {
the<org.jetbrains.dokka.gradle.DokkaExtension>().apply {
dokkaSourceSets.configureEach {
enableJdkDocumentationLink.set(false)
}
}
}
}

tasks.register("preCommitCheck") {
group = "verification"
description = "Installs pre-commit and ensures hooks are up to date."

val rootDirectory = rootDir

doLast {
val isWindows = System.getProperty("os.name").lowercase().contains("win")

fun run(cmd: List<String>): Int = ProcessBuilder(cmd)
.directory(rootDirectory)
.inheritIO()
.start()
.waitFor()

println("Installing pre-commit package")
if (isWindows) {
run(listOf("cmd", "/c", "python -m pip install pre-commit"))
} else {
run(listOf("/opt/homebrew/bin/brew", "install", "pre-commit"))
}

println("Autoupdate pre-commit config")
val autoupdateExit = if (isWindows) {
run(listOf("cmd", "/c", "pre-commit autoupdate"))
} else {
run(listOf("/opt/homebrew/bin/pre-commit", "autoupdate"))
}

println("Installing pre-commit hooks")
val installExit = if (isWindows) {
run(listOf("cmd", "/c", "pre-commit install"))
} else {
run(listOf("/opt/homebrew/bin/pre-commit", "install"))
}

if (autoupdateExit != 0 || installExit != 0) {
throw GradleException(
"\"pre-commit\" is not installed or not in PATH. Please install it before syncing."
)
}
}
}

idea.project.settings {
taskTriggers {
if (System.getenv("CI") == null) {
afterSync(tasks.getByName("preCommitCheck"))
}
}
}
1 change: 1 addition & 0 deletions config/gitleaks/.gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
title = "DevView Gitleaks configuration"
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@ import com.worldline.devview.featureflip.preview.FeaturePreviewParameterProvider
* ```
*
* @param feature The feature to display.
* @param totalFeatures Total number of features in the list (used for shape calculations).
* @param index The zero-based index of this feature in the list.
* @param isLastIndex Whether this is the last item in the list.
* @param onStateChange Callback invoked when the feature's state changes via user interaction.
* @param modifier Modifier to apply to the card container.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ public data class EndpointOverride(
*
* ## Composite string key
* Use [compositeKey] to obtain the `"{groupId}-{environmentId}-{endpointId}"` string
* required by DataStore and [com.worldline.devview.networkmock.model.NetworkMockState]:
* required by DataStore and [com.worldline.devview.networkmock.core.model.NetworkMockState]:
* ```kotlin
* val key = EndpointKey("my-backend", "staging", "getUser").compositeKey
* // "my-backend-staging-getUser"
Expand All @@ -360,9 +360,9 @@ public data class EndpointOverride(
* @property endpointId The [EndpointConfig.id] (e.g. `"getUser"`)
* @see MockMatch
* @see EndpointDescriptor
* @see com.worldline.devview.networkmock.model.NetworkMockState
* @see com.worldline.devview.networkmock.repository.MockStateRepository
* @see com.worldline.devview.networkmock.repository.MockConfigRepository
* @see com.worldline.devview.networkmock.core.model.NetworkMockState
* @see com.worldline.devview.networkmock.core.repository.MockStateRepository
* @see com.worldline.devview.networkmock.core.repository.MockConfigRepository
*/
@Immutable
@Serializable
Expand All @@ -374,7 +374,7 @@ public data class EndpointKey(
/**
* The canonical `"{groupId}-{environmentId}-{endpointId}"` string used as a
* DataStore preference key suffix and as the key in
* [com.worldline.devview.networkmock.model.NetworkMockState.endpointStates].
* [com.worldline.devview.networkmock.core.model.NetworkMockState.endpointStates].
*/
public val compositeKey: String get() = "$groupId-$environmentId-$endpointId"

Expand Down Expand Up @@ -418,7 +418,7 @@ public data class EndpointKey(
* @see EnvironmentConfig
* @see EndpointConfig
* @see EndpointKey
* @see com.worldline.devview.networkmock.repository.MockConfigRepository.findMatchingMock
* @see com.worldline.devview.networkmock.core.repository.MockConfigRepository.findMatchingMock
*/
@Immutable
public data class MockMatch(val key: EndpointKey, val config: EndpointConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import kotlinx.serialization.json.JsonClassDiscriminator
*
* ## Environment Resolution
* There is no stored "active environment" selection. The environment is derived at runtime
* by matching the incoming request's hostname against the [com.worldline.devview.networkmock.model.EnvironmentConfig.url]
* by matching the incoming request's hostname against the [com.worldline.devview.networkmock.core.model.EnvironmentConfig.url]
* of each environment across all API groups. This allows the app to simultaneously target
* different environments for different API groups without any manual selection.
*
Expand Down Expand Up @@ -85,9 +85,9 @@ public data class NetworkMockState(
* Convenience overload of [getEndpointState] that accepts three separate string
* identifiers instead of an [EndpointKey]. Delegates to the [EndpointKey] overload.
*
* @param groupId The [com.worldline.devview.networkmock.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.model.EndpointConfig] identifier
* @param groupId The [com.worldline.devview.networkmock.core.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.core.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.core.model.EndpointConfig] identifier
* @return The [EndpointMockState] if configured, or `null` if not set
*/
public fun getEndpointState(
Expand Down Expand Up @@ -117,9 +117,9 @@ public data class NetworkMockState(
* Convenience overload of [withEndpointState] that accepts three separate string
* identifiers instead of an [EndpointKey]. Delegates to the [EndpointKey] overload.
*
* @param groupId The [com.worldline.devview.networkmock.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.model.EndpointConfig] identifier
* @param groupId The [com.worldline.devview.networkmock.core.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.core.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.core.model.EndpointConfig] identifier
* @param state The new endpoint state
* @return A new [NetworkMockState] with the updated endpoint state
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,13 @@ public class MockConfigRepository(
* different environments for different API groups without any manual switching.
*
* ## Matching Steps
* 1. Iterate over all [com.worldline.devview.networkmock.model.ApiGroupConfig] entries
* 2. For each group, iterate over its [com.worldline.devview.networkmock.model.EnvironmentConfig] entries
* 3. Extract the hostname from [com.worldline.devview.networkmock.model.EnvironmentConfig.url]
* 1. Iterate over all [com.worldline.devview.networkmock.core.model.ApiGroupConfig] entries
* 2. For each group, iterate over its [com.worldline.devview.networkmock.core.model.EnvironmentConfig] entries
* 3. Extract the hostname from [com.worldline.devview.networkmock.core.model.EnvironmentConfig.url]
* and compare against the request host (case-insensitive)
* 4. On a hostname match, build the effective endpoint list for that group+environment
* via [ApiGroupConfig.effectiveEndpoints] (shared pool + overrides + additions)
* via [ApiGroupConfig.effectiveEndpoints][com.worldline.devview.networkmock.core.model.effectiveEndpoints]
* (shared pool + overrides + additions)
* 5. Match the request path and method against the effective endpoint list
* 6. Return a [MockMatch] carrying [MockMatch.groupId], [MockMatch.environmentId],
* [MockMatch.endpointId], and the resolved [MockMatch.config]
Expand Down Expand Up @@ -387,9 +388,9 @@ public class MockConfigRepository(
* - `getUser-404-detailed.json` → Not found with detailed error body
* - `getUser-500.json` → Server error
*
* @param groupId The [com.worldline.devview.networkmock.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.model.EndpointConfig] identifier
* @param groupId The [com.worldline.devview.networkmock.core.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.core.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.core.model.EndpointConfig] identifier
* @return A deduplicated, status-code-sorted list of discovered [MockResponse] objects
* (may be empty if no files are found in either location)
*/
Expand Down Expand Up @@ -488,9 +489,9 @@ public class MockConfigRepository(
* 1. `files/networkmocks/responses/{groupId}/{environmentId}/{endpointId}/{fileName}` ← tried first
* 2. `files/networkmocks/responses/{groupId}/{endpointId}/{fileName}` ← fallback
*
* @param groupId The [com.worldline.devview.networkmock.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.model.EndpointConfig] identifier
* @param groupId The [com.worldline.devview.networkmock.core.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.core.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.core.model.EndpointConfig] identifier
* @param fileName The response filename (e.g., `"getUser-200.json"`)
* @return A [MockResponse] if the file is found in either location, or `null` on error
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,9 @@ public class MockStateRepository(private val dataStore: DataStore<Preferences>)
* )
* ```
*
* @param groupId The [com.worldline.devview.networkmock.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.model.EndpointConfig] identifier
* @param groupId The [com.worldline.devview.networkmock.core.model.ApiGroupConfig] identifier
* @param environmentId The [com.worldline.devview.networkmock.core.model.EnvironmentConfig] identifier
* @param endpointId The [com.worldline.devview.networkmock.core.model.EndpointConfig] identifier
* @param state The new endpoint mock state
*/
public suspend fun setEndpointMockState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public data class NetworkMockPluginConfig(internal val config: NetworkMockConfig
*
* ## Default Installation
* When `NetworkMock` is registered via `rememberModules`, repositories are resolved
* automatically from [com.worldline.devview.networkmock.NetworkMockInitializer]:
* automatically from [com.worldline.devview.networkmock.core.NetworkMockInitializer]:
* ```kotlin
* val client = HttpClient(OkHttp) {
* install(NetworkMockPlugin)
Expand Down Expand Up @@ -121,8 +121,8 @@ public data class NetworkMockPluginConfig(internal val config: NetworkMockConfig
* without issues. State reads are atomic through DataStore.
*
* @see NetworkMockConfig
* @see com.worldline.devview.networkmock.repository.MockConfigRepository
* @see com.worldline.devview.networkmock.repository.MockStateRepository
* @see com.worldline.devview.networkmock.core.repository.MockConfigRepository
* @see com.worldline.devview.networkmock.core.repository.MockStateRepository
*/
public val NetworkMockPlugin: HttpClientPlugin<NetworkMockConfig, NetworkMockPluginConfig> =
object : HttpClientPlugin<NetworkMockConfig, NetworkMockPluginConfig> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ public sealed interface NetworkMockDestination : NavKey {
* DataStore instance without depending on each other.
*
* [initModule] passes the initialised DataStore explicitly to
* [NetworkMockInitializer.initialize], which constructs [MockStateRepository]
* and [MockConfigRepository] once for the lifetime of the process.
* [NetworkMockInitializer.initialize], which constructs
* [MockStateRepository][com.worldline.devview.networkmock.core.repository.MockStateRepository] and
* [MockConfigRepository][com.worldline.devview.networkmock.core.repository.MockConfigRepository]
* once for the lifetime of the process.
*
* ## Usage
* ```kotlin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import com.worldline.devview.networkmock.preview.PreviewSheetStatePreviewParamet
import kotlinx.coroutines.launch

/**
* A modal bottom sheet that displays a preview of one or two [MockResponse] payloads for a
* A modal bottom sheet that displays a preview of one or two
* [MockResponse][com.worldline.devview.networkmock.core.model.MockResponse] payloads for a
* network mock endpoint.
*
* When [previewSheetState] is [PreviewSheetState.Single], the sheet renders the single response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import kotlinx.collections.immutable.PersistentList
* Driven entirely by [viewModel], which is constructed and provided by
* [NetworkMock.registerContent] inside the `entry<NetworkMockDestination.Endpoint>` lambda
* so that it is scoped to the navigation entry and receives the correct
* [com.worldline.devview.networkmock.model.EndpointKey].
* [com.worldline.devview.networkmock.core.model.EndpointKey].
*
* Renders three possible states from [NetworkMockEndpointViewModel.uiState]:
* - [NetworkMockEndpointUiState.Loading] — shown while mock response files are being discovered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.worldline.devview.networkmock.model
import androidx.compose.runtime.Immutable

/**
* A flattened display entry used by [InlineDiffContent].
* A flattened display entry used by `InlineDiffContent`.
* Long runs of [DiffLine.Unchanged] are collapsed into [DisplayLine.Collapsed].
*/
@Immutable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import kotlinx.collections.immutable.PersistentList
* Each tab in the Network Mock screen represents one [GroupEnvironmentUiModel],
* showing the resolved endpoints for that specific group and environment.
*
* @property groupId The [com.worldline.devview.networkmock.model.ApiGroupConfig] identifier
* @property environmentId The [com.worldline.devview.networkmock.model.EnvironmentConfig] identifier
* @property groupId The [com.worldline.devview.networkmock.core.model.ApiGroupConfig] identifier
* @property environmentId The [com.worldline.devview.networkmock.core.model.EnvironmentConfig] identifier
* @property name Human-readable display name, e.g. `"My Backend — Staging"`
* @property url The base URL for this group in this environment
* @property endpoints The resolved endpoints with their current mock states
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public class NetworkMockViewModel(
* Combined UI state for the Network Mock screen.
*
* Combines [MockConfiguration] (loaded once from `mocks.json`), the live
* [com.worldline.devview.networkmock.model.NetworkMockState] from DataStore, the internal
* [com.worldline.devview.networkmock.core.model.NetworkMockState] from DataStore, the internal
* loading state, and the discovered [EndpointDescriptor] map into a single
* [NetworkMockUiState] emission. Re-emits whenever any of the four sources change.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import androidx.datastore.preferences.core.Preferences
*
* Each module that requires persistent storage should declare one instance of this
* class. The DataStore is initialised lazily on the first call to [init], which is
* triggered automatically by [com.worldline.devview.core.rememberModules] for any
* triggered automatically by `rememberModules` for any
* module implementing [RequiresDataStore]. The `null` guard inside [init] ensures
* the DataStore is created exactly once per process regardless of recompositions.
*
Expand Down Expand Up @@ -43,7 +43,6 @@ import androidx.datastore.preferences.core.Preferences
* ```
*
* @see RequiresDataStore
* @see com.worldline.devview.core.rememberModules
*/
public class DataStoreDelegate {
private var instance: DataStore<Preferences>? = null
Expand All @@ -58,7 +57,7 @@ public class DataStoreDelegate {
* The `null` guard ensures the DataStore is created exactly once — subsequent
* calls on recomposition are no-ops.
*
* This function is called automatically by [com.worldline.devview.core.rememberModules]
* This function is called automatically by `rememberModules`
* for any module implementing [RequiresDataStore]. It should not be called manually.
*
* @param dataStoreName The filename used for the DataStore preferences file.
Expand All @@ -74,7 +73,7 @@ public class DataStoreDelegate {
* Returns the initialised [DataStore] instance.
*
* @throws IllegalStateException if [init] has not been called yet, which
* indicates the module was not registered via [com.worldline.devview.core.rememberModules].
* indicates the module was not registered via `rememberModules`.
*/
public fun get(): DataStore<Preferences> = instance
?: error(
Expand Down
22 changes: 16 additions & 6 deletions docs/contributing/code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,22 @@ Code style guidelines for DevView.
- Use descriptive names and be consistent

## Linting & Static Analysis
- Run ktlint and detekt before submitting changes
```bash
./gradlew ktlintCheck
./gradlew detekt
```
- Fix all reported issues before opening a pull request

Detekt (with ktlint bundled) enforces style automatically. Run the full analysis across all modules:

=== "Windows"

```bat
.\gradlew.bat detektFull
```

=== "macOS / Linux"

```bash
./gradlew detektFull
```

The pre-commit hook runs this automatically on every `git commit`, so issues are caught before they reach CI. See [Development Setup](development.md) for hook installation.

## Documentation
- Use KDoc for all public classes, functions, and properties
Expand Down
Loading