aconfig: first iteration of Rust codegen

Add a new `create-rust-lib` command to generate Rust code. The output is
a src/lib.rs file; the build system is assumed to set the generated
crate's name.

For READ_ONLY flags, the generated code returns a hard-coded true or false.

For READ_WRITE flags, the generated code reaches out to DeviceConfig via
the cc_library server_configurable_flags via the
libprofcollect_libflags_rust Rust bindings. The build system is assumed
to add this to the generated crate's dependencies.

Note: libprofcollect_libflags_rust seems generic enough that it should
be moved to an official Rust wrapper for server_configurable_flags. This
is tracked in b/284096062.

Summary of module the built system is assumed to wrap the auto-generated
code in:

  rust_library {
      name: "lib<namespace>_rs",
      crate_name: "<namespace>_rs",
      edition: "2021",
      clippy_lints: "none",
      no_stdlibs: true,
      lints: "none",
      srcs: ["src/lib.rs"],
      rustlibs: [
          "libprofcollect_libflags_rust",
      ],
  }

Also add a set of test input to be used in the unit tests for a more
coherent test strategy. A follow-up CL will migrate the code in
commands.rs, codegen_java.rs and codegen_cpp.rs.

Bug: 279483360
Bug: 283907905
Test: atest aconfig.test
Test: manual: create cache from files in testdata, create rust lib, add to module template above, verify the module builds
Change-Id: I02606aa3686eda921116e33f7e2df8fd1156a7aa
diff --git a/tools/aconfig/src/codegen_rust.rs b/tools/aconfig/src/codegen_rust.rs
new file mode 100644
index 0000000..d75e315
--- /dev/null
+++ b/tools/aconfig/src/codegen_rust.rs
@@ -0,0 +1,129 @@
+/*
+ * 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_rust_code(cache: &Cache) -> Result<OutputFile> {
+    let namespace = cache.namespace().to_lowercase();
+    let parsed_flags: Vec<TemplateParsedFlag> =
+        cache.iter().map(|item| create_template_parsed_flag(&namespace, item)).collect();
+    let context = TemplateContext { namespace, parsed_flags };
+    let mut template = TinyTemplate::new();
+    template.add_template("rust_code_gen", include_str!("../templates/rust.template"))?;
+    let contents = template.render("rust_code_gen", &context)?;
+    let path = ["src", "lib.rs"].iter().collect();
+    Ok(OutputFile { contents: contents.into(), path })
+}
+
+#[derive(Serialize)]
+struct TemplateContext {
+    pub namespace: String,
+    pub parsed_flags: Vec<TemplateParsedFlag>,
+}
+
+#[derive(Serialize)]
+struct TemplateParsedFlag {
+    pub name: String,
+    pub fn_name: String,
+
+    // TinyTemplate's conditionals are limited to single <bool> expressions; list all options here
+    // Invariant: exactly one of these fields will be true
+    pub is_read_only_enabled: bool,
+    pub is_read_only_disabled: bool,
+    pub is_read_write: bool,
+}
+
+#[allow(clippy::nonminimal_bool)]
+fn create_template_parsed_flag(namespace: &str, item: &Item) -> TemplateParsedFlag {
+    let template = TemplateParsedFlag {
+        name: item.name.clone(),
+        fn_name: format!("{}_{}", namespace, item.name.replace('-', "_").to_lowercase()),
+        is_read_only_enabled: item.permission == Permission::ReadOnly
+            && item.state == FlagState::Enabled,
+        is_read_only_disabled: item.permission == Permission::ReadOnly
+            && item.state == FlagState::Disabled,
+        is_read_write: item.permission == Permission::ReadWrite,
+    };
+    #[rustfmt::skip]
+    debug_assert!(
+        (template.is_read_only_enabled && !template.is_read_only_disabled && !template.is_read_write) ||
+        (!template.is_read_only_enabled && template.is_read_only_disabled && !template.is_read_write) ||
+        (!template.is_read_only_enabled && !template.is_read_only_disabled && template.is_read_write),
+        "TemplateParsedFlag invariant failed: {} {} {}",
+        template.is_read_only_enabled,
+        template.is_read_only_disabled,
+        template.is_read_write,
+    );
+    template
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::commands::{create_cache, Input, Source};
+
+    #[test]
+    fn test_generate_rust_code() {
+        let cache = create_cache(
+            "test",
+            vec![Input {
+                source: Source::File("testdata/test.aconfig".to_string()),
+                reader: Box::new(include_bytes!("../testdata/test.aconfig").as_slice()),
+            }],
+            vec![
+                Input {
+                    source: Source::File("testdata/first.values".to_string()),
+                    reader: Box::new(include_bytes!("../testdata/first.values").as_slice()),
+                },
+                Input {
+                    source: Source::File("testdata/test.aconfig".to_string()),
+                    reader: Box::new(include_bytes!("../testdata/second.values").as_slice()),
+                },
+            ],
+        )
+        .unwrap();
+        let generated = generate_rust_code(&cache).unwrap();
+        assert_eq!("src/lib.rs", format!("{}", generated.path.display()));
+        let expected = r#"
+#[inline(always)]
+pub const fn r#test_disabled_ro() -> bool {
+    false
+}
+
+#[inline(always)]
+pub fn r#test_disabled_rw() -> bool {
+    profcollect_libflags_rust::GetServerConfigurableFlag("test", "disabled-rw", "false") == "true"
+}
+
+#[inline(always)]
+pub const fn r#test_enabled_ro() -> bool {
+    true
+}
+
+#[inline(always)]
+pub fn r#test_enabled_rw() -> bool {
+    profcollect_libflags_rust::GetServerConfigurableFlag("test", "enabled-rw", "false") == "true"
+}
+"#;
+        assert_eq!(expected.trim(), String::from_utf8(generated.contents).unwrap().trim());
+    }
+}
diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs
index c854b62..cce1d7f 100644
--- a/tools/aconfig/src/commands.rs
+++ b/tools/aconfig/src/commands.rs
@@ -26,6 +26,7 @@
 use crate::cache::{Cache, CacheBuilder};
 use crate::codegen_cpp::generate_cpp_code;
 use crate::codegen_java::generate_java_code;
+use crate::codegen_rust::generate_rust_code;
 use crate::protos::ProtoParsedFlags;
 
 #[derive(Serialize, Deserialize, Clone, Debug)]
@@ -100,6 +101,10 @@
     generate_cpp_code(cache)
 }
 
+pub fn create_rust_lib(cache: &Cache) -> Result<OutputFile> {
+    generate_rust_code(cache)
+}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
 pub enum DumpFormat {
     Text,
diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs
index d02307d..b60909b 100644
--- a/tools/aconfig/src/main.rs
+++ b/tools/aconfig/src/main.rs
@@ -28,6 +28,7 @@
 mod cache;
 mod codegen_cpp;
 mod codegen_java;
+mod codegen_rust;
 mod commands;
 mod protos;
 
@@ -55,6 +56,11 @@
                 .arg(Arg::new("out").long("out").required(true)),
         )
         .subcommand(
+            Command::new("create-rust-lib")
+                .arg(Arg::new("cache").long("cache").required(true))
+                .arg(Arg::new("out").long("out").required(true)),
+        )
+        .subcommand(
             Command::new("dump")
                 .arg(Arg::new("cache").long("cache").action(ArgAction::Append).required(true))
                 .arg(
@@ -129,6 +135,14 @@
             let generated_file = commands::create_cpp_lib(&cache)?;
             write_output_file_realtive_to_dir(&dir, &generated_file)?;
         }
+        Some(("create-rust-lib", sub_matches)) => {
+            let path = get_required_arg::<String>(sub_matches, "cache")?;
+            let file = fs::File::open(path)?;
+            let cache = Cache::read_from_reader(file)?;
+            let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
+            let generated_file = commands::create_rust_lib(&cache)?;
+            write_output_file_realtive_to_dir(&dir, &generated_file)?;
+        }
         Some(("dump", sub_matches)) => {
             let mut caches = Vec::new();
             for path in sub_matches.get_many::<String>("cache").unwrap_or_default() {