Repository: tailcallhq/tailcall (Rust)

Issue:
Support for reading environment variables
In our configuration, wherever we support mustache templates, we should support the ability to read environment variables.

**Example Usage:**
```graphql
  schema 
    @server(vars: [{key: "auth", value: "{{env.AUTH_TOKEN}}"}])
    @upstream(baseURL: "http://{{env.UPSTREAM_HOST_NAME}}:{{env.UPSTREAM_PORT}}") {
  query: Query
}
```

**Technical Requirements:**
- Environment variables should be read at the time of starting the server.
- Tests should be added in `tests/http` in yml format.


Discussion: /bounty 130
Can I work on this?
hey @tusharmath may i attempt this one.
/attempt #790

@nermalcat69: The Tailcall Inc. team prefers to assign a single contributor to the issue rather than let anyone attempt it right away. We recommend waiting for a confirmation from a member before getting started.
May I attempt this one?
@tusharmath
I am unable to make up with this. Please assign this to the next person or someone who can work on this
Hey @amitksingh1490 @tusharmath, for this one IMO we should have a new operator like `@env` for setting the env file path (or files path potentially). What do you think guys?

And btw, I'd love to work on this one :smile:
@ologbonowiwi Env variables are available to read via rust's native API I don't think we need another `@env` operator. Have a look at https://www.thorsten-hans.com/working-with-environment-variables-in-rust/



Fix (patch):
diff --git a/src/blueprint/from_config/operators/http.rs b/src/blueprint/from_config/operators/http.rs
index 32ebea4ba2..593acae3f9 100644
--- a/src/blueprint/from_config/operators/http.rs
+++ b/src/blueprint/from_config/operators/http.rs
@@ -94,8 +94,8 @@ impl<'a> MustachePartsValidator<'a> {
           return Valid::fail(format!("var '{tail}' is not set in the server config"));
         }
       }
-      "headers" => {
-        // "headers" refers to the header values known at runtime, which we can't
+      "headers" | "env" => {
+        // "headers" and "env" refers to values known at runtime, which we can't
         // validate here
       }
       _ => {
diff --git a/src/http/request_context.rs b/src/http/request_context.rs
index 19076ed8bf..3cddac7300 100644
--- a/src/http/request_context.rs
+++ b/src/http/request_context.rs
@@ -1,3 +1,4 @@
+use std::collections::HashMap;
 use std::num::NonZeroU64;
 use std::sync::{Arc, Mutex};
 
@@ -33,6 +34,7 @@ pub struct RequestContext {
   pub grpc_data_loaders: Arc<Vec<DataLoader<grpc::DataLoaderRequest, GrpcDataLoader>>>,
   min_max_age: Arc<Mutex<Option<i32>>>,
   cache_public: Arc<Mutex<Option<bool>>>,
+  pub env_vars: Arc<HashMap<String, String>>,
 }
 
 impl Default for RequestContext {
@@ -56,6 +58,7 @@ impl Default for RequestContext {
       grpc_data_loaders: Arc::new(vec![]),
       min_max_age: Arc::new(Mutex::new(None)),
       cache_public: Arc::new(Mutex::new(None)),
+      env_vars: Arc::new(HashMap::new()),
     }
   }
 }
@@ -133,6 +136,7 @@ impl From<&ServerContext> for RequestContext {
       grpc_data_loaders: server_ctx.grpc_data_loaders.clone(),
       min_max_age: Arc::new(Mutex::new(None)),
       cache_public: Arc::new(Mutex::new(None)),
+      env_vars: server_ctx.env_vars.clone(),
     }
   }
 }
diff --git a/src/http/server_context.rs b/src/http/server_context.rs
index 45d7c86a7d..25935b492f 100644
--- a/src/http/server_context.rs
+++ b/src/http/server_context.rs
@@ -1,3 +1,4 @@
+use std::collections::HashMap;
 use std::sync::Arc;
 
 use async_graphql::dynamic;
