[ffx][config] Separate nested ops from value maps

This breaks up the logic around doing nested gets/sets/removes
from the logic that does mapping and string interpolation on
leaf Value files.

In the process, it removes the currying and cons-ish recursion
of the mapping functions, in favour of a more rust-y helper
trait nested_map that you can call on either an Option<Value>
or a ConfigValue, and then call with another mapping function.

I think it's really nice that this puts the order of the value
mapping operations in linear instead of reverse order to how
they actually run. I think it makes the code much less confusing.

The various nested_* things get moved again, this time to their
own module, as they no longer perform mapping operations
(other than NestedMap::nested_map, but I think it belongs with
the other nested functions).

Some of this restructuring may be useful to create a better
starting point for resolving fxb/101249, but this CL is not
intended to fix that on its own.

Another minor inclusion is adding a file processing argument
to the ffx-config-get arguments, which proved useful in finding
the last problems with this CL.

Bug: 101249

Change-Id: Ica85b352f3e7bb1add30d9ffbec12d88a00ee77f
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/690544
Reviewed-by: Andy Weiss <dragonbear@google.com>
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
Reviewed-by: Steven Grady <slgrady@google.com>
Fuchsia-Auto-Submit: Megan Batty <mgnb@google.com>
diff --git a/src/developer/ffx/config/BUILD.gn b/src/developer/ffx/config/BUILD.gn
index fe66711..57a2f81 100644
--- a/src/developer/ffx/config/BUILD.gn
+++ b/src/developer/ffx/config/BUILD.gn
@@ -48,9 +48,9 @@
       "src/mapping/filter.rs",
       "src/mapping/flatten.rs",
       "src/mapping/home.rs",
-      "src/mapping/identity.rs",
       "src/mapping/mod.rs",
       "src/mapping/runtime.rs",
+      "src/nested.rs",
       "src/paths.rs",
       "src/runtime.rs",
       "src/sdk.rs",
diff --git a/src/developer/ffx/config/src/api/mod.rs b/src/developer/ffx/config/src/api/mod.rs
index 801eaaf..3a59c72 100644
--- a/src/developer/ffx/config/src/api/mod.rs
+++ b/src/developer/ffx/config/src/api/mod.rs
@@ -26,13 +26,10 @@
     }
 }
 
-pub(crate) async fn get_config<'a, T: Fn(Value) -> Option<Value>>(
-    query: ConfigQuery<'a>,
-    mapper: &T,
-) -> ConfigResult {
+pub(crate) async fn get_config<'a>(query: ConfigQuery<'a>) -> ConfigResult {
     let config = load_config(&query.build_dir.map(String::from)).await?;
     let read_guard = config.read().await;
-    Ok((*read_guard).get(&query, mapper).into())
+    Ok((*read_guard).get(&query).into())
 }
 
 pub(crate) fn validate_type<T>(value: Value) -> Option<Value>
diff --git a/src/developer/ffx/config/src/api/query.rs b/src/developer/ffx/config/src/api/query.rs
index 341c8e5..df69620 100644
--- a/src/developer/ffx/config/src/api/query.rs
+++ b/src/developer/ffx/config/src/api/query.rs
@@ -13,6 +13,7 @@
     All,
 }
 
+#[derive(Debug, Clone)]
 pub struct ConfigQuery<'a> {
     pub name: Option<&'a str>,
     pub level: Option<ConfigLevel>,
diff --git a/src/developer/ffx/config/src/api/value.rs b/src/developer/ffx/config/src/api/value.rs
index f9478cd..28fcff2 100644
--- a/src/developer/ffx/config/src/api/value.rs
+++ b/src/developer/ffx/config/src/api/value.rs
@@ -8,6 +8,7 @@
         ConfigError,
     },
     crate::mapping::{filter::filter, flatten::flatten},
