Skip to content

Commit 2022514

Browse files
📝 Add docstrings to feature/elabftw-extra-fields
Docstrings generation was requested by @Athemis. * #4 (comment) The following files were modified: * `src/logic/eln.rs` * `src/models/extra_fields.rs` * `src/mvu/mod.rs` * `src/ui/components/extra_fields.rs` * `src/ui/mod.rs`
1 parent c539b9f commit 2022514

File tree

5 files changed

+806
-19
lines changed

5 files changed

+806
-19
lines changed

src/logic/eln.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,35 @@ pub fn ensure_extension(mut path: PathBuf, extension: &str) -> PathBuf {
7272
path
7373
}
7474

75-
/// Build a RO-Crate archive ZIP containing the experiment text, metadata, and attachments.
75+
/// Build a RO-Crate ZIP archive containing the experiment text, metadata, and attachments.
7676
///
77-
/// Creates directories inside the archive, copies attachments with sanitized names,
78-
/// emits RO-Crate JSON-LD metadata, and writes the final ZIP to `output`.
79-
/// Parent directories for `output` are created if missing.
77+
/// Creates parent directories for `output` if missing, validates attachment integrity,
78+
/// writes attachments into a sanitized `experiment/` folder inside the archive, and emits
79+
/// `ro-crate-metadata.json` describing the dataset and files.
80+
///
81+
/// Returns `Ok(())` on success or an error with context if writing, hashing, or serialization fails.
82+
///
83+
/// # Examples
84+
///
85+
/// ```rust
86+
/// use std::path::Path;
87+
/// use time::OffsetDateTime;
88+
///
89+
/// // Call with no attachments and default parameters
90+
/// let out = Path::new("example.eln");
91+
/// let _ = crate::logic::eln::build_and_write_archive(
92+
/// out,
93+
/// "Example Title",
94+
/// "Experiment notes",
95+
/// &[], // attachments
96+
/// &[], // extra_fields
97+
/// &[], // extra_groups
98+
/// OffsetDateTime::now_utc(),
99+
/// crate::logic::eln::ArchiveGenre::Experiment,
100+
/// &[],
101+
/// crate::logic::eln::BodyFormat::Markdown,
102+
/// );
103+
/// ```
80104
#[allow(clippy::too_many_arguments)]
81105
pub fn build_and_write_archive(
82106
output: &Path,
@@ -389,4 +413,4 @@ mod tests {
389413
assert_eq!(ArchiveGenre::Resource.as_str(), "resource");
390414
assert_eq!(ArchiveGenre::Experiment.as_str(), "experiment");
391415
}
392-
}
416+
}

src/models/extra_fields.rs

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ pub enum ExtraFieldKind {
3131
}
3232

