Open michaelitindi opened 1 month ago
To add a --watch
feature to the tailcall
CLI, we need to integrate the rs-notify
library to monitor file changes and restart the server automatically when changes are detected. This involves updating the start_command
function to include file-watching capabilities, modifying the start
command to accept the --watch
flag, adding the rs-notify
dependency to the Cargo.toml
file, and ensuring proper error handling for file-watching errors.
rs-notify
DependencyAdd the rs-notify
crate to your Cargo.toml
file to enable file-watching capabilities.
[dependencies]
rs-notify = "5.0.0"
start_command
FunctionModify the start_command
function in src/cli/tc/start.rs
to include file-watching capabilities using rs-notify
.
use anyhow::Result;
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use std::sync::mpsc::channel;
use std::time::Duration;
use std::thread;
use super::helpers::log_endpoint_set;
use crate::cli::fmt::Fmt;
use crate::cli::server::Server;
use crate::core::config::reader::ConfigReader;
pub(super) async fn start_command(
file_paths: Vec<String>,
config_reader: &ConfigReader,
watch: bool,
) -> Result<()> {
if watch {
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(2))?;
for path in &file_paths {
watcher.watch(path, RecursiveMode::Recursive)?;
}
loop {
match rx.recv() {
Ok(DebouncedEvent::Write(_)) | Ok(DebouncedEvent::Create(_)) | Ok(DebouncedEvent::Remove(_)) => {
if let Err(e) = restart_server(&file_paths, config_reader).await {
eprintln!("Error restarting server: {:?}", e);
}
},
Err(e) => eprintln!("Watch error: {:?}", e),
_ => (),
}
}
} else {
restart_server(&file_paths, config_reader).await?;
}
Ok(())
}
async fn restart_server(file_paths: &Vec<String>, config_reader: &ConfigReader) -> Result<()> {
let config_module = config_reader.read_all(file_paths).await?;
log_endpoint_set(&config_module.extensions().endpoint_set);
Fmt::log_n_plus_one(false, config_module.config());
let server = Server::new(config_module);
server.fork_start().await?;
Ok(())
}
start
Command to Accept --watch
FlagModify the Start
variant of the Command
enum in src/cli/command.rs
to include an optional --watch
flag.
use clap::{Parser, Subcommand};
use strum_macros::Display;
use tailcall_version::VERSION;
use crate::core::config;
const ABOUT: &str = r"
__ _ __ ____
/ /_____ _(_) /________ _/ / /
/ __/ __ `/ / / ___/ __ `/ / /
/ /_/ /_/ / / / /__/ /_/ / / /
\__/\__,_/_/_/\___/\__,_/_/_/";
#[derive(Parser)]
#[command(name = "tailcall", author, version = VERSION.as_str(), about, long_about = Some(ABOUT))]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Display)]
pub enum Command {
/// Starts the GraphQL server on the configured port
Start {
/// Path for the configuration files or http(s) link to config files
/// separated by spaces if more than one
#[arg(required = true)]
file_paths: Vec<String>,
/// Watch for file changes and restart the server automatically
#[arg(short, long)]
watch: bool,
},
/// Validate a composition spec
Check {
/// Path for the configuration files separated by spaces if more than
/// one
#[arg(required = true)]
file_paths: Vec<String>,
/// N plus one queries
#[arg(short, long)]
n_plus_one_queries: bool,
/// Display schema
#[arg(short, long)]
schema: bool,
/// Prints the input config in the provided format
#[clap(short, long)]
format: Option<config::Source>,
},
/// Initialize a new project
Init {
// default is current directory
#[arg(default_value = ".")]
folder_path: String,
},
/// Generates a Tailcall Configuration from one or more source files.
Gen {
/// Path of the configuration file
#[arg(required = true)]
file_path: String,
},
}
Ensure that any errors arising from the rs-notify
library are properly captured and converted into CLIError
instances in src/cli/error.rs
.
use std::fmt::{Debug, Display};
use colored::Colorize;
use derive_setters::Setters;
use thiserror::Error;
use notify::Error as NotifyError; // Import notify error
use crate::core::valid::ValidationError;
#[derive(Debug, Error, Setters, PartialEq, Clone)]
pub struct CLIError {
is_root: bool,
#[setters(skip)]
color: bool,
message: String,
#[setters(strip_option)]
description: Option<String>,
trace: Vec<String>,
#[setters(skip)]
caused_by: Vec<CLIError>,
}
impl CLIError {
pub fn new(message: &str) -> Self {
CLIError {
is_root: true,
color: false,
message: message.to_string(),
description: Default::default(),
trace: Default::default(),
caused_by: Default::default(),
}
}
pub fn caused_by(mut self, error: Vec<CLIError>) -> Self {
self.caused_by = error;
for error in self.caused_by.iter_mut() {
error.is_root = false;
}
self
}
fn colored<'a>(&'a self, str: &'a str, color: colored::Color) -> String {
if self.color {
str.color(color).to_string()
} else {
str.to_string()
}
}
fn dimmed<'a>(&'a self, str: &'a str) -> String {
if self.color {
str.dimmed().to_string()
} else {
str.to_string()
}
}
pub fn color(mut self, color: bool) -> Self {
self.color = color;
for inner in self.caused_by.iter_mut() {
inner.color = color;
}
self
}
}
fn margin(str: &str, margin: usize) -> String {
let mut result = String::new();
for line in str.split_inclusive('\n') {
result.push_str(&format!("{}{}", " ".repeat(margin), line));
}
result
}
fn bullet(str: &str) -> String {
let mut chars = margin(str, 2).chars().collect::<Vec<char>>();
chars[0] = '•';
chars[1] = ' ';
chars.into_iter().collect::<String>()
}
impl Display for CLIError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let default_padding = 2;
let message_color = if self.is_root {
colored::Color::Yellow
} else {
colored::Color::White
};
f.write_str(self.colored(&self.message, message_color).as_str())?;
if let Some(description) = &self.description {
f.write_str(&self.colored(": ", message_color))?;
f.write_str(&self.colored(description.to_string().as_str(), colored::Color::White))?;
}
if !self.trace.is_empty() {
let mut buf = String::new();
buf.push_str(" [at ");
let len = self.trace.len();
for (i, trace) in self.trace.iter().enumerate() {
buf.push_str(&trace.to_string());
if i < len - 1 {
buf.push('.');
}
}
buf.push(']');
f.write_str(&self.colored(&buf, colored::Color::Cyan))?;
}
if !self.caused_by.is_empty() {
f.write_str("\n")?;
f.write_str(self.dimmed("Caused by:").as_str())?;
f.write_str("\n")?;
for (i, error) in self.caused_by.iter().enumerate() {
let message = &error.to_string();
f.write_str(&margin(bullet(message.as_str()).as_str(), default_padding))?;
if i < self.caused_by.len() - 1 {
f.write_str("\n")?;
}
}
}
Ok(())
}
}
impl From<hyper::Error> for CLIError {
fn from(error: hyper::Error) -> Self {
// TODO: add type-safety to CLIError conversion
let cli_error = CLIError::new("Server Failed");
let message = error.to_string();
if message.to_lowercase().contains("os error 48") {
cli_error
.description("The port is already in use".to_string())
.caused_by(vec![CLIError::new(message.as_str())])
} else {
cli_error.description(message)
}
}
}
impl From<rustls::Error> for CLIError {
fn from(error: rustls::Error) -> Self {
let cli_error = CLIError::new("Failed to create TLS Acceptor");
let message = error.to_string();
cli_error.description(message)
}
}
impl From<anyhow::Error> for CLIError {
fn from(error: anyhow::Error) -> Self {
// Convert other errors to CLIError
let cli_error = match error.downcast::<CLIError>() {
Ok(cli_error) => cli_error,
Err(error) => {
// Convert other errors to CLIError
let cli_error = match error.downcast::<ValidationError<String>>() {
Ok(validation_error) => CLIError::from(validation_error),
Err(error) => {
let sources = error
.source()
.map(|error| vec![CLIError::new(error.to_string().as_str())])
.unwrap_or_default();
CLIError::new(&error.to_string()).caused_by(sources)
}
};
cli_error
}
};
cli_error
}
}
impl From<std::io::Error> for CLIError {
fn from(error: std::io::Error) -> Self {
let cli_error = CLIError::new("IO Error");
let message = error.to_string();
cli_error.description(message)
}
}
impl<'a> From<ValidationError<&'a str>> for CLIError {
fn from(error: ValidationError<&'a str>) -> Self {
CLIError::new("Invalid Configuration").caused_by(
error
.as_vec()
.iter()
.map(|cause| {
let mut err =
CLIError::new(cause.message).trace(Vec::from(cause.trace.clone()));
if let Some(description) = cause.description {
err = err.description(description.to_owned());
}
err
})
.collect(),
)
}
}
impl From<ValidationError<String>> for CLIError {
fn from(error: ValidationError<String>) -> Self {
CLIError::new("Invalid Configuration").caused_by(
error
.as_vec()
.iter()
.map(|cause| {
CLIError::new(cause.message.as_str()).trace(Vec::from(cause.trace.clone()))
})
.collect(),
)
}
}
impl From<Box<dyn std::error::Error>> for CLIError {
fn from(value: Box<dyn std::error::Error>) -> Self {
CLIError::new(value.to_string().as_str())
}
}
// New implementation for notify::Error
impl From<NotifyError> for CLIError {
fn from(error: NotifyError) -> Self {
let cli_error = CLIError::new("File Watching Error");
let message = error.to_string();
cli_error.description(message)
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use stripmargin::StripMargin;
use super::*;
use crate::core::valid::Cause;
#[test]
fn test_no_newline() {
let input = "Hello";
let expected = " Hello";
assert_eq!(margin(input, 4), expected);
}
#[test]
fn test_with_newline() {
let input = "Hello\nWorld";
let expected = " Hello\n World";
assert_eq!(margin(input, 4), expected);
}
#[test]
fn test_empty_string() {
let input = "";
let expected = "";
assert_eq!(margin(input, 4), expected);
}
#[test]
fn test_zero_margin() {
let input = "Hello";
let expected = "Hello";
assert_eq!(margin(input, 0), expected);
}
#[test]
fn test_zero_margin_with_newline() {
let input = "Hello\nWorld";
let expected = "Hello\nWorld";
assert_eq!(margin(input, 0), expected);
}
#[test]
fn test_title() {
let error = CLIError::new("Server could not be started");
let expected = r"Server could not be started".strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_title_description() {
let error = CLIError::new("Server could not be started")
.description("The port is already in use".to_string());
let expected = r"|Server could not be started: The port is already in use".strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_title_description_trace() {
let error = CLIError::new("Server could not be started")
.description("The port is already in use".to_string())
.trace(vec!["@server".into(), "port".into()]);
let expected =
r"|Server could not be started: The port is already in use [at @server.port]"
.strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_title_trace_caused_by() {
let error = CLIError::new("Configuration Error").caused_by(vec![CLIError::new(
"Base URL needs to be specified",
)
.trace(vec![
"User".into(),
"posts".into(),
"@http".into(),
"baseURL".into(),
])]);
let expected = r"|Configuration Error
|Caused by:
| • Base URL needs to be specified [at User.posts.@http.baseURL]"
.strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_title_trace_multiple_caused_by() {
let error = CLIError::new("Configuration Error").caused_by(vec![
CLIError::new("Base URL needs to be specified").trace(vec![
"User".into(),
"posts".into(),
"@http".into(),
"baseURL".into(),
]),
CLIError::new("Base URL needs to be specified").trace(vec![
"Post".into(),
"users".into(),
"@http".into(),
"baseURL".into(),
]),
CLIError::new("Base URL needs to be specified")
.description("Set `baseURL` in @http or @server directives".into())
.trace(vec![
"Query".into(),
"users".into(),
"@http".into(),
"baseURL".into(),
]),
CLIError::new("Base URL needs to be specified").trace(vec![
"Query".into(),
"posts".into(),
"@http".into(),
"baseURL".into(),
]),
]);
let expected = r"|Configuration Error
|Caused by:
| • Base URL needs to be specified [at User.posts.@http.baseURL]
| • Base URL needs to be specified [at Post.users.@http.baseURL]
| • Base URL needs to be specified: Set `baseURL` in @http or @server directives [at Query.users.@http.baseURL]
| • Base URL needs to be specified [at Query.posts.@http.baseURL]"
.strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_from_validation() {
let cause = Cause::new("Base URL needs to be specified")
.description("Set `baseURL` in @http or @server directives")
.trace(vec!["Query", "users", "@http", "baseURL"]);
let valid = ValidationError::from(cause);
let error = CLIError::from(valid);
let expected = r"|Invalid Configuration
|Caused by:
| • Base URL needs to be specified: Set `baseURL` in @http or @server directives [at Query.users.@http.baseURL]"
.strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_cli_error_identity() {
let cli_error = CLIError::new("Server could not be started")
.description("The port is already in use".to_string())
.trace(vec!["@server".into(), "port".into()]);
let anyhow_error: anyhow::Error = cli_error.clone().into();
let actual = CLIError::from(anyhow_error);
let expected = cli_error;
assert_eq!(actual, expected);
}
#[test]
fn test_validation_error_identity() {
let validation_error = ValidationError::from(
Cause::new("Test Error".to_string()).trace(vec!["Query".to_string()]),
);
let anyhow_error: anyhow::Error = validation_error.clone().into();
let actual = CLIError::from(anyhow_error);
let expected = CLIError::from(validation_error);
assert_eq!(actual, expected);
}
#[test]
fn test_generic_error() {
let anyhow_error = anyhow::anyhow!("Some error msg");
let actual: CLIError = CLIError::from(anyhow_error);
let expected = CLIError::new("Some error msg");
assert_eq!(actual, expected);
}
}
Click here to create a Pull Request with the proposed solution
Files used for this task:
@codeautopilot show the parts of the code that I should change
@codeautopilot show the parts of the code that I should change
Certainly! Here are the specific parts of the code that you need to change:
rs-notify
DependencyAdd the rs-notify
crate to your Cargo.toml
file:
[dependencies]
rs-notify = "5.0.0"
start_command
FunctionModify the start_command
function in src/cli/tc/start.rs
to include file-watching capabilities using rs-notify
:
use anyhow::Result;
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use std::sync::mpsc::channel;
use std::time::Duration;
use std::thread;
use super::helpers::log_endpoint_set;
use crate::cli::fmt::Fmt;
use crate::cli::server::Server;
use crate::core::config::reader::ConfigReader;
pub(super) async fn start_command(
file_paths: Vec<String>,
config_reader: &ConfigReader,
watch: bool,
) -> Result<()> {
if watch {
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(2))?;
for path in &file_paths {
watcher.watch(path, RecursiveMode::Recursive)?;
}
loop {
match rx.recv() {
Ok(DebouncedEvent::Write(_)) | Ok(DebouncedEvent::Create(_)) | Ok(DebouncedEvent::Remove(_)) => {
if let Err(e) = restart_server(&file_paths, config_reader).await {
eprintln!("Error restarting server: {:?}", e);
}
},
Err(e) => eprintln!("Watch error: {:?}", e),
_ => (),
}
}
} else {
restart_server(&file_paths, config_reader).await?;
}
Ok(())
}
async fn restart_server(file_paths: &Vec<String>, config_reader: &ConfigReader) -> Result<()> {
let config_module = config_reader.read_all(file_paths).await?;
log_endpoint_set(&config_module.extensions().endpoint_set);
Fmt::log_n_plus_one(false, config_module.config());
let server = Server::new(config_module);
server.fork_start().await?;
Ok(())
}
start
Command to Accept --watch
FlagModify the Start
variant of the Command
enum in src/cli/command.rs
to include an optional --watch
flag:
use clap::{Parser, Subcommand};
use strum_macros::Display;
use tailcall_version::VERSION;
use crate::core::config;
const ABOUT: &str = r"
__ _ __ ____
/ /_____ _(_) /________ _/ / /
/ __/ __ `/ / / ___/ __ `/ / /
/ /_/ /_/ / / / /__/ /_/ / / /
\__/\__,_/_/_/\___/\__,_/_/_/";
#[derive(Parser)]
#[command(name = "tailcall", author, version = VERSION.as_str(), about, long_about = Some(ABOUT))]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Display)]
pub enum Command {
/// Starts the GraphQL server on the configured port
Start {
/// Path for the configuration files or http(s) link to config files
/// separated by spaces if more than one
#[arg(required = true)]
file_paths: Vec<String>,
/// Watch for file changes and restart the server automatically
#[arg(short, long)]
watch: bool,
},
/// Validate a composition spec
Check {
/// Path for the configuration files separated by spaces if more than
/// one
#[arg(required = true)]
file_paths: Vec<String>,
/// N plus one queries
#[arg(short, long)]
n_plus_one_queries: bool,
/// Display schema
#[arg(short, long)]
schema: bool,
/// Prints the input config in the provided format
#[clap(short, long)]
format: Option<config::Source>,
},
/// Initialize a new project
Init {
// default is current directory
#[arg(default_value = ".")]
folder_path: String,
},
/// Generates a Tailcall Configuration from one or more source files.
Gen {
/// Path of the configuration file
#[arg(required = true)]
file_path: String,
},
}
Ensure that any errors arising from the rs-notify
library are properly captured and converted into CLIError
instances in src/cli/error.rs
:
use std::fmt::{Debug, Display};
use colored::Colorize;
use derive_setters::Setters;
use thiserror::Error;
use notify::Error as NotifyError; // Import notify error
use crate::core::valid::ValidationError;
#[derive(Debug, Error, Setters, PartialEq, Clone)]
pub struct CLIError {
is_root: bool,
#[setters(skip)]
color: bool,
message: String,
#[setters(strip_option)]
description: Option<String>,
trace: Vec<String>,
#[setters(skip)]
caused_by: Vec<CLIError>,
}
impl CLIError {
pub fn new(message: &str) -> Self {
CLIError {
is_root: true,
color: false,
message: message.to_string(),
description: Default::default(),
trace: Default::default(),
caused_by: Default::default(),
}
}
pub fn caused_by(mut self, error: Vec<CLIError>) -> Self {
self.caused_by = error;
for error in self.caused_by.iter_mut() {
error.is_root = false;
}
self
}
fn colored<'a>(&'a self, str: &'a str, color: colored::Color) -> String {
if self.color {
str.color(color).to_string()
} else {
str.to_string()
}
}
fn dimmed<'a>(&'a self, str: &'a str) -> String {
if self.color {
str.dimmed().to_string()
} else {
str.to_string()
}
}
pub fn color(mut self, color: bool) -> Self {
self.color = color;
for inner in self.caused_by.iter_mut() {
inner.color = color;
}
self
}
}
fn margin(str: &str, margin: usize) -> String {
let mut result = String::new();
for line in str.split_inclusive('\n') {
result.push_str(&format!("{}{}", " ".repeat(margin), line));
}
result
}
fn bullet(str: &str) -> String {
let mut chars = margin(str, 2).chars().collect::<Vec<char>>();
chars[0] = '•';
chars[1] = ' ';
chars.into_iter().collect::<String>()
}
impl Display for CLIError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let default_padding = 2;
let message_color = if self.is_root {
colored::Color::Yellow
} else {
colored::Color::White
};
f.write_str(self.colored(&self.message, message_color).as_str())?;
if let Some(description) = &self.description {
f.write_str(&self.colored(": ", message_color))?;
f.write_str(&self.colored(description.to_string().as_str(), colored::Color::White))?;
}
if !self.trace.is_empty() {
let mut buf = String::new();
buf.push_str(" [at ");
let len = self.trace.len();
for (i, trace) in self.trace.iter().enumerate() {
buf.push_str(&trace.to_string());
if i < len - 1 {
buf.push('.');
}
}
buf.push(']');
f.write_str(&self.colored(&buf, colored::Color::Cyan))?;
}
if !self.caused_by.is_empty() {
f.write_str("\n")?;
f.write_str(self.dimmed("Caused by:").as_str())?;
f.write_str("\n")?;
for (i, error) in self.caused_by.iter().enumerate() {
let message = &error.to_string();
f.write_str(&margin(bullet(message.as_str()).as_str(), default_padding))?;
if i < self.caused_by.len() - 1 {
f.write_str("\n")?;
}
}
}
Ok(())
}
}
impl From<hyper::Error> for CLIError {
fn from(error: hyper::Error) -> Self {
// TODO: add type-safety to CLIError conversion
let cli_error = CLIError::new("Server Failed");
let message = error.to_string();
if message.to_lowercase().contains("os error 48") {
cli_error
.description("The port is already in use".to_string())
.caused_by(vec![CLIError::new(message.as_str())])
} else {
cli_error.description(message)
}
}
}
impl From<rustls::Error> for CLIError {
fn from(error: rustls::Error) -> Self {
let cli_error = CLIError::new("Failed to create TLS Acceptor");
let message = error.to_string();
cli_error.description(message)
}
}
impl From<anyhow::Error> for CLIError {
fn from(error: anyhow::Error) -> Self {
// Convert other errors to CLIError
let cli_error = match error.downcast::<CLIError>() {
Ok(cli_error) => cli_error,
Err(error) => {
// Convert other errors to CLIError
let cli_error = match error.downcast::<ValidationError<String>>() {
Ok(validation_error) => CLIError::from(validation_error),
Err(error) => {
let sources = error
.source()
.map(|error| vec![CLIError::new(error.to_string().as_str())])
.unwrap_or_default();
CLIError::new(&error.to_string()).caused_by(sources)
}
};
cli_error
}
};
cli_error
}
}
impl From<std::io::Error> for CLIError {
fn from(error: std::io::Error) -> Self {
let cli_error = CLIError::new("IO Error");
let message = error.to_string();
cli_error.description(message)
}
}
impl<'a> From<ValidationError<&'a str>> for CLIError {
fn from(error: ValidationError<&'a str>) -> Self {
CLIError::new("Invalid Configuration").caused_by(
error
.as_vec()
.iter()
.map(|cause| {
let mut err =
CLIError::new(cause.message).trace(Vec::from(cause.trace.clone()));
if let Some(description) = cause.description {
err = err.description(description.to_owned());
}
err
})
.collect(),
)
}
}
impl From<ValidationError<String>> for CLIError {
fn from(error: ValidationError<String>) -> Self {
CLIError::new("Invalid Configuration").caused_by(
error
.as_vec()
.iter()
.map(|cause| {
CLIError::new(cause.message.as_str()).trace(Vec::from(cause.trace.clone()))
})
.collect(),
)
}
}
impl From<Box<dyn std::error::Error>> for CLIError {
fn from(value: Box<dyn std::error::Error>) -> Self {
CLIError::new(value.to_string().as_str())
}
}
// New implementation for notify::Error
impl From<NotifyError> for CLIError {
fn from(error: NotifyError) -> Self {
let cli_error = CLIError::new("File Watching Error");
let message = error.to_string();
cli_error.description(message)
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use stripmargin::StripMargin;
use super::*;
use crate::core::valid::Cause;
#[test]
fn test_no_newline() {
let input = "Hello";
let expected = " Hello";
assert_eq!(margin(input, 4), expected);
}
#[test]
fn test_with_newline() {
let input = "Hello\nWorld";
let expected = " Hello\n World";
assert_eq!(margin(input, 4), expected);
}
#[test]
fn test_empty_string() {
let input = "";
let expected = "";
assert_eq!(margin(input, 4), expected);
}
#[test]
fn test_zero_margin() {
let input = "Hello";
let expected = "Hello";
assert_eq!(margin(input, 0), expected);
}
#[test]
fn test_zero_margin_with_newline() {
let input = "Hello\nWorld";
let expected = "Hello\nWorld";
assert_eq!(margin(input, 0), expected);
}
#[test]
fn test_title() {
let error = CLIError::new("Server could not be started");
let expected = r"Server could not be started".strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_title_description() {
let error = CLIError::new("Server could not be started")
.description("The port is already in use".to_string());
let expected = r"|Server could not be started: The port is already in use".strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_title_description_trace() {
let error = CLIError::new("Server could not be started")
.description("The port is already in use".to_string())
.trace(vec!["@server".into(), "port".into()]);
let expected =
r"|Server could not be started: The port is already in use [at @server.port]"
.strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_title_trace_caused_by() {
let error = CLIError::new("Configuration Error").caused_by(vec![CLIError::new(
"Base URL needs to be specified",
)
.trace(vec![
"User".into(),
"posts".into(),
"@http".into(),
"baseURL".into(),
])]);
let expected = r"|Configuration Error
|Caused by:
| • Base URL needs to be specified [at User.posts.@http.baseURL]"
.strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_title_trace_multiple_caused_by() {
let error = CLIError::new("Configuration Error").caused_by(vec![
CLIError::new("Base URL needs to be specified").trace(vec![
"User".into(),
"posts".into(),
"@http".into(),
"baseURL".into(),
]),
CLIError::new("Base URL needs to be specified").trace(vec![
"Post".into(),
"users".into(),
"@http".into(),
"baseURL".into(),
]),
CLIError::new("Base URL needs to be specified")
.description("Set `baseURL` in @http or @server directives".into())
.trace(vec![
"Query".into(),
"users".into(),
"@http".into(),
"baseURL".into(),
]),
CLIError::new("Base URL needs to be specified").trace(vec![
"Query".into(),
"posts".into(),
"@http".into(),
"baseURL".into(),
]),
]);
let expected = r"|Configuration Error
|Caused by:
| • Base URL needs to be specified [at User.posts.@http.baseURL]
| • Base URL needs to be specified [at Post.users.@http.baseURL]
| • Base URL needs to be specified: Set `baseURL` in @http or @server directives [at Query.users.@http.baseURL]
| • Base URL needs to be specified [at Query.posts.@http.baseURL]"
.strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_from_validation() {
let cause = Cause::new("Base URL needs to be specified")
.description("Set `baseURL` in @http or @server directives")
.trace(vec!["Query", "users", "@http", "baseURL"]);
let valid = ValidationError::from(cause);
let error = CLIError::from(valid);
let expected = r"|Invalid Configuration
|Caused by:
| • Base URL needs to be specified: Set `baseURL` in @http or @server directives [at Query.users.@http.baseURL]"
.strip_margin();
assert_eq!(error.to_string(), expected);
}
#[test]
fn test_cli_error_identity() {
let cli_error = CLIError::new("Server could not be started")
.description("The port is already in use".to_string())
.trace(vec!["@server".into(), "port".into()]);
let anyhow_error: anyhow::Error = cli_error.clone().into();
let actual = CLIError::from(anyhow_error);
let expected = cli_error;
assert_eq!(actual, expected);
}
#[test]
fn test_validation_error_identity() {
let validation_error = ValidationError::from(
Cause::new("Test Error".to_string()).trace(vec!["Query".to_string()]),
);
let anyhow_error: anyhow::Error = validation_error.clone().into();
let actual = CLIError::from(anyhow_error);
let expected = CLIError::from(validation_error);
assert_eq!(actual, expected);
}
#[test]
fn test_generic_error() {
let anyhow_error = anyhow::anyhow!("Some error msg");
let actual: CLIError = CLIError::from(anyhow_error);
let expected = CLIError::new("Some error msg");
assert_eq!(actual, expected);
}
}
These changes will add the --watch
feature to your application, allowing it to monitor file changes and restart the server automatically.
Use rs-notify library to identify if the source files have changed and restart the server when it does.
Example Usage
tailcall start ./jp.graphql --watch Technical Requirements
Restart should happen automatically when the main config or any of the linked file changes. There should be no leaks in restarts and should happen seamlessly. Should not crash the server if there is a validation error.