+    crate::nested::RecursiveMap,
     anyhow::anyhow,
     serde_json::{Map, Value},
     std::{
@@ -24,11 +25,23 @@
 #[derive(Debug)]
 pub struct ConfigValue(pub(crate) Option<Value>);
 
+// See RecursiveMap for why the value version is the main implementation.
+impl RecursiveMap for ConfigValue {
+    type Output = ConfigValue;
+    fn recursive_map<T: Fn(Value) -> Option<Value>>(self, mapper: &T) -> ConfigValue {
+        ConfigValue(self.0.recursive_map(mapper))
+    }
+}
+impl RecursiveMap for &ConfigValue {
+    type Output = ConfigValue;
+    fn recursive_map<T: Fn(Value) -> Option<Value>>(self, mapper: &T) -> ConfigValue {
+        ConfigValue(self.0.clone()).recursive_map(mapper)
+    }
+}
+
 pub trait ValueStrategy {
-    fn handle_arrays<'a, T: Fn(Value) -> Option<Value> + Sync>(
-        next: &'a T,
-    ) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
-        flatten(next)
+    fn handle_arrays(value: Value) -> Option<Value> {
+        flatten(value)
     }
 
     fn validate_query(query: &ConfigQuery<'_>) -> std::result::Result<(), ConfigError> {
@@ -52,10 +65,8 @@
 }
 
 impl ValueStrategy for Value {
-    fn handle_arrays<'a, T: Fn(Value) -> Option<Value> + Sync>(
-        next: &'a T,
-    ) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
-        Box::new(move |value| -> Option<Value> { next(value) })
+    fn handle_arrays(value: Value) -> Option<Value> {
+        Some(value)
     }
 
     fn validate_query(_query: &ConfigQuery<'_>) -> std::result::Result<(), ConfigError> {
@@ -72,10 +83,8 @@
 }
 
 impl ValueStrategy for Option<Value> {
-    fn handle_arrays<'a, T: Fn(Value) -> Option<Value> + Sync>(
-        next: &'a T,
-    ) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
-        Box::new(move |value| -> Option<Value> { next(value) })
+    fn handle_arrays(value: Value) -> Option<Value> {
+        Some(value)
     }
 
     fn validate_query(_query: &ConfigQuery<'_>) -> std::result::Result<(), ConfigError> {
@@ -224,10 +233,8 @@
 }
 
 impl<T: TryFrom<ConfigValue>> ValueStrategy for Vec<T> {
-    fn handle_arrays<'a, F: Fn(Value) -> Option<Value> + Sync>(
-        next: &'a F,
-    ) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
-        filter(next)
+    fn handle_arrays(value: Value) -> Option<Value> {
+        filter(value)
     }
 
     fn validate_query(_query: &ConfigQuery<'_>) -> std::result::Result<(), ConfigError> {
@@ -260,10 +267,8 @@
 }
 
 impl ValueStrategy for Map<String, Value> {
-    fn handle_arrays<'a, F: Fn(Value) -> Option<Value> + Sync>(
-        next: &'a F,
-    ) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
-        filter(next)
+    fn handle_arrays(value: Value) -> Option<Value> {
+        filter(value)
     }
 
     fn validate_query(_query: &ConfigQuery<'_>) -> std::result::Result<(), ConfigError> {
diff --git a/src/developer/ffx/config/src/lib.rs b/src/developer/ffx/config/src/lib.rs
index f0af6ee..9a689e7 100644
--- a/src/developer/ffx/config/src/lib.rs
+++ b/src/developer/ffx/config/src/lib.rs
@@ -14,8 +14,9 @@
     crate::environment::Environment,
     crate::mapping::{
         cache::cache, config::config, data::data, env_var::env_var, file_check::file_check,
-        home::home, identity::identity, runtime::runtime,
+        home::home, runtime::runtime,
     },
+    crate::nested::RecursiveMap,
     crate::paths::get_default_user_file_path,
     crate::storage::Config,
     analytics::{is_opted_in, set_opt_in_status},
@@ -38,6 +39,7 @@
 
 mod cache;
 mod mapping;
+mod nested;
 mod paths;
 mod runtime;
 mod storage;
@@ -84,7 +86,11 @@
 {
     let converted_query = query.into();
     T::validate_query(&converted_query)?;
-    get_config(converted_query, &validate_type::<T>).await.map_err(|e| e.into())?.try_into()
+    get_config(converted_query)
+        .await
+        .map_err(|e| e.into())?
+        .recursive_map(&validate_type::<T>)
+        .try_into()
 }
 
 pub async fn get<'a, T, U>(query: U) -> std::result::Result<T, T::Error>
@@ -95,14 +101,19 @@
 {
     let converted_query = query.into();
     T::validate_query(&converted_query)?;
-    let env_var_mapper = env_var(&validate_type::<T>);
-    let home_mapper = home(&env_var_mapper);
-    let config_mapper = config(&home_mapper);
-    let data_mapper = data(&config_mapper);
-    let cache_mapper = cache(&data_mapper);
-    let runtime_mapper = runtime(&cache_mapper);
-    let array_env_var_mapper = T::handle_arrays(&runtime_mapper);
-    get_config(converted_query, &array_env_var_mapper).await.map_err(|e| e.into())?.try_into()
+
+    get_config(converted_query)
+        .await
+        .map_err(|e| e.into())?
+        .recursive_map(&runtime)
+        .recursive_map(&cache)
+        .recursive_map(&data)
+        .recursive_map(&config)
+        .recursive_map(&home)
+        .recursive_map(&env_var)
+        .recursive_map(&T::handle_arrays)
+        .recursive_map(&validate_type::<T>)
+        .try_into()
 }
 
 pub async fn file<'a, T, U>(query: U) -> std::result::Result<T, T::Error>
@@ -113,15 +124,18 @@
 {
     let converted_query = query.into();
     T::validate_query(&converted_query)?;
-    let file_check_mapper = file_check(&identity);
-    let env_var_mapper = env_var(&file_check_mapper);
-    let home_mapper = home(&env_var_mapper);
-    let config_mapper = config(&home_mapper);
-    let data_mapper = data(&config_mapper);
-    let cache_mapper = cache(&data_mapper);
-    let runtime_mapper = runtime(&cache_mapper);
-    let array_env_var_mapper = T::handle_arrays(&runtime_mapper);
-    get_config(converted_query, &array_env_var_mapper).await.map_err(|e| e.into())?.try_into()
+    get_config(converted_query)
+        .await
+        .map_err(|e| e.into())?
+        .recursive_map(&runtime)
+        .recursive_map(&cache)
+        .recursive_map(&data)
+        .recursive_map(&config)
+        .recursive_map(&home)
+        .recursive_map(&env_var)
+        .recursive_map(&T::handle_arrays)
+        .recursive_map(&file_check)
+        .try_into()
 }
 
 pub async fn set<'a, U: Into<ConfigQuery<'a>>>(query: U, value: Value) -> Result<()> {
@@ -218,7 +232,7 @@
     check_config_files(&level, &config_query.build_dir.map(String::from))?;
     let config = load_config(&config_query.build_dir.map(String::from)).await?;
     let mut write_guard = config.write().await;
-    let config_changed = if let Some(mut current) = (*write_guard).get(&config_query, &identity) {
+    let config_changed = if let Some(mut current) = (*write_guard).get(&config_query) {
         if current.is_object() {
             bail!("cannot add a value to a subtree");
         } else {
diff --git a/src/developer/ffx/config/src/mapping/cache.rs b/src/developer/ffx/config/src/mapping/cache.rs
index e93c813..e770647 100644
--- a/src/developer/ffx/config/src/mapping/cache.rs
+++ b/src/developer/ffx/config/src/mapping/cache.rs
@@ -7,14 +7,12 @@
     regex::Regex, serde_json::Value,
 };
 
-pub(crate) fn cache<'a, T: Fn(Value) -> Option<Value> + Sync>(
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
+pub(crate) fn cache(value: Value) -> Option<Value> {
     lazy_static! {
         static ref REGEX: Regex = Regex::new(r"\$(CACHE)").unwrap();
     }
 
-    replace(&*REGEX, get_cache_base_path, next)
+    replace(&*REGEX, get_cache_base_path, value)
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -22,7 +20,6 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
 
     fn cache_dir(default: &str) -> String {
         match get_cache_base_path() {
@@ -35,19 +32,19 @@
     fn test_mapper() {
         let value = cache_dir("$CACHE");
         let test = Value::String("$CACHE".to_string());
-        assert_eq!(cache(&identity)(test), Some(Value::String(value.to_string())));
+        assert_eq!(cache(test), Some(Value::String(value.to_string())));
     }
 
     #[test]
     fn test_mapper_multiple() {
         let value = cache_dir("$CACHE");
         let test = Value::String("$CACHE/$CACHE".to_string());
-        assert_eq!(cache(&identity)(test), Some(Value::String(format!("{}/{}", value, value))));
+        assert_eq!(cache(test), Some(Value::String(format!("{}/{}", value, value))));
     }
 
     #[test]
     fn test_mapper_returns_pass_through() {
         let test = Value::String("$WHATEVER".to_string());
-        assert_eq!(cache(&identity)(test), Some(Value::String("$WHATEVER".to_string())));
+        assert_eq!(cache(test), Some(Value::String("$WHATEVER".to_string())));
     }
 }
diff --git a/src/developer/ffx/config/src/mapping/config.rs b/src/developer/ffx/config/src/mapping/config.rs
index 247db06..49eb619 100644
--- a/src/developer/ffx/config/src/mapping/config.rs
+++ b/src/developer/ffx/config/src/mapping/config.rs
@@ -7,14 +7,12 @@
     regex::Regex, serde_json::Value,
 };
 
-pub(crate) fn config<'a, T: Fn(Value) -> Option<Value> + Sync>(
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
+pub(crate) fn config(value: Value) -> Option<Value> {
     lazy_static! {
         static ref REGEX: Regex = Regex::new(r"\$(CONFIG)").unwrap();
     }
 
-    replace(&*REGEX, get_config_base_path, next)
+    replace(&*REGEX, get_config_base_path, value)
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -22,7 +20,6 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
 
     fn config_dir(default: &str) -> String {
         match get_config_base_path() {
@@ -35,19 +32,19 @@
     fn test_mapper() {
         let value = config_dir("$CONFIG");
         let test = Value::String("$CONFIG".to_string());
-        assert_eq!(config(&identity)(test), Some(Value::String(value.to_string())));
+        assert_eq!(config(test), Some(Value::String(value.to_string())));
     }
 
     #[test]
     fn test_mapper_multiple() {
         let value = config_dir("$CONFIG");
         let test = Value::String("$CONFIG/$CONFIG".to_string());
-        assert_eq!(config(&identity)(test), Some(Value::String(format!("{}/{}", value, value))));
+        assert_eq!(config(test), Some(Value::String(format!("{}/{}", value, value))));
     }
 
     #[test]
     fn test_mapper_returns_pass_through() {
         let test = Value::String("$WHATEVER".to_string());
-        assert_eq!(config(&identity)(test), Some(Value::String("$WHATEVER".to_string())));
+        assert_eq!(config(test), Some(Value::String("$WHATEVER".to_string())));
     }
 }
diff --git a/src/developer/ffx/config/src/mapping/data.rs b/src/developer/ffx/config/src/mapping/data.rs
index 538fba1..fcd81a5 100644
--- a/src/developer/ffx/config/src/mapping/data.rs
+++ b/src/developer/ffx/config/src/mapping/data.rs
@@ -7,14 +7,12 @@
     regex::Regex, serde_json::Value,
 };
 
-pub(crate) fn data<'a, T: Fn(Value) -> Option<Value> + Sync>(
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
+pub(crate) fn data(value: Value) -> Option<Value> {
     lazy_static! {
         static ref REGEX: Regex = Regex::new(r"\$(DATA)").unwrap();
     }
 
-    replace(&*REGEX, get_data_base_path, next)
+    replace(&*REGEX, get_data_base_path, value)
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -22,7 +20,6 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
 
     fn data_dir(default: &str) -> String {
         match get_data_base_path() {
@@ -35,19 +32,19 @@
     fn test_mapper() {
         let value = data_dir("$DATA");
         let test = Value::String("$DATA".to_string());
-        assert_eq!(data(&identity)(test), Some(Value::String(value.to_string())));
+        assert_eq!(data(test), Some(Value::String(value.to_string())));
     }
 
     #[test]
     fn test_mapper_multiple() {
         let value = data_dir("$DATA");
         let test = Value::String("$DATA/$DATA".to_string());
-        assert_eq!(data(&identity)(test), Some(Value::String(format!("{}/{}", value, value))));
+        assert_eq!(data(test), Some(Value::String(format!("{}/{}", value, value))));
     }
 
     #[test]
     fn test_mapper_returns_pass_through() {
         let test = Value::String("$WHATEVER".to_string());
-        assert_eq!(data(&identity)(test), Some(Value::String("$WHATEVER".to_string())));
+        assert_eq!(data(test), Some(Value::String("$WHATEVER".to_string())));
     }
 }
diff --git a/src/developer/ffx/config/src/mapping/env_var.rs b/src/developer/ffx/config/src/mapping/env_var.rs
index a559160..4f40f3c 100644
--- a/src/developer/ffx/config/src/mapping/env_var.rs
+++ b/src/developer/ffx/config/src/mapping/env_var.rs
@@ -28,29 +28,22 @@
     true
 }
 
-pub(crate) fn env_var<'a, T: Fn(Value) -> Option<Value> + Sync>(
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
+pub(crate) fn env_var(value: Value) -> Option<Value> {
     lazy_static! {
         static ref REGEX: Regex = Regex::new(r"\$([A-Z][A-Z0-9_]*)").unwrap();
     }
 
-    Box::new(move |value| -> Option<Value> {
-        let env_string = preprocess(&value);
-        if let Some(ref e) = env_string {
-            if !check(e, &*REGEX) {
-                return None;
-            }
+    let env_string = preprocess(&value);
+    if let Some(ref e) = env_string {
+        if !check(e, &*REGEX) {
+            return None;
         }
-        match env_string
-            .as_ref()
-            .map(|s| replace(s, &*REGEX, |v| env::var(v).map_err(|_| anyhow!(""))))
-            .map(postprocess)
-        {
-            Some(v) => next(v),
-            None => next(value),
-        }
-    })
+    }
+    env_string
+        .as_ref()
+        .map(|s| replace(s, &*REGEX, |v| env::var(v).map_err(|_| anyhow!(""))))
+        .map(postprocess)
+        .or(Some(value))
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -58,7 +51,6 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
 
     fn setup_test(env_vars: Vec<(&'static str, &'static str)>) -> Box<dyn FnOnce() -> ()> {
         env_vars.iter().for_each(|(var, val)| env::set_var(var, val));
@@ -72,7 +64,7 @@
         let cleanup: Box<dyn FnOnce() -> ()> =
             setup_test(vec![("FFX_TEST_ENV_VAR_MAPPER", "test")]);
         let test = Value::String("$FFX_TEST_ENV_VAR_MAPPER".to_string());
-        assert_eq!(env_var(&identity)(test), Some(Value::String("test".to_string())));
+        assert_eq!(env_var(test), Some(Value::String("test".to_string())));
         cleanup();
     }
 
@@ -83,14 +75,14 @@
         let test = Value::String(
             "$FFX_TEST_ENV_VAR_MAPPER_MULTIPLE/$FFX_TEST_ENV_VAR_MAPPER_MULTIPLE".to_string(),
         );
-        assert_eq!(env_var(&identity)(test), Some(Value::String(format!("{}/{}", "test", "test"))));
+        assert_eq!(env_var(test), Some(Value::String(format!("{}/{}", "test", "test"))));
         cleanup();
     }
 
     #[test]
     fn test_env_var_mapper_returns_none() {
         let test = Value::String("$ENVIRONMENT_VARIABLE_THAT_DOES_NOT_EXIST".to_string());
-        assert_eq!(env_var(&identity)(test), None);
+        assert_eq!(env_var(test), None);
     }
 
     #[test]
@@ -98,19 +90,19 @@
         let cleanup: Box<dyn FnOnce() -> ()> =
             setup_test(vec![("FFX_TEST_ENV_VAR_EXISTS", "test")]);
         let test = Value::String("$HOME/$ENVIRONMENT_VARIABLE_THAT_DOES_NOT_EXIST".to_string());
-        assert_eq!(env_var(&identity)(test), None);
+        assert_eq!(env_var(test), None);
         cleanup();
     }
 
     #[test]
     fn test_env_var_mapper_escapes_dollar_sign() {
         let test = Value::String("$$HOME".to_string());
-        assert_eq!(env_var(&identity)(test), Some(Value::String("$HOME".to_string())));
+        assert_eq!(env_var(test), Some(Value::String("$HOME".to_string())));
     }
 
     #[test]
     fn test_env_var_returns_value_if_not_string() {
         let test = Value::Bool(false);
-        assert_eq!(env_var(&identity)(test), Some(Value::Bool(false)));
+        assert_eq!(env_var(test), Some(Value::Bool(false)));
     }
 }
diff --git a/src/developer/ffx/config/src/mapping/file_check.rs b/src/developer/ffx/config/src/mapping/file_check.rs
index dec63e8..e65e3a1 100644
--- a/src/developer/ffx/config/src/mapping/file_check.rs
+++ b/src/developer/ffx/config/src/mapping/file_check.rs
@@ -4,18 +4,15 @@
 
 use {serde_json::Value, std::path::PathBuf};
 
-pub(crate) fn file_check<'a, T: Fn(Value) -> Option<Value> + Sync>(
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + 'a + Send + Sync> {
-    Box::new(move |value| -> Option<Value> {
-        value.as_str().map(|s| s.to_string()).and_then(|s| {
-            if PathBuf::from(s.clone()).exists() {
-                next(value)
-            } else {
-                None
-            }
-        })
-    })
+/// Filters for config values that map to files that are reachable. Returns None
+/// for strings that don't correspond to files discoverable by [`PathBuf::exists`],
+/// but maps to the same value for anything else.
+pub(crate) fn file_check(value: Value) -> Option<Value> {
+    match &value {
+        Value::String(s) if PathBuf::from(s).exists() => Some(value),
+        Value::String(_) => None, // filter out strings that don't correspond to existing files.
+        _ => Some(value),         // but let any other type through.
+    }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -23,7 +20,6 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
     use anyhow::{bail, Result};
     use serde_json::json;
     use tempfile::NamedTempFile;
@@ -33,7 +29,7 @@
         let file = NamedTempFile::new()?;
         if let Some(path) = file.path().to_str() {
             let test = Value::String(path.to_string());
-            assert_eq!(file_check(&identity)(test), Some(Value::String(path.to_string())));
+            assert_eq!(file_check(test), Some(Value::String(path.to_string())));
             Ok(())
         } else {
             bail!("Unable to get temp file path");
@@ -43,7 +39,7 @@
     #[test]
     fn test_file_mapper_returns_none() -> Result<()> {
         let test = json!("/fake_path/should_not_exist");
-        assert_eq!(file_check(&identity)(test), None);
+        assert_eq!(file_check(test), None);
         Ok(())
     }
 }
diff --git a/src/developer/ffx/config/src/mapping/filter.rs b/src/developer/ffx/config/src/mapping/filter.rs
index 53ec9f0..8679a89 100644
--- a/src/developer/ffx/config/src/mapping/filter.rs
+++ b/src/developer/ffx/config/src/mapping/filter.rs
@@ -4,22 +4,14 @@
 
 use serde_json::Value;
 
-pub(crate) fn filter<'a, T: Fn(Value) -> Option<Value> + Sync>(
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + 'a + Send + Sync> {
-    Box::new(move |value| -> Option<Value> {
-        if let Value::Array(values) = value {
-            let result: Vec<Value> =
-                values.iter().filter_map(|inner_v| next(inner_v.clone())).collect();
-            if result.len() == 0 {
-                None
-            } else {
-                Some(Value::Array(result))
-            }
-        } else {
-            next(value)
+/// Maps values that are empty (ie. empty arrays) to None.
+pub(crate) fn filter(value: Value) -> Option<Value> {
+    if let Value::Array(values) = &value {
+        if values.len() == 0 {
+            return None;
         }
-    })
+    }
+    Some(value)
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -27,27 +19,19 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
     use serde_json::json;
 
     #[test]
     fn test_returns_all() {
         let test = json!(["test1", "test2"]);
-        let result = filter(&identity)(test);
+        let result = filter(test);
         assert_eq!(result, Some(json!(["test1", "test2"])));
     }
 
     #[test]
     fn test_returns_value_if_not_array() {
         let test = Value::Bool(false);
-        let result = filter(&identity)(test);
+        let result = filter(test);
         assert_eq!(result, Some(Value::Bool(false)));
     }
-
-    #[test]
-    fn test_returns_none() {
-        let test = Value::Bool(false);
-        let result = filter(&|_| -> Option<Value> { None })(test);
-        assert_eq!(result, None);
-    }
 }
diff --git a/src/developer/ffx/config/src/mapping/flatten.rs b/src/developer/ffx/config/src/mapping/flatten.rs
index 58ef3c5..603d800 100644
--- a/src/developer/ffx/config/src/mapping/flatten.rs
+++ b/src/developer/ffx/config/src/mapping/flatten.rs
@@ -4,16 +4,13 @@
 
 use serde_json::Value;
 
-pub(crate) fn flatten<'a, T: Fn(Value) -> Option<Value> + Sync>(
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + 'a + Send + Sync> {
-    Box::new(move |value| -> Option<Value> {
-        if let Value::Array(values) = value {
-            values.iter().find_map(|inner_v| next(inner_v.clone()))
-        } else {
-            next(value)
-        }
-    })
+/// Pick the first element of Value if it's an array.
+pub(crate) fn flatten(value: Value) -> Option<Value> {
+    if let Value::Array(values) = value {
+        values.into_iter().next()
+    } else {
+        Some(value)
+    }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -21,7 +18,6 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
 
     #[test]
     fn test_returns_first() {
@@ -29,14 +25,14 @@
             Value::String("test1".to_string()),
             Value::String("test2".to_string()),
         ]);
-        let result = flatten(&identity)(test);
+        let result = flatten(test);
         assert_eq!(result, Some(Value::String("test1".to_string())));
     }
 
     #[test]
     fn test_returns_value_if_not_string() {
         let test = Value::Bool(false);
-        let result = flatten(&identity)(test);
+        let result = flatten(test);
         assert_eq!(result, Some(Value::Bool(false)));
     }
 }
diff --git a/src/developer/ffx/config/src/mapping/home.rs b/src/developer/ffx/config/src/mapping/home.rs
index 5f0121c..57e87f2 100644
--- a/src/developer/ffx/config/src/mapping/home.rs
+++ b/src/developer/ffx/config/src/mapping/home.rs
@@ -9,29 +9,22 @@
     serde_json::Value,
 };
 
-pub(crate) fn home<'a, T: Fn(Value) -> Option<Value> + Sync>(
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
+pub(crate) fn home(value: Value) -> Option<Value> {
     lazy_static! {
         static ref REGEX: Regex = Regex::new(r"\$(HOME)").unwrap();
     }
 
-    Box::new(move |value| -> Option<Value> {
-        match preprocess(&value)
-            .as_ref()
-            .map(|s| {
-                replace(s, &*REGEX, |v| {
-                    Ok(home::home_dir().map_or(v.to_string(), |home_path| {
-                        home_path.to_str().map_or(v.to_string(), |s| s.to_string())
-                    }))
-                })
+    preprocess(&value)
+        .as_ref()
+        .map(|s| {
+            replace(s, &*REGEX, |v| {
+                Ok(home::home_dir().map_or(v.to_string(), |home_path| {
+                    home_path.to_str().map_or(v.to_string(), |s| s.to_string())
+                }))
             })
-            .map(postprocess)
-        {
-            Some(v) => next(v),
-            None => next(value),
-        }
-    })
+        })
+        .map(postprocess)
+        .or(Some(value))
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -39,7 +32,6 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
 
     fn home_dir(default: &str) -> String {
         home::home_dir().map_or(default.to_string(), |home_path| {
@@ -51,19 +43,19 @@
     fn test_mapper() {
         let value = home_dir("$HOME");
         let test = Value::String("$HOME".to_string());
-        assert_eq!(home(&identity)(test), Some(Value::String(value.to_string())));
+        assert_eq!(home(test), Some(Value::String(value.to_string())));
     }
 
     #[test]
     fn test_mapper_multiple() {
         let value = home_dir("$HOME");
         let test = Value::String("$HOME/$HOME".to_string());
-        assert_eq!(home(&identity)(test), Some(Value::String(format!("{}/{}", value, value))));
+        assert_eq!(home(test), Some(Value::String(format!("{}/{}", value, value))));
     }
 
     #[test]
     fn test_mapper_returns_pass_through() {
         let test = Value::String("$WHATEVER".to_string());
-        assert_eq!(home(&identity)(test), Some(Value::String("$WHATEVER".to_string())));
+        assert_eq!(home(test), Some(Value::String("$WHATEVER".to_string())));
     }
 }
diff --git a/src/developer/ffx/config/src/mapping/identity.rs b/src/developer/ffx/config/src/mapping/identity.rs
deleted file mode 100644
index aee67a96..0000000
--- a/src/developer/ffx/config/src/mapping/identity.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2020 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-pub(crate) fn identity(value: serde_json::Value) -> Option<serde_json::Value> {
-    Some(value)
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// tests
-#[cfg(test)]
-mod test {
-    use super::*;
-    use serde_json::Value;
-
-    #[test]
-    fn test_returns_first() {
-        let test = Value::String("test1".to_string());
-        let result = identity(test);
-        assert_eq!(result, Some(Value::String("test1".to_string())));
-    }
-}
diff --git a/src/developer/ffx/config/src/mapping/mod.rs b/src/developer/ffx/config/src/mapping/mod.rs
index e8c773e..f10a7ea 100644
--- a/src/developer/ffx/config/src/mapping/mod.rs
+++ b/src/developer/ffx/config/src/mapping/mod.rs
@@ -3,9 +3,9 @@
 // found in the LICENSE file.
 
 use {
-    anyhow::{anyhow, bail, Result},
+    anyhow::Result,
     regex::{Captures, Regex},
-    serde_json::{Map, Value},
+    serde_json::Value,
     std::path::PathBuf,
 };
 
@@ -17,7 +17,6 @@
 pub(crate) mod filter;
 pub(crate) mod flatten;
 pub(crate) mod home;
-pub(crate) mod identity;
 pub(crate) mod runtime;
 
 // Negative lookbehind (or lookahead for that matter) is not supported in Rust's regex.
@@ -48,138 +47,20 @@
         .into_owned()
 }
 
-pub(crate) fn replace<'a, T, P>(
-    regex: &'a Regex,
-    base_path: P,
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a>
+pub(crate) fn replace<'a, P>(regex: &'a Regex, base_path: P, value: Value) -> Option<Value>
 where
-    T: Fn(Value) -> Option<Value> + Sync,
     P: Fn() -> Result<PathBuf> + Sync + Send + 'a,
 {
-    Box::new(move |value| -> Option<Value> {
-        match preprocess(&value)
-            .as_ref()
-            .map(|s| {
-                replace_regex(s, regex, |v| {
-                    match base_path() {
-                        Ok(p) => Ok(p.to_str().map_or(v.to_string(), |s| s.to_string())),
-                        Err(_) => Ok(v.to_string()), //just pass through
-                    }
-                })
+    preprocess(&value)
+        .as_ref()
+        .map(|s| {
+            replace_regex(s, regex, |v| {
+                match base_path() {
+                    Ok(p) => Ok(p.to_str().map_or(v.to_string(), |s| s.to_string())),
+                    Err(_) => Ok(v.to_string()), //just pass through
+                }
             })
-            .map(postprocess)
-        {
-            Some(v) => next(v),
-            None => next(value),
-        }
-    })
-}
-
-pub(crate) fn nested_map<T: Fn(Value) -> Option<Value>>(
-    cur: Option<Value>,
-    mapper: &T,
-) -> Option<Value> {
-    cur.and_then(|c| {
-        if let Value::Object(map) = c {
-            let mut result = Map::new();
-            for (key, value) in map.iter() {
-                let new_value = if value.is_object() {
-                    nested_map(map.get(key).cloned(), mapper)
-                } else {
-                    map.get(key).cloned().and_then(|v| mapper(v))
-                };
-                if let Some(new_value) = new_value {
-                    result.insert(key.to_string(), new_value);
-                }
-            }
-            if result.len() == 0 {
-                None
-            } else {
-                Some(Value::Object(result))
-            }
-        } else {
-            mapper(c)
-        }
-    })
-}
-
-pub(crate) fn nested_get<T: Fn(Value) -> Option<Value>>(
-    cur: &Option<Value>,
-    key: &str,
-    remaining_keys: &[&str],
-    mapper: &T,
-) -> Option<Value> {
-    cur.as_ref().and_then(|c| {
-        if remaining_keys.len() == 0 {
-            nested_map(c.get(key).cloned(), mapper)
-        } else {
-            nested_get(&c.get(key).cloned(), remaining_keys[0], &remaining_keys[1..], mapper)
-        }
-    })
-}
-
-pub(crate) fn nested_set(
-    cur: &mut Map<String, Value>,
-    key: &str,
-    remaining_keys: &[&str],
-    value: Value,
-) -> bool {
-    if remaining_keys.len() == 0 {
-        // Exit early if the value hasn't changed.
-        if let Some(old_value) = cur.get(key) {
-            if old_value == &value {
-                return false;
-            }
-        }
-        cur.insert(key.to_string(), value);
-        true
-    } else {
-        match cur.get(key) {
-            Some(value) => {
-                if !value.is_object() {
-                    // Any literals will be overridden.
-                    cur.insert(key.to_string(), Value::Object(Map::new()));
-                }
-            }
-            None => {
-                cur.insert(key.to_string(), Value::Object(Map::new()));
-            }
-        }
-        // Just ensured this would be the case.
-        let next_map = cur
-            .get_mut(key)
-            .expect("unable to get configuration")
-            .as_object_mut()
-            .expect("Unable to set configuration value as map");
-        nested_set(next_map, remaining_keys[0], &remaining_keys[1..], value)
-    }
-}
-
-pub(crate) fn nested_remove(
-    cur: &mut Map<String, Value>,
-    key: &str,
-    remaining_keys: &[&str],
-) -> Result<()> {
-    if remaining_keys.len() == 0 {
-        cur.remove(&key.to_string()).ok_or(anyhow!("Config key not found")).map(|_| ())
-    } else {
-        match cur.get(key) {
-            Some(value) => {
-                if !value.is_object() {
-                    bail!("Configuration literal found when expecting a map.")
-                }
-            }
-            None => {
-                bail!("Configuration key not found.");
-            }
-        }
-        // Just ensured this would be the case.
-        let next_map = cur
-            .get_mut(key)
-            .expect("unable to get configuration")
-            .as_object_mut()
-            .expect("Unable to set configuration value as map");
-        nested_remove(next_map, remaining_keys[0], &remaining_keys[1..])
-    }
+        })
+        .map(postprocess)
+        .or(Some(value))
 }
diff --git a/src/developer/ffx/config/src/mapping/runtime.rs b/src/developer/ffx/config/src/mapping/runtime.rs
index 0f689a1..8775d00 100644
--- a/src/developer/ffx/config/src/mapping/runtime.rs
+++ b/src/developer/ffx/config/src/mapping/runtime.rs
@@ -7,14 +7,12 @@
     regex::Regex, serde_json::Value,
 };
 
-pub(crate) fn runtime<'a, T: Fn(Value) -> Option<Value> + Sync>(
-    next: &'a T,
-) -> Box<dyn Fn(Value) -> Option<Value> + Send + Sync + 'a> {
+pub(crate) fn runtime(value: Value) -> Option<Value> {
     lazy_static! {
         static ref REGEX: Regex = Regex::new(r"\$(RUNTIME)").unwrap();
     }
 
-    replace(&*REGEX, get_runtime_base_path, next)
+    replace(&*REGEX, get_runtime_base_path, value)
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -22,7 +20,6 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
 
     fn runtime_dir(default: &str) -> String {
         match get_runtime_base_path() {
@@ -35,19 +32,19 @@
     fn test_mapper() {
         let value = runtime_dir("$RUNTIME");
         let test = Value::String("$RUNTIME".to_string());
-        assert_eq!(runtime(&identity)(test), Some(Value::String(value.to_string())));
+        assert_eq!(runtime(test), Some(Value::String(value.to_string())));
     }
 
     #[test]
     fn test_mapper_multiple() {
         let value = runtime_dir("$RUNTIME");
         let test = Value::String("$RUNTIME/$RUNTIME".to_string());
-        assert_eq!(runtime(&identity)(test), Some(Value::String(format!("{}/{}", value, value))));
+        assert_eq!(runtime(test), Some(Value::String(format!("{}/{}", value, value))));
     }
 
     #[test]
     fn test_mapper_returns_pass_through() {
         let test = Value::String("$WHATEVER".to_string());
-        assert_eq!(runtime(&identity)(test), Some(Value::String("$WHATEVER".to_string())));
+        assert_eq!(runtime(test), Some(Value::String("$WHATEVER".to_string())));
     }
 }
diff --git a/src/developer/ffx/config/src/nested.rs b/src/developer/ffx/config/src/nested.rs
new file mode 100644
index 0000000..e3a034a
--- /dev/null
+++ b/src/developer/ffx/config/src/nested.rs
@@ -0,0 +1,166 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Functions for interacting with nested json objects in a recursive way.
+
+use {
+    anyhow::{Context, Result},
+    serde_json::{map::Entry, Map, Value},
+    std::iter::FromIterator,
+};
+
+/// A trait that adds a recursive mapping function to a nested json value tree.
+///
+/// Note that implementations of this should be on value types and not references,
+/// with perhaps an additional impl for a reference that clones. This is because most
+/// of the time most values won't change, so it makes sense to copy the whole thing up
+/// front and only have to build a new item if necessary.
+///
+/// This also makes it so you can efficiently chain [`RecursiveMap::recursive_map`]
+/// calls together, since they will continue to use the same Value items until one
+/// is overwritten.
+pub(crate) trait RecursiveMap {
+    /// The type of output that the filtering will return. That type must implement
+    /// this trait as well.
+    type Output: RecursiveMap;
+
+    /// Filters values recursively through the function provided.
+    fn recursive_map<T: Fn(Value) -> Option<Value>>(self, mapper: &T) -> Self::Output;
+}
+
+impl RecursiveMap for Value {
+    type Output = Option<Value>;
+    fn recursive_map<T: Fn(Value) -> Option<Value>>(self, mapper: &T) -> Option<Value> {
+        match self {
+            Value::Object(map) => {
+                let mut result = Map::new();
+                for (key, value) in map.into_iter() {
+                    let new_value = if value.is_object() {
+                        value.clone().recursive_map(mapper)
+                    } else {
+                        mapper(value.clone())
+                    };
+                    if let Some(new_value) = new_value.clone() {
+                        result.insert(key.clone(), new_value);
+                    }
+                }
+                if result.len() == 0 {
+                    None
+                } else {
+                    mapper(Value::Object(result))
+                }
+            }
+            Value::Array(arr) => {
+                let result =
+                    Vec::from_iter(arr.into_iter().filter_map(|value| value.recursive_map(mapper)));
+                if result.len() == 0 {
+                    None
+                } else {
+                    mapper(Value::Array(result))
+                }
+            }
+            other => mapper(other),
+        }
+    }
+}
+
+impl RecursiveMap for &Value {
+    type Output = Option<Value>;
+    fn recursive_map<T: Fn(Value) -> Option<Value>>(self, mapper: &T) -> Self::Output {
+        self.clone().recursive_map(mapper)
+    }
+}
+impl RecursiveMap for Option<&Value> {
+    type Output = Option<Value>;
+    fn recursive_map<T: Fn(Value) -> Option<Value>>(self, mapper: &T) -> Self::Output {
+        self.and_then(|value| value.clone().recursive_map(mapper))
+    }
+}
+impl RecursiveMap for Option<Value> {
+    type Output = Option<Value>;
+    fn recursive_map<T: Fn(Value) -> Option<Value>>(self, mapper: &T) -> Self::Output {
+        self.and_then(|value| value.recursive_map(mapper))
+    }
+}
+
+/// Search the given nested json value for the given `key`, and then recurse through
+/// the rest of the tree for the `remaining_keys`. Returns the value at that position if found.
+pub(crate) fn nested_get<'a>(
+    cur: Option<&'a Value>,
+    key: &str,
+    remaining_keys: &[&str],
+) -> Option<&'a Value> {
+    cur.and_then(|cur| {
+        if remaining_keys.len() == 0 {
+            cur.get(key)
+        } else {
+            nested_get(cur.get(key), remaining_keys[0], &remaining_keys[1..])
+        }
+    })
+}
+
+/// Find `key` in `cur`, then recurisively search for the `remaining_keys` through the nested
+/// object for the position to insert, creating Object entries as it goes if necessary (including
+/// overwriting leaf values in the way). Sets the value if not already set to the same value,
+/// and returns true if it did (or false if it already existed as the same value).
+pub(crate) fn nested_set(
+    cur: &mut Map<String, Value>,
+    key: &str,
+    remaining_keys: &[&str],
+    value: Value,
+) -> bool {
+    if remaining_keys.len() == 0 {
+        // Exit early if the value hasn't changed.
+        if let Some(old_value) = cur.get(key) {
+            if old_value == &value {
+                return false;
+            }
+        }
+        cur.insert(key.to_string(), value);
+        true
+    } else {
+        if let Entry::Occupied(mut occupied) = cur.entry(key) {
+            let val = occupied.get_mut();
+            if let Value::Object(ref mut next_map) = val {
+                nested_set(next_map, remaining_keys[0], &remaining_keys[1..], value)
+            } else {
+                let mut next_map = Map::new();
+                nested_set(&mut next_map, remaining_keys[0], &remaining_keys[1..], value);
+                *val = Value::Object(next_map);
+                // since we're creating the rest of the tree, we know we either succeeded and inserted or failed and crashed.
+                true
+            }
+        } else {
+            let mut next_map = Map::new();
+            nested_set(&mut next_map, remaining_keys[0], &remaining_keys[1..], value);
+            cur.insert(key.to_string(), Value::Object(next_map));
+            // since we're creating the rest of the tree, we know we either succeeded and inserted or failed and crashed.
+            true
+        }
+    }
+}
+
+/// Searches the nested object to find the given `key`, then recursively through the `remaining_keys`,
+/// and removes it if found. If the key or any of its parent keys don't exist, it will return an error.
+pub(crate) fn nested_remove(
+    cur: &mut Map<String, Value>,
+    key: &str,
+    remaining_keys: &[&str],
+) -> Result<()> {
+    if remaining_keys.len() == 0 {
+        cur.remove(&key.to_string()).context("Config key not found").map(|_| ())
+    } else {
+        // Just ensured this would be the case.
+        let next_map = cur
+            .get_mut(key)
+            .context("Configuration key not found.")?
+            .as_object_mut()
+            .context("Configuration literal found when expecting a map.")?;
+        nested_remove(next_map, remaining_keys[0], &remaining_keys[1..])?;
+        if next_map.len() == 0 {
+            cur.remove(key).context("Current key not found trying to recursively remove")?;
+        }
+        Ok(())
+    }
+}
diff --git a/src/developer/ffx/config/src/runtime.rs b/src/developer/ffx/config/src/runtime.rs
index 4138429..7699aa2 100644
--- a/src/developer/ffx/config/src/runtime.rs
+++ b/src/developer/ffx/config/src/runtime.rs
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use crate::mapping;
+use crate::nested::nested_set;
 use anyhow::{anyhow, bail, Context, Result};
 use serde_json::{Map, Value};
 use std::{fs::File, io::BufReader, path::Path};
@@ -13,7 +13,7 @@
         let s: Vec<&str> = pair.trim().split('=').collect();
         if s.len() == 2 {
             let key_vec: Vec<&str> = s[0].split('.').collect();
-            mapping::nested_set(
+            nested_set(
                 &mut runtime_config,
                 key_vec[0],
                 &key_vec[1..],
diff --git a/src/developer/ffx/config/src/storage.rs b/src/developer/ffx/config/src/storage.rs
index 585410c..fb6cc63 100644
--- a/src/developer/ffx/config/src/storage.rs
+++ b/src/developer/ffx/config/src/storage.rs
@@ -5,7 +5,7 @@
 use {
     crate::api::query::SelectMode,
     crate::environment::Environment,
-    crate::mapping::{nested_get, nested_map, nested_remove, nested_set},
+    crate::nested::{nested_get, nested_remove, nested_set},
     crate::{ConfigLevel, ConfigQuery},
     anyhow::{bail, Context, Result},
     config_macros::include_default,
@@ -18,6 +18,7 @@
     },
 };
 
+#[derive(Debug, Clone)]
 pub struct Config {
     default: Option<Value>,
     build: Option<Value>,
@@ -169,11 +170,7 @@
         })
     }
 
-    pub fn get<T: Fn(Value) -> Option<Value>>(
-        &self,
-        key: &ConfigQuery<'_>,
-        mapper: &T,
-    ) -> Option<Value> {
+    pub fn get(&self, key: &ConfigQuery<'_>) -> Option<Value> {
         if let Some(name) = key.name {
             // Check for nested config values if there's a '.' in the key
             let key_vec: Vec<&str> = name.split('.').collect();
@@ -185,16 +182,18 @@
                     ConfigLevel::Global => &self.global,
                     ConfigLevel::Default => &self.default,
                 };
-                nested_get(config, key_vec[0], &key_vec[1..], mapper)
+                nested_get(config.as_ref(), key_vec[0], &key_vec[1..]).cloned()
             } else {
                 match key.select {
-                    SelectMode::First => {
-                        self.iter().find_map(|c| nested_get(c, key_vec[0], &key_vec[1..], mapper))
-                    }
+                    SelectMode::First => self
+                        .iter()
+                        .find_map(|c| nested_get(c.as_ref(), key_vec[0], &key_vec[1..]))
+                        .cloned(),
                     SelectMode::All => {
                         let result: Vec<Value> = self
                             .iter()
-                            .filter_map(|c| nested_get(c, key_vec[0], &key_vec[1..], mapper))
+                            .filter_map(|c| nested_get(c.as_ref(), key_vec[0], &key_vec[1..]))
+                            .cloned()
                             .collect();
                         if result.len() > 0 {
                             Some(Value::Array(result))
@@ -213,7 +212,7 @@
                     ConfigLevel::Global => &self.global,
                     ConfigLevel::Default => &self.default,
                 };
-                nested_map(config.clone(), mapper)
+                config.clone()
             } else {
                 // Not really supported now.  Maybe in the future.
                 None
@@ -330,7 +329,7 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::mapping::identity::identity;
+    use crate::nested::RecursiveMap;
     use regex::Regex;
     use serde_json::json;
     use std::io::{BufReader, BufWriter};
@@ -398,7 +397,7 @@
             None,
         );
 
-        let value = persistent_config.get(&"name".into(), &identity);
+        let value = persistent_config.get(&"name".into());
         assert!(value.is_some());
         assert_eq!(value.unwrap(), Value::String(String::from("User")));
 
@@ -477,7 +476,7 @@
             runtime: None,
         };
 
-        let value = test.get(&"name".into(), &identity);
+        let value = test.get(&"name".into());
         assert!(value.is_some());
         assert_eq!(value.unwrap(), Value::String(String::from("User")));
 
@@ -489,7 +488,7 @@
             runtime: None,
         };
 
-        let value_build = test_build.get(&"name".into(), &identity);
+        let value_build = test_build.get(&"name".into());
         assert!(value_build.is_some());
         assert_eq!(value_build.unwrap(), Value::String(String::from("Build")));
 
@@ -501,7 +500,7 @@
             runtime: None,
         };
 
-        let value_global = test_global.get(&"name".into(), &identity);
+        let value_global = test_global.get(&"name".into());
         assert!(value_global.is_some());
         assert_eq!(value_global.unwrap(), Value::String(String::from("Global")));
 
@@ -513,14 +512,14 @@
             runtime: None,
         };
 
-        let value_default = test_default.get(&"name".into(), &identity);
+        let value_default = test_default.get(&"name".into());
         assert!(value_default.is_some());
         assert_eq!(value_default.unwrap(), Value::String(String::from("Default")));
 
         let test_none =
             Config { user: None, build: None, global: None, default: None, runtime: None };
 
-        let value_none = test_none.get(&"name".into(), &identity);
+        let value_none = test_none.get(&"name".into());
         assert!(value_none.is_none());
         Ok(())
     }
@@ -535,7 +534,7 @@
             runtime: None,
         };
         test.set(&("name", &ConfigLevel::User).into(), Value::String(String::from("whatever")))?;
-        let value = test.get(&"name".into(), &identity);
+        let value = test.get(&"name".into());
         assert_eq!(value, Some(Value::String(String::from("whatever"))));
         Ok(())
     }
@@ -549,7 +548,7 @@
             default: Some(serde_json::from_str(DEFAULT)?),
             runtime: None,
         };
-        let value = test.get(&"field that does not exist".into(), &identity);
+        let value = test.get(&"field that does not exist".into());
         assert!(value.is_none());
         Ok(())
     }
@@ -564,7 +563,7 @@
             runtime: None,
         };
         test.set(&("name", &ConfigLevel::User).into(), Value::String(String::from("user-test")))?;
-        let value = test.get(&"name".into(), &identity);
+        let value = test.get(&"name".into());
         assert!(value.is_some());
         assert_eq!(value.unwrap(), Value::String(String::from("user-test")));
         Ok(())
@@ -583,28 +582,19 @@
             &("name", &ConfigLevel::User).into(),
             Value::String(String::from("user-test1"))
         )?);
-        assert_eq!(
-            test.get(&"name".into(), &identity).unwrap(),
-            Value::String(String::from("user-test1"))
-        );
+        assert_eq!(test.get(&"name".into()).unwrap(), Value::String(String::from("user-test1")));
 
         assert!(!test.set(
             &("name", &ConfigLevel::User).into(),
             Value::String(String::from("user-test1"))
         )?);
-        assert_eq!(
-            test.get(&"name".into(), &identity).unwrap(),
-            Value::String(String::from("user-test1"))
-        );
+        assert_eq!(test.get(&"name".into()).unwrap(), Value::String(String::from("user-test1")));
 
         assert!(test.set(
             &("name", &ConfigLevel::User).into(),
             Value::String(String::from("user-test2"))
         )?);
-        assert_eq!(
-            test.get(&"name".into(), &identity).unwrap(),
-            Value::String(String::from("user-test2"))
-        );
+        assert_eq!(test.get(&"name".into()).unwrap(), Value::String(String::from("user-test2")));
 
         Ok(())
     }
