Skip to content

Commit

Permalink
feat: making tailcall init interactive (#2267)
Browse files Browse the repository at this point in the history
  • Loading branch information
shashitnak committed Jul 1, 2024
1 parent 394496b commit 2fe46d4
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 81 deletions.
49 changes: 49 additions & 0 deletions src/cli/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ mod env;
mod file;
mod http;

use std::fs;
use std::hash::Hash;
use std::sync::Arc;

pub use http::NativeHttp;
use inquire::{Confirm, Select};

use crate::core::blueprint::Blueprint;
use crate::core::cache::InMemoryCache;
Expand Down Expand Up @@ -82,3 +84,50 @@ pub fn init(blueprint: &Blueprint) -> TargetRuntime {
worker: init_resolver_worker_io(blueprint.server.script.clone()),
}
}

pub async fn confirm_and_write(
runtime: TargetRuntime,
path: &str,
content: &[u8],
) -> anyhow::Result<()> {
let file_exists = fs::metadata(path).is_ok();

if file_exists {
let confirm = Confirm::new(&format!("Do you want to overwrite the file {path}?"))
.with_default(false)
.prompt()?;

if !confirm {
return Ok(());
}
}

runtime.file.write(path, content).await?;

Ok(())
}

pub async fn create_directory(folder_path: &str) -> anyhow::Result<()> {
let folder_exists = fs::metadata(folder_path).is_ok();

if !folder_exists {
let confirm = Confirm::new(&format!(
"Do you want to create the folder {}?",
folder_path
))
.with_default(false)
.prompt()?;

if confirm {
fs::create_dir_all(folder_path)?;
} else {
return Ok(());
};
}

Ok(())
}

pub fn select_prompt<T: std::fmt::Display>(message: &str, options: Vec<T>) -> anyhow::Result<T> {
Ok(Select::new(message, options).prompt()?)
}
7 changes: 0 additions & 7 deletions src/cli/tc/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::fs;

use lazy_static::lazy_static;

use crate::cli::fmt::Fmt;
Expand All @@ -16,11 +14,6 @@ lazy_static! {
pub static ref TRACKER: tailcall_tracker::Tracker = tailcall_tracker::Tracker::default();
}

/// Checks if file or folder already exists or not.
pub(super) fn is_exists(path: &str) -> bool {
fs::metadata(path).is_ok()
}

