evergreen/
script.rs

1//! Script utilities.
2use crate as eg;
3use eg::common::auth;
4use eg::date;
5use eg::db::DatabaseConnection;
6use eg::init;
7use eg::Editor;
8use eg::EgResult;
9
10const HELP_TEXT: &str = "
11Runner Additions:
12
13    --help
14        Show help text
15
16    --staff-account
17        ID of the account to use where a staff account would typically
18        be used, e.g. setting the last editor on a bib record.
19
20    --staff-workstation
21        Name of the staff login workstation.  See --staff-acount. Optional.
22
23    --announce
24        Log calls to announce() to STDOUT in addition to log::info!().
25
26Database Connector Additions:
27
28    Parameters supported when Runner is started with a database connection.
29
30    --db-host
31    --db-port
32    --db-user
33    --db-name
34";
35
36/// Account ID for updates, e.g. applying an 'editor' value to a bib record update.
37const DEFAULT_STAFF_ACCOUNT: i64 = 1;
38
39pub struct Options {
40    /// Getops Options prepopulated with script-local parameter definitions.
41    pub options: Option<getopts::Options>,
42
43    /// Connect to the Evergreen/OpenSRF message bus and load the
44    /// Evergreen IDL.
45    pub with_evergreen: bool,
46
47    /// Connect to the Evergreen database and append the database
48    /// options to the getops parameters.
49    pub with_database: bool,
50
51    /// Tell the world what your script does and how to use it.
52    pub help_text: Option<String>,
53
54    /// Pass additional command line options to the getopts parser.
55    ///
56    /// These are appended to parameters collected from the command line.
57    pub extra_params: Option<Vec<String>>,
58}
59
60/// Parts of a script runner which are thread-Sendable.
61///
62/// Useful for cloning the sendable parts of a Runner, passing it to
63/// a thread, then reconstituting a Runner on the other side.
64#[derive(Debug, Clone)]
65pub struct RunnerCore {
66    staff_account: i64,
67    staff_workstation: Option<String>,
68    authtoken: Option<String>,
69    params: getopts::Matches,
70    log_prefix: Option<String>,
71    announce: bool,
72}
73
74/// Core runner plus non-sendable components (editor, db).
75pub struct Runner {
76    core: RunnerCore,
77    editor: Option<Editor>,
78    db: Option<DatabaseConnection>,
79}
80
81impl From<RunnerCore> for Runner {
82    fn from(core: RunnerCore) -> Self {
83        Runner {
84            core,
85            editor: None,
86            db: None,
87        }
88    }
89}
90
91impl Runner {
92    /// Parse the command line parameters, connect to Evergreen, and
93    /// optionally create a direct database connection.
94    ///
95    /// Return None if a command line option results in early exit, e.g. --help.
96    ///
97    /// * `options` - Script options.
98    pub fn init(mut options: Options) -> EgResult<Option<Runner>> {
99        let mut ops_binding = None;
100
101        let ops = options.options.as_mut().unwrap_or_else(|| {
102            ops_binding = Some(getopts::Options::new());
103            ops_binding.as_mut().unwrap()
104        });
105
106        ops.optflag("h", "help", "");
107        ops.optflag("", "announce", "");
108        ops.optopt("", "staff-account", "", "");
109        ops.optopt("", "staff-workstation", "", "");
110
111        if options.with_database {
112            // Append the datbase-specifc command line options.
113            DatabaseConnection::append_options(ops);
114        }
115
116        let mut args: Vec<String> = std::env::args().collect();
117
118        if let Some(extras) = options.extra_params.as_mut() {
119            args.append(extras);
120        }
121
122        let params = ops
123            .parse(&args[1..])
124            .map_err(|e| format!("Error parsing options: {e}"))?;
125
126        if params.opt_present("help") {
127            println!(
128                "{}\n{}",
129                options
130                    .help_text
131                    .unwrap_or("No Application Help Text Provided".to_string()),
132                HELP_TEXT
133            );
134            return Ok(None);
135        }
136
137        let sa = DEFAULT_STAFF_ACCOUNT.to_string();
138        let staff_account = params.opt_get_default("staff-account", sa).unwrap();
139        let staff_account = staff_account
140            .parse::<i64>()
141            .map_err(|e| format!("Error parsing staff-account value: {e}"))?;
142
143        let staff_workstation = params.opt_str("staff-workstation").map(|v| v.to_string());
144        let announce = params.opt_present("announce");
145
146        let mut runner = Runner {
147            db: None,
148            editor: None,
149            core: RunnerCore {
150                params,
151                staff_account,
152                staff_workstation,
153                announce,
154                authtoken: None,
155                log_prefix: None,
156            },
157        };
158
159        if options.with_database {
160            runner.connect_db()?;
161        }
162
163        if options.with_evergreen {
164            // Avoid using self.connect_evergreen() here since that
165            // variant simply connects to the bus and does not
166            // initialize the IDL, logging, etc.
167            let client = init::init()?;
168            runner.editor = Some(eg::Editor::new(&client));
169        }
170
171        Ok(Some(runner))
172    }
173
174    /// Connect to the database.
175    pub fn connect_db(&mut self) -> EgResult<()> {
176        let mut db = DatabaseConnection::new_from_options(self.params());
177        db.connect()?;
178        self.db = Some(db);
179        Ok(())
180    }
181
182    /// Connects to the Evergreen bus.
183    ///
184    /// Does not parse the IDL, etc., assuming those steps have already
185    /// been taken.
186    pub fn connect_evergreen(&mut self) -> EgResult<()> {
187        let client = eg::Client::connect()?;
188        self.editor = Some(eg::Editor::new(&client));
189        Ok(())
190    }
191
192    /// Our core.
193    pub fn core(&self) -> &RunnerCore {
194        &self.core
195    }
196
197    /// Send messages to log::info! and additoinally log messages to
198    /// STDOUT when self.core.announce is true.
199    ///
200    /// Log prefix is appplied when set.
201    pub fn announce(&self, msg: &str) {
202        let pfx = self.core.log_prefix.as_deref().unwrap_or("");
203        if self.core.announce {
204            println!("{} {pfx}{msg}", date::now().format("%F %T%.3f"));
205        }
206        log::info!("{pfx}{msg}");
207    }
208
209    /// Set the announcement log prefix.
210    ///
211    /// Append a space so we don't have to do that at log time.
212    pub fn set_log_prefix(&mut self, p: &str) {
213        self.core.log_prefix = Some(p.to_string() + " ");
214    }
215
216    /// Apply an Editor.
217    ///
218    /// This does not propagate the authtoken or force the editor
219    /// to fetch/set its requestor value.  If needed, call
220    /// editor.apply_authtoken(script.authtoken()).
221    pub fn set_editor(&mut self, e: Editor) {
222        self.editor = Some(e);
223    }
224
225    /// Returns the staff account value.
226    pub fn staff_account(&self) -> i64 {
227        self.core.staff_account
228    }
229
230    /// Mutable ref to our editor.
231    ///
232    /// # Panics
233    ///
234    /// Panics if `with_evergreen` was false during init and no calls
235    /// to set_editor were made.
236    pub fn editor_mut(&mut self) -> &mut Editor {
237        self.editor.as_mut().unwrap()
238    }
239
240    /// Ref to our Editor.
241    ///
242    /// # Panics
243    ///
244    /// Panics if `with_evergreen` was false during init and no calls
245    /// to set_editor were made.
246    pub fn editor(&self) -> &Editor {
247        self.editor.as_ref().unwrap()
248    }
249
250    /// Ref to our compiled command line parameters.
251    pub fn params(&self) -> &getopts::Matches {
252        &self.core.params
253    }
254
255    /// Returns the active authtoken
256    ///
257    /// # Panics
258    ///
259    /// Panics if no auth session is present.
260    pub fn authtoken(&self) -> &str {
261        self.core.authtoken.as_deref().unwrap()
262    }
263
264    /// Returns a mutable ref to our database connection
265    ///
266    /// # Panics
267    ///
268    /// Panics if the database connection was not initialized.
269    ///
270    pub fn db(&mut self) -> &mut DatabaseConnection {
271        self.db
272            .as_mut()
273            .expect("database connection should be established")
274    }
275
276    /// Create an internal login session using the provided staff_account.
277    ///
278    /// Auth session is also linked to our Editor instance.
279    ///
280    /// Returns the auth token.
281    pub fn login_staff(&mut self) -> EgResult<String> {
282        let mut args =
283            auth::InternalLoginArgs::new(self.core.staff_account, auth::LoginType::Staff);
284
285        if let Some(ws) = self.core.staff_workstation.as_ref() {
286            args.set_workstation(ws);
287        }
288
289        let ses = auth::Session::internal_session_api(self.editor_mut().client_mut(), &args)?;
290
291        if let Some(s) = ses {
292            self.editor_mut().apply_authtoken(s.token())?;
293            self.core.authtoken = Some(s.token().to_string());
294            Ok(s.token().to_string())
295        } else {
296            Err("Could not retrieve auth session".into())
297        }
298    }
299}