@@ -613,26 +603,26 @@
     fn test_set_build_from_none() -> Result<()> {
         let mut test =
             Config { user: None, build: None, global: None, default: None, runtime: None };
-        let value_none = test.get(&"name".into(), &identity);
+        let value_none = test.get(&"name".into());
         assert!(value_none.is_none());
         let error_set = test
             .set(&("name", &ConfigLevel::Default).into(), Value::String(String::from("default")));
         assert!(error_set.is_err(), "Should not be able to set default values at runtime");
-        let value_default = test.get(&"name".into(), &identity);
+        let value_default = test.get(&"name".into());
         assert!(
             value_default.is_none(),
             "Default value should be unset after failed attempt to set it"
         );
         test.set(&("name", &ConfigLevel::Global).into(), Value::String(String::from("global")))?;
-        let value_global = test.get(&"name".into(), &identity);
+        let value_global = test.get(&"name".into());
         assert!(value_global.is_some());
         assert_eq!(value_global.unwrap(), Value::String(String::from("global")));
         test.set(&("name", &ConfigLevel::Build).into(), Value::String(String::from("build")))?;
-        let value_build = test.get(&"name".into(), &identity);
+        let value_build = test.get(&"name".into());
         assert!(value_build.is_some());
         assert_eq!(value_build.unwrap(), Value::String(String::from("build")));
         test.set(&("name", &ConfigLevel::User).into(), Value::String(String::from("user")))?;
-        let value_user = test.get(&"name".into(), &identity);
+        let value_user = test.get(&"name".into());
         assert!(value_user.is_some());
         assert_eq!(value_user.unwrap(), Value::String(String::from("user")));
         Ok(())
@@ -648,20 +638,20 @@
             runtime: None,
         };
         test.remove(&("name", &ConfigLevel::User).into())?;
