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
use crate::app::event::Event;
use crate::app::state::{Consumed, SessionHelper, SessionState};
use crate::app::DynError;
use crate::sketchbook::data_structs::SketchData;
use crate::sketchbook::event_utils::{make_reversible, make_state_change};
use crate::sketchbook::{JsonSerde, Sketch};
use std::fs::File;
use std::io::Read;

/** Constants for event path segments for various events. */

// events being delegated to `model` subcomponent
const MODEL_PATH: &str = "model";
// events being delegated to `observations` subcomponent
const OBSERVATIONS_PATH: &str = "observations";
// events being delegated to `properties` subcomponent
const PROPERTIES_PATH: &str = "properties";
// create new sketch and replace the current data
const NEW_SKETCH_PATH: &str = "new_sketch";
// export the current sketch to custom format
const EXPORT_SKETCH_PATH: &str = "export_sketch";
// export the current sketch to extended aeon format
const EXPORT_AEON_PATH: &str = "export_aeon";
// import sketch from custom format and replace the current data
const IMPORT_SKETCH_PATH: &str = "import_sketch";
// import sketch from aeon format and replace the current data
const IMPORT_AEON_PATH: &str = "import_aeon";
// import model from sbml format and replace the current data
const IMPORT_SBML_PATH: &str = "import_sbml";
// check if various components of sketch are consistent together (and report issues)
const CHECK_CONSISTENCY_PATH: &str = "check_consistency";
// assert that various components of sketch are consistent together
const ASSERT_CONSISTENCY_PATH: &str = "assert_consistency";
// set annotation for the sketch
const SET_ANNOTATION_PATH: &str = "set_annotation";
// refresh the whole sketch
const GET_WHOLE_SKETCH_PATH: &str = "get_whole_sketch";

impl SessionHelper for Sketch {}

