ankiconnect_rs/models/
note.rs

1//! Note model definitions
2
3use crate::error::NoteError;
4use crate::models::{FieldMedia, Model};
5use crate::Media;
6use std::collections::{HashMap, HashSet};
7
8/// Unique identifier for an Anki note
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct NoteId(pub u64);
11
12impl NoteId {
13    /// Gets the raw ID value
14    pub fn value(&self) -> u64 {
15        self.0
16    }
17}
18
19/// Represents a note in Anki
20#[derive(Debug, Clone)]
21pub struct Note {
22    id: Option<NoteId>, // None if not yet saved to Anki
23    model: Model,
24    field_values: HashMap<String, String>,
25    tags: HashSet<String>,
26    media: Vec<FieldMedia>,
27}
28
29impl Note {
30    /// Creates a new note with validation
31    pub fn new(
32        model: Model,
33        field_values: HashMap<String, String>,
34        tags: HashSet<String>,
35        media: Vec<FieldMedia>,
36    ) -> std::result::Result<Self, NoteError> {
37        // Check that all provided fields actually exist in the model
38        for field_name in field_values.keys() {
39            if model.get_field(field_name).is_none() {
40                return Err(NoteError::UnknownField(field_name.to_string()));
41            }
42        }
43
44        // Validate media is attached to existing fields
45        for field_media in &media {
46            if model.get_field(&field_media.field).is_none() {
47                return Err(NoteError::UnknownField(field_media.field.clone()));
48            }
49        }
50
51        Ok(Self {
52            id: None,
53            model,
54            field_values,
55            tags,
56            media,
57        })
58    }
59
60    /// Creates a note with an existing ID (for notes retrieved from Anki)
61    pub fn with_id(
62        id: NoteId,
63        model: Model,
64        field_values: HashMap<String, String>,
65        tags: HashSet<String>,
66        media: Vec<FieldMedia>,
67    ) -> std::result::Result<Self, NoteError> {
68        let mut note = Self::new(model, field_values, tags, media)?;
69        note.id = Some(id);
70        Ok(note)
71    }
72
73    /// Gets the ID of this note, if it has one
74    pub fn id(&self) -> Option<NoteId> {
75        self.id
76    }
77
78    /// Gets the model (note type) of this note
79    pub fn model(&self) -> &Model {
80        &self.model
81    }
82
83    /// Gets the field values of this note
84    pub fn field_values(&self) -> &HashMap<String, String> {
85        &self.field_values
86    }
87
88    /// Gets a specific field value
89    pub fn field_value(&self, field_name: &str) -> Option<&String> {
90        self.field_values.get(field_name)
91    }
92
93    /// Gets all tags on this note
94    pub fn tags(&self) -> &HashSet<String> {
95        &self.tags
96    }
97
98    /// Gets media attached to this note
99    pub fn media(&self) -> &[FieldMedia] {
100        &self.media
101    }
102
103    /// Returns true if this note has the given tag
104    pub fn has_tag(&self, tag: &str) -> bool {
105        self.tags.contains(tag)
106    }
107
108    /// Updates a field value
109    pub fn update_field(
110        &mut self,
111        field_name: &str,
112        value: String,
113    ) -> std::result::Result<(), NoteError> {
114        if self.model.get_field(field_name).is_none() {
115            return Err(NoteError::UnknownField(field_name.to_string()));
116        }
117        self.field_values.insert(field_name.to_string(), value);
118        Ok(())
119    }
120
121    /// Adds a tag
122    pub fn add_tag(&mut self, tag: String) {
123        self.tags.insert(tag);
124    }
125
126    /// Removes a tag
127    pub fn remove_tag(&mut self, tag: &str) -> bool {
128        self.tags.remove(tag)
129    }
130
131    /// Adds media to a field
132    pub fn add_media(
133        &mut self,
134        field_name: &str,
135        media: Media,
136    ) -> std::result::Result<(), NoteError> {
137        if self.model.get_field(field_name).is_none() {
138            return Err(NoteError::UnknownField(field_name.to_string()));
139        }
140
141        self.media.push(FieldMedia {
142            media,
143            field: field_name.to_string(),
144        });
145
146        Ok(())
147    }
148
149    /// Gets the value for the front (question) field
150    pub fn front_value(&self) -> Option<&String> {
151        self.model
152            .front_field()
153            .and_then(|field| self.field_values.get(field.name()))
154    }
155
156    /// Gets the value for the back (answer) field
157    pub fn back_value(&self) -> Option<&String> {
158        self.model
159            .back_field()
160            .and_then(|field| self.field_values.get(field.name()))
161    }
162}