vrp_cli/
lib.rs

1//! A crate for solving Vehicle Routing Problem using default metaheuristic.
2//!
3//!
4//! This crate provides ready-to-use functionality to solve rich ***Vehicle Routing Problem***.
5//!
6//! For more details check the following resources:
7//!
8//! - [`user guide`](https://reinterpretcat.github.io/vrp) describes how to use cli
9//!   application built from this crate
10//! - `vrp-core` crate implements default metaheuristic
11
12#![warn(missing_docs)]
13#![deny(unsafe_code)] // NOTE: use deny instead forbid as we need allow unsafe code for c_interop
14#![allow(clippy::items_after_test_module)]
15
16#[cfg(test)]
17#[path = "../tests/helpers/mod.rs"]
18#[macro_use]
19mod helpers;
20
21#[cfg(not(target_arch = "wasm32"))]
22#[cfg(test)]
23#[path = "../tests/features/mod.rs"]
24mod features;
25
26#[cfg(test)]
27#[path = "../tests/unit/lib_test.rs"]
28mod lib_test;
29
30pub use vrp_core as core;
31pub use vrp_pragmatic as pragmatic;
32#[cfg(feature = "scientific-format")]
33pub use vrp_scientific as scientific;
34
35pub mod extensions;
36
37use crate::extensions::import::import_problem;
38use crate::extensions::solve::config::{create_builder_from_config, Config};
39use std::io::{BufReader, BufWriter};
40use std::sync::Arc;
41use vrp_core::models::Problem as CoreProblem;
42use vrp_core::prelude::{GenericError, Solver};
43use vrp_pragmatic::format::problem::{serialize_problem, PragmaticProblem, Problem};
44use vrp_pragmatic::format::solution::{write_pragmatic, PragmaticOutputType};
45use vrp_pragmatic::format::FormatError;
46use vrp_pragmatic::get_unique_locations;
47use vrp_pragmatic::validation::ValidationContext;
48
49#[cfg(not(target_arch = "wasm32"))]
50#[allow(unsafe_code)]
51mod c_interop {
52    use super::*;
53    use crate::extensions::solve::config::read_config;
54    use std::ffi::{CStr, CString};
55    use std::os::raw::c_char;
56    use std::panic;
57    use std::panic::UnwindSafe;
58    use std::slice;
59    use vrp_core::prelude::GenericError;
60    use vrp_pragmatic::format::problem::{deserialize_matrix, deserialize_problem};
61    use vrp_pragmatic::format::{CoordIndex, MultiFormatError};
62
63    type Callback = extern "C" fn(*const c_char);
64
65    fn to_string(pointer: *const c_char) -> String {
66        let slice = unsafe { CStr::from_ptr(pointer).to_bytes() };
67        std::str::from_utf8(slice).unwrap().to_string()
68    }
69
70    fn call_back(result: Result<String, GenericError>, success: Callback, failure: Callback) {
71        match result {
72            Ok(ok) => {
73                let ok = CString::new(ok.as_bytes()).unwrap();
74                success(ok.as_ptr());
75            }
76            Err(err) => {
77                let error = CString::new(err.to_string().as_bytes()).unwrap();
78                failure(error.as_ptr());
79            }
80        };
81    }
82
83    fn catch_panic<F: FnOnce() + UnwindSafe>(failure: Callback, action: F) {
84        if let Err(err) = panic::catch_unwind(action) {
85            let message = err
86                .downcast_ref::<&str>()
87                .cloned()
88                .or_else(|| err.downcast_ref::<String>().map(|str| str.as_str()))
89                .map(|msg| format!("panic: '{msg}'"))
90                .unwrap_or_else(|| "panic with unknown type".to_string());
91
92            let error = CString::new(message.as_bytes()).unwrap();
93            failure(error.as_ptr());
94        }
95    }
96
97    /// Returns a list of unique locations which can be used to request a routing matrix.
98    /// A `problem` should be passed in `pragmatic` format.
99    #[no_mangle]
100    extern "C" fn get_routing_locations(problem: *const c_char, success: Callback, failure: Callback) {
101        catch_panic(failure, || {
102            let problem = to_string(problem);
103            let problem = BufReader::new(problem.as_bytes());
104            let result =
105                deserialize_problem(problem).map_err(From::from).and_then(|problem| get_locations_serialized(&problem));
106
107            call_back(result, success, failure);
108        });
109    }
110
111    /// Converts `problem` from format specified by `format` to `pragmatic` format.
112    #[no_mangle]
113    extern "C" fn convert_to_pragmatic(
114        format: *const c_char,
115        inputs: *const *const c_char,
116        input_len: usize,
117        success: Callback,
118        failure: Callback,
119    ) {
120        catch_panic(failure, || {
121            let format = to_string(format);
122            let inputs = unsafe { slice::from_raw_parts(inputs, input_len).to_vec() };
123            let inputs = inputs.iter().map(|p| to_string(*p)).collect::<Vec<_>>();
124            let readers = inputs.iter().map(|p| BufReader::new(p.as_bytes())).collect::<Vec<_>>();
125
126            match import_problem(format.as_str(), Some(readers)) {
127                Ok(problem) => {
128                    let mut writer = BufWriter::new(Vec::new());
129                    serialize_problem(&problem, &mut writer).unwrap();
130                    let bytes = writer.into_inner().expect("cannot use writer");
131                    let problem = CString::new(bytes).unwrap();
132
133                    success(problem.as_ptr());
134                }
135                Err(err) => {
136                    let error = CString::new(err.to_string().as_bytes()).unwrap();
137                    failure(error.as_ptr());
138                }
139            }
140        });
141    }
142
143    /// Validates Vehicle Routing Problem passed in `pragmatic` format.
144    #[no_mangle]
145    extern "C" fn validate_pragmatic(
146        problem: *const c_char,
147        matrices: *const *const c_char,
148        matrices_len: usize,
149        success: Callback,
150        failure: Callback,
151    ) {
152        catch_panic(failure, || {
153            let problem = to_string(problem);
154            let matrices = unsafe { slice::from_raw_parts(matrices, matrices_len).to_vec() };
155            let matrices = matrices.iter().map(|m| to_string(*m)).collect::<Vec<_>>();
156
157            let problem = deserialize_problem(BufReader::new(problem.as_bytes()));
158            let matrices = matrices
159                .iter()
160                .map(|matrix| deserialize_matrix(BufReader::new(matrix.as_bytes())))
161                .collect::<Result<Vec<_>, _>>();
162
163            let result = match (problem, matrices) {
164                (Ok(problem), Ok(matrices)) => {
165                    let matrices = if matrices.is_empty() { None } else { Some(&matrices) };
166                    let coord_index = CoordIndex::new(&problem);
167
168                    ValidationContext::new(&problem, matrices, &coord_index).validate()
169                }
170                (Err(errors), Ok(_)) | (Ok(_), Err(errors)) => Err(errors),
171                (Err(errors1), Err(errors2)) => {
172                    Err(MultiFormatError::from(errors1.into_iter().chain(errors2).collect::<Vec<_>>()))
173                }
174            }
175            .map_err(From::from)
176            .map(|_| "[]".to_string());
177
178            call_back(result, success, failure);
179        });
180    }
181
182    /// Solves Vehicle Routing Problem passed in `pragmatic` format.
183    #[no_mangle]
184    extern "C" fn solve_pragmatic(
185        problem: *const c_char,
186        matrices: *const *const c_char,
187        matrices_len: usize,
188        config: *const c_char,
189        success: Callback,
190        failure: Callback,
191    ) {
192        catch_panic(failure, || {
193            let problem = to_string(problem);
194            let matrices = unsafe { slice::from_raw_parts(matrices, matrices_len).to_vec() };
195            let matrices = matrices.iter().map(|m| to_string(*m)).collect::<Vec<_>>();
196
197            let result =
198                if matrices.is_empty() { problem.read_pragmatic() } else { (problem, matrices).read_pragmatic() }
199                    .map_err(From::from)
200                    .and_then(|problem| {
201                        read_config(BufReader::new(to_string(config).as_bytes()))
202                            .map_err(|err| GenericError::from(serialize_as_config_error(err.to_string().as_str())))
203                            .map(|config| (problem, config))
204                    })
205                    .and_then(|(problem, config)| get_solution_serialized(Arc::new(problem), config));
206
207            call_back(result, success, failure);
208        });
209    }
210
211    #[cfg(test)]
212    mod tests {
213        use super::*;
214        use crate::helpers::generate::SIMPLE_PROBLEM;
215
216        #[test]
217        fn can_use_to_string() {
218            let c_str = CString::new("asd").unwrap();
219            assert_eq!(to_string(c_str.as_ptr() as *const c_char), "asd".to_string());
220        }
221
222        #[test]
223        fn can_use_callback() {
224            // TODO: real check that data is passed is not really there
225
226            extern "C" fn success1(_: *const c_char) {}
227            extern "C" fn failure1(_: *const c_char) {
228                unreachable!()
229            }
230            call_back(Ok("success".to_string()), success1, failure1);
231
232            extern "C" fn success2(_: *const c_char) {
233                unreachable!()
234            }
235            extern "C" fn failure2(_: *const c_char) {}
236            call_back(Err("failure".into()), success2, failure2);
237        }
238
239        #[test]
240        fn can_catch_panic_with_string_literal() {
241            extern "C" fn callback(msg: *const c_char) {
242                assert_eq!(to_string(msg), "panic: 'invaders detected!'");
243            }
244            catch_panic(callback, || panic!("invaders detected!"));
245            catch_panic(callback, || panic!("invaders {}!", "detected"));
246        }
247
248        #[test]
249        fn can_get_locations() {
250            extern "C" fn success(locations: *const c_char) {
251                let locations = to_string(locations);
252                assert!(locations.starts_with('['));
253                assert!(locations.ends_with(']'));
254                assert!(locations.len() > 2);
255            }
256            extern "C" fn failure(err: *const c_char) {
257                unreachable!("got {}", to_string(err))
258            }
259
260            let problem = CString::new(SIMPLE_PROBLEM).unwrap();
261            get_routing_locations(problem.as_ptr() as *const c_char, success, failure)
262        }
263
264        #[test]
265        fn can_validate_simple_problem() {
266            extern "C" fn success(solution: *const c_char) {
267                assert_eq!(to_string(solution), "[]")
268            }
269            extern "C" fn failure(err: *const c_char) {
270                unreachable!("{}", to_string(err))
271            }
272
273            let problem = CString::new(SIMPLE_PROBLEM).unwrap();
274            let matrices = CString::new("[]").unwrap();
275
276            validate_pragmatic(
277                problem.as_ptr() as *const c_char,
278                matrices.as_ptr() as *const *const c_char,
279                0,
280                success,
281                failure,
282            );
283        }
284
285        #[test]
286        fn can_validate_empty_problem() {
287            extern "C" fn success(solution: *const c_char) {
288                unreachable!("got {}", to_string(solution))
289            }
290            extern "C" fn failure(err: *const c_char) {
291                let err = to_string(err);
292                assert!(err.contains("E0000"));
293                assert!(err.contains("cause"));
294                assert!(err.contains("action"));
295            }
296
297            let problem = CString::new("").unwrap();
298            let matrices = CString::new("[]").unwrap();
299
300            validate_pragmatic(
301                problem.as_ptr() as *const c_char,
302                matrices.as_ptr() as *const *const c_char,
303                0,
304                success,
305                failure,
306            );
307        }
308
309        #[test]
310        fn can_solve_problem() {
311            extern "C" fn success(solution: *const c_char) {
312                let solution = to_string(solution);
313                assert!(solution.starts_with('{'));
314                assert!(solution.ends_with('}'));
315                assert!(solution.len() > 2);
316            }
317            extern "C" fn failure(err: *const c_char) {
318                unreachable!("{}", to_string(err))
319            }
320
321            let problem = CString::new(SIMPLE_PROBLEM).unwrap();
322            let matrices = CString::new("[]").unwrap();
323            let config = CString::new("{\"termination\": {\"max-generations\": 1}}").unwrap();
324
325            solve_pragmatic(
326                problem.as_ptr() as *const c_char,
327                matrices.as_ptr() as *const *const c_char,
328                0,
329                config.as_ptr() as *const c_char,
330                success,
331                failure,
332            );
333        }
334    }
335}
336
337#[cfg(feature = "py_bindings")]
338#[cfg(not(target_arch = "wasm32"))]
339mod py_interop {
340    use super::*;
341    use crate::extensions::solve::config::read_config;
342    use pyo3::exceptions::PyOSError;
343    use pyo3::prelude::*;
344    use std::io::BufReader;
345    use vrp_pragmatic::format::problem::{deserialize_matrix, deserialize_problem};
346    use vrp_pragmatic::format::CoordIndex;
347
348    // TODO avoid duplications between 3 interop approaches
349
350    /// Converts `problem` from format specified by `format` to `pragmatic` format.
351    #[pyfunction]
352    fn convert_to_pragmatic(format: &str, inputs: Vec<String>) -> PyResult<String> {
353        let readers = inputs.iter().map(|p| BufReader::new(p.as_bytes())).collect::<Vec<_>>();
354        import_problem(format, Some(readers))
355            .and_then(|problem| {
356                let mut writer = BufWriter::new(Vec::new());
357                serialize_problem(&problem, &mut writer).unwrap();
358
359                writer
360                    .into_inner()
361                    .map_err(|err| format!("BufWriter: {err}").into())
362                    .and_then(|bytes| String::from_utf8(bytes).map_err(|err| format!("StringUTF8: {err}").into()))
363            })
364            .map_err(|err| PyOSError::new_err(err.to_string()))
365    }
366
367    /// Returns a list of unique locations which can be used to request a routing matrix.
368    #[pyfunction]
369    fn get_routing_locations(problem: String) -> PyResult<String> {
370        deserialize_problem(BufReader::new(problem.as_bytes()))
371            .map_err(From::from)
372            .and_then(|problem| get_locations_serialized(&problem))
373            .map_err(|err| PyOSError::new_err(err.to_string()))
374    }
375
376    /// Validates and solves Vehicle Routing Problem.
377    #[pyfunction]
378    fn solve_pragmatic(problem: String, matrices: Vec<String>, config: String) -> PyResult<String> {
379        // validate first
380        deserialize_problem(BufReader::new(problem.as_bytes()))
381            .and_then(|problem| {
382                matrices
383                    .iter()
384                    .map(|m| deserialize_matrix(BufReader::new(m.as_bytes())))
385                    .collect::<Result<Vec<_>, _>>()
386                    .map(|matrices| (problem, matrices))
387            })
388            .and_then(|(problem, matrices)| {
389                let matrices = if matrices.is_empty() { None } else { Some(&matrices) };
390                let coord_index = CoordIndex::new(&problem);
391
392                ValidationContext::new(&problem, matrices, &coord_index).validate()
393            })
394            .map_err(|errs| PyOSError::new_err(errs.to_string()))?;
395
396        // try solve problem
397        if matrices.is_empty() { problem.read_pragmatic() } else { (problem, matrices).read_pragmatic() }
398            .map_err(From::from)
399            .and_then(|problem| {
400                read_config(BufReader::new(config.as_bytes()))
401                    .map_err(|err| GenericError::from(serialize_as_config_error(err.to_string().as_str())))
402                    .map(|config| (problem, config))
403            })
404            .and_then(|(problem, config)| get_solution_serialized(Arc::new(problem), config))
405            .map_err(|err| PyOSError::new_err(err.to_string()))
406    }
407
408    #[pymodule]
409    fn vrp_cli(m: &Bound<'_, PyModule>) -> PyResult<()> {
410        m.add_function(wrap_pyfunction!(convert_to_pragmatic, m)?)?;
411        m.add_function(wrap_pyfunction!(get_routing_locations, m)?)?;
412        m.add_function(wrap_pyfunction!(solve_pragmatic, m)?)?;
413        Ok(())
414    }
415}
416
417#[cfg(target_arch = "wasm32")]
418mod wasm {
419    extern crate serde_json;
420    extern crate wasm_bindgen;
421
422    use super::*;
423    use vrp_pragmatic::format::problem::Matrix;
424    use vrp_pragmatic::format::CoordIndex;
425    use wasm_bindgen::prelude::*;
426
427    /// Returns a list of unique locations which can be used to request a routing matrix.
428    /// A `problem` should be passed in `pragmatic` format.
429    #[wasm_bindgen]
430    pub fn get_routing_locations(problem: JsValue) -> Result<JsValue, JsValue> {
431        let problem: Problem =
432            serde_wasm_bindgen::from_value(problem).map_err(|err| JsValue::from_str(err.to_string().as_str()))?;
433
434        get_locations_serialized(&problem)
435            .map(|locations| JsValue::from_str(locations.as_str()))
436            .map_err(|err| JsValue::from_str(err.to_string().as_str()))
437    }
438
439    /// Validates Vehicle Routing Problem passed in `pragmatic` format.
440    #[wasm_bindgen]
441    pub fn validate_pragmatic(problem: JsValue, matrices: JsValue) -> Result<JsValue, JsValue> {
442        let problem: Problem =
443            serde_wasm_bindgen::from_value(problem).map_err(|err| JsValue::from_str(err.to_string().as_str()))?;
444        let matrices: Vec<Matrix> =
445            serde_wasm_bindgen::from_value(matrices).map_err(|err| JsValue::from_str(err.to_string().as_str()))?;
446        let coord_index = CoordIndex::new(&problem);
447
448        let matrices = if matrices.is_empty() { None } else { Some(&matrices) };
449        ValidationContext::new(&problem, matrices, &coord_index)
450            .validate()
451            .map_err(|errs| JsValue::from_str(errs.to_json().as_str()))
452            .map(|_| JsValue::from_str("[]"))
453    }
454
455    /// Converts `problem` from format specified by `format` to `pragmatic` format.
456    #[wasm_bindgen]
457    pub fn convert_to_pragmatic(format: &str, inputs: JsValue) -> Result<JsValue, JsValue> {
458        let inputs: Vec<String> =
459            serde_wasm_bindgen::from_value(inputs).map_err(|err| JsValue::from_str(err.to_string().as_str()))?;
460
461        let readers = inputs.iter().map(|input| BufReader::new(input.as_bytes())).collect();
462
463        match import_problem(format, Some(readers)) {
464            Ok(problem) => {
465                let mut writer = BufWriter::new(Vec::new());
466                serialize_problem(&problem, &mut writer).unwrap();
467
468                let bytes = writer.into_inner().unwrap();
469                let result = String::from_utf8(bytes).unwrap();
470
471                Ok(JsValue::from_str(result.as_str()))
472            }
473            Err(err) => Err(JsValue::from_str(err.to_string().as_str())),
474        }
475    }
476
477    /// Solves Vehicle Routing Problem passed in `pragmatic` format.
478    #[wasm_bindgen]
479    pub fn solve_pragmatic(problem: JsValue, matrices: JsValue, config: JsValue) -> Result<JsValue, JsValue> {
480        let problem: Problem =
481            serde_wasm_bindgen::from_value(problem).map_err(|err| JsValue::from_str(err.to_string().as_str()))?;
482
483        let matrices: Vec<Matrix> =
484            serde_wasm_bindgen::from_value(matrices).map_err(|err| JsValue::from_str(err.to_string().as_str()))?;
485
486        let problem = Arc::new(
487            if matrices.is_empty() { problem.read_pragmatic() } else { (problem, matrices).read_pragmatic() }
488                .map_err(|errs| JsValue::from_str(errs.to_json().as_str()))?,
489        );
490
491        let config: Config = serde_wasm_bindgen::from_value(config)
492            .map_err(|err| serialize_as_config_error(&err.to_string()))
493            .map_err(|err| JsValue::from_str(err.as_str()))?;
494
495        get_solution_serialized(problem, config)
496            .map(|problem| JsValue::from_str(problem.as_str()))
497            .map_err(|err| JsValue::from_str(&err.to_string()))
498    }
499}
500
501/// Gets locations serialized in json.
502pub fn get_locations_serialized(problem: &Problem) -> Result<String, GenericError> {
503    // TODO validate the problem?
504
505    let locations = get_unique_locations(problem);
506    serde_json::to_string_pretty(&locations).map_err(|err| err.to_string().into())
507}
508
509/// Gets solution serialized in json.
510pub fn get_solution_serialized(problem: Arc<CoreProblem>, config: Config) -> Result<String, GenericError> {
511    let solution = create_builder_from_config(problem.clone(), Default::default(), &config)
512        .and_then(|builder| builder.build())
513        .map(|config| Solver::new(problem.clone(), config))
514        .and_then(|solver| solver.solve())
515        .map_err(|err| {
516            FormatError::new(
517                "E0003".to_string(),
518                "cannot find any solution".to_string(),
519                format!("please submit a bug and share original problem and routing matrix. Error: '{err}'"),
520            )
521            .to_json()
522        })?;
523
524    let output_type = if config.output.and_then(|output_cfg| output_cfg.include_geojson).unwrap_or(false) {
525        PragmaticOutputType::Combined
526    } else {
527        Default::default()
528    };
529
530    let mut writer = BufWriter::new(Vec::new());
531    write_pragmatic(problem.as_ref(), &solution, output_type, &mut writer)?;
532
533    let bytes = writer.into_inner().map_err(|err| format!("{err}"))?;
534    let result = String::from_utf8(bytes).map_err(|err| format!("{err}"))?;
535
536    Ok(result)
537}
538
539fn serialize_as_config_error(err: &str) -> String {
540    FormatError::new(
541        "E0004".to_string(),
542        "cannot read config".to_string(),
543        format!("check config definition. Error: '{err}'"),
544    )
545    .to_json()
546}