termite_dmg/cpp/
mod.rs

1//!
2//! This module handles generation of c++ code to support a data model, it
3//! includes the ability to create a header file, (de)serialization and
4//! documentation.
5//!
6//! For any data model to work the termite dependency must be generatred from
7//! get_termite_dependency() and be saved as "termite.hpp" at a location where
8//! it can be included as "#include <termite.hpp>"
9//!
10
11use indoc::formatdoc;
12use std::{
13    char::ToLowercase,
14    collections::{HashMap, HashSet},
15    fmt,
16};
17
18mod type_array;
19mod type_constrained;
20mod type_enum;
21mod type_struct;
22mod type_variant;
23
24use type_array::Array;
25use type_constrained::ConstrainedType;
26use type_enum::Enum;
27use type_struct::Struct;
28use type_variant::Variant;
29
30use crate::data_model;
31
32/// Iterator to convert an iterator of chars to snake case converting all
33/// uppercase characters to an underscore and the lowercase character
34struct ToSnakeCase<'a> {
35    /// The characters to convert to snake case
36    chars: &'a mut dyn Iterator<Item = char>,
37    /// The characters currently being converted to lowercase
38    set_lower: Option<ToLowercase>,
39}
40
41impl<'a> ToSnakeCase<'a> {
42    /// Creates a new ToSnakeCase object
43    ///
44    /// # Parameters
45    ///
46    /// chars: The iterator of the characters to convert
47    fn new(chars: &'a mut dyn Iterator<Item = char>) -> Self {
48        // Make sure the first character is lowercase without an underscore
49        let set_lower = if let Some(first_char) = chars.next() {
50            Some(first_char.to_lowercase())
51        } else {
52            None
53        };
54
55        return Self { chars, set_lower };
56    }
57}
58
59impl<'a> Iterator for ToSnakeCase<'a> {
60    type Item = char;
61
62    fn next(&mut self) -> Option<Self::Item> {
63        // Set to lower case
64        if let Some(set_lower) = &mut self.set_lower {
65            // Get the next character
66            if let Some(next_char) = set_lower.next() {
67                return Some(next_char);
68            }
69
70            // Finish up setting to lowercase
71            self.set_lower = None;
72        }
73
74        // Get next character
75        return if let Some(next_char) = self.chars.next() {
76            // Set to lowercase if it is uppercase
77            if next_char.is_uppercase() {
78                self.set_lower = Some(next_char.to_lowercase());
79                Some('_')
80            } else {
81                Some(next_char)
82            }
83        } else {
84            None
85        };
86    }
87}
88
89/// Obtains the base termite c++ dependency required for all generated data
90/// models
91pub fn get_termite_dependency() -> &'static str {
92    return include_str!("termite.hpp");
93}
94
95/// Obtains the yaml-cpp interface header and source for reading and writing yaml files
96pub fn get_yaml_interface() -> (&'static str, &'static str) {
97    return (
98        include_str!("termite-yaml.h"),
99        include_str!("termite-yaml.cpp"),
100    );
101}
102
103/// Obtains the nlohmann::json interface header and source for reading and writing json files
104pub fn get_json_interface() -> (&'static str, &'static str) {
105    return (
106        include_str!("termite-json.h"),
107        include_str!("termite-json.cpp"),
108    );
109}
110
111/// An entire data model
112#[derive(Clone, Debug, PartialEq)]
113pub struct DataModel {
114    /// List of the the data types to implement
115    data_types: Vec<DataType>,
116    /// List of all header data used to include external packages
117    headers: Headers,
118    /// List of all footer data
119    footers: Footers,
120    /// The nested namespace to put the data model into
121    namespace: Vec<String>,
122    /// A map of all macros to expand default values
123    macros: HashMap<String, data_model::SerializationModel>,
124}
125
126impl DataModel {
127    /// Constructs a new c++ data model from a generic data model
128    ///
129    /// # Parameters
130    ///
131    /// data: The generic data type to convert
132    pub fn new(data: crate::DataModel) -> Result<Self, Error> {
133        let data_types = data
134            .data_types
135            .into_iter()
136            .enumerate()
137            .map(|(i, data_type)| {
138                return match DataType::new(data_type) {
139                    Ok(result) => Ok(result),
140                    Err(error) => Err(error.add_element("data_types", i)),
141                };
142            })
143            .collect::<Result<Vec<DataType>, Error>>()?;
144        let headers = match Headers::new(data.headers) {
145            Ok(result) => result,
146            Err(error) => return Err(error.add_field("headers")),
147        };
148        let footers = match Footers::new(data.footers) {
149            Ok(result) => result,
150            Err(error) => return Err(error.add_field("footers")),
151        };
152
153        return Ok(Self {
154            data_types,
155            headers,
156            footers,
157            namespace: data.namespace,
158            macros: data.macros,
159        });
160    }
161
162    /// Generates the header file
163    ///
164    /// # Parameters
165    ///
166    /// name: The name of the header file (used for header guard so should be capslocked)
167    ///
168    /// indent: The number of spaces to use for indentation
169    pub fn get_header(&self, name: &str, indent: usize) -> Result<String, Error> {
170        // Get the namespace
171        let namespace = self.namespace.join("::");
172        let namespace_begin = if namespace.is_empty() {
173            format!("")
174        } else {
175            format!("namespace {namespace} {{")
176        };
177        let namespace_end = if namespace.is_empty() {
178            format!("")
179        } else {
180            format!("}} // namespace {namespace}")
181        };
182
183        // Get all structs
184        let data_types = self
185            .data_types
186            .iter()
187            .map(|data_type| data_type.get_definition_header(indent))
188            .collect::<Vec<String>>()
189            .join("\n\n");
190
191        // Get all parsers
192        let parsers = self
193            .data_types
194            .iter()
195            .map(|data_type| data_type.get_parser_header(&self.namespace))
196            .collect::<Vec<String>>()
197            .join("\n\n");
198
199        // Expand macros in the header and footer
200        let header = match data_model::expand_macros(
201            &data_model::SerializationModel::Value(self.headers.header.clone()),
202            &self.macros,
203            &mut HashSet::new(),
204        )? {
205            data_model::SerializationModel::Value(value) => value,
206            _ => {
207                return Err(Error {
208                    location: "".to_string(),
209                    error: ErrorCore::HeaderMacro(self.headers.header.clone()),
210                })
211            }
212        };
213        let footer = match data_model::expand_macros(
214            &data_model::SerializationModel::Value(self.footers.header.clone()),
215            &self.macros,
216            &mut HashSet::new(),
217        )? {
218            data_model::SerializationModel::Value(value) => value,
219            _ => {
220                return Err(Error {
221                    location: "".to_string(),
222                    error: ErrorCore::FooterMacro(self.footers.header.clone()),
223                })
224            }
225        };
226
227        return Ok(formatdoc!(
228            "
229            // Generated with the Termite Data Model Generator
230            #ifndef {name}_TERMITE_H_INCLUDED
231            #define {name}_TERMITE_H_INCLUDED
232
233            #include <iostream>
234            #include <sstream>
235            #include <optional>
236            #include <variant>
237            #include <algorithm>
238            #include <termite.hpp>
239
240            {header}
241
242            {namespace_begin}
243
244            {data_types}
245
246            {namespace_end}
247
248            namespace termite {{
249
250            {parsers}
251
252            }} // namespace termite
253            
254            {footer}
255            
256            #endif
257            ",
258        ));
259    }
260
261    /// Generates the source file
262    ///
263    /// # Parameters
264    ///
265    /// name: The file location for the associated header file (is used for #include "name")
266    ///
267    /// indent: The number of spaces to use for indentation
268    pub fn get_source(&self, name: &str, indent: usize) -> Result<String, Error> {
269        // Get the namespace
270        let namespace = self.namespace.join("::");
271        let namespace_begin = if namespace.is_empty() {
272            format!("")
273        } else {
274            format!("namespace {namespace} {{")
275        };
276        let namespace_end = if namespace.is_empty() {
277            format!("")
278        } else {
279            format!("}} // namespace {namespace}")
280        };
281
282        // Get all structs
283        let data_types = self
284            .data_types
285            .iter()
286            .map(|data_type| data_type.get_definition_source(&self.macros, indent))
287            .collect::<Result<Vec<_>, _>>()?
288            .join("\n\n");
289
290        // Get all parsers
291        let parsers = self
292            .data_types
293            .iter()
294            .map(|data_type| data_type.get_parser_source(indent, &self.namespace, &self.data_types))
295            .collect::<Vec<String>>()
296            .join("\n\n");
297
298        // Expand macros in the header and footer
299        let header = match data_model::expand_macros(
300            &data_model::SerializationModel::Value(self.headers.source.clone()),
301            &self.macros,
302            &mut HashSet::new(),
303        )? {
304            data_model::SerializationModel::Value(value) => value,
305            _ => {
306                return Err(Error {
307                    location: "".to_string(),
308                    error: ErrorCore::HeaderMacro(self.headers.source.clone()),
309                })
310            }
311        };
312        let footer = match data_model::expand_macros(
313            &data_model::SerializationModel::Value(self.footers.source.clone()),
314            &self.macros,
315            &mut HashSet::new(),
316        )? {
317            data_model::SerializationModel::Value(value) => value,
318            _ => {
319                return Err(Error {
320                    location: "".to_string(),
321                    error: ErrorCore::FooterMacro(self.footers.source.clone()),
322                })
323            }
324        };
325
326        return Ok(formatdoc!("
327            // Generated with the Termite Data Model Generator
328            #include \"{name}.h\"
329
330            {header}
331
332            {namespace_begin}
333
334            namespace {{
335
336            // Code to make printing easier
337            template <typename T, typename = void>
338            struct has_insertion_operator : std::false_type {{}};
339            template <typename T>
340            struct has_insertion_operator<T, std::void_t<decltype(std::declval<std::ostream &>() << std::declval<T>())>> : std::true_type {{}};
341
342            template <typename T>
343            typename std::enable_if<has_insertion_operator<T>::value, std::ostream &>::type
344            operator<<(std::ostream &os, const std::optional<T> &value) {{
345            {0:indent$}if (value) {{
346            {0:indent$}{0:indent$}return os << *value;
347            {0:indent$}}} else {{
348            {0:indent$}{0:indent$}return os << \"nullopt\";
349            {0:indent$}}}
350            }}
351
352            template <typename T>
353            typename std::enable_if<has_insertion_operator<T>::value, std::ostream &>::type
354            operator<<(std::ostream &os, const std::vector<T> &value) {{
355            {0:indent$}os << \"[ \";
356            {0:indent$}for (auto value_it = value.cbegin(); value_it != value.cend(); ++value_it) {{
357            {0:indent$}{0:indent$}if (value_it != value.cbegin()) {{
358            {0:indent$}{0:indent$}{0:indent$}os << \", \";
359            {0:indent$}{0:indent$}}}
360            {0:indent$}{0:indent$}os << *value_it;
361            {0:indent$}}}
362            {0:indent$}return os << \" ]\";
363            }}
364
365            }} // namespace
366
367            {data_types}
368
369            {namespace_end}
370
371            namespace termite {{
372            
373            {parsers}
374
375            }} // namespace termite
376            
377            {footer}
378            ",
379            "",
380        ));
381    }
382}
383
384/// All of the headers for the different files
385#[derive(Clone, Debug, PartialEq)]
386struct Headers {
387    /// For the header file
388    header: String,
389    /// For the source file
390    source: String,
391}
392
393impl Headers {
394    /// Constructs a new c++ header from a generic header
395    ///
396    /// # Parameters
397    ///
398    /// data: The generic data type to convert
399    fn new(mut data: HashMap<String, String>) -> Result<Self, Error> {
400        let source = match data.remove("cpp-source") {
401            Some(value) => value,
402            None => String::new(),
403        };
404        let header = match data.remove("cpp-header") {
405            Some(value) => value,
406            None => String::new(),
407        };
408
409        return Ok(Self { header, source });
410    }
411}
412
413/// All of the footers for the different files
414#[derive(Clone, Debug, PartialEq)]
415struct Footers {
416    /// For the header file
417    header: String,
418    /// For the source file
419    source: String,
420}
421
422impl Footers {
423    /// Constructs a new c++ footer from a generic footer
424    ///
425    /// # Parameters
426    ///
427    /// data: The generic data type to convert
428    fn new(mut data: HashMap<String, String>) -> Result<Self, Error> {
429        let source = match data.remove("cpp-source") {
430            Some(value) => value,
431            None => String::new(),
432        };
433        let header = match data.remove("cpp-header") {
434            Some(value) => value,
435            None => String::new(),
436        };
437
438        return Ok(Self { header, source });
439    }
440}
441
442/// Any data type (struct, variant, ect.)
443#[derive(Clone, Debug, PartialEq)]
444struct DataType {
445    /// The name of the type
446    name: String,
447    /// The description of the type
448    description: Option<String>,
449    /// The type specific data
450    data: DataTypeData,
451}
452
453impl DataType {
454    /// Constructs a new c++ data type from a generic data type
455    ///
456    /// # Parameters
457    ///
458    /// data: The generic data type to convert
459    fn new(data: crate::DataType) -> Result<Self, Error> {
460        // Convert the data
461        let processed_data = match DataTypeData::new(data.data) {
462            Ok(data) => data,
463            Err(error) => return Err(error.add_field(&data.name)),
464        };
465
466        return Ok(Self {
467            name: data.name,
468            description: data.description,
469            data: processed_data,
470        });
471    }
472
473    /// Generates the description if it is supplied
474    fn get_description(&self) -> String {
475        return match &self.description {
476            Some(description) => description.clone(),
477            None => "".to_string(),
478        };
479    }
480
481    /// Converts the data type to a string for use in the header file
482    ///
483    /// # Parameters
484    ///
485    /// indent: The number of spaces to use for indentation
486    fn get_definition_header(&self, indent: usize) -> String {
487        return formatdoc!(
488            "
489            /**
490             * @brief {description}
491             * 
492             */
493            {definition}",
494            description = self.get_description(),
495            definition = self.data.get_definition_header(&self.name, indent),
496        );
497    }
498
499    /// Converts the data type to a string for use in the source file
500    ///
501    /// # Parameters
502    ///
503    /// macros: A map of all macros to expand default values
504    ///
505    /// indent: The number of spaces to use for indentation
506    fn get_definition_source(
507        &self,
508        macros: &HashMap<String, data_model::SerializationModel>,
509        indent: usize,
510    ) -> Result<String, Error> {
511        return Ok(formatdoc!(
512            "
513            {definition}",
514            definition = self
515                .data
516                .get_definition_source(&self.name, macros, indent)?,
517        ));
518    }
519
520    /// Gets the header code for the parser for this type allowing it to be read from a file
521    ///
522    /// # Parameters
523    ///
524    /// namespace: The namespace of the type
525    pub(super) fn get_parser_header(&self, namespace: &[String]) -> String {
526        return self.data.get_parser_header(&self.name, namespace);
527    }
528
529    /// Gets the source code for the parser for this type allowing it to be read from a file
530    ///
531    /// # Parameters
532    ///
533    /// indent: The number of spaces to use for indentation
534    ///
535    /// namespace: The namespace of the type
536    ///
537    /// data_types: List of all the data types defined in the data model
538    pub(super) fn get_parser_source(
539        &self,
540        indent: usize,
541        namespace: &[String],
542        data_types: &[DataType],
543    ) -> String {
544        return self
545            .data
546            .get_parser_source(&self.name, indent, namespace, data_types);
547    }
548}
549
550/// Supplies the type specific information for a data type
551#[derive(Clone, Debug, PartialEq)]
552enum DataTypeData {
553    /// Describes a struct
554    Struct(Struct),
555    /// Describes an array
556    Array(Array),
557    /// Describes a variant
558    Variant(Variant),
559    /// Describes an enum
560    Enum(Enum),
561    /// Describes a constrained type
562    ConstrainedType(ConstrainedType),
563}
564
565impl DataTypeData {
566    /// Constructs a new c++ data type data from a generic data type data
567    ///
568    /// # Parameters
569    ///
570    /// data: The generic data type data to convert
571    fn new(data: crate::DataTypeData) -> Result<Self, Error> {
572        let result = match data {
573            crate::DataTypeData::Struct(data) => DataTypeData::Struct(Struct::new(data)?),
574            crate::DataTypeData::Array(data) => DataTypeData::Array(Array::new(data)?),
575            crate::DataTypeData::Variant(data) => DataTypeData::Variant(Variant::new(data)?),
576            crate::DataTypeData::Enum(data) => DataTypeData::Enum(Enum::new(data)?),
577            crate::DataTypeData::ConstrainedType(data) => {
578                DataTypeData::ConstrainedType(ConstrainedType::new(data)?)
579            }
580        };
581
582        return Ok(result);
583    }
584
585    /// Converts the data type data to a string for use in the header file
586    ///
587    /// # Parameters
588    ///
589    /// name: The name of the data type
590    ///
591    /// indent: The number of spaces to use for indentation
592    fn get_definition_header(&self, name: &str, indent: usize) -> String {
593        return match self {
594            DataTypeData::Struct(data) => data.get_definition_header(name, indent),
595            DataTypeData::Array(data) => data.get_definition_header(name, indent),
596            DataTypeData::Variant(data) => data.get_definition_header(name, indent),
597            DataTypeData::Enum(data) => data.get_definition_header(name, indent),
598            DataTypeData::ConstrainedType(data) => data.get_definition_header(name, indent),
599        };
600    }
601
602    /// Converts the data type data to a string for use in the source file
603    ///
604    /// # Parameters
605    ///
606    /// name: The name of the data type
607    ///
608    /// macros: A map of all macros to expand default values
609    ///
610    /// indent: The number of spaces to use for indentation
611    fn get_definition_source(
612        &self,
613        name: &str,
614        macros: &HashMap<String, data_model::SerializationModel>,
615        indent: usize,
616    ) -> Result<String, Error> {
617        return match self {
618            DataTypeData::Struct(data) => data.get_definition_source(name, macros, indent),
619            DataTypeData::Array(data) => Ok(data.get_definition_source(name, indent)),
620            DataTypeData::Variant(data) => Ok(data.get_definition_source(name, indent)),
621            DataTypeData::Enum(data) => Ok(data.get_definition_source(name, indent)),
622            DataTypeData::ConstrainedType(data) => Ok(data.get_definition_source(name, indent)),
623        };
624    }
625
626    /// Gets the header code for the parser for this type allowing it to be read from a file
627    ///
628    /// # Parameters
629    ///
630    /// name: The name of the type
631    ///
632    /// namespace: The namespace of the type
633    pub(super) fn get_parser_header(&self, name: &str, namespace: &[String]) -> String {
634        return match self {
635            DataTypeData::Struct(data) => data.get_parser_header(name, namespace),
636            DataTypeData::Array(data) => data.get_parser_header(name, namespace),
637            DataTypeData::Variant(data) => data.get_parser_header(name, namespace),
638            DataTypeData::Enum(data) => data.get_parser_header(name, namespace),
639            DataTypeData::ConstrainedType(data) => data.get_parser_header(name, namespace),
640        };
641    }
642
643    /// Gets the source code for the parser for this type allowing it to be read from a file
644    ///
645    /// # Parameters
646    ///
647    /// name: The name of the type
648    ///
649    /// indent: The number of spaces to use for indentation
650    ///
651    /// namespace: The namespace of the type
652    ///
653    /// data_types: List of all the data types defined in the data model
654    pub(super) fn get_parser_source(
655        &self,
656        name: &str,
657        indent: usize,
658        namespace: &[String],
659        data_types: &[DataType],
660    ) -> String {
661        return match self {
662            DataTypeData::Struct(data) => {
663                data.get_parser_source(name, indent, namespace, data_types)
664            }
665            DataTypeData::Array(data) => {
666                data.get_parser_source(name, indent, namespace, data_types)
667            }
668            DataTypeData::Variant(data) => {
669                data.get_parser_source(name, indent, namespace, data_types)
670            }
671            DataTypeData::Enum(data) => data.get_parser_source(name, indent, namespace, data_types),
672            DataTypeData::ConstrainedType(data) => {
673                data.get_parser_source(name, indent, namespace, data_types)
674            }
675        };
676    }
677}
678
679/// Errors for when converting generic data models into c++ data models
680/// including location
681#[derive(Debug, Clone)]
682pub struct Error {
683    /// The location where the error occured
684    pub location: String,
685    /// The actual error that occured
686    pub error: ErrorCore,
687}
688
689impl Error {
690    /// Sets the current location to be the field of the given base
691    ///
692    /// # Parameters
693    ///
694    /// base: The base to set in the location
695    fn add_field(self, base: &str) -> Error {
696        let location = if !self.location.is_empty() {
697            format!("{}.{}", base, self.location)
698        } else {
699            base.to_string()
700        };
701
702        return Error {
703            location,
704            error: self.error,
705        };
706    }
707
708    /// Sets the current location to be the element of a field of the given base
709    ///
710    /// # Parameters
711    ///
712    /// base: The base to set in the location
713    ///
714    /// index: The index of the field
715    fn add_element(self, base: &str, index: usize) -> Error {
716        let location = if !self.location.is_empty() {
717            format!("{}[{}].{}", base, index, self.location)
718        } else {
719            format!("{}[{}]", base, index)
720        };
721
722        return Error {
723            location,
724            error: self.error,
725        };
726    }
727}
728
729impl fmt::Display for Error {
730    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
731        return write!(f, "{}: {}", self.location, self.error);
732    }
733}
734
735impl From<data_model::Error> for Error {
736    fn from(value: data_model::Error) -> Self {
737        return Error {
738            location: value.location.clone(),
739            error: ErrorCore::MacroError(value),
740        };
741    }
742}
743
744/// Errors for when converting generic data models into c++ data models
745#[derive(thiserror::Error, Debug, Clone)]
746pub enum ErrorCore {
747    /// Error expanding macros
748    #[error("An error occured when expanding macros: {:?}", .0)]
749    MacroError(data_model::Error),
750    /// The macro expansion in the header failed
751    #[error("The header \"{:?}\" must only expand to a string when using macros", .0)]
752    HeaderMacro(String),
753    /// The macro expansion in the footer failed
754    #[error("The footer \"{:?}\" must only expand to a string when using macros", .0)]
755    FooterMacro(String),
756}
757
758#[cfg(test)]
759pub(crate) mod test_utils {
760    use std::{fs, path, process};
761
762    pub(crate) fn str_diff(lhs: &str, rhs: &str) -> Option<(usize, String, String)> {
763        if let Some(error) = lhs
764            .trim()
765            .lines()
766            .zip(rhs.trim().lines())
767            .enumerate()
768            .filter_map(|(index, (lhs, rhs))| {
769                return if lhs.trim() == rhs.trim() {
770                    None
771                } else {
772                    Some((index + 1, lhs.trim().to_string(), rhs.trim().to_string()))
773                };
774            })
775            .next()
776        {
777            return Some(error);
778        }
779
780        if lhs.trim().lines().count() != rhs.trim().lines().count() {
781            return Some((
782                0,
783                format!("{}", lhs.trim().lines().count()),
784                format!("{}", rhs.trim().lines().count()),
785            ));
786        }
787
788        return None;
789    }
790
791    fn get_source_path(name: &str) -> path::PathBuf {
792        // Get the filename
793        let filename = path::Path::new(name).file_name().unwrap().to_str().unwrap();
794
795        return path::Path::new("tests/cpp")
796            .join(format!("{name}"))
797            .join(format!("{filename}.cpp"));
798    }
799
800    fn get_test_path(name: &str) -> path::PathBuf {
801        // Get the filename
802        let filename = path::Path::new(name).file_name().unwrap().to_str().unwrap();
803
804        return path::Path::new("tests/cpp")
805            .join(format!("{name}"))
806            .join(format!("{filename}_test.cpp"));
807    }
808
809    fn get_exe_path(name: &str) -> path::PathBuf {
810        // Get the filename
811        let filename = path::Path::new(name).file_name().unwrap().to_str().unwrap();
812
813        return path::Path::new("target/tests/cpp")
814            .join(format!("{name}"))
815            .join(filename);
816    }
817
818    pub(crate) fn compile_and_test(name: &str) {
819        // Get the paths
820        let source_path = get_source_path(name);
821        let test_path = get_test_path(name);
822        let exe_path = get_exe_path(name);
823
824        // Create the output directory
825        fs::create_dir_all(exe_path.parent().unwrap()).unwrap();
826
827        // Compile code
828        let compile_output = if cfg!(target_os = "windows") {
829            process::Command::new("cmd")
830                .arg("/C")
831                .arg(format!(
832                    "g++ {} {} -Isrc/cpp -Wall -std=c++17 -o {}.exe",
833                    source_path.to_str().unwrap(),
834                    test_path.to_str().unwrap(),
835                    exe_path.to_str().unwrap()
836                ))
837                .output()
838                .expect("failed to compile")
839        } else {
840            process::Command::new("sh")
841                .arg("-c")
842                .arg(format!(
843                    "g++ {} {} -Isrc/cpp -Wall -std=c++17 -o {}",
844                    source_path.to_str().unwrap(),
845                    test_path.to_str().unwrap(),
846                    exe_path.to_str().unwrap()
847                ))
848                .output()
849                .expect("failed to compile")
850        };
851
852        // Make sure it comiled without any warnings
853        assert_eq!(compile_output.status.code().expect("Unable to compile"), 0);
854        assert_eq!(compile_output.stdout.len(), 0);
855        assert_eq!(compile_output.stderr.len(), 0);
856
857        // Run the test executable
858        let test_output = if cfg!(target_os = "windows") {
859            process::Command::new("cmd")
860                .arg("/C")
861                .arg(format!(
862                    ".\\{}.exe",
863                    exe_path.to_str().unwrap().replace('/', "\\")
864                ))
865                .output()
866                .expect("failed to test")
867        } else {
868            process::Command::new("sh")
869                .arg("-c")
870                .arg(format!("./{}", exe_path.to_str().unwrap()))
871                .output()
872                .expect("failed to test")
873        };
874
875        assert_eq!(test_output.status.code().expect("Unable to run test"), 0);
876    }
877}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882    use crate::cpp::test_utils::*;
883    use std::process;
884
885    #[test]
886    fn termite_basis() {
887        if cfg!(target_os = "windows") {
888            process::Command::new("cmd")
889                .current_dir("tests/cpp/termite")
890                .arg("/C")
891                .arg("mkdir build")
892                .output()
893                .expect("failed to compile");
894        } else {
895            process::Command::new("sh")
896                .current_dir("tests/cpp/termite")
897                .arg("-c")
898                .arg("mkdir build")
899                .output()
900                .expect("failed to compile");
901        };
902
903        let compile_output = if cfg!(target_os = "windows") {
904            process::Command::new("cmd")
905                .current_dir("tests/cpp/termite/build")
906                .arg("/C")
907                .arg("cmake ..")
908                .output()
909                .expect("failed to compile")
910        } else {
911            process::Command::new("sh")
912                .current_dir("tests/cpp/termite/build")
913                .arg("-c")
914                .arg("cmake ..")
915                .output()
916                .expect("failed to compile")
917        };
918
919        assert_eq!(compile_output.status.code().expect("Unable to compile"), 0);
920        assert_eq!(compile_output.stderr.len(), 0);
921
922        let compile_output2 = if cfg!(target_os = "windows") {
923            process::Command::new("cmd")
924                .current_dir("tests/cpp/termite/build")
925                .arg("/C")
926                .arg("cmake --build .")
927                .output()
928                .expect("failed to compile")
929        } else {
930            process::Command::new("sh")
931                .current_dir("tests/cpp/termite/build")
932                .arg("-c")
933                .arg("cmake --build .")
934                .output()
935                .expect("failed to compile")
936        };
937
938        assert_eq!(compile_output2.status.code().expect("Unable to compile"), 0);
939        assert_eq!(compile_output2.stderr.len(), 0);
940
941        let test_output = if cfg!(target_os = "windows") {
942            process::Command::new("cmd")
943                .current_dir("tests/cpp/termite/build")
944                .arg("/C")
945                .arg(".\\Debug\\termite.exe")
946                .output()
947                .expect("failed to test")
948        } else {
949            process::Command::new("sh")
950                .current_dir("tests/cpp/termite/build")
951                .arg("-c")
952                .arg("./termite")
953                .output()
954                .expect("failed to test")
955        };
956
957        assert_eq!(test_output.status.code().expect("Unable to run"), 0);
958
959        let test_output_yaml = if cfg!(target_os = "windows") {
960            process::Command::new("cmd")
961                .current_dir("tests/cpp/termite/build")
962                .arg("/C")
963                .arg(".\\Debug\\termite-yaml.exe")
964                .output()
965                .expect("failed to test")
966        } else {
967            process::Command::new("sh")
968                .current_dir("tests/cpp/termite/build")
969                .arg("-c")
970                .arg("./termite-yaml")
971                .output()
972                .expect("failed to test")
973        };
974
975        assert_eq!(test_output_yaml.status.code().expect("Unable to run"), 0);
976
977        let test_output_json = if cfg!(target_os = "windows") {
978            process::Command::new("cmd")
979                .current_dir("tests/cpp/termite/build")
980                .arg("/C")
981                .arg(".\\Debug\\termite-json.exe")
982                .output()
983                .expect("failed to test")
984        } else {
985            process::Command::new("sh")
986                .current_dir("tests/cpp/termite/build")
987                .arg("-c")
988                .arg("./termite-json")
989                .output()
990                .expect("failed to test")
991        };
992
993        assert_eq!(test_output_json.status.code().expect("Unable to run"), 0);
994    }
995
996    #[test]
997    fn header() {
998        // Check c++ code
999        compile_and_test("header");
1000
1001        // Make sure it generates the correct code
1002        let data_model = DataModel {
1003            headers: Headers {
1004                header: "// header header".to_string(),
1005                source: "// header source".to_string(),
1006            },
1007            footers: Footers {
1008                header: "".to_string(),
1009                source: "".to_string(),
1010            },
1011            data_types: vec![],
1012            namespace: vec![],
1013            macros: HashMap::new(),
1014        };
1015
1016        // Create the header file
1017        let header_file = data_model.get_header("HEADER", 2).unwrap();
1018        let source_file = data_model.get_source("header", 2).unwrap();
1019        let expected_header = include_str!("../../tests/cpp/header/header.h");
1020        let expected_source = include_str!("../../tests/cpp/header/header.cpp");
1021
1022        // Check that they are the same
1023        assert_eq!(str_diff(&header_file, &expected_header), None);
1024        assert_eq!(str_diff(&source_file, &expected_source), None);
1025    }
1026
1027    #[test]
1028    fn footer() {
1029        // Check c++ code
1030        compile_and_test("footer");
1031
1032        // Make sure it generates the correct code
1033        let data_model = DataModel {
1034            headers: Headers {
1035                header: "".to_string(),
1036                source: "".to_string(),
1037            },
1038            footers: Footers {
1039                header: "// footer header".to_string(),
1040                source: "// footer source".to_string(),
1041            },
1042            data_types: vec![],
1043            namespace: vec![],
1044            macros: HashMap::new(),
1045        };
1046
1047        // Create the header file
1048        let header_file = data_model.get_header("HEADER", 2).unwrap();
1049        let source_file = data_model.get_source("footer", 2).unwrap();
1050        let expected_header = include_str!("../../tests/cpp/footer/footer.h");
1051        let expected_source = include_str!("../../tests/cpp/footer/footer.cpp");
1052
1053        // Check that they are the same
1054        assert_eq!(str_diff(&header_file, &expected_header), None);
1055        assert_eq!(str_diff(&source_file, &expected_source), None);
1056    }
1057
1058    #[test]
1059    fn namespace() {
1060        // Check c++ code
1061        compile_and_test("namespace");
1062
1063        // Make sure it generates the correct code
1064        let data_model = DataModel {
1065            headers: Headers {
1066                header: "".to_string(),
1067                source: "".to_string(),
1068            },
1069            footers: Footers {
1070                header: "".to_string(),
1071                source: "".to_string(),
1072            },
1073            data_types: vec![],
1074            namespace: vec!["test1".to_string(), "test2".to_string()],
1075            macros: HashMap::new(),
1076        };
1077
1078        // Create the header file
1079        let header_file = data_model.get_header("HEADER", 2).unwrap();
1080        let source_file = data_model.get_source("namespace", 2).unwrap();
1081        let expected_header = include_str!("../../tests/cpp/namespace/namespace.h");
1082        let expected_source = include_str!("../../tests/cpp/namespace/namespace.cpp");
1083
1084        // Check that they are the same
1085        assert_eq!(str_diff(&header_file, &expected_header), None);
1086        assert_eq!(str_diff(&source_file, &expected_source), None);
1087    }
1088
1089    #[test]
1090    fn outline() {
1091        // Check c++ code
1092        compile_and_test("outline");
1093
1094        // Make sure it generates the correct code
1095        let data_model = DataModel {
1096            headers: Headers {
1097                header: "// Header header".to_string(),
1098                source: "// Header source".to_string(),
1099            },
1100            footers: Footers {
1101                header: "// Footer header".to_string(),
1102                source: "// Footer source".to_string(),
1103            },
1104            data_types: vec![
1105                DataType {
1106                    name: "DataType1".to_string(),
1107                    description: Some("description1".to_string()),
1108                    data: DataTypeData::Struct(Struct { fields: vec![] }),
1109                },
1110                DataType {
1111                    name: "DataType2".to_string(),
1112                    description: Some("description2".to_string()),
1113                    data: DataTypeData::Struct(Struct { fields: vec![] }),
1114                },
1115            ],
1116            namespace: vec!["test".to_string()],
1117            macros: HashMap::new(),
1118        };
1119
1120        // Create the header file
1121        let header_file = data_model.get_header("HEADER", 2).unwrap();
1122        let source_file = data_model.get_source("outline", 2).unwrap();
1123        let expected_header = include_str!("../../tests/cpp/outline/outline.h");
1124        let expected_source = include_str!("../../tests/cpp/outline/outline.cpp");
1125
1126        // Check that they are the same
1127        assert_eq!(str_diff(&header_file, &expected_header), None);
1128        assert_eq!(str_diff(&source_file, &expected_source), None);
1129    }
1130
1131    #[test]
1132    fn full_example() {
1133        // Check c++ code
1134        if cfg!(target_os = "windows") {
1135            process::Command::new("cmd")
1136                .current_dir("tests/cpp/full_example")
1137                .arg("/C")
1138                .arg("mkdir build")
1139                .output()
1140                .expect("failed to compile");
1141        } else {
1142            process::Command::new("sh")
1143                .current_dir("tests/cpp/full_example")
1144                .arg("-c")
1145                .arg("mkdir build")
1146                .output()
1147                .expect("failed to compile");
1148        };
1149
1150        let compile_output = if cfg!(target_os = "windows") {
1151            process::Command::new("cmd")
1152                .current_dir("tests/cpp/full_example/build")
1153                .arg("/C")
1154                .arg("cmake ..")
1155                .output()
1156                .expect("failed to compile")
1157        } else {
1158            process::Command::new("sh")
1159                .current_dir("tests/cpp/full_example/build")
1160                .arg("-c")
1161                .arg("cmake ..")
1162                .output()
1163                .expect("failed to compile")
1164        };
1165
1166        assert_eq!(compile_output.status.code().expect("Unable to compile"), 0);
1167        assert_eq!(compile_output.stderr.len(), 0);
1168
1169        let compile_output2 = if cfg!(target_os = "windows") {
1170            process::Command::new("cmd")
1171                .current_dir("tests/cpp/full_example/build")
1172                .arg("/C")
1173                .arg("cmake --build .")
1174                .output()
1175                .expect("failed to compile")
1176        } else {
1177            process::Command::new("sh")
1178                .current_dir("tests/cpp/full_example/build")
1179                .arg("-c")
1180                .arg("cmake --build .")
1181                .output()
1182                .expect("failed to compile")
1183        };
1184
1185        assert_eq!(compile_output2.status.code().expect("Unable to compile"), 0);
1186        assert_eq!(compile_output2.stderr.len(), 0);
1187
1188        let test_output = if cfg!(target_os = "windows") {
1189            process::Command::new("cmd")
1190                .current_dir("tests/cpp/full_example/build")
1191                .arg("/C")
1192                .arg(".\\Debug\\full_example.exe")
1193                .output()
1194                .expect("failed to test")
1195        } else {
1196            process::Command::new("sh")
1197                .current_dir("tests/cpp/full_example/build")
1198                .arg("-c")
1199                .arg("./full_example")
1200                .output()
1201                .expect("failed to test")
1202        };
1203
1204        assert_eq!(test_output.status.code().expect("Unable to run"), 0);
1205
1206        // Make sure it generates the correct code
1207        let yaml_model = include_str!("../../tests/cpp/full_example/full_example_datamodel.yaml");
1208        let model = crate::DataModel::import_yaml(yaml_model).unwrap();
1209        let data_model = DataModel::new(model).unwrap();
1210
1211        // Create the header file
1212        let header_file = data_model.get_header("FULL_EXAMPLE", 2).unwrap();
1213        let source_file = data_model.get_source("full_example", 2).unwrap();
1214        let expected_header = include_str!("../../tests/cpp/full_example/full_example.h");
1215        let expected_source = include_str!("../../tests/cpp/full_example/full_example.cpp");
1216        //std::fs::write("tests/cpp/full_example/full_example.h", &header_file).unwrap();
1217        //std::fs::write("tests/cpp/full_example/full_example.cpp", &source_file).unwrap();
1218
1219        // Check that they are the same
1220        assert_eq!(str_diff(&header_file, &expected_header), None);
1221        assert_eq!(str_diff(&source_file, &expected_source), None);
1222    }
1223}