@@ -23,6 +24,7 @@ pub struct ServerContext {
   pub gql_data_loaders: Arc<Vec<DataLoader<DataLoaderRequest, GraphqlDataLoader>>>,
   pub cache: ChronoCache<u64, ConstValue>,
   pub grpc_data_loaders: Arc<Vec<DataLoader<grpc::DataLoaderRequest, GrpcDataLoader>>>,
+  pub env_vars: Arc<HashMap<String, String>>,
 }
 
 impl ServerContext {
@@ -105,6 +107,7 @@ impl ServerContext {
     }
 
     let schema = blueprint.to_schema();
+    let env = std::env::vars().collect();
 
     ServerContext {
       schema,
@@ -115,6 +118,7 @@ impl ServerContext {
       gql_data_loaders: Arc::new(gql_data_loaders),
       cache: ChronoCache::new(),
       grpc_data_loaders: Arc::new(grpc_data_loaders),
+      env_vars: Arc::new(env),
     }
   }
 }
diff --git a/src/lambda/evaluation_context.rs b/src/lambda/evaluation_context.rs
index 65977cebfe..1dde1a3d04 100644
--- a/src/lambda/evaluation_context.rs
+++ b/src/lambda/evaluation_context.rs
@@ -49,6 +49,10 @@ impl<'a, Ctx: ResolverContextLike<'a>> EvaluationContext<'a, Ctx> {
     value.to_str().ok()
   }
 
+  pub fn env_var(&self, key: &str) -> Option<&str> {
+    self.req_ctx.env_vars.get(key).map(|v| v.as_str())
+  }
+
   pub fn var(&self, key: &str) -> Option<&str> {
     let vars = &self.req_ctx.server.vars;
 
diff --git a/src/mustache.rs b/src/mustache.rs
index 308332fafc..c8d24dacb1 100644
--- a/src/mustache.rs
+++ b/src/mustache.rs
@@ -78,15 +78,19 @@ impl Mustache {
 }
 
 fn parse_name(input: &str) -> IResult<&str, String> {
-  nom::combinator::map(
-    nom::sequence::tuple((
-      nom::character::complete::multispace0,
-      nom::character::complete::alpha1,
-      nom::character::complete::alphanumeric0,
-      nom::character::complete::multispace0,
-    )),
-    |(_, a, b, _)| format!("{}{}", a, b),
-  )(input)
+  let spaces = nom::character::complete::multispace0;
+  let alpha = nom::character::complete::alpha1;
+  let alphanumeric_or_underscore = nom::multi::many0(nom::branch::alt((
+    nom::character::complete::alphanumeric1,
+    nom::bytes::complete::tag("_"),
+  )));
+
+  let parser = nom::sequence::tuple((spaces, alpha, alphanumeric_or_underscore, spaces));
+
+  nom::combinator::map(parser, |(_, a, b, _)| {
+    let b: String = b.into_iter().collect();
+    format!("{}{}", a, b)
+  })(input)
 }
 
 fn parse_expression(input: &str) -> IResult<&str, Vec<String>> {
@@ -228,6 +232,27 @@ mod tests {
       let mustache = Mustache::parse("123").unwrap();
       assert_eq!(mustache, Mustache::from(vec![Segment::Literal("123".to_string())]));
     }
+
+    #[test]
+    fn parse_env_name() {
+      let result = Mustache::parse("{{env.FOO}}").unwrap();
+      assert_eq!(
+        result,
+        Mustache::from(vec![Segment::Expression(vec!["env".to_string(), "FOO".to_string()])])
+      );
+    }
+
+    #[test]
+    fn parse_env_with_underscores() {
+      let result = Mustache::parse("{{env.FOO_BAR}}").unwrap();
+      assert_eq!(
+        result,
+        Mustache::from(vec![Segment::Expression(vec![
+          "env".to_string(),
+          "FOO_BAR".to_string()
+        ])])
+      );
+    }
   }
   mod render {
     use std::borrow::Cow;
diff --git a/src/path.rs b/src/path.rs
index ef16686278..10803ea007 100644
--- a/src/path.rs
+++ b/src/path.rs
@@ -70,6 +70,7 @@ impl<'a, Ctx: ResolverContextLike<'a>> PathString for EvaluationContext<'a, Ctx>
       "args" => convert_value(ctx.arg(tail)?),
       "headers" => ctx.header(tail[0].as_ref()).map(|v| v.into()),
       "vars" => ctx.var(tail[0].as_ref()).map(|v| v.into()),
+      "env" => ctx.env_var(tail[0].as_ref()).map(|v| v.into()),
       _ => None,
     })
   }
