diff --git a/tools/aconfig/Android.bp b/tools/aconfig/Android.bp index e762f331f6..9617e0eb48 100644 --- a/tools/aconfig/Android.bp +++ b/tools/aconfig/Android.bp @@ -23,6 +23,7 @@ rust_defaults { "libprotobuf", "libserde", "libserde_json", + "libtinytemplate", ], } diff --git a/tools/aconfig/Cargo.toml b/tools/aconfig/Cargo.toml index b439858c9a..8517dd2443 100644 --- a/tools/aconfig/Cargo.toml +++ b/tools/aconfig/Cargo.toml @@ -14,6 +14,7 @@ clap = { version = "4.1.8", features = ["derive"] } protobuf = "3.2.0" serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" +tinytemplate = "1.2.1" [build-dependencies] protobuf-codegen = "3.2.0" diff --git a/tools/aconfig/src/codegen_java.rs b/tools/aconfig/src/codegen_java.rs new file mode 100644 index 0000000000..9d52ccede8 --- /dev/null +++ b/tools/aconfig/src/codegen_java.rs @@ -0,0 +1,142 @@ +/* + * 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}; + +pub struct GeneratedFile { + pub file_content: String, + pub file_name: String, +} + +pub fn generate_java_code(cache: &Cache) -> Result { + let class_elements: Vec = cache.iter().map(create_class_element).collect(); + let readwrite = class_elements.iter().any(|item| item.readwrite); + let namespace = uppercase_first_letter( + cache.iter().find(|item| !item.namespace.is_empty()).unwrap().namespace.as_str(), + ); + let context = Context { namespace: namespace.clone(), readwrite, class_elements }; + let mut template = TinyTemplate::new(); + template.add_template("java_code_gen", include_str!("../templates/java.template"))?; + let file_content = template.render("java_code_gen", &context)?; + Ok(GeneratedFile { file_content, file_name: format!("{}.java", namespace) }) +} + +#[derive(Serialize)] +struct Context { + pub namespace: String, + pub readwrite: bool, + pub class_elements: Vec, +} + +#[derive(Serialize)] +struct ClassElement { + pub method_name: String, + pub readwrite: bool, + pub default_value: String, + pub feature_name: String, + pub flag_name: String, +} + +fn create_class_element(item: &Item) -> ClassElement { + ClassElement { + method_name: item.name.clone(), + readwrite: item.permission == Permission::ReadWrite, + default_value: if item.state == FlagState::Enabled { + "true".to_string() + } else { + "false".to_string() + }, + feature_name: item.name.clone(), + flag_name: item.name.clone(), + } +} + +fn uppercase_first_letter(s: &str) -> String { + s.chars() + .enumerate() + .map( + |(index, ch)| { + if index == 0 { + ch.to_ascii_uppercase() + } else { + ch.to_ascii_lowercase() + } + }, + ) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::aconfig::{Flag, Value}; + use crate::commands::Source; + + #[test] + fn test_generate_java_code() { + let namespace = "TeSTFlaG"; + let mut cache = Cache::new(1, namespace.to_string()); + cache + .add_flag( + Source::File("test.txt".to_string()), + Flag { + name: "test".to_string(), + description: "buildtime enable".to_string(), + values: vec![Value::default(FlagState::Enabled, Permission::ReadOnly)], + }, + ) + .unwrap(); + cache + .add_flag( + Source::File("test2.txt".to_string()), + Flag { + name: "test2".to_string(), + description: "runtime disable".to_string(), + values: vec![Value::default(FlagState::Disabled, Permission::ReadWrite)], + }, + ) + .unwrap(); + let expect_content = "package com.android.aconfig; + + import android.provider.DeviceConfig; + + public final class Testflag { + + public static boolean test() { + return true; + } + + public static boolean test2() { + return DeviceConfig.getBoolean( + \"Testflag\", + \"test2__test2\", + false + ); + } + + } + "; + let expected_file_name = format!("{}.java", uppercase_first_letter(namespace)); + let generated_file = generate_java_code(&cache).unwrap(); + assert_eq!(expected_file_name, generated_file.file_name); + assert_eq!(expect_content.replace(' ', ""), generated_file.file_content.replace(' ', "")); + } +} diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs index 2c80a4a898..1487e723a8 100644 --- a/tools/aconfig/src/commands.rs +++ b/tools/aconfig/src/commands.rs @@ -23,6 +23,7 @@ use std::io::Read; use crate::aconfig::{Namespace, Override}; use crate::cache::Cache; +use crate::codegen_java::{generate_java_code, GeneratedFile}; use crate::protos::ProtoParsedFlags; #[derive(Serialize, Deserialize, Clone, Debug)] @@ -84,6 +85,10 @@ pub fn create_cache( Ok(cache) } +pub fn generate_code(cache: &Cache) -> Result { + generate_java_code(cache) +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] pub enum Format { Text, diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs index f25373533a..f29186ae1b 100644 --- a/tools/aconfig/src/main.rs +++ b/tools/aconfig/src/main.rs @@ -24,6 +24,7 @@ use std::io::Write; mod aconfig; mod cache; +mod codegen_java; mod commands; mod protos; @@ -46,6 +47,11 @@ fn cli() -> Command { .arg(Arg::new("override").long("override").action(ArgAction::Append)) .arg(Arg::new("cache").long("cache").required(true)), ) + .subcommand( + Command::new("create-java-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").required(true)) @@ -81,6 +87,17 @@ fn main() -> Result<()> { let file = fs::File::create(path)?; cache.write_to_writer(file)?; } + Some(("create-java-lib", sub_matches)) => { + let path = sub_matches.get_one::("cache").unwrap(); + let file = fs::File::open(path)?; + let cache = Cache::read_from_reader(file)?; + let out = sub_matches.get_one::("out").unwrap(); + let generated_file = commands::generate_code(&cache).unwrap(); + fs::write( + format!("{}/{}", out, generated_file.file_name), + generated_file.file_content, + )?; + } Some(("dump", sub_matches)) => { let path = sub_matches.get_one::("cache").unwrap(); let file = fs::File::open(path)?; diff --git a/tools/aconfig/templates/java.template b/tools/aconfig/templates/java.template new file mode 100644 index 0000000000..38545797c6 --- /dev/null +++ b/tools/aconfig/templates/java.template @@ -0,0 +1,19 @@ +package com.android.aconfig; +{{ if readwrite }} +import android.provider.DeviceConfig; +{{ endif }} +public final class {namespace} \{ + {{ for item in class_elements}} + public static boolean {item.method_name}() \{ + {{ if item.readwrite- }} + return DeviceConfig.getBoolean( + "{namespace}", + "{item.feature_name}__{item.flag_name}", + {item.default_value} + ); + {{ -else- }} + return {item.default_value}; + {{ -endif }} + } + {{ endfor }} +}