Repository: tailcallhq/tailcall (Rust)

Issue:
Bug: Invalid Configuration Error
Given the following input 

1. Create the following `config.graphql` with a port that's not a number.
  ```graphql
  schema @server(port: "8000", baseURL: "http://jsonplaceholder.typicode.com") {
    query: Query
  }
  
  type Query {
    posts: [Post] @http(path: "/posts")
  }
  
  type Post {
    id: Int!
    userId: Int!
    title: String!
    body: String!
  }
  ```

2. Start the server using `tc start ./config.graphql` or `cargo run -- start ./config.graphql`

**Expectation**
A validation error should be shown for the value of the port.

**Actual**
```bash
Error: Invalid Configuration
Caused by:
  • No base URL defined [at Query.posts.@http]
```


Discussion: /bounty $120
💎 **$120** bounty created by tusharmath
🙋 If you start working on this, comment `/attempt #417` to notify everyone
👉 To claim this bounty, submit a pull request that includes the text `/claim #417` somewhere in its body
📝 Before proceeding, please make sure you can [**receive payouts**](https://docs.algora.io/bounties/payments#country-support) in your country
💵 Payment arrives in your account 2-5 days after the bounty is rewarded
💯 You keep 100% of the bounty award
🙏 Thank you for contributing to tailcallhq/tailcall!
🙋‍♂️ Join our [discord](https://discord.gg/7fseDEXUNU) channel if you need help.

| Attempt | Started (GMT+0) | Solution |
|---|---|---|
| 🟢 @bharattech | Oct 4, 2023, 8:06:38 AM | [#423](https://github.com/tailcallhq/tailcall/pull/423) |
/attempt #417

<details id="algora-options">
  <summary>Options</summary>
  <ul>
    <li>
      <a href="https://console.algora.io/api/bounties/clnbf0uxi0002l20fxafvega6/cancel-attempt">
        Cancel my attempt
      </a>
    </li>
  </ul>
</details>

Hi @tusharmath 
I am working on this issue on priority, let me know your thoughts
You can collaborate with @amitksingh1490 i think he is looking at it. 
Hi @tusharmath @amitksingh1490 

Could you kindly review my code and provide your feedback?
@bharattech Do you wish to collaborate on this here 
https://github.com/tailcallhq/tailcall/pull/425

Hi @amitksingh1490 
Yes, I'm open to collaboration. Can you please provide more details, How this works?



Fix (patch):
diff --git a/src/config/config.rs b/src/config/config.rs
index e498444148..d4341690b3 100644
--- a/src/config/config.rs
+++ b/src/config/config.rs
@@ -10,6 +10,7 @@ use super::{Proxy, Server};
 use crate::batch::Batch;
 use crate::http::Method;
 use crate::json::JsonSchema;
+use crate::valid::{Valid, ValidExtensions};
 
 fn is_default<T: Default + Eq>(val: &T) -> bool {
   *val == T::default()
@@ -262,10 +263,12 @@ impl Config {
     Ok(serde_yaml::from_str(yaml)?)
   }
 
-  pub fn from_sdl(sdl: &str) -> Result<Self> {
-    let doc = async_graphql::parser::parse_schema(sdl)?;
-
-    Ok(Config::from(doc))
+  pub fn from_sdl(sdl: &str) -> Valid<Self, String> {
+    let doc = async_graphql::parser::parse_schema(sdl);
+    match doc {
+      Ok(doc) => Config::try_from(doc),
+      Err(e) => Valid::fail(e.to_string()),
+    }
   }
 
   pub fn n_plus_one(&self) -> Vec<Vec<(String, String)>> {
diff --git a/src/config/from_document.rs b/src/config/from_document.rs
index 86c354b3dd..1bbb851c56 100644
--- a/src/config/from_document.rs
+++ b/src/config/from_document.rs
@@ -13,11 +13,15 @@ use crate::batch::Batch;
 use crate::config;
 use crate::config::{Config, GraphQL, RootSchema, Server, Union};
 use crate::directive::DirectiveCodec;
+use crate::valid::{Valid as ValidDefault, ValidExtensions, ValidationError};
 
-fn from_document(doc: ServiceDocument) -> Config {
-  Config { server: schema_definition(&doc).and_then(server).unwrap_or_default(), graphql: graphql(&doc) }
+type Valid<A> = ValidDefault<A, String>;
+fn from_document(doc: ServiceDocument) -> Valid<Config> {
+  let schema_definition = schema_definition(&doc)?;
+
+  Valid::Ok(Config { server: server(schema_definition)?, graphql: graphql(&doc)? })
 }
-fn graphql(doc: &ServiceDocument) -> GraphQL {
+fn graphql(doc: &ServiceDocument) -> Valid<GraphQL> {
   let type_definitions: Vec<_> = doc
     .definitions
     .iter()
@@ -27,22 +31,30 @@ fn graphql(doc: &ServiceDocument) -> GraphQL {
     })
     .collect();
 
-  let root_schema = schema_definition(doc).map_or_else(RootSchema::default, to_root_schema);
+  let root_schema = to_root_schema(schema_definition(doc)?);
 
-  GraphQL { schema: root_schema, types: to_types(&type_definitions), unions: to_union_types(&type_definitions) }
+  Valid::Ok(GraphQL {
+    schema: root_schema,
+    types: to_types(&type_definitions),
+    unions: to_union_types(&type_definitions),
+  })
 }
 
-fn schema_definition(doc: &ServiceDocument) -> Option<&SchemaDefinition> {
-  doc.definitions.iter().find_map(|def| match def {
+fn schema_definition(doc: &ServiceDocument) -> Valid<&SchemaDefinition> {
+  let p = doc.definitions.iter().find_map(|def| match def {
     TypeSystemDefinition::Schema(schema_definition) => Some(&schema_definition.node),
     _ => None,
-  })
+  });
+  p.map_or_else(
+    || Valid::fail("schema not found".to_string()).trace("schema"),
+    Valid::Ok,
+  )
 }
-fn server(schema_definition: &SchemaDefinition) -> Option<Server> {
-  let mut server = None;
+fn server(schema_definition: &SchemaDefinition) -> Valid<Server> {
+  let mut server = Valid::Ok(Server::default());
   for directive in schema_definition.directives.iter() {
     if directive.node.name.node == "server" {
-      server = Server::from_directive(&directive.node).ok();
+      server = Server::from_directive(&directive.node)
     }
   }
   server
@@ -300,8 +312,10 @@ impl HasName for InputValueDefinition {
   }
 }
 
-impl From<ServiceDocument> for Config {
-  fn from(doc: ServiceDocument) -> Self {
-    from_document(doc)
+impl TryFrom<ServiceDocument> for Config {
+  type Error = ValidationError<String>;
+
+  fn try_from(value: ServiceDocument) -> Valid<Self> {
+    from_document(value)
   }
 }
diff --git a/src/directive.rs b/src/directive.rs
index 8d754bf220..c3a3ffd897 100644
--- a/src/directive.rs
+++ b/src/directive.rs
@@ -1,23 +1,30 @@
-use anyhow::Result;
 use async_graphql::parser::types::ConstDirective;
 use async_graphql::{Name, Pos, Positioned};
 use serde::{Deserialize, Serialize};
 use serde_json::{Map, Value};
 use serde_path_to_error::deserialize;
 
-use crate::valid::ValidationError;
+use crate::valid::{Valid, ValidationError, VectorExtension};
 
 fn pos<A>(a: A) -> Positioned<A> {
   Positioned::new(a, Pos::default())
 }
 
-fn from_directive<'a, A: Deserialize<'a>>(directive: &'a ConstDirective) -> Result<A> {
+fn from_directive<'a, A: Deserialize<'a>>(directive: &'a ConstDirective) -> Valid<A, String> {
   let mut map = Map::new();
-  for (k, v) in directive.arguments.clone() {
-    map.insert(k.node.as_str().to_string(), serde_json::to_value(&v.node)?);
-  }
-
-  Ok(deserialize(Value::Object(map)).map_err(|e| ValidationError::from(e).trace(directive.name.node.as_str()))?)
+  let p = directive.arguments.iter().validate_all(|(k, v)| {
+    Valid::Ok((
+      k.node.as_str().to_string(),
+      serde_json::to_value(&v.node)
+        .map_err(|e| ValidationError::new(e.to_string()).trace(format!("@{}", directive.name.node).as_str()))?,
+    ))
+  })?;
+  p.into_iter().for_each(|(k, v)| {
+    map.insert(k, v);
+  });
+
+  deserialize(Value::Object(map))
+    .map_err(|e| ValidationError::from(e).trace(format!("@{}", directive.name.node).as_str()))
 }
 
 fn to_directive<A: Serialize>(a: &A, name: String) -> ConstDirective {
@@ -37,12 +44,12 @@ fn to_directive<A: Serialize>(a: &A, name: String) -> ConstDirective {
 }
 
 pub trait DirectiveCodec<'a, A> {
-  fn from_directive(directive: &'a ConstDirective) -> Result<A>;
+  fn from_directive(directive: &'a ConstDirective) -> Valid<A, String>;
   fn to_directive(&'a self, name: String) -> ConstDirective;
 }
 
 impl<'a, A: Deserialize<'a> + Serialize> DirectiveCodec<'a, A> for A {
-  fn from_directive(directive: &'a ConstDirective) -> Result<A> {
+  fn from_directive(directive: &'a ConstDirective) -> Valid<A, String> {
     from_directive(directive)
   }
 
diff --git a/src/valid/error.rs b/src/valid/error.rs
index dc8eabc85c..ef83f164ef 100644
--- a/src/valid/error.rs
+++ b/src/valid/error.rs
@@ -100,7 +100,9 @@ impl From<serde_path_to_error::Error<serde_json::Error>> for ValidationError<Str
     }
 
     let re = Regex::new(r" at line \d+ column \d+$").unwrap();
-    let message = re.replace(error.inner().to_string().as_str(), "").into_owned();
+    let message = re
+      .replace(format!("Parsing failed because of {}", error.inner()).as_str(), "")
+      .into_owned();
 
     ValidationError(vec![Cause::new(message).trace(trace.into())])
   }
@@ -121,7 +123,9 @@ mod tests {
   fn test_from_serde_error() {
     let foo = &mut serde_json::Deserializer::from_str("{ \"a\": true }");
     let actual = ValidationError::from(serde_path_to_error::deserialize::<_, Foo>(foo).unwrap_err());
-    let expected = ValidationError::new("invalid type: boolean `true`, expected i32".to_string()).trace("a");
+    let expected =
+      ValidationError::new("Parsing failed because of invalid type: boolean `true`, expected i32".to_string())
+        .trace("a");
     assert_eq!(actual, expected);
   }
 }