@@ -88,6 +89,7 @@ impl<'a, Ctx: ResolverContextLike<'a>> PathGraphql for EvaluationContext<'a, Ctx
       "args" => Some(ctx.arg(tail)?.to_string()),
       "headers" => ctx.header(tail[0].as_ref()).map(|v| format!(r#""{v}""#)),
       "vars" => ctx.var(tail[0].as_ref()).map(|v| format!(r#""{v}""#)),
+      "env" => ctx.env_var(tail[0].as_ref()).map(|v| format!(r#""{v}""#)),
       _ => None,
     })
   }
@@ -97,7 +99,8 @@ impl<'a, Ctx: ResolverContextLike<'a>> PathGraphql for EvaluationContext<'a, Ctx
 mod tests {
   mod evaluation_context {
     use std::borrow::Cow;
-    use std::collections::BTreeMap;
+    use std::collections::{BTreeMap, HashMap};
+    use std::sync::Arc;
 
     use async_graphql::SelectionField;
     use async_graphql_value::{ConstValue as Value, Name, Number};
@@ -152,6 +155,14 @@ mod tests {
       map
     });
 
+    static TEST_ENV_VARS: Lazy<HashMap<String, String>> = Lazy::new(|| {
+      let mut map = HashMap::new();
+
+      map.insert("existing".to_owned(), "env".to_owned());
+
+      map
+    });
+
     struct MockGraphqlContext;
 
     impl<'a> ResolverContextLike<'a> for MockGraphqlContext {
@@ -174,6 +185,7 @@ mod tests {
       let mut req_ctx = RequestContext::default().req_headers(TEST_HEADERS.clone());
 
       req_ctx.server.vars = TEST_VARS.clone();
+      req_ctx.env_vars = Arc::new(TEST_ENV_VARS.clone());
 
       req_ctx
     });
@@ -232,6 +244,15 @@ mod tests {
         EVAL_CTX.path_string(&["vars"]),
         Some(Cow::Borrowed(r#"{"existing":"var"}"#))
       );
+
+      // envs
+      assert_eq!(EVAL_CTX.path_string(&["env", "existing"]), Some(Cow::Borrowed("env")));
+      assert_eq!(EVAL_CTX.path_string(&["env", "x-missing"]), None);
+
+      // other value types
+      assert_eq!(EVAL_CTX.path_string(&["foo", "key"]), None);
+      assert_eq!(EVAL_CTX.path_string(&["bar", "key"]), None);
+      assert_eq!(EVAL_CTX.path_string(&["baz", "key"]), None);
     }
 
     #[test]
@@ -272,6 +293,15 @@ mod tests {
       // vars
       assert_eq!(EVAL_CTX.path_graphql(&["vars", "existing"]), Some("\"var\"".to_owned()));
       assert_eq!(EVAL_CTX.path_graphql(&["vars", "missing"]), None);
+
+      // envs
+      assert_eq!(EVAL_CTX.path_graphql(&["env", "existing"]), Some("\"env\"".to_owned()));
+      assert_eq!(EVAL_CTX.path_graphql(&["env", "x-missing"]), None);
+
+      // other value types
+      assert_eq!(EVAL_CTX.path_graphql(&["foo", "key"]), None);
+      assert_eq!(EVAL_CTX.path_graphql(&["bar", "key"]), None);
+      assert_eq!(EVAL_CTX.path_graphql(&["baz", "key"]), None);
     }
   }
 }