1use 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
32struct ToSnakeCase<'a> {
35 chars: &'a mut dyn Iterator<Item = char>,
37 set_lower: Option<ToLowercase>,
39}
40
41impl<'a> ToSnakeCase<'a> {
42 fn new(chars: &'a mut dyn Iterator<Item = char>) -> Self {
48 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 if let Some(set_lower) = &mut self.set_lower {
65 if let Some(next_char) = set_lower.next() {
67 return Some(next_char);
68 }
69
70 self.set_lower = None;
72 }
73
74 return if let Some(next_char) = self.chars.next() {
76 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
89pub fn get_termite_dependency() -> &'static str {
92 return include_str!("termite.hpp");
93}
94
95pub 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
103pub 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#[derive(Clone, Debug, PartialEq)]
113pub struct DataModel {
114 data_types: Vec<DataType>,
116 headers: Headers,
118 footers: Footers,
120 namespace: Vec<String>,
122 macros: HashMap<String, data_model::SerializationModel>,
124}
125
126impl DataModel {
127 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 pub fn get_header(&self, name: &str, indent: usize) -> Result<String, Error> {
170 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 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 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 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 pub fn get_source(&self, name: &str, indent: usize) -> Result<String, Error> {
269 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 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 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 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#[derive(Clone, Debug, PartialEq)]
386struct Headers {
387 header: String,
389 source: String,
391}
392
393impl Headers {
394 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#[derive(Clone, Debug, PartialEq)]
415struct Footers {
416 header: String,
418 source: String,
420}
421
422impl Footers {
423 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#[derive(Clone, Debug, PartialEq)]
444struct DataType {
445 name: String,
447 description: Option<String>,
449 data: DataTypeData,
451}
452
453impl DataType {
454 fn new(data: crate::DataType) -> Result<Self, Error> {
460 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 fn get_description(&self) -> String {
475 return match &self.description {
476 Some(description) => description.clone(),
477 None => "".to_string(),
478 };
479 }
480
481 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 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 pub(super) fn get_parser_header(&self, namespace: &[String]) -> String {
526 return self.data.get_parser_header(&self.name, namespace);
527 }
528
529 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#[derive(Clone, Debug, PartialEq)]
552enum DataTypeData {
553 Struct(Struct),
555 Array(Array),
557 Variant(Variant),
559 Enum(Enum),
561 ConstrainedType(ConstrainedType),
563}
564
565impl DataTypeData {
566 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 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 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 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 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#[derive(Debug, Clone)]
682pub struct Error {
683 pub location: String,
685 pub error: ErrorCore,
687}
688
689impl Error {
690 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 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#[derive(thiserror::Error, Debug, Clone)]
746pub enum ErrorCore {
747 #[error("An error occured when expanding macros: {:?}", .0)]
749 MacroError(data_model::Error),
750 #[error("The header \"{:?}\" must only expand to a string when using macros", .0)]
752 HeaderMacro(String),
753 #[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 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 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 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 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 fs::create_dir_all(exe_path.parent().unwrap()).unwrap();
826
827 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 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 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 compile_and_test("header");
1000
1001 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 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 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 compile_and_test("footer");
1031
1032 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 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 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 compile_and_test("namespace");
1062
1063 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 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 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 compile_and_test("outline");
1093
1094 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 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 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 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 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 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 assert_eq!(str_diff(&header_file, &expected_header), None);
1221 assert_eq!(str_diff(&source_file, &expected_source), None);
1222 }
1223}