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),
}
