-
Notifications
You must be signed in to change notification settings - Fork 99
make library agent friendly #2085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+436
−0
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <OCS/ocs> API". Scaffolds the models, constants, operation, and tests following the governance package conventions and the standards in AGENTS.md. | ||
| --- | ||
|
|
||
| <!-- | ||
| ~ SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| ~ SPDX-License-Identifier: MIT | ||
| --> | ||
|
|
||
| # 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<T>`, `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 `<feature>.model` | ||
|
|
||
| For each new wire type create a Kotlin `@Serializable data class` (or `enum` with `@SerialName`) in | ||
| `com.nextcloud.android.lib.resources.<feature>.model`. Never put models beside the operation or in an | ||
| unrelated package. Reuse the shared `OcsResponse<T>` 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<T>` 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<OcsResponse<T>>(method.getResponseBodyAsString()).ocs.data`. | ||
| - Success: `RemoteOperationResult<T>(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/<feature>/`, mirroring the governance | ||
| parsing tests — feed a sample OCS JSON string through `ocsJson.decodeFromString<OcsResponse<…>>(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/<feature>/` 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.<feature>.*" | ||
| ./gradlew :library:detekt | ||
|
alperozturk96 marked this conversation as resolved.
|
||
| ./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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,299 @@ | ||
| <!-- | ||
| ~ SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| ~ SPDX-License-Identifier: MIT | ||
| --> | ||
|
|
||
| # 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<T>` 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<T>` / `Ocs<T>` | ||
| envelope, `ocsJson` (the strict shared `Json`), `SEPARATOR` (`"/"`). | ||
| - **Modern package (`nextcloud`):** `library/src/main/java/com/nextcloud/android/lib/` | ||
| - Remote operations: `…/resources/<feature>/` | ||
| - Wire models/enums: `…/resources/<feature>/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<OcsResponse<EntityLabels>>(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.<feature>`. **Models used by remote | ||
| operations go in a `<feature>.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<T>` 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<List<HoldLabelInfo>>() { | ||
| @Suppress("TooGenericExceptionCaught") | ||
| override fun run(client: NextcloudClient): RemoteOperationResult<List<HoldLabelInfo>> { | ||
| 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<OcsResponse<List<HoldLabelInfo>>>( | ||
| getMethod.getResponseBodyAsString() | ||
| ) | ||
| val data = response.ocs.data | ||
| RemoteOperationResult<List<HoldLabelInfo>>(true, getMethod).apply { resultData = data } | ||
| } catch (e: Exception) { | ||
| failure(e) | ||
| } finally { | ||
| getMethod.releaseConnection() | ||
| } | ||
| } | ||
|
|
||
| @Suppress("DEPRECATION") | ||
| private fun failure(e: Exception): RemoteOperationResult<List<HoldLabelInfo>> = | ||
| RemoteOperationResult<List<HoldLabelInfo>>(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/<feature>/`, mirroring the governance | ||
| parsing tests — feed a sample OCS JSON string through `ocsJson.decodeFromString<OcsResponse<…>>(json)` | ||
| and assert the decoded fields. | ||
|
|
||
| ```bash | ||
| ./gradlew :library:testDebugUnitTest --tests "com.nextcloud.android.lib.resources.<feature>.*" | ||
| ``` | ||
|
|
||
| ### 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/<feature>/` 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.<pkg>.*" # 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<T>`. | ||
| - [ ] No new Java classes. | ||
| - [ ] Models/enums under `<feature>.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 `<feature>.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/<feature>/`). | ||
| 6. Add an integration test (`*IT`) under | ||
| `library/src/androidTest/java/com/nextcloud/android/lib/resources/<feature>/`. | ||
| 7. Run the CI checks: compile + tests + `detekt` + `spotlessKotlinCheck` + `lint`. | ||
| 8. Review the diff; remove anything unrelated. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.