Skip to content
Open
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
146 changes: 99 additions & 47 deletions .claude/skills/add-remote-endpoint/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
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.
description: Use when adding a new remote/network endpoint (a RemoteOperation on OcsKotlinRemoteOperation) to this Nextcloud Android library, or when the user says "add an endpoint", "new remote operation", "implement this API call", "create endpoints for <feature>", or "wire up <OCS/ocs> API". Scopes the calls to the client flow, then scaffolds verb/sub-feature packages, models, operations and integration tests following the teams package conventions and the standards in AGENTS.md.
---

<!--
Expand All @@ -10,78 +10,130 @@ description: Use when adding a new remote/network endpoint (a RemoteOperation /

# Add a remote endpoint

Implement a new `RemoteOperation` for this library the way the existing ones are written. The full,
Implement new `RemoteOperation`s 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:
Read 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`
- `library/src/main/java/com/nextcloud/operations/OcsKotlinRemoteOperation.kt` — the base class every
OCS operation extends (call, guard, decode, logging, release).
- `library/src/main/java/com/nextcloud/android/lib/resources/teams/` — the canonical feature:
verb/sub-feature packages, model placement, `createMethod` + `parse` operations.
- `library/src/androidTest/java/com/nextcloud/android/lib/resources/teams/` — one `*IT` per operation.

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.
Do **not** use `resources/governance/` as a template; it predates the base class.

## 1. Confirm the contract
## 1. Scope the calls to the flow

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.
Ask for (or extract from the issue) the client flow: the screens and user actions. List each step and
map it to the one server call it needs. Implement only those calls.

## 2. Models — under `<feature>.model`
- Server/web-frontend API files (e.g. a `api.ts`) list far more than a mobile flow needs. Don't port
them wholesale.
- Leave out admin-only endpoints, other apps' APIs and calls the library already has (WebDAV, sharees).
- Name skipped-but-related calls to the human as possible follow-ups instead of implementing them.

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.
If the flow is unclear, ask before coding.

## 3. Constants
## 2. Confirm each contract against the server

- 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.
For every call, read the server controller (and `routes.php` / `#[ApiRoute]`) at the relevant commit:

## 4. Operation
- HTTP verb and full path (`/ocs/v2.php/apps/<app>/…` or core `/ocs/v2.php/<area>/…`)
- request parameters / JSON body field names and types
- the exact `DataResponse` shape: which fields always exist, which can be `null`, whether `data` is an
object, a list, or an object wrapping a list
- non-200 cases the client should know about (e.g. 404 when nothing is linked)

Extend `OCSRemoteOperation<T>` and follow the template in AGENTS.md exactly:
## 3. Plan the packages

- `@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.
```
resources/<feature>/
model/ shared by more than one inner package
<verb>/<subfeature>/ the operation
<verb>/<subfeature>/model/ models only that operation uses
```

`<verb>` is `get` / `post` / `put` / `delete`; `<subfeature>` is a short noun (`team`, `members`,
`folder`). For each model, list which operations use it, then place it in the narrowest package that
covers all of them. Never one big `<feature>/model` package.

## 4. Models

- `@Serializable data class` per wire type, one public type per file; enums with `@SerialName`.
- Required fields for what the server always sends; defaults/nullable only for what it can omit or
null.
- Request bodies: `internal @Serializable` classes, **no default values** (`ocsJson` drops defaults).
- Response wrappers that only exist to match the wire (`{ "resources": [...] }`): `internal`.
- Computed helpers for server semantics the flow needs (`Team.isMember`, `TeamMember.role`).
- Numeric enum wire values: file-level `private const val`s (detekt `MagicNumber`).
- No comments or KDoc.

## 5. Operation

Extend `OcsKotlinRemoteOperation<T>`; implement only `createMethod` and `parse`, plus companion
constants:

```kotlin
class GetTeamFolderRemoteOperation(
private val teamId: String
) : OcsKotlinRemoteOperation<TeamFolder>() {
override fun createMethod(baseUrl: String) =
GetMethod(baseUrl + ENDPOINT + SEPARATOR + teamId + FOLDER + JSON_FORMAT, true)

override fun parse(body: String) = decode(body, TeamFolder.serializer())

## 5. Tests
companion object {
private const val ENDPOINT = "/ocs/v2.php/apps/circles/teams"
private const val FOLDER = "/folder"
}
}
```

- List: `decode(body, ListSerializer(X.serializer()))`.
- Wrapper: `decode(body, XResponse.serializer()).items`.
- Body: `PostMethod(url, true, jsonBody(XRequest(...), XRequest.serializer()))`.
- Nothing to return: `OcsKotlinRemoteOperation<Void>` with `override fun parse(body: String): Void? = null`.
- Reuse `SEPARATOR`; every path segment is a `private const val`; `JSON_FORMAT` is inherited.
- Never override `run()`. If the base class can't express something, extend the base class once.
- No comments or KDoc. License header with the author's own name and e-mail.

## 6. Integration tests

No parsing unit tests. One `*IT` per operation, same package layout under
`library/src/androidTest/java/com/nextcloud/android/lib/resources/<feature>/<verb>/<subfeature>/`:

- `class XRemoteOperationIT : AbstractIT()`, gated with `testOnlyOnServer(NextcloudVersion.nextcloud_XX)`
(existing constant).
- Create the data the test needs with the library's own operations, using unique names.
- Assert `isSuccess`, the important `resultData` fields, and `httpCode` for expected failures.
- `nextcloudClient` is the non-admin `test` user on CI; use `client2` only for admin-only endpoints.

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.
## 7. Stay inside the feature

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).
Don't touch `.github/` workflows, `NextcloudVersion` or other shared files as part of the endpoint
task. If CI lacks an app the tests need, tell the human.

## 6. Verify
## 8. Verify

Verify — `detekt`, `spotlessKotlinCheck`, and `lint` (see `.github/workflows/check.yml`):
While working, compile only:

```bash
./gradlew :library:compileDebugKotlin
./gradlew :library:testDebugUnitTest --tests "com.nextcloud.android.lib.resources.<feature>.*"
./gradlew :library:compileDebugAndroidTestKotlin
```

Present the result for review. After the human agrees, run the CI checks once
(`.github/workflows/check.yml`):

```bash
./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.
All three must pass (spotless runs locally, like in CI). Then review the
diff against the "Definition of done" checklist in AGENTS.md and remove anything unrelated.
Loading
Loading