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
use super::{Error, QuerySchema, ValidationError, FORM_ENCODED_MIME_TYPE};
use crate::blobs::Blob;
#[cfg(feature = "fp-bindgen")]
use fp_bindgen::prelude::Serializable;
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;

/// A request for a provider to provide auto-suggestions.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, TypedBuilder)]
#[cfg_attr(
    feature = "fp-bindgen",
    derive(Serializable),
    fp(rust_module = "fiberplane_models::providers")
)]
#[non_exhaustive]
pub struct AutoSuggestRequest {
    /// The value of the field being typed by the user, up to the focus offset.
    pub query: String,

    /// The query type of the provider we're requesting suggestions for.
    pub query_type: String,

    /// The field in the query form we're requesting suggestions for.
    pub field: String,

    /// Some other fields of the cell data.
    /// The choice of which other fields are sent in the request is
    /// left to the caller.
    /// The encoding of the other fields is left to the implementation
    /// in Studio, and follows the format of
    /// cells [Query Data](crate::ProviderCell::query_data).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub other_field_data: Option<String>,
}

impl AutoSuggestRequest {
    pub fn parse(query_data: Blob) -> Result<Self, Error> {
        if query_data.mime_type != FORM_ENCODED_MIME_TYPE {
            return Err(Error::UnsupportedRequest);
        }

        let mut query = String::new();
        let mut query_type = String::new();
        let mut field = String::new();
        let mut other_field_data = None;
        for (key, value) in form_urlencoded::parse(&query_data.data) {
            match key.as_ref() {
                "query" => query = value.to_string(),
                "query_type" => query_type = value.to_string(),
                "field" => field = value.to_string(),
                "other_field_data" => other_field_data = Some(value.to_string()),
                _ => {}
            }
        }

        let mut errors = Vec::new();
        if field.is_empty() {
            errors.push(
                ValidationError::builder()
                    .field_name("field".to_owned())
                    .message("Missing field".to_owned())
                    .build(),
            );
        }
        if query_type.is_empty() {
            errors.push(
                ValidationError::builder()
                    .field_name("query_type".to_owned())
                    .message("Missing query_type".to_owned())
                    .build(),
            );
        }

        match errors.is_empty() {
            true => Ok(Self {
                query,
                query_type,
                field,
                other_field_data,
            }),
            false => Err(Error::ValidationError { errors }),
        }
    }

    pub fn schema() -> QuerySchema {
        // This returns an empty schema, because Studio doesn't use a schema
        // for auto-suggest requests anyway.
        QuerySchema::default()
    }
}

/// A suggestion for a provider's auto-suggest functionality.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, TypedBuilder)]
#[cfg_attr(
    feature = "fp-bindgen",
    derive(Serializable),
    fp(rust_module = "fiberplane_models::providers")
)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Suggestion {
    /// The offset to start applying the suggestion,
    ///
    /// All text should be replaced from that offset up to the end of the
    /// query in AutoSuggestRequest.
    ///
    /// When missing, append the suggestion to the cursor
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<u32>,
    pub text: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}