-        let user_value = test.get(&"name".into(), &identity);
+        let user_value = test.get(&"name".into());
         assert!(user_value.is_some());
         assert_eq!(user_value.unwrap(), Value::String(String::from("Build")));
         test.remove(&("name", &ConfigLevel::Build).into())?;
-        let global_value = test.get(&"name".into(), &identity);
+        let global_value = test.get(&"name".into());
         assert!(global_value.is_some());
         assert_eq!(global_value.unwrap(), Value::String(String::from("Global")));
         test.remove(&("name", &ConfigLevel::Global).into())?;
-        let default_value = test.get(&"name".into(), &identity);
+        let default_value = test.get(&"name".into());
         assert!(default_value.is_some());
         assert_eq!(default_value.unwrap(), Value::String(String::from("Default")));
         let error_removed = test.remove(&("name", &ConfigLevel::Default).into());
         assert!(error_removed.is_err(), "Should not be able to remove a default value");
-        let default_value = test.get(&"name".into(), &identity);
+        let default_value = test.get(&"name".into());
         assert_eq!(
             default_value,
             Some(Value::String(String::from("Default"))),
@@ -674,7 +664,7 @@
     #[test]
     fn test_default() {
         let test = Config::new(None, None, None, None);
-        let default_value = test.get(&"log.enabled".into(), &identity);
+        let default_value = test.get(&"log.enabled".into());
         assert_eq!(
             default_value.unwrap(),
             Value::Array(vec![Value::String("$FFX_LOG_ENABLED".to_string()), Value::Bool(true)])
@@ -704,11 +694,13 @@
     }
 
     fn test_map(value: Value) -> Option<Value> {
-        if value == "TEST_MAP".to_string() {
-            Some(Value::String("passed".to_string()))
-        } else {
-            Some(Value::String("failed".to_string()))
-        }
+        value
+            .as_str()
+            .map(|s| match s {
+                "TEST_MAP" => Value::String("passed".to_string()),
+                _ => Value::String("failed".to_string()),
+            })
+            .or(Some(value))
     }
 
     #[test]
@@ -722,9 +714,9 @@
         };
         let test_mapping = "TEST_MAP".to_string();
         let test_passed = "passed".to_string();
-        let mapped_value = test.get(&"name".into(), &test_map);
+        let mapped_value = test.get(&"name".into()).as_ref().recursive_map(&test_map);
         assert_eq!(mapped_value, Some(Value::String(test_passed)));
-        let identity_value = test.get(&"name".into(), &identity);
+        let identity_value = test.get(&"name".into());
         assert_eq!(identity_value, Some(Value::String(test_mapping)));
         Ok(())
     }