impl SessionState for Sketch {
    fn perform_event(&mut self, event: &Event, at_path: &[&str]) -> Result<Consumed, DynError> {
        // just distribute the events one layer down, or answer some specific cases
        if let Some(at_path) = Self::starts_with(MODEL_PATH, at_path) {
            self.model.perform_event(event, at_path)
        } else if let Some(at_path) = Self::starts_with(OBSERVATIONS_PATH, at_path) {
            self.observations.perform_event(event, at_path)
        } else if let Some(at_path) = Self::starts_with(PROPERTIES_PATH, at_path) {
            self.properties.perform_event(event, at_path)
        } else if Self::starts_with(NEW_SKETCH_PATH, at_path).is_some() {
            self.set_to_empty();
            let sketch_data = SketchData::new_from_sketch(self);
            let state_change = make_state_change(&["sketch", "set_all"], &sketch_data);
            // this is probably one of the real irreversible changes
            Ok(Consumed::Irreversible {
                state_change,
                reset: true,
            })
        } else if Self::starts_with(EXPORT_SKETCH_PATH, at_path).is_some() {
            let path = Self::clone_payload_str(event, "sketch")?;
            self.export_to_custom_json(&path)?;
            Ok(Consumed::NoChange)
        } else if Self::starts_with(EXPORT_AEON_PATH, at_path).is_some() {
            let path = Self::clone_payload_str(event, "sketch")?;
            self.export_to_aeon(&path)?;
            Ok(Consumed::NoChange)
        } else if Self::starts_with(IMPORT_SKETCH_PATH, at_path).is_some() {
            let file_path = Self::clone_payload_str(event, "sketch")?;
            // read the file contents
            let mut file = File::open(file_path)?;
            let mut contents = String::new();
            file.read_to_string(&mut contents)?;

            // parse the SketchData, modify the sketch
            let sketch_data = SketchData::from_json_str(&contents)?;
            self.modify_from_sketch_data(&sketch_data)?;

            let state_change = make_state_change(&["sketch", "set_all"], &sketch_data);
            // this is probably one of the real irreversible changes
            Ok(Consumed::Irreversible {
                state_change,
                reset: true,
            })
        } else if Self::starts_with(IMPORT_AEON_PATH, at_path).is_some() {
            let file_path = Self::clone_payload_str(event, "sketch")?;
            // read the file contents
            let mut file = File::open(file_path)?;
            let mut contents = String::new();
            file.read_to_string(&mut contents)?;

            // parse the AEON format
            // TODO: aeon format currently does not support template properties and datasets
            let new_sketch = Sketch::from_aeon(&contents)?;
            self.modify_from_sketch(&new_sketch);

            let sketch_data = SketchData::new_from_sketch(self);
            let state_change = make_state_change(&["sketch", "set_all"], &sketch_data);
            // this is probably one of the real irreversible changes
            Ok(Consumed::Irreversible {
                state_change,
                reset: true,
            })
        } else if Self::starts_with(IMPORT_SBML_PATH, at_path).is_some() {
            let file_path = Self::clone_payload_str(event, "sketch")?;
            // read the file contents
            let mut file = File::open(file_path)?;
            let mut contents = String::new();
            file.read_to_string(&mut contents)?;

            // parse the SBML format (only psbn, no additional properties or datasets)
            let new_sketch = Sketch::from_sbml(&contents)?;
            self.modify_from_sketch(&new_sketch);

            let sketch_data = SketchData::new_from_sketch(self);
            let state_change = make_state_change(&["sketch", "set_all"], &sketch_data);
            // this is probably one of the real irreversible changes
            Ok(Consumed::Irreversible {
                state_change,
                reset: true,
            })
        } else if Self::starts_with(CHECK_CONSISTENCY_PATH, at_path).is_some() {
            let (success, message) = self.run_consistency_check();
            let results = if success {
                "No issues with the sketch were discovered!".to_string()
            } else {
                format!("There are issues with the sketch:\n\n{message}")
            };

            let payload = serde_json::to_string(&results).unwrap();
            let state_change = Event::build(&["sketch", "consistency_results"], Some(&payload));
            // irreversible change that should just bypass the stack (not reset it)
            Ok(Consumed::Irreversible {
                state_change,
                reset: false,
            })
        } else if Self::starts_with(SET_ANNOTATION_PATH, at_path).is_some() {
            let new_annotation = Self::clone_payload_str(event, "sketch")?;
            let orig_annotation = self.get_annotation().to_string();
            if new_annotation == orig_annotation {
                return Ok(Consumed::NoChange);
            }

            // set the annotation and prepare state-change + reverse events
            self.set_annotation(&new_annotation);
            let payload = serde_json::to_string(&new_annotation).unwrap();
            let state_change = Event::build(&["sketch", "set_annotation"], Some(&payload));
            let mut reverse_event = event.clone();
            reverse_event.payload = Some(orig_annotation);

            Ok(make_reversible(state_change, event, reverse_event))
        } else if Self::starts_with(ASSERT_CONSISTENCY_PATH, at_path).is_some() {
            // this is a "synthetic" event that either returns an error, or Consumed::NoChange
            self.assert_consistency()?;
            Ok(Consumed::NoChange)
        } else {
            Self::invalid_path_error_generic(at_path)
        }
    }

    fn refresh(&self, full_path: &[String], at_path: &[&str]) -> Result<Event, DynError> {
        // just distribute the events one layer down, or answer some specific cases
        if let Some(at_path) = Self::starts_with(MODEL_PATH, at_path) {
            self.model.refresh(full_path, at_path)
        } else if let Some(at_path) = Self::starts_with(OBSERVATIONS_PATH, at_path) {
            self.observations.refresh(full_path, at_path)
        } else if let Some(at_path) = Self::starts_with(PROPERTIES_PATH, at_path) {
            self.properties.refresh(full_path, at_path)
        } else if Self::starts_with(GET_WHOLE_SKETCH_PATH, at_path).is_some() {
            let sketch_data = SketchData::new_from_sketch(self);
            Ok(Event {
                path: full_path.to_vec(),
                payload: Some(sketch_data.to_json_str()),
            })
        } else {
            Self::invalid_path_error_generic(at_path)
        }
    }
}