diff --git a/.claude/skills/add-remote-endpoint/SKILL.md b/.claude/skills/add-remote-endpoint/SKILL.md new file mode 100644 index 000000000..5783ded14 --- /dev/null +++ b/.claude/skills/add-remote-endpoint/SKILL.md @@ -0,0 +1,87 @@ +--- +name: add-remote-endpoint +description: Use when adding a new remote/network endpoint (a RemoteOperation / OCSRemoteOperation) to this Nextcloud Android library, or when the user says "add an endpoint", "new remote operation", "implement this API call", or "wire up API". Scaffolds the models, constants, operation, and tests following the governance package conventions and the standards in AGENTS.md. +--- + + + +# Add a remote endpoint + +Implement a new `RemoteOperation` for this library the way the existing ones are written. The full, +authoritative standards live in **[AGENTS.md](../../../AGENTS.md)** — this skill is the step-by-step +procedure. When they disagree, AGENTS.md wins. + +## 0. Ground yourself in the reference + +Read the canonical package before writing anything: + +- `library/src/main/java/com/nextcloud/android/lib/resources/governance/` — operations (GET/POST/DELETE) +- `library/src/main/java/com/nextcloud/android/lib/resources/governance/model/` — `@Serializable` models + enums +- `library/src/main/java/com/owncloud/android/lib/ocs/OcsResponse.kt` — shared `OcsResponse`, `ocsJson`, `SEPARATOR` + +Pick the existing operation closest to the one you're adding (list-returning GET, single-object GET, +POST with body, DELETE) and mirror its structure. + +## 1. Confirm the contract + +Establish, from the user or the API: HTTP method, path (base + segments), path/query params, request +body shape (if any), and the JSON response shape. If any of these is unclear, ask before coding. + +## 2. Models — under `.model` + +For each new wire type create a Kotlin `@Serializable data class` (or `enum` with `@SerialName`) in +`com.nextcloud.android.lib.resources..model`. Never put models beside the operation or in an +unrelated package. Reuse the shared `OcsResponse` envelope — do not model the `ocs`/`data`/`meta` +wrapper yourself. + +## 3. Constants + +- Reuse `com.owncloud.android.lib.ocs.SEPARATOR` for `"/"` — never inline it. +- Put the base path and each path segment in the operation's `companion object` as `private const val` + (`ENDPOINT`, `AVAILABLE`, …). No hardcoded endpoint strings, param keys, or headers. + +## 4. Operation + +Extend `OCSRemoteOperation` and follow the template in AGENTS.md exactly: + +- `@Suppress("TooGenericExceptionCaught")` on `run()`. +- Build the method (`GetMethod`/`PostMethod`/`DeleteMethod`), then `return try { … } catch (e: Exception) { failure(e) } finally { method.releaseConnection() }`. +- Guard clause first: `if (status != HttpStatus.SC_OK) return RemoteOperationResult(false, method)`. + No nested if/else; keep the body linear. +- Decode with `ocsJson.decodeFromString>(method.getResponseBodyAsString()).ocs.data`. +- Success: `RemoteOperationResult(true, method).apply { resultData = data }` (property, not a setter). +- Keep **≤ 2 `return` keywords** total (guard + `return try`). The success and `failure` values are + expressions, not returns. +- Add the `private fun failure(e: Exception)` helper with `@Suppress("DEPRECATION")` (for `logMessage`) + that logs via `Log_OC.e` and returns `RemoteOperationResult(e)`. +- No class-level KDoc that just restates the class name. + +## 5. Tests + +Add unit tests beside the existing ones in +`library/src/test/java/com/nextcloud/android/lib/resources//`, mirroring the governance +parsing tests — feed a sample OCS JSON string through `ocsJson.decodeFromString>(json)` +and assert the decoded fields. + +Add integration test that makes the actual API call and add an instrumented test under +`library/src/androidTest/java/com/nextcloud/android/lib/resources//` that runs the operation +against a live server and asserts the `RemoteOperationResult` (mirror the existing `*IT` tests). + +## 6. Verify + +Verify — `detekt`, `spotlessKotlinCheck`, and `lint` (see `.github/workflows/check.yml`): + +```bash +./gradlew :library:compileDebugKotlin +./gradlew :library:testDebugUnitTest --tests "com.nextcloud.android.lib.resources..*" +./gradlew :library:detekt +./gradlew :library:spotlessKotlinCheck +./gradlew :library:lint +``` + +All must pass. Check lines are ≤ 120 (spotless check can't always be relied on locally — see +AGENTS.md). Then review the diff against the "Definition of done" checklist in AGENTS.md and remove +anything unrelated. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..dc90f0109 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,299 @@ + + +# AGENTS.md + +This file provides guidance to AI agents (Claude, Codex, Gemini, etc.) working with the +`nextcloud-android-library` repository. **This library only does networking** — it has no UI, no screens, and no +persistence layer. + +## Your Role + +You implement and change **remote/network endpoints** — the `RemoteOperation`s that talk to the +Nextcloud/OCS APIs/WebDAV APIs and that the client apps consumes. Write documentation and code that a +less-experienced contributor can follow. Fix bugs and add endpoints without inventing new patterns +where an existing one fits. + +## Project Overview + +`nextcloud-android-library` is the networking/SDK layer for the Nextcloud Android client. It issues +HTTP requests against Nextcloud Server, OCS APIs and WebDAV APIs (de)serializes the responses, and returns typed +`RemoteOperationResult` values. It is a mixed **Java + Kotlin** codebase; **all new code is +Kotlin**. There is no user-facing surface here — no Activities, no Compose, no XML layouts, no +Material Design concerns. Everything is request → response → typed result. + +## Project Structure: AI Agent Handling Guidelines + +- **Legacy package (`owncloud`):** `library/src/main/java/com/owncloud/android/lib/` + - Shared OCS types live here: `com.owncloud.android.lib.ocs` — `OcsResponse` / `Ocs` + envelope, `ocsJson` (the strict shared `Json`), `SEPARATOR` (`"/"`). +- **Modern package (`nextcloud`):** `library/src/main/java/com/nextcloud/android/lib/` + - Remote operations: `…/resources//` + - Wire models/enums: `…/resources//model/` +- **Canonical reference implementation:** + `library/src/main/java/com/nextcloud/android/lib/resources/governance/` +- **Tests:** + - Unit tests: `library/src/test/java/…` + - Instrumented / integration tests (real API calls): `library/src/androidTest/java/…` + +**Before writing anything, read an existing endpoint and copy its shape.** Copy the *architecture*, +not the text. Do not invent new patterns unless a requirement genuinely cannot be met with the +existing one. + +## General Guidance + +- **One public type per source file.** +- **License header** on every new file (this library is MIT): + + ```kotlin + /* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + ``` + +- Keep diffs minimal and focused; do not reformat or refactor unrelated code. + +## Language + +- **Kotlin only** for new code. Do **not** create new Java classes. Existing Java may be edited only + when required for compatibility (the project targets Java 17). +- Prefer `data class`, `val` over `var`, nullable types (`?`), expression bodies, and stdlib utilities. +- Do **not** translate Java idioms into Kotlin (manual getters/setters, needless mutable state). + +## Serialization + +- Use **`kotlinx.serialization`** for all (de)serialization. Do **not** add Gson usage, converters, + or `@SerializedName`. +- The compiler plugin is already applied in `library/build.gradle` + (`org.jetbrains.kotlin.plugin.serialization`). Every model that crosses the wire is `@Serializable`. +- Enum wire values use `@SerialName`. +- Decode with the shared strict parser, never a new `Json { }`: + +```kotlin +import com.owncloud.android.lib.ocs.OcsResponse // { "ocs": { "data": T } } envelope +import com.owncloud.android.lib.ocs.ocsJson // Json { ignoreUnknownKeys = true } + +val response = ocsJson.decodeFromString>(method.getResponseBodyAsString()) +val data = response.ocs.data +``` + +`ocsJson` is strict on purpose (fail fast): unknown keys are ignored, but a missing/mismatched +required field throws instead of yielding a half-populated object. + +## Package structure + +New endpoints live under `com.nextcloud.android.lib.resources.`. **Models used by remote +operations go in a `.model` subpackage** — never beside the operations, never in an unrelated +package. Reusable, cross-endpoint types (the OCS envelope, `ocsJson`, `SEPARATOR`) live in +`com.owncloud.android.lib.ocs`. + +``` +resources/governance/ + ├── GetEntityLabelsRemoteOperation.kt # operations + ├── SetLabelRemoteOperation.kt + ├── … + └── model/ # data classes + enums + ├── EntityLabels.kt + ├── HoldLabelInfo.kt + ├── LabelType.kt + └── … +``` + +Create dedicated `Request` / `Response` / model / enum classes when appropriate. Do not reuse +unrelated models just to avoid a new file, and do not expose internal models as API contracts. + +## Remote operation pattern + +Every operation extends `OCSRemoteOperation` and follows this exact shape (this is +`GetAvailableHoldLabelsRemoteOperation`, the canonical template): + +```kotlin +package com.nextcloud.android.lib.resources.governance + +import com.nextcloud.android.lib.resources.governance.model.HoldLabelInfo +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.ocs.OcsResponse +import com.owncloud.android.lib.ocs.SEPARATOR +import com.owncloud.android.lib.ocs.ocsJson +import com.owncloud.android.lib.resources.OCSRemoteOperation +import org.apache.commons.httpclient.HttpStatus + +class GetAvailableHoldLabelsRemoteOperation( + private val entityType: String, + private val entityId: Long +) : OCSRemoteOperation>() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult> { + val getMethod = + GetMethod( + client.baseUri.toString() + ENDPOINT + entityType + SEPARATOR + entityId + + HOLD_AVAILABLE + JSON_FORMAT, + true + ) + return try { + val status = client.execute(getMethod) + if (status != HttpStatus.SC_OK) { + return RemoteOperationResult(false, getMethod) // guard clause — fail fast + } + val response = ocsJson.decodeFromString>>( + getMethod.getResponseBodyAsString() + ) + val data = response.ocs.data + RemoteOperationResult>(true, getMethod).apply { resultData = data } + } catch (e: Exception) { + failure(e) + } finally { + getMethod.releaseConnection() + } + } + + @Suppress("DEPRECATION") + private fun failure(e: Exception): RemoteOperationResult> = + RemoteOperationResult>(e).also { + Log_OC.e(TAG, "Get available hold labels failed: " + it.logMessage, it.exception) + } + + companion object { + private val TAG = GetAvailableHoldLabelsRemoteOperation::class.java.simpleName + private const val ENDPOINT = "/ocs/v2.php/apps/governance/v1/labels/" + private const val HOLD_AVAILABLE = "/hold/available" + } +} +``` + +Why it is written this way — keep all of it: + +- **Fail fast, read top to bottom.** A guard clause returns early on non-OK status. No nested + `if/else`, no `else` branch. +- **Return count ≤ 2.** The structure has exactly two `return` keywords: the guard `return` and the + outer `return try`. The success value and `failure(e)` are *expressions* of the `try`/`catch`, not + extra returns. Do not add a third `return` (detekt `ReturnCount: max: 2`). +- **One catch.** Catch `Exception` once and delegate to `failure(...)`; do not stack + `catch (IOException)` + `catch (Exception)`. Add `@Suppress("TooGenericExceptionCaught")` on `run()`. + Split into a specific catch only when an exception genuinely needs different handling. +- **`finally { method.releaseConnection() }`** always runs, including on the early `return`. +- **Property access**, not setters: `apply { resultData = data }` — `RemoteOperationResult` exposes + `resultData` as a Kotlin property. Use a `set…()` method only when Java interop / an existing API + contract forces it. +- **`failure(...)` helper** carries `@Suppress("DEPRECATION")` because `RemoteOperationResult.logMessage` + is a deprecated Java accessor. Suppress at the usage only — never globally. + +## Constants — no hardcoded strings + +- Path separators: reuse the shared `com.owncloud.android.lib.ocs.SEPARATOR` (`"/"`). Never inline `"/"`. +- Endpoint base paths and per-endpoint path segments: `private const val` in the operation's + `companion object` (`ENDPOINT`, `HOLD_AVAILABLE`, …). +- The same applies to parameter keys, headers, and repeated log/event strings. + +## Class design + +Class names describe responsibility (`GetAvailableHoldLabelsRemoteOperation`). **Do not add class-level +KDoc that just restates the name.** Comment only non-obvious business logic, deliberate decisions, or +external limitations. + +## Testing + +### Unit tests + +Small, isolated tests with no live server. Add them beside the existing ones in +`library/src/test/java/com/nextcloud/android/lib/resources//`, mirroring the governance +parsing tests — feed a sample OCS JSON string through `ocsJson.decodeFromString>(json)` +and assert the decoded fields. + +```bash +./gradlew :library:testDebugUnitTest --tests "com.nextcloud.android.lib.resources..*" +``` + +### Integration / end-to-end tests + +Also add an integration test that makes the actual API call**. Add an instrumented test under +`library/src/androidTest/java/com/nextcloud/android/lib/resources//` that runs the operation +against a live Nextcloud server and asserts the `RemoteOperationResult` (`isSuccess`, `resultData`, +error codes). Mirror the existing `*IT` tests for server setup and teardown. + +## Build & verify + +CI runs three checks in parallel — **`detekt`, `spotlessKotlinCheck`, and `lint`** (see +`.github/workflows/check.yml`). Run the same ones locally, plus compile and tests, before finishing. +All must pass. + +```bash +./gradlew :library:compileDebugKotlin # compiles +./gradlew :library:testDebugUnitTest --tests "com..*" # unit tests for your package +./gradlew :library:detekt # static analysis (see thresholds below) +./gradlew :library:spotlessKotlinCheck # ktlint formatting +./gradlew :library:lint # Android lint +``` + +- **detekt** config: `library/detekt.yml`. The rules that bite endpoint code: + - `ReturnCount: max: 2` — a `run()` may contain at most **two** `return` keywords. + - `TooGenericExceptionCaught: active` — catching `Exception` requires `@Suppress("TooGenericExceptionCaught")`. + - `ThrowsCount: max: 2`. +- **Line length: 120** (`.editorconfig`). Import ordering is disabled — do not reorder imports to "fix" lint. +- **Formatting** is ktlint via spotless. `:library:spotlessKotlinCheck` may fail at *configuration* + time locally (a spotbugs/AGP incompatibility unrelated to your code); it still runs in CI, so + verify formatting by hand against the 120-char limit and the patterns above when it can't run. + +## Nextcloud Contribution Policy + +Follow the +[Nextcloud AI contribution policy](https://github.com/nextcloud/.github/blob/master/CONTRIBUTING.md). + +**Must do:** + +- Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` git trailer when an agent contributed to a commit. +- Disclose AI use in the PR description. +- Keep pull requests focused and scoped. +- Verify any dependency against its actual registry before adding it. +- Explicitly warn the human when a change would violate policy, and when a PR is getting large. + +**Must never do:** + +- Submit contributions autonomously or open/merge PRs without a human in the loop. +- Add `Signed-off-by` tags on behalf of a human. +- Generate security reports without human verification. +- Write PR descriptions as if you were the contributor. +- Submit unreviewed code. + +## Commit and Pull Request Guidelines + +- Only commit or push **when asked**. If you are on the default branch, create a branch first. +- Sign commits (`git commit -s`) and follow Conventional Commits v1.0.0 (e.g. + `feat(governance): add hold-label endpoint`). +- Reference the relevant issue in the PR body (e.g. "Closes #123"). + +## Definition of done + +- [ ] No Gson introduced; all wire models `@Serializable`, decoded via `ocsJson` + `OcsResponse`. +- [ ] No new Java classes. +- [ ] Models/enums under `.model`; reusable types in `com.owncloud.android.lib.ocs`. +- [ ] `run()` is a guard-clause + single `return try/catch/finally`; ≤ 2 returns; no nested if/else. +- [ ] Exactly one `catch (e: Exception)` → `failure(e)`, with `@Suppress("TooGenericExceptionCaught")`. +- [ ] No hardcoded `"/"` or endpoint strings; constants used. +- [ ] `resultData = data` (property), not a setter, unless interop demands otherwise. +- [ ] No name-restating class comments. +- [ ] Lines ≤ 120. +- [ ] Unit tests **and** an integration test (`*IT`) added. +- [ ] `compileDebugKotlin`, `testDebugUnitTest`, `detekt`, `spotlessKotlinCheck`, and `lint` all pass. +- [ ] Diff is minimal and focused — no drive-by changes. + +## Workflow + +1. Open the closest existing endpoint in `resources/governance/` and mirror it. +2. Add request/response/model/enum classes under `.model`. +3. Add endpoint path constants to the companion object. +4. Implement the operation using the template above. +5. Add unit tests next to the existing governance parsing tests + (`library/src/test/java/com/nextcloud/android/lib/resources//`). +6. Add an integration test (`*IT`) under + `library/src/androidTest/java/com/nextcloud/android/lib/resources//`. +7. Run the CI checks: compile + tests + `detekt` + `spotlessKotlinCheck` + `lint`. +8. Review the diff; remove anything unrelated. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..739c15a07 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,50 @@ + +# CLAUDE.md + +Nextcloud Android **library** — the networking/SDK layer (`RemoteOperation`s against Nextcloud/OCS +APIs) consumed by the Nextcloud Android app. Mixed Java + Kotlin; new code is Kotlin. + +## Read first + +**[AGENTS.md](AGENTS.md) is the source of truth for adding or changing network endpoints** — package +layout, the `RemoteOperation` template, error handling, serialization, constants, and the detekt +thresholds that gate CI. Follow it for any endpoint work. The canonical reference implementation is +`library/src/main/java/com/nextcloud/android/lib/resources/governance/`. + +## Where things live + +- Remote operations: `library/src/main/java/com/nextcloud/android/lib/resources//` +- Wire models/enums: `…/resources//model/` +- Shared OCS types: `com.owncloud.android.lib.ocs` — `OcsResponse` / `Ocs` envelope, + `ocsJson` (strict `Json`), `SEPARATOR` (`"/"`) +- Unit tests: `library/src/test/java/…`  ·  instrumented tests: `library/src/androidTest/java/…` + +## Commands + +```bash +./gradlew :library:compileDebugKotlin +./gradlew :library:testDebugUnitTest --tests "com.nextcloud.android.lib.resources.governance.*" +./gradlew :library:detekt +``` + +## Gotchas + +- **kotlinx.serialization, not Gson.** The compiler plugin is applied in `library/build.gradle`; the + `kotlinx-serialization-json` runtime dependency alone is not enough — both are required. +- **detekt (`library/detekt.yml`) is strict on operations:** `ReturnCount: max 2`, + `TooGenericExceptionCaught` active (needs `@Suppress` on `run()`), `ThrowsCount: max 2`. AGENTS.md + shows the two-return / single-catch pattern that satisfies these. +- **Line length 120** (`.editorconfig`); import ordering is disabled — don't reorder imports. +- **`:library:spotlessKotlinCheck` currently fails at configuration time** (a spotbugs/AGP issue, + not your code). Verify Kotlin formatting by hand against the 120-char limit and the AGENTS.md patterns. +- **`RemoteOperationResult.logMessage`** is a deprecated Java accessor — suppress at the call site + only (`@Suppress("DEPRECATION")` on the `failure(...)` helper), never globally. + +## House rules + +- Kotlin only for new classes; don't add Java classes. Java 17 is available for editing existing Java. +- Keep diffs minimal and focused; don't reformat or refactor unrelated code. +- Only commit or push when asked.