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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
use crate::errors::ParserError;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs::File, io::BufReader};

/// Gets the default error message when the user doesn't fill out a mandatory field.
fn default_input_err_msg() -> String {
    "This field is required, please enter a value.".to_string()
}

/// The possible types of configuration files (this allows main files to be different from internationalization files).
// Note: Markdown is supported in three places: an instructional endpoint, the preamble of a report endpoint, and a text element in a section.
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Config {
    /// A root configuration file that defines languages that have their own configuration files.
    Root {
        /// A map of the languages supported to filenames, a structure that separates each language into a separate Tribble file.
        languages: HashMap<String, String>,
    },
    /// A configuration file for a single language.
    Language {
        /// The error message when a user doesn't fill out a mandatory field. This is allowed to enable i18n at an arbitrary scale. This field does not support Markdown.
        #[serde(default = "default_input_err_msg")]
        input_err_msg: String,
        /// All the workflow in this Tribble instance. Each workflow is a separate contribution experience, and multiple workflows are generally best suited for things like separate products.
        workflows: HashMap<String, Workflow>,
    },
}
impl Config {
    /// Creates a new instance of the raw configuration from a file.
    pub fn new(filename: &str) -> Result<Self, ParserError> {
        // We'll parse it directly from a reader for efficiency
        let file = File::open(filename).map_err(|err| ParserError::FsError {
            filename: filename.to_string(),
            source: err,
        })?;
        let reader = BufReader::new(file);
        let contents: Self =
            serde_yaml::from_reader(reader).map_err(|err| ParserError::ParseRawError {
                filename: filename.to_string(),
                source: err,
            })?;

        Ok(contents)
    }
}

