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::{fmt::Debug, marker::PhantomData};
188
189extern crate crud_api_derive;
190#[doc(hidden)]
191pub mod cli;
192#[doc(hidden)]
193pub mod completions;
194#[doc(hidden)]
195pub mod error;
196mod formats;
197#[doc(hidden)]
198pub mod http;
199#[doc(hidden)]
200pub mod settings;
201
202#[doc(hidden)]
203pub struct ApiInputOptions {
204 pub conflicts_with_all: Vec<Id>,
205}
206
207#[doc(hidden)]
208pub trait ApiInput {
209 /// Generate the clap command declatations.
210 fn clap(app: Command, options: Option<ApiInputOptions>) -> Command;
211 fn from_clap_matches(matches: &ArgMatches) -> Result<Self>
212 where
213 Self: Sized;
214}
215
216#[doc(hidden)]
217#[async_trait]
218pub trait Query {
219 async fn query<P, T, R, Q>(
220 &self,
221 payload: Option<P>,
222 argument: Option<Q>,
223 t: Option<PhantomData<T>>,
224 ) -> Result<R>
225 where
226 P: Send + Serialize + Debug,
227 T: TryInto<R, Error = String> + DeserializeOwned + Send,
228 R: Send + DeserializeOwned + Debug + Default,
229 Q: Send + Serialize + Debug;
230 async fn stream<P, Q>(
231 &self,
232 payload: Option<P>,
233 argument: Option<Q>,
234 filename: Option<String>,
235 ) -> Result<()>
236 where
237 P: Send + Serialize + Debug,
238 Q: Send + Serialize + Debug;
239}
240
241#[doc(hidden)]
242pub trait Api {
243 fn to_table_header(&self) -> Vec<String>;
244 fn to_table(&self) -> Result<Vec<String>>;
245 fn to_output(&self) -> Result<String>;
246
247 #[cfg(any(feature = "json", feature = "toml", feature = "yaml", feature = "csv"))]
248 fn output(&self, format: Option<OutputFormat>) -> Result<()>
249 where
250 Self: Serialize + Debug,
251 {
252 let out = match format {
253 Some(format) => match format {
254 #[cfg(feature = "json")]
255 OutputFormat::Json => Some(serde_json::to_string_pretty(self).into_diagnostic()?),
256 #[cfg(feature = "toml")]
257 OutputFormat::Toml => Some(toml::to_string(self).into_diagnostic()?),
258 #[cfg(feature = "yaml")]
259 OutputFormat::Yaml => Some(serde_yaml::to_string(self).into_diagnostic()?),
260 #[cfg(feature = "csv")]
261 OutputFormat::Csv => {
262 let mut wtr = csv::Writer::from_writer(std::io::stdout());
263 wtr.serialize(self).into_diagnostic()?;
264 wtr.flush().into_diagnostic()?;
265 None
266 }
267 #[cfg(feature = "csv")]
268 OutputFormat::Tsv => {
269 let mut wtr = csv::WriterBuilder::new()
270 .delimiter(b'\t')
271 .quote_style(csv::QuoteStyle::NonNumeric)
272 .from_writer(std::io::stdout());
273 wtr.serialize(self).into_diagnostic()?;
274 wtr.flush().into_diagnostic()?;
275 None
276 }
277 },
278 None => Some(self.to_output()?),
279 };
280
281 if let Some(out) = out {
282 print!("{out}");
283 }
284 Ok(())
285 }
286
287 #[cfg(all(
288 not(feature = "json"),
289 not(feature = "toml"),
290 not(feature = "yaml"),
291 not(feature = "csv")
292 ))]
293 fn output(&self, _format: Option<OutputFormat>) -> Result<()>
294 where
295 Self: Serialize + Debug,
296 {
297 Ok(())
298 }
299
300 #[cfg(any(feature = "json", feature = "toml", feature = "yaml", feature = "csv"))]
301 fn output_multiple(results: &Vec<Self>, format: Option<OutputFormat>) -> Result<()>
302 where
303 Self: Sized + Serialize + Debug,
304 {
305 let out = match format {
306 Some(format) => match format {
307 #[cfg(feature = "json")]
308 OutputFormat::Json => Some(serde_json::to_string_pretty(results).into_diagnostic()?),
309 #[cfg(feature = "toml")]
310 OutputFormat::Toml => Some(toml::to_string(results).into_diagnostic()?),
311 #[cfg(feature = "yaml")]
312 OutputFormat::Yaml => Some(serde_yaml::to_string(results).into_diagnostic()?),
313 #[cfg(feature = "csv")]
314 OutputFormat::Csv => {
315 let mut wtr = csv::Writer::from_writer(std::io::stdout());
316 for result in results {
317 wtr.serialize(result).into_diagnostic()?;
318 }
319 wtr.flush().into_diagnostic()?;
320 None
321 }
322 #[cfg(feature = "csv")]
323 OutputFormat::Tsv => {
324 let mut wtr = csv::WriterBuilder::new()
325 .delimiter(b'\t')
326 .quote_style(csv::QuoteStyle::NonNumeric)
327 .from_writer(std::io::stdout());
328 for result in results {
329 wtr.serialize(result).into_diagnostic()?;
330 }
331 wtr.flush().into_diagnostic()?;
332 None
333 }
334 },
335 None => {
336 if !results.is_empty() {
337 let mut table = vec![results.iter().next().unwrap().to_table_header()];
338 table.append(
339 &mut results
340 .iter()
341 .map(|row| row.to_table().expect("Formating data table"))
342 .collect(),
343 ); // FIXME: replace expect by something better from miette
344 display_table(&table, TableConfig::default());
345 }
346 None
347 }
348 };
349 if let Some(out) = out {
350 println!("{out}");
351 }
352 Ok(())
353 }
354
355 #[cfg(all(
356 not(feature = "json"),
357 not(feature = "toml"),
358 not(feature = "yaml"),
359 not(feature = "csv")
360 ))]
361 fn output_multiple(_results: &Vec<Self>, _format: Option<OutputFormat>) -> Result<()>
362 where
363 Self: Sized + Serialize + Debug,
364 {
365 Ok(())
366 }
367}
368
369/// An empty response. Use it in `result_struct`
370#[derive(Debug, Default, Deserialize, Serialize, PrettyPrint)]
371pub struct EmptyResponse {}
372impl Api for EmptyResponse {
373 fn to_table_header(&self) -> Vec<String> {
374 vec![]
375 }
376
377 fn to_table(&self) -> Result<Vec<String>> {
378 Ok(vec![])
379 }
380
381 fn to_output(&self) -> Result<String> {
382 Ok(String::new())
383 }
384}
385
386#[derive(Deserialize)]
387pub struct DummyTryFrom;
388
389impl TryFrom<DummyTryFrom> for EmptyResponse {
390 type Error = String;
391 fn try_from(_value: DummyTryFrom) -> std::result::Result<Self, Self::Error> {
392 Err(String::new())
393 }
394}
395
396impl<T> TryFrom<DummyTryFrom> for Vec<T> {
397 type Error = String;
398 fn try_from(_value: DummyTryFrom) -> std::result::Result<Self, Self::Error> {
399 Err(String::new())
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 #[test]
406 fn it_works() {
407 let result = 2 + 2;
408 assert_eq!(result, 4);
409 }
410}