@@ -738,7 +730,7 @@
             default: None,
             runtime: Some(serde_json::from_str(NESTED)?),
         };
-        let value = test.get(&"name.nested".into(), &identity);
+        let value = test.get(&"name.nested".into());
         assert_eq!(value, Some(Value::String("Nested".to_string())));
         Ok(())
     }
@@ -752,7 +744,7 @@
             default: Some(serde_json::from_str(DEFAULT)?),
             runtime: Some(serde_json::from_str(NESTED)?),
         };
-        let value = test.get(&"name".into(), &identity);
+        let value = test.get(&"name".into());
         assert_eq!(value, Some(serde_json::from_str("{\"nested\": \"Nested\"}")?));
         Ok(())
     }
@@ -766,7 +758,7 @@
             default: Some(serde_json::from_str(NESTED)?),
             runtime: Some(serde_json::from_str(RUNTIME)?),
         };
-        let value = test.get(&"name.nested".into(), &identity);
+        let value = test.get(&"name.nested".into());
         assert_eq!(value, Some(Value::String("Nested".to_string())));
         Ok(())
     }
@@ -780,7 +772,7 @@
             default: Some(serde_json::from_str(NESTED)?),
             runtime: Some(serde_json::from_str(DEEP)?),
         };
-        let value = test.get(&"name.nested".into(), &test_map);
+        let value = test.get(&"name.nested".into()).as_ref().recursive_map(&test_map);
         assert_eq!(value, Some(serde_json::from_str("{\"deep\": {\"name\": \"passed\"}}")?));
         Ok(())
     }
