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 tracing::debug!(path = %path.as_str(), "Executing OCS GET request");
69 let request = build_request(HttpMethod::Get, path.clone(), None::<&()>, &self.auth)?;
70 let response = self.transport.execute(request).await?;
71 decode_response(HttpMethod::Get, path, &response)
72 }
73
74 /// Performs a `POST` against an OCS path with a serialised request body.
75 ///
76 /// The call sets `Accept: application/json`, `Content-Type: application/json`,
77 /// and `OCS-APIRequest: true`, and applies credentials.
78 ///
79 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
80 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
81 ///
82 /// See Nextcloud OCS API documentation:
83 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
84 pub async fn post<Req: Serialize, Res: DeserializeOwned>(
85 &self,
86 path: ApiPath,
87 body: &Req,
88 ) -> Result<Res, Error> {
89 self.post_with_query(path, Query::new(), body).await
90 }
91
92 /// Performs a `POST` against an OCS path with query parameters and a
93 /// serialised request body.
94 ///
95 /// The call sets `Accept: application/json`, `Content-Type: application/json`,
96 /// and `OCS-APIRequest: true`, and applies credentials.
97 ///
98 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
99 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
100 ///
101 /// See Nextcloud OCS API documentation:
102 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
103 pub async fn post_with_query<Req: Serialize, Res: DeserializeOwned>(
104 &self,
105 path: ApiPath,
106 query: Query,
107 body: &Req,
108 ) -> Result<Res, Error> {
109 let path = with_query(&require_ocs(path)?, &query);
110 tracing::debug!(path = %path.as_str(), "Executing OCS POST request");
111 let request = build_request(HttpMethod::Post, path.clone(), Some(body), &self.auth)?;
112 let response = self.transport.execute(request).await?;
113 decode_response(HttpMethod::Post, path, &response)
114 }
115
116 /// Performs a `PUT` against an OCS path with a serialised request body.
117 ///
118 /// The call sets `Accept: application/json`, `Content-Type: application/json`,
119 /// and `OCS-APIRequest: true`, and applies credentials.
120 ///
121 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
122 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
123 ///
124 /// See Nextcloud OCS API documentation:
125 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
126 pub async fn put<Req: Serialize, Res: DeserializeOwned>(
127 &self,
128 path: ApiPath,
129 body: &Req,
130 ) -> Result<Res, Error> {
131 self.put_with_query(path, Query::new(), body).await
132 }
133
134 /// Performs a `PUT` against an OCS path with query parameters and a
135 /// serialised request body.
136 ///
137 /// The call sets `Accept: application/json`, `Content-Type: application/json`,
138 /// and `OCS-APIRequest: true`, and applies credentials.
139 ///
140 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
141 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
142 ///
143 /// See Nextcloud OCS API documentation:
144 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
145 pub async fn put_with_query<Req: Serialize, Res: DeserializeOwned>(
146 &self,
147 path: ApiPath,
148 query: Query,
149 body: &Req,
150 ) -> Result<Res, Error> {
151 let path = with_query(&require_ocs(path)?, &query);
152 tracing::debug!(path = %path.as_str(), "Executing OCS PUT request");
153 let request = build_request(HttpMethod::Put, path.clone(), Some(body), &self.auth)?;
154 let response = self.transport.execute(request).await?;
155 decode_response(HttpMethod::Put, path, &response)
156 }
157
158 /// Performs a `DELETE` against an OCS path.
159 ///
160 /// The call sets `Accept: application/json` and `OCS-APIRequest: true`
161 /// and applies credentials.
162 ///
163 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
164 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
165 ///
166 /// See Nextcloud OCS API documentation:
167 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
168 pub async fn delete<Res: DeserializeOwned>(&self, path: ApiPath) -> Result<Res, Error> {
169 self.delete_with_query(path, Query::new()).await
170 }
171
172 /// Performs a `DELETE` against an OCS path with query parameters.
173 ///
174 /// The call sets `Accept: application/json` and `OCS-APIRequest: true`
175 /// and applies credentials.
176 ///
177 /// When `Res` is `()`, calls succeed if the server returns an OCS envelope
178 /// reporting success (`statuscode` 100 or 200), ignoring empty or present payload data.
179 ///
180 /// See Nextcloud OCS API documentation:
181 /// <https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/>
182 pub async fn delete_with_query<Res: DeserializeOwned>(
183 &self,
184 path: ApiPath,
185 query: Query,
186 ) -> Result<Res, Error> {
187 let path = with_query(&require_ocs(path)?, &query);
188 tracing::debug!(path = %path.as_str(), "Executing OCS DELETE request");
189 let request = build_request(HttpMethod::Delete, path.clone(), None::<&()>, &self.auth)?;
190 let response = self.transport.execute(request).await?;
191 decode_response(HttpMethod::Delete, path, &response)
192 }
193}