pub(super) fn log_endpoint_set(endpoint_set: &EndpointSet<Unchecked>) {
let mut endpoints = endpoint_set.get_endpoints().clone();
endpoints.sort_by(|a, b| {
Expand Down
168 changes: 95 additions & 73 deletions src/cli/tc/init.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
use std::fs;
use std::collections::BTreeMap;
use std::path::Path;

use anyhow::Result;
use inquire::Confirm;
use stripmargin::StripMargin;

use super::helpers::{is_exists, FILE_NAME, JSON_FILE_NAME, YML_FILE_NAME};

pub(super) async fn init_command(folder_path: &str) -> Result<()> {
let folder_exists = is_exists(folder_path);

if !folder_exists {
let confirm = Confirm::new(&format!(
"Do you want to create the folder {}?",
folder_path
))
.with_default(false)
.prompt()?;

if confirm {
fs::create_dir_all(folder_path)?;
} else {
return Ok(());
};
}

use super::helpers::{FILE_NAME, JSON_FILE_NAME, YML_FILE_NAME};
use crate::cli::runtime::{confirm_and_write, create_directory, select_prompt};
use crate::core::config::{Config, Expr, Field, RootSchema, Source, Type};
use crate::core::merge_right::MergeRight;
use crate::core::runtime::TargetRuntime;

pub(super) async fn init_command(runtime: TargetRuntime, folder_path: &str) -> Result<()> {
create_directory(folder_path).await?;

let selection = select_prompt(
"Please select the format in which you want to generate the config.",
vec![Source::GraphQL, Source::Json, Source::Yml],
)?;

let tailcallrc = include_str!("../../../generated/.tailcallrc.graphql");
let tailcallrc_json: &str = include_str!("../../../generated/.tailcallrc.schema.json");
Expand All @@ -32,64 +24,94 @@ pub(super) async fn init_command(folder_path: &str) -> Result<()> {
let json_file_path = Path::new(folder_path).join(JSON_FILE_NAME);
let yml_file_path = Path::new(folder_path).join(YML_FILE_NAME);

let tailcall_exists = fs::metadata(&file_path).is_ok();
confirm_and_write(
runtime.clone(),
&file_path.display().to_string(),
tailcallrc.as_bytes(),
)
.await?;
confirm_and_write(
runtime.clone(),
&json_file_path.display().to_string(),
tailcallrc_json.as_bytes(),
)
.await?;
confirm_and_write_yml(runtime.clone(), &yml_file_path).await?;
create_main(runtime.clone(), folder_path, selection).await?;

if tailcall_exists {
// confirm overwrite
let confirm = Confirm::new(&format!("Do you want to overwrite the file {}?", FILE_NAME))
.with_default(false)
.prompt()?;

if confirm {
fs::write(&file_path, tailcallrc.as_bytes())?;
fs::write(&json_file_path, tailcallrc_json.as_bytes())?;
}
} else {
fs::write(&file_path, tailcallrc.as_bytes())?;
fs::write(&json_file_path, tailcallrc_json.as_bytes())?;
}
Ok(())
}

let yml_exists = fs::metadata(&yml_file_path).is_ok();
fn default_graphqlrc() -> serde_yaml::Value {
serde_yaml::Value::Mapping(serde_yaml::mapping::Mapping::from_iter([(
"schema".into(),
serde_yaml::Value::Sequence(vec!["./.tailcallrc.graphql".into()]),
)]))
}

if !yml_exists {
fs::write(&yml_file_path, "")?;
async fn confirm_and_write_yml(
runtime: TargetRuntime,
yml_file_path: impl AsRef<Path>,
) -> Result<()> {
let yml_file_path = yml_file_path.as_ref().display().to_string();

let graphqlrc = r"|schema:
|- './.tailcallrc.graphql'
"
.strip_margin();
let mut final_graphqlrc = default_graphqlrc();

fs::write(&yml_file_path, graphqlrc)?;
match runtime.file.read(yml_file_path.as_ref()).await {
Ok(yml_content) => {
let graphqlrc: serde_yaml::Value = serde_yaml::from_str(&yml_content)?;
final_graphqlrc = graphqlrc.merge_right(final_graphqlrc);
let content = serde_yaml::to_string(&final_graphqlrc)?;
confirm_and_write(runtime.clone(), &yml_file_path, content.as_bytes()).await
}
Err(_) => {
let content = serde_yaml::to_string(&final_graphqlrc)?;
runtime.file.write(&yml_file_path, content.as_bytes()).await
}
}
}

let graphqlrc = fs::read_to_string(&yml_file_path)?;

let file_path = file_path.to_str().unwrap();

let mut yaml: serde_yaml::Value = serde_yaml::from_str(&graphqlrc)?;

if let Some(mapping) = yaml.as_mapping_mut() {
let schema = mapping
.entry("schema".into())
.or_insert(serde_yaml::Value::Sequence(Default::default()));
if let Some(schema) = schema.as_sequence_mut() {
if !schema
.iter()
.any(|v| v == &serde_yaml::Value::from("./.tailcallrc.graphql"))
{
let confirm =
Confirm::new(&format!("Do you want to add {} to the schema?", file_path))
.with_default(false)
.prompt()?;

if confirm {
schema.push(serde_yaml::Value::from("./.tailcallrc.graphql"));
let updated = serde_yaml::to_string(&yaml)?;
fs::write(yml_file_path, updated)?;
}
}
}
fn main_config() -> Config {
let field = Field {
type_of: "String".to_string(),
required: true,
const_field: Some(Expr { body: "Hello, World!".into() }),
..Default::default()
};

let query_type = Type {
fields: BTreeMap::from([("greet".into(), field)]),
..Default::default()
};

Config {
server: Default::default(),
upstream: Default::default(),
schema: RootSchema { query: Some("Query".to_string()), ..Default::default() },
types: BTreeMap::from([("Query".into(), query_type)]),
..Default::default()
}
}

async fn create_main(
runtime: TargetRuntime,
folder_path: impl AsRef<Path>,
source: Source,
) -> Result<()> {
let config = main_config();

let content = match source {
Source::GraphQL => config.to_sdl(),
Source::Json => config.to_json(true)?,
Source::Yml => config.to_yaml()?,
};

let path = folder_path
.as_ref()
.join(format!("main.{}", source.ext()))
.display()
.to_string();

confirm_and_write(runtime.clone(), &path, content.as_bytes()).await?;
Ok(())
}
2 changes: 1 addition & 1 deletion src/cli/tc/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async fn run_command(cli: Cli, config_reader: ConfigReader, runtime: TargetRunti
.await?;
}
Command::Init { folder_path } => {
init::init_command(&folder_path).await?;
init::init_command(runtime, &folder_path).await?;
}
Command::Gen { file_path } => {
gen::gen_command(&file_path, runtime).await?;
Expand Down
10 changes: 10 additions & 0 deletions src/core/config/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ pub enum Source {
GraphQL,
}

impl std::fmt::Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Source::Json => write!(f, "JSON"),
Source::Yml => write!(f, "YML"),
Source::GraphQL => write!(f, "GraphQL"),
}
}
}

const JSON_EXT: &str = "json";
const YML_EXT: &str = "yml";
const GRAPHQL_EXT: &str = "graphql";
Expand Down
47 changes: 47 additions & 0 deletions src/core/merge_right.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::sync::Arc;

use serde_yaml::Value;

pub trait MergeRight {
fn merge_right(self, other: Self) -> Self;
}
Expand Down Expand Up @@ -77,3 +79,48 @@ where
self
}
}

impl MergeRight for Value {
fn merge_right(self, other: Self) -> Self {
match (self, other) {
(Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_), other) => other,
(Value::Sequence(mut lhs), other) => match other {
Value::Sequence(rhs) => {
lhs.extend(rhs);
Value::Sequence(lhs)
}
other => {
lhs.push(other);
Value::Sequence(lhs)
}
},
(Value::Mapping(mut lhs), other) => match other {
Value::Mapping(rhs) => {
for (key, mut value) in rhs {
if let Some(lhs_value) = lhs.remove(&key) {
value = lhs_value.merge_right(value);
}
lhs.insert(key, value);
}
Value::Mapping(lhs)
}
Value::Sequence(mut rhs) => {
rhs.push(Value::Mapping(lhs));
Value::Sequence(rhs)
}
other => other,
},
(Value::Tagged(mut lhs), other) => match other {
Value::Tagged(rhs) => {
if lhs.tag == rhs.tag {
lhs.value = lhs.value.merge_right(rhs.value);
Value::Tagged(lhs)
} else {
Value::Tagged(rhs)
}
}
other => other,
},
}
}
}

0 comments on commit 2fe46d4

Please sign in to comment.