aconfig: read from new storage in Rust codegen
Read storage values for read-only flags, and log failures or mismatches
with compiled-in defaults. Continue to return values fetched from the legacy storage.
Test: cargo t
Bug: 328444881
Ignore-AOSP-First: internal storage migration, this code will be deleted
Change-Id: I4651617270129794dec64e324b4aa7836cce7a43
diff --git a/tools/aconfig/aconfig/src/codegen/rust.rs b/tools/aconfig/aconfig/src/codegen/rust.rs
index 33c3d37..591781e 100644
--- a/tools/aconfig/aconfig/src/codegen/rust.rs
+++ b/tools/aconfig/aconfig/src/codegen/rust.rs
@@ -20,26 +20,32 @@
use aconfig_protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag};
+use std::collections::HashMap;
+
use crate::codegen;
use crate::codegen::CodegenMode;
use crate::commands::OutputFile;
pub fn generate_rust_code<I>(
package: &str,
+ flag_ids: HashMap<String, u16>,
parsed_flags_iter: I,
codegen_mode: CodegenMode,
+ allow_instrumentation: bool,
) -> Result<OutputFile>
where
I: Iterator<Item = ProtoParsedFlag>,
{
- let template_flags: Vec<TemplateParsedFlag> =
- parsed_flags_iter.map(|pf| TemplateParsedFlag::new(package, &pf)).collect();
+ let template_flags: Vec<TemplateParsedFlag> = parsed_flags_iter
+ .map(|pf| TemplateParsedFlag::new(package, flag_ids.clone(), &pf))
+ .collect();
let has_readwrite = template_flags.iter().any(|item| item.readwrite);
let context = TemplateContext {
package: package.to_string(),
template_flags,
modules: package.split('.').map(|s| s.to_string()).collect::<Vec<_>>(),
has_readwrite,
+ allow_instrumentation,
};
let mut template = TinyTemplate::new();
template.add_template(
@@ -62,6 +68,7 @@
pub template_flags: Vec<TemplateParsedFlag>,
pub modules: Vec<String>,
pub has_readwrite: bool,
+ pub allow_instrumentation: bool,
}
#[derive(Serialize)]
@@ -69,25 +76,28 @@
pub readwrite: bool,
pub default_value: String,
pub name: String,
+ pub container: String,
+ pub flag_offset: u16,
pub device_config_namespace: String,
pub device_config_flag: String,
}
impl TemplateParsedFlag {
#[allow(clippy::nonminimal_bool)]
- fn new(package: &str, pf: &ProtoParsedFlag) -> Self {
- let template = TemplateParsedFlag {
+ fn new(package: &str, flag_offsets: HashMap<String, u16>, pf: &ProtoParsedFlag) -> Self {
+ Self {
readwrite: pf.permission() == ProtoFlagPermission::READ_WRITE,
default_value: match pf.state() {
ProtoFlagState::ENABLED => "true".to_string(),
ProtoFlagState::DISABLED => "false".to_string(),
},
name: pf.name().to_string(),
+ container: pf.container().to_string(),
+ flag_offset: *flag_offsets.get(pf.name()).expect("didnt find package offset :("),
device_config_namespace: pf.namespace().to_string(),
device_config_flag: codegen::create_device_config_ident(package, pf.name())
.expect("values checked at flag parse time"),
- };
- template
+ }
}
}
@@ -97,6 +107,14 @@
const PROD_EXPECTED: &str = r#"
//! codegenerated rust flag lib
+use aconfig_storage_read_api::{StorageFileType, get_mapped_storage_file, get_boolean_flag_value, get_package_offset};
+use std::path::Path;
+use std::io::Write;
+use log::{info, error, LevelFilter};
+
+static STORAGE_MIGRATION_MARKER_FILE: &str =
+ "/metadata/aconfig/storage_test_mission_1";
+static MIGRATION_LOG_TAG: &str = "AconfigStorageTestMission1";
/// flag provider
pub struct FlagProvider;
@@ -492,6 +510,14 @@
const EXPORTED_EXPECTED: &str = r#"
//! codegenerated rust flag lib
+use aconfig_storage_read_api::{StorageFileType, get_mapped_storage_file, get_boolean_flag_value, get_package_offset};
+use std::path::Path;
+use std::io::Write;
+use log::{info, error, LevelFilter};
+
+static STORAGE_MIGRATION_MARKER_FILE: &str =
+ "/metadata/aconfig/storage_test_mission_1";
+static MIGRATION_LOG_TAG: &str = "AconfigStorageTestMission1";
/// flag provider
pub struct FlagProvider;
@@ -520,17 +546,20 @@
impl FlagProvider {
/// query flag disabled_rw_exported
pub fn disabled_rw_exported(&self) -> bool {
- *CACHED_disabled_rw_exported
+ let result = *CACHED_disabled_rw_exported;
+ result
}
/// query flag enabled_fixed_ro_exported
pub fn enabled_fixed_ro_exported(&self) -> bool {
- *CACHED_enabled_fixed_ro_exported
+ let result = *CACHED_enabled_fixed_ro_exported;
+ result
}
/// query flag enabled_ro_exported
pub fn enabled_ro_exported(&self) -> bool {
- *CACHED_enabled_ro_exported
+ let result = *CACHED_enabled_ro_exported;
+ result
}
}
@@ -558,6 +587,14 @@
const FORCE_READ_ONLY_EXPECTED: &str = r#"
//! codegenerated rust flag lib
+use aconfig_storage_read_api::{StorageFileType, get_mapped_storage_file, get_boolean_flag_value, get_package_offset};
+use std::path::Path;
+use std::io::Write;
+use log::{info, error, LevelFilter};
+
+static STORAGE_MIGRATION_MARKER_FILE: &str =
+ "/metadata/aconfig/storage_test_mission_1";
+static MIGRATION_LOG_TAG: &str = "AconfigStorageTestMission1";
/// flag provider
pub struct FlagProvider;
@@ -565,32 +602,344 @@
impl FlagProvider {
/// query flag disabled_ro
pub fn disabled_ro(&self) -> bool {
- false
+ let result = false;
+
+ if !Path::new(STORAGE_MIGRATION_MARKER_FILE).exists() {
+ return result;
+ }
+
+ // This will be called multiple times. Subsequent calls after the first
+ // are noops.
+ logger::init(
+ logger::Config::default()
+ .with_tag_on_device(MIGRATION_LOG_TAG)
+ .with_max_level(LevelFilter::Info),
+ );
+
+ unsafe {
+ let package_map = match get_mapped_storage_file("system", StorageFileType::PackageMap) {
+ Ok(file) => file,
+ Err(err) => {
+ error!("failed to read flag 'disabled_ro': {}", err);
+ return result;
+ }
+ };
+ let package_offset = match get_package_offset(&package_map, "com.android.aconfig.test") {
+ Ok(Some(offset)) => offset,
+ Ok(None) => {
+ error!("failed to read flag 'disabled_ro', not found in package map");
+ return result;
+ },
+ Err(err) => {
+ error!("failed to read flag 'disabled_ro': {}", err);
+ return result;
+ }
+ };
+ let flag_val_map = match get_mapped_storage_file("system", StorageFileType::FlagVal) {
+ Ok(val_map) => val_map,
+ Err(err) => {
+ error!("failed to read flag 'disabled_ro': {}", err);
+ return result;
+ }
+ };
+ let value = match get_boolean_flag_value(&flag_val_map, 0 + package_offset.boolean_offset) {
+ Ok(val) => val,
+ Err(err) => {
+ error!("failed to read flag 'disabled_ro': {}", err);
+ return result;
+ }
+ };
+ if false != value {
+ let default_value = false;
+ error!("flag mismatch for 'disabled_ro'. Legacy storage was {default_value}, new storage was {value}")
+ }
+ }
+ result
}
/// query flag disabled_rw
pub fn disabled_rw(&self) -> bool {
- false
+ let result = false;
+
+ if !Path::new(STORAGE_MIGRATION_MARKER_FILE).exists() {
+ return result;
+ }
+
+ // This will be called multiple times. Subsequent calls after the first
+ // are noops.
+ logger::init(
+ logger::Config::default()
+ .with_tag_on_device(MIGRATION_LOG_TAG)
+ .with_max_level(LevelFilter::Info),
+ );
+
+ unsafe {
+ let package_map = match get_mapped_storage_file("system", StorageFileType::PackageMap) {
+ Ok(file) => file,
+ Err(err) => {
+ error!("failed to read flag 'disabled_rw': {}", err);
+ return result;
+ }
+ };
+ let package_offset = match get_package_offset(&package_map, "com.android.aconfig.test") {
+ Ok(Some(offset)) => offset,
+ Ok(None) => {
+ error!("failed to read flag 'disabled_rw', not found in package map");
+ return result;
+ },
+ Err(err) => {
+ error!("failed to read flag 'disabled_rw': {}", err);
+ return result;
+ }
+ };
+ let flag_val_map = match get_mapped_storage_file("system", StorageFileType::FlagVal) {
+ Ok(val_map) => val_map,
+ Err(err) => {
+ error!("failed to read flag 'disabled_rw': {}", err);
+ return result;
+ }
+ };
+ let value = match get_boolean_flag_value(&flag_val_map, 1 + package_offset.boolean_offset) {
+ Ok(val) => val,
+ Err(err) => {
+ error!("failed to read flag 'disabled_rw': {}", err);
+ return result;
+ }
+ };
+ if false != value {
+ let default_value = false;
+ error!("flag mismatch for 'disabled_rw'. Legacy storage was {default_value}, new storage was {value}")
+ }
+ }
+ result
}
/// query flag disabled_rw_in_other_namespace
pub fn disabled_rw_in_other_namespace(&self) -> bool {
- false
+ let result = false;
+
+ if !Path::new(STORAGE_MIGRATION_MARKER_FILE).exists() {
+ return result;
+ }
+
+ // This will be called multiple times. Subsequent calls after the first
+ // are noops.
+ logger::init(
+ logger::Config::default()
+ .with_tag_on_device(MIGRATION_LOG_TAG)
+ .with_max_level(LevelFilter::Info),
+ );
+
+ unsafe {
+ let package_map = match get_mapped_storage_file("system", StorageFileType::PackageMap) {
+ Ok(file) => file,
+ Err(err) => {
+ error!("failed to read flag 'disabled_rw_in_other_namespace': {}", err);
+ return result;
+ }
+ };
+ let package_offset = match get_package_offset(&package_map, "com.android.aconfig.test") {
+ Ok(Some(offset)) => offset,
+ Ok(None) => {
+ error!("failed to read flag 'disabled_rw_in_other_namespace', not found in package map");
+ return result;
+ },
+ Err(err) => {
+ error!("failed to read flag 'disabled_rw_in_other_namespace': {}", err);
+ return result;
+ }
+ };
+ let flag_val_map = match get_mapped_storage_file("system", StorageFileType::FlagVal) {
+ Ok(val_map) => val_map,
+ Err(err) => {
+ error!("failed to read flag 'disabled_rw_in_other_namespace': {}", err);
+ return result;
+ }
+ };
+ let value = match get_boolean_flag_value(&flag_val_map, 2 + package_offset.boolean_offset) {
+ Ok(val) => val,
+ Err(err) => {
+ error!("failed to read flag 'disabled_rw_in_other_namespace': {}", err);
+ return result;
+ }
+ };
+ if false != value {
+ let default_value = false;
+ error!("flag mismatch for 'disabled_rw_in_other_namespace'. Legacy storage was {default_value}, new storage was {value}")
+ }
+ }
+ result
}
/// query flag enabled_fixed_ro
pub fn enabled_fixed_ro(&self) -> bool {
- true
+ let result = true;
+
+ if !Path::new(STORAGE_MIGRATION_MARKER_FILE).exists() {
+ return result;
+ }
+
+ // This will be called multiple times. Subsequent calls after the first
+ // are noops.
+ logger::init(
+ logger::Config::default()
+ .with_tag_on_device(MIGRATION_LOG_TAG)
+ .with_max_level(LevelFilter::Info),
+ );
+
+ unsafe {
+ let package_map = match get_mapped_storage_file("system", StorageFileType::PackageMap) {
+ Ok(file) => file,
+ Err(err) => {
+ error!("failed to read flag 'enabled_fixed_ro': {}", err);
+ return result;
+ }
+ };
+ let package_offset = match get_package_offset(&package_map, "com.android.aconfig.test") {
+ Ok(Some(offset)) => offset,
+ Ok(None) => {
+ error!("failed to read flag 'enabled_fixed_ro', not found in package map");
+ return result;
+ },
+ Err(err) => {
+ error!("failed to read flag 'enabled_fixed_ro': {}", err);
+ return result;
+ }
+ };
+ let flag_val_map = match get_mapped_storage_file("system", StorageFileType::FlagVal) {
+ Ok(val_map) => val_map,
+ Err(err) => {
+ error!("failed to read flag 'enabled_fixed_ro': {}", err);
+ return result;
+ }
+ };
+ let value = match get_boolean_flag_value(&flag_val_map, 3 + package_offset.boolean_offset) {
+ Ok(val) => val,
+ Err(err) => {
+ error!("failed to read flag 'enabled_fixed_ro': {}", err);
+ return result;
+ }
+ };
+ if true != value {
+ let default_value = true;
+ error!("flag mismatch for 'enabled_fixed_ro'. Legacy storage was {default_value}, new storage was {value}")
+ }
+ }
+ result
}
/// query flag enabled_ro
pub fn enabled_ro(&self) -> bool {
- true
+ let result = true;
+
+ if !Path::new(STORAGE_MIGRATION_MARKER_FILE).exists() {
+ return result;
+ }
+
+ // This will be called multiple times. Subsequent calls after the first
+ // are noops.
+ logger::init(
+ logger::Config::default()
+ .with_tag_on_device(MIGRATION_LOG_TAG)
+ .with_max_level(LevelFilter::Info),
+ );
+
+ unsafe {
+ let package_map = match get_mapped_storage_file("system", StorageFileType::PackageMap) {
+ Ok(file) => file,
+ Err(err) => {
+ error!("failed to read flag 'enabled_ro': {}", err);
+ return result;
+ }
+ };
+ let package_offset = match get_package_offset(&package_map, "com.android.aconfig.test") {
+ Ok(Some(offset)) => offset,
+ Ok(None) => {
+ error!("failed to read flag 'enabled_ro', not found in package map");
+ return result;
+ },
+ Err(err) => {
+ error!("failed to read flag 'enabled_ro': {}", err);
+ return result;
+ }
+ };
+ let flag_val_map = match get_mapped_storage_file("system", StorageFileType::FlagVal) {
+ Ok(val_map) => val_map,
+ Err(err) => {
+ error!("failed to read flag 'enabled_ro': {}", err);
+ return result;
+ }
+ };
+ let value = match get_boolean_flag_value(&flag_val_map, 4 + package_offset.boolean_offset) {
+ Ok(val) => val,
+ Err(err) => {
+ error!("failed to read flag 'enabled_ro': {}", err);
+ return result;
+ }
+ };
+ if true != value {
+ let default_value = true;
+ error!("flag mismatch for 'enabled_ro'. Legacy storage was {default_value}, new storage was {value}")
+ }
+ }
+ result
}
/// query flag enabled_rw
pub fn enabled_rw(&self) -> bool {
- true
+ let result = true;
+
+ if !Path::new(STORAGE_MIGRATION_MARKER_FILE).exists() {
+ return result;
+ }
+
+ // This will be called multiple times. Subsequent calls after the first
+ // are noops.
+ logger::init(
+ logger::Config::default()
+ .with_tag_on_device(MIGRATION_LOG_TAG)
+ .with_max_level(LevelFilter::Info),
+ );
+
+ unsafe {
+ let package_map = match get_mapped_storage_file("system", StorageFileType::PackageMap) {
+ Ok(file) => file,
+ Err(err) => {
+ error!("failed to read flag 'enabled_rw': {}", err);
+ return result;
+ }
+ };
+ let package_offset = match get_package_offset(&package_map, "com.android.aconfig.test") {
+ Ok(Some(offset)) => offset,
+ Ok(None) => {
+ error!("failed to read flag 'enabled_rw', not found in package map");
+ return result;
+ },
+ Err(err) => {
+ error!("failed to read flag 'enabled_rw': {}", err);
+ return result;
+ }
+ };
+ let flag_val_map = match get_mapped_storage_file("system", StorageFileType::FlagVal) {
+ Ok(val_map) => val_map,
+ Err(err) => {
+ error!("failed to read flag 'enabled_rw': {}", err);
+ return result;
+ }
+ };
+ let value = match get_boolean_flag_value(&flag_val_map, 5 + package_offset.boolean_offset) {
+ Ok(val) => val,
+ Err(err) => {
+ error!("failed to read flag 'enabled_rw': {}", err);
+ return result;
+ }
+ };
+ if true != value {
+ let default_value = true;
+ error!("flag mismatch for 'enabled_rw'. Legacy storage was {default_value}, new storage was {value}")
+ }
+ }
+ result
}
}
@@ -633,14 +982,22 @@
true
}
"#;
+ use crate::commands::assign_flag_ids;
- fn test_generate_rust_code(mode: CodegenMode) {
+ fn test_generate_rust_code(mode: CodegenMode, instrumentation: bool) {
let parsed_flags = crate::test::parse_test_flags();
let modified_parsed_flags =
crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
- let generated =
- generate_rust_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
- .unwrap();
+ let flag_ids =
+ assign_flag_ids(crate::test::TEST_PACKAGE, modified_parsed_flags.iter()).unwrap();
+ let generated = generate_rust_code(
+ crate::test::TEST_PACKAGE,
+ flag_ids,
+ modified_parsed_flags.into_iter(),
+ mode,
+ instrumentation,
+ )
+ .unwrap();
assert_eq!("src/lib.rs", format!("{}", generated.path.display()));
assert_eq!(
None,
@@ -658,21 +1015,21 @@
#[test]
fn test_generate_rust_code_for_prod() {
- test_generate_rust_code(CodegenMode::Production);
+ test_generate_rust_code(CodegenMode::Production, false);
}
#[test]
fn test_generate_rust_code_for_test() {
- test_generate_rust_code(CodegenMode::Test);
+ test_generate_rust_code(CodegenMode::Test, true);
}
#[test]
fn test_generate_rust_code_for_exported() {
- test_generate_rust_code(CodegenMode::Exported);
+ test_generate_rust_code(CodegenMode::Exported, true);
}
#[test]
fn test_generate_rust_code_for_force_read_only() {
- test_generate_rust_code(CodegenMode::ForceReadOnly);
+ test_generate_rust_code(CodegenMode::ForceReadOnly, true);
}
}
diff --git a/tools/aconfig/aconfig/src/commands.rs b/tools/aconfig/aconfig/src/commands.rs
index 7736ce7..66bcb82 100644
--- a/tools/aconfig/aconfig/src/commands.rs
+++ b/tools/aconfig/aconfig/src/commands.rs
@@ -218,7 +218,11 @@
generate_cpp_code(&package, modified_parsed_flags.into_iter(), codegen_mode)
}
-pub fn create_rust_lib(mut input: Input, codegen_mode: CodegenMode) -> Result<OutputFile> {
+pub fn create_rust_lib(
+ mut input: Input,
+ codegen_mode: CodegenMode,
+ allow_instrumentation: bool,
+) -> Result<OutputFile> {
// // TODO(327420679): Enable export mode for native flag library
ensure!(
codegen_mode != CodegenMode::Exported,
@@ -230,8 +234,14 @@
bail!("no parsed flags, or the parsed flags use different packages");
};
let package = package.to_string();
- let _flag_ids = assign_flag_ids(&package, modified_parsed_flags.iter())?;
- generate_rust_code(&package, modified_parsed_flags.into_iter(), codegen_mode)
+ let flag_ids = assign_flag_ids(&package, modified_parsed_flags.iter())?;
+ generate_rust_code(
+ &package,
+ flag_ids,
+ modified_parsed_flags.into_iter(),
+ codegen_mode,
+ allow_instrumentation,
+ )
}
pub fn create_storage(
diff --git a/tools/aconfig/aconfig/src/main.rs b/tools/aconfig/aconfig/src/main.rs
index 69f5458..6dae8ac 100644
--- a/tools/aconfig/aconfig/src/main.rs
+++ b/tools/aconfig/aconfig/src/main.rs
@@ -90,6 +90,12 @@
.arg(Arg::new("cache").long("cache").required(true))
.arg(Arg::new("out").long("out").required(true))
.arg(
+ Arg::new("allow-instrumentation")
+ .long("allow-instrumentation")
+ .value_parser(clap::value_parser!(bool))
+ .default_value("false"),
+ )
+ .arg(
Arg::new("mode")
.long("mode")
.value_parser(EnumValueParser::<CodegenMode>::new())
@@ -251,8 +257,10 @@
Some(("create-rust-lib", sub_matches)) => {
let cache = open_single_file(sub_matches, "cache")?;
let mode = get_required_arg::<CodegenMode>(sub_matches, "mode")?;
- let generated_file =
- commands::create_rust_lib(cache, *mode).context("failed to create rust lib")?;
+ let allow_instrumentation =
+ get_required_arg::<bool>(sub_matches, "allow-instrumentation")?;
+ let generated_file = commands::create_rust_lib(cache, *mode, *allow_instrumentation)
+ .context("failed to create rust lib")?;
let dir = PathBuf::from(get_required_arg::<String>(sub_matches, "out")?);
write_output_file_realtive_to_dir(&dir, &generated_file)?;
}
diff --git a/tools/aconfig/aconfig/templates/rust.template b/tools/aconfig/aconfig/templates/rust.template
index f9a2829..4b1ad83 100644
--- a/tools/aconfig/aconfig/templates/rust.template
+++ b/tools/aconfig/aconfig/templates/rust.template
@@ -1,5 +1,14 @@
//! codegenerated rust flag lib
+use aconfig_storage_read_api::\{StorageFileType, get_mapped_storage_file, get_boolean_flag_value, get_package_offset};
+use std::path::Path;
+use std::io::Write;
+use log::\{info, error, LevelFilter};
+
+static STORAGE_MIGRATION_MARKER_FILE: &str =
+ "/metadata/aconfig/storage_test_mission_1";
+static MIGRATION_LOG_TAG: &str = "AconfigStorageTestMission1";
+
/// flag provider
pub struct FlagProvider;
@@ -22,11 +31,76 @@
{{ for flag in template_flags }}
/// query flag {flag.name}
pub fn {flag.name}(&self) -> bool \{
- {{ -if flag.readwrite }}
+ {{ if not allow_instrumentation }}
+
+ {{ -if flag.readwrite }}
*CACHED_{flag.name}
- {{ -else }}
+ {{ -else }}
{flag.default_value}
- {{ -endif }}
+ {{ -endif }}
+
+ {{ else }}
+ let result = {{ -if flag.readwrite }} *CACHED_{flag.name}{{ else }} {flag.default_value} {{ -endif }};
+
+ {{ if flag.readwrite }}
+ result
+ {{ else }}
+
+ if !Path::new(STORAGE_MIGRATION_MARKER_FILE).exists() \{
+ return result;
+ }
+
+ // This will be called multiple times. Subsequent calls after the first
+ // are noops.
+ logger::init(
+ logger::Config::default()
+ .with_tag_on_device(MIGRATION_LOG_TAG)
+ .with_max_level(LevelFilter::Info),
+ );
+
+ unsafe \{
+ let package_map = match get_mapped_storage_file("system", StorageFileType::PackageMap) \{
+ Ok(file) => file,
+ Err(err) => \{
+ error!("failed to read flag '{flag.name}': \{}", err);
+ return result;
+ }
+ };
+ let package_offset = match get_package_offset(&package_map, "{package}") \{
+ Ok(Some(offset)) => offset,
+ Ok(None) => \{
+ error!("failed to read flag '{flag.name}', not found in package map");
+ return result;
+ },
+ Err(err) => \{
+ error!("failed to read flag '{flag.name}': \{}", err);
+ return result;
+ }
+ };
+ let flag_val_map = match get_mapped_storage_file("{flag.container}", StorageFileType::FlagVal) \{
+ Ok(val_map) => val_map,
+ Err(err) => \{
+ error!("failed to read flag '{flag.name}': \{}", err);
+ return result;
+ }
+ };
+ let value = match get_boolean_flag_value(&flag_val_map, {flag.flag_offset} + package_offset.boolean_offset) \{
+ Ok(val) => val,
+ Err(err) => \{
+ error!("failed to read flag '{flag.name}': \{}", err);
+ return result;
+ }
+ };
+
+ if {flag.default_value} != value \{
+ let default_value = {flag.default_value};
+ error!("flag mismatch for '{flag.name}'. Legacy storage was \{default_value}, new storage was \{value}")
+ }
+ }
+
+ result
+ {{ endif }}
+ {{ endif }}
}
{{ endfor }}