@@ -790,7 +782,7 @@
         let mut test =
             Config { user: None, build: None, global: None, default: None, runtime: None };
         test.set(&("name.nested", &ConfigLevel::User).into(), Value::Bool(false))?;
-        let nested_value = test.get(&"name".into(), &identity);
+        let nested_value = test.get(&"name".into());
         assert_eq!(nested_value, Some(serde_json::from_str("{\"nested\": false}")?));
         Ok(())
     }
@@ -809,7 +801,7 @@
            "nested": "Nested",
            "updated": true
         });
-        let nested_value = test.get(&"name".into(), &identity);
+        let nested_value = test.get(&"name".into());
         assert_eq!(nested_value, Some(expected));
         Ok(())
     }
@@ -827,10 +819,10 @@
         let expected = json!({
            "updated": true
         });
-        let nested_value = test.get(&"name".into(), &identity);
+        let nested_value = test.get(&"name".into());
         assert_eq!(nested_value, Some(expected));
         test.set(&("name.updated", &ConfigLevel::User).into(), serde_json::from_str(NESTED)?)?;
-        let nested_value = test.get(&"name.updated.name.nested".into(), &identity);
+        let nested_value = test.get(&"name.updated.name.nested".into());
         assert_eq!(nested_value, Some(Value::String(String::from("Nested"))));
         Ok(())
     }
