aconfig: introduce cache

Introduce the Cache struct to represent parsed and verified aconfig and
override content. Most commands in aconfig will work of an existing
cache file, eliminating the need to re-read the input every time.

Restructure main.rs to use clap to create a proper command line
interface with support for sub-commands. main.rs is responsible for
parsing the command line, performing disk I/O and calling the correct
subcommand implementation (in commands.rs).

To simplify unit tests, subcommands never perform explicit I/O; instead
they only work with Read and Write traits.

Also add dependencies on clap, serde and serde_json.

Bug: 279485059
Test: atest aconfig.test
Change-Id: Ib6abf2eabd264009804f253874b6fba924fc391b
diff --git a/tools/aconfig/Android.bp b/tools/aconfig/Android.bp
index e708b8a..e762f33 100644
--- a/tools/aconfig/Android.bp
+++ b/tools/aconfig/Android.bp
@@ -19,7 +19,10 @@
     rustlibs: [
         "libaconfig_protos",
         "libanyhow",
+        "libclap",
         "libprotobuf",
+        "libserde",
+        "libserde_json",
     ],
 }
 
diff --git a/tools/aconfig/Cargo.toml b/tools/aconfig/Cargo.toml
index 10cd26b..b439858 100644
--- a/tools/aconfig/Cargo.toml
+++ b/tools/aconfig/Cargo.toml
@@ -10,7 +10,10 @@
 
 [dependencies]
 anyhow = "1.0.69"
+clap = { version = "4.1.8", features = ["derive"] }
 protobuf = "3.2.0"
+serde = { version = "1.0.152", features = ["derive"] }
+serde_json = "1.0.93"
 
 [build-dependencies]
 protobuf-codegen = "3.2.0"
