fm_script_client/
odata_api.rs

1use crate::{Connection, Error, FileMakerError, ScriptClient};
2use async_trait::async_trait;
3use reqwest::Client;
4use serde::de::DeserializeOwned;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::sync::Arc;
8use url::Url;
9
10/// OData API script client.
11///
12/// The OData API script client is the currently preferred method to issue script calls against
13/// FileMaker. If you are unable to utilize the OData API, you can fall back to the
14/// [`crate::data_api::DataApiScriptClient`].
15pub struct ODataApiScriptClient {
16    connection: Arc<Connection>,
17    client: Client,
18}
19
20impl ODataApiScriptClient {
21    /// Creates a new OData API script client.
22    ///
23    /// # Examples
24    ///
25    /// ```
26    /// use fm_script_client::Connection;
27    /// use fm_script_client::odata_api::ODataApiScriptClient;
28    ///
29    /// let client = ODataApiScriptClient::new(
30    ///     "https://foo:bar@example.com/example_database".try_into().unwrap(),
31    /// );
32    /// ```
33    pub fn new(connection: Connection) -> Self {
34        Self {
35            connection: Arc::new(connection),
36            client: Client::new(),
37        }
38    }
39}
40
41#[derive(Debug, Serialize)]
42#[serde(rename_all = "camelCase")]
43struct RequestBody<T> {
44    #[serde(skip_serializing_if = "Option::is_none")]
45    script_parameter_value: Option<T>,
46}
47
48#[derive(Debug, Deserialize)]
49#[serde(rename_all = "camelCase")]
50struct ScriptResult {
51    code: i64,
52    result_parameter: Value,
53}
54
55#[derive(Debug, Deserialize)]
56#[serde(rename_all = "camelCase")]
57struct ResponseBody {
58    script_result: ScriptResult,
59}
60
61#[derive(Debug, Deserialize)]
62struct ErrorResponseBody {
63    error: FileMakerError,
64}
65
66#[async_trait]
67impl ScriptClient for ODataApiScriptClient {
68    async fn execute<T: DeserializeOwned, P: Serialize + Send + Sync>(
69        &self,
70        script_name: impl Into<String> + Send,
71        parameter: Option<P>,
72    ) -> Result<T, Error> {
73        let mut url = Url::parse(&format!(
74            "{}://{}/fmi/odata/v4/{}/Script.{}",
75            if self.connection.disable_tls {
76                "http"
77            } else {
78                "https"
79            },
80            self.connection.hostname,
81            self.connection.database,
82            script_name.into(),
83        ))?;
84
85        if let Some(port) = self.connection.port {
86            let _ = url.set_port(Some(port));
87        }
88
89        let body = RequestBody {
90            script_parameter_value: parameter,
91        };
92
93        let response = self
94            .client
95            .post(url)
96            .basic_auth(&self.connection.username, Some(&self.connection.password))
97            .header("Content-Type", "application/json")
98            .header("Accept", "application/json")
99            .json(&body)
100            .send()
101            .await?;
102
103        let status = response.status();
104
105        if status.is_success() {
106            let result: ResponseBody = response.json().await?;
107
108            if result.script_result.code != 0 {
109                return Err(Error::ScriptFailure {
110                    code: result.script_result.code,
111                    data: result.script_result.result_parameter.to_string(),
112                });
113            }
114
115            let result: T = serde_json::from_value(result.script_result.result_parameter)?;
116            return Ok(result);
117        }
118
119        match response.json::<ErrorResponseBody>().await {
120            Ok(result) => Err(Error::FileMaker(result.error)),
121            Err(_) => Err(Error::UnknownResponse(status)),
122        }
123    }
124}