From 4c07d0a2f610e58bcae26ba437a28d8fd56f6ee7 Mon Sep 17 00:00:00 2001 From: Natoandro Date: Tue, 26 Nov 2024 09:17:20 +0300 Subject: [PATCH] separate crate for metagen-client-rs --- .ghjk/lock.json | 20 +- Cargo.lock | 115 +- Cargo.toml | 10 +- src/metagen-client-rs/Cargo.toml | 19 + src/metagen-client-rs/src/args.rs | 164 + src/metagen-client-rs/src/files.rs | 274 ++ src/metagen-client-rs/src/graphql.rs | 1125 +++++++ src/metagen-client-rs/src/lib.rs | 33 + src/metagen-client-rs/src/nodes.rs | 266 ++ src/metagen-client-rs/src/selection.rs | 800 +++++ src/metagen/src/client_rs/mod.rs | 35 +- src/metagen/src/client_rs/static/Cargo.toml | 19 - src/metagen/src/client_rs/static/client.rs | 2635 +--------------- src/metagen/src/client_rs/static/lib.rs | 1 - tests/metagen/typegraphs/sample/rs/Cargo.toml | 22 +- tests/metagen/typegraphs/sample/rs/client.rs | 2640 +--------------- tests/metagen/typegraphs/sample/rs/main.rs | 1 + .../typegraphs/sample/rs_upload/Cargo.toml | 21 +- .../typegraphs/sample/rs_upload/client.rs | 2642 +---------------- .../typegraphs/sample/rs_upload/main.rs | 1 + tests/metagen/typegraphs/sample/ts/client.ts | 2 +- 21 files changed, 2830 insertions(+), 8015 deletions(-) create mode 100644 src/metagen-client-rs/Cargo.toml create mode 100644 src/metagen-client-rs/src/args.rs create mode 100644 src/metagen-client-rs/src/files.rs create mode 100644 src/metagen-client-rs/src/graphql.rs create mode 100644 src/metagen-client-rs/src/lib.rs create mode 100644 src/metagen-client-rs/src/nodes.rs create mode 100644 src/metagen-client-rs/src/selection.rs delete mode 100644 src/metagen/src/client_rs/static/Cargo.toml delete mode 100644 src/metagen/src/client_rs/static/lib.rs diff --git a/.ghjk/lock.json b/.ghjk/lock.json index f46b7e3d5e..dc28719b40 100644 --- a/.ghjk/lock.json +++ b/.ghjk/lock.json @@ -1082,37 +1082,37 @@ "ty": "denoFile@v1", "key": "dev-gate6", "desc": "Launch the typegate from a locally found meta bin.", - "envKey": "bciqcljozrbuwh7aum6v6soif6qr2nvpwr7gbyxrmob3ybh4bsxpqfyy" + "envKey": "bciqk7rioqxzcpzf572poq2vwefpcmqnboofojgkgonvpih3q6wwyldq" }, "dev-gate5": { "ty": "denoFile@v1", "key": "dev-gate5", "desc": "Launch the typegate from the latests published image.", - "envKey": "bciqcljozrbuwh7aum6v6soif6qr2nvpwr7gbyxrmob3ybh4bsxpqfyy" + "envKey": "bciqk7rioqxzcpzf572poq2vwefpcmqnboofojgkgonvpih3q6wwyldq" }, "dev-gate4": { "ty": "denoFile@v1", "key": "dev-gate4", "desc": "Launch the typegate from the locally built typegate image.", - "envKey": "bciqcljozrbuwh7aum6v6soif6qr2nvpwr7gbyxrmob3ybh4bsxpqfyy" + "envKey": "bciqk7rioqxzcpzf572poq2vwefpcmqnboofojgkgonvpih3q6wwyldq" }, "dev-gate3": { "ty": "denoFile@v1", "key": "dev-gate3", "desc": "Launch the typegate from meta-cli cmd.", - "envKey": "bciqcljozrbuwh7aum6v6soif6qr2nvpwr7gbyxrmob3ybh4bsxpqfyy" + "envKey": "bciqk7rioqxzcpzf572poq2vwefpcmqnboofojgkgonvpih3q6wwyldq" }, "dev-gate2": { "ty": "denoFile@v1", "key": "dev-gate2", "desc": "Launch the typegate in sync mode.", - "envKey": "bciqgfe63ayh7e7kzg4f47bmaleew7jcdukchs3cg45tvdiwoxotxzfy" + "envKey": "bciqe4fan2davv7bngzw6aygwwbrd7vjviea4rylpwikafl4kqyaxyuq" }, "dev-gate1": { "ty": "denoFile@v1", "key": "dev-gate1", "desc": "Launch the typegate in single-instance mode.", - "envKey": "bciqcljozrbuwh7aum6v6soif6qr2nvpwr7gbyxrmob3ybh4bsxpqfyy" + "envKey": "bciqk7rioqxzcpzf572poq2vwefpcmqnboofojgkgonvpih3q6wwyldq" }, "dev-eg-tgraphs": { "ty": "denoFile@v1", @@ -1561,7 +1561,7 @@ } ] }, - "bciqcljozrbuwh7aum6v6soif6qr2nvpwr7gbyxrmob3ybh4bsxpqfyy": { + "bciqk7rioqxzcpzf572poq2vwefpcmqnboofojgkgonvpih3q6wwyldq": { "provides": [ { "ty": "posix.envVar", @@ -1596,7 +1596,7 @@ { "ty": "posix.envVar", "key": "LOG_LEVEL", - "val": "DEBUG" + "val": "DEBUG,substantial=ERROR" }, { "ty": "posix.envVar", @@ -1629,7 +1629,7 @@ } ] }, - "bciqgfe63ayh7e7kzg4f47bmaleew7jcdukchs3cg45tvdiwoxotxzfy": { + "bciqe4fan2davv7bngzw6aygwwbrd7vjviea4rylpwikafl4kqyaxyuq": { "provides": [ { "ty": "posix.envVar", @@ -1664,7 +1664,7 @@ { "ty": "posix.envVar", "key": "LOG_LEVEL", - "val": "DEBUG" + "val": "DEBUG,substantial=ERROR" }, { "ty": "posix.envVar", diff --git a/Cargo.lock b/Cargo.lock index b66630a110..254cbb4ff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1462,20 +1462,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" -[[package]] -name = "client_rs_static" -version = "0.0.1" -dependencies = [ - "derive_more 1.0.0", - "futures", - "lazy_static 1.5.0", - "mime_guess", - "reqwest", - "serde", - "serde_json", - "tokio-util 0.7.11", -] - [[package]] name = "clipboard-win" version = "5.4.0" @@ -6076,7 +6062,7 @@ dependencies = [ "socket2 0.5.7", "widestring", "windows-sys 0.48.0", - "winreg 0.50.0", + "winreg", ] [[package]] @@ -6984,6 +6970,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "metagen-client" +version = "0.5.0-rc.6" +dependencies = [ + "derive_more 1.0.0", + "futures", + "lazy_static 1.5.0", + "mime_guess", + "reqwest", + "serde", + "serde_json", + "tokio-util 0.7.11", +] + [[package]] name = "metagen_fdk_rust_static" version = "0.0.1" @@ -9621,9 +9621,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64 0.22.1", "bytes", @@ -9636,6 +9636,7 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.4.1", + "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", @@ -9651,7 +9652,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "system-configuration", "tokio", "tokio-native-tls", @@ -9662,7 +9663,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg 0.52.0", + "windows-registry", ] [[package]] @@ -10144,6 +10145,27 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sample_client" +version = "0.5.0-rc.6" +dependencies = [ + "metagen-client", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "sample_client_upload" +version = "0.5.0-rc.6" +dependencies = [ + "metagen-client", + "serde", + "serde_json", + "tokio", + "tokio-util 0.7.11", +] + [[package]] name = "saturating" version = "0.1.0" @@ -11794,6 +11816,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -11840,20 +11865,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -14162,7 +14187,7 @@ checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" dependencies = [ "windows-implement", "windows-interface", - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -14188,6 +14213,17 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -14197,6 +14233,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -14373,16 +14428,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winres" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index bdab3d2acd..1486a1d40f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,19 +5,20 @@ members = [ "src/meta-cli", "src/metagen", "src/metagen/src/fdk_rust/static", - "src/metagen/src/client_rs/static", "src/mt_deno", "src/typegate/engine", "src/typegate/standalone", "src/typegraph/core", "src/xtask", - "src/substantial" -] + "src/substantial", + "src/metagen-client-rs", + "tests/metagen/typegraphs/sample/rs", + "tests/metagen/typegraphs/sample/rs_upload", + ] exclude = [ "tests/runtimes/wasm_reflected/rust", "tests/runtimes/wasm_wire/rust", - "tests/metagen/typegraphs/sample/rs", "tests/metagen/typegraphs/sample/rs_upload", "src/pyrt_wit_wire", ] @@ -33,6 +34,7 @@ mt_deno = { path = "src/mt_deno/" } common = { path = "src/common/" } substantial = { path = "src/substantial/" } metagen = { path = "src/metagen/" } +metagen-client = { path = "src/metagen-client-rs" } typegate_engine = { path = "src/typegate/engine" } # cli diff --git a/src/metagen-client-rs/Cargo.toml b/src/metagen-client-rs/Cargo.toml new file mode 100644 index 0000000000..d920a31dea --- /dev/null +++ b/src/metagen-client-rs/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "metagen-client" +version.workspace = true +edition.workspace = true + +[dependencies] +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" +reqwest = { version = "0.12.9", features = ["blocking", "json", "stream", "multipart"] } +mime_guess = "2.0" +futures = { version = "0.3" } +tokio-util = { version = "0.7", features = ["compat", "io"] } +derive_more = { version = "1.0", features = ["debug"] } +lazy_static = "1.5" + +# [features] +# default = ["sync"] +# sync = ["reqwest/blocking"] +# async = ["dep:futures", "dep:tokio-util"] diff --git a/src/metagen-client-rs/src/args.rs b/src/metagen-client-rs/src/args.rs new file mode 100644 index 0000000000..978e5f20da --- /dev/null +++ b/src/metagen-client-rs/src/args.rs @@ -0,0 +1,164 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +use crate::interlude::*; + +pub enum NodeArgs { + Inline(ArgT), + Placeholder(PlaceholderValue), +} + +impl From for NodeArgs { + fn from(value: ArgT) -> Self { + Self::Inline(value) + } +} + +#[derive(Debug)] +pub enum NodeArgsErased { + None, + Inline(serde_json::Value), + Placeholder(PlaceholderValue), +} + +impl From> for NodeArgsErased +where + ArgT: Serialize, +{ + fn from(value: NodeArgs) -> Self { + match value { + NodeArgs::Inline(arg) => Self::Inline(to_json_value(arg)), + NodeArgs::Placeholder(ph) => Self::Placeholder(ph), + } + } +} + +pub enum NodeArgsMerged { + Inline(HashMap), + Placeholder { + value: PlaceholderValue, + arg_types: HashMap, + }, +} + +/// This checks the input arg json for a node +/// against the arg description from the [`NodeMeta`]. +pub(crate) fn check_node_args( + args: serde_json::Value, + arg_types: &HashMap, +) -> Result, String> { + let args = match args { + serde_json::Value::Object(val) => val, + _ => unreachable!(), + }; + let mut instance_args = HashMap::new(); + for (name, value) in args { + let Some(type_name) = arg_types.get(&name[..]) else { + return Err(name); + }; + instance_args.insert( + name.into(), + NodeArgValue { + type_name: type_name.clone(), + value, + }, + ); + } + Ok(instance_args) +} + +pub struct NodeArgValue { + pub type_name: CowStr, + pub value: serde_json::Value, +} + +pub struct PreparedArgs; + +impl PreparedArgs { + pub fn get(&mut self, key: impl Into, fun: F) -> NodeArgs + where + In: serde::de::DeserializeOwned, + F: Fn(In) -> ArgT + 'static + Send + Sync, + ArgT: Serialize, + { + NodeArgs::Placeholder(PlaceholderValue { + key: key.into(), + fun: Box::new(move |value| { + let value = serde_json::from_value(value)?; + let value = fun(value); + serde_json::to_value(value) + }), + }) + } + pub fn arg(&mut self, key: impl Into, fun: F) -> T + where + T: From>, + In: serde::de::DeserializeOwned, + F: Fn(In) -> ArgT + 'static + Send + Sync, + ArgT: Serialize, + { + T::from(PlaceholderArg { + value: PlaceholderValue { + key: key.into(), + fun: Box::new(move |value| { + let value = serde_json::from_value(value)?; + let value = fun(value); + serde_json::to_value(value) + }), + }, + _phantom: PhantomData, + }) + } + pub fn arg_select( + &mut self, + key: impl Into, + selection: SelT, + fun: F, + ) -> T + where + T: From>, + In: serde::de::DeserializeOwned, + F: Fn(In) -> ArgT + 'static + Send + Sync, + ArgT: Serialize, + { + T::from(PlaceholderArgSelect { + value: PlaceholderValue { + key: key.into(), + fun: Box::new(move |value| { + let value = serde_json::from_value(value)?; + let value = fun(value); + serde_json::to_value(value) + }), + }, + selection, + _phantom: PhantomData, + }) + } +} + +pub struct PlaceholderValue { + pub key: CowStr, + pub fun: Box< + dyn Fn(serde_json::Value) -> Result + Send + Sync, + >, +} + +impl std::fmt::Debug for PlaceholderValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PlaceholderValue") + .field("key", &self.key) + .finish_non_exhaustive() + } +} + +pub struct PlaceholderArg { + pub value: PlaceholderValue, + _phantom: PhantomData, +} +pub struct PlaceholderArgSelect { + pub value: PlaceholderValue, + pub selection: SelT, + _phantom: PhantomData, +} + +pub struct PlaceholderArgs(Arg); diff --git a/src/metagen-client-rs/src/files.rs b/src/metagen-client-rs/src/files.rs new file mode 100644 index 0000000000..ec866accc5 --- /dev/null +++ b/src/metagen-client-rs/src/files.rs @@ -0,0 +1,274 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +use crate::interlude::*; + +#[derive(Debug, Clone)] +pub struct TypePath(pub &'static [&'static str]); + +// fn path_segment_as_prop(segment: &str) -> Option<&str> { +// segment.strip_prefix('.') +// } +// +#[derive(Debug, Clone)] +pub struct PathToInputFiles(pub &'static [&'static [&'static str]]); + +#[derive(Debug)] +pub enum ValuePathSegment { + Optional, + Index(usize), + Prop(&'static str), +} + +#[derive(Default, Debug)] +pub struct ValuePath(Vec); + +lazy_static::lazy_static! { + static ref LATEST_FILE_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + static ref FILE_STORE: std::sync::Mutex> = Default::default(); +} + +enum FileData { + Path(std::path::PathBuf), + Bytes(Vec), + Reader(Box), + Async(reqwest::Body), +} + +pub struct File { + data: FileData, + file_name: Option, + mime_type: Option, +} + +#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Copy)] +pub struct FileId(usize); + +impl TryFrom for FileId { + type Error = BoxErr; + + fn try_from(file: File) -> Result { + let file_id = LATEST_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let mut guard = FILE_STORE.lock().map_err(|_| "file store lock poisoned")?; + guard.insert(FileId(file_id), file); + Ok(FileId(file_id)) + } +} + +impl TryFrom for File { + type Error = BoxErr; + + fn try_from(file_id: FileId) -> Result { + let mut guard = FILE_STORE.lock().map_err(|_| "file store lock poisoned")?; + let file = guard.remove(&file_id).ok_or("file not found")?; + if file.file_name.is_none() { + Ok(file.file_name(file_id.0.to_string())) + } else { + Ok(file) + } + } +} + +impl File { + pub fn from_path>(path: P) -> Self { + Self { + data: FileData::Path(path.into()), + file_name: None, + mime_type: None, + } + } + + pub fn from_bytes>>(data: B) -> Self { + Self { + data: FileData::Bytes(data.into()), + file_name: None, + mime_type: None, + } + } + + pub fn from_reader(reader: R) -> Self { + Self { + data: FileData::Reader(Box::new(reader)), + file_name: None, + mime_type: None, + } + } + + pub fn from_async_reader(reader: R) -> Self { + use tokio_util::compat::FuturesAsyncReadCompatExt as _; + let reader = reader.compat(); + Self { + data: FileData::Async(reqwest::Body::wrap_stream( + tokio_util::io::ReaderStream::new(reader), + )), + file_name: None, + mime_type: None, + } + } +} + +impl File { + pub fn file_name(mut self, file_name: impl Into) -> Self { + self.file_name = Some(file_name.into()); + self + } + + pub fn mime_type(mut self, mime_type: impl Into) -> Self { + self.mime_type = Some(mime_type.into()); + self + } +} + +impl TryFrom for reqwest::blocking::multipart::Part { + type Error = BoxErr; + + fn try_from(file: File) -> Result { + let mut part = match file.data { + FileData::Path(path) => { + let file = std::fs::File::open(path.as_path())?; + let file_size = file.metadata()?.len(); + let mut part = + reqwest::blocking::multipart::Part::reader_with_length(file, file_size); + if let Some(name) = path.file_name() { + part = part.file_name(name.to_string_lossy().into_owned()); + } + part = part.mime_str( + mime_guess::from_path(&path) + .first_or_octet_stream() + .as_ref(), + )?; + part + } + + FileData::Bytes(data) => reqwest::blocking::multipart::Part::bytes(data), + + FileData::Reader(reader) => reqwest::blocking::multipart::Part::reader(reader), + + FileData::Async(_) => { + return Err("async readers are not supported".into()); + } + }; + + if let Some(file_name) = file.file_name { + part = part.file_name(file_name); + } + if let Some(mime_type) = file.mime_type { + part = part.mime_str(&mime_type)?; + } + Ok(part) + } +} + +impl File { + pub(crate) async fn into_reqwest_part(self) -> Result { + let mut part = match self.data { + FileData::Path(path) => reqwest::multipart::Part::file(path).await?, + FileData::Bytes(data) => reqwest::multipart::Part::bytes(data), + FileData::Async(body) => reqwest::multipart::Part::stream(body), + FileData::Reader(_) => { + return Err("sync readers are not supported".into()); + } + }; + + if let Some(file_name) = self.file_name { + part = part.file_name(file_name); + } + if let Some(mime_type) = self.mime_type { + part = part.mime_str(&mime_type)?; + } + Ok(part) + } +} + +#[derive(Debug)] +pub(crate) struct FileExtractor { + path: TypePath, + prefix: String, + current_path: ValuePath, + output: HashMap, +} + +impl FileExtractor { + pub fn extract_all_from( + variables: &mut JsonObject, + mut path_to_files: HashMap>, + ) -> Result, BoxErr> { + let mut output = HashMap::new(); + + for (key, value) in variables.iter_mut() { + let paths = path_to_files.remove(key).unwrap_or_default(); + for path in paths.into_iter() { + let mut extractor = Self { + path, + prefix: key.clone(), + current_path: ValuePath::default(), + output: std::mem::take(&mut output), + }; + extractor.extract_from_value(value)?; + output = extractor.output; + } + } + + Ok(output) + } + + fn extract_from_value(&mut self, value: &mut serde_json::Value) -> Result<(), BoxErr> { + let cursor = self.current_path.0.len(); + if cursor == self.path.0.len() { + // end of type_path; replace file_id with null + let file_id: FileId = serde_json::from_value(value.take())?; + self.output.insert(self.format_path(), file_id); + return Ok(()); + } + let segment = self.path.0[cursor]; + use ValuePathSegment as VPSeg; + match segment { + "?" => { + if !value.is_null() { + self.current_path.0.push(VPSeg::Optional); + self.extract_from_value(value)?; + self.current_path.0.pop(); + } + } + "[]" => { + let items = value + .as_array_mut() + .ok_or_else(|| format!("expected an array at {:?}", self.format_path()))?; + for (idx, item) in items.iter_mut().enumerate() { + self.current_path.0.push(VPSeg::Index(idx)); + self.extract_from_value(item)?; + self.current_path.0.pop(); + } + } + x if x.starts_with('.') => { + let key = &x[1..]; + let object = value + .as_object_mut() + .ok_or_else(|| format!("expected an object at {:?}", self.format_path()))?; + let mut null = serde_json::Value::Null; + let value = object.get_mut(key).unwrap_or(&mut null); + self.current_path.0.push(VPSeg::Prop(key)); + self.extract_from_value(value)?; + self.current_path.0.pop(); + } + _ => unreachable!(), + } + + Ok(()) + } + + /// format the path following the GraphQL multipart request spec + /// see: https://github.com/jaydenseric/graphql-multipart-request-spec + fn format_path(&self) -> String { + let mut res = self.prefix.clone(); + use ValuePathSegment as VPSeg; + for seg in &self.current_path.0 { + match seg { + VPSeg::Optional => {} + VPSeg::Index(idx) => res.push_str(&format!(".{}", idx)), + VPSeg::Prop(key) => res.push_str(&format!(".{}", key)), + } + } + res + } +} diff --git a/src/metagen-client-rs/src/graphql.rs b/src/metagen-client-rs/src/graphql.rs new file mode 100644 index 0000000000..2d90cae8f4 --- /dev/null +++ b/src/metagen-client-rs/src/graphql.rs @@ -0,0 +1,1125 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +use crate::args::{NodeArgsMerged, PlaceholderValue, PreparedArgs}; +use crate::files::{File, FileExtractor, PathToInputFiles, TypePath}; +use crate::interlude::*; +use crate::nodes::{SelectNodeErased, SubNodes, ToMutationDoc, ToQueryDoc, ToSelectDoc}; +use std::sync::Arc; + +pub type TyToGqlTyMap = Arc>; + +#[derive(Default, Clone)] +pub struct GraphQlTransportOptions { + headers: reqwest::header::HeaderMap, + timeout: Option, +} + +// PlaceholderValue, fieldName -> gql_var_name +type FoundPlaceholders = Vec<(PlaceholderValue, HashMap)>; + +struct GqlRequest { + doc: String, + variables: JsonObject, + placeholders: FoundPlaceholders, + path_to_files: HashMap>, +} + +struct GqlRequestBuilder<'a> { + ty_to_gql_ty_map: &'a TyToGqlTyMap, + variable_values: JsonObject, + variable_types: HashMap, + // map variable name to path to file types + path_to_files: HashMap>, + doc: String, + placeholders: Vec<(PlaceholderValue, HashMap)>, +} + +impl<'a> GqlRequestBuilder<'a> { + fn new(ty_to_gql_ty_map: &'a TyToGqlTyMap) -> Self { + Self { + ty_to_gql_ty_map, + variable_values: Default::default(), + variable_types: Default::default(), + path_to_files: Default::default(), + doc: Default::default(), + placeholders: Default::default(), + } + } + + fn register_path_to_files(&mut self, name: String, key: &str, files: &PathToInputFiles) { + let path_to_files = files + .0 + .iter() + .filter_map(|path| { + let first = path[0]; + if first.starts_with('.') && &first[1..] == key { + Some(TypePath(&path[1..])) + } else { + None + } + }) + .collect::>(); + self.path_to_files.insert(name, path_to_files); + } + + fn select_node_to_gql(&mut self, node: SelectNodeErased) -> std::fmt::Result { + use std::fmt::Write; + if node.instance_name != node.node_name { + write!(self.doc, "{}: {}", node.instance_name, node.node_name)?; + } else { + write!(self.doc, "{}", node.node_name)?; + } + + if let Some(args) = node.args { + match args { + NodeArgsMerged::Inline(args) => { + if !args.is_empty() { + write!(&mut self.doc, "(")?; + for (key, val) in args { + let name = format!("in{}", self.variable_types.len()); + + let mut map = serde_json::Map::new(); + map.insert(key.clone().into(), val.value.clone()); + let mut object = serde_json::Value::Object(map); + + if let Some(files) = node.input_files.as_ref() { + self.register_path_to_files(name.clone(), key.as_ref(), files); + } + + write!(&mut self.doc, "{key}: ${name}, ")?; + self.variable_values.insert( + name.clone(), + object + .as_object_mut() + .unwrap() + .remove(key.as_ref()) + .unwrap(), + ); + self.variable_types.insert(name.into(), val.type_name); + } + write!(&mut self.doc, ")")?; + } + } + NodeArgsMerged::Placeholder { value, arg_types } => { + if !arg_types.is_empty() { + write!(&mut self.doc, "(")?; + let mut map = HashMap::new(); + for (key, type_name) in arg_types { + let name = format!("in{}", self.variable_types.len()); + if let Some(files) = node.input_files.as_ref() { + self.register_path_to_files(name.clone(), key.as_ref(), files); + } + write!(&mut self.doc, "{key}: ${name}, ")?; + self.variable_types.insert(name.clone().into(), type_name); + map.insert(key, name.into()); + } + write!(&mut self.doc, ")")?; + self.placeholders.push((value, map)); + } + } + } + } + + match node.sub_nodes { + SubNodes::None => {} + SubNodes::Atomic(sub_nodes) => { + write!(&mut self.doc, "{{ ")?; + for node in sub_nodes { + self.select_node_to_gql(node)?; + write!(&mut self.doc, " ")?; + } + write!(&mut self.doc, " }}")?; + } + SubNodes::Union(variants) => { + write!(&mut self.doc, "{{ ")?; + for (ty, sub_nodes) in variants { + let gql_ty = self + .ty_to_gql_ty_map + .get(&ty[..]) + .expect("impossible: no GraphQL type equivalent found for variant type"); + let gql_ty = match gql_ty.strip_suffix('!') { + Some(val) => val, + None => &gql_ty[..], + }; + write!(&mut self.doc, " ... on {gql_ty} {{ ")?; + for node in sub_nodes { + self.select_node_to_gql(node)?; + write!(&mut self.doc, " ")?; + } + write!(&mut self.doc, " }}")?; + } + write!(&mut self.doc, " }}")?; + } + } + Ok(()) + } + + fn build( + mut self, + nodes: Vec, + ty: &'static str, + name: Option, + ) -> Result { + use std::fmt::Write; + + for (idx, node) in nodes.into_iter().enumerate() { + let node = SelectNodeErased { + instance_name: format!("node{idx}").into(), + ..node + }; + write!(&mut self.doc, " ").expect("error building to string"); + self.select_node_to_gql(node) + .expect("error building to string"); + writeln!(&mut self.doc).expect("error building to string"); + } + + let mut args_row = String::new(); + if !self.variable_types.is_empty() { + write!(&mut args_row, "(").expect("error building to string"); + for (key, ty) in &self.variable_types { + let gql_ty = self.ty_to_gql_ty_map.get(&ty[..]).ok_or_else(|| { + GraphQLRequestError::InvalidQuery { + error: Box::from(format!("unknown typegraph type found: {}", ty)), + } + })?; + write!(&mut args_row, "${key}: {gql_ty}, ").expect("error building to string"); + } + write!(&mut args_row, ")").expect("error building to string"); + } + + let name = name.unwrap_or_else(|| "".into()); + let doc = format!("{ty} {name}{args_row} {{\n{doc}}}", doc = self.doc); + Ok(GqlRequest { + doc, + variables: self.variable_values, + placeholders: self.placeholders, + path_to_files: self.path_to_files, + }) + } +} + +// enum GraphQLRequestBody { +// Json(serde_json::Value), +// Multipart(reqwest::multipart::Form), +// } +// +// struct GraphQLRequest { +// addr: Url, +// method: reqwest::Method, +// headers: reqwest::header::HeaderMap, +// body: GraphQLRequestBody, +// } + +use reqwest::blocking::{Client as ClientSync, RequestBuilder as RequestBuilderSync}; + +enum BuildReqError { + FileUpload { error: BoxErr }, +} + +fn build_gql_req_sync( + client: &ClientSync, + addr: Url, + doc: &str, + mut variables: JsonObject, + path_to_files: HashMap>, + opts: &GraphQlTransportOptions, +) -> Result { + use reqwest::blocking::multipart::Form; + + let files = FileExtractor::extract_all_from(&mut variables, path_to_files) + .map_err(|error| BuildReqError::FileUpload { error })?; + + let mut request = client.request(reqwest::Method::POST, addr); + if let Some(timeout) = opts.timeout { + request = request.timeout(timeout); + } + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::ACCEPT, + "application/json".try_into().unwrap(), + ); + headers.extend(opts.headers.clone()); + + let operations = serde_json::json!({ + "query": doc, + "variables": variables + }); + + // TODO rename files + + if !files.is_empty() { + // multipart + let mut form = Form::new(); + + form = form.text("operations", serde_json::to_string(&operations).unwrap()); + + let (map, files): (HashMap<_, _>, Vec<_>) = files + .into_iter() + .enumerate() + .map(|(idx, (path, file_id))| { + ( + (idx.to_string(), vec![format!("variables.{path}")]), + file_id, + ) + }) + .unzip(); + + form = form.text("map", serde_json::to_string(&map).unwrap()); + + for (idx, file_id) in files.into_iter().enumerate() { + let file: File = file_id + .try_into() + .map_err(|error| BuildReqError::FileUpload { error })?; + form = form.part( + idx.to_string(), + file.try_into() + .map_err(|error| BuildReqError::FileUpload { error })?, + ); + } + + Ok(request.headers(headers).multipart(form)) + } else { + headers.insert( + reqwest::header::CONTENT_TYPE, + "application/json".try_into().unwrap(), + ); + Ok(request.headers(headers).json(&operations)) + } +} + +use reqwest::{Client, RequestBuilder, Url}; +async fn build_gql_req( + client: &Client, + addr: Url, + doc: &str, + mut variables: JsonObject, + path_to_files: HashMap>, + opts: &GraphQlTransportOptions, +) -> Result { + use reqwest::multipart::Form; + + let files = FileExtractor::extract_all_from(&mut variables, path_to_files) + .map_err(|error| BuildReqError::FileUpload { error })?; + + let request = client.request(reqwest::Method::POST, addr); + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::ACCEPT, + "application/json".try_into().unwrap(), + ); + headers.extend(opts.headers.clone()); + + let operations = serde_json::json!({ + "query": doc, + "variables": variables + }); + + if !files.is_empty() { + // multipart + let mut form = Form::new(); + + form = form.text("operations", serde_json::to_string(&operations).unwrap()); + + let (map, files): (HashMap<_, _>, Vec<_>) = files + .into_iter() + .enumerate() + .map(|(idx, (path, file_id))| { + ( + (idx.to_string(), vec![format!("variables.{path}")]), + file_id, + ) + }) + .unzip(); + + form = form.text("map", serde_json::to_string(&map).unwrap()); + + for (idx, file_id) in files.into_iter().enumerate() { + let file: File = file_id + .try_into() + .map_err(|error| BuildReqError::FileUpload { error })?; + form = form.part( + idx.to_string(), + file.into_reqwest_part() + .await + .map_err(|error| BuildReqError::FileUpload { error })?, + ); + } + + Ok(request.headers(headers).multipart(form)) + } else { + headers.insert( + reqwest::header::CONTENT_TYPE, + "application/json".try_into().unwrap(), + ); + Ok(request.headers(headers).json(&operations)) + } +} + +#[derive(Debug)] +pub struct GraphQLResponse { + pub status: reqwest::StatusCode, + pub headers: reqwest::header::HeaderMap, + pub body: JsonObject, +} + +fn handle_response( + response: GraphQLResponse, + nodes_len: usize, +) -> Result, GraphQLRequestError> { + if !response.status.is_success() { + return Err(GraphQLRequestError::RequestFailed { response }); + } + #[derive(Debug, Deserialize)] + struct Response { + data: Option, + errors: Option>, + } + let body: Response = match serde_json::from_value(serde_json::Value::Object(response.body)) { + Ok(body) => body, + Err(error) => { + return Err(GraphQLRequestError::BodyError { + error: Box::new(error), + }) + } + }; + if let Some(errors) = body.errors { + return Err(GraphQLRequestError::RequestErrors { + errors, + data: body.data, + }); + } + let Some(mut body) = body.data else { + return Err(GraphQLRequestError::BodyError { + error: Box::from("body response doesn't contain data field"), + }); + }; + (0..nodes_len) + .map(|idx| { + body.remove(&format!("node{idx}")) + .ok_or_else(|| GraphQLRequestError::BodyError { + error: Box::from(format!( + "expecting response under node key 'node{idx}' but none found" + )), + }) + }) + .collect::, _>>() +} + +#[derive(Debug)] +pub enum GraphQLRequestError { + /// GraphQL errors recieived + RequestErrors { + errors: Vec, + data: Option, + }, + /// Http error codes recieived + RequestFailed { + response: GraphQLResponse, + }, + /// Unable to deserialize body + BodyError { + error: BoxErr, + }, + /// Unable to make http request + NetworkError { + error: BoxErr, + }, + InvalidQuery { + error: BoxErr, + }, + /// Unable to upload file + FileUpload { + error: BoxErr, + }, +} + +impl From for GraphQLRequestError { + fn from(error: BuildReqError) -> Self { + match error { + BuildReqError::FileUpload { error } => GraphQLRequestError::FileUpload { error }, + } + } +} + +impl std::fmt::Display for GraphQLRequestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GraphQLRequestError::RequestErrors { errors, .. } => { + write!(f, "graphql errors in response: ")?; + for err in errors { + write!(f, "{}, ", err.message)?; + } + } + GraphQLRequestError::RequestFailed { response } => { + write!(f, "request failed with status {}", response.status)?; + } + GraphQLRequestError::BodyError { error } => { + write!(f, "error reading request body: {error}")?; + } + GraphQLRequestError::NetworkError { error } => { + write!(f, "error making http request: {error}")?; + } + GraphQLRequestError::InvalidQuery { error } => { + write!(f, "error building request: {error}")? + } + GraphQLRequestError::FileUpload { error } => { + write!(f, "error uploading file: {error}")? + } + } + Ok(()) + } +} +impl std::error::Error for GraphQLRequestError {} + +#[derive(Debug, Deserialize)] +pub struct ErrorLocation { + pub line: u32, + pub column: u32, +} +#[derive(Debug, Deserialize)] +pub struct GraphqlError { + pub message: String, + pub locations: Option>, + pub path: Option>, +} + +#[derive(Debug)] +pub enum PathSegment { + Field(String), + Index(u64), +} + +impl<'de> serde::de::Deserialize<'de> for PathSegment { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde_json::Value; + let val = Value::deserialize(deserializer)?; + match val { + Value::Number(n) => Ok(PathSegment::Index(n.as_u64().unwrap())), + Value::String(s) => Ok(PathSegment::Field(s)), + _ => panic!("invalid path segment type"), + } + } +} + +#[derive(Clone)] +pub struct GraphQlTransportReqwestSync { + addr: Url, + ty_to_gql_ty_map: TyToGqlTyMap, + client: reqwest::blocking::Client, +} + +#[derive(Clone)] +pub struct GraphQlTransportReqwest { + addr: Url, + ty_to_gql_ty_map: TyToGqlTyMap, + client: reqwest::Client, +} + +impl GraphQlTransportReqwestSync { + pub fn new(addr: Url, ty_to_gql_ty_map: TyToGqlTyMap) -> Self { + Self { + addr, + ty_to_gql_ty_map, + client: reqwest::blocking::Client::new(), + } + } + + fn fetch( + &self, + nodes: Vec, + opts: &GraphQlTransportOptions, + ty: &'static str, + ) -> Result, GraphQLRequestError> { + let nodes_len = nodes.len(); + let GqlRequest { + doc, + variables, + placeholders, + path_to_files, + } = GqlRequestBuilder::new(&self.ty_to_gql_ty_map).build(nodes, ty, None)?; + if !placeholders.is_empty() { + panic!("placeholders found in non-prepared query") + } + let req = build_gql_req_sync( + &self.client, + self.addr.clone(), + &doc, + variables, + path_to_files, + opts, + )?; + match req.send() { + Ok(res) => { + let status = res.status(); + let headers = res.headers().clone(); + match res.json::() { + Ok(body) => handle_response( + GraphQLResponse { + status, + headers, + body, + }, + nodes_len, + ), + Err(error) => Err(GraphQLRequestError::BodyError { + error: Box::new(error), + }), + } + } + Err(error) => Err(GraphQLRequestError::NetworkError { + error: Box::new(error), + }), + } + } + + pub fn query( + &self, + nodes: Doc, + ) -> Result { + self.query_with_opts(nodes, &Default::default()) + } + + pub fn query_with_opts( + &self, + nodes: Doc, + opts: &GraphQlTransportOptions, + ) -> Result { + let resp = self.fetch(nodes.to_select_doc(), opts, "query")?; + let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { + error: Box::from(format!( + "error deserializing response into output type: {err}" + )), + })?; + Ok(resp) + } + + pub fn mutation( + &self, + nodes: Doc, + ) -> Result { + self.mutation_with_opts(nodes, &Default::default()) + } + + pub fn mutation_with_opts( + &self, + nodes: Doc, + opts: &GraphQlTransportOptions, + ) -> Result { + let resp = self.fetch(nodes.to_select_doc(), opts, "mutation")?; + let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { + error: Box::from(format!( + "error deserializing response into output type: {err}" + )), + })?; + Ok(resp) + } + pub fn prepare_query( + &self, + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + ) -> Result, PrepareRequestError> { + self.prepare_query_with_opts(fun, Default::default()) + } + + pub fn prepare_query_with_opts( + &self, + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + opts: GraphQlTransportOptions, + ) -> Result, PrepareRequestError> { + PreparedRequestReqwestSync::new( + fun, + self.addr.clone(), + opts, + "query", + &self.ty_to_gql_ty_map, + ) + } + + pub fn prepare_mutation( + &self, + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + ) -> Result, PrepareRequestError> { + self.prepare_mutation_with_opts(fun, Default::default()) + } + + pub fn prepare_mutation_with_opts( + &self, + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + opts: GraphQlTransportOptions, + ) -> Result, PrepareRequestError> { + PreparedRequestReqwestSync::new( + fun, + self.addr.clone(), + opts, + "mutation", + &self.ty_to_gql_ty_map, + ) + } +} + +impl GraphQlTransportReqwest { + pub fn new(addr: Url, ty_to_gql_ty_map: TyToGqlTyMap) -> Self { + Self { + addr, + ty_to_gql_ty_map, + client: reqwest::Client::new(), + } + } + + async fn fetch( + &self, + nodes: Vec, + opts: &GraphQlTransportOptions, + ty: &'static str, + ) -> Result, GraphQLRequestError> { + let nodes_len = nodes.len(); + let GqlRequest { + doc, + variables, + placeholders, + path_to_files, + } = GqlRequestBuilder::new(&self.ty_to_gql_ty_map).build(nodes, ty, None)?; + if !placeholders.is_empty() { + panic!("placeholders found in non-prepared query") + } + + let req = build_gql_req( + &self.client, + self.addr.clone(), + &doc, + variables, + path_to_files, + opts, + ) + .await?; + match req.send().await { + Ok(res) => { + let status = res.status(); + let headers = res.headers().clone(); + match res.json::().await { + Ok(body) => handle_response( + GraphQLResponse { + status, + headers, + body, + }, + nodes_len, + ), + Err(error) => Err(GraphQLRequestError::BodyError { + error: Box::new(error), + }), + } + } + Err(error) => Err(GraphQLRequestError::NetworkError { + error: Box::new(error), + }), + } + } + + pub async fn query( + &self, + nodes: Doc, + ) -> Result { + self.query_with_opts(nodes, &Default::default()).await + } + + pub async fn query_with_opts( + &self, + nodes: Doc, + opts: &GraphQlTransportOptions, + ) -> Result { + let resp = self.fetch(nodes.to_select_doc(), opts, "query").await?; + let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { + error: Box::from(format!( + "error deserializing response into output type: {err}" + )), + })?; + Ok(resp) + } + + pub async fn mutation( + &self, + nodes: Doc, + ) -> Result { + self.mutation_with_opts(nodes, &Default::default()).await + } + + pub async fn mutation_with_opts( + &self, + nodes: Doc, + opts: &GraphQlTransportOptions, + ) -> Result { + let resp = self.fetch(nodes.to_select_doc(), opts, "mutation").await?; + let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { + error: Box::from(format!( + "error deserializing response into output type: {err}" + )), + })?; + Ok(resp) + } + pub fn prepare_query( + &self, + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + ) -> Result, PrepareRequestError> { + self.prepare_query_with_opts(fun, Default::default()) + } + + pub fn prepare_query_with_opts( + &self, + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + opts: GraphQlTransportOptions, + ) -> Result, PrepareRequestError> { + PreparedRequestReqwest::new( + fun, + self.addr.clone(), + opts, + "query", + &self.ty_to_gql_ty_map, + ) + } + + pub fn prepare_mutation( + &self, + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + ) -> Result, PrepareRequestError> { + self.prepare_mutation_with_opts(fun, Default::default()) + } + + pub fn prepare_mutation_with_opts( + &self, + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + opts: GraphQlTransportOptions, + ) -> Result, PrepareRequestError> { + PreparedRequestReqwest::new( + fun, + self.addr.clone(), + opts, + "mutation", + &self.ty_to_gql_ty_map, + ) + } +} + +fn resolve_prepared_variables( + placeholders: &FoundPlaceholders, + mut inline_variables: JsonObject, + mut args: HashMap, +) -> Result { + for (ph, key_map) in placeholders { + let Some(value) = args.remove(&ph.key) else { + return Err(PrepareRequestError::PlaceholderError(Box::from(format!( + "no value found for placeholder expected under key '{}'", + ph.key + )))); + }; + let value = (ph.fun)(value).map_err(|err| { + PrepareRequestError::PlaceholderError(Box::from(format!( + "error applying placeholder closure for value under key '{}': {err}", + ph.key + ))) + })?; + let serde_json::Value::Object(mut value) = value else { + unreachable!("placeholder closures must return structs"); + }; + for (key, var_key) in key_map { + inline_variables.insert( + var_key.clone().into(), + value.remove(&key[..]).unwrap_or(serde_json::Value::Null), + ); + } + } + Ok(inline_variables) +} + +pub struct PreparedRequestReqwest { + addr: Url, + client: reqwest::Client, + nodes_len: usize, + pub doc: String, + variables: JsonObject, + path_to_files: HashMap>, + opts: GraphQlTransportOptions, + placeholders: Arc, + _phantom: PhantomData, +} + +pub struct PreparedRequestReqwestSync { + addr: Url, + client: reqwest::blocking::Client, + nodes_len: usize, + pub doc: String, + variables: JsonObject, + path_to_files: HashMap>, + opts: GraphQlTransportOptions, + placeholders: Arc, + _phantom: PhantomData, +} + +impl PreparedRequestReqwestSync { + fn new( + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + addr: Url, + opts: GraphQlTransportOptions, + ty: &'static str, + ty_to_gql_ty_map: &TyToGqlTyMap, + ) -> Result { + let nodes = fun(&mut PreparedArgs); + let nodes = nodes.to_select_doc(); + let nodes_len = nodes.len(); + let GqlRequest { + doc, + variables, + placeholders, + path_to_files, + } = GqlRequestBuilder::new(ty_to_gql_ty_map) + .build(nodes, ty, None) + .map_err(PrepareRequestError::BuildError)?; + Ok(Self { + doc, + variables, + path_to_files, + nodes_len, + addr, + client: reqwest::blocking::Client::new(), + opts, + placeholders: Arc::new(placeholders), + _phantom: PhantomData, + }) + } + + pub fn perform( + &self, + args: impl Into>, + ) -> Result + where + K: Into, + V: serde::Serialize, + { + let args: HashMap = args.into(); + let args = args + .into_iter() + .map(|(key, val)| (key.into(), to_json_value(val))) + .collect(); + let variables = + resolve_prepared_variables(&self.placeholders, self.variables.clone(), args)?; + // TODO extract files from variables after resolution + let req = build_gql_req_sync( + &self.client, + self.addr.clone(), + &self.doc, + variables, + self.path_to_files.clone(), + &self.opts, + )?; + let res = match req.send() { + Ok(res) => { + let status = res.status(); + let headers = res.headers().clone(); + match res.json::() { + Ok(body) => handle_response( + GraphQLResponse { + status, + headers, + body, + }, + self.nodes_len, + ) + .map_err(PrepareRequestError::RequestError)?, + Err(error) => { + return Err(PrepareRequestError::RequestError( + GraphQLRequestError::BodyError { + error: Box::new(error), + }, + )) + } + } + } + Err(error) => { + return Err(PrepareRequestError::RequestError( + GraphQLRequestError::NetworkError { + error: Box::new(error), + }, + )) + } + }; + Doc::parse_response(res).map_err(|err| { + PrepareRequestError::RequestError(GraphQLRequestError::BodyError { + error: Box::from(format!( + "error deserializing response into output type: {err}" + )), + }) + }) + } +} + +impl PreparedRequestReqwest { + fn new( + fun: impl FnOnce(&mut PreparedArgs) -> Doc, + addr: Url, + opts: GraphQlTransportOptions, + ty: &'static str, + ty_to_gql_ty_map: &TyToGqlTyMap, + ) -> Result { + let nodes = fun(&mut PreparedArgs); + let nodes = nodes.to_select_doc(); + let nodes_len = nodes.len(); + let GqlRequest { + doc, + variables, + placeholders, + path_to_files, + } = GqlRequestBuilder::new(ty_to_gql_ty_map) + .build(nodes, ty, None) + .map_err(PrepareRequestError::BuildError)?; + let placeholders = std::sync::Arc::new(placeholders); + Ok(Self { + doc, + variables, + path_to_files, + nodes_len, + addr, + client: reqwest::Client::new(), + opts, + placeholders, + _phantom: PhantomData, + }) + } + + pub async fn perform( + &self, + args: impl Into>, + ) -> Result + where + K: Into, + V: serde::Serialize, + { + let args: HashMap = args.into(); + let args = args + .into_iter() + .map(|(key, val)| (key.into(), to_json_value(val))) + .collect(); + let variables = + resolve_prepared_variables(&self.placeholders, self.variables.clone(), args)?; + // TODO extract files from variables + let req = build_gql_req( + &self.client, + self.addr.clone(), + &self.doc, + variables, + self.path_to_files.clone(), + &self.opts, + ) + .await?; + let res = match req.send().await { + Ok(res) => { + let status = res.status(); + let headers = res.headers().clone(); + match res.json::().await { + Ok(body) => handle_response( + GraphQLResponse { + status, + headers, + body, + }, + self.nodes_len, + ) + .map_err(PrepareRequestError::RequestError)?, + Err(error) => { + return Err(PrepareRequestError::RequestError( + GraphQLRequestError::BodyError { + error: Box::new(error), + }, + )) + } + } + } + Err(error) => { + return Err(PrepareRequestError::RequestError( + GraphQLRequestError::NetworkError { + error: Box::new(error), + }, + )) + } + }; + Doc::parse_response(res).map_err(|err| { + PrepareRequestError::RequestError(GraphQLRequestError::BodyError { + error: Box::from(format!( + "error deserializing response into output type: {err}" + )), + }) + }) + } +} + +// we need a manual clone impl since the derive will +// choke if Doc isn't clone +impl Clone for PreparedRequestReqwestSync { + fn clone(&self) -> Self { + Self { + addr: self.addr.clone(), + client: self.client.clone(), + nodes_len: self.nodes_len, + doc: self.doc.clone(), + variables: self.variables.clone(), + path_to_files: self.path_to_files.clone(), + opts: self.opts.clone(), + placeholders: self.placeholders.clone(), + _phantom: PhantomData, + } + } +} +impl Clone for PreparedRequestReqwest { + fn clone(&self) -> Self { + Self { + addr: self.addr.clone(), + client: self.client.clone(), + nodes_len: self.nodes_len, + doc: self.doc.clone(), + variables: self.variables.clone(), + path_to_files: self.path_to_files.clone(), + opts: self.opts.clone(), + placeholders: self.placeholders.clone(), + _phantom: PhantomData, + } + } +} + +#[derive(Debug)] +pub enum PrepareRequestError { + BuildError(GraphQLRequestError), + PlaceholderError(BoxErr), + RequestError(GraphQLRequestError), + FileUploadError(BoxErr), +} + +impl From for PrepareRequestError { + fn from(error: BuildReqError) -> Self { + match error { + BuildReqError::FileUpload { error } => PrepareRequestError::FileUploadError(error), + } + } +} + +impl std::error::Error for PrepareRequestError {} +impl std::fmt::Display for PrepareRequestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + /* PrepareRequestError::FunctionError(err) => { + write!(f, "error calling doc builder closure: {err}") + } */ + PrepareRequestError::BuildError(err) => write!(f, "error building request: {err}"), + PrepareRequestError::PlaceholderError(err) => { + write!(f, "error resolving placeholder values: {err}") + } + PrepareRequestError::RequestError(err) => { + write!(f, "error making graphql request: {err}") + } + PrepareRequestError::FileUploadError(err) => { + write!(f, "error uploading file: {err}") + } + } + } +} diff --git a/src/metagen-client-rs/src/lib.rs b/src/metagen-client-rs/src/lib.rs new file mode 100644 index 0000000000..c0c217a7af --- /dev/null +++ b/src/metagen-client-rs/src/lib.rs @@ -0,0 +1,33 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +mod args; +mod files; +mod graphql; +mod nodes; +mod selection; + +mod interlude { + pub use serde::{Deserialize, Serialize}; + pub use std::collections::HashMap; + pub use std::marker::PhantomData; + + pub type CowStr = std::borrow::Cow<'static, str>; + pub type BoxErr = Box; + pub type JsonObject = serde_json::Map; + + pub fn to_json_value(val: T) -> serde_json::Value { + serde_json::to_value(val).expect("error serializing value") + } +} + +pub mod prelude { + pub use crate::args::*; + pub use crate::files::*; + pub use crate::graphql::*; + pub use crate::interlude::BoxErr; + pub use crate::nodes::*; + pub use crate::selection::*; + pub use crate::{impl_selection_traits, impl_union_selection_traits}; + pub use reqwest::Url; +} diff --git a/src/metagen-client-rs/src/nodes.rs b/src/metagen-client-rs/src/nodes.rs new file mode 100644 index 0000000000..33815c04d3 --- /dev/null +++ b/src/metagen-client-rs/src/nodes.rs @@ -0,0 +1,266 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +use crate::selection::{selection_to_node_set, SelectionErased, SelectionErasedMap}; + +use crate::{ + args::{NodeArgsErased, NodeArgsMerged}, + files::PathToInputFiles, + interlude::*, + selection::CompositeSelection, +}; + +pub type NodeMetaFn = fn() -> NodeMeta; + +/// How the [`node_metas`] module encodes the description +/// of the typegraph. +pub struct NodeMeta { + pub sub_nodes: Option>, + pub arg_types: Option>, + pub variants: Option>, + pub input_files: Option, +} + +pub enum SubNodes { + None, + Atomic(Vec), + Union(HashMap>), +} + +/// The final form of the nodes used in queries. +pub struct SelectNodeErased { + pub node_name: CowStr, + pub instance_name: CowStr, + pub args: Option, + pub sub_nodes: SubNodes, + pub input_files: Option, +} + +/// Wrappers around [`SelectNodeErased`] that only holds query nodes +pub struct QueryNode(pub SelectNodeErased, pub PhantomData<(Out,)>); +/// Wrappers around [`SelectNodeErased`] that only holds mutation nodes +pub struct MutationNode(pub SelectNodeErased, pub PhantomData<(Out,)>); + +/* /// Trait used to track the `Out` type parameter for [`QueryNode`]/[`MutationNode`] +pub trait ToSelectNode { + type Out; + + fn erased(self) -> SelectNodeErased; +} */ + +/// A variation of [`ToSelectNode`] to only be implemented +/// by aggregates of select nodes like [Vec]s. +pub trait ToSelectDoc { + type Out; + + fn to_select_doc(self) -> Vec; + fn parse_response(data: Vec) -> Result; +} + +/// Marker trait for [`ToSelectDoc`] implementors that only carry query nodes. +pub trait ToQueryDoc {} +/// Marker trait for [`ToSelectDoc`] implementors that only carry mutation nodes. +pub trait ToMutationDoc {} + +/// Struct used to mark query associated types that are generic about effect. +pub struct QueryMarker; +/// Struct used to mark mutationo associated types that are generic about effect. +pub struct MutationMarker; + +/// A node that's yet to have it's subnodes specified. +/// Use [`select`][Self::select] and [`select_aliased`][Self::select_aliased] +/// to finalize it. +/// [`select_aliased`][Self::select_aliased] will allow you to use [`alias`] +/// nodes but the returned object will be a raw [`serde_json::Value`]. +/// This type is generic over effect using the `QTy` parameter. +pub struct UnselectedNode { + pub root_name: CowStr, + pub root_meta: NodeMetaFn, + pub args: NodeArgsErased, + pub _marker: PhantomData<(SelT, SelAliasedT, QTy, Out)>, +} + +impl UnselectedNode +where + SelT: Into, +{ + fn select_erased(self, select: SelT) -> SelectNodeErased { + let nodes = selection_to_node_set( + SelectionErasedMap( + [( + self.root_name.clone(), + match self.args { + NodeArgsErased::None => SelectionErased::Composite(select.into()), + args => SelectionErased::CompositeArgs(args, select.into()), + }, + )] + .into(), + ), + &[(self.root_name, self.root_meta)].into(), + "$q".into(), + ) + .unwrap(); + nodes.into_iter().next().unwrap() + } +} + +impl UnselectedNode +where + SelAliased: Into, +{ + fn select_aliased_erased(self, select: SelAliased) -> SelectNodeErased { + let nodes = selection_to_node_set( + SelectionErasedMap( + [( + self.root_name.clone(), + match self.args { + NodeArgsErased::None => SelectionErased::Composite(select.into()), + args => SelectionErased::CompositeArgs(args, select.into()), + }, + )] + .into(), + ), + &[(self.root_name, self.root_meta)].into(), + "$q".into(), + ) + .unwrap(); + nodes.into_iter().next().unwrap() + } +} + +// NOTE: we'll need a select method implementation for each ATy x QTy pair + +impl UnselectedNode +where + SelT: Into, +{ + pub fn select(self, select: SelT) -> QueryNode { + QueryNode(self.select_erased(select), PhantomData) + } +} +impl UnselectedNode +where + SelAliased: Into, +{ + pub fn select_aliased(self, select: SelAliased) -> QueryNode { + QueryNode(self.select_aliased_erased(select), PhantomData) + } +} +impl UnselectedNode +where + SelT: Into, +{ + pub fn select(self, select: SelT) -> MutationNode { + MutationNode(self.select_erased(select), PhantomData) + } +} +impl UnselectedNode +where + SelAliased: Into, +{ + pub fn select_aliased(self, select: SelAliased) -> MutationNode { + MutationNode(self.select_aliased_erased(select), PhantomData) + } +} + +// --- --- Impl ToSelectDoc --- --- /// + +impl ToSelectDoc for QueryNode +where + Out: serde::de::DeserializeOwned, +{ + type Out = Out; + + fn to_select_doc(self) -> Vec { + vec![self.0] + } + + fn parse_response(data: Vec) -> Result { + let mut data = data.into_iter(); + serde_json::from_value(data.next().unwrap()) + } +} +impl ToQueryDoc for QueryNode {} +impl ToSelectDoc for MutationNode +where + Out: serde::de::DeserializeOwned, +{ + type Out = Out; + + fn to_select_doc(self) -> Vec { + vec![self.0] + } + + fn parse_response(data: Vec) -> Result { + let mut data = data.into_iter(); + serde_json::from_value(data.next().unwrap()) + } +} +impl ToMutationDoc for MutationNode {} + +#[macro_export] +macro_rules! impl_for_tuple { + ($($idx:tt $ty:tt),+) => { + impl<$($ty,)+> ToSelectDoc for ($(QueryNode<$ty>,)+) + where $($ty: serde::de::DeserializeOwned,)+ + { + type Out = ($($ty,)+); + + fn to_select_doc(self) -> Vec { + vec![ + $(self.$idx.0,)+ + ] + } + fn parse_response(data: Vec) -> Result { + let mut data = data.into_iter(); + let mut next = move |_idx| data.next().unwrap(); + Ok(( + + $(serde_json::from_value(next($idx))?,)+ + )) + } + } + impl<$($ty,)+> ToSelectDoc for ($(MutationNode<$ty>,)+) + where $($ty: serde::de::DeserializeOwned,)+ + { + type Out = ($($ty,)+); + + fn to_select_doc(self) -> Vec { + vec![ + $(self.$idx.0,)+ + ] + } + fn parse_response(data: Vec) -> Result { + let mut data = data.into_iter(); + let mut next = move |_idx| data.next().unwrap(); + Ok(( + + $(serde_json::from_value(next($idx))?,)+ + )) + } + } + + impl<$($ty,)+> ToQueryDoc for ($($ty,)+) + where + $($ty: ToQueryDoc,)+ + {} + + impl<$($ty,)+> ToMutationDoc for ($($ty,)+) + where + $($ty: ToMutationDoc,)+ + {} + }; +} + +impl_for_tuple!(0 N0); +impl_for_tuple!(0 N0, 1 N1); +impl_for_tuple!(0 N0, 1 N1, 2 N2); +impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3); +impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4); +impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5); +impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6); +impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7); +impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8); +impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9); +impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9, 10 N10); +impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9, 10 N10, 11 N11); diff --git a/src/metagen-client-rs/src/selection.rs b/src/metagen-client-rs/src/selection.rs new file mode 100644 index 0000000000..103d372c01 --- /dev/null +++ b/src/metagen-client-rs/src/selection.rs @@ -0,0 +1,800 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +use crate::args::{ + check_node_args, NodeArgsErased, NodeArgsMerged, PlaceholderArg, PlaceholderArgSelect, +}; +use crate::interlude::*; +use crate::nodes::{NodeMeta, NodeMetaFn, SelectNodeErased, SubNodes}; + +/// Build the SelectNodeErased tree from the SelectionErasedMap tree +/// according to the NodeMeta tree. In this function +/// - arguments are associated with their types +/// - aliases get splatted into the node tree +/// - light query validation takes place +/// +/// I.e. the user's selection is joined with the description of the graph found +/// in the static NodeMetas to fill in any blank spaces +pub fn selection_to_node_set( + selection: SelectionErasedMap, + metas: &HashMap, + parent_path: String, +) -> Result, SelectionError> { + let mut out = vec![]; + let mut selection = selection.0; + let mut found_nodes = selection + .keys() + .cloned() + .collect::>(); + for (node_name, meta_fn) in metas.iter() { + found_nodes.remove(&node_name[..]); + + let Some(node_selection) = selection.remove(&node_name[..]) else { + // this node was not selected + continue; + }; + + // we can have multiple selection instances for a node + // if aliases are involved + let node_instances = match node_selection { + // this noe was not selected + SelectionErased::None => continue, + SelectionErased::Scalar => vec![(node_name.clone(), NodeArgsErased::None, None)], + SelectionErased::ScalarArgs(args) => { + vec![(node_name.clone(), args, None)] + } + SelectionErased::Composite(select) => { + vec![(node_name.clone(), NodeArgsErased::None, Some(select))] + } + SelectionErased::CompositeArgs(args, select) => { + vec![(node_name.clone(), args, Some(select))] + } + SelectionErased::Alias(aliases) => aliases + .into_iter() + .map(|(instance_name, selection)| { + let (args, select) = match selection { + AliasSelection::Scalar => (NodeArgsErased::None, None), + AliasSelection::ScalarArgs(args) => (args, None), + AliasSelection::Composite(select) => (NodeArgsErased::None, Some(select)), + AliasSelection::CompositeArgs(args, select) => (args, Some(select)), + }; + (instance_name, args, select) + }) + .collect(), + }; + + let meta = meta_fn(); + for (instance_name, args, select) in node_instances { + out.push(selection_to_select_node( + instance_name, + node_name.clone(), + args, + select, + &parent_path, + &meta, + )?) + } + } + Ok(out) +} + +pub fn selection_to_select_node( + instance_name: CowStr, + node_name: CowStr, + args: NodeArgsErased, + select: Option, + parent_path: &str, + meta: &NodeMeta, +) -> Result { + let args = if let Some(arg_types) = &meta.arg_types { + match args { + NodeArgsErased::Inline(args) => { + let instance_args = check_node_args(args, arg_types).map_err(|name| { + SelectionError::UnexpectedArgs { + name, + path: format!("{parent_path}.{instance_name}"), + } + })?; + Some(NodeArgsMerged::Inline(instance_args)) + } + NodeArgsErased::Placeholder(ph) => Some(NodeArgsMerged::Placeholder { + value: ph, + // FIXME: this clone can be improved + arg_types: arg_types.clone(), + }), + NodeArgsErased::None => { + return Err(SelectionError::MissingArgs { + path: format!("{parent_path}.{instance_name}"), + }) + } + } + } else { + None + }; + let sub_nodes = match (&meta.variants, &meta.sub_nodes) { + (Some(_), Some(_)) => unreachable!("union/either node metas can't have sub_nodes"), + (None, None) => SubNodes::None, + (variants, sub_nodes) => { + let Some(select) = select else { + return Err(SelectionError::MissingSubNodes { + path: format!("{parent_path}.{instance_name}"), + }); + }; + match select { + CompositeSelection::Atomic(select) => { + let Some(sub_nodes) = sub_nodes else { + return Err(SelectionError::UnexpectedUnion { + path: format!("{parent_path}.{instance_name}"), + }); + }; + SubNodes::Atomic(selection_to_node_set( + select, + sub_nodes, + format!("{parent_path}.{instance_name}"), + )?) + } + CompositeSelection::Union(mut variant_select) => { + let Some(variants) = variants else { + return Err(SelectionError::MissingUnion { + path: format!("{parent_path}.{instance_name}"), + }); + }; + let mut out = HashMap::new(); + for (variant_ty, variant_meta) in variants { + let variant_meta = variant_meta(); + // this union member is a scalar + let Some(sub_nodes) = variant_meta.sub_nodes else { + continue; + }; + let mut nodes = if let Some(select) = variant_select.remove(variant_ty) { + selection_to_node_set( + select, + &sub_nodes, + format!("{parent_path}.{instance_name}.variant({variant_ty})"), + )? + } else { + vec![] + }; + nodes.push(SelectNodeErased { + node_name: "__typename".into(), + instance_name: "__typename".into(), + args: None, + sub_nodes: SubNodes::None, + input_files: meta.input_files.clone(), + }); + out.insert(variant_ty.clone(), nodes); + } + if !variant_select.is_empty() { + return Err(SelectionError::UnexpectedVariants { + path: format!("{parent_path}.{instance_name}"), + varaint_tys: variant_select.into_keys().collect(), + }); + } + SubNodes::Union(out) + } + } + } + }; + Ok(SelectNodeErased { + node_name, + instance_name, + args, + sub_nodes, + input_files: meta.input_files.clone(), + }) +} + +#[derive(Debug)] +pub enum SelectionError { + MissingArgs { + path: String, + }, + MissingSubNodes { + path: String, + }, + MissingUnion { + path: String, + }, + UnexpectedArgs { + path: String, + name: String, + }, + UnexpectedUnion { + path: String, + }, + UnexpectedVariants { + path: String, + varaint_tys: Vec, + }, +} + +impl std::fmt::Display for SelectionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SelectionError::MissingArgs { path } => write!(f, "args are missing at node {path}"), + SelectionError::UnexpectedArgs { path, name } => { + write!(f, "unexpected arg '${name}' at node {path}") + } + SelectionError::MissingSubNodes { path } => { + write!(f, "node at {path} is a composite but no selection found") + } + SelectionError::MissingUnion { path } => write!( + f, + "node at {path} is a union but provided selection is atomic" + ), + SelectionError::UnexpectedUnion { path } => write!( + f, + "node at {path} is an atomic type but union selection provided" + ), + SelectionError::UnexpectedVariants { + path, + varaint_tys: varaint_ty, + } => { + write!( + f, + "node at {path} has none of the variants called '{varaint_ty:?}'" + ) + } + } + } +} +impl std::error::Error for SelectionError {} + +// This is a newtype for Into trait impl purposes +#[derive(Debug)] +pub struct SelectionErasedMap(pub HashMap); + +#[derive(Debug)] +pub enum CompositeSelection { + Atomic(SelectionErasedMap), + Union(HashMap), +} + +impl Default for CompositeSelection { + fn default() -> Self { + CompositeSelection::Atomic(SelectionErasedMap(Default::default())) + } +} + +#[derive(Debug)] +pub enum SelectionErased { + None, + Scalar, + ScalarArgs(NodeArgsErased), + Composite(CompositeSelection), + CompositeArgs(NodeArgsErased, CompositeSelection), + Alias(HashMap), +} + +#[derive(Debug)] +pub enum AliasSelection { + Scalar, + ScalarArgs(NodeArgsErased), + Composite(CompositeSelection), + CompositeArgs(NodeArgsErased, CompositeSelection), +} + +#[derive(Default, Clone, Copy, Debug)] +pub struct HasAlias; +#[derive(Default, Clone, Copy, Debug)] +pub struct NoAlias; + +#[derive(Debug)] +pub struct AliasInfo { + aliases: HashMap, + _phantom: PhantomData<(ArgT, SelT, ATyag)>, +} + +#[derive(Debug)] +pub enum ScalarSelect { + Get, + Skip, + Alias(AliasInfo<(), (), ATy>), +} +#[derive(Debug)] +pub enum ScalarSelectArgs { + Get(NodeArgsErased, PhantomData), + Skip, + Alias(AliasInfo), +} +#[derive(Debug)] +pub enum CompositeSelect { + Get(CompositeSelection, PhantomData), + Skip, + Alias(AliasInfo<(), SelT, ATy>), +} +#[derive(Debug)] +pub enum CompositeSelectArgs { + Get( + NodeArgsErased, + CompositeSelection, + PhantomData<(ArgT, SelT)>, + ), + Skip, + Alias(AliasInfo), +} + +pub struct Get; +pub struct Skip; +pub struct Args(ArgT); +pub struct Select(SelT); +pub struct ArgSelect(ArgT, SelT); +pub struct Alias(AliasInfo); + +/// Shorthand for `Default::default`. All selections generally default +/// to [`skip`]. +pub fn default() -> T { + T::default() +} +/// Include all sub nodes excpet those that require arguments +pub fn all() -> T { + T::all() +} +/// Select the node for inclusion. +pub fn get>() -> T { + T::from(Get) +} +/// Skip this node when queryig. +pub fn skip>() -> T { + T::from(Skip) +} +/// Provide argumentns for a scalar node. +pub fn args>>(args: ArgT) -> T { + T::from(Args(args)) +} +/// Provide selections for a composite node that takes no args. +pub fn select>>(selection: SelT) -> T { + T::from(Select(selection)) +} +/// Provide arguments and selections for a composite node. +pub fn arg_select>>(args: ArgT, selection: SelT) -> T { + T::from(ArgSelect(args, selection)) +} + +/// Query the same node multiple times using aliases. +/// +/// WARNING: make sure your alias names don't clash across sibling +/// nodes. +pub fn alias(info: impl Into>) -> T +where + S: Into, + ASelT: Into, + T: From> + FromAliasSelection, +{ + let info: HashMap<_, _> = info.into(); + T::from(Alias(AliasInfo { + aliases: info + .into_iter() + .map(|(name, sel)| (name.into(), sel.into())) + .collect(), + _phantom: PhantomData, + })) +} + +pub trait Selection { + /// Include all sub nodes excpet those that require arguments + fn all() -> Self; +} + +// --- Impl SelectionType impls --- // + +impl Selection for ScalarSelect { + fn all() -> Self { + Self::Get + } +} +impl Selection for ScalarSelectArgs { + fn all() -> Self { + Self::Skip + } +} +impl Selection for CompositeSelect +where + SelT: Selection + Into, +{ + fn all() -> Self { + let sel = SelT::all(); + Self::Get(sel.into(), PhantomData) + } +} +impl Selection for CompositeSelectArgs +where + SelT: Selection, +{ + fn all() -> Self { + Self::Skip + } +} +// --- Default impls --- // + +impl Default for ScalarSelect { + fn default() -> Self { + Self::Skip + } +} +impl Default for ScalarSelectArgs { + fn default() -> Self { + Self::Skip + } +} +impl Default for CompositeSelect { + fn default() -> Self { + Self::Skip + } +} +impl Default for CompositeSelectArgs { + fn default() -> Self { + Self::Skip + } +} + +// --- From Get/Skip...etc impls --- // + +impl From for ScalarSelect { + fn from(_: Get) -> Self { + Self::Get + } +} + +impl From for ScalarSelect { + fn from(_: Skip) -> Self { + Self::Skip + } +} +impl From for ScalarSelectArgs { + fn from(_: Skip) -> Self { + Self::Skip + } +} +impl From for CompositeSelect { + fn from(_: Skip) -> Self { + Self::Skip + } +} +impl From for CompositeSelectArgs { + fn from(_: Skip) -> Self { + Self::Skip + } +} + +impl From> for ScalarSelectArgs +where + ArgT: Serialize, +{ + fn from(Args(args): Args) -> Self { + Self::Get(NodeArgsErased::Inline(to_json_value(args)), PhantomData) + } +} + +impl From> for CompositeSelect +where + SelT: Into, +{ + fn from(Select(selection): Select) -> Self { + Self::Get(selection.into(), PhantomData) + } +} + +impl From> for CompositeSelectArgs +where + ArgT: Serialize, + SelT: Into, +{ + fn from(ArgSelect(args, selection): ArgSelect) -> Self { + Self::Get( + NodeArgsErased::Inline(to_json_value(args)), + selection.into(), + PhantomData, + ) + } +} + +impl From> for ScalarSelectArgs { + fn from(value: PlaceholderArg) -> Self { + Self::Get(NodeArgsErased::Placeholder(value.value), PhantomData) + } +} +impl From> + for CompositeSelectArgs +where + SelT: Into, +{ + fn from(value: PlaceholderArgSelect) -> Self { + Self::Get( + NodeArgsErased::Placeholder(value.value), + value.selection.into(), + PhantomData, + ) + } +} + +// --- ToAliasSelection impls --- // + +/// This is a marker trait that allows the core selection types +/// like CompositeSelectNoArgs to mark which types can be used +/// as their aliasing nodes. This prevents usage of invalid selections +/// on aliases like [`Skip`]. +pub trait FromAliasSelection {} + +impl FromAliasSelection for ScalarSelect {} +impl FromAliasSelection> for ScalarSelectArgs {} +impl FromAliasSelection> for CompositeSelect {} +impl FromAliasSelection> + for CompositeSelectArgs +{ +} + +// --- From Alias impls --- // + +impl From>> for ScalarSelect { + fn from(Alias(info): Alias<(), ScalarSelect>) -> Self { + Self::Alias(AliasInfo { + aliases: info.aliases, + _phantom: PhantomData, + }) + } +} +impl From> for ScalarSelectArgs { + fn from(Alias(info): Alias) -> Self { + Self::Alias(info) + } +} +impl From> for CompositeSelect { + fn from(Alias(info): Alias<(), SelT>) -> Self { + Self::Alias(info) + } +} +impl From> for CompositeSelectArgs { + fn from(Alias(info): Alias) -> Self { + Self::Alias(info) + } +} + +// --- Into SelectionErased impls --- // + +impl From> for SelectionErased { + fn from(value: AliasInfo) -> SelectionErased { + SelectionErased::Alias(value.aliases) + } +} + +impl From> for SelectionErased { + fn from(value: ScalarSelect) -> SelectionErased { + use ScalarSelect::*; + match value { + Get => SelectionErased::Scalar, + Skip => SelectionErased::None, + Alias(alias) => alias.into(), + } + } +} + +impl From> for SelectionErased { + fn from(value: ScalarSelectArgs) -> SelectionErased { + use ScalarSelectArgs::*; + match value { + Get(arg, _) => SelectionErased::ScalarArgs(arg), + Skip => SelectionErased::None, + Alias(alias) => alias.into(), + } + } +} + +impl From> for SelectionErased { + fn from(value: CompositeSelect) -> SelectionErased { + use CompositeSelect::*; + match value { + Get(selection, _) => SelectionErased::Composite(selection), + Skip => SelectionErased::None, + Alias(alias) => alias.into(), + } + } +} + +impl From> for SelectionErased +where + SelT: Into, +{ + fn from(value: CompositeSelectArgs) -> SelectionErased { + use CompositeSelectArgs::*; + match value { + Get(args, selection, _) => SelectionErased::CompositeArgs(args, selection), + Skip => SelectionErased::None, + Alias(alias) => alias.into(), + } + } +} + +// --- UnionMember impls --- // + +/// The following trait is used for types that implement +/// selections for the composite members of unions. +/// +/// The err return value indicates the case where +/// aliases are used selections on members which is an error +/// +/// This state is currently impossible to arrive at since +/// AliasInfo has no public construction methods with NoAlias +/// set. Union selection types make sure all their immediate +/// member selection use NoAlias to prevent this invalid stat.e +pub trait UnionMember { + fn composite(self) -> Option; +} + +/// Internal marker trait use to make sure we can't have union members +/// selection being another union selection. +pub trait NotUnionSelection {} + +// NOTE: UnionMembers are all NoAlias +impl UnionMember for ScalarSelect { + fn composite(self) -> Option { + None + } +} + +impl UnionMember for ScalarSelectArgs { + fn composite(self) -> Option { + None + } +} + +impl UnionMember for CompositeSelect +where + SelT: NotUnionSelection, +{ + fn composite(self) -> Option { + use CompositeSelect::*; + match self { + Get(CompositeSelection::Atomic(selection), _) => Some(selection), + Skip => None, + Get(CompositeSelection::Union(_), _) => { + unreachable!("union selection on union member selection. how??") + } + Alias(_) => unreachable!("alias discovored on union/either member. how??"), + } + } +} + +impl UnionMember for CompositeSelectArgs +where + SelT: NotUnionSelection, +{ + fn composite(self) -> Option { + use CompositeSelectArgs::*; + match self { + Get(_args, CompositeSelection::Atomic(selection), _) => Some(selection), + Skip => None, + Get(_args, CompositeSelection::Union(_), _) => { + unreachable!("union selection on union member selection. how??") + } + Alias(_) => unreachable!("alias discovored on union/either member. how??"), + } + } +} + +// --- Into AliasSelection impls --- // + +impl From for AliasSelection { + fn from(_val: Get) -> Self { + AliasSelection::Scalar + } +} +impl From> for AliasSelection +where + ArgT: Serialize, +{ + fn from(val: Args) -> Self { + AliasSelection::ScalarArgs(NodeArgsErased::Inline(to_json_value(val.0))) + } +} +impl From> for AliasSelection +where + SelT: Into, +{ + fn from(val: Select) -> Self { + let map = val.0.into(); + AliasSelection::Composite(map) + } +} + +impl From> for AliasSelection +where + ArgT: Serialize, + SelT: Into, +{ + fn from(val: ArgSelect) -> Self { + let map = val.1.into(); + AliasSelection::CompositeArgs(NodeArgsErased::Inline(to_json_value(val.0)), map) + } +} +impl From> for AliasSelection { + fn from(val: ScalarSelect) -> Self { + use ScalarSelect::*; + match val { + Get => AliasSelection::Scalar, + _ => unreachable!(), + } + } +} +impl From> for AliasSelection { + fn from(val: ScalarSelectArgs) -> Self { + use ScalarSelectArgs::*; + match val { + Get(args, _) => AliasSelection::ScalarArgs(args), + _ => unreachable!(), + } + } +} + +impl From> for AliasSelection +where + SelT: Into, +{ + fn from(val: CompositeSelect) -> Self { + use CompositeSelect::*; + match val { + Get(select, _) => AliasSelection::Composite(select), + _ => unreachable!(), + } + } +} +impl From> for AliasSelection +where + SelT: Into, +{ + fn from(val: CompositeSelectArgs) -> Self { + use CompositeSelectArgs::*; + match val { + Get(args, selection, _) => AliasSelection::CompositeArgs(args, selection), + _ => unreachable!(), + } + } +} + +// TODO: convert to proc_macro +#[macro_export] +macro_rules! impl_selection_traits { + ($ty:ident,$($field:tt),+) => { + impl From<$ty> for CompositeSelection { + fn from(value: $ty) -> CompositeSelection { + CompositeSelection::Atomic(SelectionErasedMap( + [ + $((stringify!($field).into(), value.$field.into()),)+ + ] + .into(), + )) + } + } + + impl Selection for $ty { + fn all() -> Self { + Self { + $($field: all(),)+ + } + } + } + + impl NotUnionSelection for $ty {} + }; +} +#[macro_export] +macro_rules! impl_union_selection_traits { + ($ty:ident,$(($variant_ty:tt, $field:tt)),+) => { + impl From<$ty> for CompositeSelection { + fn from(value: $ty) -> CompositeSelection { + CompositeSelection::Union( + [ + $({ + let selection = + UnionMember::composite(value.$field); + selection.map(|val| ($variant_ty.into(), val)) + },)+ + ] + .into_iter() + .filter_map(|val| val) + .collect(), + ) + } + } + }; +} diff --git a/src/metagen/src/client_rs/mod.rs b/src/metagen/src/client_rs/mod.rs index 5bfd319990..ad33617a32 100644 --- a/src/metagen/src/client_rs/mod.rs +++ b/src/metagen/src/client_rs/mod.rs @@ -292,7 +292,7 @@ impl QueryGraph {{ /// Render the common sections like the transports fn render_static(dest: &mut GenDestBuf) -> core::fmt::Result { let client_rs = include_str!("static/client.rs"); - writeln!(dest, "{}", client_rs)?; + write!(dest, "{}", client_rs)?; Ok(()) } @@ -428,13 +428,32 @@ impl NameMapper { } pub fn gen_cargo_toml(crate_name: Option<&str>) -> String { - let cargo_toml = include_str!("static/Cargo.toml"); - if let Some(crate_name) = crate_name { - const DEF_CRATE_NAME: &str = "client_rs_static"; - cargo_toml.replace(DEF_CRATE_NAME, crate_name) - } else { - cargo_toml.to_string() - } + let crate_name = crate_name.unwrap_or("client_rs_static"); + #[cfg(debug_assertions)] + let dependency = "metagen-client.workspace = true"; + #[cfg(not(debug_assertions))] + let dependency = format!( + "metagen-client = {{ git = \"https://github.com/metatypedev/metatype.git\", branch = \"{version}\" }}", + version = env!("CARGO_PKG_VERSION") + ); + format!( + r#"[package] +name = "{crate_name}" +edition = "2021" +version = "0.0.1" + +[dependencies] +{dependency} +serde = {{ version = "1.0", features = ["derive"] }} +serde_json = "1.0" + +# The options after here are configured for crates intended to be +# wasm artifacts. Remove them if your usage is different +[lib] +path = "lib.rs" +crate-type = ["cdylib", "rlib"] +"# + ) } pub fn gen_lib_rs() -> String { diff --git a/src/metagen/src/client_rs/static/Cargo.toml b/src/metagen/src/client_rs/static/Cargo.toml deleted file mode 100644 index bd1963114f..0000000000 --- a/src/metagen/src/client_rs/static/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -package.name = "client_rs_static" -package.edition = "2021" -package.version = "0.0.1" - -[dependencies] -serde = { version = "1.0.210", features = ["derive"] } -serde_json = "1.0.128" -reqwest = { version = "0.12", features = ["blocking","json", "stream", "multipart"] } -mime_guess = "2.0" -futures = "0.3" -tokio-util = { version = "0.7", features = ["compat", "io"] } -derive_more = { version = "1.0", features = ["debug"] } -lazy_static = "1.5" - -# The options after here are configured for crates intended to be -# wasm artifacts. Remove them if your usage is different -[lib] -path = "lib.rs" -crate-type = ["cdylib", "rlib"] diff --git a/src/metagen/src/client_rs/static/client.rs b/src/metagen/src/client_rs/static/client.rs index 35682d379e..3326a6fc8b 100644 --- a/src/metagen/src/client_rs/static/client.rs +++ b/src/metagen/src/client_rs/static/client.rs @@ -1,2636 +1,5 @@ -use std::{collections::HashMap, marker::PhantomData}; - -use reqwest::Url; -use serde::{Deserialize, Serialize}; - -pub type CowStr = std::borrow::Cow<'static, str>; -pub type BoxErr = Box; -pub type JsonObject = serde_json::Map; - -fn to_json_value(val: T) -> serde_json::Value { - serde_json::to_value(val).expect("error serializing value") -} - -/// Build the SelectNodeErased tree from the SelectionErasedMap tree -/// according to the NodeMeta tree. In this function -/// - arguments are associated with their types -/// - aliases get splatted into the node tree -/// - light query validation takes place -/// -/// I.e. the user's selection is joined with the description of the graph found -/// in the static NodeMetas to fill in any blank spaces -fn selection_to_node_set( - selection: SelectionErasedMap, - metas: &HashMap, - parent_path: String, -) -> Result, SelectionError> { - let mut out = vec![]; - let mut selection = selection.0; - let mut found_nodes = selection - .keys() - .cloned() - .collect::>(); - for (node_name, meta_fn) in metas.iter() { - found_nodes.remove(&node_name[..]); - - let Some(node_selection) = selection.remove(&node_name[..]) else { - // this node was not selected - continue; - }; - - // we can have multiple selection instances for a node - // if aliases are involved - let node_instances = match node_selection { - // this noe was not selected - SelectionErased::None => continue, - SelectionErased::Scalar => vec![(node_name.clone(), NodeArgsErased::None, None)], - SelectionErased::ScalarArgs(args) => { - vec![(node_name.clone(), args, None)] - } - SelectionErased::Composite(select) => { - vec![(node_name.clone(), NodeArgsErased::None, Some(select))] - } - SelectionErased::CompositeArgs(args, select) => { - vec![(node_name.clone(), args, Some(select))] - } - SelectionErased::Alias(aliases) => aliases - .into_iter() - .map(|(instance_name, selection)| { - let (args, select) = match selection { - AliasSelection::Scalar => (NodeArgsErased::None, None), - AliasSelection::ScalarArgs(args) => (args, None), - AliasSelection::Composite(select) => (NodeArgsErased::None, Some(select)), - AliasSelection::CompositeArgs(args, select) => (args, Some(select)), - }; - (instance_name, args, select) - }) - .collect(), - }; - - let meta = meta_fn(); - for (instance_name, args, select) in node_instances { - out.push(selection_to_select_node( - instance_name, - node_name.clone(), - args, - select, - &parent_path, - &meta, - )?) - } - } - Ok(out) -} - -fn selection_to_select_node( - instance_name: CowStr, - node_name: CowStr, - args: NodeArgsErased, - select: Option, - parent_path: &str, - meta: &NodeMeta, -) -> Result { - let args = if let Some(arg_types) = &meta.arg_types { - match args { - NodeArgsErased::Inline(args) => { - let instance_args = check_node_args(args, arg_types).map_err(|name| { - SelectionError::UnexpectedArgs { - name, - path: format!("{parent_path}.{instance_name}"), - } - })?; - Some(NodeArgsMerged::Inline(instance_args)) - } - NodeArgsErased::Placeholder(ph) => Some(NodeArgsMerged::Placeholder { - value: ph, - // FIXME: this clone can be improved - arg_types: arg_types.clone(), - }), - NodeArgsErased::None => { - return Err(SelectionError::MissingArgs { - path: format!("{parent_path}.{instance_name}"), - }) - } - } - } else { - None - }; - let sub_nodes = match (&meta.variants, &meta.sub_nodes) { - (Some(_), Some(_)) => unreachable!("union/either node metas can't have sub_nodes"), - (None, None) => SubNodes::None, - (variants, sub_nodes) => { - let Some(select) = select else { - return Err(SelectionError::MissingSubNodes { - path: format!("{parent_path}.{instance_name}"), - }); - }; - match select { - CompositeSelection::Atomic(select) => { - let Some(sub_nodes) = sub_nodes else { - return Err(SelectionError::UnexpectedUnion { - path: format!("{parent_path}.{instance_name}"), - }); - }; - SubNodes::Atomic(selection_to_node_set( - select, - sub_nodes, - format!("{parent_path}.{instance_name}"), - )?) - } - CompositeSelection::Union(mut variant_select) => { - let Some(variants) = variants else { - return Err(SelectionError::MissingUnion { - path: format!("{parent_path}.{instance_name}"), - }); - }; - let mut out = HashMap::new(); - for (variant_ty, variant_meta) in variants { - let variant_meta = variant_meta(); - // this union member is a scalar - let Some(sub_nodes) = variant_meta.sub_nodes else { - continue; - }; - let mut nodes = if let Some(select) = variant_select.remove(variant_ty) { - selection_to_node_set( - select, - &sub_nodes, - format!("{parent_path}.{instance_name}.variant({variant_ty})"), - )? - } else { - vec![] - }; - nodes.push(SelectNodeErased { - node_name: "__typename".into(), - instance_name: "__typename".into(), - args: None, - sub_nodes: SubNodes::None, - input_files: meta.input_files.clone(), - }); - out.insert(variant_ty.clone(), nodes); - } - if !variant_select.is_empty() { - return Err(SelectionError::UnexpectedVariants { - path: format!("{parent_path}.{instance_name}"), - varaint_tys: variant_select.into_keys().collect(), - }); - } - SubNodes::Union(out) - } - } - } - }; - Ok(SelectNodeErased { - node_name, - instance_name, - args, - sub_nodes, - input_files: meta.input_files.clone(), - }) -} - -#[derive(Debug)] -pub enum SelectionError { - MissingArgs { - path: String, - }, - MissingSubNodes { - path: String, - }, - MissingUnion { - path: String, - }, - UnexpectedArgs { - path: String, - name: String, - }, - UnexpectedUnion { - path: String, - }, - UnexpectedVariants { - path: String, - varaint_tys: Vec, - }, -} - -impl std::fmt::Display for SelectionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SelectionError::MissingArgs { path } => write!(f, "args are missing at node {path}"), - SelectionError::UnexpectedArgs { path, name } => { - write!(f, "unexpected arg '${name}' at node {path}") - } - SelectionError::MissingSubNodes { path } => { - write!(f, "node at {path} is a composite but no selection found") - } - SelectionError::MissingUnion { path } => write!( - f, - "node at {path} is a union but provided selection is atomic" - ), - SelectionError::UnexpectedUnion { path } => write!( - f, - "node at {path} is an atomic type but union selection provided" - ), - SelectionError::UnexpectedVariants { - path, - varaint_tys: varaint_ty, - } => { - write!( - f, - "node at {path} has none of the variants called '{varaint_ty:?}'" - ) - } - } - } -} -impl std::error::Error for SelectionError {} - -// -// --- --- Input files --- --- // -// - -#[derive(Debug, Clone)] -pub struct TypePath(&'static [&'static str]); - -fn path_segment_as_prop(segment: &str) -> Option<&str> { - segment.strip_prefix('.') -} - -#[derive(Debug, Clone)] -pub struct PathToInputFiles(&'static [&'static [&'static str]]); - -#[derive(Debug)] -pub enum ValuePathSegment { - Optional, - Index(usize), - Prop(&'static str), -} - -#[derive(Default, Debug)] -pub struct ValuePath(Vec); - -lazy_static::lazy_static! { - static ref LATEST_FILE_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); - static ref FILE_STORE: std::sync::Mutex> = Default::default(); -} - -enum FileData { - Path(std::path::PathBuf), - Bytes(Vec), - Reader(Box), - Async(reqwest::Body), -} - -pub struct File { - data: FileData, - file_name: Option, - mime_type: Option, -} - -#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Copy)] -pub struct FileId(usize); - -impl TryFrom for FileId { - type Error = BoxErr; - - fn try_from(file: File) -> Result { - let file_id = LATEST_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let mut guard = FILE_STORE.lock().map_err(|_| "file store lock poisoned")?; - guard.insert(FileId(file_id), file); - Ok(FileId(file_id)) - } -} - -impl TryFrom for File { - type Error = BoxErr; - - fn try_from(file_id: FileId) -> Result { - let mut guard = FILE_STORE.lock().map_err(|_| "file store lock poisoned")?; - let file = guard.remove(&file_id).ok_or("file not found")?; - if file.file_name.is_none() { - Ok(file.file_name(file_id.0.to_string())) - } else { - Ok(file) - } - } -} - -impl File { - pub fn from_path>(path: P) -> Self { - Self { - data: FileData::Path(path.into()), - file_name: None, - mime_type: None, - } - } - - pub fn from_bytes>>(data: B) -> Self { - Self { - data: FileData::Bytes(data.into()), - file_name: None, - mime_type: None, - } - } - - pub fn from_reader(reader: R) -> Self { - Self { - data: FileData::Reader(Box::new(reader)), - file_name: None, - mime_type: None, - } - } - - pub fn from_async_reader(reader: R) -> Self { - use tokio_util::compat::FuturesAsyncReadCompatExt as _; - let reader = reader.compat(); - Self { - data: FileData::Async(reqwest::Body::wrap_stream( - tokio_util::io::ReaderStream::new(reader), - )), - file_name: None, - mime_type: None, - } - } -} - -impl File { - pub fn file_name(mut self, file_name: impl Into) -> Self { - self.file_name = Some(file_name.into()); - self - } - - pub fn mime_type(mut self, mime_type: impl Into) -> Self { - self.mime_type = Some(mime_type.into()); - self - } -} - -impl TryFrom for reqwest::blocking::multipart::Part { - type Error = BoxErr; - - fn try_from(file: File) -> Result { - let mut part = match file.data { - FileData::Path(path) => { - let file = std::fs::File::open(path.as_path())?; - let file_size = file.metadata()?.len(); - let mut part = - reqwest::blocking::multipart::Part::reader_with_length(file, file_size); - if let Some(name) = path.file_name() { - part = part.file_name(name.to_string_lossy().into_owned()); - } - part = part.mime_str( - mime_guess::from_path(&path) - .first_or_octet_stream() - .as_ref(), - )?; - part - } - - FileData::Bytes(data) => reqwest::blocking::multipart::Part::bytes(data), - - FileData::Reader(reader) => reqwest::blocking::multipart::Part::reader(reader), - - FileData::Async(_) => { - return Err("async readers are not supported".into()); - } - }; - - if let Some(file_name) = file.file_name { - part = part.file_name(file_name); - } - if let Some(mime_type) = file.mime_type { - part = part.mime_str(&mime_type)?; - } - Ok(part) - } -} - -impl File { - async fn into_reqwest_part(self) -> Result { - let mut part = match self.data { - FileData::Path(path) => reqwest::multipart::Part::file(path).await?, - FileData::Bytes(data) => reqwest::multipart::Part::bytes(data), - FileData::Async(body) => reqwest::multipart::Part::stream(body), - FileData::Reader(_) => { - return Err("sync readers are not supported".into()); - } - }; - - if let Some(file_name) = self.file_name { - part = part.file_name(file_name); - } - if let Some(mime_type) = self.mime_type { - part = part.mime_str(&mime_type)?; - } - Ok(part) - } -} - -#[derive(Debug)] -struct FileExtractor { - path: TypePath, - prefix: String, - current_path: ValuePath, - output: HashMap, -} - -impl FileExtractor { - fn extract_all_from( - variables: &mut JsonObject, - mut path_to_files: HashMap>, - ) -> Result, BoxErr> { - let mut output = HashMap::new(); - - for (key, value) in variables.iter_mut() { - let paths = path_to_files.remove(key).unwrap_or_default(); - for path in paths.into_iter() { - let mut extractor = Self { - path, - prefix: key.clone(), - current_path: ValuePath::default(), - output: std::mem::take(&mut output), - }; - extractor.extract_from_value(value)?; - output = extractor.output; - } - } - - Ok(output) - } - - fn extract_from_value(&mut self, value: &mut serde_json::Value) -> Result<(), BoxErr> { - let cursor = self.current_path.0.len(); - if cursor == self.path.0.len() { - // end of type_path; replace file_id with null - let file_id: FileId = serde_json::from_value(value.take())?; - self.output.insert(self.format_path(), file_id); - return Ok(()); - } - let segment = self.path.0[cursor]; - use ValuePathSegment as VPSeg; - match segment { - "?" => { - if !value.is_null() { - self.current_path.0.push(VPSeg::Optional); - self.extract_from_value(value)?; - self.current_path.0.pop(); - } - } - "[]" => { - let items = value - .as_array_mut() - .ok_or_else(|| format!("expected an array at {:?}", self.format_path()))?; - for (idx, item) in items.iter_mut().enumerate() { - self.current_path.0.push(VPSeg::Index(idx)); - self.extract_from_value(item)?; - self.current_path.0.pop(); - } - } - x if x.starts_with('.') => { - let key = &x[1..]; - let object = value - .as_object_mut() - .ok_or_else(|| format!("expected an object at {:?}", self.format_path()))?; - let mut null = serde_json::Value::Null; - let value = object.get_mut(key).unwrap_or(&mut null); - self.current_path.0.push(VPSeg::Prop(key)); - self.extract_from_value(value)?; - self.current_path.0.pop(); - } - _ => unreachable!(), - } - - Ok(()) - } - - /// format the path following the GraphQL multipart request spec - /// see: https://github.com/jaydenseric/graphql-multipart-request-spec - fn format_path(&self) -> String { - let mut res = self.prefix.clone(); - use ValuePathSegment as VPSeg; - for seg in &self.current_path.0 { - match seg { - VPSeg::Optional => {} - VPSeg::Index(idx) => res.push_str(&format!(".{}", idx)), - VPSeg::Prop(key) => res.push_str(&format!(".{}", key)), - } - } - res - } -} - -// -// --- --- Graph node types --- --- // -// - -type NodeMetaFn = fn() -> NodeMeta; - -/// How the [`node_metas`] module encodes the description -/// of the typegraph. -struct NodeMeta { - sub_nodes: Option>, - arg_types: Option>, - variants: Option>, - input_files: Option, -} - -enum SubNodes { - None, - Atomic(Vec), - Union(HashMap>), -} - -/// The final form of the nodes used in queries. -pub struct SelectNodeErased { - node_name: CowStr, - instance_name: CowStr, - args: Option, - sub_nodes: SubNodes, - input_files: Option, -} - -/// Wrappers around [`SelectNodeErased`] that only holds query nodes -pub struct QueryNode(SelectNodeErased, PhantomData<(Out,)>); -/// Wrappers around [`SelectNodeErased`] that only holds mutation nodes -pub struct MutationNode(SelectNodeErased, PhantomData<(Out,)>); - -/* /// Trait used to track the `Out` type parameter for [`QueryNode`]/[`MutationNode`] -pub trait ToSelectNode { - type Out; - - fn erased(self) -> SelectNodeErased; -} */ - -/// A variation of [`ToSelectNode`] to only be implemented -/// by aggregates of select nodes like [Vec]s. -pub trait ToSelectDoc { - type Out; - - fn to_select_doc(self) -> Vec; - fn parse_response(data: Vec) -> Result; -} - -/// Marker trait for [`ToSelectDoc`] implementors that only carry query nodes. -pub trait ToQueryDoc {} -/// Marker trait for [`ToSelectDoc`] implementors that only carry mutation nodes. -pub trait ToMutationDoc {} - -/// Struct used to mark query associated types that are generic about effect. -pub struct QueryMarker; -/// Struct used to mark mutationo associated types that are generic about effect. -pub struct MutationMarker; - -/// A node that's yet to have it's subnodes specified. -/// Use [`select`][Self::select] and [`select_aliased`][Self::select_aliased] -/// to finalize it. -/// [`select_aliased`][Self::select_aliased] will allow you to use [`alias`] -/// nodes but the returned object will be a raw [`serde_json::Value`]. -/// This type is generic over effect using the `QTy` parameter. -pub struct UnselectedNode { - root_name: CowStr, - root_meta: NodeMetaFn, - args: NodeArgsErased, - _marker: PhantomData<(SelT, SelAliasedT, QTy, Out)>, -} - -impl UnselectedNode -where - SelT: Into, -{ - fn select_erased(self, select: SelT) -> SelectNodeErased { - let nodes = selection_to_node_set( - SelectionErasedMap( - [( - self.root_name.clone(), - match self.args { - NodeArgsErased::None => SelectionErased::Composite(select.into()), - args => SelectionErased::CompositeArgs(args, select.into()), - }, - )] - .into(), - ), - &[(self.root_name, self.root_meta)].into(), - "$q".into(), - ) - .unwrap(); - nodes.into_iter().next().unwrap() - } -} - -impl UnselectedNode -where - SelAliased: Into, -{ - fn select_aliased_erased(self, select: SelAliased) -> SelectNodeErased { - let nodes = selection_to_node_set( - SelectionErasedMap( - [( - self.root_name.clone(), - match self.args { - NodeArgsErased::None => SelectionErased::Composite(select.into()), - args => SelectionErased::CompositeArgs(args, select.into()), - }, - )] - .into(), - ), - &[(self.root_name, self.root_meta)].into(), - "$q".into(), - ) - .unwrap(); - nodes.into_iter().next().unwrap() - } -} - -// NOTE: we'll need a select method implementation for each ATy x QTy pair - -impl UnselectedNode -where - SelT: Into, -{ - pub fn select(self, select: SelT) -> QueryNode { - QueryNode(self.select_erased(select), PhantomData) - } -} -impl UnselectedNode -where - SelAliased: Into, -{ - pub fn select_aliased(self, select: SelAliased) -> QueryNode { - QueryNode(self.select_aliased_erased(select), PhantomData) - } -} -impl UnselectedNode -where - SelT: Into, -{ - pub fn select(self, select: SelT) -> MutationNode { - MutationNode(self.select_erased(select), PhantomData) - } -} -impl UnselectedNode -where - SelAliased: Into, -{ - pub fn select_aliased(self, select: SelAliased) -> MutationNode { - MutationNode(self.select_aliased_erased(select), PhantomData) - } -} - -// --- --- Impl ToSelectDoc --- --- /// - -impl ToSelectDoc for QueryNode -where - Out: serde::de::DeserializeOwned, -{ - type Out = Out; - - fn to_select_doc(self) -> Vec { - vec![self.0] - } - - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - serde_json::from_value(data.next().unwrap()) - } -} -impl ToQueryDoc for QueryNode {} -impl ToSelectDoc for MutationNode -where - Out: serde::de::DeserializeOwned, -{ - type Out = Out; - - fn to_select_doc(self) -> Vec { - vec![self.0] - } - - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - serde_json::from_value(data.next().unwrap()) - } -} -impl ToMutationDoc for MutationNode {} - -#[macro_export] -macro_rules! impl_for_tuple { - ($($idx:tt $ty:tt),+) => { - impl<$($ty,)+> ToSelectDoc for ($(QueryNode<$ty>,)+) - where $($ty: serde::de::DeserializeOwned,)+ - { - type Out = ($($ty,)+); - - fn to_select_doc(self) -> Vec { - vec![ - $(self.$idx.0,)+ - ] - } - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - let mut next = move |_idx| data.next().unwrap(); - Ok(( - - $(serde_json::from_value(next($idx))?,)+ - )) - } - } - impl<$($ty,)+> ToSelectDoc for ($(MutationNode<$ty>,)+) - where $($ty: serde::de::DeserializeOwned,)+ - { - type Out = ($($ty,)+); - - fn to_select_doc(self) -> Vec { - vec![ - $(self.$idx.0,)+ - ] - } - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - let mut next = move |_idx| data.next().unwrap(); - Ok(( - - $(serde_json::from_value(next($idx))?,)+ - )) - } - } - - impl<$($ty,)+> ToQueryDoc for ($($ty,)+) - where - $($ty: ToQueryDoc,)+ - {} - - impl<$($ty,)+> ToMutationDoc for ($($ty,)+) - where - $($ty: ToMutationDoc,)+ - {} - }; -} - -impl_for_tuple!(0 N0); -impl_for_tuple!(0 N0, 1 N1); -impl_for_tuple!(0 N0, 1 N1, 2 N2); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9, 10 N10); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9, 10 N10, 11 N11); - -// -// --- -- --- Selection types --- --- // -// - -// This is a newtype for Into trait impl purposes -#[derive(Debug)] -pub struct SelectionErasedMap(HashMap); - -#[derive(Debug)] -pub enum CompositeSelection { - Atomic(SelectionErasedMap), - Union(HashMap), -} - -impl Default for CompositeSelection { - fn default() -> Self { - CompositeSelection::Atomic(SelectionErasedMap(Default::default())) - } -} - -#[derive(Debug)] -enum SelectionErased { - None, - Scalar, - ScalarArgs(NodeArgsErased), - Composite(CompositeSelection), - CompositeArgs(NodeArgsErased, CompositeSelection), - Alias(HashMap), -} - -#[derive(Debug)] -pub enum AliasSelection { - Scalar, - ScalarArgs(NodeArgsErased), - Composite(CompositeSelection), - CompositeArgs(NodeArgsErased, CompositeSelection), -} - -#[derive(Default, Clone, Copy, Debug)] -pub struct HasAlias; -#[derive(Default, Clone, Copy, Debug)] -pub struct NoAlias; - -#[derive(Debug)] -pub struct AliasInfo { - aliases: HashMap, - _phantom: PhantomData<(ArgT, SelT, ATyag)>, -} - -#[derive(Debug)] -pub enum ScalarSelect { - Get, - Skip, - Alias(AliasInfo<(), (), ATy>), -} -#[derive(Debug)] -pub enum ScalarSelectArgs { - Get(NodeArgsErased, PhantomData), - Skip, - Alias(AliasInfo), -} -#[derive(Debug)] -pub enum CompositeSelect { - Get(CompositeSelection, PhantomData), - Skip, - Alias(AliasInfo<(), SelT, ATy>), -} -#[derive(Debug)] -pub enum CompositeSelectArgs { - Get( - NodeArgsErased, - CompositeSelection, - PhantomData<(ArgT, SelT)>, - ), - Skip, - Alias(AliasInfo), -} - -pub struct Get; -pub struct Skip; -pub struct Args(ArgT); -pub struct Select(SelT); -pub struct ArgSelect(ArgT, SelT); -pub struct Alias(AliasInfo); - -/// Shorthand for `Default::default`. All selections generally default -/// to [`skip`]. -pub fn default() -> T { - T::default() -} -/// Include all sub nodes excpet those that require arguments -pub fn all() -> T { - T::all() -} -/// Select the node for inclusion. -pub fn get>() -> T { - T::from(Get) -} -/// Skip this node when queryig. -pub fn skip>() -> T { - T::from(Skip) -} -/// Provide argumentns for a scalar node. -pub fn args>>(args: ArgT) -> T { - T::from(Args(args)) -} -/// Provide selections for a composite node that takes no args. -pub fn select>>(selection: SelT) -> T { - T::from(Select(selection)) -} -/// Provide arguments and selections for a composite node. -pub fn arg_select>>(args: ArgT, selection: SelT) -> T { - T::from(ArgSelect(args, selection)) -} - -/// Query the same node multiple times using aliases. -/// -/// WARNING: make sure your alias names don't clash across sibling -/// nodes. -pub fn alias(info: impl Into>) -> T -where - S: Into, - ASelT: Into, - T: From> + FromAliasSelection, -{ - let info: HashMap<_, _> = info.into(); - T::from(Alias(AliasInfo { - aliases: info - .into_iter() - .map(|(name, sel)| (name.into(), sel.into())) - .collect(), - _phantom: PhantomData, - })) -} - -pub trait Selection { - /// Include all sub nodes excpet those that require arguments - fn all() -> Self; -} - -// --- Impl SelectionType impls --- // - -impl Selection for ScalarSelect { - fn all() -> Self { - Self::Get - } -} -impl Selection for ScalarSelectArgs { - fn all() -> Self { - Self::Skip - } -} -impl Selection for CompositeSelect -where - SelT: Selection + Into, -{ - fn all() -> Self { - let sel = SelT::all(); - Self::Get(sel.into(), PhantomData) - } -} -impl Selection for CompositeSelectArgs -where - SelT: Selection, -{ - fn all() -> Self { - Self::Skip - } -} -// --- Default impls --- // - -impl Default for ScalarSelect { - fn default() -> Self { - Self::Skip - } -} -impl Default for ScalarSelectArgs { - fn default() -> Self { - Self::Skip - } -} -impl Default for CompositeSelect { - fn default() -> Self { - Self::Skip - } -} -impl Default for CompositeSelectArgs { - fn default() -> Self { - Self::Skip - } -} - -// --- From Get/Skip...etc impls --- // - -impl From for ScalarSelect { - fn from(_: Get) -> Self { - Self::Get - } -} - -impl From for ScalarSelect { - fn from(_: Skip) -> Self { - Self::Skip - } -} -impl From for ScalarSelectArgs { - fn from(_: Skip) -> Self { - Self::Skip - } -} -impl From for CompositeSelect { - fn from(_: Skip) -> Self { - Self::Skip - } -} -impl From for CompositeSelectArgs { - fn from(_: Skip) -> Self { - Self::Skip - } -} - -impl From> for ScalarSelectArgs -where - ArgT: Serialize, -{ - fn from(Args(args): Args) -> Self { - Self::Get(NodeArgsErased::Inline(to_json_value(args)), PhantomData) - } -} - -impl From> for CompositeSelect -where - SelT: Into, -{ - fn from(Select(selection): Select) -> Self { - Self::Get(selection.into(), PhantomData) - } -} - -impl From> for CompositeSelectArgs -where - ArgT: Serialize, - SelT: Into, -{ - fn from(ArgSelect(args, selection): ArgSelect) -> Self { - Self::Get( - NodeArgsErased::Inline(to_json_value(args)), - selection.into(), - PhantomData, - ) - } -} - -impl From> for ScalarSelectArgs { - fn from(value: PlaceholderArg) -> Self { - Self::Get(NodeArgsErased::Placeholder(value.value), PhantomData) - } -} -impl From> - for CompositeSelectArgs -where - SelT: Into, -{ - fn from(value: PlaceholderArgSelect) -> Self { - Self::Get( - NodeArgsErased::Placeholder(value.value), - value.selection.into(), - PhantomData, - ) - } -} - -// --- ToAliasSelection impls --- // - -/// This is a marker trait that allows the core selection types -/// like CompositeSelectNoArgs to mark which types can be used -/// as their aliasing nodes. This prevents usage of invalid selections -/// on aliases like [`Skip`]. -pub trait FromAliasSelection {} - -impl FromAliasSelection for ScalarSelect {} -impl FromAliasSelection> for ScalarSelectArgs {} -impl FromAliasSelection> for CompositeSelect {} -impl FromAliasSelection> - for CompositeSelectArgs -{ -} - -// --- From Alias impls --- // - -impl From>> for ScalarSelect { - fn from(Alias(info): Alias<(), ScalarSelect>) -> Self { - Self::Alias(AliasInfo { - aliases: info.aliases, - _phantom: PhantomData, - }) - } -} -impl From> for ScalarSelectArgs { - fn from(Alias(info): Alias) -> Self { - Self::Alias(info) - } -} -impl From> for CompositeSelect { - fn from(Alias(info): Alias<(), SelT>) -> Self { - Self::Alias(info) - } -} -impl From> for CompositeSelectArgs { - fn from(Alias(info): Alias) -> Self { - Self::Alias(info) - } -} - -// --- Into SelectionErased impls --- // - -impl From> for SelectionErased { - fn from(value: AliasInfo) -> SelectionErased { - SelectionErased::Alias(value.aliases) - } -} - -impl From> for SelectionErased { - fn from(value: ScalarSelect) -> SelectionErased { - use ScalarSelect::*; - match value { - Get => SelectionErased::Scalar, - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -impl From> for SelectionErased { - fn from(value: ScalarSelectArgs) -> SelectionErased { - use ScalarSelectArgs::*; - match value { - Get(arg, _) => SelectionErased::ScalarArgs(arg), - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -impl From> for SelectionErased { - fn from(value: CompositeSelect) -> SelectionErased { - use CompositeSelect::*; - match value { - Get(selection, _) => SelectionErased::Composite(selection), - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -impl From> for SelectionErased -where - SelT: Into, -{ - fn from(value: CompositeSelectArgs) -> SelectionErased { - use CompositeSelectArgs::*; - match value { - Get(args, selection, _) => SelectionErased::CompositeArgs(args, selection), - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -// --- UnionMember impls --- // - -/// The following trait is used for types that implement -/// selections for the composite members of unions. -/// -/// The err return value indicates the case where -/// aliases are used selections on members which is an error -/// -/// This state is currently impossible to arrive at since -/// AliasInfo has no public construction methods with NoAlias -/// set. Union selection types make sure all their immediate -/// member selection use NoAlias to prevent this invalid stat.e -pub trait UnionMember { - fn composite(self) -> Option; -} - -/// Internal marker trait use to make sure we can't have union members -/// selection being another union selection. -trait NotUnionSelection {} - -// NOTE: UnionMembers are all NoAlias -impl UnionMember for ScalarSelect { - fn composite(self) -> Option { - None - } -} - -impl UnionMember for ScalarSelectArgs { - fn composite(self) -> Option { - None - } -} - -impl UnionMember for CompositeSelect -where - SelT: NotUnionSelection, -{ - fn composite(self) -> Option { - use CompositeSelect::*; - match self { - Get(CompositeSelection::Atomic(selection), _) => Some(selection), - Skip => None, - Get(CompositeSelection::Union(_), _) => { - unreachable!("union selection on union member selection. how??") - } - Alias(_) => unreachable!("alias discovored on union/either member. how??"), - } - } -} - -impl UnionMember for CompositeSelectArgs -where - SelT: NotUnionSelection, -{ - fn composite(self) -> Option { - use CompositeSelectArgs::*; - match self { - Get(_args, CompositeSelection::Atomic(selection), _) => Some(selection), - Skip => None, - Get(_args, CompositeSelection::Union(_), _) => { - unreachable!("union selection on union member selection. how??") - } - Alias(_) => unreachable!("alias discovored on union/either member. how??"), - } - } -} - -// --- Into AliasSelection impls --- // - -impl From for AliasSelection { - fn from(_val: Get) -> Self { - AliasSelection::Scalar - } -} -impl From> for AliasSelection -where - ArgT: Serialize, -{ - fn from(val: Args) -> Self { - AliasSelection::ScalarArgs(NodeArgsErased::Inline(to_json_value(val.0))) - } -} -impl From> for AliasSelection -where - SelT: Into, -{ - fn from(val: Select) -> Self { - let map = val.0.into(); - AliasSelection::Composite(map) - } -} - -impl From> for AliasSelection -where - ArgT: Serialize, - SelT: Into, -{ - fn from(val: ArgSelect) -> Self { - let map = val.1.into(); - AliasSelection::CompositeArgs(NodeArgsErased::Inline(to_json_value(val.0)), map) - } -} -impl From> for AliasSelection { - fn from(val: ScalarSelect) -> Self { - use ScalarSelect::*; - match val { - Get => AliasSelection::Scalar, - _ => unreachable!(), - } - } -} -impl From> for AliasSelection { - fn from(val: ScalarSelectArgs) -> Self { - use ScalarSelectArgs::*; - match val { - Get(args, _) => AliasSelection::ScalarArgs(args), - _ => unreachable!(), - } - } -} - -impl From> for AliasSelection -where - SelT: Into, -{ - fn from(val: CompositeSelect) -> Self { - use CompositeSelect::*; - match val { - Get(select, _) => AliasSelection::Composite(select), - _ => unreachable!(), - } - } -} -impl From> for AliasSelection -where - SelT: Into, -{ - fn from(val: CompositeSelectArgs) -> Self { - use CompositeSelectArgs::*; - match val { - Get(args, selection, _) => AliasSelection::CompositeArgs(args, selection), - _ => unreachable!(), - } - } -} - -// TODO: convert to proc_macro -#[macro_export] -macro_rules! impl_selection_traits { - ($ty:ident,$($field:tt),+) => { - impl From<$ty> for CompositeSelection { - fn from(value: $ty) -> CompositeSelection { - CompositeSelection::Atomic(SelectionErasedMap( - [ - $((stringify!($field).into(), value.$field.into()),)+ - ] - .into(), - )) - } - } - - impl Selection for $ty { - fn all() -> Self { - Self { - $($field: all(),)+ - } - } - } - - impl NotUnionSelection for $ty {} - }; -} -#[macro_export] -macro_rules! impl_union_selection_traits { - ($ty:ident,$(($variant_ty:tt, $field:tt)),+) => { - impl From<$ty> for CompositeSelection { - fn from(value: $ty) -> CompositeSelection { - CompositeSelection::Union( - [ - $({ - let selection = - UnionMember::composite(value.$field); - selection.map(|val| ($variant_ty.into(), val)) - },)+ - ] - .into_iter() - .filter_map(|val| val) - .collect(), - ) - } - } - }; -} - -// -// --- --- Argument types --- --- // -// - -pub enum NodeArgs { - Inline(ArgT), - Placeholder(PlaceholderValue), -} - -impl From for NodeArgs { - fn from(value: ArgT) -> Self { - Self::Inline(value) - } -} - -#[derive(Debug)] -pub enum NodeArgsErased { - None, - Inline(serde_json::Value), - Placeholder(PlaceholderValue), -} - -impl From> for NodeArgsErased -where - ArgT: Serialize, -{ - fn from(value: NodeArgs) -> Self { - match value { - NodeArgs::Inline(arg) => Self::Inline(to_json_value(arg)), - NodeArgs::Placeholder(ph) => Self::Placeholder(ph), - } - } -} - -enum NodeArgsMerged { - Inline(HashMap), - Placeholder { - value: PlaceholderValue, - arg_types: HashMap, - }, -} - -/// This checks the input arg json for a node -/// against the arg description from the [`NodeMeta`]. -fn check_node_args( - args: serde_json::Value, - arg_types: &HashMap, -) -> Result, String> { - let args = match args { - serde_json::Value::Object(val) => val, - _ => unreachable!(), - }; - let mut instance_args = HashMap::new(); - for (name, value) in args { - let Some(type_name) = arg_types.get(&name[..]) else { - return Err(name); - }; - instance_args.insert( - name.into(), - NodeArgValue { - type_name: type_name.clone(), - value, - }, - ); - } - Ok(instance_args) -} - -struct NodeArgValue { - type_name: CowStr, - value: serde_json::Value, -} - -pub struct PreparedArgs; - -impl PreparedArgs { - pub fn get(&mut self, key: impl Into, fun: F) -> NodeArgs - where - In: serde::de::DeserializeOwned, - F: Fn(In) -> ArgT + 'static + Send + Sync, - ArgT: Serialize, - { - NodeArgs::Placeholder(PlaceholderValue { - key: key.into(), - fun: Box::new(move |value| { - let value = serde_json::from_value(value)?; - let value = fun(value); - serde_json::to_value(value) - }), - }) - } - pub fn arg(&mut self, key: impl Into, fun: F) -> T - where - T: From>, - In: serde::de::DeserializeOwned, - F: Fn(In) -> ArgT + 'static + Send + Sync, - ArgT: Serialize, - { - T::from(PlaceholderArg { - value: PlaceholderValue { - key: key.into(), - fun: Box::new(move |value| { - let value = serde_json::from_value(value)?; - let value = fun(value); - serde_json::to_value(value) - }), - }, - _phantom: PhantomData, - }) - } - pub fn arg_select( - &mut self, - key: impl Into, - selection: SelT, - fun: F, - ) -> T - where - T: From>, - In: serde::de::DeserializeOwned, - F: Fn(In) -> ArgT + 'static + Send + Sync, - ArgT: Serialize, - { - T::from(PlaceholderArgSelect { - value: PlaceholderValue { - key: key.into(), - fun: Box::new(move |value| { - let value = serde_json::from_value(value)?; - let value = fun(value); - serde_json::to_value(value) - }), - }, - selection, - _phantom: PhantomData, - }) - } -} - -pub struct PlaceholderValue { - key: CowStr, - fun: Box< - dyn Fn(serde_json::Value) -> Result + Send + Sync, - >, -} - -impl std::fmt::Debug for PlaceholderValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PlaceholderValue") - .field("key", &self.key) - .finish_non_exhaustive() - } -} - -pub struct PlaceholderArg { - value: PlaceholderValue, - _phantom: PhantomData, -} -pub struct PlaceholderArgSelect { - value: PlaceholderValue, - selection: SelT, - _phantom: PhantomData, -} - -pub struct PlaceholderArgs(Arg); - -// -// --- --- GraphQL types --- --- // -// - -use graphql::*; -pub mod graphql { - use std::sync::Arc; - - use super::*; - - pub(super) type TyToGqlTyMap = Arc>; - - #[derive(Default, Clone)] - pub struct GraphQlTransportOptions { - headers: reqwest::header::HeaderMap, - timeout: Option, - } - - // PlaceholderValue, fieldName -> gql_var_name - type FoundPlaceholders = Vec<(PlaceholderValue, HashMap)>; - - struct GqlRequest { - doc: String, - variables: JsonObject, - placeholders: FoundPlaceholders, - path_to_files: HashMap>, - } - - struct GqlRequestBuilder<'a> { - ty_to_gql_ty_map: &'a TyToGqlTyMap, - variable_values: JsonObject, - variable_types: HashMap, - // map variable name to path to file types - path_to_files: HashMap>, - doc: String, - placeholders: Vec<(PlaceholderValue, HashMap)>, - } - - impl<'a> GqlRequestBuilder<'a> { - fn new(ty_to_gql_ty_map: &'a TyToGqlTyMap) -> Self { - Self { - ty_to_gql_ty_map, - variable_values: Default::default(), - variable_types: Default::default(), - path_to_files: Default::default(), - doc: Default::default(), - placeholders: Default::default(), - } - } - - fn register_path_to_files(&mut self, name: String, key: &str, files: &PathToInputFiles) { - let path_to_files = files - .0 - .iter() - .filter_map(|path| { - let first = path[0]; - if first.starts_with('.') && &first[1..] == key { - Some(TypePath(&path[1..])) - } else { - None - } - }) - .collect::>(); - self.path_to_files.insert(name, path_to_files); - } - - fn select_node_to_gql(&mut self, node: SelectNodeErased) -> std::fmt::Result { - use std::fmt::Write; - if node.instance_name != node.node_name { - write!(self.doc, "{}: {}", node.instance_name, node.node_name)?; - } else { - write!(self.doc, "{}", node.node_name)?; - } - - if let Some(args) = node.args { - match args { - NodeArgsMerged::Inline(args) => { - if !args.is_empty() { - write!(&mut self.doc, "(")?; - for (key, val) in args { - let name = format!("in{}", self.variable_types.len()); - - let mut map = serde_json::Map::new(); - map.insert(key.clone().into(), val.value.clone()); - let mut object = serde_json::Value::Object(map); - - if let Some(files) = node.input_files.as_ref() { - self.register_path_to_files(name.clone(), key.as_ref(), files); - } - - write!(&mut self.doc, "{key}: ${name}, ")?; - self.variable_values.insert( - name.clone(), - object - .as_object_mut() - .unwrap() - .remove(key.as_ref()) - .unwrap(), - ); - self.variable_types.insert(name.into(), val.type_name); - } - write!(&mut self.doc, ")")?; - } - } - NodeArgsMerged::Placeholder { value, arg_types } => { - if !arg_types.is_empty() { - write!(&mut self.doc, "(")?; - let mut map = HashMap::new(); - for (key, type_name) in arg_types { - let name = format!("in{}", self.variable_types.len()); - if let Some(files) = node.input_files.as_ref() { - self.register_path_to_files(name.clone(), key.as_ref(), files); - } - write!(&mut self.doc, "{key}: ${name}, ")?; - self.variable_types.insert(name.clone().into(), type_name); - map.insert(key, name.into()); - } - write!(&mut self.doc, ")")?; - self.placeholders.push((value, map)); - } - } - } - } - - match node.sub_nodes { - SubNodes::None => {} - SubNodes::Atomic(sub_nodes) => { - write!(&mut self.doc, "{{ ")?; - for node in sub_nodes { - self.select_node_to_gql(node)?; - write!(&mut self.doc, " ")?; - } - write!(&mut self.doc, " }}")?; - } - SubNodes::Union(variants) => { - write!(&mut self.doc, "{{ ")?; - for (ty, sub_nodes) in variants { - let gql_ty = self.ty_to_gql_ty_map.get(&ty[..]).expect( - "impossible: no GraphQL type equivalent found for variant type", - ); - let gql_ty = match gql_ty.strip_suffix('!') { - Some(val) => val, - None => &gql_ty[..], - }; - write!(&mut self.doc, " ... on {gql_ty} {{ ")?; - for node in sub_nodes { - self.select_node_to_gql(node)?; - write!(&mut self.doc, " ")?; - } - write!(&mut self.doc, " }}")?; - } - write!(&mut self.doc, " }}")?; - } - } - Ok(()) - } - - fn build( - mut self, - nodes: Vec, - ty: &'static str, - name: Option, - ) -> Result { - use std::fmt::Write; - - for (idx, node) in nodes.into_iter().enumerate() { - let node = SelectNodeErased { - instance_name: format!("node{idx}").into(), - ..node - }; - write!(&mut self.doc, " ").expect("error building to string"); - self.select_node_to_gql(node) - .expect("error building to string"); - writeln!(&mut self.doc).expect("error building to string"); - } - - let mut args_row = String::new(); - if !self.variable_types.is_empty() { - write!(&mut args_row, "(").expect("error building to string"); - for (key, ty) in &self.variable_types { - let gql_ty = self.ty_to_gql_ty_map.get(&ty[..]).ok_or_else(|| { - GraphQLRequestError::InvalidQuery { - error: Box::from(format!("unknown typegraph type found: {}", ty)), - } - })?; - write!(&mut args_row, "${key}: {gql_ty}, ").expect("error building to string"); - } - write!(&mut args_row, ")").expect("error building to string"); - } - - let name = name.unwrap_or_else(|| "".into()); - let doc = format!("{ty} {name}{args_row} {{\n{doc}}}", doc = self.doc); - Ok(GqlRequest { - doc, - variables: self.variable_values, - placeholders: self.placeholders, - path_to_files: self.path_to_files, - }) - } - } - - enum GraphQLRequestBody { - Json(serde_json::Value), - Multipart(reqwest::multipart::Form), - } - - struct GraphQLRequest { - addr: Url, - method: reqwest::Method, - headers: reqwest::header::HeaderMap, - body: GraphQLRequestBody, - } - - use reqwest::blocking::{Client as ClientSync, RequestBuilder as RequestBuilderSync}; - - enum BuildReqError { - FileUpload { error: BoxErr }, - } - - fn build_gql_req_sync( - client: &ClientSync, - addr: Url, - doc: &str, - mut variables: JsonObject, - path_to_files: HashMap>, - opts: &GraphQlTransportOptions, - ) -> Result { - use reqwest::blocking::multipart::Form; - - let files = FileExtractor::extract_all_from(&mut variables, path_to_files) - .map_err(|error| BuildReqError::FileUpload { error })?; - - let mut request = client.request(reqwest::Method::POST, addr); - if let Some(timeout) = opts.timeout { - request = request.timeout(timeout); - } - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::ACCEPT, - "application/json".try_into().unwrap(), - ); - headers.extend(opts.headers.clone()); - - let operations = serde_json::json!({ - "query": doc, - "variables": variables - }); - - // TODO rename files - - if !files.is_empty() { - // multipart - let mut form = Form::new(); - - form = form.text("operations", serde_json::to_string(&operations).unwrap()); - - let (map, files): (HashMap<_, _>, Vec<_>) = files - .into_iter() - .enumerate() - .map(|(idx, (path, file_id))| { - ( - (idx.to_string(), vec![format!("variables.{path}")]), - file_id, - ) - }) - .unzip(); - - form = form.text("map", serde_json::to_string(&map).unwrap()); - - for (idx, file_id) in files.into_iter().enumerate() { - let file: File = file_id - .try_into() - .map_err(|error| BuildReqError::FileUpload { error })?; - form = form.part( - idx.to_string(), - file.try_into() - .map_err(|error| BuildReqError::FileUpload { error })?, - ); - } - - Ok(request.headers(headers).multipart(form)) - } else { - headers.insert( - reqwest::header::CONTENT_TYPE, - "application/json".try_into().unwrap(), - ); - Ok(request.headers(headers).json(&operations)) - } - } - - use reqwest::{Client, RequestBuilder}; - async fn build_gql_req( - client: &Client, - addr: Url, - doc: &str, - mut variables: JsonObject, - path_to_files: HashMap>, - opts: &GraphQlTransportOptions, - ) -> Result { - use reqwest::multipart::Form; - - let files = FileExtractor::extract_all_from(&mut variables, path_to_files) - .map_err(|error| BuildReqError::FileUpload { error })?; - - let request = client.request(reqwest::Method::POST, addr); - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::ACCEPT, - "application/json".try_into().unwrap(), - ); - headers.extend(opts.headers.clone()); - - let operations = serde_json::json!({ - "query": doc, - "variables": variables - }); - - if !files.is_empty() { - // multipart - let mut form = Form::new(); - - form = form.text("operations", serde_json::to_string(&operations).unwrap()); - - let (map, files): (HashMap<_, _>, Vec<_>) = files - .into_iter() - .enumerate() - .map(|(idx, (path, file_id))| { - ( - (idx.to_string(), vec![format!("variables.{path}")]), - file_id, - ) - }) - .unzip(); - - form = form.text("map", serde_json::to_string(&map).unwrap()); - - for (idx, file_id) in files.into_iter().enumerate() { - let file: File = file_id - .try_into() - .map_err(|error| BuildReqError::FileUpload { error })?; - form = form.part( - idx.to_string(), - file.into_reqwest_part() - .await - .map_err(|error| BuildReqError::FileUpload { error })?, - ); - } - - Ok(request.headers(headers).multipart(form)) - } else { - headers.insert( - reqwest::header::CONTENT_TYPE, - "application/json".try_into().unwrap(), - ); - Ok(request.headers(headers).json(&operations)) - } - } - - #[derive(Debug)] - pub struct GraphQLResponse { - pub status: reqwest::StatusCode, - pub headers: reqwest::header::HeaderMap, - pub body: JsonObject, - } - - fn handle_response( - response: GraphQLResponse, - nodes_len: usize, - ) -> Result, GraphQLRequestError> { - if !response.status.is_success() { - return Err(GraphQLRequestError::RequestFailed { response }); - } - #[derive(Debug, Deserialize)] - struct Response { - data: Option, - errors: Option>, - } - let body: Response = match serde_json::from_value(serde_json::Value::Object(response.body)) - { - Ok(body) => body, - Err(error) => { - return Err(GraphQLRequestError::BodyError { - error: Box::new(error), - }) - } - }; - if let Some(errors) = body.errors { - return Err(GraphQLRequestError::RequestErrors { - errors, - data: body.data, - }); - } - let Some(mut body) = body.data else { - return Err(GraphQLRequestError::BodyError { - error: Box::from("body response doesn't contain data field"), - }); - }; - (0..nodes_len) - .map(|idx| { - body.remove(&format!("node{idx}")) - .ok_or_else(|| GraphQLRequestError::BodyError { - error: Box::from(format!( - "expecting response under node key 'node{idx}' but none found" - )), - }) - }) - .collect::, _>>() - } - - #[derive(Debug)] - pub enum GraphQLRequestError { - /// GraphQL errors recieived - RequestErrors { - errors: Vec, - data: Option, - }, - /// Http error codes recieived - RequestFailed { - response: GraphQLResponse, - }, - /// Unable to deserialize body - BodyError { - error: BoxErr, - }, - /// Unable to make http request - NetworkError { - error: BoxErr, - }, - InvalidQuery { - error: BoxErr, - }, - /// Unable to upload file - FileUpload { - error: BoxErr, - }, - } - - impl From for GraphQLRequestError { - fn from(error: BuildReqError) -> Self { - match error { - BuildReqError::FileUpload { error } => GraphQLRequestError::FileUpload { error }, - } - } - } - - impl std::fmt::Display for GraphQLRequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - GraphQLRequestError::RequestErrors { errors, .. } => { - write!(f, "graphql errors in response: ")?; - for err in errors { - write!(f, "{}, ", err.message)?; - } - } - GraphQLRequestError::RequestFailed { response } => { - write!(f, "request failed with status {}", response.status)?; - } - GraphQLRequestError::BodyError { error } => { - write!(f, "error reading request body: {error}")?; - } - GraphQLRequestError::NetworkError { error } => { - write!(f, "error making http request: {error}")?; - } - GraphQLRequestError::InvalidQuery { error } => { - write!(f, "error building request: {error}")? - } - GraphQLRequestError::FileUpload { error } => { - write!(f, "error uploading file: {error}")? - } - } - Ok(()) - } - } - impl std::error::Error for GraphQLRequestError {} - - #[derive(Debug, Deserialize)] - pub struct ErrorLocation { - pub line: u32, - pub column: u32, - } - #[derive(Debug, Deserialize)] - pub struct GraphqlError { - pub message: String, - pub locations: Option>, - pub path: Option>, - } - - #[derive(Debug)] - pub enum PathSegment { - Field(String), - Index(u64), - } - - impl<'de> serde::de::Deserialize<'de> for PathSegment { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - use serde_json::Value; - let val = Value::deserialize(deserializer)?; - match val { - Value::Number(n) => Ok(PathSegment::Index(n.as_u64().unwrap())), - Value::String(s) => Ok(PathSegment::Field(s)), - _ => panic!("invalid path segment type"), - } - } - } - - #[derive(Clone)] - pub struct GraphQlTransportReqwestSync { - addr: Url, - ty_to_gql_ty_map: TyToGqlTyMap, - client: reqwest::blocking::Client, - } - - #[derive(Clone)] - pub struct GraphQlTransportReqwest { - addr: Url, - ty_to_gql_ty_map: TyToGqlTyMap, - client: reqwest::Client, - } - - impl GraphQlTransportReqwestSync { - pub fn new(addr: Url, ty_to_gql_ty_map: TyToGqlTyMap) -> Self { - Self { - addr, - ty_to_gql_ty_map, - client: reqwest::blocking::Client::new(), - } - } - - fn fetch( - &self, - nodes: Vec, - opts: &GraphQlTransportOptions, - ty: &'static str, - ) -> Result, GraphQLRequestError> { - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(&self.ty_to_gql_ty_map).build(nodes, ty, None)?; - if !placeholders.is_empty() { - panic!("placeholders found in non-prepared query") - } - let req = build_gql_req_sync( - &self.client, - self.addr.clone(), - &doc, - variables, - path_to_files, - opts, - )?; - match req.send() { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::() { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - nodes_len, - ), - Err(error) => Err(GraphQLRequestError::BodyError { - error: Box::new(error), - }), - } - } - Err(error) => Err(GraphQLRequestError::NetworkError { - error: Box::new(error), - }), - } - } - - pub fn query( - &self, - nodes: Doc, - ) -> Result { - self.query_with_opts(nodes, &Default::default()) - } - - pub fn query_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "query")?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - - pub fn mutation( - &self, - nodes: Doc, - ) -> Result { - self.mutation_with_opts(nodes, &Default::default()) - } - - pub fn mutation_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "mutation")?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - pub fn prepare_query( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_query_with_opts(fun, Default::default()) - } - - pub fn prepare_query_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwestSync::new( - fun, - self.addr.clone(), - opts, - "query", - &self.ty_to_gql_ty_map, - ) - } - - pub fn prepare_mutation( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_mutation_with_opts(fun, Default::default()) - } - - pub fn prepare_mutation_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwestSync::new( - fun, - self.addr.clone(), - opts, - "mutation", - &self.ty_to_gql_ty_map, - ) - } - } - - impl GraphQlTransportReqwest { - pub fn new(addr: Url, ty_to_gql_ty_map: TyToGqlTyMap) -> Self { - Self { - addr, - ty_to_gql_ty_map, - client: reqwest::Client::new(), - } - } - - async fn fetch( - &self, - nodes: Vec, - opts: &GraphQlTransportOptions, - ty: &'static str, - ) -> Result, GraphQLRequestError> { - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(&self.ty_to_gql_ty_map).build(nodes, ty, None)?; - if !placeholders.is_empty() { - panic!("placeholders found in non-prepared query") - } - - let req = build_gql_req( - &self.client, - self.addr.clone(), - &doc, - variables, - path_to_files, - opts, - ) - .await?; - match req.send().await { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::().await { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - nodes_len, - ), - Err(error) => Err(GraphQLRequestError::BodyError { - error: Box::new(error), - }), - } - } - Err(error) => Err(GraphQLRequestError::NetworkError { - error: Box::new(error), - }), - } - } - - pub async fn query( - &self, - nodes: Doc, - ) -> Result { - self.query_with_opts(nodes, &Default::default()).await - } - - pub async fn query_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "query").await?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - - pub async fn mutation( - &self, - nodes: Doc, - ) -> Result { - self.mutation_with_opts(nodes, &Default::default()).await - } - - pub async fn mutation_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "mutation").await?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - pub fn prepare_query( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_query_with_opts(fun, Default::default()) - } - - pub fn prepare_query_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwest::new( - fun, - self.addr.clone(), - opts, - "query", - &self.ty_to_gql_ty_map, - ) - } - - pub fn prepare_mutation( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_mutation_with_opts(fun, Default::default()) - } - - pub fn prepare_mutation_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwest::new( - fun, - self.addr.clone(), - opts, - "mutation", - &self.ty_to_gql_ty_map, - ) - } - } - - fn resolve_prepared_variables( - placeholders: &FoundPlaceholders, - mut inline_variables: JsonObject, - mut args: HashMap, - ) -> Result { - for (ph, key_map) in placeholders { - let Some(value) = args.remove(&ph.key) else { - return Err(PrepareRequestError::PlaceholderError(Box::from(format!( - "no value found for placeholder expected under key '{}'", - ph.key - )))); - }; - let value = (ph.fun)(value).map_err(|err| { - PrepareRequestError::PlaceholderError(Box::from(format!( - "error applying placeholder closure for value under key '{}': {err}", - ph.key - ))) - })?; - let serde_json::Value::Object(mut value) = value else { - unreachable!("placeholder closures must return structs"); - }; - for (key, var_key) in key_map { - inline_variables.insert( - var_key.clone().into(), - value.remove(&key[..]).unwrap_or(serde_json::Value::Null), - ); - } - } - Ok(inline_variables) - } - - pub struct PreparedRequestReqwest { - addr: Url, - client: reqwest::Client, - nodes_len: usize, - pub doc: String, - variables: JsonObject, - path_to_files: HashMap>, - opts: GraphQlTransportOptions, - placeholders: Arc, - _phantom: PhantomData, - } - - pub struct PreparedRequestReqwestSync { - addr: Url, - client: reqwest::blocking::Client, - nodes_len: usize, - pub doc: String, - variables: JsonObject, - path_to_files: HashMap>, - opts: GraphQlTransportOptions, - placeholders: Arc, - _phantom: PhantomData, - } - - impl PreparedRequestReqwestSync { - fn new( - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - addr: Url, - opts: GraphQlTransportOptions, - ty: &'static str, - ty_to_gql_ty_map: &TyToGqlTyMap, - ) -> Result { - let nodes = fun(&mut PreparedArgs); - let nodes = nodes.to_select_doc(); - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(ty_to_gql_ty_map) - .build(nodes, ty, None) - .map_err(PrepareRequestError::BuildError)?; - Ok(Self { - doc, - variables, - path_to_files, - nodes_len, - addr, - client: reqwest::blocking::Client::new(), - opts, - placeholders: Arc::new(placeholders), - _phantom: PhantomData, - }) - } - - pub fn perform( - &self, - args: impl Into>, - ) -> Result - where - K: Into, - V: serde::Serialize, - { - let args: HashMap = args.into(); - let args = args - .into_iter() - .map(|(key, val)| (key.into(), to_json_value(val))) - .collect(); - let variables = - resolve_prepared_variables(&self.placeholders, self.variables.clone(), args)?; - // TODO extract files from variables after resolution - let req = build_gql_req_sync( - &self.client, - self.addr.clone(), - &self.doc, - variables, - self.path_to_files.clone(), - &self.opts, - )?; - let res = match req.send() { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::() { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - self.nodes_len, - ) - .map_err(PrepareRequestError::RequestError)?, - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::BodyError { - error: Box::new(error), - }, - )) - } - } - } - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::NetworkError { - error: Box::new(error), - }, - )) - } - }; - Doc::parse_response(res).map_err(|err| { - PrepareRequestError::RequestError(GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - }) - }) - } - } - - impl PreparedRequestReqwest { - fn new( - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - addr: Url, - opts: GraphQlTransportOptions, - ty: &'static str, - ty_to_gql_ty_map: &TyToGqlTyMap, - ) -> Result { - let nodes = fun(&mut PreparedArgs); - let nodes = nodes.to_select_doc(); - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(ty_to_gql_ty_map) - .build(nodes, ty, None) - .map_err(PrepareRequestError::BuildError)?; - let placeholders = std::sync::Arc::new(placeholders); - Ok(Self { - doc, - variables, - path_to_files, - nodes_len, - addr, - client: reqwest::Client::new(), - opts, - placeholders, - _phantom: PhantomData, - }) - } - - pub async fn perform( - &self, - args: impl Into>, - ) -> Result - where - K: Into, - V: serde::Serialize, - { - let args: HashMap = args.into(); - let args = args - .into_iter() - .map(|(key, val)| (key.into(), to_json_value(val))) - .collect(); - let variables = - resolve_prepared_variables(&self.placeholders, self.variables.clone(), args)?; - // TODO extract files from variables - let req = build_gql_req( - &self.client, - self.addr.clone(), - &self.doc, - variables, - self.path_to_files.clone(), - &self.opts, - ) - .await?; - let res = match req.send().await { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::().await { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - self.nodes_len, - ) - .map_err(PrepareRequestError::RequestError)?, - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::BodyError { - error: Box::new(error), - }, - )) - } - } - } - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::NetworkError { - error: Box::new(error), - }, - )) - } - }; - Doc::parse_response(res).map_err(|err| { - PrepareRequestError::RequestError(GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - }) - }) - } - } - - // we need a manual clone impl since the derive will - // choke if Doc isn't clone - impl Clone for PreparedRequestReqwestSync { - fn clone(&self) -> Self { - Self { - addr: self.addr.clone(), - client: self.client.clone(), - nodes_len: self.nodes_len, - doc: self.doc.clone(), - variables: self.variables.clone(), - path_to_files: self.path_to_files.clone(), - opts: self.opts.clone(), - placeholders: self.placeholders.clone(), - _phantom: PhantomData, - } - } - } - impl Clone for PreparedRequestReqwest { - fn clone(&self) -> Self { - Self { - addr: self.addr.clone(), - client: self.client.clone(), - nodes_len: self.nodes_len, - doc: self.doc.clone(), - variables: self.variables.clone(), - path_to_files: self.path_to_files.clone(), - opts: self.opts.clone(), - placeholders: self.placeholders.clone(), - _phantom: PhantomData, - } - } - } - - #[derive(Debug)] - pub enum PrepareRequestError { - BuildError(GraphQLRequestError), - PlaceholderError(BoxErr), - RequestError(GraphQLRequestError), - FileUploadError(BoxErr), - } - - impl From for PrepareRequestError { - fn from(error: BuildReqError) -> Self { - match error { - BuildReqError::FileUpload { error } => PrepareRequestError::FileUploadError(error), - } - } - } - - impl std::error::Error for PrepareRequestError {} - impl std::fmt::Display for PrepareRequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - /* PrepareRequestError::FunctionError(err) => { - write!(f, "error calling doc builder closure: {err}") - } */ - PrepareRequestError::BuildError(err) => write!(f, "error building request: {err}"), - PrepareRequestError::PlaceholderError(err) => { - write!(f, "error resolving placeholder values: {err}") - } - PrepareRequestError::RequestError(err) => { - write!(f, "error making graphql request: {err}") - } - PrepareRequestError::FileUploadError(err) => { - write!(f, "error uploading file: {err}") - } - } - } - } -} +use core::marker::PhantomData; +use metagen_client::prelude::*; // // --- --- QueryGraph types --- --- // diff --git a/src/metagen/src/client_rs/static/lib.rs b/src/metagen/src/client_rs/static/lib.rs deleted file mode 100644 index b9babe5bc1..0000000000 --- a/src/metagen/src/client_rs/static/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod client; diff --git a/tests/metagen/typegraphs/sample/rs/Cargo.toml b/tests/metagen/typegraphs/sample/rs/Cargo.toml index 231246c12c..645aa66b55 100644 --- a/tests/metagen/typegraphs/sample/rs/Cargo.toml +++ b/tests/metagen/typegraphs/sample/rs/Cargo.toml @@ -1,20 +1,14 @@ -package.name = "sample_fdk" -package.edition = "2021" -package.version = "0.0.1" +[package] +name = "sample_client" +edition = "2021" +version = "0.5.0-rc.6" [dependencies] -serde = { version = "1.0.203", features = ["derive"] } -serde_json = "1.0.117" -reqwest = { version = "0.12", features = ["blocking", "json", "stream", "multipart"] } -mime_guess = "2.0" -futures = "0.3" -tokio-util = { version = "0.7", features = ["compat", "io"] } -derive_more = { version = "1.0", features = ["debug"] } -lazy_static = "1.5" +metagen-client.workspace = true +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" tokio = { version = "1", features = ["rt-multi-thread"] } -# The options after here are configured for crates intended to be -# wasm artifacts. Remove them if your usage is different [[bin]] -name = "sample_fdk" +name = "sample_client" path = "main.rs" diff --git a/tests/metagen/typegraphs/sample/rs/client.rs b/tests/metagen/typegraphs/sample/rs/client.rs index 5a75247657..f9829ec774 100644 --- a/tests/metagen/typegraphs/sample/rs/client.rs +++ b/tests/metagen/typegraphs/sample/rs/client.rs @@ -1,2643 +1,8 @@ // This file was @generated by metagen and is intended // to be generated again on subsequent metagen runs. -use std::{collections::HashMap, marker::PhantomData}; - -use reqwest::Url; -use serde::{Deserialize, Serialize}; - -pub type CowStr = std::borrow::Cow<'static, str>; -pub type BoxErr = Box; -pub type JsonObject = serde_json::Map; - -fn to_json_value(val: T) -> serde_json::Value { - serde_json::to_value(val).expect("error serializing value") -} - -/// Build the SelectNodeErased tree from the SelectionErasedMap tree -/// according to the NodeMeta tree. In this function -/// - arguments are associated with their types -/// - aliases get splatted into the node tree -/// - light query validation takes place -/// -/// I.e. the user's selection is joined with the description of the graph found -/// in the static NodeMetas to fill in any blank spaces -fn selection_to_node_set( - selection: SelectionErasedMap, - metas: &HashMap, - parent_path: String, -) -> Result, SelectionError> { - let mut out = vec![]; - let mut selection = selection.0; - let mut found_nodes = selection - .keys() - .cloned() - .collect::>(); - for (node_name, meta_fn) in metas.iter() { - found_nodes.remove(&node_name[..]); - - let Some(node_selection) = selection.remove(&node_name[..]) else { - // this node was not selected - continue; - }; - - // we can have multiple selection instances for a node - // if aliases are involved - let node_instances = match node_selection { - // this noe was not selected - SelectionErased::None => continue, - SelectionErased::Scalar => vec![(node_name.clone(), NodeArgsErased::None, None)], - SelectionErased::ScalarArgs(args) => { - vec![(node_name.clone(), args, None)] - } - SelectionErased::Composite(select) => { - vec![(node_name.clone(), NodeArgsErased::None, Some(select))] - } - SelectionErased::CompositeArgs(args, select) => { - vec![(node_name.clone(), args, Some(select))] - } - SelectionErased::Alias(aliases) => aliases - .into_iter() - .map(|(instance_name, selection)| { - let (args, select) = match selection { - AliasSelection::Scalar => (NodeArgsErased::None, None), - AliasSelection::ScalarArgs(args) => (args, None), - AliasSelection::Composite(select) => (NodeArgsErased::None, Some(select)), - AliasSelection::CompositeArgs(args, select) => (args, Some(select)), - }; - (instance_name, args, select) - }) - .collect(), - }; - - let meta = meta_fn(); - for (instance_name, args, select) in node_instances { - out.push(selection_to_select_node( - instance_name, - node_name.clone(), - args, - select, - &parent_path, - &meta, - )?) - } - } - Ok(out) -} - -fn selection_to_select_node( - instance_name: CowStr, - node_name: CowStr, - args: NodeArgsErased, - select: Option, - parent_path: &str, - meta: &NodeMeta, -) -> Result { - let args = if let Some(arg_types) = &meta.arg_types { - match args { - NodeArgsErased::Inline(args) => { - let instance_args = check_node_args(args, arg_types).map_err(|name| { - SelectionError::UnexpectedArgs { - name, - path: format!("{parent_path}.{instance_name}"), - } - })?; - Some(NodeArgsMerged::Inline(instance_args)) - } - NodeArgsErased::Placeholder(ph) => Some(NodeArgsMerged::Placeholder { - value: ph, - // FIXME: this clone can be improved - arg_types: arg_types.clone(), - }), - NodeArgsErased::None => { - return Err(SelectionError::MissingArgs { - path: format!("{parent_path}.{instance_name}"), - }) - } - } - } else { - None - }; - let sub_nodes = match (&meta.variants, &meta.sub_nodes) { - (Some(_), Some(_)) => unreachable!("union/either node metas can't have sub_nodes"), - (None, None) => SubNodes::None, - (variants, sub_nodes) => { - let Some(select) = select else { - return Err(SelectionError::MissingSubNodes { - path: format!("{parent_path}.{instance_name}"), - }); - }; - match select { - CompositeSelection::Atomic(select) => { - let Some(sub_nodes) = sub_nodes else { - return Err(SelectionError::UnexpectedUnion { - path: format!("{parent_path}.{instance_name}"), - }); - }; - SubNodes::Atomic(selection_to_node_set( - select, - sub_nodes, - format!("{parent_path}.{instance_name}"), - )?) - } - CompositeSelection::Union(mut variant_select) => { - let Some(variants) = variants else { - return Err(SelectionError::MissingUnion { - path: format!("{parent_path}.{instance_name}"), - }); - }; - let mut out = HashMap::new(); - for (variant_ty, variant_meta) in variants { - let variant_meta = variant_meta(); - // this union member is a scalar - let Some(sub_nodes) = variant_meta.sub_nodes else { - continue; - }; - let mut nodes = if let Some(select) = variant_select.remove(variant_ty) { - selection_to_node_set( - select, - &sub_nodes, - format!("{parent_path}.{instance_name}.variant({variant_ty})"), - )? - } else { - vec![] - }; - nodes.push(SelectNodeErased { - node_name: "__typename".into(), - instance_name: "__typename".into(), - args: None, - sub_nodes: SubNodes::None, - input_files: meta.input_files.clone(), - }); - out.insert(variant_ty.clone(), nodes); - } - if !variant_select.is_empty() { - return Err(SelectionError::UnexpectedVariants { - path: format!("{parent_path}.{instance_name}"), - varaint_tys: variant_select.into_keys().collect(), - }); - } - SubNodes::Union(out) - } - } - } - }; - Ok(SelectNodeErased { - node_name, - instance_name, - args, - sub_nodes, - input_files: meta.input_files.clone(), - }) -} - -#[derive(Debug)] -pub enum SelectionError { - MissingArgs { - path: String, - }, - MissingSubNodes { - path: String, - }, - MissingUnion { - path: String, - }, - UnexpectedArgs { - path: String, - name: String, - }, - UnexpectedUnion { - path: String, - }, - UnexpectedVariants { - path: String, - varaint_tys: Vec, - }, -} - -impl std::fmt::Display for SelectionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SelectionError::MissingArgs { path } => write!(f, "args are missing at node {path}"), - SelectionError::UnexpectedArgs { path, name } => { - write!(f, "unexpected arg '${name}' at node {path}") - } - SelectionError::MissingSubNodes { path } => { - write!(f, "node at {path} is a composite but no selection found") - } - SelectionError::MissingUnion { path } => write!( - f, - "node at {path} is a union but provided selection is atomic" - ), - SelectionError::UnexpectedUnion { path } => write!( - f, - "node at {path} is an atomic type but union selection provided" - ), - SelectionError::UnexpectedVariants { - path, - varaint_tys: varaint_ty, - } => { - write!( - f, - "node at {path} has none of the variants called '{varaint_ty:?}'" - ) - } - } - } -} -impl std::error::Error for SelectionError {} - -// -// --- --- Input files --- --- // -// - -#[derive(Debug, Clone)] -pub struct TypePath(&'static [&'static str]); - -fn path_segment_as_prop(segment: &str) -> Option<&str> { - if segment.starts_with('.') { - Some(&segment[1..]) - } else { - None - } -} - -#[derive(Debug, Clone)] -pub struct PathToInputFiles(&'static [&'static [&'static str]]); - -#[derive(Debug)] -pub enum ValuePathSegment { - Optional, - Index(usize), - Prop(&'static str), -} - -#[derive(Default, Debug)] -pub struct ValuePath(Vec); - -lazy_static::lazy_static! { - static ref LATEST_FILE_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); - static ref FILE_STORE: std::sync::Mutex> = Default::default(); -} - -enum FileData { - Path(std::path::PathBuf), - Bytes(Vec), - Reader(Box), - Async(reqwest::Body), -} - -pub struct File { - data: FileData, - file_name: Option, - mime_type: Option, -} - -#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Copy)] -pub struct FileId(usize); - -impl TryFrom for FileId { - type Error = BoxErr; - - fn try_from(file: File) -> Result { - let file_id = LATEST_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let mut guard = FILE_STORE.lock().map_err(|_| "file store lock poisoned")?; - guard.insert(FileId(file_id), file); - Ok(FileId(file_id)) - } -} - -impl TryFrom for File { - type Error = BoxErr; - - fn try_from(file_id: FileId) -> Result { - let mut guard = FILE_STORE.lock().map_err(|_| "file store lock poisoned")?; - let file = guard.remove(&file_id).ok_or_else(|| "file not found")?; - if file.file_name.is_none() { - Ok(file.file_name(file_id.0.to_string())) - } else { - Ok(file) - } - } -} - -impl File { - pub fn from_path>(path: P) -> Self { - Self { - data: FileData::Path(path.into()), - file_name: None, - mime_type: None, - } - } - - pub fn from_bytes>>(data: B) -> Self { - Self { - data: FileData::Bytes(data.into()), - file_name: None, - mime_type: None, - } - } - - pub fn from_reader(reader: R) -> Self { - Self { - data: FileData::Reader(Box::new(reader)), - file_name: None, - mime_type: None, - } - } - - pub fn from_async_reader(reader: R) -> Self { - use tokio_util::compat::FuturesAsyncReadCompatExt as _; - let reader = reader.compat(); - Self { - data: FileData::Async(reqwest::Body::wrap_stream( - tokio_util::io::ReaderStream::new(reader), - )), - file_name: None, - mime_type: None, - } - } -} - -impl File { - pub fn file_name(mut self, file_name: impl Into) -> Self { - self.file_name = Some(file_name.into()); - self - } - - pub fn mime_type(mut self, mime_type: impl Into) -> Self { - self.mime_type = Some(mime_type.into()); - self - } -} - -impl TryFrom for reqwest::blocking::multipart::Part { - type Error = BoxErr; - - fn try_from(file: File) -> Result { - let mut part = match file.data { - FileData::Path(path) => { - let file = std::fs::File::open(path.as_path())?; - let file_size = file.metadata()?.len(); - let mut part = - reqwest::blocking::multipart::Part::reader_with_length(file, file_size); - if let Some(name) = path.file_name() { - part = part.file_name(name.to_string_lossy().into_owned()); - } - part = part.mime_str( - mime_guess::from_path(&path) - .first_or_octet_stream() - .as_ref(), - )?; - part - } - - FileData::Bytes(data) => reqwest::blocking::multipart::Part::bytes(data), - - FileData::Reader(reader) => reqwest::blocking::multipart::Part::reader(reader), - - FileData::Async(_) => { - return Err("async readers are not supported".into()); - } - }; - - if let Some(file_name) = file.file_name { - part = part.file_name(file_name); - } - if let Some(mime_type) = file.mime_type { - part = part.mime_str(&mime_type)?; - } - Ok(part) - } -} - -impl File { - async fn into_reqwest_part(self) -> Result { - let mut part = match self.data { - FileData::Path(path) => reqwest::multipart::Part::file(path).await?, - FileData::Bytes(data) => reqwest::multipart::Part::bytes(data), - FileData::Async(body) => reqwest::multipart::Part::stream(body), - FileData::Reader(_) => { - return Err("sync readers are not supported".into()); - } - }; - - if let Some(file_name) = self.file_name { - part = part.file_name(file_name); - } - if let Some(mime_type) = self.mime_type { - part = part.mime_str(&mime_type)?; - } - Ok(part) - } -} - -#[derive(Debug)] -struct FileExtractor { - path: TypePath, - prefix: String, - current_path: ValuePath, - output: HashMap, -} - -impl FileExtractor { - fn extract_all_from( - variables: &mut JsonObject, - mut path_to_files: HashMap>, - ) -> Result, BoxErr> { - let mut output = HashMap::new(); - - for (key, value) in variables.iter_mut() { - let paths = path_to_files.remove(key).unwrap_or_default(); - for path in paths.into_iter() { - let mut extractor = Self { - path, - prefix: key.clone(), - current_path: ValuePath::default(), - output: std::mem::take(&mut output), - }; - extractor.extract_from_value(value)?; - output = extractor.output; - } - } - - Ok(output) - } - - fn extract_from_value(&mut self, value: &mut serde_json::Value) -> Result<(), BoxErr> { - let cursor = self.current_path.0.len(); - if cursor == self.path.0.len() { - // end of type_path; replace file_id with null - let file_id: FileId = serde_json::from_value(value.take())?; - self.output.insert(self.format_path(), file_id); - return Ok(()); - } - let segment = self.path.0[cursor]; - use ValuePathSegment as VPSeg; - match segment { - "?" => { - if !value.is_null() { - self.current_path.0.push(VPSeg::Optional); - self.extract_from_value(value)?; - self.current_path.0.pop(); - } - } - "[]" => { - let items = value - .as_array_mut() - .ok_or_else(|| format!("expected an array at {:?}", self.format_path()))?; - for (idx, item) in items.iter_mut().enumerate() { - self.current_path.0.push(VPSeg::Index(idx)); - self.extract_from_value(item)?; - self.current_path.0.pop(); - } - } - x if x.starts_with('.') => { - let key = &x[1..]; - let object = value - .as_object_mut() - .ok_or_else(|| format!("expected an object at {:?}", self.format_path()))?; - let mut null = serde_json::Value::Null; - let value = object.get_mut(key).unwrap_or(&mut null); - self.current_path.0.push(VPSeg::Prop(key)); - self.extract_from_value(value)?; - self.current_path.0.pop(); - } - _ => unreachable!(), - } - - Ok(()) - } - - /// format the path following the GraphQL multipart request spec - /// see: https://github.com/jaydenseric/graphql-multipart-request-spec - fn format_path(&self) -> String { - let mut res = self.prefix.clone(); - use ValuePathSegment as VPSeg; - for seg in &self.current_path.0 { - match seg { - VPSeg::Optional => {} - VPSeg::Index(idx) => res.push_str(&format!(".{}", idx)), - VPSeg::Prop(key) => res.push_str(&format!(".{}", key)), - } - } - res - } -} - -// -// --- --- Graph node types --- --- // -// - -type NodeMetaFn = fn() -> NodeMeta; - -/// How the [`node_metas`] module encodes the description -/// of the typegraph. -struct NodeMeta { - sub_nodes: Option>, - arg_types: Option>, - variants: Option>, - input_files: Option, -} - -enum SubNodes { - None, - Atomic(Vec), - Union(HashMap>), -} - -/// The final form of the nodes used in queries. -pub struct SelectNodeErased { - node_name: CowStr, - instance_name: CowStr, - args: Option, - sub_nodes: SubNodes, - input_files: Option, -} - -/// Wrappers around [`SelectNodeErased`] that only holds query nodes -pub struct QueryNode(SelectNodeErased, PhantomData<(Out,)>); -/// Wrappers around [`SelectNodeErased`] that only holds mutation nodes -pub struct MutationNode(SelectNodeErased, PhantomData<(Out,)>); - -/* /// Trait used to track the `Out` type parameter for [`QueryNode`]/[`MutationNode`] -pub trait ToSelectNode { - type Out; - - fn erased(self) -> SelectNodeErased; -} */ - -/// A variation of [`ToSelectNode`] to only be implemented -/// by aggregates of select nodes like [Vec]s. -pub trait ToSelectDoc { - type Out; - - fn to_select_doc(self) -> Vec; - fn parse_response(data: Vec) -> Result; -} - -/// Marker trait for [`ToSelectDoc`] implementors that only carry query nodes. -pub trait ToQueryDoc {} -/// Marker trait for [`ToSelectDoc`] implementors that only carry mutation nodes. -pub trait ToMutationDoc {} - -/// Struct used to mark query associated types that are generic about effect. -pub struct QueryMarker; -/// Struct used to mark mutationo associated types that are generic about effect. -pub struct MutationMarker; - -/// A node that's yet to have it's subnodes specified. -/// Use [`select`][Self::select] and [`select_aliased`][Self::select_aliased] -/// to finalize it. -/// [`select_aliased`][Self::select_aliased] will allow you to use [`alias`] -/// nodes but the returned object will be a raw [`serde_json::Value`]. -/// This type is generic over effect using the `QTy` parameter. -pub struct UnselectedNode { - root_name: CowStr, - root_meta: NodeMetaFn, - args: NodeArgsErased, - _marker: PhantomData<(SelT, SelAliasedT, QTy, Out)>, -} - -impl UnselectedNode -where - SelT: Into, -{ - fn select_erased(self, select: SelT) -> SelectNodeErased { - let nodes = selection_to_node_set( - SelectionErasedMap( - [( - self.root_name.clone(), - match self.args { - NodeArgsErased::None => SelectionErased::Composite(select.into()), - args => SelectionErased::CompositeArgs(args, select.into()), - }, - )] - .into(), - ), - &[(self.root_name, self.root_meta)].into(), - "$q".into(), - ) - .unwrap(); - nodes.into_iter().next().unwrap() - } -} - -impl UnselectedNode -where - SelAliased: Into, -{ - fn select_aliased_erased(self, select: SelAliased) -> SelectNodeErased { - let nodes = selection_to_node_set( - SelectionErasedMap( - [( - self.root_name.clone(), - match self.args { - NodeArgsErased::None => SelectionErased::Composite(select.into()), - args => SelectionErased::CompositeArgs(args, select.into()), - }, - )] - .into(), - ), - &[(self.root_name, self.root_meta)].into(), - "$q".into(), - ) - .unwrap(); - nodes.into_iter().next().unwrap() - } -} - -// NOTE: we'll need a select method implementation for each ATy x QTy pair - -impl UnselectedNode -where - SelT: Into, -{ - pub fn select(self, select: SelT) -> QueryNode { - QueryNode(self.select_erased(select), PhantomData) - } -} -impl UnselectedNode -where - SelAliased: Into, -{ - pub fn select_aliased(self, select: SelAliased) -> QueryNode { - QueryNode(self.select_aliased_erased(select), PhantomData) - } -} -impl UnselectedNode -where - SelT: Into, -{ - pub fn select(self, select: SelT) -> MutationNode { - MutationNode(self.select_erased(select), PhantomData) - } -} -impl UnselectedNode -where - SelAliased: Into, -{ - pub fn select_aliased(self, select: SelAliased) -> MutationNode { - MutationNode(self.select_aliased_erased(select), PhantomData) - } -} - -// --- --- Impl ToSelectDoc --- --- /// - -impl ToSelectDoc for QueryNode -where - Out: serde::de::DeserializeOwned, -{ - type Out = Out; - - fn to_select_doc(self) -> Vec { - vec![self.0] - } - - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - serde_json::from_value(data.next().unwrap()) - } -} -impl ToQueryDoc for QueryNode {} -impl ToSelectDoc for MutationNode -where - Out: serde::de::DeserializeOwned, -{ - type Out = Out; - - fn to_select_doc(self) -> Vec { - vec![self.0] - } - - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - serde_json::from_value(data.next().unwrap()) - } -} -impl ToMutationDoc for MutationNode {} - -#[macro_export] -macro_rules! impl_for_tuple { - ($($idx:tt $ty:tt),+) => { - impl<$($ty,)+> ToSelectDoc for ($(QueryNode<$ty>,)+) - where $($ty: serde::de::DeserializeOwned,)+ - { - type Out = ($($ty,)+); - - fn to_select_doc(self) -> Vec { - vec![ - $(self.$idx.0,)+ - ] - } - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - let mut next = move |_idx| data.next().unwrap(); - Ok(( - - $(serde_json::from_value(next($idx))?,)+ - )) - } - } - impl<$($ty,)+> ToSelectDoc for ($(MutationNode<$ty>,)+) - where $($ty: serde::de::DeserializeOwned,)+ - { - type Out = ($($ty,)+); - - fn to_select_doc(self) -> Vec { - vec![ - $(self.$idx.0,)+ - ] - } - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - let mut next = move |_idx| data.next().unwrap(); - Ok(( - - $(serde_json::from_value(next($idx))?,)+ - )) - } - } - - impl<$($ty,)+> ToQueryDoc for ($($ty,)+) - where - $($ty: ToQueryDoc,)+ - {} - - impl<$($ty,)+> ToMutationDoc for ($($ty,)+) - where - $($ty: ToMutationDoc,)+ - {} - }; -} - -impl_for_tuple!(0 N0); -impl_for_tuple!(0 N0, 1 N1); -impl_for_tuple!(0 N0, 1 N1, 2 N2); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9, 10 N10); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9, 10 N10, 11 N11); - -// -// --- -- --- Selection types --- --- // -// - -// This is a newtype for Into trait impl purposes -#[derive(Debug)] -pub struct SelectionErasedMap(HashMap); - -#[derive(Debug)] -pub enum CompositeSelection { - Atomic(SelectionErasedMap), - Union(HashMap), -} - -impl Default for CompositeSelection { - fn default() -> Self { - CompositeSelection::Atomic(SelectionErasedMap(Default::default())) - } -} - -#[derive(Debug)] -enum SelectionErased { - None, - Scalar, - ScalarArgs(NodeArgsErased), - Composite(CompositeSelection), - CompositeArgs(NodeArgsErased, CompositeSelection), - Alias(HashMap), -} - -#[derive(Debug)] -pub enum AliasSelection { - Scalar, - ScalarArgs(NodeArgsErased), - Composite(CompositeSelection), - CompositeArgs(NodeArgsErased, CompositeSelection), -} - -#[derive(Default, Clone, Copy, Debug)] -pub struct HasAlias; -#[derive(Default, Clone, Copy, Debug)] -pub struct NoAlias; - -#[derive(Debug)] -pub struct AliasInfo { - aliases: HashMap, - _phantom: PhantomData<(ArgT, SelT, ATyag)>, -} - -#[derive(Debug)] -pub enum ScalarSelect { - Get, - Skip, - Alias(AliasInfo<(), (), ATy>), -} -#[derive(Debug)] -pub enum ScalarSelectArgs { - Get(NodeArgsErased, PhantomData), - Skip, - Alias(AliasInfo), -} -#[derive(Debug)] -pub enum CompositeSelect { - Get(CompositeSelection, PhantomData), - Skip, - Alias(AliasInfo<(), SelT, ATy>), -} -#[derive(Debug)] -pub enum CompositeSelectArgs { - Get( - NodeArgsErased, - CompositeSelection, - PhantomData<(ArgT, SelT)>, - ), - Skip, - Alias(AliasInfo), -} - -pub struct Get; -pub struct Skip; -pub struct Args(ArgT); -pub struct Select(SelT); -pub struct ArgSelect(ArgT, SelT); -pub struct Alias(AliasInfo); - -/// Shorthand for `Default::default`. All selections generally default -/// to [`skip`]. -pub fn default() -> T { - T::default() -} -/// Include all sub nodes excpet those that require arguments -pub fn all() -> T { - T::all() -} -/// Select the node for inclusion. -pub fn get>() -> T { - T::from(Get) -} -/// Skip this node when queryig. -pub fn skip>() -> T { - T::from(Skip) -} -/// Provide argumentns for a scalar node. -pub fn args>>(args: ArgT) -> T { - T::from(Args(args)) -} -/// Provide selections for a composite node that takes no args. -pub fn select>>(selection: SelT) -> T { - T::from(Select(selection)) -} -/// Provide arguments and selections for a composite node. -pub fn arg_select>>(args: ArgT, selection: SelT) -> T { - T::from(ArgSelect(args, selection)) -} - -/// Query the same node multiple times using aliases. -/// -/// WARNING: make sure your alias names don't clash across sibling -/// nodes. -pub fn alias(info: impl Into>) -> T -where - S: Into, - ASelT: Into, - T: From> + FromAliasSelection, -{ - let info: HashMap<_, _> = info.into(); - T::from(Alias(AliasInfo { - aliases: info - .into_iter() - .map(|(name, sel)| (name.into(), sel.into())) - .collect(), - _phantom: PhantomData, - })) -} - -pub trait Selection { - /// Include all sub nodes excpet those that require arguments - fn all() -> Self; -} - -// --- Impl SelectionType impls --- // - -impl Selection for ScalarSelect { - fn all() -> Self { - Self::Get - } -} -impl Selection for ScalarSelectArgs { - fn all() -> Self { - Self::Skip - } -} -impl Selection for CompositeSelect -where - SelT: Selection + Into, -{ - fn all() -> Self { - let sel = SelT::all(); - Self::Get(sel.into(), PhantomData) - } -} -impl Selection for CompositeSelectArgs -where - SelT: Selection, -{ - fn all() -> Self { - Self::Skip - } -} -// --- Default impls --- // - -impl Default for ScalarSelect { - fn default() -> Self { - Self::Skip - } -} -impl Default for ScalarSelectArgs { - fn default() -> Self { - Self::Skip - } -} -impl Default for CompositeSelect { - fn default() -> Self { - Self::Skip - } -} -impl Default for CompositeSelectArgs { - fn default() -> Self { - Self::Skip - } -} - -// --- From Get/Skip...etc impls --- // - -impl From for ScalarSelect { - fn from(_: Get) -> Self { - Self::Get - } -} - -impl From for ScalarSelect { - fn from(_: Skip) -> Self { - Self::Skip - } -} -impl From for ScalarSelectArgs { - fn from(_: Skip) -> Self { - Self::Skip - } -} -impl From for CompositeSelect { - fn from(_: Skip) -> Self { - Self::Skip - } -} -impl From for CompositeSelectArgs { - fn from(_: Skip) -> Self { - Self::Skip - } -} - -impl From> for ScalarSelectArgs -where - ArgT: Serialize, -{ - fn from(Args(args): Args) -> Self { - Self::Get(NodeArgsErased::Inline(to_json_value(args)), PhantomData) - } -} - -impl From> for CompositeSelect -where - SelT: Into, -{ - fn from(Select(selection): Select) -> Self { - Self::Get(selection.into(), PhantomData) - } -} - -impl From> for CompositeSelectArgs -where - ArgT: Serialize, - SelT: Into, -{ - fn from(ArgSelect(args, selection): ArgSelect) -> Self { - Self::Get( - NodeArgsErased::Inline(to_json_value(args)), - selection.into(), - PhantomData, - ) - } -} - -impl From> for ScalarSelectArgs { - fn from(value: PlaceholderArg) -> Self { - Self::Get(NodeArgsErased::Placeholder(value.value), PhantomData) - } -} -impl From> - for CompositeSelectArgs -where - SelT: Into, -{ - fn from(value: PlaceholderArgSelect) -> Self { - Self::Get( - NodeArgsErased::Placeholder(value.value), - value.selection.into(), - PhantomData, - ) - } -} - -// --- ToAliasSelection impls --- // - -/// This is a marker trait that allows the core selection types -/// like CompositeSelectNoArgs to mark which types can be used -/// as their aliasing nodes. This prevents usage of invalid selections -/// on aliases like [`Skip`]. -pub trait FromAliasSelection {} - -impl FromAliasSelection for ScalarSelect {} -impl FromAliasSelection> for ScalarSelectArgs {} -impl FromAliasSelection> for CompositeSelect {} -impl FromAliasSelection> - for CompositeSelectArgs -{ -} - -// --- From Alias impls --- // - -impl From>> for ScalarSelect { - fn from(Alias(info): Alias<(), ScalarSelect>) -> Self { - Self::Alias(AliasInfo { - aliases: info.aliases, - _phantom: PhantomData, - }) - } -} -impl From> for ScalarSelectArgs { - fn from(Alias(info): Alias) -> Self { - Self::Alias(info) - } -} -impl From> for CompositeSelect { - fn from(Alias(info): Alias<(), SelT>) -> Self { - Self::Alias(info) - } -} -impl From> for CompositeSelectArgs { - fn from(Alias(info): Alias) -> Self { - Self::Alias(info) - } -} - -// --- Into SelectionErased impls --- // - -impl From> for SelectionErased { - fn from(value: AliasInfo) -> SelectionErased { - SelectionErased::Alias(value.aliases) - } -} - -impl From> for SelectionErased { - fn from(value: ScalarSelect) -> SelectionErased { - use ScalarSelect::*; - match value { - Get => SelectionErased::Scalar, - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -impl From> for SelectionErased { - fn from(value: ScalarSelectArgs) -> SelectionErased { - use ScalarSelectArgs::*; - match value { - Get(arg, _) => SelectionErased::ScalarArgs(arg), - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -impl From> for SelectionErased { - fn from(value: CompositeSelect) -> SelectionErased { - use CompositeSelect::*; - match value { - Get(selection, _) => SelectionErased::Composite(selection), - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -impl From> for SelectionErased -where - SelT: Into, -{ - fn from(value: CompositeSelectArgs) -> SelectionErased { - use CompositeSelectArgs::*; - match value { - Get(args, selection, _) => SelectionErased::CompositeArgs(args, selection), - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -// --- UnionMember impls --- // - -/// The following trait is used for types that implement -/// selections for the composite members of unions. -/// -/// The err return value indicates the case where -/// aliases are used selections on members which is an error -/// -/// This state is currently impossible to arrive at since -/// AliasInfo has no public construction methods with NoAlias -/// set. Union selection types make sure all their immediate -/// member selection use NoAlias to prevent this invalid stat.e -pub trait UnionMember { - fn composite(self) -> Option; -} - -/// Internal marker trait use to make sure we can't have union members -/// selection being another union selection. -trait NotUnionSelection {} - -// NOTE: UnionMembers are all NoAlias -impl UnionMember for ScalarSelect { - fn composite(self) -> Option { - None - } -} - -impl UnionMember for ScalarSelectArgs { - fn composite(self) -> Option { - None - } -} - -impl UnionMember for CompositeSelect -where - SelT: NotUnionSelection, -{ - fn composite(self) -> Option { - use CompositeSelect::*; - match self { - Get(CompositeSelection::Atomic(selection), _) => Some(selection), - Skip => None, - Get(CompositeSelection::Union(_), _) => { - unreachable!("union selection on union member selection. how??") - } - Alias(_) => unreachable!("alias discovored on union/either member. how??"), - } - } -} - -impl UnionMember for CompositeSelectArgs -where - SelT: NotUnionSelection, -{ - fn composite(self) -> Option { - use CompositeSelectArgs::*; - match self { - Get(_args, CompositeSelection::Atomic(selection), _) => Some(selection), - Skip => None, - Get(_args, CompositeSelection::Union(_), _) => { - unreachable!("union selection on union member selection. how??") - } - Alias(_) => unreachable!("alias discovored on union/either member. how??"), - } - } -} - -// --- Into AliasSelection impls --- // - -impl From for AliasSelection { - fn from(_val: Get) -> Self { - AliasSelection::Scalar - } -} -impl From> for AliasSelection -where - ArgT: Serialize, -{ - fn from(val: Args) -> Self { - AliasSelection::ScalarArgs(NodeArgsErased::Inline(to_json_value(val.0))) - } -} -impl From> for AliasSelection -where - SelT: Into, -{ - fn from(val: Select) -> Self { - let map = val.0.into(); - AliasSelection::Composite(map) - } -} - -impl From> for AliasSelection -where - ArgT: Serialize, - SelT: Into, -{ - fn from(val: ArgSelect) -> Self { - let map = val.1.into(); - AliasSelection::CompositeArgs(NodeArgsErased::Inline(to_json_value(val.0)), map) - } -} -impl From> for AliasSelection { - fn from(val: ScalarSelect) -> Self { - use ScalarSelect::*; - match val { - Get => AliasSelection::Scalar, - _ => unreachable!(), - } - } -} -impl From> for AliasSelection { - fn from(val: ScalarSelectArgs) -> Self { - use ScalarSelectArgs::*; - match val { - Get(args, _) => AliasSelection::ScalarArgs(args), - _ => unreachable!(), - } - } -} - -impl From> for AliasSelection -where - SelT: Into, -{ - fn from(val: CompositeSelect) -> Self { - use CompositeSelect::*; - match val { - Get(select, _) => AliasSelection::Composite(select), - _ => unreachable!(), - } - } -} -impl From> for AliasSelection -where - SelT: Into, -{ - fn from(val: CompositeSelectArgs) -> Self { - use CompositeSelectArgs::*; - match val { - Get(args, selection, _) => AliasSelection::CompositeArgs(args, selection), - _ => unreachable!(), - } - } -} - -// TODO: convert to proc_macro -#[macro_export] -macro_rules! impl_selection_traits { - ($ty:ident,$($field:tt),+) => { - impl From<$ty> for CompositeSelection { - fn from(value: $ty) -> CompositeSelection { - CompositeSelection::Atomic(SelectionErasedMap( - [ - $((stringify!($field).into(), value.$field.into()),)+ - ] - .into(), - )) - } - } - - impl Selection for $ty { - fn all() -> Self { - Self { - $($field: all(),)+ - } - } - } - - impl NotUnionSelection for $ty {} - }; -} -#[macro_export] -macro_rules! impl_union_selection_traits { - ($ty:ident,$(($variant_ty:tt, $field:tt)),+) => { - impl From<$ty> for CompositeSelection { - fn from(value: $ty) -> CompositeSelection { - CompositeSelection::Union( - [ - $({ - let selection = - UnionMember::composite(value.$field); - selection.map(|val| ($variant_ty.into(), val)) - },)+ - ] - .into_iter() - .filter_map(|val| val) - .collect(), - ) - } - } - }; -} - -// -// --- --- Argument types --- --- // -// - -pub enum NodeArgs { - Inline(ArgT), - Placeholder(PlaceholderValue), -} - -impl From for NodeArgs { - fn from(value: ArgT) -> Self { - Self::Inline(value) - } -} - -#[derive(Debug)] -pub enum NodeArgsErased { - None, - Inline(serde_json::Value), - Placeholder(PlaceholderValue), -} - -impl From> for NodeArgsErased -where - ArgT: Serialize, -{ - fn from(value: NodeArgs) -> Self { - match value { - NodeArgs::Inline(arg) => Self::Inline(to_json_value(arg)), - NodeArgs::Placeholder(ph) => Self::Placeholder(ph), - } - } -} - -enum NodeArgsMerged { - Inline(HashMap), - Placeholder { - value: PlaceholderValue, - arg_types: HashMap, - }, -} - -/// This checks the input arg json for a node -/// against the arg description from the [`NodeMeta`]. -fn check_node_args( - args: serde_json::Value, - arg_types: &HashMap, -) -> Result, String> { - let args = match args { - serde_json::Value::Object(val) => val, - _ => unreachable!(), - }; - let mut instance_args = HashMap::new(); - for (name, value) in args { - let Some(type_name) = arg_types.get(&name[..]) else { - return Err(name); - }; - instance_args.insert( - name.into(), - NodeArgValue { - type_name: type_name.clone(), - value, - }, - ); - } - Ok(instance_args) -} - -struct NodeArgValue { - type_name: CowStr, - value: serde_json::Value, -} - -pub struct PreparedArgs; - -impl PreparedArgs { - pub fn get(&mut self, key: impl Into, fun: F) -> NodeArgs - where - In: serde::de::DeserializeOwned, - F: Fn(In) -> ArgT + 'static + Send + Sync, - ArgT: Serialize, - { - NodeArgs::Placeholder(PlaceholderValue { - key: key.into(), - fun: Box::new(move |value| { - let value = serde_json::from_value(value)?; - let value = fun(value); - serde_json::to_value(value) - }), - }) - } - pub fn arg(&mut self, key: impl Into, fun: F) -> T - where - T: From>, - In: serde::de::DeserializeOwned, - F: Fn(In) -> ArgT + 'static + Send + Sync, - ArgT: Serialize, - { - T::from(PlaceholderArg { - value: PlaceholderValue { - key: key.into(), - fun: Box::new(move |value| { - let value = serde_json::from_value(value)?; - let value = fun(value); - serde_json::to_value(value) - }), - }, - _phantom: PhantomData, - }) - } - pub fn arg_select( - &mut self, - key: impl Into, - selection: SelT, - fun: F, - ) -> T - where - T: From>, - In: serde::de::DeserializeOwned, - F: Fn(In) -> ArgT + 'static + Send + Sync, - ArgT: Serialize, - { - T::from(PlaceholderArgSelect { - value: PlaceholderValue { - key: key.into(), - fun: Box::new(move |value| { - let value = serde_json::from_value(value)?; - let value = fun(value); - serde_json::to_value(value) - }), - }, - selection, - _phantom: PhantomData, - }) - } -} - -pub struct PlaceholderValue { - key: CowStr, - fun: Box< - dyn Fn(serde_json::Value) -> Result + Send + Sync, - >, -} - -impl std::fmt::Debug for PlaceholderValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PlaceholderValue") - .field("key", &self.key) - .finish_non_exhaustive() - } -} - -pub struct PlaceholderArg { - value: PlaceholderValue, - _phantom: PhantomData, -} -pub struct PlaceholderArgSelect { - value: PlaceholderValue, - selection: SelT, - _phantom: PhantomData, -} - -pub struct PlaceholderArgs(Arg); - -// -// --- --- GraphQL types --- --- // -// - -use graphql::*; -pub mod graphql { - use std::sync::Arc; - - use super::*; - - pub(super) type TyToGqlTyMap = Arc>; - - #[derive(Default, Clone)] - pub struct GraphQlTransportOptions { - headers: reqwest::header::HeaderMap, - timeout: Option, - } - - // PlaceholderValue, fieldName -> gql_var_name - type FoundPlaceholders = Vec<(PlaceholderValue, HashMap)>; - - struct GqlRequest { - doc: String, - variables: JsonObject, - placeholders: FoundPlaceholders, - path_to_files: HashMap>, - } - - struct GqlRequestBuilder<'a> { - ty_to_gql_ty_map: &'a TyToGqlTyMap, - variable_values: JsonObject, - variable_types: HashMap, - // map variable name to path to file types - path_to_files: HashMap>, - doc: String, - placeholders: Vec<(PlaceholderValue, HashMap)>, - } - - impl<'a> GqlRequestBuilder<'a> { - fn new(ty_to_gql_ty_map: &'a TyToGqlTyMap) -> Self { - Self { - ty_to_gql_ty_map, - variable_values: Default::default(), - variable_types: Default::default(), - path_to_files: Default::default(), - doc: Default::default(), - placeholders: Default::default(), - } - } - - fn register_path_to_files(&mut self, name: String, key: &str, files: &PathToInputFiles) { - let path_to_files = files - .0 - .iter() - .filter_map(|path| { - let first = path[0]; - if first.starts_with('.') && &first[1..] == key { - Some(TypePath(&path[1..])) - } else { - None - } - }) - .collect::>(); - self.path_to_files.insert(name, path_to_files); - } - - fn select_node_to_gql(&mut self, node: SelectNodeErased) -> std::fmt::Result { - use std::fmt::Write; - if node.instance_name != node.node_name { - write!(self.doc, "{}: {}", node.instance_name, node.node_name)?; - } else { - write!(self.doc, "{}", node.node_name)?; - } - - if let Some(args) = node.args { - match args { - NodeArgsMerged::Inline(args) => { - if !args.is_empty() { - write!(&mut self.doc, "(")?; - for (key, val) in args { - let name = format!("in{}", self.variable_types.len()); - - let mut map = serde_json::Map::new(); - map.insert(key.clone().into(), val.value.clone()); - let mut object = serde_json::Value::Object(map); - - if let Some(files) = node.input_files.as_ref() { - self.register_path_to_files(name.clone(), key.as_ref(), files); - } - - write!(&mut self.doc, "{key}: ${name}, ")?; - self.variable_values.insert( - name.clone(), - object - .as_object_mut() - .unwrap() - .remove(key.as_ref()) - .unwrap(), - ); - self.variable_types.insert(name.into(), val.type_name); - } - write!(&mut self.doc, ")")?; - } - } - NodeArgsMerged::Placeholder { value, arg_types } => { - if !arg_types.is_empty() { - write!(&mut self.doc, "(")?; - let mut map = HashMap::new(); - for (key, type_name) in arg_types { - let name = format!("in{}", self.variable_types.len()); - if let Some(files) = node.input_files.as_ref() { - self.register_path_to_files(name.clone(), key.as_ref(), files); - } - write!(&mut self.doc, "{key}: ${name}, ")?; - self.variable_types.insert(name.clone().into(), type_name); - map.insert(key, name.into()); - } - write!(&mut self.doc, ")")?; - self.placeholders.push((value, map)); - } - } - } - } - - match node.sub_nodes { - SubNodes::None => {} - SubNodes::Atomic(sub_nodes) => { - write!(&mut self.doc, "{{ ")?; - for node in sub_nodes { - self.select_node_to_gql(node)?; - write!(&mut self.doc, " ")?; - } - write!(&mut self.doc, " }}")?; - } - SubNodes::Union(variants) => { - write!(&mut self.doc, "{{ ")?; - for (ty, sub_nodes) in variants { - let gql_ty = self.ty_to_gql_ty_map.get(&ty[..]).expect( - "impossible: no GraphQL type equivalent found for variant type", - ); - let gql_ty = match gql_ty.strip_suffix('!') { - Some(val) => val, - None => &gql_ty[..], - }; - write!(&mut self.doc, " ... on {gql_ty} {{ ")?; - for node in sub_nodes { - self.select_node_to_gql(node)?; - write!(&mut self.doc, " ")?; - } - write!(&mut self.doc, " }}")?; - } - write!(&mut self.doc, " }}")?; - } - } - Ok(()) - } - - fn build( - mut self, - nodes: Vec, - ty: &'static str, - name: Option, - ) -> Result { - use std::fmt::Write; - - for (idx, node) in nodes.into_iter().enumerate() { - let node = SelectNodeErased { - instance_name: format!("node{idx}").into(), - ..node - }; - write!(&mut self.doc, " ").expect("error building to string"); - self.select_node_to_gql(node) - .expect("error building to string"); - writeln!(&mut self.doc).expect("error building to string"); - } - - let mut args_row = String::new(); - if !self.variable_types.is_empty() { - write!(&mut args_row, "(").expect("error building to string"); - for (key, ty) in &self.variable_types { - let gql_ty = self.ty_to_gql_ty_map.get(&ty[..]).ok_or_else(|| { - GraphQLRequestError::InvalidQuery { - error: Box::from(format!("unknown typegraph type found: {}", ty)), - } - })?; - write!(&mut args_row, "${key}: {gql_ty}, ").expect("error building to string"); - } - write!(&mut args_row, ")").expect("error building to string"); - } - - let name = name.unwrap_or_else(|| "".into()); - let doc = format!("{ty} {name}{args_row} {{\n{doc}}}", doc = self.doc); - Ok(GqlRequest { - doc, - variables: self.variable_values, - placeholders: self.placeholders, - path_to_files: self.path_to_files, - }) - } - } - - enum GraphQLRequestBody { - Json(serde_json::Value), - Multipart(reqwest::multipart::Form), - } - - struct GraphQLRequest { - addr: Url, - method: reqwest::Method, - headers: reqwest::header::HeaderMap, - body: GraphQLRequestBody, - } - - use reqwest::blocking::{Client as ClientSync, RequestBuilder as RequestBuilderSync}; - - enum BuildReqError { - FileUpload { error: BoxErr }, - } - - fn build_gql_req_sync( - client: &ClientSync, - addr: Url, - doc: &str, - mut variables: JsonObject, - path_to_files: HashMap>, - opts: &GraphQlTransportOptions, - ) -> Result { - use reqwest::blocking::multipart::Form; - - let files = FileExtractor::extract_all_from(&mut variables, path_to_files) - .map_err(|error| BuildReqError::FileUpload { error })?; - - let mut request = client.request(reqwest::Method::POST, addr); - if let Some(timeout) = opts.timeout { - request = request.timeout(timeout); - } - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::ACCEPT, - "application/json".try_into().unwrap(), - ); - headers.extend(opts.headers.clone()); - - let operations = serde_json::json!({ - "query": doc, - "variables": variables - }); - - // TODO rename files - - if files.len() > 0 { - // multipart - let mut form = Form::new(); - - form = form.text("operations", serde_json::to_string(&operations).unwrap()); - - let (map, files): (HashMap<_, _>, Vec<_>) = files - .into_iter() - .enumerate() - .map(|(idx, (path, file_id))| { - ( - (idx.to_string(), vec![format!("variables.{path}")]), - file_id, - ) - }) - .unzip(); - - form = form.text("map", serde_json::to_string(&map).unwrap()); - - for (idx, file_id) in files.into_iter().enumerate() { - let file: File = file_id - .try_into() - .map_err(|error| BuildReqError::FileUpload { error })?; - form = form.part( - idx.to_string(), - file.try_into() - .map_err(|error| BuildReqError::FileUpload { error })?, - ); - } - - Ok(request.headers(headers).multipart(form)) - } else { - headers.insert( - reqwest::header::CONTENT_TYPE, - "application/json".try_into().unwrap(), - ); - Ok(request.headers(headers).json(&operations)) - } - } - - use reqwest::{Client, RequestBuilder}; - async fn build_gql_req( - client: &Client, - addr: Url, - doc: &str, - mut variables: JsonObject, - path_to_files: HashMap>, - opts: &GraphQlTransportOptions, - ) -> Result { - use reqwest::multipart::Form; - - let files = FileExtractor::extract_all_from(&mut variables, path_to_files) - .map_err(|error| BuildReqError::FileUpload { error })?; - - let request = client.request(reqwest::Method::POST, addr); - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::ACCEPT, - "application/json".try_into().unwrap(), - ); - headers.extend(opts.headers.clone()); - - let operations = serde_json::json!({ - "query": doc, - "variables": variables - }); - - if files.len() > 0 { - // multipart - let mut form = Form::new(); - - form = form.text("operations", serde_json::to_string(&operations).unwrap()); - - let (map, files): (HashMap<_, _>, Vec<_>) = files - .into_iter() - .enumerate() - .map(|(idx, (path, file_id))| { - ( - (idx.to_string(), vec![format!("variables.{path}")]), - file_id, - ) - }) - .unzip(); - - form = form.text("map", serde_json::to_string(&map).unwrap()); - - for (idx, file_id) in files.into_iter().enumerate() { - let file: File = file_id - .try_into() - .map_err(|error| BuildReqError::FileUpload { error })?; - form = form.part( - idx.to_string(), - file.into_reqwest_part() - .await - .map_err(|error| BuildReqError::FileUpload { error })?, - ); - } - - Ok(request.headers(headers).multipart(form)) - } else { - headers.insert( - reqwest::header::CONTENT_TYPE, - "application/json".try_into().unwrap(), - ); - Ok(request.headers(headers).json(&operations)) - } - } - - #[derive(Debug)] - pub struct GraphQLResponse { - pub status: reqwest::StatusCode, - pub headers: reqwest::header::HeaderMap, - pub body: JsonObject, - } - - fn handle_response( - response: GraphQLResponse, - nodes_len: usize, - ) -> Result, GraphQLRequestError> { - if !response.status.is_success() { - return Err(GraphQLRequestError::RequestFailed { response }); - } - #[derive(Debug, Deserialize)] - struct Response { - data: Option, - errors: Option>, - } - let body: Response = match serde_json::from_value(serde_json::Value::Object(response.body)) - { - Ok(body) => body, - Err(error) => { - return Err(GraphQLRequestError::BodyError { - error: Box::new(error), - }) - } - }; - if let Some(errors) = body.errors { - return Err(GraphQLRequestError::RequestErrors { - errors, - data: body.data, - }); - } - let Some(mut body) = body.data else { - return Err(GraphQLRequestError::BodyError { - error: Box::from("body response doesn't contain data field"), - }); - }; - (0..nodes_len) - .map(|idx| { - body.remove(&format!("node{idx}")) - .ok_or_else(|| GraphQLRequestError::BodyError { - error: Box::from(format!( - "expecting response under node key 'node{idx}' but none found" - )), - }) - }) - .collect::, _>>() - } - - #[derive(Debug)] - pub enum GraphQLRequestError { - /// GraphQL errors recieived - RequestErrors { - errors: Vec, - data: Option, - }, - /// Http error codes recieived - RequestFailed { - response: GraphQLResponse, - }, - /// Unable to deserialize body - BodyError { - error: BoxErr, - }, - /// Unable to make http request - NetworkError { - error: BoxErr, - }, - InvalidQuery { - error: BoxErr, - }, - /// Unable to upload file - FileUpload { - error: BoxErr, - }, - } - - impl From for GraphQLRequestError { - fn from(error: BuildReqError) -> Self { - match error { - BuildReqError::FileUpload { error } => GraphQLRequestError::FileUpload { error }, - } - } - } - - impl std::fmt::Display for GraphQLRequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - GraphQLRequestError::RequestErrors { errors, .. } => { - write!(f, "graphql errors in response: ")?; - for err in errors { - write!(f, "{}, ", err.message)?; - } - } - GraphQLRequestError::RequestFailed { response } => { - write!(f, "request failed with status {}", response.status)?; - } - GraphQLRequestError::BodyError { error } => { - write!(f, "error reading request body: {error}")?; - } - GraphQLRequestError::NetworkError { error } => { - write!(f, "error making http request: {error}")?; - } - GraphQLRequestError::InvalidQuery { error } => { - write!(f, "error building request: {error}")? - } - GraphQLRequestError::FileUpload { error } => { - write!(f, "error uploading file: {error}")? - } - } - Ok(()) - } - } - impl std::error::Error for GraphQLRequestError {} - - #[derive(Debug, Deserialize)] - pub struct ErrorLocation { - pub line: u32, - pub column: u32, - } - #[derive(Debug, Deserialize)] - pub struct GraphqlError { - pub message: String, - pub locations: Option>, - pub path: Option>, - } - - #[derive(Debug)] - pub enum PathSegment { - Field(String), - Index(u64), - } - - impl<'de> serde::de::Deserialize<'de> for PathSegment { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - use serde_json::Value; - let val = Value::deserialize(deserializer)?; - match val { - Value::Number(n) => Ok(PathSegment::Index(n.as_u64().unwrap())), - Value::String(s) => Ok(PathSegment::Field(s)), - _ => panic!("invalid path segment type"), - } - } - } - - #[derive(Clone)] - pub struct GraphQlTransportReqwestSync { - addr: Url, - ty_to_gql_ty_map: TyToGqlTyMap, - client: reqwest::blocking::Client, - } - - #[derive(Clone)] - pub struct GraphQlTransportReqwest { - addr: Url, - ty_to_gql_ty_map: TyToGqlTyMap, - client: reqwest::Client, - } - - impl GraphQlTransportReqwestSync { - pub fn new(addr: Url, ty_to_gql_ty_map: TyToGqlTyMap) -> Self { - Self { - addr, - ty_to_gql_ty_map, - client: reqwest::blocking::Client::new(), - } - } - - fn fetch( - &self, - nodes: Vec, - opts: &GraphQlTransportOptions, - ty: &'static str, - ) -> Result, GraphQLRequestError> { - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(&self.ty_to_gql_ty_map).build(nodes, ty, None)?; - if !placeholders.is_empty() { - panic!("placeholders found in non-prepared query") - } - let req = build_gql_req_sync( - &self.client, - self.addr.clone(), - &doc, - variables, - path_to_files, - opts, - )?; - match req.send() { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::() { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - nodes_len, - ), - Err(error) => Err(GraphQLRequestError::BodyError { - error: Box::new(error), - }), - } - } - Err(error) => Err(GraphQLRequestError::NetworkError { - error: Box::new(error), - }), - } - } - - pub fn query( - &self, - nodes: Doc, - ) -> Result { - self.query_with_opts(nodes, &Default::default()) - } - - pub fn query_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "query")?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - - pub fn mutation( - &self, - nodes: Doc, - ) -> Result { - self.mutation_with_opts(nodes, &Default::default()) - } - - pub fn mutation_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "mutation")?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - pub fn prepare_query( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_query_with_opts(fun, Default::default()) - } - - pub fn prepare_query_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwestSync::new( - fun, - self.addr.clone(), - opts, - "query", - &self.ty_to_gql_ty_map, - ) - } - - pub fn prepare_mutation( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_mutation_with_opts(fun, Default::default()) - } - - pub fn prepare_mutation_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwestSync::new( - fun, - self.addr.clone(), - opts, - "mutation", - &self.ty_to_gql_ty_map, - ) - } - } - - impl GraphQlTransportReqwest { - pub fn new(addr: Url, ty_to_gql_ty_map: TyToGqlTyMap) -> Self { - Self { - addr, - ty_to_gql_ty_map, - client: reqwest::Client::new(), - } - } - - async fn fetch( - &self, - nodes: Vec, - opts: &GraphQlTransportOptions, - ty: &'static str, - ) -> Result, GraphQLRequestError> { - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(&self.ty_to_gql_ty_map).build(nodes, ty, None)?; - if !placeholders.is_empty() { - panic!("placeholders found in non-prepared query") - } - - let req = build_gql_req( - &self.client, - self.addr.clone(), - &doc, - variables, - path_to_files, - opts, - ) - .await?; - match req.send().await { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::().await { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - nodes_len, - ), - Err(error) => Err(GraphQLRequestError::BodyError { - error: Box::new(error), - }), - } - } - Err(error) => Err(GraphQLRequestError::NetworkError { - error: Box::new(error), - }), - } - } - - pub async fn query( - &self, - nodes: Doc, - ) -> Result { - self.query_with_opts(nodes, &Default::default()).await - } - - pub async fn query_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "query").await?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - - pub async fn mutation( - &self, - nodes: Doc, - ) -> Result { - self.mutation_with_opts(nodes, &Default::default()).await - } - - pub async fn mutation_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "mutation").await?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - pub fn prepare_query( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_query_with_opts(fun, Default::default()) - } - - pub fn prepare_query_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwest::new( - fun, - self.addr.clone(), - opts, - "query", - &self.ty_to_gql_ty_map, - ) - } - - pub fn prepare_mutation( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_mutation_with_opts(fun, Default::default()) - } - - pub fn prepare_mutation_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwest::new( - fun, - self.addr.clone(), - opts, - "mutation", - &self.ty_to_gql_ty_map, - ) - } - } - - fn resolve_prepared_variables( - placeholders: &FoundPlaceholders, - mut inline_variables: JsonObject, - mut args: HashMap, - ) -> Result { - for (ph, key_map) in placeholders { - let Some(value) = args.remove(&ph.key) else { - return Err(PrepareRequestError::PlaceholderError(Box::from(format!( - "no value found for placeholder expected under key '{}'", - ph.key - )))); - }; - let value = (ph.fun)(value).map_err(|err| { - PrepareRequestError::PlaceholderError(Box::from(format!( - "error applying placeholder closure for value under key '{}': {err}", - ph.key - ))) - })?; - let serde_json::Value::Object(mut value) = value else { - unreachable!("placeholder closures must return structs"); - }; - for (key, var_key) in key_map { - inline_variables.insert( - var_key.clone().into(), - value.remove(&key[..]).unwrap_or(serde_json::Value::Null), - ); - } - } - Ok(inline_variables) - } - - pub struct PreparedRequestReqwest { - addr: Url, - client: reqwest::Client, - nodes_len: usize, - pub doc: String, - variables: JsonObject, - path_to_files: HashMap>, - opts: GraphQlTransportOptions, - placeholders: Arc, - _phantom: PhantomData, - } - - pub struct PreparedRequestReqwestSync { - addr: Url, - client: reqwest::blocking::Client, - nodes_len: usize, - pub doc: String, - variables: JsonObject, - path_to_files: HashMap>, - opts: GraphQlTransportOptions, - placeholders: Arc, - _phantom: PhantomData, - } - - impl PreparedRequestReqwestSync { - fn new( - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - addr: Url, - opts: GraphQlTransportOptions, - ty: &'static str, - ty_to_gql_ty_map: &TyToGqlTyMap, - ) -> Result { - let nodes = fun(&mut PreparedArgs); - let nodes = nodes.to_select_doc(); - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(ty_to_gql_ty_map) - .build(nodes, ty, None) - .map_err(PrepareRequestError::BuildError)?; - Ok(Self { - doc, - variables, - path_to_files, - nodes_len, - addr, - client: reqwest::blocking::Client::new(), - opts, - placeholders: Arc::new(placeholders), - _phantom: PhantomData, - }) - } - - pub fn perform( - &self, - args: impl Into>, - ) -> Result - where - K: Into, - V: serde::Serialize, - { - let args: HashMap = args.into(); - let args = args - .into_iter() - .map(|(key, val)| (key.into(), to_json_value(val))) - .collect(); - let variables = - resolve_prepared_variables(&self.placeholders, self.variables.clone(), args)?; - // TODO extract files from variables after resolution - let req = build_gql_req_sync( - &self.client, - self.addr.clone(), - &self.doc, - variables, - self.path_to_files.clone(), - &self.opts, - )?; - let res = match req.send() { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::() { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - self.nodes_len, - ) - .map_err(PrepareRequestError::RequestError)?, - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::BodyError { - error: Box::new(error), - }, - )) - } - } - } - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::NetworkError { - error: Box::new(error), - }, - )) - } - }; - Doc::parse_response(res).map_err(|err| { - PrepareRequestError::RequestError(GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - }) - }) - } - } - - impl PreparedRequestReqwest { - fn new( - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - addr: Url, - opts: GraphQlTransportOptions, - ty: &'static str, - ty_to_gql_ty_map: &TyToGqlTyMap, - ) -> Result { - let nodes = fun(&mut PreparedArgs); - let nodes = nodes.to_select_doc(); - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(ty_to_gql_ty_map) - .build(nodes, ty, None) - .map_err(PrepareRequestError::BuildError)?; - let placeholders = std::sync::Arc::new(placeholders); - Ok(Self { - doc, - variables, - path_to_files, - nodes_len, - addr, - client: reqwest::Client::new(), - opts, - placeholders, - _phantom: PhantomData, - }) - } - - pub async fn perform( - &self, - args: impl Into>, - ) -> Result - where - K: Into, - V: serde::Serialize, - { - let args: HashMap = args.into(); - let args = args - .into_iter() - .map(|(key, val)| (key.into(), to_json_value(val))) - .collect(); - let variables = - resolve_prepared_variables(&self.placeholders, self.variables.clone(), args)?; - // TODO extract files from variables - let req = build_gql_req( - &self.client, - self.addr.clone(), - &self.doc, - variables, - self.path_to_files.clone(), - &self.opts, - ) - .await?; - let res = match req.send().await { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::().await { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - self.nodes_len, - ) - .map_err(PrepareRequestError::RequestError)?, - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::BodyError { - error: Box::new(error), - }, - )) - } - } - } - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::NetworkError { - error: Box::new(error), - }, - )) - } - }; - Doc::parse_response(res).map_err(|err| { - PrepareRequestError::RequestError(GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - }) - }) - } - } - - // we need a manual clone impl since the derive will - // choke if Doc isn't clone - impl Clone for PreparedRequestReqwestSync { - fn clone(&self) -> Self { - Self { - addr: self.addr.clone(), - client: self.client.clone(), - nodes_len: self.nodes_len, - doc: self.doc.clone(), - variables: self.variables.clone(), - path_to_files: self.path_to_files.clone(), - opts: self.opts.clone(), - placeholders: self.placeholders.clone(), - _phantom: PhantomData, - } - } - } - impl Clone for PreparedRequestReqwest { - fn clone(&self) -> Self { - Self { - addr: self.addr.clone(), - client: self.client.clone(), - nodes_len: self.nodes_len, - doc: self.doc.clone(), - variables: self.variables.clone(), - path_to_files: self.path_to_files.clone(), - opts: self.opts.clone(), - placeholders: self.placeholders.clone(), - _phantom: PhantomData, - } - } - } - - #[derive(Debug)] - pub enum PrepareRequestError { - BuildError(GraphQLRequestError), - PlaceholderError(BoxErr), - RequestError(GraphQLRequestError), - FileUploadError(BoxErr), - } - - impl From for PrepareRequestError { - fn from(error: BuildReqError) -> Self { - match error { - BuildReqError::FileUpload { error } => PrepareRequestError::FileUploadError(error), - } - } - } - - impl std::error::Error for PrepareRequestError {} - impl std::fmt::Display for PrepareRequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - /* PrepareRequestError::FunctionError(err) => { - write!(f, "error calling doc builder closure: {err}") - } */ - PrepareRequestError::BuildError(err) => write!(f, "error building request: {err}"), - PrepareRequestError::PlaceholderError(err) => { - write!(f, "error resolving placeholder values: {err}") - } - PrepareRequestError::RequestError(err) => { - write!(f, "error making graphql request: {err}") - } - PrepareRequestError::FileUploadError(err) => { - write!(f, "error uploading file: {err}") - } - } - } - } -} +use core::marker::PhantomData; +use metagen_client::prelude::*; // // --- --- QueryGraph types --- --- // @@ -2662,7 +27,6 @@ impl QueryGraph { // --- --- Typegraph types --- --- // // - #[allow(non_snake_case)] mod node_metas { use super::*; diff --git a/tests/metagen/typegraphs/sample/rs/main.rs b/tests/metagen/typegraphs/sample/rs/main.rs index 6d50f372f1..2de88e6ab9 100644 --- a/tests/metagen/typegraphs/sample/rs/main.rs +++ b/tests/metagen/typegraphs/sample/rs/main.rs @@ -6,6 +6,7 @@ #[rustfmt::skip] pub mod client; use client::*; +use metagen_client::prelude::*; fn main() -> Result<(), BoxErr> { let port = std::env::var("TG_PORT")?; diff --git a/tests/metagen/typegraphs/sample/rs_upload/Cargo.toml b/tests/metagen/typegraphs/sample/rs_upload/Cargo.toml index 7dd5666a07..e3ee256375 100644 --- a/tests/metagen/typegraphs/sample/rs_upload/Cargo.toml +++ b/tests/metagen/typegraphs/sample/rs_upload/Cargo.toml @@ -1,18 +1,15 @@ -package.name = "sample_fdk" -package.edition = "2021" -package.version = "0.0.1" +[package] +name = "sample_client_upload" +edition = "2021" +version = "0.5.0-rc.6" [dependencies] -serde = { version = "1.0.210", features = ["derive"] } -serde_json = "1.0.128" -reqwest = { version = "0.12", features = ["blocking","json", "stream", "multipart"] } -mime_guess = "2.0" -futures = "0.3" -tokio-util = { version = "0.7", features = ["compat", "io"] } -derive_more = { version = "1.0", features = ["debug"] } -lazy_static = "1.5" +metagen-client.workspace = true +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" tokio = { version = "1", features = ["rt-multi-thread"] } +tokio-util = "0.7" [[bin]] -name = "sample_client" +name = "sample_client_upload" path = "main.rs" diff --git a/tests/metagen/typegraphs/sample/rs_upload/client.rs b/tests/metagen/typegraphs/sample/rs_upload/client.rs index 767d56d0be..49d22560e4 100644 --- a/tests/metagen/typegraphs/sample/rs_upload/client.rs +++ b/tests/metagen/typegraphs/sample/rs_upload/client.rs @@ -1,2646 +1,8 @@ // This file was @generated by metagen and is intended // to be generated again on subsequent metagen runs. -// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. -// SPDX-License-Identifier: MPL-2.0 - -use std::{collections::HashMap, marker::PhantomData}; - -use reqwest::Url; -use serde::{Deserialize, Serialize}; - -pub type CowStr = std::borrow::Cow<'static, str>; -pub type BoxErr = Box; -pub type JsonObject = serde_json::Map; - -fn to_json_value(val: T) -> serde_json::Value { - serde_json::to_value(val).expect("error serializing value") -} - -/// Build the SelectNodeErased tree from the SelectionErasedMap tree -/// according to the NodeMeta tree. In this function -/// - arguments are associated with their types -/// - aliases get splatted into the node tree -/// - light query validation takes place -/// -/// I.e. the user's selection is joined with the description of the graph found -/// in the static NodeMetas to fill in any blank spaces -fn selection_to_node_set( - selection: SelectionErasedMap, - metas: &HashMap, - parent_path: String, -) -> Result, SelectionError> { - let mut out = vec![]; - let mut selection = selection.0; - let mut found_nodes = selection - .keys() - .cloned() - .collect::>(); - for (node_name, meta_fn) in metas.iter() { - found_nodes.remove(&node_name[..]); - - let Some(node_selection) = selection.remove(&node_name[..]) else { - // this node was not selected - continue; - }; - - // we can have multiple selection instances for a node - // if aliases are involved - let node_instances = match node_selection { - // this noe was not selected - SelectionErased::None => continue, - SelectionErased::Scalar => vec![(node_name.clone(), NodeArgsErased::None, None)], - SelectionErased::ScalarArgs(args) => { - vec![(node_name.clone(), args, None)] - } - SelectionErased::Composite(select) => { - vec![(node_name.clone(), NodeArgsErased::None, Some(select))] - } - SelectionErased::CompositeArgs(args, select) => { - vec![(node_name.clone(), args, Some(select))] - } - SelectionErased::Alias(aliases) => aliases - .into_iter() - .map(|(instance_name, selection)| { - let (args, select) = match selection { - AliasSelection::Scalar => (NodeArgsErased::None, None), - AliasSelection::ScalarArgs(args) => (args, None), - AliasSelection::Composite(select) => (NodeArgsErased::None, Some(select)), - AliasSelection::CompositeArgs(args, select) => (args, Some(select)), - }; - (instance_name, args, select) - }) - .collect(), - }; - - let meta = meta_fn(); - for (instance_name, args, select) in node_instances { - out.push(selection_to_select_node( - instance_name, - node_name.clone(), - args, - select, - &parent_path, - &meta, - )?) - } - } - Ok(out) -} - -fn selection_to_select_node( - instance_name: CowStr, - node_name: CowStr, - args: NodeArgsErased, - select: Option, - parent_path: &str, - meta: &NodeMeta, -) -> Result { - let args = if let Some(arg_types) = &meta.arg_types { - match args { - NodeArgsErased::Inline(args) => { - let instance_args = check_node_args(args, arg_types).map_err(|name| { - SelectionError::UnexpectedArgs { - name, - path: format!("{parent_path}.{instance_name}"), - } - })?; - Some(NodeArgsMerged::Inline(instance_args)) - } - NodeArgsErased::Placeholder(ph) => Some(NodeArgsMerged::Placeholder { - value: ph, - // FIXME: this clone can be improved - arg_types: arg_types.clone(), - }), - NodeArgsErased::None => { - return Err(SelectionError::MissingArgs { - path: format!("{parent_path}.{instance_name}"), - }) - } - } - } else { - None - }; - let sub_nodes = match (&meta.variants, &meta.sub_nodes) { - (Some(_), Some(_)) => unreachable!("union/either node metas can't have sub_nodes"), - (None, None) => SubNodes::None, - (variants, sub_nodes) => { - let Some(select) = select else { - return Err(SelectionError::MissingSubNodes { - path: format!("{parent_path}.{instance_name}"), - }); - }; - match select { - CompositeSelection::Atomic(select) => { - let Some(sub_nodes) = sub_nodes else { - return Err(SelectionError::UnexpectedUnion { - path: format!("{parent_path}.{instance_name}"), - }); - }; - SubNodes::Atomic(selection_to_node_set( - select, - sub_nodes, - format!("{parent_path}.{instance_name}"), - )?) - } - CompositeSelection::Union(mut variant_select) => { - let Some(variants) = variants else { - return Err(SelectionError::MissingUnion { - path: format!("{parent_path}.{instance_name}"), - }); - }; - let mut out = HashMap::new(); - for (variant_ty, variant_meta) in variants { - let variant_meta = variant_meta(); - // this union member is a scalar - let Some(sub_nodes) = variant_meta.sub_nodes else { - continue; - }; - let mut nodes = if let Some(select) = variant_select.remove(variant_ty) { - selection_to_node_set( - select, - &sub_nodes, - format!("{parent_path}.{instance_name}.variant({variant_ty})"), - )? - } else { - vec![] - }; - nodes.push(SelectNodeErased { - node_name: "__typename".into(), - instance_name: "__typename".into(), - args: None, - sub_nodes: SubNodes::None, - input_files: meta.input_files.clone(), - }); - out.insert(variant_ty.clone(), nodes); - } - if !variant_select.is_empty() { - return Err(SelectionError::UnexpectedVariants { - path: format!("{parent_path}.{instance_name}"), - varaint_tys: variant_select.into_keys().collect(), - }); - } - SubNodes::Union(out) - } - } - } - }; - Ok(SelectNodeErased { - node_name, - instance_name, - args, - sub_nodes, - input_files: meta.input_files.clone(), - }) -} - -#[derive(Debug)] -pub enum SelectionError { - MissingArgs { - path: String, - }, - MissingSubNodes { - path: String, - }, - MissingUnion { - path: String, - }, - UnexpectedArgs { - path: String, - name: String, - }, - UnexpectedUnion { - path: String, - }, - UnexpectedVariants { - path: String, - varaint_tys: Vec, - }, -} - -impl std::fmt::Display for SelectionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SelectionError::MissingArgs { path } => write!(f, "args are missing at node {path}"), - SelectionError::UnexpectedArgs { path, name } => { - write!(f, "unexpected arg '${name}' at node {path}") - } - SelectionError::MissingSubNodes { path } => { - write!(f, "node at {path} is a composite but no selection found") - } - SelectionError::MissingUnion { path } => write!( - f, - "node at {path} is a union but provided selection is atomic" - ), - SelectionError::UnexpectedUnion { path } => write!( - f, - "node at {path} is an atomic type but union selection provided" - ), - SelectionError::UnexpectedVariants { - path, - varaint_tys: varaint_ty, - } => { - write!( - f, - "node at {path} has none of the variants called '{varaint_ty:?}'" - ) - } - } - } -} -impl std::error::Error for SelectionError {} - -// -// --- --- Input files --- --- // -// - -#[derive(Debug, Clone)] -pub struct TypePath(&'static [&'static str]); - -fn path_segment_as_prop(segment: &str) -> Option<&str> { - if segment.starts_with('.') { - Some(&segment[1..]) - } else { - None - } -} - -#[derive(Debug, Clone)] -pub struct PathToInputFiles(&'static [&'static [&'static str]]); - -#[derive(Debug)] -pub enum ValuePathSegment { - Optional, - Index(usize), - Prop(&'static str), -} - -#[derive(Default, Debug)] -pub struct ValuePath(Vec); - -lazy_static::lazy_static! { - static ref LATEST_FILE_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); - static ref FILE_STORE: std::sync::Mutex> = Default::default(); -} - -enum FileData { - Path(std::path::PathBuf), - Bytes(Vec), - Reader(Box), - Async(reqwest::Body), -} - -pub struct File { - data: FileData, - file_name: Option, - mime_type: Option, -} - -#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Copy)] -pub struct FileId(usize); - -impl TryFrom for FileId { - type Error = BoxErr; - - fn try_from(file: File) -> Result { - let file_id = LATEST_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let mut guard = FILE_STORE.lock().map_err(|_| "file store lock poisoned")?; - guard.insert(FileId(file_id), file); - Ok(FileId(file_id)) - } -} - -impl TryFrom for File { - type Error = BoxErr; - - fn try_from(file_id: FileId) -> Result { - let mut guard = FILE_STORE.lock().map_err(|_| "file store lock poisoned")?; - let file = guard.remove(&file_id).ok_or_else(|| "file not found")?; - if file.file_name.is_none() { - Ok(file.file_name(file_id.0.to_string())) - } else { - Ok(file) - } - } -} - -impl File { - pub fn from_path>(path: P) -> Self { - Self { - data: FileData::Path(path.into()), - file_name: None, - mime_type: None, - } - } - - pub fn from_bytes>>(data: B) -> Self { - Self { - data: FileData::Bytes(data.into()), - file_name: None, - mime_type: None, - } - } - - pub fn from_reader(reader: R) -> Self { - Self { - data: FileData::Reader(Box::new(reader)), - file_name: None, - mime_type: None, - } - } - - pub fn from_async_reader(reader: R) -> Self { - use tokio_util::compat::FuturesAsyncReadCompatExt as _; - let reader = reader.compat(); - Self { - data: FileData::Async(reqwest::Body::wrap_stream( - tokio_util::io::ReaderStream::new(reader), - )), - file_name: None, - mime_type: None, - } - } -} - -impl File { - pub fn file_name(mut self, file_name: impl Into) -> Self { - self.file_name = Some(file_name.into()); - self - } - - pub fn mime_type(mut self, mime_type: impl Into) -> Self { - self.mime_type = Some(mime_type.into()); - self - } -} - -impl TryFrom for reqwest::blocking::multipart::Part { - type Error = BoxErr; - - fn try_from(file: File) -> Result { - let mut part = match file.data { - FileData::Path(path) => { - let file = std::fs::File::open(path.as_path())?; - let file_size = file.metadata()?.len(); - let mut part = - reqwest::blocking::multipart::Part::reader_with_length(file, file_size); - if let Some(name) = path.file_name() { - part = part.file_name(name.to_string_lossy().into_owned()); - } - part = part.mime_str( - mime_guess::from_path(&path) - .first_or_octet_stream() - .as_ref(), - )?; - part - } - - FileData::Bytes(data) => reqwest::blocking::multipart::Part::bytes(data), - - FileData::Reader(reader) => reqwest::blocking::multipart::Part::reader(reader), - - FileData::Async(_) => { - return Err("async readers are not supported".into()); - } - }; - - if let Some(file_name) = file.file_name { - part = part.file_name(file_name); - } - if let Some(mime_type) = file.mime_type { - part = part.mime_str(&mime_type)?; - } - Ok(part) - } -} - -impl File { - async fn into_reqwest_part(self) -> Result { - let mut part = match self.data { - FileData::Path(path) => reqwest::multipart::Part::file(path).await?, - FileData::Bytes(data) => reqwest::multipart::Part::bytes(data), - FileData::Async(body) => reqwest::multipart::Part::stream(body), - FileData::Reader(_) => { - return Err("sync readers are not supported".into()); - } - }; - - if let Some(file_name) = self.file_name { - part = part.file_name(file_name); - } - if let Some(mime_type) = self.mime_type { - part = part.mime_str(&mime_type)?; - } - Ok(part) - } -} - -#[derive(Debug)] -struct FileExtractor { - path: TypePath, - prefix: String, - current_path: ValuePath, - output: HashMap, -} - -impl FileExtractor { - fn extract_all_from( - variables: &mut JsonObject, - mut path_to_files: HashMap>, - ) -> Result, BoxErr> { - let mut output = HashMap::new(); - - for (key, value) in variables.iter_mut() { - let paths = path_to_files.remove(key).unwrap_or_default(); - for path in paths.into_iter() { - let mut extractor = Self { - path, - prefix: key.clone(), - current_path: ValuePath::default(), - output: std::mem::take(&mut output), - }; - extractor.extract_from_value(value)?; - output = extractor.output; - } - } - - Ok(output) - } - - fn extract_from_value(&mut self, value: &mut serde_json::Value) -> Result<(), BoxErr> { - let cursor = self.current_path.0.len(); - if cursor == self.path.0.len() { - // end of type_path; replace file_id with null - let file_id: FileId = serde_json::from_value(value.take())?; - self.output.insert(self.format_path(), file_id); - return Ok(()); - } - let segment = self.path.0[cursor]; - use ValuePathSegment as VPSeg; - match segment { - "?" => { - if !value.is_null() { - self.current_path.0.push(VPSeg::Optional); - self.extract_from_value(value)?; - self.current_path.0.pop(); - } - } - "[]" => { - let items = value - .as_array_mut() - .ok_or_else(|| format!("expected an array at {:?}", self.format_path()))?; - for (idx, item) in items.iter_mut().enumerate() { - self.current_path.0.push(VPSeg::Index(idx)); - self.extract_from_value(item)?; - self.current_path.0.pop(); - } - } - x if x.starts_with('.') => { - let key = &x[1..]; - let object = value - .as_object_mut() - .ok_or_else(|| format!("expected an object at {:?}", self.format_path()))?; - let mut null = serde_json::Value::Null; - let value = object.get_mut(key).unwrap_or(&mut null); - self.current_path.0.push(VPSeg::Prop(key)); - self.extract_from_value(value)?; - self.current_path.0.pop(); - } - _ => unreachable!(), - } - - Ok(()) - } - - /// format the path following the GraphQL multipart request spec - /// see: https://github.com/jaydenseric/graphql-multipart-request-spec - fn format_path(&self) -> String { - let mut res = self.prefix.clone(); - use ValuePathSegment as VPSeg; - for seg in &self.current_path.0 { - match seg { - VPSeg::Optional => {} - VPSeg::Index(idx) => res.push_str(&format!(".{}", idx)), - VPSeg::Prop(key) => res.push_str(&format!(".{}", key)), - } - } - res - } -} - -// -// --- --- Graph node types --- --- // -// - -type NodeMetaFn = fn() -> NodeMeta; - -/// How the [`node_metas`] module encodes the description -/// of the typegraph. -struct NodeMeta { - sub_nodes: Option>, - arg_types: Option>, - variants: Option>, - input_files: Option, -} - -enum SubNodes { - None, - Atomic(Vec), - Union(HashMap>), -} - -/// The final form of the nodes used in queries. -pub struct SelectNodeErased { - node_name: CowStr, - instance_name: CowStr, - args: Option, - sub_nodes: SubNodes, - input_files: Option, -} - -/// Wrappers around [`SelectNodeErased`] that only holds query nodes -pub struct QueryNode(SelectNodeErased, PhantomData<(Out,)>); -/// Wrappers around [`SelectNodeErased`] that only holds mutation nodes -pub struct MutationNode(SelectNodeErased, PhantomData<(Out,)>); - -/* /// Trait used to track the `Out` type parameter for [`QueryNode`]/[`MutationNode`] -pub trait ToSelectNode { - type Out; - - fn erased(self) -> SelectNodeErased; -} */ - -/// A variation of [`ToSelectNode`] to only be implemented -/// by aggregates of select nodes like [Vec]s. -pub trait ToSelectDoc { - type Out; - - fn to_select_doc(self) -> Vec; - fn parse_response(data: Vec) -> Result; -} - -/// Marker trait for [`ToSelectDoc`] implementors that only carry query nodes. -pub trait ToQueryDoc {} -/// Marker trait for [`ToSelectDoc`] implementors that only carry mutation nodes. -pub trait ToMutationDoc {} - -/// Struct used to mark query associated types that are generic about effect. -pub struct QueryMarker; -/// Struct used to mark mutationo associated types that are generic about effect. -pub struct MutationMarker; - -/// A node that's yet to have it's subnodes specified. -/// Use [`select`][Self::select] and [`select_aliased`][Self::select_aliased] -/// to finalize it. -/// [`select_aliased`][Self::select_aliased] will allow you to use [`alias`] -/// nodes but the returned object will be a raw [`serde_json::Value`]. -/// This type is generic over effect using the `QTy` parameter. -pub struct UnselectedNode { - root_name: CowStr, - root_meta: NodeMetaFn, - args: NodeArgsErased, - _marker: PhantomData<(SelT, SelAliasedT, QTy, Out)>, -} - -impl UnselectedNode -where - SelT: Into, -{ - fn select_erased(self, select: SelT) -> SelectNodeErased { - let nodes = selection_to_node_set( - SelectionErasedMap( - [( - self.root_name.clone(), - match self.args { - NodeArgsErased::None => SelectionErased::Composite(select.into()), - args => SelectionErased::CompositeArgs(args, select.into()), - }, - )] - .into(), - ), - &[(self.root_name, self.root_meta)].into(), - "$q".into(), - ) - .unwrap(); - nodes.into_iter().next().unwrap() - } -} - -impl UnselectedNode -where - SelAliased: Into, -{ - fn select_aliased_erased(self, select: SelAliased) -> SelectNodeErased { - let nodes = selection_to_node_set( - SelectionErasedMap( - [( - self.root_name.clone(), - match self.args { - NodeArgsErased::None => SelectionErased::Composite(select.into()), - args => SelectionErased::CompositeArgs(args, select.into()), - }, - )] - .into(), - ), - &[(self.root_name, self.root_meta)].into(), - "$q".into(), - ) - .unwrap(); - nodes.into_iter().next().unwrap() - } -} - -// NOTE: we'll need a select method implementation for each ATy x QTy pair - -impl UnselectedNode -where - SelT: Into, -{ - pub fn select(self, select: SelT) -> QueryNode { - QueryNode(self.select_erased(select), PhantomData) - } -} -impl UnselectedNode -where - SelAliased: Into, -{ - pub fn select_aliased(self, select: SelAliased) -> QueryNode { - QueryNode(self.select_aliased_erased(select), PhantomData) - } -} -impl UnselectedNode -where - SelT: Into, -{ - pub fn select(self, select: SelT) -> MutationNode { - MutationNode(self.select_erased(select), PhantomData) - } -} -impl UnselectedNode -where - SelAliased: Into, -{ - pub fn select_aliased(self, select: SelAliased) -> MutationNode { - MutationNode(self.select_aliased_erased(select), PhantomData) - } -} - -// --- --- Impl ToSelectDoc --- --- /// - -impl ToSelectDoc for QueryNode -where - Out: serde::de::DeserializeOwned, -{ - type Out = Out; - - fn to_select_doc(self) -> Vec { - vec![self.0] - } - - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - serde_json::from_value(data.next().unwrap()) - } -} -impl ToQueryDoc for QueryNode {} -impl ToSelectDoc for MutationNode -where - Out: serde::de::DeserializeOwned, -{ - type Out = Out; - - fn to_select_doc(self) -> Vec { - vec![self.0] - } - - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - serde_json::from_value(data.next().unwrap()) - } -} -impl ToMutationDoc for MutationNode {} - -#[macro_export] -macro_rules! impl_for_tuple { - ($($idx:tt $ty:tt),+) => { - impl<$($ty,)+> ToSelectDoc for ($(QueryNode<$ty>,)+) - where $($ty: serde::de::DeserializeOwned,)+ - { - type Out = ($($ty,)+); - - fn to_select_doc(self) -> Vec { - vec![ - $(self.$idx.0,)+ - ] - } - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - let mut next = move |_idx| data.next().unwrap(); - Ok(( - - $(serde_json::from_value(next($idx))?,)+ - )) - } - } - impl<$($ty,)+> ToSelectDoc for ($(MutationNode<$ty>,)+) - where $($ty: serde::de::DeserializeOwned,)+ - { - type Out = ($($ty,)+); - - fn to_select_doc(self) -> Vec { - vec![ - $(self.$idx.0,)+ - ] - } - fn parse_response(data: Vec) -> Result { - let mut data = data.into_iter(); - let mut next = move |_idx| data.next().unwrap(); - Ok(( - - $(serde_json::from_value(next($idx))?,)+ - )) - } - } - - impl<$($ty,)+> ToQueryDoc for ($($ty,)+) - where - $($ty: ToQueryDoc,)+ - {} - - impl<$($ty,)+> ToMutationDoc for ($($ty,)+) - where - $($ty: ToMutationDoc,)+ - {} - }; -} - -impl_for_tuple!(0 N0); -impl_for_tuple!(0 N0, 1 N1); -impl_for_tuple!(0 N0, 1 N1, 2 N2); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9, 10 N10); -impl_for_tuple!(0 N0, 1 N1, 2 N2, 3 N3, 4 N4, 5 N5, 6 N6, 7 N7, 8 N8, 9 N9, 10 N10, 11 N11); - -// -// --- -- --- Selection types --- --- // -// - -// This is a newtype for Into trait impl purposes -#[derive(Debug)] -pub struct SelectionErasedMap(HashMap); - -#[derive(Debug)] -pub enum CompositeSelection { - Atomic(SelectionErasedMap), - Union(HashMap), -} - -impl Default for CompositeSelection { - fn default() -> Self { - CompositeSelection::Atomic(SelectionErasedMap(Default::default())) - } -} - -#[derive(Debug)] -enum SelectionErased { - None, - Scalar, - ScalarArgs(NodeArgsErased), - Composite(CompositeSelection), - CompositeArgs(NodeArgsErased, CompositeSelection), - Alias(HashMap), -} - -#[derive(Debug)] -pub enum AliasSelection { - Scalar, - ScalarArgs(NodeArgsErased), - Composite(CompositeSelection), - CompositeArgs(NodeArgsErased, CompositeSelection), -} - -#[derive(Default, Clone, Copy, Debug)] -pub struct HasAlias; -#[derive(Default, Clone, Copy, Debug)] -pub struct NoAlias; - -#[derive(Debug)] -pub struct AliasInfo { - aliases: HashMap, - _phantom: PhantomData<(ArgT, SelT, ATyag)>, -} - -#[derive(Debug)] -pub enum ScalarSelect { - Get, - Skip, - Alias(AliasInfo<(), (), ATy>), -} -#[derive(Debug)] -pub enum ScalarSelectArgs { - Get(NodeArgsErased, PhantomData), - Skip, - Alias(AliasInfo), -} -#[derive(Debug)] -pub enum CompositeSelect { - Get(CompositeSelection, PhantomData), - Skip, - Alias(AliasInfo<(), SelT, ATy>), -} -#[derive(Debug)] -pub enum CompositeSelectArgs { - Get( - NodeArgsErased, - CompositeSelection, - PhantomData<(ArgT, SelT)>, - ), - Skip, - Alias(AliasInfo), -} - -pub struct Get; -pub struct Skip; -pub struct Args(ArgT); -pub struct Select(SelT); -pub struct ArgSelect(ArgT, SelT); -pub struct Alias(AliasInfo); - -/// Shorthand for `Default::default`. All selections generally default -/// to [`skip`]. -pub fn default() -> T { - T::default() -} -/// Include all sub nodes excpet those that require arguments -pub fn all() -> T { - T::all() -} -/// Select the node for inclusion. -pub fn get>() -> T { - T::from(Get) -} -/// Skip this node when queryig. -pub fn skip>() -> T { - T::from(Skip) -} -/// Provide argumentns for a scalar node. -pub fn args>>(args: ArgT) -> T { - T::from(Args(args)) -} -/// Provide selections for a composite node that takes no args. -pub fn select>>(selection: SelT) -> T { - T::from(Select(selection)) -} -/// Provide arguments and selections for a composite node. -pub fn arg_select>>(args: ArgT, selection: SelT) -> T { - T::from(ArgSelect(args, selection)) -} - -/// Query the same node multiple times using aliases. -/// -/// WARNING: make sure your alias names don't clash across sibling -/// nodes. -pub fn alias(info: impl Into>) -> T -where - S: Into, - ASelT: Into, - T: From> + FromAliasSelection, -{ - let info: HashMap<_, _> = info.into(); - T::from(Alias(AliasInfo { - aliases: info - .into_iter() - .map(|(name, sel)| (name.into(), sel.into())) - .collect(), - _phantom: PhantomData, - })) -} - -pub trait Selection { - /// Include all sub nodes excpet those that require arguments - fn all() -> Self; -} - -// --- Impl SelectionType impls --- // - -impl Selection for ScalarSelect { - fn all() -> Self { - Self::Get - } -} -impl Selection for ScalarSelectArgs { - fn all() -> Self { - Self::Skip - } -} -impl Selection for CompositeSelect -where - SelT: Selection + Into, -{ - fn all() -> Self { - let sel = SelT::all(); - Self::Get(sel.into(), PhantomData) - } -} -impl Selection for CompositeSelectArgs -where - SelT: Selection, -{ - fn all() -> Self { - Self::Skip - } -} -// --- Default impls --- // - -impl Default for ScalarSelect { - fn default() -> Self { - Self::Skip - } -} -impl Default for ScalarSelectArgs { - fn default() -> Self { - Self::Skip - } -} -impl Default for CompositeSelect { - fn default() -> Self { - Self::Skip - } -} -impl Default for CompositeSelectArgs { - fn default() -> Self { - Self::Skip - } -} - -// --- From Get/Skip...etc impls --- // - -impl From for ScalarSelect { - fn from(_: Get) -> Self { - Self::Get - } -} - -impl From for ScalarSelect { - fn from(_: Skip) -> Self { - Self::Skip - } -} -impl From for ScalarSelectArgs { - fn from(_: Skip) -> Self { - Self::Skip - } -} -impl From for CompositeSelect { - fn from(_: Skip) -> Self { - Self::Skip - } -} -impl From for CompositeSelectArgs { - fn from(_: Skip) -> Self { - Self::Skip - } -} - -impl From> for ScalarSelectArgs -where - ArgT: Serialize, -{ - fn from(Args(args): Args) -> Self { - Self::Get(NodeArgsErased::Inline(to_json_value(args)), PhantomData) - } -} - -impl From> for CompositeSelect -where - SelT: Into, -{ - fn from(Select(selection): Select) -> Self { - Self::Get(selection.into(), PhantomData) - } -} - -impl From> for CompositeSelectArgs -where - ArgT: Serialize, - SelT: Into, -{ - fn from(ArgSelect(args, selection): ArgSelect) -> Self { - Self::Get( - NodeArgsErased::Inline(to_json_value(args)), - selection.into(), - PhantomData, - ) - } -} - -impl From> for ScalarSelectArgs { - fn from(value: PlaceholderArg) -> Self { - Self::Get(NodeArgsErased::Placeholder(value.value), PhantomData) - } -} -impl From> - for CompositeSelectArgs -where - SelT: Into, -{ - fn from(value: PlaceholderArgSelect) -> Self { - Self::Get( - NodeArgsErased::Placeholder(value.value), - value.selection.into(), - PhantomData, - ) - } -} - -// --- ToAliasSelection impls --- // - -/// This is a marker trait that allows the core selection types -/// like CompositeSelectNoArgs to mark which types can be used -/// as their aliasing nodes. This prevents usage of invalid selections -/// on aliases like [`Skip`]. -pub trait FromAliasSelection {} - -impl FromAliasSelection for ScalarSelect {} -impl FromAliasSelection> for ScalarSelectArgs {} -impl FromAliasSelection> for CompositeSelect {} -impl FromAliasSelection> - for CompositeSelectArgs -{ -} - -// --- From Alias impls --- // - -impl From>> for ScalarSelect { - fn from(Alias(info): Alias<(), ScalarSelect>) -> Self { - Self::Alias(AliasInfo { - aliases: info.aliases, - _phantom: PhantomData, - }) - } -} -impl From> for ScalarSelectArgs { - fn from(Alias(info): Alias) -> Self { - Self::Alias(info) - } -} -impl From> for CompositeSelect { - fn from(Alias(info): Alias<(), SelT>) -> Self { - Self::Alias(info) - } -} -impl From> for CompositeSelectArgs { - fn from(Alias(info): Alias) -> Self { - Self::Alias(info) - } -} - -// --- Into SelectionErased impls --- // - -impl From> for SelectionErased { - fn from(value: AliasInfo) -> SelectionErased { - SelectionErased::Alias(value.aliases) - } -} - -impl From> for SelectionErased { - fn from(value: ScalarSelect) -> SelectionErased { - use ScalarSelect::*; - match value { - Get => SelectionErased::Scalar, - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -impl From> for SelectionErased { - fn from(value: ScalarSelectArgs) -> SelectionErased { - use ScalarSelectArgs::*; - match value { - Get(arg, _) => SelectionErased::ScalarArgs(arg), - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -impl From> for SelectionErased { - fn from(value: CompositeSelect) -> SelectionErased { - use CompositeSelect::*; - match value { - Get(selection, _) => SelectionErased::Composite(selection), - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -impl From> for SelectionErased -where - SelT: Into, -{ - fn from(value: CompositeSelectArgs) -> SelectionErased { - use CompositeSelectArgs::*; - match value { - Get(args, selection, _) => SelectionErased::CompositeArgs(args, selection), - Skip => SelectionErased::None, - Alias(alias) => alias.into(), - } - } -} - -// --- UnionMember impls --- // - -/// The following trait is used for types that implement -/// selections for the composite members of unions. -/// -/// The err return value indicates the case where -/// aliases are used selections on members which is an error -/// -/// This state is currently impossible to arrive at since -/// AliasInfo has no public construction methods with NoAlias -/// set. Union selection types make sure all their immediate -/// member selection use NoAlias to prevent this invalid stat.e -pub trait UnionMember { - fn composite(self) -> Option; -} - -/// Internal marker trait use to make sure we can't have union members -/// selection being another union selection. -trait NotUnionSelection {} - -// NOTE: UnionMembers are all NoAlias -impl UnionMember for ScalarSelect { - fn composite(self) -> Option { - None - } -} - -impl UnionMember for ScalarSelectArgs { - fn composite(self) -> Option { - None - } -} - -impl UnionMember for CompositeSelect -where - SelT: NotUnionSelection, -{ - fn composite(self) -> Option { - use CompositeSelect::*; - match self { - Get(CompositeSelection::Atomic(selection), _) => Some(selection), - Skip => None, - Get(CompositeSelection::Union(_), _) => { - unreachable!("union selection on union member selection. how??") - } - Alias(_) => unreachable!("alias discovored on union/either member. how??"), - } - } -} - -impl UnionMember for CompositeSelectArgs -where - SelT: NotUnionSelection, -{ - fn composite(self) -> Option { - use CompositeSelectArgs::*; - match self { - Get(_args, CompositeSelection::Atomic(selection), _) => Some(selection), - Skip => None, - Get(_args, CompositeSelection::Union(_), _) => { - unreachable!("union selection on union member selection. how??") - } - Alias(_) => unreachable!("alias discovored on union/either member. how??"), - } - } -} - -// --- Into AliasSelection impls --- // - -impl From for AliasSelection { - fn from(_val: Get) -> Self { - AliasSelection::Scalar - } -} -impl From> for AliasSelection -where - ArgT: Serialize, -{ - fn from(val: Args) -> Self { - AliasSelection::ScalarArgs(NodeArgsErased::Inline(to_json_value(val.0))) - } -} -impl From> for AliasSelection -where - SelT: Into, -{ - fn from(val: Select) -> Self { - let map = val.0.into(); - AliasSelection::Composite(map) - } -} - -impl From> for AliasSelection -where - ArgT: Serialize, - SelT: Into, -{ - fn from(val: ArgSelect) -> Self { - let map = val.1.into(); - AliasSelection::CompositeArgs(NodeArgsErased::Inline(to_json_value(val.0)), map) - } -} -impl From> for AliasSelection { - fn from(val: ScalarSelect) -> Self { - use ScalarSelect::*; - match val { - Get => AliasSelection::Scalar, - _ => unreachable!(), - } - } -} -impl From> for AliasSelection { - fn from(val: ScalarSelectArgs) -> Self { - use ScalarSelectArgs::*; - match val { - Get(args, _) => AliasSelection::ScalarArgs(args), - _ => unreachable!(), - } - } -} - -impl From> for AliasSelection -where - SelT: Into, -{ - fn from(val: CompositeSelect) -> Self { - use CompositeSelect::*; - match val { - Get(select, _) => AliasSelection::Composite(select), - _ => unreachable!(), - } - } -} -impl From> for AliasSelection -where - SelT: Into, -{ - fn from(val: CompositeSelectArgs) -> Self { - use CompositeSelectArgs::*; - match val { - Get(args, selection, _) => AliasSelection::CompositeArgs(args, selection), - _ => unreachable!(), - } - } -} - -// TODO: convert to proc_macro -#[macro_export] -macro_rules! impl_selection_traits { - ($ty:ident,$($field:tt),+) => { - impl From<$ty> for CompositeSelection { - fn from(value: $ty) -> CompositeSelection { - CompositeSelection::Atomic(SelectionErasedMap( - [ - $((stringify!($field).into(), value.$field.into()),)+ - ] - .into(), - )) - } - } - - impl Selection for $ty { - fn all() -> Self { - Self { - $($field: all(),)+ - } - } - } - - impl NotUnionSelection for $ty {} - }; -} -#[macro_export] -macro_rules! impl_union_selection_traits { - ($ty:ident,$(($variant_ty:tt, $field:tt)),+) => { - impl From<$ty> for CompositeSelection { - fn from(value: $ty) -> CompositeSelection { - CompositeSelection::Union( - [ - $({ - let selection = - UnionMember::composite(value.$field); - selection.map(|val| ($variant_ty.into(), val)) - },)+ - ] - .into_iter() - .filter_map(|val| val) - .collect(), - ) - } - } - }; -} - -// -// --- --- Argument types --- --- // -// - -pub enum NodeArgs { - Inline(ArgT), - Placeholder(PlaceholderValue), -} - -impl From for NodeArgs { - fn from(value: ArgT) -> Self { - Self::Inline(value) - } -} - -#[derive(Debug)] -pub enum NodeArgsErased { - None, - Inline(serde_json::Value), - Placeholder(PlaceholderValue), -} - -impl From> for NodeArgsErased -where - ArgT: Serialize, -{ - fn from(value: NodeArgs) -> Self { - match value { - NodeArgs::Inline(arg) => Self::Inline(to_json_value(arg)), - NodeArgs::Placeholder(ph) => Self::Placeholder(ph), - } - } -} - -enum NodeArgsMerged { - Inline(HashMap), - Placeholder { - value: PlaceholderValue, - arg_types: HashMap, - }, -} - -/// This checks the input arg json for a node -/// against the arg description from the [`NodeMeta`]. -fn check_node_args( - args: serde_json::Value, - arg_types: &HashMap, -) -> Result, String> { - let args = match args { - serde_json::Value::Object(val) => val, - _ => unreachable!(), - }; - let mut instance_args = HashMap::new(); - for (name, value) in args { - let Some(type_name) = arg_types.get(&name[..]) else { - return Err(name); - }; - instance_args.insert( - name.into(), - NodeArgValue { - type_name: type_name.clone(), - value, - }, - ); - } - Ok(instance_args) -} - -struct NodeArgValue { - type_name: CowStr, - value: serde_json::Value, -} - -pub struct PreparedArgs; - -impl PreparedArgs { - pub fn get(&mut self, key: impl Into, fun: F) -> NodeArgs - where - In: serde::de::DeserializeOwned, - F: Fn(In) -> ArgT + 'static + Send + Sync, - ArgT: Serialize, - { - NodeArgs::Placeholder(PlaceholderValue { - key: key.into(), - fun: Box::new(move |value| { - let value = serde_json::from_value(value)?; - let value = fun(value); - serde_json::to_value(value) - }), - }) - } - pub fn arg(&mut self, key: impl Into, fun: F) -> T - where - T: From>, - In: serde::de::DeserializeOwned, - F: Fn(In) -> ArgT + 'static + Send + Sync, - ArgT: Serialize, - { - T::from(PlaceholderArg { - value: PlaceholderValue { - key: key.into(), - fun: Box::new(move |value| { - let value = serde_json::from_value(value)?; - let value = fun(value); - serde_json::to_value(value) - }), - }, - _phantom: PhantomData, - }) - } - pub fn arg_select( - &mut self, - key: impl Into, - selection: SelT, - fun: F, - ) -> T - where - T: From>, - In: serde::de::DeserializeOwned, - F: Fn(In) -> ArgT + 'static + Send + Sync, - ArgT: Serialize, - { - T::from(PlaceholderArgSelect { - value: PlaceholderValue { - key: key.into(), - fun: Box::new(move |value| { - let value = serde_json::from_value(value)?; - let value = fun(value); - serde_json::to_value(value) - }), - }, - selection, - _phantom: PhantomData, - }) - } -} - -pub struct PlaceholderValue { - key: CowStr, - fun: Box< - dyn Fn(serde_json::Value) -> Result + Send + Sync, - >, -} - -impl std::fmt::Debug for PlaceholderValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PlaceholderValue") - .field("key", &self.key) - .finish_non_exhaustive() - } -} - -pub struct PlaceholderArg { - value: PlaceholderValue, - _phantom: PhantomData, -} -pub struct PlaceholderArgSelect { - value: PlaceholderValue, - selection: SelT, - _phantom: PhantomData, -} - -pub struct PlaceholderArgs(Arg); - -// -// --- --- GraphQL types --- --- // -// - -use graphql::*; -pub mod graphql { - use std::sync::Arc; - - use super::*; - - pub(super) type TyToGqlTyMap = Arc>; - - #[derive(Default, Clone)] - pub struct GraphQlTransportOptions { - headers: reqwest::header::HeaderMap, - timeout: Option, - } - - // PlaceholderValue, fieldName -> gql_var_name - type FoundPlaceholders = Vec<(PlaceholderValue, HashMap)>; - - struct GqlRequest { - doc: String, - variables: JsonObject, - placeholders: FoundPlaceholders, - path_to_files: HashMap>, - } - - struct GqlRequestBuilder<'a> { - ty_to_gql_ty_map: &'a TyToGqlTyMap, - variable_values: JsonObject, - variable_types: HashMap, - // map variable name to path to file types - path_to_files: HashMap>, - doc: String, - placeholders: Vec<(PlaceholderValue, HashMap)>, - } - - impl<'a> GqlRequestBuilder<'a> { - fn new(ty_to_gql_ty_map: &'a TyToGqlTyMap) -> Self { - Self { - ty_to_gql_ty_map, - variable_values: Default::default(), - variable_types: Default::default(), - path_to_files: Default::default(), - doc: Default::default(), - placeholders: Default::default(), - } - } - - fn register_path_to_files(&mut self, name: String, key: &str, files: &PathToInputFiles) { - let path_to_files = files - .0 - .iter() - .filter_map(|path| { - let first = path[0]; - if first.starts_with('.') && &first[1..] == key { - Some(TypePath(&path[1..])) - } else { - None - } - }) - .collect::>(); - self.path_to_files.insert(name, path_to_files); - } - - fn select_node_to_gql(&mut self, node: SelectNodeErased) -> std::fmt::Result { - use std::fmt::Write; - if node.instance_name != node.node_name { - write!(self.doc, "{}: {}", node.instance_name, node.node_name)?; - } else { - write!(self.doc, "{}", node.node_name)?; - } - - if let Some(args) = node.args { - match args { - NodeArgsMerged::Inline(args) => { - if !args.is_empty() { - write!(&mut self.doc, "(")?; - for (key, val) in args { - let name = format!("in{}", self.variable_types.len()); - - let mut map = serde_json::Map::new(); - map.insert(key.clone().into(), val.value.clone()); - let mut object = serde_json::Value::Object(map); - - if let Some(files) = node.input_files.as_ref() { - self.register_path_to_files(name.clone(), key.as_ref(), files); - } - - write!(&mut self.doc, "{key}: ${name}, ")?; - self.variable_values.insert( - name.clone(), - object - .as_object_mut() - .unwrap() - .remove(key.as_ref()) - .unwrap(), - ); - self.variable_types.insert(name.into(), val.type_name); - } - write!(&mut self.doc, ")")?; - } - } - NodeArgsMerged::Placeholder { value, arg_types } => { - if !arg_types.is_empty() { - write!(&mut self.doc, "(")?; - let mut map = HashMap::new(); - for (key, type_name) in arg_types { - let name = format!("in{}", self.variable_types.len()); - if let Some(files) = node.input_files.as_ref() { - self.register_path_to_files(name.clone(), key.as_ref(), files); - } - write!(&mut self.doc, "{key}: ${name}, ")?; - self.variable_types.insert(name.clone().into(), type_name); - map.insert(key, name.into()); - } - write!(&mut self.doc, ")")?; - self.placeholders.push((value, map)); - } - } - } - } - - match node.sub_nodes { - SubNodes::None => {} - SubNodes::Atomic(sub_nodes) => { - write!(&mut self.doc, "{{ ")?; - for node in sub_nodes { - self.select_node_to_gql(node)?; - write!(&mut self.doc, " ")?; - } - write!(&mut self.doc, " }}")?; - } - SubNodes::Union(variants) => { - write!(&mut self.doc, "{{ ")?; - for (ty, sub_nodes) in variants { - let gql_ty = self.ty_to_gql_ty_map.get(&ty[..]).expect( - "impossible: no GraphQL type equivalent found for variant type", - ); - let gql_ty = match gql_ty.strip_suffix('!') { - Some(val) => val, - None => &gql_ty[..], - }; - write!(&mut self.doc, " ... on {gql_ty} {{ ")?; - for node in sub_nodes { - self.select_node_to_gql(node)?; - write!(&mut self.doc, " ")?; - } - write!(&mut self.doc, " }}")?; - } - write!(&mut self.doc, " }}")?; - } - } - Ok(()) - } - - fn build( - mut self, - nodes: Vec, - ty: &'static str, - name: Option, - ) -> Result { - use std::fmt::Write; - - for (idx, node) in nodes.into_iter().enumerate() { - let node = SelectNodeErased { - instance_name: format!("node{idx}").into(), - ..node - }; - write!(&mut self.doc, " ").expect("error building to string"); - self.select_node_to_gql(node) - .expect("error building to string"); - writeln!(&mut self.doc).expect("error building to string"); - } - - let mut args_row = String::new(); - if !self.variable_types.is_empty() { - write!(&mut args_row, "(").expect("error building to string"); - for (key, ty) in &self.variable_types { - let gql_ty = self.ty_to_gql_ty_map.get(&ty[..]).ok_or_else(|| { - GraphQLRequestError::InvalidQuery { - error: Box::from(format!("unknown typegraph type found: {}", ty)), - } - })?; - write!(&mut args_row, "${key}: {gql_ty}, ").expect("error building to string"); - } - write!(&mut args_row, ")").expect("error building to string"); - } - - let name = name.unwrap_or_else(|| "".into()); - let doc = format!("{ty} {name}{args_row} {{\n{doc}}}", doc = self.doc); - Ok(GqlRequest { - doc, - variables: self.variable_values, - placeholders: self.placeholders, - path_to_files: self.path_to_files, - }) - } - } - - enum GraphQLRequestBody { - Json(serde_json::Value), - Multipart(reqwest::multipart::Form), - } - - struct GraphQLRequest { - addr: Url, - method: reqwest::Method, - headers: reqwest::header::HeaderMap, - body: GraphQLRequestBody, - } - - use reqwest::blocking::{Client as ClientSync, RequestBuilder as RequestBuilderSync}; - - enum BuildReqError { - FileUpload { error: BoxErr }, - } - - fn build_gql_req_sync( - client: &ClientSync, - addr: Url, - doc: &str, - mut variables: JsonObject, - path_to_files: HashMap>, - opts: &GraphQlTransportOptions, - ) -> Result { - use reqwest::blocking::multipart::Form; - - let files = FileExtractor::extract_all_from(&mut variables, path_to_files) - .map_err(|error| BuildReqError::FileUpload { error })?; - - let mut request = client.request(reqwest::Method::POST, addr); - if let Some(timeout) = opts.timeout { - request = request.timeout(timeout); - } - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::ACCEPT, - "application/json".try_into().unwrap(), - ); - headers.extend(opts.headers.clone()); - - let operations = serde_json::json!({ - "query": doc, - "variables": variables - }); - - // TODO rename files - - if files.len() > 0 { - // multipart - let mut form = Form::new(); - - form = form.text("operations", serde_json::to_string(&operations).unwrap()); - - let (map, files): (HashMap<_, _>, Vec<_>) = files - .into_iter() - .enumerate() - .map(|(idx, (path, file_id))| { - ( - (idx.to_string(), vec![format!("variables.{path}")]), - file_id, - ) - }) - .unzip(); - - form = form.text("map", serde_json::to_string(&map).unwrap()); - - for (idx, file_id) in files.into_iter().enumerate() { - let file: File = file_id - .try_into() - .map_err(|error| BuildReqError::FileUpload { error })?; - form = form.part( - idx.to_string(), - file.try_into() - .map_err(|error| BuildReqError::FileUpload { error })?, - ); - } - - Ok(request.headers(headers).multipart(form)) - } else { - headers.insert( - reqwest::header::CONTENT_TYPE, - "application/json".try_into().unwrap(), - ); - Ok(request.headers(headers).json(&operations)) - } - } - - use reqwest::{Client, RequestBuilder}; - async fn build_gql_req( - client: &Client, - addr: Url, - doc: &str, - mut variables: JsonObject, - path_to_files: HashMap>, - opts: &GraphQlTransportOptions, - ) -> Result { - use reqwest::multipart::Form; - - let files = FileExtractor::extract_all_from(&mut variables, path_to_files) - .map_err(|error| BuildReqError::FileUpload { error })?; - - let request = client.request(reqwest::Method::POST, addr); - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::ACCEPT, - "application/json".try_into().unwrap(), - ); - headers.extend(opts.headers.clone()); - - let operations = serde_json::json!({ - "query": doc, - "variables": variables - }); - - if files.len() > 0 { - // multipart - let mut form = Form::new(); - - form = form.text("operations", serde_json::to_string(&operations).unwrap()); - - let (map, files): (HashMap<_, _>, Vec<_>) = files - .into_iter() - .enumerate() - .map(|(idx, (path, file_id))| { - ( - (idx.to_string(), vec![format!("variables.{path}")]), - file_id, - ) - }) - .unzip(); - - form = form.text("map", serde_json::to_string(&map).unwrap()); - - for (idx, file_id) in files.into_iter().enumerate() { - let file: File = file_id - .try_into() - .map_err(|error| BuildReqError::FileUpload { error })?; - form = form.part( - idx.to_string(), - file.into_reqwest_part() - .await - .map_err(|error| BuildReqError::FileUpload { error })?, - ); - } - - Ok(request.headers(headers).multipart(form)) - } else { - headers.insert( - reqwest::header::CONTENT_TYPE, - "application/json".try_into().unwrap(), - ); - Ok(request.headers(headers).json(&operations)) - } - } - - #[derive(Debug)] - pub struct GraphQLResponse { - pub status: reqwest::StatusCode, - pub headers: reqwest::header::HeaderMap, - pub body: JsonObject, - } - - fn handle_response( - response: GraphQLResponse, - nodes_len: usize, - ) -> Result, GraphQLRequestError> { - if !response.status.is_success() { - return Err(GraphQLRequestError::RequestFailed { response }); - } - #[derive(Debug, Deserialize)] - struct Response { - data: Option, - errors: Option>, - } - let body: Response = match serde_json::from_value(serde_json::Value::Object(response.body)) - { - Ok(body) => body, - Err(error) => { - return Err(GraphQLRequestError::BodyError { - error: Box::new(error), - }) - } - }; - if let Some(errors) = body.errors { - return Err(GraphQLRequestError::RequestErrors { - errors, - data: body.data, - }); - } - let Some(mut body) = body.data else { - return Err(GraphQLRequestError::BodyError { - error: Box::from("body response doesn't contain data field"), - }); - }; - (0..nodes_len) - .map(|idx| { - body.remove(&format!("node{idx}")) - .ok_or_else(|| GraphQLRequestError::BodyError { - error: Box::from(format!( - "expecting response under node key 'node{idx}' but none found" - )), - }) - }) - .collect::, _>>() - } - - #[derive(Debug)] - pub enum GraphQLRequestError { - /// GraphQL errors recieived - RequestErrors { - errors: Vec, - data: Option, - }, - /// Http error codes recieived - RequestFailed { - response: GraphQLResponse, - }, - /// Unable to deserialize body - BodyError { - error: BoxErr, - }, - /// Unable to make http request - NetworkError { - error: BoxErr, - }, - InvalidQuery { - error: BoxErr, - }, - /// Unable to upload file - FileUpload { - error: BoxErr, - }, - } - - impl From for GraphQLRequestError { - fn from(error: BuildReqError) -> Self { - match error { - BuildReqError::FileUpload { error } => GraphQLRequestError::FileUpload { error }, - } - } - } - - impl std::fmt::Display for GraphQLRequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - GraphQLRequestError::RequestErrors { errors, .. } => { - write!(f, "graphql errors in response: ")?; - for err in errors { - write!(f, "{}, ", err.message)?; - } - } - GraphQLRequestError::RequestFailed { response } => { - write!(f, "request failed with status {}", response.status)?; - } - GraphQLRequestError::BodyError { error } => { - write!(f, "error reading request body: {error}")?; - } - GraphQLRequestError::NetworkError { error } => { - write!(f, "error making http request: {error}")?; - } - GraphQLRequestError::InvalidQuery { error } => { - write!(f, "error building request: {error}")? - } - GraphQLRequestError::FileUpload { error } => { - write!(f, "error uploading file: {error}")? - } - } - Ok(()) - } - } - impl std::error::Error for GraphQLRequestError {} - - #[derive(Debug, Deserialize)] - pub struct ErrorLocation { - pub line: u32, - pub column: u32, - } - #[derive(Debug, Deserialize)] - pub struct GraphqlError { - pub message: String, - pub locations: Option>, - pub path: Option>, - } - - #[derive(Debug)] - pub enum PathSegment { - Field(String), - Index(u64), - } - - impl<'de> serde::de::Deserialize<'de> for PathSegment { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - use serde_json::Value; - let val = Value::deserialize(deserializer)?; - match val { - Value::Number(n) => Ok(PathSegment::Index(n.as_u64().unwrap())), - Value::String(s) => Ok(PathSegment::Field(s)), - _ => panic!("invalid path segment type"), - } - } - } - - #[derive(Clone)] - pub struct GraphQlTransportReqwestSync { - addr: Url, - ty_to_gql_ty_map: TyToGqlTyMap, - client: reqwest::blocking::Client, - } - - #[derive(Clone)] - pub struct GraphQlTransportReqwest { - addr: Url, - ty_to_gql_ty_map: TyToGqlTyMap, - client: reqwest::Client, - } - - impl GraphQlTransportReqwestSync { - pub fn new(addr: Url, ty_to_gql_ty_map: TyToGqlTyMap) -> Self { - Self { - addr, - ty_to_gql_ty_map, - client: reqwest::blocking::Client::new(), - } - } - - fn fetch( - &self, - nodes: Vec, - opts: &GraphQlTransportOptions, - ty: &'static str, - ) -> Result, GraphQLRequestError> { - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(&self.ty_to_gql_ty_map).build(nodes, ty, None)?; - if !placeholders.is_empty() { - panic!("placeholders found in non-prepared query") - } - let req = build_gql_req_sync( - &self.client, - self.addr.clone(), - &doc, - variables, - path_to_files, - opts, - )?; - match req.send() { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::() { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - nodes_len, - ), - Err(error) => Err(GraphQLRequestError::BodyError { - error: Box::new(error), - }), - } - } - Err(error) => Err(GraphQLRequestError::NetworkError { - error: Box::new(error), - }), - } - } - - pub fn query( - &self, - nodes: Doc, - ) -> Result { - self.query_with_opts(nodes, &Default::default()) - } - - pub fn query_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "query")?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - - pub fn mutation( - &self, - nodes: Doc, - ) -> Result { - self.mutation_with_opts(nodes, &Default::default()) - } - - pub fn mutation_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "mutation")?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - pub fn prepare_query( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_query_with_opts(fun, Default::default()) - } - - pub fn prepare_query_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwestSync::new( - fun, - self.addr.clone(), - opts, - "query", - &self.ty_to_gql_ty_map, - ) - } - - pub fn prepare_mutation( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_mutation_with_opts(fun, Default::default()) - } - - pub fn prepare_mutation_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwestSync::new( - fun, - self.addr.clone(), - opts, - "mutation", - &self.ty_to_gql_ty_map, - ) - } - } - - impl GraphQlTransportReqwest { - pub fn new(addr: Url, ty_to_gql_ty_map: TyToGqlTyMap) -> Self { - Self { - addr, - ty_to_gql_ty_map, - client: reqwest::Client::new(), - } - } - - async fn fetch( - &self, - nodes: Vec, - opts: &GraphQlTransportOptions, - ty: &'static str, - ) -> Result, GraphQLRequestError> { - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(&self.ty_to_gql_ty_map).build(nodes, ty, None)?; - if !placeholders.is_empty() { - panic!("placeholders found in non-prepared query") - } - - let req = build_gql_req( - &self.client, - self.addr.clone(), - &doc, - variables, - path_to_files, - opts, - ) - .await?; - match req.send().await { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::().await { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - nodes_len, - ), - Err(error) => Err(GraphQLRequestError::BodyError { - error: Box::new(error), - }), - } - } - Err(error) => Err(GraphQLRequestError::NetworkError { - error: Box::new(error), - }), - } - } - - pub async fn query( - &self, - nodes: Doc, - ) -> Result { - self.query_with_opts(nodes, &Default::default()).await - } - - pub async fn query_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "query").await?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - - pub async fn mutation( - &self, - nodes: Doc, - ) -> Result { - self.mutation_with_opts(nodes, &Default::default()).await - } - - pub async fn mutation_with_opts( - &self, - nodes: Doc, - opts: &GraphQlTransportOptions, - ) -> Result { - let resp = self.fetch(nodes.to_select_doc(), opts, "mutation").await?; - let resp = Doc::parse_response(resp).map_err(|err| GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - })?; - Ok(resp) - } - pub fn prepare_query( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_query_with_opts(fun, Default::default()) - } - - pub fn prepare_query_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwest::new( - fun, - self.addr.clone(), - opts, - "query", - &self.ty_to_gql_ty_map, - ) - } - - pub fn prepare_mutation( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - ) -> Result, PrepareRequestError> { - self.prepare_mutation_with_opts(fun, Default::default()) - } - - pub fn prepare_mutation_with_opts( - &self, - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - opts: GraphQlTransportOptions, - ) -> Result, PrepareRequestError> { - PreparedRequestReqwest::new( - fun, - self.addr.clone(), - opts, - "mutation", - &self.ty_to_gql_ty_map, - ) - } - } - - fn resolve_prepared_variables( - placeholders: &FoundPlaceholders, - mut inline_variables: JsonObject, - mut args: HashMap, - ) -> Result { - for (ph, key_map) in placeholders { - let Some(value) = args.remove(&ph.key) else { - return Err(PrepareRequestError::PlaceholderError(Box::from(format!( - "no value found for placeholder expected under key '{}'", - ph.key - )))); - }; - let value = (ph.fun)(value).map_err(|err| { - PrepareRequestError::PlaceholderError(Box::from(format!( - "error applying placeholder closure for value under key '{}': {err}", - ph.key - ))) - })?; - let serde_json::Value::Object(mut value) = value else { - unreachable!("placeholder closures must return structs"); - }; - for (key, var_key) in key_map { - inline_variables.insert( - var_key.clone().into(), - value.remove(&key[..]).unwrap_or(serde_json::Value::Null), - ); - } - } - Ok(inline_variables) - } - - pub struct PreparedRequestReqwest { - addr: Url, - client: reqwest::Client, - nodes_len: usize, - pub doc: String, - variables: JsonObject, - path_to_files: HashMap>, - opts: GraphQlTransportOptions, - placeholders: Arc, - _phantom: PhantomData, - } - - pub struct PreparedRequestReqwestSync { - addr: Url, - client: reqwest::blocking::Client, - nodes_len: usize, - pub doc: String, - variables: JsonObject, - path_to_files: HashMap>, - opts: GraphQlTransportOptions, - placeholders: Arc, - _phantom: PhantomData, - } - - impl PreparedRequestReqwestSync { - fn new( - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - addr: Url, - opts: GraphQlTransportOptions, - ty: &'static str, - ty_to_gql_ty_map: &TyToGqlTyMap, - ) -> Result { - let nodes = fun(&mut PreparedArgs); - let nodes = nodes.to_select_doc(); - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(ty_to_gql_ty_map) - .build(nodes, ty, None) - .map_err(PrepareRequestError::BuildError)?; - Ok(Self { - doc, - variables, - path_to_files, - nodes_len, - addr, - client: reqwest::blocking::Client::new(), - opts, - placeholders: Arc::new(placeholders), - _phantom: PhantomData, - }) - } - - pub fn perform( - &self, - args: impl Into>, - ) -> Result - where - K: Into, - V: serde::Serialize, - { - let args: HashMap = args.into(); - let args = args - .into_iter() - .map(|(key, val)| (key.into(), to_json_value(val))) - .collect(); - let variables = - resolve_prepared_variables(&self.placeholders, self.variables.clone(), args)?; - // TODO extract files from variables after resolution - let req = build_gql_req_sync( - &self.client, - self.addr.clone(), - &self.doc, - variables, - self.path_to_files.clone(), - &self.opts, - )?; - let res = match req.send() { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::() { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - self.nodes_len, - ) - .map_err(PrepareRequestError::RequestError)?, - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::BodyError { - error: Box::new(error), - }, - )) - } - } - } - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::NetworkError { - error: Box::new(error), - }, - )) - } - }; - Doc::parse_response(res).map_err(|err| { - PrepareRequestError::RequestError(GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - }) - }) - } - } - - impl PreparedRequestReqwest { - fn new( - fun: impl FnOnce(&mut PreparedArgs) -> Doc, - addr: Url, - opts: GraphQlTransportOptions, - ty: &'static str, - ty_to_gql_ty_map: &TyToGqlTyMap, - ) -> Result { - let nodes = fun(&mut PreparedArgs); - let nodes = nodes.to_select_doc(); - let nodes_len = nodes.len(); - let GqlRequest { - doc, - variables, - placeholders, - path_to_files, - } = GqlRequestBuilder::new(ty_to_gql_ty_map) - .build(nodes, ty, None) - .map_err(PrepareRequestError::BuildError)?; - let placeholders = std::sync::Arc::new(placeholders); - Ok(Self { - doc, - variables, - path_to_files, - nodes_len, - addr, - client: reqwest::Client::new(), - opts, - placeholders, - _phantom: PhantomData, - }) - } - - pub async fn perform( - &self, - args: impl Into>, - ) -> Result - where - K: Into, - V: serde::Serialize, - { - let args: HashMap = args.into(); - let args = args - .into_iter() - .map(|(key, val)| (key.into(), to_json_value(val))) - .collect(); - let variables = - resolve_prepared_variables(&self.placeholders, self.variables.clone(), args)?; - // TODO extract files from variables - let req = build_gql_req( - &self.client, - self.addr.clone(), - &self.doc, - variables, - self.path_to_files.clone(), - &self.opts, - ) - .await?; - let res = match req.send().await { - Ok(res) => { - let status = res.status(); - let headers = res.headers().clone(); - match res.json::().await { - Ok(body) => handle_response( - GraphQLResponse { - status, - headers, - body, - }, - self.nodes_len, - ) - .map_err(PrepareRequestError::RequestError)?, - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::BodyError { - error: Box::new(error), - }, - )) - } - } - } - Err(error) => { - return Err(PrepareRequestError::RequestError( - GraphQLRequestError::NetworkError { - error: Box::new(error), - }, - )) - } - }; - Doc::parse_response(res).map_err(|err| { - PrepareRequestError::RequestError(GraphQLRequestError::BodyError { - error: Box::from(format!( - "error deserializing response into output type: {err}" - )), - }) - }) - } - } - - // we need a manual clone impl since the derive will - // choke if Doc isn't clone - impl Clone for PreparedRequestReqwestSync { - fn clone(&self) -> Self { - Self { - addr: self.addr.clone(), - client: self.client.clone(), - nodes_len: self.nodes_len, - doc: self.doc.clone(), - variables: self.variables.clone(), - path_to_files: self.path_to_files.clone(), - opts: self.opts.clone(), - placeholders: self.placeholders.clone(), - _phantom: PhantomData, - } - } - } - impl Clone for PreparedRequestReqwest { - fn clone(&self) -> Self { - Self { - addr: self.addr.clone(), - client: self.client.clone(), - nodes_len: self.nodes_len, - doc: self.doc.clone(), - variables: self.variables.clone(), - path_to_files: self.path_to_files.clone(), - opts: self.opts.clone(), - placeholders: self.placeholders.clone(), - _phantom: PhantomData, - } - } - } - - #[derive(Debug)] - pub enum PrepareRequestError { - BuildError(GraphQLRequestError), - PlaceholderError(BoxErr), - RequestError(GraphQLRequestError), - FileUploadError(BoxErr), - } - - impl From for PrepareRequestError { - fn from(error: BuildReqError) -> Self { - match error { - BuildReqError::FileUpload { error } => PrepareRequestError::FileUploadError(error), - } - } - } - - impl std::error::Error for PrepareRequestError {} - impl std::fmt::Display for PrepareRequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - /* PrepareRequestError::FunctionError(err) => { - write!(f, "error calling doc builder closure: {err}") - } */ - PrepareRequestError::BuildError(err) => write!(f, "error building request: {err}"), - PrepareRequestError::PlaceholderError(err) => { - write!(f, "error resolving placeholder values: {err}") - } - PrepareRequestError::RequestError(err) => { - write!(f, "error making graphql request: {err}") - } - PrepareRequestError::FileUploadError(err) => { - write!(f, "error uploading file: {err}") - } - } - } - } -} +use core::marker::PhantomData; +use metagen_client::prelude::*; // // --- --- QueryGraph types --- --- // diff --git a/tests/metagen/typegraphs/sample/rs_upload/main.rs b/tests/metagen/typegraphs/sample/rs_upload/main.rs index c09f881a1e..8717035c25 100644 --- a/tests/metagen/typegraphs/sample/rs_upload/main.rs +++ b/tests/metagen/typegraphs/sample/rs_upload/main.rs @@ -5,6 +5,7 @@ mod client; use client::*; +use metagen_client::prelude::*; fn main() -> Result<(), BoxErr> { let port = std::env::var("TG_PORT")?; diff --git a/tests/metagen/typegraphs/sample/ts/client.ts b/tests/metagen/typegraphs/sample/ts/client.ts index 25f0bb1d91..0c2619e473 100644 --- a/tests/metagen/typegraphs/sample/ts/client.ts +++ b/tests/metagen/typegraphs/sample/ts/client.ts @@ -353,7 +353,7 @@ function convertQueryNodeGql( : `${node.instanceName}: ${node.nodeName}`; const args = node.args; - if (args) { + if (args && Object.keys(args).length > 0) { out = `${out} (${ Object.entries(args) .map(([key, val]) => {