Repository: tailcallhq/tailcall (Rust)

Issue:
Add an `--out` param to the `check` command
The `check` command currently composes multiple files into one configuration and then validates the composed file.

We want to extend this capability to also output the composed file. 

**Example usage**

```bash
tailcall check a.yml b.graphql c.json --out=abc.graphql
```
In the above example because the output file's extension is `.graphql` we can print the generated configuration in .graphql format.

**Other Usage:**
This command can also be used to convert file from one format to another. 

```bash
tailcall check a.yml --out=a.graphql
```

**Additional Note**
This will turn out quite handy when we have many files that we wish to compose and review.


Discussion: /bounty 100$
/attempt #752
@A-N-uraag: 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.
@rajdip-b: 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.
/attempt #752
@etorresh: 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.



Fix (patch):
diff --git a/examples/jsonplaceholder.json b/examples/jsonplaceholder.json
index a3bb406860..1f137c6f35 100644
--- a/examples/jsonplaceholder.json
+++ b/examples/jsonplaceholder.json
@@ -1,9 +1,8 @@
 {
   "server": {
-    "port": 8000,
     "graphiql": true,
-    "queryValidation": false,
-    "hostname": "0.0.0.0"
+    "hostname": "0.0.0.0",
+    "port": 8000
   },
   "upstream": {
     "baseURL": "http://jsonplaceholder.typicode.com",
@@ -47,6 +46,18 @@
           "http": {
             "path": "/posts"
           }
+        },
+        "user": {
+          "type": "User",
+          "args": {
+            "id": {
+              "type": "Int",
+              "required": true
+            }
+          },
+          "http": {
+            "path": "/users/{{args.id}}"
+          }
         }
       }
     },
diff --git a/examples/jsonplaceholder.yml b/examples/jsonplaceholder.yml
index 4d694b36ae..c6224182f2 100644
--- a/examples/jsonplaceholder.yml
+++ b/examples/jsonplaceholder.yml
@@ -1,10 +1,9 @@
 server:
-  port: 8000
   graphiql: true
-  queryValidation: false
-  hostname: "0.0.0.0"
+  hostname: 0.0.0.0
+  port: 8000
 upstream:
-  baseURL: "http://jsonplaceholder.typicode.com"
+  baseURL: http://jsonplaceholder.typicode.com
   httpCache: true
 schema:
   query: Query
@@ -34,6 +33,14 @@ types:
         list: true
         http:
           path: /posts
+      user:
+        type: User
+        args:
+          id:
+            type: Int
+            required: true
+        http:
+          path: /users/{{args.id}}
   User:
     fields:
       email:
diff --git a/src/cli/command.rs b/src/cli/command.rs
index badd2203de..0df3adb606 100644
--- a/src/cli/command.rs
+++ b/src/cli/command.rs
@@ -42,6 +42,10 @@ pub enum Command {
     /// Display schema
     #[arg(short, long)]
     schema: bool,
+
+    /// Path of the file to save final configuration in
+    #[arg(short, long("out"))]
+    out_file_path: Option<String>,
   },
   /// Initialize a new project
   Init { file_path: String },
