crud_api/lib.rs
1//! # Crud Api
2//!
3//! This crate provides a framework to generate an executable to manipulate your HTTP API from CLI.
4//!
5//! The apps using this lib can replace your _curl_ queries when you need to access to your favorite API.
6//!
7//! ## Features
8//!
9//! API:
10//! - data are encoded in JSON. It don't support XML, grpc, ...
11//! - output can be formated on json, yaml, toml, csv or tsv
12//! - output stream on stdout or in a file
13//!
14//!
15//! ## Tutorial
16//!
17//! Let's create an CLI for [jsonplaceholder](http://jsonplaceholder.typicode.com/) API.
18//! For the impatients, the whole code of this example can be found in [`examples/jsonplaceholder_api.rs`](./examples/jsonplaceholder_api.rs "jsonplaceholder_api.rs")
19//!
20//! First add these dependencies to `Cargo.toml`:
21//! ```toml
22//! [dependencies]
23//! log = "0.4"
24//! pretty_env_logger = "0.5"
25//! clap = "4.3"
26//! crud-api = {version = "0.1", path="../crud/crud-api", default-features=false, features=["toml","json","yaml"]}
27//! crud-auth = {version = "0.1", path="../crud/crud-auth"}
28//! crud-auth-bearer = {version = "0.1", path="../crud/crud-auth-bearer"}
29//! hyper = { version = "0.14", features = ["client","http1"] }
30//! hyper-tls = "0.5"
31//! miette = { version = "5.9", features = ["fancy"] }
32//! tokio = { version = "1", features = ["full"] }
33//! serde = { version = "1.0", features = ["derive"] }
34//! # To force static openssl
35//! openssl = { version = "0.10", features = ["vendored"] }
36//! ```
37//!
38//! Now, create a minimal runner stucture and a `main` function.
39//! `ApiRun` on `JSONPlaceHolder` derives all the CLI.
40//! ```rust
41//! use crud_api::ApiRun;
42//! use crud_auth::CrudAuth;
43//! use crud_auth_no_auth::Auth;
44//! use miette::{IntoDiagnostic, Result};
45//!
46//! #[derive(ApiRun)]
47//! struct JSONPlaceHolder;
48//!
49//! #[tokio::main]
50//! async fn main() -> Result<()> {
51//! JSONPlaceHolder::run().await
52//! }
53//! ```
54//! [`crud_api_endpoint::ApiRun`] accepts some parameters. They are documented in `crud_api_endoint` crate.
55//! Let's customize our CLI with a `base_url` for our API, a `name` used in the documentation and the settings. `qualifier` and `organisation` is used to compute the settings location and `env_prefix` is the prefix of the environment variables
56//! ```rust
57//! # use crud_api::ApiRun;
58//! # use crud_auth::CrudAuth;
59//! # use crud_auth_no_auth::Auth;
60//! # use miette::{IntoDiagnostic, Result};
61//! #[derive(ApiRun)]
62//! #[api(infos(
63//! base_url = "http://jsonplaceholder.typicode.com",
64//! name = "jsonplaceholder",
65//! qualifier = "com",
66//! organisation = "typicode",
67//! env_prefix = "JSONPLACEHOLDER"
68//! ))]
69//! struct JSONPlaceHolder;
70//! # #[tokio::main]
71//! # async fn main() -> Result<()> {
72//! # JSONPlaceHolder::run().await
73//! # }
74//! ```
75//! Before creating the first endpoint we need to describe its output structure.
76//! ```rust
77//! use serde::{Deserialize, Serialize};
78//! #[derive(Debug, Default, Deserialize, Serialize)]
79//! struct Post {
80//! id: u32,
81//! #[serde(rename = "userId")]
82//! user_id: u32,
83//! title: String,
84//! body: String,
85//! }
86//! ```
87//!
88//! Now, we can declare the endpoint.
89//! The minimal parameters are:
90//! - `route`, the target api route.
91//! - `cli_route`, the route transcipted as cli arguments. Each slash separate a subcommand.
92//! The other parameters can found in [`crud_api_endpoint::Api`] and [`crud_api_endpoint::Enpoint`] structs documentation.
93//!
94//! ```rust
95//! # use serde::{Deserialize, Serialize};
96//! use crud_api::Api;
97//! #[derive(Api, Debug, Default, Deserialize, Serialize)]
98//! #[api(
99//! endpoint(
100//! route = "/posts",
101//! cli_route = "/post",
102//! multiple_results,
103//! ))]
104//! struct Post {
105//! id: u32,
106//! #[serde(rename = "userId")]
107//! user_id: u32,
108//! title: String,
109//! body: String,
110//! }
111//! ```
112//! We can create more complex enpoint. Let's create an edit route.
113//!
114//! - The `route` parameter takes a post's `id` argument. This argument should be present in the `cli_route`.
115//! - the HTTP method is set with the `method` parameter.
116//! - Some help can be provided via the parameters `cli_help` and `cli_long_help`.
117//! - the payload is described by the struct declared with the `payload_struct`. The query parameter can be add with the `query_struct` parameter.
118//!
119//! In this step, the payload structure is `PostCreate` (the same structure is used for both creation and update). `PostCreate` derives `ApiInput`. All `PostCreate` fields parameters are describe in the [`crud_api_endpoint::ApiInputConfig`] structs.
120//!
121//!
122//! ```rust
123//! # use serde::{Deserialize, Serialize};
124//! use crud_api::{Api, ApiInput};
125//! #[derive(Api, Debug, Default, Deserialize, Serialize)]
126//! #[api(
127//! endpoint(
128//! route = "/posts",
129//! cli_route = "/post",
130//! multiple_results,
131//! ),
132//! endpoint(
133//! route = "/posts/{id}",
134//! method = "PUT",
135//! payload_struct = "PostCreate",
136//! cli_route = "/post/{id}/replace",
137//! cli_help = "Update a Posts (replace the whole post)"
138//! )
139//! )]
140//! struct Post {
141//! id: u32,
142//! #[serde(rename = "userId")]
143//! user_id: u32,
144//! title: String,
145//! body: String,
146//! }
147//!
148//! #[derive(Debug, ApiInput, Default, Serialize, Deserialize)]
149//! #[allow(dead_code, non_snake_case)]
150//! struct PostCreate {
151//! #[api(long = "user-id")]
152//! userId: u32,
153//! #[api(no_short, help = "Title of the post")]
154//! title: String,
155//! #[api(no_short)]
156//! body: String,
157//! }
158//! ```
159//!
160//! ## Output Customization
161//!
162//! ### Tables
163//!
164//! Results arrays are formatted using the crate [`crud-tidy-viewer`](crud_tidy_viewer).
165//! The available table column options are:
166//! - [`table_skip`](../crud_api_endpoint/struct.ApiField.html#structfield.table_skip): don't display this field in the table.
167//! - [`table_format`](../crud_api_endpoint/struct.ApiField.html#structfield.table_format): format this field in table.
168//! - date formatter: `date(format = "%Y-%m-%d %H:%M:%S")`
169//!
170//! ### Pretty Structures
171//!
172//! The crate [`crud-pretty-struct`](crud_pretty_struct) can format a single (json) struct.
173
174use async_trait::async_trait;
175use clap::{ArgMatches, Command, Id};
176pub use crud_api_derive::*;
177use crud_pretty_struct::PrettyPrint;
178#[cfg(any(feature = "json", feature = "toml", feature = "yaml", feature = "csv"))]
179use crud_tidy_viewer::{display_table, TableConfig};
180use formats::OutputFormat;
181#[doc(hidden)]
182pub use formats::{
183 clap_match_input_from_file, clap_match_output_format, clap_match_template, clap_output_format_decl,
184};
185use miette::{IntoDiagnostic, Result};
186use serde::{de::DeserializeOwned, Deserialize, Serialize};
187use std::{
188 fmt::Debug,
189 marker::{PhantomData, Sized},
190};
191
192extern crate crud_api_derive;
193#[doc(hidden)]
194pub mod cli;
195#[doc(hidden)]
196pub mod completions;
197#[doc(hidden)]
198pub mod error;
199mod formats;
200#[doc(hidden)]
201pub mod http;
202#[doc(hidden)]
203pub mod settings;
204
205#[doc(hidden)]
206pub struct ApiInputOptions {
207 pub conflicts_with_all: Vec<Id>,
208}
209
210#[doc(hidden)]
211pub trait ApiInput {
212 /// Generate the clap command declatations.
213 fn clap(app: Command, options: Option<ApiInputOptions>) -> Command;
214 fn from_clap_matches(matches: &ArgMatches) -> Result<Self>
215 where
216 Self: Sized;
217}
218
219#[doc(hidden)]
220#[async_trait]
221pub trait Query {
222 async fn query<P, T, R, Q>(
223 &self,
224 payload: Option<P>,
225 argument: Option<Q>,
226 t: Option<PhantomData<T>>,
227 ) -> Result<R>
228 where
229 P: Send + Serialize + Debug,
230 T: TryInto<R, Error = String> + DeserializeOwned + Send,
231 R: Send + DeserializeOwned + Debug + Default,
232 Q: Send + Serialize + Debug;
233 async fn stream<P, Q>(
234 &self,
235 payload: Option<P>,
236 argument: Option<Q>,
237 filename: Option<String>,
238 ) -> Result<()>
239 where
240 P: Send + Serialize + Debug,
241 Q: Send + Serialize + Debug;
242}
243
244#[doc(hidden)]
245pub trait Api {
246 fn to_table_header(&self) -> Vec<String>;
247 fn to_table(&self) -> Result<Vec<String>>;
248 fn to_output(&self) -> Result<String>;
249
250 #[cfg(any(feature = "json", feature = "toml", feature = "yaml", feature = "csv"))]
251 fn output(&self, format: Option<OutputFormat>) -> Result<()>
252 where
253 Self: Serialize + Debug,
254 {
255 let out = match format {
256 Some(format) => match format {
257 #[cfg(feature = "json")]
258 OutputFormat::Json => Some(serde_json::to_string_pretty(self).into_diagnostic()?),
259 #[cfg(feature = "toml")]
260 OutputFormat::Toml => Some(toml::to_string(self).into_diagnostic()?),
261 #[cfg(feature = "yaml")]
262 OutputFormat::Yaml => Some(serde_yaml::to_string(self).into_diagnostic()?),
263 #[cfg(feature = "csv")]
264 OutputFormat::Csv => {
265 let mut wtr = csv::Writer::from_writer(std::io::stdout());
266 wtr.serialize(self).into_diagnostic()?;
267 wtr.flush().into_diagnostic()?;
268 None
269 }
270 #[cfg(feature = "csv")]
271 OutputFormat::Tsv => {
272 let mut wtr = csv::WriterBuilder::new()
273 .delimiter(b'\t')
274 .quote_style(csv::QuoteStyle::NonNumeric)
275 .from_writer(std::io::stdout());
276 wtr.serialize(self).into_diagnostic()?;
277 wtr.flush().into_diagnostic()?;
278 None
279 }
280 },
281 None => Some(self.to_output()?),
282 };
283
284 if let Some(out) = out {
285 print!("{out}");
286 }
287 Ok(())
288 }
289
290 #[cfg(all(
291 not(feature = "json"),
292 not(feature = "toml"),
293 not(feature = "yaml"),
294 not(feature = "csv")
295 ))]
296 fn output(&self, _format: Option<OutputFormat>) -> Result<()>
297 where
298 Self: Serialize + Debug,
299 {
300 Ok(())
301 }
302
303 #[cfg(any(feature = "json", feature = "toml", feature = "yaml", feature = "csv"))]
304 fn output_multiple(results: &Vec<Self>, format: Option<OutputFormat>) -> Result<()>
305 where
306 Self: Sized + Serialize + Debug,
307 {
308 let out = match format {
309 Some(format) => match format {
310 #[cfg(feature = "json")]
311 OutputFormat::Json => Some(serde_json::to_string_pretty(results).into_diagnostic()?),
312 #[cfg(feature = "toml")]
313 OutputFormat::Toml => Some(toml::to_string(results).into_diagnostic()?),
314 #[cfg(feature = "yaml")]
315 OutputFormat::Yaml => Some(serde_yaml::to_string(results).into_diagnostic()?),
316 #[cfg(feature = "csv")]
317 OutputFormat::Csv => {
318 let mut wtr = csv::Writer::from_writer(std::io::stdout());
319 for result in results {
320 wtr.serialize(result).into_diagnostic()?;
321 }
322 wtr.flush().into_diagnostic()?;
323 None
324 }
325 #[cfg(feature = "csv")]
326 OutputFormat::Tsv => {
327 let mut wtr = csv::WriterBuilder::new()
328 .delimiter(b'\t')
329 .quote_style(csv::QuoteStyle::NonNumeric)
330 .from_writer(std::io::stdout());
331 for result in results {
332 wtr.serialize(result).into_diagnostic()?;
333 }
334 wtr.flush().into_diagnostic()?;
335 None
336 }
337 },
338 None => {
339 if !results.is_empty() {
340 let mut table = vec![results.iter().next().unwrap().to_table_header()];
341 table.append(
342 &mut results
343 .iter()
344 .map(|row| row.to_table().expect("Formating data table"))
345 .collect(),
346 ); // FIXME: replace expect by something better from miette
347 display_table(&table, TableConfig::default());
348 }
349 None
350 }
351 };
352 if let Some(out) = out {
353 println!("{out}");
354 }
355 Ok(())
356 }
357
358 #[cfg(all(
359 not(feature = "json"),
360 not(feature = "toml"),
361 not(feature = "yaml"),
362 not(feature = "csv")
363 ))]
364 fn output_multiple(_results: &Vec<Self>, _format: Option<OutputFormat>) -> Result<()>
365 where
366 Self: Sized + Serialize + Debug,
367 {
368 Ok(())
369 }
370}
371
372/// An empty response. Use it in `result_struct`
373#[derive(Debug, Default, Deserialize, Serialize, PrettyPrint)]
374pub struct EmptyResponse {}
375impl Api for EmptyResponse {
376 fn to_table_header(&self) -> Vec<String> {
377 vec![]
378 }
379
380 fn to_table(&self) -> Result<Vec<String>> {
381 Ok(vec![])
382 }
383
384 fn to_output(&self) -> Result<String> {
385 Ok(String::new())
386 }
387}
388
389#[derive(Deserialize)]
390pub struct DummyTryFrom;
391
392impl TryFrom<DummyTryFrom> for EmptyResponse {
393 type Error = String;
394 fn try_from(_value: DummyTryFrom) -> std::result::Result<Self, Self::Error> {
395 Err(String::new())
396 }
397}
398
399impl<T> TryFrom<DummyTryFrom> for Vec<T> {
400 type Error = String;
401 fn try_from(_value: DummyTryFrom) -> std::result::Result<Self, Self::Error> {
402 Err(String::new())
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 #[test]
409 fn it_works() {
410 let result = 2 + 2;
411 assert_eq!(result, 4);
412 }
413}