nextcloud_ocs/client/ocs_client.rs
1use nextcloud_sdk_core::{ApiPath, AuthProvider, Error, HttpMethod, Transport};
2use serde::Serialize;
3use serde::de::DeserializeOwned;
4
5use super::path::{require_ocs, with_query};
6use super::request::build_request;
7use super::response::decode_response;
8use crate::query::Query;
9
10/// Executes OCS calls and unwraps their envelope.
11///
12/// Generic over the [`Transport`] that carries bytes and the [`AuthProvider`]
13/// that applies credentials, so a client is built from any transport
14/// implementation without virtual dispatch. The transport already knows the
15/// instance URL and turns an [`ApiPath`] into an absolute URL, so the client
16/// holds no base URL of its own.
17///
18/// All OCS calls follow the Nextcloud Open Collaboration Services (OCS)
19/// specification (<https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>):
20/// - Every request sets `OCS-APIRequest: true` and `Accept: application/json`.
21/// - Mutation requests (`POST`, `PUT`) serialize request bodies as JSON and set
22/// `Content-Type: application/json`.
23/// - Responses are wrapped in an [`OcsEnvelope`](crate::OcsEnvelope) carrying
24/// a `meta.statuscode` verdict (`100` or `200` for success; failure codes such
25/// as `101`, `102`, `997`, `998`, or `999` map to [`Error::Status`]).
26/// - Calling endpoints with unit response `()` succeeds on valid envelopes even
27/// when the response payload is empty (e.g. `[]` or `{}`), or when the caller
28/// only cares about verdict success.
29#[derive(Clone, Debug)]
30pub struct OcsClient<T, A> {
31 transport: T,
32 auth: A,
33}
34
35impl<T: Transport, A: AuthProvider> OcsClient<T, A> {
36 /// Pairs a transport with the credentials applied to every call.
37 pub const fn new(transport: T, auth: A) -> Self {
38 Self { transport, auth }
39 }
40
41 /// The transport this client sends through.
42 pub const fn transport(&self) -> &T {
43 &self.transport
44 }
45
46 /// The credential provider applied to every call.
47 pub const fn auth(&self) -> &A {
48 &self.auth
49 }
50
51 /// Performs a `GET` against an OCS path and returns the envelope payload.
52 ///
53 /// The call sets `Accept: application/json` and `OCS-APIRequest: true` and
54 /// applies credentials. Three cases fail: a path outside `/ocs/v2.php`, a
55 /// response status or an OCS verdict the caller cannot accept, and a body
56 /// that is not an envelope of the requested type. A failure keeps the raw
57 /// body, so the OCS code (`997` app not installed, `998` not allowed,
58 /// `999` invalid) and its message stay reachable through [`Error::body`].
59 ///
60 /// See Nextcloud OCS API documentation:
61 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
62 pub async fn get<Res: DeserializeOwned>(
63 &self,
64 path: ApiPath,
65 query: Query,
66 ) -> Result<Res, Error> {
67 let path = with_query(&require_ocs(path)?, &query);
68 let request = build_request(HttpMethod::Get, path.clone(), None::<&()>, &self.auth)?;
69 let response = self.transport.execute(request).await?;
70 decode_response(HttpMethod::Get, path, &response)
71 }
72
73 /// Performs a `POST` against an OCS path with a serialised request body.
74 ///
75 /// The call sets `Accept: application/json`, `Content-Type: application/json`,
76 /// and `OCS-APIRequest: true`, and applies credentials.
77 ///
78 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
79 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
80 ///
81 /// See Nextcloud OCS API documentation:
82 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
83 pub async fn post<Req: Serialize, Res: DeserializeOwned>(
84 &self,
85 path: ApiPath,
86 body: &Req,
87 ) -> Result<Res, Error> {
88 self.post_with_query(path, Query::new(), body).await
89 }
90
91 /// Performs a `POST` against an OCS path with query parameters and a
92 /// serialised request body.
93 ///
94 /// The call sets `Accept: application/json`, `Content-Type: application/json`,
95 /// and `OCS-APIRequest: true`, and applies credentials.
96 ///
97 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
98 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
99 ///
100 /// See Nextcloud OCS API documentation:
101 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
102 pub async fn post_with_query<Req: Serialize, Res: DeserializeOwned>(
103 &self,
104 path: ApiPath,
105 query: Query,
106 body: &Req,
107 ) -> Result<Res, Error> {
108 let path = with_query(&require_ocs(path)?, &query);
109 let request = build_request(HttpMethod::Post, path.clone(), Some(body), &self.auth)?;
110 let response = self.transport.execute(request).await?;
111 decode_response(HttpMethod::Post, path, &response)
112 }
113
114 /// Performs a `PUT` against an OCS path with a serialised request body.
115 ///
116 /// The call sets `Accept: application/json`, `Content-Type: application/json`,
117 /// and `OCS-APIRequest: true`, and applies credentials.
118 ///
119 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
120 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
121 ///
122 /// See Nextcloud OCS API documentation:
123 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
124 pub async fn put<Req: Serialize, Res: DeserializeOwned>(
125 &self,
126 path: ApiPath,
127 body: &Req,
128 ) -> Result<Res, Error> {
129 self.put_with_query(path, Query::new(), body).await
130 }
131
132 /// Performs a `PUT` against an OCS path with query parameters and a
133 /// serialised request body.
134 ///
135 /// The call sets `Accept: application/json`, `Content-Type: application/json`,
136 /// and `OCS-APIRequest: true`, and applies credentials.
137 ///
138 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
139 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
140 ///
141 /// See Nextcloud OCS API documentation:
142 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
143 pub async fn put_with_query<Req: Serialize, Res: DeserializeOwned>(
144 &self,
145 path: ApiPath,
146 query: Query,
147 body: &Req,
148 ) -> Result<Res, Error> {
149 let path = with_query(&require_ocs(path)?, &query);
150 let request = build_request(HttpMethod::Put, path.clone(), Some(body), &self.auth)?;
151 let response = self.transport.execute(request).await?;
152 decode_response(HttpMethod::Put, path, &response)
153 }
154
155 /// Performs a `DELETE` against an OCS path.
156 ///
157 /// The call sets `Accept: application/json` and `OCS-APIRequest: true`
158 /// and applies credentials.
159 ///
160 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
161 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
162 ///
163 /// See Nextcloud OCS API documentation:
164 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
165 pub async fn delete<Res: DeserializeOwned>(&self, path: ApiPath) -> Result<Res, Error> {
166 self.delete_with_query(path, Query::new()).await
167 }
168
169 /// Performs a `DELETE` against an OCS path with query parameters.
170 ///
171 /// The call sets `Accept: application/json` and `OCS-APIRequest: true`
172 /// and applies credentials.
173 ///
174 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
175 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
176 ///
177 /// See Nextcloud OCS API documentation:
178 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
179 pub async fn delete_with_query<Res: DeserializeOwned>(
180 &self,
181 path: ApiPath,
182 query: Query,
183 ) -> Result<Res, Error> {
184 let path = with_query(&require_ocs(path)?, &query);
185 let request = build_request(HttpMethod::Delete, path.clone(), None::<&()>, &self.auth)?;
186 let response = self.transport.execute(request).await?;
187 decode_response(HttpMethod::Delete, path, &response)
188 }
189}