aconfig: Add first iteration of cpp codegen to aconfig
The general idea to reuse java codegen's very neat tiny template idea.
For generated cpp code, it is in the form of a collection of classes
inside a namespace. The reason we choose a collection of classes rather
than a collection of static functions is because gmock test technology
only supports mocking virtual method.
Bug: b/279483801
Test: atest aconfig.test
Change-Id: I9ba00667437ff7c3e147ff2828171fc95528bebf
diff --git a/tools/aconfig/src/codegen_cpp.rs b/tools/aconfig/src/codegen_cpp.rs
new file mode 100644
index 0000000..cb266f1
--- /dev/null
+++ b/tools/aconfig/src/codegen_cpp.rs
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use anyhow::Result;
+use serde::Serialize;
+use tinytemplate::TinyTemplate;
+
+use crate::aconfig::{FlagState, Permission};
+use crate::cache::{Cache, Item};
+use crate::commands::OutputFile;
+
+pub fn generate_cpp_code(cache: &Cache) -> Result<OutputFile> {
+ let class_elements: Vec<ClassElement> = cache.iter().map(create_class_element).collect();
+ let readwrite = class_elements.iter().any(|item| item.readwrite);
+ let namespace = cache.namespace().to_lowercase();
+ let context = Context { namespace: namespace.clone(), readwrite, class_elements };
+ let mut template = TinyTemplate::new();
+ template.add_template("cpp_code_gen", include_str!("../templates/cpp.template"))?;
+ let contents = template.render("cpp_code_gen", &context)?;
+ let path = ["aconfig", &(namespace + ".h")].iter().collect();
+ Ok(OutputFile { contents: contents.into(), path })
+}
+
+#[derive(Serialize)]
+struct Context {
+ pub namespace: String,
+ pub readwrite: bool,
+ pub class_elements: Vec<ClassElement>,
+}
+
+#[derive(Serialize)]
+struct ClassElement {
+ pub readwrite: bool,
+ pub default_value: String,
+ pub flag_name: String,
+}
+
+fn create_class_element(item: &Item) -> ClassElement {
+ ClassElement {
+ readwrite: item.permission == Permission::ReadWrite,
+ default_value: if item.state == FlagState::Enabled {
+ "true".to_string()
+ } else {
+ "false".to_string()
+ },
+ flag_name: item.name.clone(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::aconfig::{FlagDeclaration, FlagState, FlagValue, Permission};
+ use crate::commands::Source;
+
+ #[test]
+ fn test_cpp_codegen_build_time_flag_only() {
+ let namespace = "my_namespace";
+ let mut cache = Cache::new(namespace.to_string()).unwrap();
+ cache
+ .add_flag_declaration(
+ Source::File("aconfig_one.txt".to_string()),
+ FlagDeclaration {
+ name: "my_flag_one".to_string(),
+ description: "buildtime disable".to_string(),
+ },
+ )
+ .unwrap();
+ cache
+ .add_flag_value(
+ Source::Memory,
+ FlagValue {
+ namespace: namespace.to_string(),
+ name: "my_flag_one".to_string(),
+ state: FlagState::Disabled,
+ permission: Permission::ReadOnly,
+ },
+ )
+ .unwrap();
+ cache
+ .add_flag_declaration(
+ Source::File("aconfig_two.txt".to_string()),
+ FlagDeclaration {
+ name: "my_flag_two".to_string(),
+ description: "buildtime enable".to_string(),
+ },
+ )
+ .unwrap();
+ cache
+ .add_flag_value(
+ Source::Memory,
+ FlagValue {
+ namespace: namespace.to_string(),
+ name: "my_flag_two".to_string(),
+ state: FlagState::Enabled,
+ permission: Permission::ReadOnly,
+ },
+ )
+ .unwrap();
+ let expect_content = r#"#ifndef my_namespace_HEADER_H
+ #define my_namespace_HEADER_H
+ #include "my_namespace.h"
+
+ namespace my_namespace {
+
+ class my_flag_one {
+ public:
+ virtual const bool value() {
+ return false;
+ }
+ }
+
+ class my_flag_two {
+ public:
+ virtual const bool value() {
+ return true;
+ }
+ }
+
+ }
+ #endif
+ "#;
+ let file = generate_cpp_code(&cache).unwrap();
+ assert_eq!("aconfig/my_namespace.h", file.path.to_str().unwrap());
+ assert_eq!(
+ expect_content.replace(' ', ""),
+ String::from_utf8(file.contents).unwrap().replace(' ', "")
+ );
+ }
+
+ #[test]
+ fn test_cpp_codegen_runtime_flag() {
+ let namespace = "my_namespace";
+ let mut cache = Cache::new(namespace.to_string()).unwrap();
+ cache
+ .add_flag_declaration(
+ Source::File("aconfig_one.txt".to_string()),
+ FlagDeclaration {
+ name: "my_flag_one".to_string(),
+ description: "buildtime disable".to_string(),
+ },
+ )
+ .unwrap();
+ cache
+ .add_flag_declaration(
+ Source::File("aconfig_two.txt".to_string()),
+ FlagDeclaration {
+ name: "my_flag_two".to_string(),
+ description: "runtime enable".to_string(),
+ },
+ )
+ .unwrap();
+ cache
+ .add_flag_value(
+ Source::Memory,
+ FlagValue {
+ namespace: namespace.to_string(),
+ name: "my_flag_two".to_string(),
+ state: FlagState::Enabled,
+ permission: Permission::ReadWrite,
+ },
+ )
+ .unwrap();
+ let expect_content = r#"#ifndef my_namespace_HEADER_H
+ #define my_namespace_HEADER_H
+ #include "my_namespace.h"
+
+ #include <server_configurable_flags/get_flags.h>
+ using namespace server_configurable_flags;
+
+ namespace my_namespace {
+
+ class my_flag_one {
+ public:
+ virtual const bool value() {
+ return GetServerConfigurableFlag(
+ "my_namespace",
+ "my_flag_one",
+ "false") == "true";
+ }
+ }
+
+ class my_flag_two {
+ public:
+ virtual const bool value() {
+ return GetServerConfigurableFlag(
+ "my_namespace",
+ "my_flag_two",
+ "true") == "true";
+ }
+ }
+
+ }
+ #endif
+ "#;
+ let file = generate_cpp_code(&cache).unwrap();
+ assert_eq!("aconfig/my_namespace.h", file.path.to_str().unwrap());
+ assert_eq!(
+ expect_content.replace(' ', ""),
+ String::from_utf8(file.contents).unwrap().replace(' ', "")
+ );
+ }
+}