ced/
processor.rs

1/// Processor is a main struct for csv editing.
2///
3/// Generic workflow of ced processor is followed.
4///
5/// - Add a page(csv value) to a processor
6/// - Use processor api with page names
7/// - Discard or save modified data to a file
8///
9/// * Usage
10/// ```rust
11/// use ced::Processor;
12/// let mut processor = Processor::new();
13///
14/// processor.import_from_file("test.csv", true, None, false).unwrap();
15///
16/// // Get current cursor(page_name) for later uses
17/// let page_name = processor.get_cursor().unwrap();
18///
19/// // Processor can hold multiple pages and needs page_name for every operation to work on the
20/// // page
21/// processor.add_row_from_string_array(&page_name, processor.last_row_index(&page_name)?, &["a","b"]).unwrap();
22///
23/// processor.overwrite_to_file(&page_name,true).unwrap();
24/// ```
25use std::fs::File;
26use std::io::Write;
27use std::path::Path;
28
29#[cfg(feature = "cli")]
30use crate::cli::preset::Preset;
31use crate::error::{CedError, CedResult};
32use crate::page::Page;
33use crate::utils;
34use dcsv::Column;
35use dcsv::{Value, ValueLimiter, ValueType};
36use std::collections::HashMap;
37
38/// Csv processor
39///
40/// Processor has multiple pages which can be accessed with page_name. Processor has currently
41/// selected page which name can be accessed with ```get_cursor``` method.
42pub struct Processor {
43    pub(crate) pages: HashMap<String, Page>,
44    pub(crate) cursor: Option<String>,
45    pub(crate) print_logs: bool,
46    #[cfg(feature = "cli")]
47    preset: Preset,
48    #[cfg(feature = "cli")]
49    pub(crate) no_loop: bool,
50}
51
52impl Default for Processor {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl Processor {
59    /// Create empty processor
60    pub fn new() -> Self {
61        Self {
62            pages: HashMap::new(),
63            cursor: None,
64            print_logs: true,
65            #[cfg(feature = "cli")]
66            preset: Preset::empty(),
67            #[cfg(feature = "cli")]
68            no_loop: false,
69        }
70    }
71
72    /// Change current cusor(page)
73    ///
74    /// This doesn't affect page itself but change cursor.
75    /// * This returns boolean value whether change succeeded or not
76    pub fn change_cursor(&mut self, page_name: &str) -> bool {
77        if !self.pages.contains_key(page_name) {
78            false
79        } else {
80            self.cursor = Some(page_name.to_owned());
81            true
82        }
83    }
84
85    /// Get current cursor (page_name)
86    pub fn get_cursor(&self) -> Option<String> {
87        self.cursor.as_ref().map(|s| s.to_string())
88    }
89
90    /// Add a new page
91    ///
92    /// # Args
93    ///
94    /// * page : Page name to create
95    /// * data : Csv data to store inside a page
96    /// * has_header : Whether csv data has header or not.
97    /// * line_ending : Optional line_ending configuration.
98    /// * raw_mode : This decides whether page be data or array
99    pub fn add_page(
100        &mut self,
101        page: &str,
102        data: &str,
103        has_header: bool,
104        line_ending: Option<char>,
105        raw_mode: bool,
106    ) -> CedResult<()> {
107        if self.pages.contains_key(page) {
108            return Err(CedError::InvalidPageOperation(format!(
109                "\"{}\" already exists",
110                page
111            )));
112        } else {
113            let mut ignore_empty_row = true;
114            if let Ok(val) = std::env::var("CED_READ_STRICT") {
115                if val.to_lowercase() == "true" {
116                    ignore_empty_row = false;
117                }
118            }
119            let mut reader = dcsv::Reader::new()
120                .use_line_delimiter(line_ending.unwrap_or('\n'))
121                .has_header(has_header)
122                .ignore_empty_row(ignore_empty_row);
123
124            let page_data = if raw_mode {
125                Page::new_array(reader.array_from_stream(data.as_bytes())?)
126            } else {
127                Page::new_data(reader.data_from_stream(data.as_bytes())?)
128            };
129            self.pages.insert(page.to_owned(), page_data);
130            self.cursor = Some(page.to_owned());
131            Ok(())
132        }
133    }
134
135    /// Remove page with given name
136    ///
137    /// This doesn't panic and silent do nothing if page name is non-existent
138    pub fn remove_page(&mut self, page_name: &str) {
139        self.pages.remove_entry(page_name);
140    }
141
142    /// Check if processor contains a page
143    pub fn contains_page(&self, page: &str) -> bool {
144        self.pages.contains_key(page)
145    }
146
147    /// Try get page data but panic if cursor is empty
148    ///
149    /// # Return
150    ///
151    /// This return data's mutable reference as result
152    pub(crate) fn get_page_data_mut(&mut self, page: &str) -> CedResult<&mut Page> {
153        self.pages.get_mut(page).ok_or_else(|| {
154            CedError::InvalidPageOperation(format!(
155                "Cannot get page from cursor which is \"{:?}\"",
156                self.cursor
157            ))
158        })
159    }
160
161    /// Try get page data but panic if page is non-existent
162    pub(crate) fn get_page_data(&self, page: &str) -> CedResult<&Page> {
163        self.pages.get(page).ok_or_else(|| {
164            CedError::InvalidPageOperation(format!(
165                "Cannot get page from cursor which is \"{:?}\"",
166                self.cursor
167            ))
168        })
169    }
170
171    pub(crate) fn log(&self, log: &str) -> CedResult<()> {
172        if self.print_logs {
173            utils::write_to_stdout(log)?;
174        }
175        Ok(())
176    }
177
178    /// Drop all data from processor
179    pub fn drop_pages(&mut self) -> CedResult<()> {
180        self.pages.clear();
181        self.cursor = None;
182        Ok(())
183    }
184
185    /// Import file content as page
186    ///
187    /// This will drop the page if given page name already exists.
188    ///
189    /// # Args
190    ///
191    /// * path: File path to import from
192    /// * has_header : Whether csv file has header or not
193    /// * line_ending : Optional line_ending of csv
194    /// * raw_mode : Whether imported as data or array
195    pub fn import_from_file(
196        &mut self,
197        path: impl AsRef<Path>,
198        has_header: bool,
199        line_ending: Option<char>,
200        raw_mode: bool,
201    ) -> CedResult<()> {
202        let content = std::fs::read_to_string(&path).map_err(|err| {
203            CedError::io_error(
204                err,
205                &format!("Failed to import file \"{}\"", path.as_ref().display()),
206            )
207        })?;
208        let page_name = &path.as_ref().display().to_string();
209
210        self.add_page(page_name, &content, has_header, line_ending, raw_mode)?;
211
212        // Set source file because it was imported from file
213        self.pages
214            .get_mut(page_name)
215            .unwrap()
216            .set_source_file(path.as_ref().to_owned());
217        Ok(())
218    }
219
220    /// Write all page's content into a file
221    pub fn write_to_file(&self, page: &str, file: impl AsRef<Path>) -> CedResult<()> {
222        let mut file = File::create(file)
223            .map_err(|err| CedError::io_error(err, "Failed to open file for write"))?;
224
225        file.write_all(self.get_page_as_string(page)?.as_bytes())
226            .map_err(|err| CedError::io_error(err, "Failed to write csv content to file"))?;
227        Ok(())
228    }
229
230    /// Overwrite virtual data's content into a imported file
231    ///
232    /// * cache : whether to backup original file's content into temp directory
233    pub fn overwrite_to_file(&self, page: &str, cache: bool) -> CedResult<bool> {
234        let page = self.get_page_data(page)?;
235        let file = page.source_file.as_ref();
236        if file.is_none() {
237            return Ok(false);
238        }
239
240        let file = file.unwrap();
241        let csv = page.to_string();
242        // Cache file into temp directory
243        if cache {
244            std::fs::copy(file, std::env::temp_dir().join("cache.csv"))
245                .map_err(|err| CedError::io_error(err, "Failed to create cache for overwrite"))?;
246        }
247        std::fs::write(file, csv.as_bytes())
248            .map_err(|err| CedError::io_error(err, "Failed to overwrite file with content"))?;
249        Ok(true)
250    }
251
252    /// Edit a cell by given coordinate
253    pub fn edit_cell(&mut self, page: &str, x: usize, y: usize, input: &str) -> CedResult<()> {
254        self.get_page_data_mut(page)?
255            .set_cell_from_string(x, y, input)?;
256        Ok(())
257    }
258
259    /// Edit a column by given coordinate
260    ///
261    /// This overwrite all column values with given input
262    pub fn edit_column(&mut self, page: &str, column: &str, input: &str) -> CedResult<()> {
263        self.get_page_data_mut(page)?
264            .set_column(column, Value::Text(input.to_owned()))?;
265        Ok(())
266    }
267
268    /// Edit a row with values
269    ///
270    /// This assumes given input accords with order of a target record.
271    ///
272    /// # Args
273    ///
274    /// * page : Page name
275    /// * row_index : Target row
276    /// * input : Inputs are array of options. Some will overwrite and none will not.
277    pub fn edit_row(
278        &mut self,
279        page: &str,
280        row_index: usize,
281        input: &[Option<Value>],
282    ) -> CedResult<()> {
283        self.get_page_data_mut(page)?.edit_row(row_index, input)?;
284        Ok(())
285    }
286
287    /// Set a row with given values
288    ///
289    /// This assumes given input accords with order of a target record.
290    /// This method overwrite an entire row with given values.
291    pub fn set_row(&mut self, page: &str, row_index: usize, input: &[Value]) -> CedResult<()> {
292        self.get_page_data_mut(page)?.set_row(row_index, input)?;
293        Ok(())
294    }
295
296    /// Set a row with given string array
297    ///
298    /// This assumes given input accords with order of a target record.
299    /// This method overwrite an entire row with given values.
300    pub fn set_row_from_string_array(
301        &mut self,
302        page: &str,
303        row_index: usize,
304        input: &[impl AsRef<str>],
305    ) -> CedResult<()> {
306        self.get_page_data_mut(page)?.set_row(
307            row_index,
308            &input
309                .iter()
310                .map(|s| Value::Text(s.as_ref().to_owned()))
311                .collect::<Vec<_>>(),
312        )?;
313        Ok(())
314    }
315
316    /// Add a new row
317    ///
318    /// This assumes given input accords with order of a target record.
319    ///
320    /// # Args
321    ///
322    /// * page: Target page
323    /// * row_index : Target row
324    /// * values : Option. "None" will converted as default values.
325    pub fn add_row(
326        &mut self,
327        page: &str,
328        row_index: usize,
329        values: Option<&[Value]>,
330    ) -> CedResult<()> {
331        self.get_page_data_mut(page)?
332            .insert_row(row_index, values)?;
333        Ok(())
334    }
335
336    /// Add a new row but from array of strings
337    ///
338    /// This assumes given input accords with order of a target record.
339    ///
340    /// # Args
341    ///
342    /// * page: Target page
343    /// * row_index : Target row
344    /// * values : Option. "None" will converted as default values.
345    pub fn add_row_from_string_array(
346        &mut self,
347        page: &str,
348        row_index: usize,
349        src: &[impl AsRef<str>],
350    ) -> CedResult<()> {
351        let values = src
352            .iter()
353            .map(|a| Value::Text(a.as_ref().to_string()))
354            .collect::<Vec<Value>>();
355        self.add_row(page, row_index, Some(&values))?;
356        Ok(())
357    }
358
359    /// Add a new column into a page
360    pub fn add_column(
361        &mut self,
362        page: &str,
363        column_index: usize,
364        column_name: &str,
365        column_type: ValueType,
366        limiter: Option<ValueLimiter>,
367        placeholder: Option<Value>,
368    ) -> CedResult<()> {
369        self.get_page_data_mut(page)?.insert_column_with_type(
370            column_index,
371            column_name,
372            column_type,
373            limiter,
374            placeholder,
375        )?;
376        Ok(())
377    }
378
379    /// Remove a row from a page
380    pub fn remove_row(&mut self, page: &str, row_index: usize) -> CedResult<bool> {
381        Ok(self.get_page_data_mut(page)?.delete_row(row_index))
382    }
383
384    /// Remove a column from a page
385    pub fn remove_column(&mut self, page: &str, column_index: usize) -> CedResult<()> {
386        self.get_page_data_mut(page)?.delete_column(column_index)?;
387        Ok(())
388    }
389
390    /// Add columns into a page
391    ///
392    /// This method dosn't require any column configurators
393    pub fn add_column_array(&mut self, page: &str, columns: &[impl AsRef<str>]) -> CedResult<()> {
394        for col in columns {
395            let column_count = self.get_page_data_mut(page)?.get_column_count();
396            self.add_column(
397                page,
398                column_count,
399                col.as_ref(),
400                ValueType::Text,
401                None,
402                None,
403            )?;
404        }
405        Ok(())
406    }
407
408    /// Move a rom from an index to a target index
409    pub fn move_row(&mut self, page: &str, src: usize, target: usize) -> CedResult<()> {
410        self.get_page_data_mut(page)?.move_row(src, target)?;
411        Ok(())
412    }
413
414    /// Move a column from an index to a target index
415    pub fn move_column(&mut self, page: &str, src: usize, target: usize) -> CedResult<()> {
416        self.get_page_data_mut(page)?.move_column(src, target)?;
417        Ok(())
418    }
419
420    /// Rename a column into a new name
421    pub fn rename_column(&mut self, page: &str, column: &str, new_name: &str) -> CedResult<()> {
422        let page = self.get_page_data_mut(page)?;
423        if let Some(column) = page.try_get_column_index(column) {
424            page.rename_column(column, new_name)?;
425        } else {
426            return Err(CedError::OutOfRangeError);
427        }
428        Ok(())
429    }
430
431    /// Export page's schema
432    pub fn export_schema(&self, page: &str) -> CedResult<String> {
433        let page = self.get_page_data(page)?;
434        if page.is_array() {
435            return Err(CedError::InvalidPageOperation(String::from(
436                "Cannot export schmea from virtual array",
437            )));
438        }
439        if !page.is_array() {
440            // Sincie it is not an array, it is ok to unwrap
441            Ok(page.get_data().unwrap().export_schema())
442        } else {
443            Err(CedError::InvalidPageOperation(
444                "Cannot export schmea when csv is imported as array".to_string(),
445            ))
446        }
447    }
448
449    /// Apply schema into a given page
450    ///
451    /// # Args
452    ///
453    /// * page : Page name
454    /// * path : Schema file path
455    /// * panic : Whether to panic if current value fails to qualify schema. If not every
456    /// unqualified values are overwritten to default qualifying values.
457    pub fn set_schema(&mut self, page: &str, path: impl AsRef<Path>, panic: bool) -> CedResult<()> {
458        if self.get_page_data_mut(page)?.is_array() {
459            return Err(CedError::InvalidPageOperation(
460                "Cannot set schema in array mode".to_string(),
461            ));
462        }
463
464        let content = std::fs::read_to_string(&path).map_err(|err| {
465            CedError::io_error(
466                err,
467                &format!("Failed to import file \"{}\"", path.as_ref().display()),
468            )
469        })?;
470        let mut content = content.lines();
471
472        let header = content.next();
473        if header.is_none() {
474            return Err(CedError::InvalidRowData(
475                "Given file does not have a header".to_string(),
476            ));
477        }
478
479        let mut row = content.next();
480        while let Some(row_src) = row {
481            let row_args = dcsv::utils::csv_row_to_vector(row_src, None, false);
482            let limiter = ValueLimiter::from_line(&row_args[1..].to_vec())?;
483            self.set_limiter(page, &row_args[0], &limiter, panic)?;
484            row = content.next();
485        }
486        Ok(())
487    }
488
489    /// Set a limiter to a column
490    ///
491    /// # Args
492    ///
493    /// * page : Page name
494    /// * column : Target column name(index)
495    /// * limiter : A limiter to apply to column
496    /// * panic : Whether to panic if current value fails to qualify liimter. If not, every
497    /// unqualified values are overwritten to default qualifying values.
498    pub fn set_limiter(
499        &mut self,
500        page: &str,
501        column: &str,
502        limiter: &ValueLimiter,
503        panic: bool,
504    ) -> CedResult<()> {
505        if self.get_page_data(page)?.is_array() {
506            return Err(CedError::InvalidPageOperation(String::from(
507                "Cannot set limiter for virtual array",
508            )));
509        }
510        let column = self
511            .get_page_data_mut(page)?
512            .try_get_column_index(column)
513            .ok_or_else(|| CedError::InvalidColumn(format!("{} is not a valid column", column)))?;
514        self.get_page_data_mut(page)?
515            .set_limiter(column, limiter, panic)?;
516        Ok(())
517    }
518
519    // <PRESETS>
520    //
521    #[cfg(feature = "cli")]
522    pub(crate) fn configure_preset(&mut self, use_defualt: bool) -> CedResult<()> {
523        self.preset = Preset::new(use_defualt)?;
524        Ok(())
525    }
526
527    #[cfg(feature = "cli")]
528    pub(crate) fn set_limiter_from_preset(
529        &mut self,
530        page: &str,
531        column: &str,
532        preset_name: &str,
533        panic: bool,
534    ) -> CedResult<()> {
535        let preset = self.preset.get(preset_name).cloned();
536        if let Some(limiter) = preset {
537            self.set_limiter(page, column, &limiter, panic)?;
538        }
539        Ok(())
540    }
541
542    // <MISC>
543    pub fn get_row_count(&self, page: &str) -> CedResult<usize> {
544        Ok(self.get_page_data(page)?.get_row_count())
545    }
546
547    pub fn get_column_count(&self, page: &str) -> CedResult<usize> {
548        Ok(self.get_page_data(page)?.get_column_count())
549    }
550
551    /// Get last row index
552    pub fn last_row_index(&self, page: &str) -> CedResult<usize> {
553        Ok(self.get_page_data(page)?.get_row_count().max(1) - 1)
554    }
555
556    /// Get last column index
557    pub fn last_column_index(&self, page: &str) -> CedResult<usize> {
558        Ok(self.get_page_data(page)?.get_column_count().max(1) - 1)
559    }
560
561    /// Get virtual data as string form
562    pub fn get_page_as_string(&self, page: &str) -> CedResult<String> {
563        Ok(self.get_page_data(page)?.to_string())
564    }
565
566    /// Get cell from page
567    ///
568    /// This fails when page or coordinate doesn't exist
569    pub fn get_cell(
570        &self,
571        page: &str,
572        row_index: usize,
573        column_index: usize,
574    ) -> CedResult<Option<&Value>> {
575        Ok(self.get_page_data(page)?.get_cell(row_index, column_index))
576    }
577
578    /// Get column from page
579    ///
580    /// This fails when either page or column doesn't exist
581    pub fn get_column(&self, page: &str, column_index: usize) -> CedResult<Option<&Column>> {
582        let page = self.get_page_data(page)?;
583        Ok(page.get_columns().get(column_index))
584    }
585
586    /// Get column from page by name
587    ///
588    /// This fails when either page or column doesn't exist
589    pub fn get_column_by_name(&self, page: &str, column_name: &str) -> CedResult<Option<&Column>> {
590        let page = self.get_page_data(page)?;
591        Ok(match page.try_get_column_index(column_name) {
592            Some(index) => page.get_columns().get(index),
593            None => None,
594        })
595    }
596}