/// The components of a workflow.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct Workflow {
    /// The title of the page dedicated to this workflow (appears in tabs).
    pub title: String,
    /// The sections that the page can make use of.
    pub sections: HashMap<String, Section>,
    /// The section to start on, which must be a valid key in the `sections` map.
    pub index: String,
    /// The endpoints that the user can exit the process from.
    pub endpoints: HashMap<String, Endpoint>,
}
/// A type alias for a section, which is simply an ordered list of elements.
pub type Section = Vec<SectionElem>;
/// The possible parts of a section.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum SectionElem {
    /// Simple text to be displayed to the user. Markdown is supported here, and this will be rendered to HTML to be interpolated into the page.
    Text(String),
    /// A progression option for moving to another section.
    Progression {
        /// The text to display to the user. This does not support Markdown, as it goes inside an HTML `button`.
        text: String,
        /// The name of the section to navigate to. If this is prefixed with `endpoint:`, it will navigate to an endpoint instead of a section.
        link: String,
        /// Any tags that should be accumulated as a result of proceeding through this route.
        tags: Vec<String>,
    },
    /// A form input that the user can fill out. This must have an associated ID, because its value can be referenced later in an endpoint.
    Input(InputSectionElem),
}
/// The properties of an input element. This needs to be passed around, so it's broken out of the `SectionElem` input.
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct InputSectionElem {
    /// The input's ID, which can be used to reference its value later for interpolation in a formatted report.
    pub id: String,
    /// The label for the input. This does not support Markdown.
    pub label: String,
    /// Whether or not the input is optional.
    #[serde(default)]
    pub optional: bool,
    /// The default value for the input. If the input is optional, this will be the value used for interpolation. If the input is not optional, this will be the default,
    /// which means it will be left as this if the user doesn't fill it in. If a value should be provided, you should make it mandatory and set a default, as optional fields should
    /// be assumed to potentially not contain any value (even though they always will if a default value is provided).
    ///
    /// If the input is a `Select`, this must correspond to an entry in `options`.
    pub default: Option<String>,
    /// The actual properties of the input (unique depending on the input's type).
    #[serde(flatten)]
    // The user can just continue to supply these properties without having to put them inside `input`
    pub input: Input,
}
/// The different types of inputs.
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Input {
    /// Simple text.
    Text {
        /// The input's HTML type.
        #[serde(flatten)]
        // The user should be able to specify the properties in the same line as the rest of the input (with a `type` field as well)
        input_type: InputType,
    },
    /// A select element that provides a dropdown for the user to select a single option.
    Select {
        /// The options that the user can select from.
        options: Vec<SelectOption>,
        /// Whether or not the user can select multiple options.
        #[serde(default)]
        can_select_multiple: bool,
    },
}
/// The possible types an input can have.
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
#[serde(rename_all = "kebab-case")]
pub enum InputType {
    /// A boolean input.
    Boolean {
        /// A list of tags that will be accumulated if this boolean is set to `true`.
        tags: Option<Vec<String>>,
    },
    /// A multiline text input.
    Multiline,
    /// A color picker (only in supported browsers).
    Color,
    /// A simple text element (default).
    Text,
    /// A date input.
    Date,
    /// A datetime input, with no time offset (by UTC has been deprecated at the standard-level).
    DatetimeLocal,
    /// An email input.
    Email,
    /// A month input.
    Month,
    /// A numerical input.
    Number {
        /// The smallest number the user can input.
        #[serde(default)]
        min: Option<i32>,
        /// The largest number the user can input.
        #[serde(default)]
        max: Option<i32>,
    },
    /// A password input (characters are obfuscated).
    Password,
    /// A range slider.
    Range {
        /// The minimum value on the slider.
        min: i32,
        /// The maximum value on the slider.
        max: i32,
    },
    /// A telephone number input.
    Tel,
    /// A time picker.
    Time,
    /// A URL input.
    Url,
    /// A week input.
    Week,
}
impl Default for InputType {
    fn default() -> Self {
        Self::Text
    }
}
impl ToString for InputType {
    fn to_string(&self) -> String {
        match self {
            Self::Boolean { .. } => "checkbox".to_string(),
            Self::Multiline => "multiline".to_string(),
            Self::Color => "color".to_string(),
            Self::Text => "text".to_string(),
            Self::Date => "date".to_string(),
            Self::DatetimeLocal => "datetime-local".to_string(),
            Self::Email => "email".to_string(),
            Self::Month => "month".to_string(),
            Self::Number { .. } => "number".to_string(),
            Self::Password => "password".to_string(),
            Self::Range { .. } => "range".to_string(),
            Self::Tel => "tel".to_string(),
            Self::Time => "time".to_string(),
            Self::Url => "url".to_string(),
            Self::Week => "week".to_string(),
        }
    }
}
/// The properties for an option for a select element. The text of this MUST NOT contain commas, otherwise all sorts of runtime errors WILL occur!
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum SelectOption {
    /// A select element that simply has a value.
    Simple(String),
    WithTags {
        /// The displayed text of the option. This does not support Markdown.
        text: String,
        /// A list of tags that should be accumulated if this option is selected. If multiple options can be selected and there are duplications, tags will only be assigned once.
        tags: Vec<String>,
    },
}
/// The possible endpoint types (endpoints are sections that allow the user to exit the contribution process).
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Endpoint {
    /// A report endpoint, which gives the user a formatted report in Markdown to send to the project.
    // TODO Add functionality to actually send the report somewhere
    Report {
        /// The preamble text to display before the actual formatted report. Markdown can be used here.
        preamble: String,
        /// The formatted report. The UI will not allow the user to edit this, but will provide a copy button. Interpolation of form values is allowed here with `${form_id}` syntax. This
        /// should be written in the appropriate templating language for your issue reporting system (e.g. Markdown for GitHub issues), and will be displayed as a raw, pre-formatted string.
        text: String,
        /// The text of a button for sending teh user to wherever they'll report the issue. This does not support Markdown.
        dest_text: String,
        /// A URL to send the user to so that they can report the issue. If the platform supports interpolating text to be sent
        /// into the URL, you can do so by interpolating `%s` into this field.
        dest_url: String,
    },
    /// An instructional endpoint, which tells the user to do something. This supports Markdown.
    Instructional(String),
}