3333
impl ExtraFieldKind {
34+
/// Maps an eLabFTW field type identifier string to the corresponding `ExtraFieldKind`.
35+
///
36+
/// Unknown identifiers are captured in the `Unknown` variant containing the original string.
37+
///
38+
/// # Examples
39+
///
40+
/// ```
41+
/// let k = ExtraFieldKind::from_str("number");
42+
/// assert!(matches!(k, ExtraFieldKind::Number));
43+
///
44+
/// let u = ExtraFieldKind::from_str("custom-type");
45+
/// match u {
46+
/// ExtraFieldKind::Unknown(s) => assert_eq!(s, "custom-type"),
47+
/// _ => panic!("expected Unknown variant"),
48+
/// }
49+
/// ```
3450
fn from_str(raw: &str) -> Self {
3551
match raw {
3652
"text" => Self::Text,
@@ -71,13 +87,81 @@ pub struct ExtraField {
7187
}
7288

7389
impl ExtraField {
74-
/// Sort helper: position first, then label.
90+
/// Build a sort key that orders by position (missing positions sort last) and then by label.
91+
///
92+
/// The returned tuple is (position_or_max, label).
93+
///
94+
/// # Examples
95+
///
96+
/// ```
97+
/// use crate::models::extra_fields::ExtraField;
98+
///
99+
/// let a = ExtraField {
100+
/// label: "A".into(),
101+
/// kind: crate::models::extra_fields::ExtraFieldKind::Text,
102+
/// value: "".into(),
103+
/// value_multi: vec![],
104+
/// options: vec![],
105+
/// unit: None,
106+
/// units: vec![],
107+
/// position: Some(1),
108+
/// required: false,
109+
/// description: None,
110+
/// allow_multi_values: false,
111+
/// blank_value_on_duplicate: false,
112+
/// group_id: None,
113+
/// readonly: false,
114+
/// };
115+
/// let b = ExtraField { position: None, ..a.clone() };
116+
/// assert_eq!(a.cmp_key(), (1, "A"));
117+
/// assert_eq!(b.cmp_key().0, std::i32::MAX);
118+
/// ```
75119
pub fn cmp_key(&self) -> (i32, &str) {
76120
(self.position.unwrap_or(i32::MAX), &self.label)
77121
}
78122
}
79123

80-
/// Pure validation of a single extra field. Returns `Some(reason_code)` when invalid.
124+
/// Validate a single extra field and return a short reason code when it is invalid.
125+
///
126+
/// This performs minimal, pure validation based on the field's kind and required flag:
127+
/// - If the field is required and empty, returns `Some("required")`.
128+
/// - For `Url`: empty values are allowed; otherwise the value must parse as an `http` or `https` URL with a host, otherwise returns `Some("invalid_url")`.
129+
/// - For `Number`: empty values are allowed; otherwise the value must parse as a floating-point number, otherwise returns `Some("invalid_number")`.
130+
/// - For `Items`, `Experiments`, `Users`: empty values are allowed; otherwise the value must parse as a 64-bit integer, otherwise returns `Some("invalid_integer")`.
131+
/// - For all other kinds, no validation error is produced.
132+
///
133+
/// # Returns
134+
///
135+
/// `Some(reason)` when validation fails, where `reason` is one of:
136+
/// - `"required"`
137+
/// - `"invalid_url"`
138+
/// - `"invalid_number"`
139+
/// - `"invalid_integer"`
140+
///
141+
/// Returns `None` when the field is valid.
142+
///
143+
/// # Examples
144+
///
145+
/// ```
146+
/// # use crate::models::extra_fields::{ExtraField, ExtraFieldKind, validate_field};
147+
/// let f = ExtraField {
148+
/// label: "Website".into(),
149+
/// kind: ExtraFieldKind::Url,
150+
/// value: "https://example.com".into(),
151+
/// value_multi: vec![],
152+
/// options: vec![],
153+
/// unit: None,
154+
/// units: vec![],
155+
/// position: None,
156+
/// required: false,
157+
/// description: None,
158+
/// allow_multi_values: false,
159+
/// blank_value_on_duplicate: false,
160+
/// group_id: None,
161+
/// readonly: false,
162+
/// };
163+
/// assert_eq!(validate_field(&f), None);
164+
/// ```
81165
pub fn validate_field(field: &ExtraField) -> Option<&'static str> {
82166
let value = field.value.trim();
83167

@@ -181,7 +265,21 @@ pub struct ExtraFieldsImport {
181265
pub groups: Vec<ExtraFieldGroup>,
182266
}
183267

184-
/// Parse the `extra_fields` object from an eLabFTW metadata JSON string.
268+
/// Parses eLabFTW metadata JSON and extracts extra field definitions and groups.
269+
///
270+
/// Deserializes the provided JSON string and maps the `extra_fields` object and optional
271+
/// `elabftw.extra_fields_groups` into an `ExtraFieldsImport` containing normalized
272+
/// `ExtraField` entries and `ExtraFieldGroup` metadata. Returns a parsing error if the JSON
273+
/// is invalid or cannot be deserialized into the expected structure.
274+
///
275+
/// # Examples
276+
///
277+
/// ```
278+
/// let json = r#"{ "extra_fields": {} }"#;
279+
/// let import = parse_elabftw_extra_fields(json).unwrap();
280+
/// assert!(import.fields.is_empty());
281+
/// assert!(import.groups.is_empty());
282+
/// ```
185283
pub fn parse_elabftw_extra_fields(json: &str) -> Result<ExtraFieldsImport> {
186284
let env: ExtraFieldsEnvelope =
187285
serde_json::from_str(json).context("Failed to parse eLabFTW metadata JSON")?;
@@ -264,6 +362,23 @@ pub fn parse_elabftw_extra_fields(json: &str) -> Result<ExtraFieldsImport> {
264362
Ok(ExtraFieldsImport { fields, groups })
265363
}
266364

365+
/// Convert a JSON value to a string following the module's serialization rules.
366+
///
367+
/// Maps `Option<&serde_json::Value>` to `Option<String>`:
368+
/// - JSON string -> the inner string
369+
/// - JSON number -> its decimal string representation
370+
/// - JSON boolean -> `"on"` for `true`, `""` for `false`
371+
/// - any other JSON value -> the value's JSON string representation
372+
///
373+
/// # Examples
374+
///
375+
/// ```
376+
/// use serde_json::json;
377+
/// assert_eq!(value_to_string(Some(&json!("text"))), Some("text".to_string()));
378+
/// assert_eq!(value_to_string(Some(&json!(42))), Some("42".to_string()));
379+
/// assert_eq!(value_to_string(Some(&json!(true))), Some("on".to_string()));
380+
/// assert_eq!(value_to_string(None), None);
381+
/// ```
267382
fn value_to_string(val: Option<&Value>) -> Option<String> {
268383
match val? {
269384
Value::String(s) => Some(s.clone()),
@@ -297,4 +412,4 @@ mod tests {
297412
);
298413
assert_eq!(import.fields[1].value, "1.540562");
299414
}
300-
}
415+
}

src/mvu/mod.rs

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,18 @@ pub struct SavePayload {
102102
pub body_format: crate::logic::eln::BodyFormat,
103103
}
104104

105-
/// Update the application model and enqueue commands.
105+
/// Apply a `Msg` to the top-level `AppModel` and append any resulting `Command`s.
106+
///
107+
/// This mutates `model` to reflect the message and pushes zero or more commands onto `cmds` for later execution.
108+
///
109+
/// # Examples
110+
///
111+
/// ```
112+
/// let mut model = AppModel::default();
113+
/// let mut cmds = Vec::new();
114+
/// update(&mut model, Msg::EntryTitleChanged("New title".into()), &mut cmds);
115+
/// assert_eq!(model.entry_title, "New title");
116+
/// ```
106117
pub fn update(model: &mut AppModel, msg: Msg, cmds: &mut Vec<Command>) {
107118
match msg {
108119
Msg::EntryTitleChanged(text) => model.entry_title = text,
@@ -193,7 +204,26 @@ pub fn update(model: &mut AppModel, msg: Msg, cmds: &mut Vec<Command>) {
193204
}
194205
}
195206

196-
/// Execute a command synchronously (single-threaded for now) and return a resulting message.
207+
/// Run a `Command` and produce the corresponding `Msg`.
208+
///
209+
/// This executes the side-effect described by `cmd` (file dialogs, file IO,
210+
/// hashing, thumbnail loading, archive writing, etc.) and returns the message
211+
/// that represents the outcome of that command.
212+
///
213+
/// # Examples
214+
///
215+
/// ```
216+
/// use std::path::PathBuf;
217+
/// // Example: compute hash for a file path (tests should create the file first)
218+
/// let cmd = crate::mvu::Command::HashFile { path: PathBuf::from("example.txt"), _retry: 0 };
219+
/// let msg = crate::mvu::run_command(cmd);
220+
/// match msg {
221+
/// crate::mvu::Msg::Attachments(crate::attachments::AttachmentsMsg::HashComputed { path, .. }) => {
222+
/// assert_eq!(path, PathBuf::from("example.txt"));
223+
/// }
224+
/// _ => panic!("unexpected message"),
225+
/// }
226+
/// ```
197227
pub fn run_command(cmd: Command) -> Msg {
198228
match cmd {
199229
Command::PickFiles => {
@@ -274,7 +304,29 @@ fn surface_event(model: &mut AppModel, message: String, is_error: bool) {
274304
model.status = Some(message);
275305
}
276306

277-
/// Validate model state and build the payload required to save an archive.
307+
/// Validate the current application model and construct a `SavePayload` for writing an archive.
308+
///
309+
/// Performs these checks and conversions:
310+
/// - Ensures the entry title is non-empty.
311+
/// - Trims body text and collects normalized keywords.
312+
/// - Converts the datetime picker value to an `OffsetDateTime`.
313+
/// - Converts attachments to domain metadata and enforces unique sanitized filenames.
314+
/// - Validates each extra field and returns a field-specific user-facing error message on failure.
315+
///
316+
/// # Returns
317+
///
318+
/// `Ok(SavePayload)` containing the assembled data required to save an archive on success, `Err(String)` with a user-facing error message describing the first validation failure otherwise.
319+
///
320+
/// # Examples
321+
///
322+
/// ```rust,no_run
323+
/// // Given a populated `model: AppModel` and an output path:
324+
/// let payload = validate_for_save(&model, std::path::PathBuf::from("entry.eln"));
325+
/// match payload {
326+
/// Ok(p) => println!("Ready to save to {:?}", p.output),
327+
/// Err(msg) => eprintln!("Validation failed: {}", msg),
328+
/// }
329+
/// ```
278330
fn validate_for_save(model: &AppModel, output_path: PathBuf) -> Result<SavePayload, String> {
279331
let title = model.entry_title.trim().to_string();
280332
if title.is_empty() {
@@ -510,6 +562,21 @@ mod tests {
510562
}
511563
}
512564

565+
/// Ensures that a `Users` extra field containing a valid integer string passes save validation.
566+
///
567+
/// This test sets a title and body, adds a `Users`-typed extra field with the value `"12345"`,
568+
/// and asserts that `validate_for_save` returns `Ok`.
569+
///
570+
/// # Examples
571+
///
572+
/// ```
573+
/// let mut model = AppModel::default();
574+
/// model.entry_title = "Has int".into();
575+
/// model.markdown.text = "Body".into();
576+
/// add_typed_field(&mut model, ExtraFieldKind::Users, "12345");
577+
/// let res = validate_for_save(&model, PathBuf::from("/tmp/out.eln"));
578+
/// assert!(res.is_ok());
579+
/// ```
513580
#[test]
514581
fn validate_accepts_valid_integer_field() {
515582
let mut model = AppModel::default();
@@ -523,6 +590,24 @@ mod tests {
523590
assert!(res.is_ok());
524591
}
525592

593+
/// Adds a new extra field of kind `Url` via the extra-fields editor flow and sets its value.
594+
///
595+
/// The function simulates the editor interactions required to create a URL-typed extra field:
596+
/// it starts an add-field modal, sets the label to "URL", switches the kind to URL, commits the
597+
/// field, and then writes `value` into the newly created field.
598+
///
599+
/// # Parameters
600+
///
601+
/// - `model`: mutable reference to the top-level `AppModel` used to drive the MVU update flow.
602+
/// - `value`: the string value to store in the newly created URL field.
603+
///
604+
/// # Examples
605+
///
606+
/// ```rust,ignore
607+
/// let mut model = AppModel::default_for_tests();
608+
/// add_url_field(&mut model, "https://example.com");
609+
/// // model.extra_fields now contains a URL field with the provided value
610+
/// ```
526611
fn add_url_field(model: &mut AppModel, value: &str) {
527612
let mut cmds = Vec::new();
528613

@@ -598,4 +683,4 @@ mod tests {
598683
"typed field setup should not enqueue commands"
599684
);
600685
}
601-
}
686+
}

0 commit comments

Comments
 (0)