diff --git a/Cargo.lock b/Cargo.lock index 5f9809096c..d7c7d2d415 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2651,9 +2651,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -3118,6 +3118,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "minicov" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4931,9 +4941,9 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.206" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284" +checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" dependencies = [ "serde_derive", ] @@ -4981,9 +4991,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.206" +version = "1.0.207" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" +checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" dependencies = [ "proc-macro2", "quote", @@ -5627,9 +5637,9 @@ dependencies = [ name = "tailcall-prettier" version = "0.1.0" dependencies = [ - "derive_more 0.99.18", "lazy_static", "strum_macros", + "thiserror", "tokio", ] @@ -6506,19 +6516,20 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", @@ -6531,9 +6542,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -6543,9 +6554,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6553,9 +6564,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", @@ -6566,18 +6577,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasm-bindgen-test" -version = "0.3.42" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" +checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" dependencies = [ "console_error_panic_hook", "js-sys", + "minicov", "scoped-tls", "wasm-bindgen", "wasm-bindgen-futures", @@ -6586,9 +6598,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.42" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" +checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" dependencies = [ "proc-macro2", "quote", diff --git a/src/cli/generator/config.rs b/src/cli/generator/config.rs index 8ae02e5609..40a101b785 100644 --- a/src/cli/generator/config.rs +++ b/src/cli/generator/config.rs @@ -11,7 +11,7 @@ use url::Url; use crate::core::config::transformer::Preset; use crate::core::config::{self, ConfigReaderContext}; -use crate::core::mustache::Mustache; +use crate::core::mustache::TemplateString; use crate::core::valid::{Valid, ValidateFrom, Validator}; #[derive(Deserialize, Serialize, Debug, Default, Setters)] @@ -24,6 +24,8 @@ pub struct Config { #[serde(skip_serializing_if = "Option::is_none")] pub preset: Option, pub schema: Schema, + #[serde(skip_serializing_if = "TemplateString::is_empty")] + pub secret: TemplateString, } #[derive(Clone, Deserialize, Serialize, Debug, Default)] @@ -48,7 +50,7 @@ pub struct Location( #[derive(Deserialize, Serialize, Debug)] #[serde(transparent)] pub struct Headers( - #[serde(skip_serializing_if = "is_default")] pub Option>, + #[serde(skip_serializing_if = "is_default")] pub Option>, #[serde(skip)] PhantomData, ); @@ -176,30 +178,22 @@ impl Location { } impl Headers { - pub fn headers(&self) -> &Option> { + pub fn headers(&self) -> &Option> { &self.0 } } impl Headers { - pub fn resolve( - self, - reader_context: &ConfigReaderContext, - ) -> anyhow::Result> { + pub fn resolve(self, reader_context: &ConfigReaderContext) -> Headers { // Resolve the header values with mustache template. - let resolved_headers = if let Some(headers_inner) = self.0 { - let mut resolved_headers = BTreeMap::new(); - for (key, value) in headers_inner.into_iter() { - let template = Mustache::parse(&value)?; - let resolved_value = template.render(reader_context); - resolved_headers.insert(key, resolved_value); - } - Some(resolved_headers) - } else { - None - }; - - Ok(Headers(resolved_headers, PhantomData)) + let resolved_headers = self.0.map(|headers_inner| { + headers_inner + .into_iter() + .map(|(k, v)| (k, v.resolve(reader_context))) + .collect::>() + }); + + Headers(resolved_headers, PhantomData) } } @@ -221,7 +215,7 @@ impl Source { match self { Source::Curl { src, field_name, headers } => { let resolved_path = src.into_resolved(parent_dir); - let resolved_headers = headers.resolve(reader_context)?; + let resolved_headers = headers.resolve(reader_context); Ok(Source::Curl { src: resolved_path, field_name, headers: resolved_headers }) } Source::Proto { src } => { @@ -264,7 +258,13 @@ impl Config { let output = self.output.resolve(parent_dir)?; - Ok(Config { inputs, output, schema: self.schema, preset: self.preset }) + Ok(Config { + inputs, + output, + schema: self.schema, + preset: self.preset, + secret: self.secret.resolve(&reader_context), + }) } } @@ -283,7 +283,7 @@ mod tests { Location(s.as_ref().to_string(), PhantomData) } - fn to_headers(raw_headers: BTreeMap) -> Headers { + fn to_headers(raw_headers: BTreeMap) -> Headers { Headers(Some(raw_headers), PhantomData) } @@ -292,7 +292,7 @@ mod tests { let mut headers = BTreeMap::new(); headers.insert( "Authorization".to_owned(), - "Bearer {{.env.TOKEN}}".to_owned(), + "Bearer {{.env.TOKEN}}".try_into().unwrap(), ); let mut env_vars = HashMap::new(); @@ -310,18 +310,19 @@ mod tests { headers: Default::default(), }; - let resolved_headers = unresolved_headers.resolve(&reader_ctx).unwrap(); + let resolved_headers = unresolved_headers.resolve(&reader_ctx); - let expected = format!("Bearer {token}"); - let result = resolved_headers + let expected = TemplateString::try_from(format!("Bearer {token}").as_str()).unwrap(); + let actual = resolved_headers .headers() - .to_owned() + .as_ref() .unwrap() .get("Authorization") .unwrap() .to_owned(); + assert_eq!( - result, expected, + actual, expected, "Authorization header should be resolved correctly" ); } @@ -329,7 +330,7 @@ mod tests { #[test] fn test_config_codec() { let mut headers = BTreeMap::new(); - headers.insert("user-agent".to_owned(), "tailcall-v1".to_owned()); + headers.insert("user-agent".to_owned(), "tailcall-v1".try_into().unwrap()); let config = Config::default().inputs(vec![Input { source: Source::Curl { src: location("https://example.com"), @@ -402,7 +403,7 @@ mod tests { fn test_raise_error_unknown_field_at_root_level() { let json = r#"{"input": "value"}"#; let expected_error = - "unknown field `input`, expected one of `inputs`, `output`, `preset`, `schema` at line 1 column 8"; + "unknown field `input`, expected one of `inputs`, `output`, `preset`, `schema`, `secret` at line 1 column 8"; assert_deserialization_error(json, expected_error); } @@ -472,4 +473,29 @@ mod tests { let expected_error = "unknown field `querys`, expected `query` at line 3 column 22"; assert_deserialization_error(json, expected_error); } + + #[test] + fn test_secret() { + let mut env_vars = HashMap::new(); + let token = "eyJhbGciOiJIUzI1NiIsInR5"; + env_vars.insert("TAILCALL_SECRET".to_owned(), token.to_owned()); + + let mut runtime = crate::core::runtime::test::init(None); + runtime.env = Arc::new(TestEnvIO::init(env_vars)); + + let reader_ctx = ConfigReaderContext { + runtime: &runtime, + vars: &Default::default(), + headers: Default::default(), + }; + + let config = + Config::default().secret(TemplateString::parse("{{.env.TAILCALL_SECRET}}").unwrap()); + let resolved_config = config.into_resolved("", reader_ctx).unwrap(); + + let actual = resolved_config.secret; + let expected = TemplateString::try_from("eyJhbGciOiJIUzI1NiIsInR5").unwrap(); + + assert_eq!(actual, expected); + } } diff --git a/src/cli/generator/generator.rs b/src/cli/generator/generator.rs index 4b52ebf99a..8e0efe05df 100644 --- a/src/cli/generator/generator.rs +++ b/src/cli/generator/generator.rs @@ -112,7 +112,7 @@ impl Generator { let mut header_map = HeaderMap::new(); for (key, value) in headers_inner { let header_name = HeaderName::try_from(key)?; - let header_value = HeaderValue::try_from(value)?; + let header_value = HeaderValue::try_from(value.to_string())?; header_map.insert(header_name, header_value); } *request.headers_mut() = header_map; @@ -150,6 +150,7 @@ impl Generator { let config = self.read().await?; let path = config.output.path.0.to_owned(); let query_type = config.schema.query.clone(); + let secret = config.secret.clone(); let preset = config.preset.clone().unwrap_or_default(); let preset: Preset = preset.validate_into().to_result()?; let input_samples = self.resolve_io(config).await?; @@ -165,11 +166,12 @@ impl Generator { let mut config = config_gen.generate(true)?; if infer_type_names { - let key = self - .runtime - .env - .get("TAILCALL_SECRET") - .map(|s| s.into_owned()); + let key = if !secret.is_empty() { + Some(secret.to_string()) + } else { + None + }; + let mut llm_gen = InferTypeName::new(key); let suggested_names = llm_gen.generate(config.config()).await?; let cfg = RenameTypes::new(suggested_names.iter()) diff --git a/src/cli/llm/wizard.rs b/src/cli/llm/wizard.rs index 862e205b84..1604d7f15f 100644 --- a/src/cli/llm/wizard.rs +++ b/src/cli/llm/wizard.rs @@ -1,6 +1,7 @@ use derive_setters::Setters; use genai::adapter::AdapterKind; use genai::chat::{ChatOptions, ChatRequest, ChatResponse}; +use genai::resolver::AuthResolver; use genai::Client; use super::Result; @@ -18,18 +19,18 @@ impl Wizard { pub fn new(model: Model, secret: Option) -> Self { let mut config = genai::adapter::AdapterConfig::default(); if let Some(key) = secret { - config = config.with_auth_env_name(key); + config = config.with_auth_resolver(AuthResolver::from_key_value(key)); } let adapter = AdapterKind::from_model(model.as_str()).unwrap_or(AdapterKind::Ollama); + let chat_options = ChatOptions::default() + .with_json_mode(true) + .with_temperature(0.0); + Self { client: Client::builder() - .with_chat_options( - ChatOptions::default() - .with_json_mode(true) - .with_temperature(0.0), - ) + .with_chat_options(chat_options) .insert_adapter_config(adapter, config) .build(), model, diff --git a/src/core/mustache/mod.rs b/src/core/mustache/mod.rs index 44ef01eda8..964a0ee6bd 100644 --- a/src/core/mustache/mod.rs +++ b/src/core/mustache/mod.rs @@ -1,6 +1,8 @@ mod eval; mod model; mod parse; +mod template_string; pub use eval::Eval; pub use model::*; +pub use template_string::TemplateString; diff --git a/src/core/mustache/model.rs b/src/core/mustache/model.rs index e68aa42325..af18d19e68 100644 --- a/src/core/mustache/model.rs +++ b/src/core/mustache/model.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq, Hash, Default)] pub struct Mustache(Vec); #[derive(Debug, Clone, PartialEq, Hash)] diff --git a/src/core/mustache/template_string.rs b/src/core/mustache/template_string.rs new file mode 100644 index 0000000000..9a66b63fa8 --- /dev/null +++ b/src/core/mustache/template_string.rs @@ -0,0 +1,140 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use super::{Mustache, Segment}; +use crate::core::path::PathString; + +/// TemplateString acts as wrapper over mustache but supports serialization and +/// deserialization. It provides utilities for parsing, resolving, and comparing +/// template strings. +#[derive(Debug, derive_more::Display, Default, Clone)] +pub struct TemplateString(Mustache); + +impl PartialEq for TemplateString { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl TryFrom<&str> for TemplateString { + type Error = anyhow::Error; + fn try_from(value: &str) -> anyhow::Result { + Ok(Self(Mustache::parse(value)?)) + } +} + +impl TemplateString { + pub fn is_empty(&self) -> bool { + self.0.to_string().is_empty() + } + + pub fn parse(value: &str) -> anyhow::Result { + Ok(Self(Mustache::parse(value)?)) + } + + pub fn resolve(&self, ctx: &impl PathString) -> Self { + let resolved_secret = Mustache::from(vec![Segment::Literal(self.0.render(ctx))]); + Self(resolved_secret) + } +} + +impl Serialize for TemplateString { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.0.to_string()) + } +} + +impl<'de> Deserialize<'de> for TemplateString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let template_string = String::deserialize(deserializer)?; + let mustache = Mustache::parse(&template_string).map_err(serde::de::Error::custom)?; + + Ok(TemplateString(mustache)) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::sync::Arc; + + use crate::core::config::ConfigReaderContext; + use crate::core::mustache::TemplateString; + use crate::core::tests::TestEnvIO; + use crate::core::Mustache; + + #[test] + fn test_default() { + let default_template = TemplateString::default(); + assert!(default_template.is_empty()); + } + + #[test] + fn test_from_str() { + let template_str = "Hello, World!"; + let template = TemplateString::try_from(template_str).unwrap(); + assert_eq!(template.0.to_string(), template_str); + } + + #[test] + fn test_is_empty() { + let empty_template = TemplateString::default(); + assert!(empty_template.is_empty()); + + let non_empty_template = TemplateString::try_from("Hello").unwrap(); + assert!(!non_empty_template.is_empty()); + } + + #[test] + fn test_parse() { + let actual = TemplateString::parse("{{.env.TAILCALL_SECRET}}").unwrap(); + let expected = Mustache::parse("{{.env.TAILCALL_SECRET}}").unwrap(); + assert_eq!(actual.0, expected); + } + + #[test] + fn test_resolve() { + let mut env_vars = HashMap::new(); + let token = "eyJhbGciOiJIUzI1NiIsInR5"; + env_vars.insert("TAILCALL_SECRET".to_owned(), token.to_owned()); + + let mut runtime = crate::core::runtime::test::init(None); + runtime.env = Arc::new(TestEnvIO::init(env_vars)); + + let ctx = ConfigReaderContext { + runtime: &runtime, + vars: &Default::default(), + headers: Default::default(), + }; + + let actual = TemplateString::parse("{{.env.TAILCALL_SECRET}}") + .unwrap() + .resolve(&ctx); + let expected = TemplateString::try_from("eyJhbGciOiJIUzI1NiIsInR5").unwrap(); + + assert_eq!(actual, expected); + } + + #[test] + fn test_serialize() { + let template = TemplateString::try_from("{{.env.TEST}}").unwrap(); + let serialized = serde_json::to_string(&template).unwrap(); + assert_eq!(serialized, "\"{{env.TEST}}\""); + } + + #[test] + fn test_deserialize() { + let serialized = "\"{{.env.TEST}}\""; + let template: TemplateString = serde_json::from_str(serialized).unwrap(); + + let actual = template.0; + let expected = Mustache::parse("{{.env.TEST}}").unwrap(); + + assert_eq!(actual, expected); + } +} diff --git a/tailcall-prettier/Cargo.toml b/tailcall-prettier/Cargo.toml index 5889cc37e3..ea5597e7d3 100644 --- a/tailcall-prettier/Cargo.toml +++ b/tailcall-prettier/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -derive_more = { workspace = true } lazy_static = "1.4.0" strum_macros = "0.26.2" +thiserror.workspace = true tokio.workspace = true diff --git a/tailcall-prettier/src/error.rs b/tailcall-prettier/src/error.rs index 40edd1bb5c..e975bcbbed 100644 --- a/tailcall-prettier/src/error.rs +++ b/tailcall-prettier/src/error.rs @@ -1,49 +1,44 @@ -use std::fmt::Display; +use std::borrow::Cow; use std::string::FromUtf8Error; use std::sync::Arc; -use derive_more::{DebugCustom, From}; +use thiserror::Error; use tokio::task::JoinError; -#[derive(From, DebugCustom)] +#[derive(Error)] pub enum Error { - #[debug(fmt = "Std IO Error: {}", _0)] - IO(std::io::Error), + #[error("Std IO Error: {0}")] + IO(#[from] std::io::Error), - #[debug(fmt = "Join Error: {}", _0)] - Join(JoinError), + #[error("Join Error: {0}")] + Join(#[from] JoinError), - #[debug(fmt = "From Utf8 Error: {}", _0)] - FromUtf8(FromUtf8Error), + #[error("From Utf8 Error: {0}")] + FromUtf8(#[from] FromUtf8Error), - #[debug(fmt = "Prettier formatting failed: {}", _0)] + #[error("Prettier formatting failed: {0}")] PrettierFormattingFailed(String), - #[debug(fmt = "No file extension found")] + #[error("{0} command was not found. Ensure you have it installed and available in the PATH")] + CommandNotFound(String), + + #[error("No file extension found")] FileExtensionNotFound, - #[debug(fmt = "Unsupported file type")] + #[error("Unsupported file type")] UnsupportedFiletype, - #[debug(fmt = "{}\n\nCaused by:\n {}", context, source)] - Context { source: Arc, context: String }, + #[error("{}\n\nCaused by:\n {}", context, source)] + Context { + #[source] + source: Arc, + context: String, + }, } -impl Display for Error { +impl std::fmt::Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::IO(error) => write!(f, "Std IO Error: {}", error), - Error::Join(error) => write!(f, "Join Error: {}", error), - Error::FromUtf8(error) => write!(f, "From Utf8 Error: {}", error), - Error::PrettierFormattingFailed(msg) => { - write!(f, "Prettier formatting failed: {}", msg) - } - Error::FileExtensionNotFound => write!(f, "No file extension found"), - Error::UnsupportedFiletype => write!(f, "Unsupported file type"), - Error::Context { source, context } => { - write!(f, "{}\n\nCaused by:\n {}", context, source) - } - } + std::fmt::Display::fmt(self, f) } } @@ -51,6 +46,13 @@ impl Error { pub fn with_context(self, context: String) -> Self { Error::Context { source: Arc::new(self), context } } + + pub fn from_io_error(command: Cow<'static, str>) -> impl Fn(std::io::Error) -> Self { + move |error| match error.kind() { + std::io::ErrorKind::NotFound => Error::CommandNotFound(command.to_string()), + _ => Error::IO(error), + } + } } pub type Result = std::result::Result; diff --git a/tailcall-prettier/src/prettier.rs b/tailcall-prettier/src/prettier.rs index 1129452aa3..639fe7a479 100644 --- a/tailcall-prettier/src/prettier.rs +++ b/tailcall-prettier/src/prettier.rs @@ -71,7 +71,11 @@ impl Prettier { child = child.arg("--config").arg(config); } - let mut child = child.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?; + let mut child = child + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .map_err(Error::from_io_error("prettier".into()))?; if let Some(ref mut stdin) = child.stdin { stdin.write_all(source.as_bytes())?;