@@ -868,7 +860,7 @@
             runtime: None,
         };
         test.remove(&("name.nested.deep.name", &ConfigLevel::User).into())?;
-        let value = test.get(&"name".into(), &identity);
+        let value = test.get(&"name".into());
         assert_eq!(value, None);
         Ok(())
     }
@@ -883,7 +875,7 @@
             runtime: None,
         };
         test.remove(&("name.nested", &ConfigLevel::User).into())?;
-        let value = test.get(&"name".into(), &identity);
+        let value = test.get(&"name".into());
         assert_eq!(value, None);
         Ok(())
     }
@@ -897,7 +889,7 @@
             default: Some(serde_json::from_str(DEFAULT)?),
             runtime: Some(serde_json::from_str(RUNTIME)?),
         };
-        let value = test.get(&("name", &SelectMode::All).into(), &identity);
+        let value = test.get(&("name", &SelectMode::All).into());
         match value {
             Some(Value::Array(v)) => {
                 assert_eq!(v.len(), 5);
diff --git a/src/developer/ffx/plugins/config/src/args.rs b/src/developer/ffx/plugins/config/src/args.rs
index 00eda08..060377a 100644
--- a/src/developer/ffx/plugins/config/src/args.rs
+++ b/src/developer/ffx/plugins/config/src/args.rs
@@ -58,6 +58,7 @@
 pub enum MappingMode {
     Raw,
     Substitute,
+    File,
 }
 
 #[derive(FromArgs, Debug, PartialEq)]
@@ -78,8 +79,8 @@
         default = "MappingMode::Substitute",
         short = 'p'
     )]