diff --git a/tools/aconfig/src/aconfig.rs b/tools/aconfig/src/aconfig.rs
index a6e1d1b..41815b7 100644
--- a/tools/aconfig/src/aconfig.rs
+++ b/tools/aconfig/src/aconfig.rs
@@ -26,6 +26,7 @@
 }
 
 impl Flag {
+    #[allow(dead_code)] // only used in unit tests
     pub fn try_from_text_proto(text_proto: &str) -> Result<Flag> {
         let proto: ProtoFlag = crate::protos::try_from_text_proto(text_proto)
             .with_context(|| text_proto.to_owned())?;
@@ -63,6 +64,7 @@
 }
 
 impl Override {
+    #[allow(dead_code)] // only used in unit tests
     pub fn try_from_text_proto(text_proto: &str) -> Result<Override> {
         let proto: ProtoOverride = crate::protos::try_from_text_proto(text_proto)?;
         proto.try_into()
diff --git a/tools/aconfig/src/cache.rs b/tools/aconfig/src/cache.rs
new file mode 100644
index 0000000..d60cc20
--- /dev/null
+++ b/tools/aconfig/src/cache.rs
@@ -0,0 +1,146 @@
+/*
+ * 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::{anyhow, Result};
+use serde::{Deserialize, Serialize};
+use std::io::{Read, Write};
+
+use crate::aconfig::{Flag, Override};
+use crate::commands::Source;
+
+#[derive(Serialize, Deserialize)]
+pub struct Value {
+    pub value: bool,
+    pub source: Source,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Item {
+    pub id: String,
+    pub description: String,
+    pub values: Vec<Value>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Cache {
+    items: Vec<Item>,
+}
+
+impl Cache {
+    pub fn new() -> Cache {
+        Cache { items: vec![] }
+    }
+
+    pub fn read_from_reader(reader: impl Read) -> Result<Cache> {
+        serde_json::from_reader(reader).map_err(|e| e.into())
+    }
+
+    pub fn write_to_writer(&self, writer: impl Write) -> Result<()> {
+        serde_json::to_writer(writer, self).map_err(|e| e.into())
+    }
+
+    pub fn add_flag(&mut self, source: Source, flag: Flag) -> Result<()> {
+        if let Some(existing_item) = self.items.iter().find(|&item| item.id == flag.id) {
+            return Err(anyhow!(
+                "failed to add flag {} from {}: already added from {}",
+                flag.id,
+                source,
+                existing_item.values.first().unwrap().source
+            ));
+        }
+        self.items.push(Item {
+            id: flag.id.clone(),
+            description: flag.description.clone(),
+            values: vec![Value { value: flag.value, source }],
+        });
+        Ok(())
+    }
+
+    pub fn add_override(&mut self, source: Source, override_: Override) -> Result<()> {
+        let Some(existing_item) = self.items.iter_mut().find(|item| item.id == override_.id) else {
+            return Err(anyhow!("failed to override flag {}: unknown flag", override_.id));
+        };
+        existing_item.values.push(Value { value: override_.value, source });
+        Ok(())
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = &Item> {
+        self.items.iter()
+    }
+}
+
+impl Item {
+    pub fn value(&self) -> bool {
+        self.values.last().unwrap().value
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_add_flag() {
+        let mut cache = Cache::new();
+        cache
+            .add_flag(
+                Source::File("first.txt".to_string()),
+                Flag { id: "foo".to_string(), description: "desc".to_string(), value: true },
+            )
+            .unwrap();
+        let error = cache
+            .add_flag(
+                Source::File("second.txt".to_string()),
+                Flag { id: "foo".to_string(), description: "desc".to_string(), value: false },
+            )
+            .unwrap_err();
+        assert_eq!(
+            &format!("{:?}", error),
+            "failed to add flag foo from second.txt: already added from first.txt"
+        );
+    }
+
+    #[test]
+    fn test_add_override() {
+        fn get_value(cache: &Cache, id: &str) -> bool {
+            cache.iter().find(|&item| item.id == id).unwrap().value()
+        }
+
+        let mut cache = Cache::new();
+        let error = cache
+            .add_override(Source::Memory, Override { id: "foo".to_string(), value: false })
+            .unwrap_err();
+        assert_eq!(&format!("{:?}", error), "failed to override flag foo: unknown flag");
+
+        cache
+            .add_flag(
+                Source::File("first.txt".to_string()),
+                Flag { id: "foo".to_string(), description: "desc".to_string(), value: true },
+            )
+            .unwrap();
+        assert!(get_value(&cache, "foo"));
+
+        cache
+            .add_override(Source::Memory, Override { id: "foo".to_string(), value: false })
+            .unwrap();
+        assert!(!get_value(&cache, "foo"));
+
+        cache
+            .add_override(Source::Memory, Override { id: "foo".to_string(), value: true })
+            .unwrap();
+        assert!(get_value(&cache, "foo"));
+    }
+}
diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs
new file mode 100644
index 0000000..0e377aa
--- /dev/null
+++ b/tools/aconfig/src/commands.rs
@@ -0,0 +1,123 @@
+/*
+ * 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::{Context, Result};
+use clap::ValueEnum;
+use serde::{Deserialize, Serialize};
+use std::fmt;
+use std::io::Read;
+
+use crate::aconfig::{Flag, Override};
+use crate::cache::Cache;
+
+#[derive(Clone, Serialize, Deserialize)]
+pub enum Source {
+    #[allow(dead_code)] // only used in unit tests
+    Memory,
+    File(String),
+}
+
+impl fmt::Display for Source {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Memory => write!(f, "<memory>"),
+            Self::File(path) => write!(f, "{}", path),
+        }
+    }
+}
+
+pub struct Input {
+    pub source: Source,
+    pub reader: Box<dyn Read>,
+}
+
+pub fn create_cache(aconfigs: Vec<Input>, overrides: Vec<Input>) -> Result<Cache> {
+    let mut cache = Cache::new();
+
+    for mut input in aconfigs {
+        let mut contents = String::new();
+        input.reader.read_to_string(&mut contents)?;
+        let flags = Flag::try_from_text_proto_list(&contents)
+            .with_context(|| format!("Failed to parse {}", input.source))?;
+        for flag in flags {
+            cache.add_flag(input.source.clone(), flag)?;
+        }
+    }
+
+    for mut input in overrides {
+        let mut contents = String::new();
+        input.reader.read_to_string(&mut contents)?;
+        let overrides = Override::try_from_text_proto_list(&contents)
+            .with_context(|| format!("Failed to parse {}", input.source))?;
+        for override_ in overrides {
+            cache.add_override(input.source.clone(), override_)?;
+        }
+    }
+
+    Ok(cache)
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
+pub enum Format {
+    Text,
+    Debug,
+}
+
+pub fn dump_cache(cache: Cache, format: Format) -> Result<()> {
+    match format {
+        Format::Text => {
+            for item in cache.iter() {
+                println!("{}: {}", item.id, item.value());
+            }
+        }
+        Format::Debug => {
+            for item in cache.iter() {
+                println!("{}: {}", item.id, item.value());
+                for value in &item.values {
+                    println!("    {}: {}", value.source, value.value);
+                }
+            }
+        }
+    }
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_create_cache() {
+        let s = r#"
+        flag {
+            id: "a"
+            description: "Description of a"
+            value: true
+        }
+        "#;
+        let aconfigs = vec![Input { source: Source::Memory, reader: Box::new(s.as_bytes()) }];
+        let o = r#"
+        override {
+            id: "a"
+            value: false
+        }
+        "#;
+        let overrides = vec![Input { source: Source::Memory, reader: Box::new(o.as_bytes()) }];
+        let cache = create_cache(aconfigs, overrides).unwrap();
+        let value = cache.iter().find(|&item| item.id == "a").unwrap().value();
+        assert!(!value);
+    }
+}
diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs
index 9d87ca4..79ac920 100644
--- a/tools/aconfig/src/main.rs
+++ b/tools/aconfig/src/main.rs
@@ -17,22 +17,67 @@
 //! `aconfig` is a build time tool to manage build time configurations, such as feature flags.
 
 use anyhow::Result;
+use clap::{builder::ArgAction, builder::EnumValueParser, Arg, Command};
+use std::fs;
 
 mod aconfig;
+mod cache;
+mod commands;
 mod protos;
 
-use aconfig::{Flag, Override};
+use crate::cache::Cache;
+use commands::{Input, Source};
+
+fn cli() -> Command {
+    Command::new("aconfig")
+        .subcommand_required(true)
+        .subcommand(
+            Command::new("create-cache")
+                .arg(Arg::new("aconfig").long("aconfig").action(ArgAction::Append))
+                .arg(Arg::new("override").long("override").action(ArgAction::Append))
+                .arg(Arg::new("cache").long("cache").required(true)),
+        )
+        .subcommand(
+            Command::new("dump").arg(Arg::new("cache").long("cache").required(true)).arg(
+                Arg::new("format")
+                    .long("format")
+                    .value_parser(EnumValueParser::<commands::Format>::new())
+                    .default_value("text"),
+            ),
+        )
+}
 
 fn main() -> Result<()> {
-    let flag = Flag::try_from_text_proto(r#"id: "a" description: "description of a" value: true"#)?;
-    println!("{:?}", flag);
-    let flags = Flag::try_from_text_proto_list(
-        r#"flag { id: "a" description: "description of a" value: true }"#,
-    )?;
-    println!("{:?}", flags);
-    let override_ = Override::try_from_text_proto(r#"id: "foo" value: true"#)?;
-    println!("{:?}", override_);
-    let overrides = Override::try_from_text_proto_list(r#"override { id: "foo" value: true }"#)?;
-    println!("{:?}", overrides);
+    let matches = cli().get_matches();
+    match matches.subcommand() {
+        Some(("create-cache", sub_matches)) => {
+            let mut aconfigs = vec![];
+            for path in
+                sub_matches.get_many::<String>("aconfig").unwrap_or_default().collect::<Vec<_>>()
+            {
+                let file = Box::new(fs::File::open(path)?);
+                aconfigs.push(Input { source: Source::File(path.to_string()), reader: file });
+            }
+            let mut overrides = vec![];
+            for path in
+                sub_matches.get_many::<String>("override").unwrap_or_default().collect::<Vec<_>>()
+            {
+                let file = Box::new(fs::File::open(path)?);
+                overrides.push(Input { source: Source::File(path.to_string()), reader: file });
+            }
+            let cache = commands::create_cache(aconfigs, overrides)?;
+            let path = sub_matches.get_one::<String>("cache").unwrap();
+            let file = fs::File::create(path)?;
+            cache.write_to_writer(file)?;
+        }
+        Some(("dump", sub_matches)) => {
+            let path = sub_matches.get_one::<String>("cache").unwrap();
+            let file = fs::File::open(path)?;
+            let cache = Cache::read_from_reader(file)?;
+            let format = sub_matches.get_one("format").unwrap();
+            commands::dump_cache(cache, *format)?;
+        }
+        _ => unreachable!(),
+    }
     Ok(())
 }