diff --git a/src/cli/tc.rs b/src/cli/tc.rs
index cd3096fd2b..3b0736ffa7 100644
--- a/src/cli/tc.rs
+++ b/src/cli/tc.rs
@@ -23,7 +23,8 @@ pub fn run() -> Result<()> {
       env_logger::Builder::new()
         .filter_level(log_level.unwrap_or(Level::Info).to_level_filter())
         .init();
-      let config = tokio::runtime::Runtime::new()?.block_on(async { Config::from_iter(file_paths.iter()).await })?;
+      let config =
+        tokio::runtime::Runtime::new()?.block_on(async { Config::read_from_files(file_paths.iter()).await })?;
       log::info!("N + 1: {}", config.n_plus_one().len().to_string());
       let runtime = Builder::new_multi_thread()
         .worker_threads(config.server.get_workers())
@@ -33,8 +34,9 @@ pub fn run() -> Result<()> {
       runtime.block_on(server.start())?;
       Ok(())
     }
-    Command::Check { file_path, n_plus_one_queries, schema } => {
-      let config = tokio::runtime::Runtime::new()?.block_on(async { Config::from_iter(file_path.iter()).await })?;
+    Command::Check { file_path, n_plus_one_queries, schema, out_file_path } => {
+      let config =
+        tokio::runtime::Runtime::new()?.block_on(async { Config::read_from_files(file_path.iter()).await })?;
       let blueprint = Blueprint::try_from(&config);
       match blueprint {
         Ok(blueprint) => {
@@ -42,6 +44,12 @@ pub fn run() -> Result<()> {
           if schema {
             display_schema(&blueprint);
           }
+          if let Some(out_file) = out_file_path {
+            tokio::runtime::Runtime::new()?.block_on(async { config.write_file(&out_file).await })?;
+            Fmt::display(Fmt::success(
+              &format!("Schema has been written to {}", out_file).to_string(),
+            ));
+          }
           Ok(())
         }
         Err(e) => Err(e.into()),
diff --git a/src/config/config.rs b/src/config/config.rs
index d0ed094c26..8dc032e8fb 100644
--- a/src/config/config.rs
+++ b/src/config/config.rs
@@ -11,13 +11,14 @@ use super::{Server, Upstream};
 use crate::config::from_document::from_document;
 use crate::config::reader::ConfigReader;
 use crate::config::source::Source;
+use crate::config::writer::ConfigWriter;
 use crate::config::{is_default, KeyValues};
 use crate::directive::DirectiveCodec;
 use crate::http::Method;
 use crate::json::JsonSchema;
 use crate::valid::Valid;
 
-#[derive(Serialize, Deserialize, Clone, Debug, Default, Setters, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, Default, Setters, PartialEq, Eq)]
 #[serde(rename_all = "camelCase")]
 pub struct Config {
   #[serde(default)]
@@ -28,7 +29,7 @@ pub struct Config {
   #[serde(default)]
   #[setters(skip)]
   pub types: BTreeMap<String, Type>,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub unions: BTreeMap<String, Union>,
 }
 impl Config {
@@ -83,8 +84,12 @@ impl Config {
     Ok(serde_yaml::to_string(self)?)
   }
 
-  pub fn to_json(&self) -> Result<String> {
-    Ok(serde_json::to_string(self)?)
+  pub fn to_json(&self, pretty: bool) -> Result<String> {
+    if pretty {
+      Ok(serde_json::to_string_pretty(self)?)
+    } else {
+      Ok(serde_json::to_string(self)?)
+    }
   }
 
   pub fn to_document(&self) -> ServiceDocument {
@@ -123,21 +128,28 @@ impl Config {
 
     Self { server, upstream, types, schema, unions }
   }
+
+  pub async fn write_file(self, filename: &String) -> Result<()> {
+    let config_writer = ConfigWriter::init(self);
+
+    config_writer.write(filename).await
+  }
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
 pub struct Type {
   pub fields: BTreeMap<String, Field>,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub added_fields: Vec<AddField>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub doc: Option<String>,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub interface: bool,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub implements: BTreeSet<String>,
-  #[serde(rename = "enum", default)]
+  #[serde(rename = "enum", default, skip_serializing_if = "is_default")]
   pub variants: Option<BTreeSet<String>>,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub scalar: bool,
 }
 
@@ -189,11 +201,13 @@ fn merge_unions(
   self_unions
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, Default, Setters, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, Default, Setters, PartialEq, Eq)]
 #[setters(strip_option)]
 pub struct RootSchema {
   pub query: Option<String>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub mutation: Option<String>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub subscription: Option<String>,
 }
 
@@ -208,27 +222,30 @@ impl RootSchema {
   }
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, Default, Setters, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, Default, Setters, PartialEq, Eq)]
 #[setters(strip_option)]
 pub struct Field {
-  #[serde(rename = "type")]
+  #[serde(rename = "type", default, skip_serializing_if = "is_default")]
   pub type_of: String,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub list: bool,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub required: bool,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub list_type_required: bool,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub args: BTreeMap<String, Arg>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub doc: Option<String>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub modify: Option<Modify>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub http: Option<Http>,
-  #[serde(rename = "unsafe")]
+  #[serde(rename = "unsafe", default, skip_serializing_if = "is_default")]
   pub unsafe_operation: Option<Unsafe>,
-
-  #[serde(rename = "const")]
+  #[serde(rename = "const", default, skip_serializing_if = "is_default")]
   pub const_field: Option<Const>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub graphql: Option<GraphQL>,
 }
 
@@ -281,38 +298,41 @@ impl Field {
   }
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub struct Unsafe {
   pub script: String,
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub struct Modify {
+  #[serde(default, skip_serializing_if = "is_default")]
   pub name: Option<String>,
-  #[serde(default)]
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub omit: bool,
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub struct Inline {
   pub path: Vec<String>,
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub struct Arg {
   #[serde(rename = "type")]
   pub type_of: String,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub list: bool,
-  #[serde(default)]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub required: bool,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub doc: Option<String>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub modify: Option<Modify>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub default_value: Option<Value>,
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub struct Union {
   pub types: BTreeSet<String>,
   pub doc: Option<String>,
@@ -325,39 +345,37 @@ impl Union {
   }
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
 pub struct Http {
   pub path: String,
-  #[serde(default)]
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub method: Method,
-  #[serde(default)]
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub query: KeyValues,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub input: Option<JsonSchema>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub output: Option<JsonSchema>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub body: Option<String>,
-  #[serde(rename = "baseURL")]
+  #[serde(rename = "baseURL", default, skip_serializing_if = "is_default")]
   pub base_url: Option<String>,
-  #[serde(default)]
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub headers: KeyValues,
-  #[serde(default)]
-  #[serde(rename = "groupBy", skip_serializing_if = "is_default")]
+  #[serde(rename = "groupBy", default, skip_serializing_if = "is_default")]
   pub group_by: Vec<String>,
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
 pub struct GraphQL {
   pub name: String,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub args: Option<KeyValues>,
   #[serde(rename = "baseURL")]
   pub base_url: Option<String>,
-  #[serde(default)]
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub headers: KeyValues,
-  #[serde(default)]
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub batch: bool,
 }
 
@@ -378,12 +396,12 @@ impl Display for GraphQLOperationType {
   }
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub struct Const {
   pub data: Value,
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub struct AddField {
   pub name: String,
   pub path: Vec<String>,
@@ -413,11 +431,12 @@ impl Config {
       Source::Yml => Ok(Config::from_yaml(schema)?),
     }
   }
+
   pub fn n_plus_one(&self) -> Vec<Vec<(String, String)>> {
     super::n_plus_one::n_plus_one(self)
   }
 
-  pub async fn from_iter<Iter>(file_paths: Iter) -> Result<Config>
+  pub async fn read_from_files<Iter>(file_paths: Iter) -> Result<Config>
   where
     Iter: Iterator,
     Iter::Item: AsRef<str>,
diff --git a/src/config/mod.rs b/src/config/mod.rs
index ec7c579e21..49b3b65219 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -7,6 +7,7 @@ mod n_plus_one;
 mod reader;
 mod server;
 mod source;
+mod writer;
 
 pub use config::*;
 pub use key_values::*;
diff --git a/src/config/server.rs b/src/config/server.rs
index f4ffaa7626..fddafe77d6 100644
--- a/src/config/server.rs
+++ b/src/config/server.rs
@@ -5,29 +5,42 @@ use serde::{Deserialize, Serialize};
 
 use crate::config::{is_default, KeyValues};
 
-#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
 #[serde(rename_all = "camelCase")]
 pub struct Server {
+  #[serde(default, skip_serializing_if = "is_default")]
   pub apollo_tracing: Option<bool>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub cache_control_header: Option<bool>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub graphiql: Option<bool>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub introspection: Option<bool>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub query_validation: Option<bool>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub response_validation: Option<bool>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub batch_requests: Option<bool>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub global_response_timeout: Option<i64>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub workers: Option<usize>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub hostname: Option<String>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub port: Option<u16>,
   #[serde(default, skip_serializing_if = "is_default")]
   pub vars: KeyValues,
   #[serde(skip_serializing_if = "is_default", default)]
   pub response_headers: KeyValues,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub version: Option<HttpVersion>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub cert: Option<String>,
+  #[serde(default, skip_serializing_if = "is_default")]
   pub key: Option<String>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub pipeline_flush: Option<bool>,
 }
 
@@ -139,34 +152,33 @@ pub struct Proxy {
 #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Setters, Default)]
 #[serde(rename_all = "camelCase", default)]
 pub struct Upstream {
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub pool_idle_timeout: Option<u64>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub pool_max_idle_per_host: Option<usize>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub keep_alive_interval: Option<u64>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub keep_alive_timeout: Option<u64>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub keep_alive_while_idle: Option<bool>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub proxy: Option<Proxy>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub connect_timeout: Option<u64>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub timeout: Option<u64>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub tcp_keep_alive: Option<u64>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub user_agent: Option<String>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub allowed_headers: Option<BTreeSet<String>>,
-  #[serde(rename = "baseURL")]
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(rename = "baseURL", default, skip_serializing_if = "is_default")]
   pub base_url: Option<String>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub http_cache: Option<bool>,
-  #[serde(skip_serializing_if = "is_default")]
+  #[serde(default, skip_serializing_if = "is_default")]
   pub batch: Option<Batch>,
 }
 
diff --git a/src/config/writer.rs b/src/config/writer.rs
new file mode 100644
index 0000000000..971d5f626d
--- /dev/null
+++ b/src/config/writer.rs
@@ -0,0 +1,28 @@
+use anyhow::Result;
+use tokio::fs::File;
+use tokio::io::AsyncWriteExt;
+
+use crate::config::{Config, Source};
+
+pub struct ConfigWriter {
+  config: Config,
+}
+
+impl ConfigWriter {
+  pub fn init(config: Config) -> Self {
+    Self { config }
+  }
+
+  pub async fn write(&self, filename: &String) -> Result<()> {
+    let contents = match Source::detect(filename)? {
+      Source::GraphQL => self.config.to_sdl(),
+      Source::Json => self.config.to_json(true)?,
+      Source::Yml => self.config.to_yaml()?,
+    };
+
+    let mut file = File::create(filename).await?;
+    file.write_all(contents.as_bytes()).await?;
+
+    Ok(())
+  }
+}
diff --git a/src/json/json_schema.rs b/src/json/json_schema.rs
index d5fa9b87cf..7ead11e995 100644
--- a/src/json/json_schema.rs
+++ b/src/json/json_schema.rs
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
 
 use crate::valid::Valid;
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename = "schema")]
 pub enum JsonSchema {
   Obj(HashMap<String, JsonSchema>),