-    /// how to process results. Possible values are "r/raw", and "s/sub/substitute".  Defaults
-    /// to "substitute". Currently only supported if a name is given.
+    /// how to process results. Possible values are "r/raw", "s/sub/substitute", or "f/file".
+    /// Defaults to "substitute". Currently only supported if a name is given.
     pub process: MappingMode,
 
     #[argh(option, from_str_fn(parse_mode), default = "SelectMode::First", short = 's')]
@@ -203,7 +204,10 @@
     match value {
         "r" | "raw" => Ok(MappingMode::Raw),
         "s" | "sub" | "substitute" => Ok(MappingMode::Substitute),
-        _ => Err(String::from("Unrecognized value. Possible values are \"raw\", \"sub\".")),
+        "f" | "file" => Ok(MappingMode::File),
+        _ => Err(String::from(
+            "Unrecognized value. Possible values are \"raw\", \"sub\", or \"file\".",
+        )),
     }
 }
 
diff --git a/src/developer/ffx/plugins/config/src/lib.rs b/src/developer/ffx/plugins/config/src/lib.rs
index f84fbd6..c2b8813 100644
--- a/src/developer/ffx/plugins/config/src/lib.rs
+++ b/src/developer/ffx/plugins/config/src/lib.rs
@@ -6,8 +6,8 @@
     anyhow::{anyhow, Context, Result},
     errors::{ffx_bail, ffx_bail_with_code},
     ffx_config::{
-        add, api::query::ConfigQuery, api::ConfigError, env_file, environment::Environment, get,
-        print_config, raw, remove, set, set_metrics_status, show_metrics_status, ConfigLevel,
+        add, api::query::ConfigQuery, api::ConfigError, env_file, environment::Environment, file,
+        get, print_config, raw, remove, set, set_metrics_status, show_metrics_status, ConfigLevel,
     },
     ffx_config_plugin_args::{
         AddCommand, AnalyticsCommand, AnalyticsControlCommand, ConfigCommand, EnvAccessCommand,
@@ -78,6 +78,10 @@
                 let value: std::result::Result<Vec<Value>, _> = get(query).await;
                 output_array(writer, value)
             }
+            MappingMode::File => {
+                let value = file(query).await?;
+                output(writer, value)
+            }
         },
         None => print_config(writer, &get_cmd.build_dir).await,
     }