1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use std::{marker::PhantomData, pin::Pin};

use serde::{Deserialize, Serialize};
use std::future::Future;
use wasm_bindgen::{prelude::*, JsCast};

#[derive(Clone, Debug, Serialize, Deserialize, TypedBuilder)]
#[builder(field_defaults(default, setter(strip_option)))]
pub struct HttpsCallableOptions {
    pub timeout: Option<usize>,
}

#[derive(Deserialize)]
struct HttpsCallableResponse<T> {
    data: T,
}

pub struct HttpsCallable<Req, Res>
where
    Req: Serialize,
    Res: for<'de> Deserialize<'de>,
{
    callable: js_sys::Function,
    _request_data: PhantomData<Req>,
    _response_data: PhantomData<Res>,
}

impl<Req, Res> HttpsCallable<Req, Res>
where
    Req: Serialize,
    Res: for<'de> Deserialize<'de>,
{
    #[track_caller]
    fn call_(
        &self,
        data: Req,
    ) -> impl Future<Output = Result<Result<Res, serde_wasm_bindgen::Error>, JsValue>> {
        let data = serde_wasm_bindgen::to_value(&data).expect("data to serialize successfully");

        let res = self.callable.call1(&JsValue::UNDEFINED, &data).unwrap();

        let fut = wasm_bindgen_futures::JsFuture::from(res.unchecked_into::<js_sys::Promise>());

        async move {
            fut.await.map(|res| {
                serde_wasm_bindgen::from_value::<HttpsCallableResponse<Res>>(res)
                    .map(|res| res.data)
            })
        }
    }
}

impl<Req, Res> FnOnce<(Req,)> for HttpsCallable<Req, Res>
where
    Req: Serialize + 'static,
    Res: for<'de> Deserialize<'de> + 'static,
{
    type Output =
        Pin<Box<dyn Future<Output = Result<Result<Res, serde_wasm_bindgen::Error>, JsValue>>>>;

    extern "rust-call" fn call_once(self, args: (Req,)) -> Self::Output {
        Box::pin(self.call_(args.0))
    }
}

impl<Req, Res> FnMut<(Req,)> for HttpsCallable<Req, Res>
where
    Req: Serialize + 'static,
    Res: for<'de> Deserialize<'de> + 'static,
{
    extern "rust-call" fn call_mut(&mut self, args: (Req,)) -> Self::Output {
        Box::pin(self.call_(args.0))
    }
}

impl<Req, Res> Fn<(Req,)> for HttpsCallable<Req, Res>
where
    Req: Serialize + 'static,
    Res: for<'de> Deserialize<'de> + 'static,
{
    extern "rust-call" fn call(&self, args: (Req,)) -> Self::Output {
        Box::pin(self.call_(args.0))
    }
}

#[track_caller]
pub fn https_callable<Req, Res>(
    functions_instance: &Functions,
    name: &str,
    options: Option<HttpsCallableOptions>,
) -> HttpsCallable<Req, Res>
where
    Req: Serialize,
    Res: for<'de> Deserialize<'de>,
{
    let options = serde_wasm_bindgen::to_value(&options).unwrap();

    let callable = https_callable_(functions_instance, name, options);

    HttpsCallable {
        callable,
        _request_data: PhantomData::default(),
        _response_data: PhantomData::default(),
    }
}

#[wasm_bindgen(module = "firebase/functions")]
extern "C" {
    pub type Functions;

    #[wasm_bindgen(js_name = getFunctions)]
    pub fn get_functions() -> Functions;

    #[wasm_bindgen(js_name = httpsCallable)]
    fn https_callable_(
        functions_instance: &Functions,
        name: &str,
        options: JsValue,
    ) -> js_sys::Function;
}