Repository: Brendonovich/prisma-client-rust (language: rust)

Issue:
Better cli errors
closes #184 


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6c79a733..be199540 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3459,6 +3459,7 @@ dependencies = [
 name = "prisma-client-rust-cli"
 version = "0.6.2"
 dependencies = [
+ "datamodel",
  "prisma-client-rust-sdk",
  "proc-macro2",
  "quote",
@@ -3466,6 +3467,7 @@ dependencies = [
  "serde_json",
  "serde_path_to_error",
  "syn",
+ "thiserror",
 ]
 
 [[package]]
@@ -3489,6 +3491,7 @@ dependencies = [
  "serde_json",
  "serde_path_to_error",
  "syn",
+ "thiserror",
 ]
 
 [[package]]
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index aeeb7cf4..4981991e 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -27,3 +27,6 @@ serde = { version = "1.0.136", features = ["derive"] }
 proc-macro2 = "1.0.39"
 
 prisma-client-rust-sdk = { path = "../sdk", default_features = false }
+
+datamodel = { git = "https://github.com/Brendonovich/prisma-engines", rev = "06a1b97ff1ca597521ec9f3d10c1e274065f5e93" }
+thiserror = "1.0.37"
diff --git a/cli/src/generator/header.rs b/cli/src/generator/header.rs
index a045928a..c35231d4 100644
--- a/cli/src/generator/header.rs
+++ b/cli/src/generator/header.rs
@@ -13,11 +13,11 @@ fn find_migrations_path(schema_path: &PathBuf) -> PathBuf {
 }
 
 pub fn generate(args: &GenerateArgs) -> TokenStream {
-    let database_string = &args.ctx.datasources[0].provider;
+    let database_string = &args.dmmf.datasources[0].provider;
 
     let pcr = quote!(::prisma_client_rust);
 
-    let schema_path_str = &args.ctx.schema_path;
+    let schema_path_str = &args.dmmf.schema_path;
     let schema_path = schema_path_str
         .parse()
         .expect("Failed to parse schema path!");
diff --git a/cli/src/generator/mod.rs b/cli/src/generator/mod.rs
index 31081afb..42906825 100644
--- a/cli/src/generator/mod.rs
+++ b/cli/src/generator/mod.rs
@@ -6,6 +6,7 @@ mod models;
 mod read_filters;
 
 use prisma_client_rust_sdk::prelude::*;
+use serde::{Deserialize, Serialize};
 
 fn default_module_path() -> String {
     "prisma".to_string()
@@ -17,16 +18,26 @@ pub struct PrismaClientRustGenerator {
     module_path: String,
 }
 
+#[derive(Debug, Serialize, thiserror::Error)]
+pub enum Error {
+    #[error("Failed to parse module_path")]
+    InvalidModulePath,
+}
+
 impl PrismaGenerator for PrismaClientRustGenerator {
     const NAME: &'static str = "Prisma Client Rust";
     const DEFAULT_OUTPUT: &'static str = "../src/prisma.rs";
 
-    fn generate(self, args: GenerateArgs) -> TokenStream {
-        let header = header::generate(&args);
+    type Error = Error;
+
+    fn generate(self, args: GenerateArgs) -> Result<String, Self::Error> {
+        let mut header = header::generate(&args);
 
         let models = models::generate(
             &args,
-            self.module_path.parse().expect("Invalid module path"),
+            self.module_path
+                .parse()
+                .map_err(|_| Error::InvalidModulePath)?,
         );
 
         let internal_enums = internal_enums::generate(&args);
@@ -44,10 +55,11 @@ impl PrismaGenerator for PrismaClientRustGenerator {
                 pub use _prisma::QueryMode;
             )
         });
+
         let read_filters_module = read_filters::generate_module(&args);
         let enums = enums::generate(&args);
 
-        quote! {
+        let tokens = quote! {
             #header
 
             #(#models)*
@@ -62,6 +74,8 @@ impl PrismaGenerator for PrismaClientRustGenerator {
             #use_query_mode
 
             #enums
-        }
+        };
+
+        Ok(tokens.to_string())
     }
 }
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index 7cdc9446..623084cd 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -36,3 +36,4 @@ query-core = { git = "https://github.com/Brendonovich/prisma-engines", rev = "06
 dmmf = { git = "https://github.com/Brendonovich/prisma-engines", rev = "06a1b97ff1ca597521ec9f3d10c1e274065f5e93" }
 
 proc-macro2 = "1.0.39"
+thiserror = "1.0.37"
diff --git a/sdk/src/args.rs b/sdk/src/args.rs
index 63bf35a4..135095d9 100644
--- a/sdk/src/args.rs
+++ b/sdk/src/args.rs
@@ -10,11 +10,11 @@ use dmmf::{DmmfInputField, DmmfInputType, DmmfSchema, TypeLocation};
 use proc_macro2::TokenStream;
 use quote::quote;
 
-use crate::{casing::Casing, dmmf::GeneratorCtx, FieldTypeExt};
+use crate::{casing::Casing, dmmf::EngineDMMF, FieldTypeExt};
 
 pub struct GenerateArgs {
     pub dml: datamodel::dml::Datamodel,
-    pub ctx: GeneratorCtx,
+    pub dmmf: EngineDMMF,
     pub schema: DmmfSchema,
     pub read_filters: Vec<Filter>,
     pub write_filters: Vec<Filter>,
@@ -22,7 +22,7 @@ pub struct GenerateArgs {
 }
 
 impl GenerateArgs {
-    pub fn new(mut dml: datamodel::dml::Datamodel, schema: DmmfSchema, ctx: GeneratorCtx) -> Self {
+    pub fn new(mut dml: datamodel::dml::Datamodel, schema: DmmfSchema, dmmf: EngineDMMF) -> Self {
         let scalars = {
             let mut scalars = Vec::new();
             for scalar in schema.input_object_types.get("prisma").unwrap() {
@@ -256,7 +256,7 @@ impl GenerateArgs {
         };
 
         use builtin_connectors::*;
-        let connector = match &ctx.datasources[0].provider {
+        let connector = match &dmmf.datasources[0].provider {
             #[cfg(feature = "sqlite")]
             p if SQLITE.is_provider(p) => SQLITE,
             #[cfg(feature = "postgresql")]
@@ -269,12 +269,14 @@ impl GenerateArgs {
             p if MYSQL.is_provider(p) => MYSQL,
             #[cfg(feature = "mongodb")]
             p if MONGODB.is_provider(p) => MONGODB,
-            _ => unreachable!(),
+            p => panic!(
+                "Database provider {p} is not available. Have you enabled its Cargo.toml feature?"
+            ),
         };
 
         Self {
             dml,
-            ctx,
+            dmmf,
             schema,
             read_filters,
             write_filters,
diff --git a/sdk/src/dmmf.rs b/sdk/src/dmmf.rs
index f029b0e5..bc0aed3b 100644
--- a/sdk/src/dmmf.rs
+++ b/sdk/src/dmmf.rs
@@ -4,11 +4,10 @@ use serde_json::Map;
 /// Provided by Prisma CLI to generators
 #[derive(Debug, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
-pub struct GeneratorCtx {
+pub struct EngineDMMF {
     pub generator: Generator,
     pub schema_path: String,
-    #[serde(rename = "datamodel")]
-    pub datamodel_str: String,
+    pub datamodel: String,
     pub datasources: Vec<Datasource>,
 }
 
diff --git a/sdk/src/jsonrpc.rs b/sdk/src/jsonrpc.rs
index 10c98506..b4cc7558 100644
--- a/sdk/src/jsonrpc.rs
+++ b/sdk/src/jsonrpc.rs
@@ -25,9 +25,17 @@ pub struct Request {
     pub params: Value,
 }
 
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub enum ResponseData {
+    Result(Value),
+    Error { code: i32, message: String },
+}
+
 #[derive(Serialize, Deserialize, Debug)]
 pub struct Response {
     pub jsonrpc: String,
     pub id: i32,
-    pub result: Value,
+    #[serde(flatten)]
+    pub data: ResponseData,
 }
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
index 212e973e..d77df3e4 100644
--- a/sdk/src/lib.rs
+++ b/sdk/src/lib.rs
@@ -9,9 +9,9 @@ mod prisma_cli;
 mod runtime;
 mod utils;
 
-use proc_macro2::TokenStream;
-use serde::de::DeserializeOwned;
+use serde::{de::DeserializeOwned, Serialize};
 use serde_json::{Map, Value};
+use thiserror::Error;
 
 use runtime::{run_generator, GeneratorMetadata};
 
@@ -50,19 +50,44 @@ pub mod prelude {
     }
 }
 
+pub type GenerateFn = fn(GenerateArgs, Map<String, Value>) -> GenerateResult;
+pub type GenerateResult = Result<String, GeneratorError>;
+
+#[derive(Debug, Error)]
+pub enum GeneratorError {
+    #[error("Schema contains invalid names \n{0}")]
+    ReservedNames(String),
+    #[error("Failed to create client file")]
+    FileCreate(std::io::Error),
+    #[error("Failed to write generated client to file")]
+    FileWrite(std::io::Error),
+    #[error("Failed to deserialize generator arguments")]
+    ArgDeserialize(serde_json::Error),
+    #[error("Generator {name} failed: \n{message}")]
+    InternalError { name: &'static str, message: String },
+}
+
 pub trait PrismaGenerator: DeserializeOwned {
     const NAME: &'static str;
     const DEFAULT_OUTPUT: &'static str;
 
-    fn generate(self, args: GenerateArgs) -> TokenStream;
+    type Error: Serialize + std::error::Error;
 
-    fn erased_generate(args: GenerateArgs, config: Map<String, Value>) -> TokenStream
+    fn generate(self, args: GenerateArgs) -> Result<String, Self::Error>;
+
+    fn erased_generate(args: GenerateArgs, config: Map<String, Value>) -> GenerateResult
     where
         Self: Sized,
     {
-        serde_json::from_value::<Self>(Value::Object(config))
-            .unwrap()
+        let generator = serde_json::from_value::<Self>(Value::Object(config))
+            .map_err(GeneratorError::ArgDeserialize)?;
+
+        generator
             .generate(args)
+            .map_err(|e| GeneratorError::InternalError {
+                name: Self::NAME,
+                message: e.to_string(),
+            })
     }
 }
 
diff --git a/sdk/src/runtime.rs b/sdk/src/runtime.rs
index 84d31234..d752c4fd 100644
--- a/sdk/src/runtime.rs
+++ b/sdk/src/runtime.rs
@@ -5,28 +5,23 @@ use std::{
 };
 
 use datamodel::parse_schema;
-use proc_macro2::TokenStream;
-use serde_json::{Map, Value};
 
 use crate::{
     args::GenerateArgs,
-    dmmf::GeneratorCtx,
+    dmmf::EngineDMMF,
     jsonrpc, prisma_cli,
     utils::{build_schema, rustfmt, validate_names},
+    GenerateFn, GeneratorError,
 };
 
 pub struct GeneratorMetadata {
-    generate_fn: fn(GenerateArgs, Map<String, Value>) -> TokenStream,
+    generate_fn: GenerateFn,
     name: &'static str,
     default_output: &'static str,
 }
 
 impl GeneratorMetadata {
-    pub fn new(
-        generate_fn: fn(GenerateArgs, Map<String, Value>) -> TokenStream,
-        name: &'static str,
-        default_output: &'static str,
-    ) -> Self {
+    pub fn new(generate_fn: GenerateFn, name: &'static str, default_output: &'static str) -> Self {
         Self {
             generate_fn,
             name,
@@ -45,42 +40,52 @@ pub fn run_generator(generator: GeneratorMetadata, args: &Vec<String>) {
         let mut content = String::new();
         BufReader::new(stdin())
             .read_line(&mut content)
-            .expect("Failed to read Prisma engine output");
-
-        let input: jsonrpc::Request = serde_json::from_str(&content).unwrap();
-
-        let value = match input.method.as_str() {
-            "getManifest" => serde_json::to_value(jsonrpc::ManifestResponse {
-                manifest: jsonrpc::Manifest {
-                    default_output: generator.default_output.to_string(),
-                    pretty_name: generator.name.to_string(),
-                    ..Default::default()
-                },
-            })
-            .unwrap(),
+            .expect("Failed to read engine output");
+
+        let input: jsonrpc::Request =
+            serde_json::from_str(&content).expect("Failed to marshal jsonrpc input");
+
+        let data = match input.method.as_str() {
+            "getManifest" => jsonrpc::ResponseData::Result(
+                serde_json::to_value(jsonrpc::ManifestResponse {
+                    manifest: jsonrpc::Manifest {
+                        default_output: generator.default_output.to_string(),
+                        pretty_name: generator.name.to_string(),
+                        ..Default::default()
+                    },
+                })
+                .expect("Failed to convert manifest to json"), // literally will never fail
+            ),
             "generate" => {
                 let params_str = input.params.to_string();
 
                 let deserializer = &mut serde_json::Deserializer::from_str(&params_str);
 
-                let generator_ctx = serde_path_to_error::deserialize(deserializer)
+                let dmmf = serde_path_to_error::deserialize(deserializer)
                     .expect("Failed to deserialize DMMF from Prisma engines");
 
-                generate(&generator, generator_ctx);
-
-                serde_json::Value::Null
+                match generate(&generator, dmmf) {
+                    Ok(_) => jsonrpc::ResponseData::Result(serde_json::Value::Null),
+                    Err(e) => jsonrpc::ResponseData::Error {
+                        code: 0,
+                        message: e.to_string(),
+                    },
+                }
             }
-            method => panic!("Unknown generator method {}", method),
+            method => jsonrpc::ResponseData::Error {
+                code: 0,
+                message: format!("{} cannot handle method {}", generator.name, method),
+            },
         };
 
         let response = jsonrpc::Response {
             jsonrpc: "2.0".to_string(),
             id: input.id,
-            result: value,
+            data,
         };
 
         let mut bytes =
-            serde_json::to_vec(&response).expect("Could not marshal json data for reply");
+            serde_json::to_vec(&response).expect("Failed to marshal json data for reply");
 
         bytes.push(b'\n');
 
@@ -95,37 +100,39 @@ pub fn run_generator(generator: GeneratorMetadata, args: &Vec<String>) {
     }
 }
 
-fn generate(generator: &GeneratorMetadata, ctx: GeneratorCtx) {
+fn generate(generator: &GeneratorMetadata, dmmf: EngineDMMF) -> Result<(), GeneratorError> {
     let (configuration, datamodel) =
-        parse_schema(&ctx.datamodel_str).expect("Failed to parse datamodel");
+        parse_schema(&dmmf.datamodel).expect("Failed to parse datamodel"); // Verified by CLI
+                                                                           // before generating
     let schema = build_schema(&datamodel, &configuration);
 
-    let output_str = ctx.generator.output.get_value();
+    let output_str = dmmf.generator.output.get_value();
     let output_path = Path::new(&output_str);
 
-    let config = ctx.generator.config.clone();
+    let config = dmmf.generator.config.clone();
 
-    let args = GenerateArgs::new(datamodel, schema.schema, ctx);
+    let mut file = create_generated_file(&output_path)?;
 
-    validate_names(&args);
+    let args = GenerateArgs::new(datamodel, schema.schema, dmmf);
 
-    let generated_str = (generator.generate_fn)(args, config);
+    validate_names(&args)?;
 
-    let mut file = create_generated_file(&output_path);
+    let mut generated_str = format!("// Code generated by {}. DO NOT EDIT\n\n", generator.name);
 
-    file.write(format!("// Code generated by {}. DO NOT EDIT\n\n", generator.name).as_bytes())
-        .expect("Failed to write file header");
+    generated_str += &(generator.generate_fn)(args, config)?;
 
-    file.write(generated_str.to_string().as_bytes())
-        .expect("Failed to write generated code");
+    file.write(generated_str.as_bytes())
+        .map_err(GeneratorError::FileWrite)?;
 
     rustfmt(output_path);
+
+    Ok(())
 }
 
-fn create_generated_file(path: &Path) -> File {
+fn create_generated_file(path: &Path) -> Result<File, GeneratorError> {
     if let Some(parent) = path.parent() {
-        fs::create_dir_all(parent).expect("Failed to create output directory");
+        fs::create_dir_all(parent).map_err(GeneratorError::FileCreate)?;
     }
 
-    File::create(&path).expect("Failed to open output file")
+    File::create(&path).map_err(GeneratorError::FileCreate)
 }
diff --git a/sdk/src/utils.rs b/sdk/src/utils.rs
index 463662ee..61a06920 100644
--- a/sdk/src/utils.rs
+++ b/sdk/src/utils.rs
@@ -6,51 +6,55 @@ use dmmf::{from_precomputed_parts, DataModelMetaFormat};
 use prisma_models::InternalDataModelBuilder;
 use query_core::schema_builder;
 
-use crate::{args::GenerateArgs, casing::Casing, keywords::is_reserved_keyword};
+use crate::{args::GenerateArgs, casing::Casing, keywords::is_reserved_keyword, GeneratorError};
 
 pub fn rustfmt(path: &Path) {
-    match Command::new("rustfmt")
+    Command::new("rustfmt")
         .arg("--edition=2021")
         .arg(path.to_str().unwrap())
         .output()
-    {
-        _ => {}
-    };
+        .ok();
 }
 
 /// Validates that names of models, fields and enums do not overlap with reserved Rust keywords.
-pub fn validate_names(args: &GenerateArgs) {
+pub fn validate_names(args: &GenerateArgs) -> Result<(), GeneratorError> {
+    let mut errors = vec![];
     // ensure that model and field names are not conflicting with keywords
     for model in &args.dml.models {
         if is_reserved_keyword(&model.name.to_case(Case::Snake)) {
-            panic!(
-                "Model '{}' produces reserved keyword '{}' and must be changed",
+            errors.push(format!(
+                "> Model '{}' produces reserved keyword '{}' and must be changed",
                 model.name,
                 model.name.to_case(Case::Snake)
-            );
+            ));
         }
 
         for field in &model.fields {
             if is_reserved_keyword(&field.name().to_case(Case::Snake)) {
-                panic!(
-                    "Field '{}' of model '{}' produces reserved keyword '{}' and must be changed",
+                errors.push(format!(
+                    "> Field '{}' of model '{}' produces reserved keyword '{}' and must be changed",
                     field.name(),
                     model.name,
                     field.name().to_case(Case::Snake)
-                );
+                ));
             }
         }
     }
 
     for e in &args.dml.enums {
         if is_reserved_keyword(&e.name.to_case(Case::Pascal)) {
-            panic!(
-                "Enum '{}' produces reserved keyword '{}' and must be changed",
+            errors.push(format!(
+                "> Enum '{}' produces reserved keyword '{}' and must be changed",
                 e.name,
                 e.name.to_case(Case::Pascal)
-            );
+            ));
         }
     }
+
+    match errors.len() {
+        0 => Ok(()),
+        _ => Err(GeneratorError::ReservedNames(errors.join("\n"))),
+    }
 }
 
 pub fn build_schema(datamodel: &Datamodel, configuration: &Configuration) -> DataModelMetaFormat {


Test patch: