typeform_rs/
lib.rs

1//! This crate wraps Typeform's REST API.
2//! Main entry-point is [`Typeform`] - once build it can be used to receive responses.
3
4#![warn(
5    anonymous_parameters,
6    missing_copy_implementations,
7    missing_debug_implementations,
8    rust_2018_idioms,
9    rustdoc::private_doc_tests,
10    trivial_casts,
11    trivial_numeric_casts,
12    unused,
13    future_incompatible,
14    nonstandard_style,
15    unsafe_code,
16    unused_import_braces,
17    unused_results,
18    variant_size_differences
19)]
20
21use std::io::Read;
22
23use eyre::{Result, WrapErr};
24use isahc::{prelude::*, Request};
25use serde::{Deserialize, Serialize};
26
27const DEFAULT_TYPEFORM_URL: &str = "https://api.typeform.com";
28const GET_FORM_RESPONSES_PATH: &str = "/forms/{form_id}/responses";
29
30/// Main entry point to work with.
31#[derive(Debug)]
32pub struct Typeform {
33    url: String,
34    form_id: String,
35    token: String,
36}
37
38impl Typeform {
39    /// Default [`Typeform`] constructor.
40    pub fn new(form_id: &str, token: &str) -> Typeform {
41        Typeform {
42            url: DEFAULT_TYPEFORM_URL.to_string(),
43            form_id: form_id.to_string(),
44            token: token.to_owned(),
45        }
46    }
47
48    /// Retrieve all [`Responses`].
49    ///
50    /// [API Reference](https://developer.typeform.com/responses/reference/retrieve-responses/).
51    pub fn responses(&self) -> Result<Responses> {
52        Request::get(format!(
53            "{}{}",
54            self.url,
55            GET_FORM_RESPONSES_PATH.replace("{form_id}", &self.form_id),
56        ))
57        .header("Authorization", format!("Bearer {}", &self.token))
58        .body(())
59        .wrap_err("Failed to build a request.")?
60        .send()
61        .wrap_err("Failed to send get request.")?
62        .json()
63        .wrap_err("Failed to deserialize a response.")
64    }
65
66    /// Retrieve all [`Responses`] which goes after response with a [`token`] specified as argument.
67    ///
68    /// [API Reference](https://developer.typeform.com/responses/reference/retrieve-responses/).
69    pub fn responses_after(&self, token: &str) -> Result<Responses> {
70        Request::get(format!(
71            "{}{}?after={}&page_size=1",
72            self.url,
73            GET_FORM_RESPONSES_PATH.replace("{form_id}", &self.form_id),
74            token,
75        ))
76        .header("Authorization", format!("Bearer {}", &self.token))
77        .body(())
78        .wrap_err("Failed to build a request.")?
79        .send()
80        .wrap_err("Failed to send get request.")?
81        .json()
82        .wrap_err("Failed to deserialize a response.")
83    }
84
85    /// Retrieves a file uploaded as an answer for a submission.
86    ///
87    /// [API Reference](https://developer.typeform.com/responses/reference/retrieve-response-file/)
88    pub fn response_file(
89        &self,
90        response_id: String,
91        field_id: String,
92        filename: String,
93    ) -> Result<Vec<u8>> {
94        let mut bytes = Vec::new();
95        let url = format!(
96            "{}{}/{}/fields/{}/files/{}",
97            self.url,
98            GET_FORM_RESPONSES_PATH.replace("{form_id}", &self.form_id),
99            response_id,
100            field_id,
101            urlencoding::encode(&filename)
102        );
103        dbg!(&url);
104        let _size = Request::get(url)
105            .header("Authorization", format!("Bearer {}", &self.token))
106            .body(())
107            .expect("Failed to build a request.")
108            .send()
109            .expect("Failed to send get request.")
110            .body_mut()
111            .read_to_end(&mut bytes)
112            .expect("Failed to deserialize a response.");
113        Ok(bytes)
114    }
115}
116
117/// Paged list of [`Response`]s.
118#[derive(Clone, Debug, Serialize, Deserialize)]
119pub struct Responses {
120    /// Total number of items in the retrieved collection.
121    pub total_items: Option<u16>,
122    /// Number of pages.
123    pub page_count: Option<u8>,
124    /// Array of [Responses](Response).
125    pub items: Vec<Response>,
126}
127
128/// Unique form's response.
129#[derive(Clone, Debug, Serialize, Deserialize)]
130#[serde(rename_all = "snake_case")]
131pub struct Response {
132    /// Each response to your typeform starts with a general data object that includes the token.
133    pub token: String,
134    /// Unique ID for the response. Note that response_id values are unique per form but are not unique globally.
135    pub response_id: Option<String>,
136    /// Time of the form landing. In ISO 8601 format, UTC time, to the second, with T as a delimiter between the date and time.
137    pub landed_at: String,
138    /// Time that the form response was submitted. In ISO 8601 format, UTC time, to the second, with T as a delimiter between the date and time.
139    pub submitted_at: String,
140    /// Metadata about a client's HTTP request.
141    pub metadata: Metadata,
142    /// Subset of a complete form definition to be included with a submission.
143    pub definition: Option<Definition>,
144    pub answers: Option<Answers>,
145    pub calculated: Calculated,
146}
147
148/// Metadata about a client's HTTP request.
149#[derive(Clone, Debug, Serialize, Deserialize)]
150pub struct Metadata {
151    pub user_agent: String,
152    /// Derived from user agent.
153    pub platform: Option<String>,
154    pub referer: String,
155    /// IP of the client.
156    pub network_id: String,
157}
158
159/// Subset of a complete form definition to be included with a submission.
160#[derive(Clone, Debug, Serialize, Deserialize)]
161pub struct Definition {
162    pub fields: Fields,
163}
164
165#[derive(Clone, Debug, Serialize, Deserialize)]
166pub struct Fields(Vec<Field>);
167
168#[derive(Clone, Debug, Serialize, Deserialize)]
169pub struct Field {
170    pub id: String,
171    pub _type: String,
172    pub title: String,
173    pub description: String,
174}
175
176#[derive(Clone, Debug, Serialize, Deserialize)]
177pub struct Answers(Vec<Answer>);
178
179#[derive(Clone, Debug, Serialize, Deserialize)]
180pub struct Answer {
181    pub field: AnswerField,
182    /// The answer-fields's type.
183    #[serde(rename = "type")]
184    pub _type: AnswerType,
185    /// Represents single choice answers for dropdown-like fields.
186    pub choice: Option<Choice>,
187    /// Represents multiple choice answers.
188    pub choices: Option<Choices>,
189    pub date: Option<String>,
190    pub email: Option<String>,
191    pub file_url: Option<String>,
192    pub number: Option<i32>,
193    pub boolean: Option<bool>,
194    pub text: Option<String>,
195    pub url: Option<String>,
196    pub payment: Option<Payment>,
197    pub phone_number: Option<String>,
198}
199
200impl Answer {
201    pub fn as_str(&self) -> Option<&str> {
202        match &self._type {
203            AnswerType::Text => self.text.as_deref(),
204            AnswerType::Url => self.url.as_deref(),
205            AnswerType::FileUrl => self.file_url.as_deref(),
206            AnswerType::Choice => {
207                let choice = self.choice.as_ref()?;
208                match choice {
209                    Choice {
210                        label: Some(label), ..
211                    } => Some(label.as_str()),
212                    Choice {
213                        label: None,
214                        other: Some(other),
215                    } => Some(other.as_str()),
216                    _ => None,
217                }
218            }
219            AnswerType::PhoneNumber => self.phone_number.as_deref(),
220            AnswerType::Email => self.email.as_deref(),
221            _ => None,
222        }
223    }
224}
225
226impl Answers {
227    /// Tries to find an answer based on a [`_ref`] and returns `None` if fails to do it.
228    pub fn find(&self, _ref: &str) -> Option<&Answer> {
229        self.0.iter().find(|answer| answer.field._ref == _ref)
230    }
231}
232
233#[derive(Clone, Debug, Serialize, Deserialize)]
234pub struct AnswerField {
235    /// The unique id of the form field the answer refers to.
236    pub id: String,
237    /// The field's type in the original form.
238    #[serde(rename = "type")]
239    pub _type: String,
240    /// The reference for the question the answer relates to. Use the ref value to match answers with questions. The Responses payload only includes ref for the fields where you specified them when you created the form.
241    #[serde(rename = "ref")]
242    pub _ref: String,
243    /// The form field's title which the answer is related to.
244    pub title: Option<String>,
245}
246
247#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
248#[serde(rename_all = "snake_case")]
249pub enum AnswerType {
250    Choice,
251    Choices,
252    Date,
253    Email,
254    Url,
255    FileUrl,
256    Number,
257    Boolean,
258    Text,
259    Payment,
260    PhoneNumber,
261}
262
263#[derive(Clone, Debug, Serialize, Deserialize)]
264pub struct Choice {
265    pub label: Option<String>,
266    pub other: Option<String>,
267}
268
269#[derive(Clone, Debug, Serialize, Deserialize)]
270pub struct Labels(Vec<String>);
271
272#[derive(Clone, Debug, Serialize, Deserialize)]
273pub struct Choices {
274    pub labels: Labels,
275    pub other: Option<String>,
276}
277
278#[derive(Clone, Debug, Serialize, Deserialize)]
279pub struct Payment {
280    pub amount: String,
281    pub last4: String,
282    pub name: String,
283}
284
285#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
286pub struct Calculated {
287    pub score: i32,
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    use std::fs::File;
295    use std::io::BufReader;
296
297    #[test]
298    fn parse_valid_responses_from_json_should_pass() {
299        let file = File::open("tests/typeform_responses.json").expect("Failed to open a file.");
300        let reader = BufReader::new(file);
301        let _responses: Responses =
302            serde_json::from_reader(reader).expect("Failed to build responses from reader.");
303    }
304
305    #[test]
306    fn parse_valid_responses2_from_json_should_pass() {
307        let file = File::open("tests/typeform_responses2.json").expect("Failed to open a file.");
308        let reader = BufReader::new(file);
309        let _responses: Responses =
310            serde_json::from_reader(reader).expect("Failed to build responses from reader.");
311    }
312
313    #[test]
314    fn parse_valid_responses3_from_json_should_pass() {
315        let file = File::open("tests/typeform_responses3.json").expect("Failed to open a file.");
316        let reader = BufReader::new(file);
317        let _responses: Responses =
318            serde_json::from_reader(reader).expect("Failed to build responses from reader.");
319    }
320
321    #[test]
322    fn parse_valid_responses4_from_json_should_pass() {
323        let file = File::open("tests/typeform_responses4.json").expect("Failed to open a file.");
324        let reader = BufReader::new(file);
325        let _responses: Responses =
326            serde_json::from_reader(reader).expect("Failed to build responses from reader.");
327    }
328
329    #[test]
330    fn build_request() {
331        Request::get(format!("https://api.typeform.com/forms/ED6iRRjj/responses/448xvbqqrfemwrz4cu038448xvqx6d3w/fields/wYuykcQCh8F3/files/{}", urlencoding::encode("860786e2f952-1080х1080.png")))
332            .body(())
333            .expect("Failed to build a request.");
334    }
335}