fm_script_client/
data_api.rs1use crate::result::ScriptResultDeserialize;
2use crate::{Connection, Error, FileMakerError, ScriptClient};
3use async_trait::async_trait;
4use reqwest::{Client, Response};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9use tokio::sync::Mutex;
10use url::Url;
11
12pub struct ScriptLayoutContext {
24 layout: String,
25 search_field: String,
26 search_value: String,
27}
28
29impl ScriptLayoutContext {
30 pub fn new(layout: &str, search_field: &str, search_value: &str) -> Self {
44 Self {
45 layout: layout.to_string(),
46 search_field: search_field.to_string(),
47 search_value: search_value.to_string(),
48 }
49 }
50}
51
52pub struct DataApiScriptClient {
61 connection: Arc<Connection>,
62 context: Arc<ScriptLayoutContext>,
63 client: Client,
64 token: Mutex<Option<Token>>,
65}
66
67impl DataApiScriptClient {
68 pub fn new(connection: Connection, context: ScriptLayoutContext) -> Self {
82 Self {
83 connection: Arc::new(connection),
84 context: Arc::new(context),
85 client: Client::new(),
86 token: Mutex::new(None),
87 }
88 }
89
90 pub async fn release_token(&self) -> Result<(), Error> {
95 let token = match self.token.lock().await.take() {
96 Some(token) => token,
97 None => return Ok(()),
98 };
99
100 let url = self.create_url(&format!("/sessions/{}", token.token))?;
101 self.client.delete(url).send().await?;
102
103 Ok(())
104 }
105
106 async fn get_token(&self) -> Result<String, Error> {
107 let mut token = self.token.lock().await;
108 let now = Instant::now();
109
110 if let Some(ref mut token) = *token {
111 token.expiry = now + Duration::from_secs(60 * 14);
112
113 if token.expiry < now {
114 return Ok(token.token.clone());
115 }
116 }
117
118 let url = self.create_url("/sessions")?;
119 let response = self
120 .client
121 .post(url)
122 .basic_auth(&self.connection.username, Some(&self.connection.password))
123 .header("Content-Type", "application/json")
124 .body("{}")
125 .send()
126 .await?;
127
128 if response.status().is_success() {
129 let access_token = match response.headers().get("X-FM-Data-Access-Token") {
130 Some(token) => match token.to_str() {
131 Ok(token) => token.to_string(),
132 Err(_) => return Err(Error::MissingAccessToken),
133 },
134 None => return Err(Error::MissingAccessToken),
135 };
136
137 *token = Some(Token {
138 token: access_token.clone(),
139 expiry: now + Duration::from_secs(60 * 14),
140 });
141
142 return Ok(access_token);
143 }
144
145 Err(self.error_from_response(response).await)
146 }
147
148 async fn error_from_response(&self, response: Response) -> Error {
149 let status = response.status();
150
151 match response.json::<ErrorResponseBody>().await {
152 Ok(result) => {
153 if let Some(error) = result.messages.into_iter().next() {
154 Error::FileMaker(error)
155 } else {
156 Error::UnknownResponse(status)
157 }
158 }
159 Err(_) => Error::UnknownResponse(status),
160 }
161 }
162
163 fn create_url(&self, path: &str) -> Result<Url, Error> {
164 let mut url = Url::parse(&format!(
165 "{}://{}/fmi/data/v1/databases/{}{}",
166 if self.connection.disable_tls {
167 "http"
168 } else {
169 "https"
170 },
171 self.connection.hostname,
172 self.connection.database,
173 path
174 ))?;
175
176 if let Some(port) = self.connection.port {
177 let _ = url.set_port(Some(port));
178 }
179
180 Ok(url)
181 }
182}
183
184#[derive(Debug)]
185struct Token {
186 token: String,
187 expiry: Instant,
188}
189
190#[derive(Debug, Serialize)]
191#[serde(rename_all = "camelCase")]
192struct RequestBody<T> {
193 query: Vec<HashMap<String, String>>,
194 limit: u8,
195 script: String,
196 #[serde(skip_serializing_if = "Option::is_none", rename = "script.param")]
197 script_param: Option<T>,
198}
199
200#[derive(Debug, Deserialize)]
201#[serde(rename_all = "camelCase")]
202struct ResponseBody {
203 response: InnerResponse,
204}
205
206#[derive(Debug, Deserialize)]
207#[serde(rename_all = "camelCase")]
208struct InnerResponse {
209 script_result: Option<String>,
210 script_error: String,
211}
212
213#[derive(Debug, Deserialize)]
214struct ErrorResponseBody {
215 messages: Vec<FileMakerError>,
216}
217
218#[async_trait]
219impl ScriptClient for DataApiScriptClient {
220 async fn execute<T: ScriptResultDeserialize, P: Serialize + Send + Sync>(
221 &self,
222 script_name: impl Into<String> + Send,
223 parameter: Option<P>,
224 ) -> Result<T, Error> {
225 let token = self.get_token().await?;
226 let url = self.create_url(&format!("/layouts/{}/_find", self.context.layout))?;
227
228 let mut query = HashMap::new();
229 query.insert(
230 self.context.search_field.clone(),
231 self.context.search_value.clone(),
232 );
233
234 let body = RequestBody {
235 query: vec![query],
236 limit: 1,
237 script: script_name.into(),
238 script_param: Some(serde_json::to_string(¶meter)?),
239 };
240
241 let response = self
242 .client
243 .post(url)
244 .header("Authorization", format!("Bearer {}", &token))
245 .header("Content-Type", "application/json")
246 .header("Accept", "application/json")
247 .json(&body)
248 .send()
249 .await?;
250
251 let status = response.status();
252
253 if status.is_success() {
254 let ResponseBody { response } = response.json().await?;
255
256 if response.script_error != "0" {
257 return Err(Error::ScriptFailure {
258 code: response.script_error.parse().unwrap_or(-1),
259 data: response.script_result,
260 });
261 }
262
263 let result = T::from_string(response.script_result)?;
264 return Ok(result);
265 }
266
267 Err(self.error_from_response(response).await)
268 }
269}