crud_api_endpoint/
lib.rs

1//! Tools crate for `crud-api` and `crud` crates.
2//!
3//!
4
5mod api;
6mod api_run;
7mod config;
8mod input;
9mod types;
10
11pub use api::{table_impl, Api, ApiField, ApiVariant, FieldFormat};
12pub use api_run::{ApiInformation, ApiRun};
13pub use config::{arg_config, ApiInputConfig};
14use darling::FromMeta;
15use derive_builder::Builder;
16pub use input::{ApiInputFieldSerde, ApiInputSerde, ApiInputVariantSerde, DataSerde};
17use serde::{Deserialize, Serialize};
18use std::{collections::HashMap, fs::File, io::BufReader, path::PathBuf};
19pub use types::VecStringWrapper;
20
21#[macro_use]
22extern crate lazy_static;
23
24/// Specify an Http endpoint
25#[derive(Debug, Clone, Builder, FromMeta, Serialize, Deserialize)]
26#[builder(setter(into))]
27#[builder(default)]
28#[darling(default)]
29pub struct Endpoint {
30  /// Absolute route as format template
31  /// Variables are written in curly braces `{}`.
32  ///
33  /// Examples:
34  /// ```text
35  /// /root/{id}/sub/{arg}
36  /// ```
37  #[serde(skip_serializing_if = "String::is_empty")]
38  pub route: String,
39  #[serde(skip_serializing_if = "String::is_empty")]
40  pub method: String,
41  #[serde(skip_serializing_if = "Option::is_none")]
42  pub payload_struct: Option<String>,
43  #[serde(skip_serializing_if = "Option::is_none")]
44  pub query_struct: Option<String>,
45  // #[serde(skip_serializing_if = "String::is_empty")]
46  // pub attributes_struct: String,
47  /// Expected status if query is ok
48  #[serde(skip_serializing_if = "String::is_empty")]
49  pub result_ok_status: String,
50  #[darling(multiple)]
51  pub result_ko_status: Vec<EndpointStatus>,
52  #[serde(skip_serializing_if = "String::is_empty")]
53  pub result_struct: String,
54  /// returns a list of results
55  #[darling(rename = "multiple_results")]
56  pub result_multiple: bool,
57  /// returns a stream of bytes for this endpoint
58  /// This flag generates the `--output` arguments.
59  /// This flag disables the `--format` arguments.
60  #[darling(rename = "stream")]
61  pub result_is_stream: bool,
62  /// Add extra header to this endpoint.
63  #[darling(default)]
64  #[darling(multiple)]
65  pub extra_header: Vec<Header>,
66  /// Action to run on the data returned by the http call.
67  /// The signature of the action is: (data:R, settings: &Config)-> Result<()>
68  /// where R is the struct returned by the api.
69  #[serde(skip_serializing_if = "Option::is_none")]
70  pub extra_action: Option<String>,
71  /// This endpoint is not authenticated
72  pub no_auth: bool,
73  /// Transform result from this type.
74  ///
75  /// This type should implement `TryFrom` for `T` and `Vec<T>`.
76  ///
77  /// ```ignore
78  /// #[derive(Debug, Deserialize)]
79  /// struct ApiResult {
80  ///   status: String,
81  ///   detail: Option<String>,
82  ///   result: Option<Vec<MyStruct>>,
83  /// }
84  ///
85  /// impl TryFrom<ApiResult> for Vec<MyStruct> {
86  ///   type Error = String;
87  ///
88  ///   fn try_from(value: ApiResult) -> Result<Self, Self::Error> {
89  ///       // I don't check errors here...
90  ///       Ok(value.result.clone().unwrap_or_default())
91  ///   }
92  /// }
93  ///
94  /// impl TryFrom<ApiResult> for MyStruct {
95  ///   type Error = String;
96  ///
97  ///   fn try_from(value: ApiResult<MyStruct>) -> Result<Self, Self::Error> {
98  ///     if value.status == "ERR" {
99  ///       Err(value.detail.clone().unwrap_or_default())
100  ///     } else {
101  ///       let r = value.result.clone().unwrap_or_default();
102  ///       if r.is_empty() {
103  ///         Ok(MyStruct::default())
104  ///       } else {
105  ///         Ok(r[0].clone())
106  ///       }
107  ///     }
108  ///   }
109  /// }
110  /// ```
111  pub transform_from: Option<String>,
112
113  /// clap route separated by slash (`/`)
114  ///
115  /// Variables should match the variables declared in the `route` configuration.
116  /// ```text
117  /// /command/{id}/subcommand/{arg}
118  /// ```
119  #[serde(skip_serializing_if = "String::is_empty")]
120  pub cli_route: String,
121  /// Short help string for this endpoint
122  #[serde(skip_serializing_if = "Option::is_none")]
123  pub cli_help: Option<String>,
124  /// Long help string for this endpoint.
125  #[serde(skip_serializing_if = "Option::is_none")]
126  pub cli_long_help: Option<String>,
127  #[serde(skip_serializing_if = "Option::is_none")]
128  pub cli_visible_aliases: Option<VecStringWrapper>,
129  #[serde(skip_serializing_if = "Option::is_none")]
130  pub cli_long_flag_aliases: Option<VecStringWrapper>,
131  #[serde(skip_serializing_if = "Option::is_none")]
132  pub cli_aliases: Option<VecStringWrapper>,
133  #[serde(skip_serializing_if = "Option::is_none")]
134  pub cli_short_flag_aliases: Option<VecStringWrapper>,
135  /// This empty have no output to display.
136  /// It can be combined with the `EmptyResponse` result structure.
137  ///
138  /// Examples:
139  /// ```text
140  /// endpoint(
141  ///   result_ok_status = "NO_CONTENT",
142  ///   cli_no_output,
143  ///   result_struct = "EmptyResponse",
144  ///   route = "...",
145  ///   cli_route = "...",
146  /// ),
147  /// ```
148  pub cli_no_output: bool,
149  #[serde(skip_serializing_if = "Option::is_none")]
150  pub cli_output_formats: Option<VecStringWrapper>,
151  /// Force the generation of '--format' args in variable sub command.
152  /// There's cases where the arg is not generated automatically.
153  ///
154  /// Example:
155  /// ```text
156  /// /route/{var}'
157  /// ```
158  /// By default, `{var}` don't generate `--format`.
159  /// If route is just a passthrough, you need the `cli_force_output_format` to generate
160  /// the `--format` args.
161  pub cli_force_output_format: bool,
162
163  #[darling(default)]
164  #[darling(multiple)]
165  pub config: Vec<ApiInputConfig>,
166}
167
168#[derive(Debug, Clone, Default, Builder, FromMeta, Serialize, Deserialize)]
169#[builder(setter(into))]
170#[builder(default)]
171#[darling(default)]
172pub struct EndpointStatus {
173  pub status: String,
174  pub message: String,
175}
176
177#[derive(Debug, Clone, FromMeta, Serialize, Deserialize)]
178pub struct Header {
179  pub key: String,
180  pub value: String,
181}
182
183impl Default for Endpoint {
184  fn default() -> Self {
185    Self {
186      method: "GET".into(),
187      route: Default::default(),
188      payload_struct: Default::default(),
189      query_struct: Default::default(),
190      //      attributes_struct: Default::default(),
191      result_ok_status: "OK".into(),
192      result_ko_status: Default::default(),
193      result_struct: Default::default(),
194      result_multiple: Default::default(),
195      result_is_stream: false,
196      extra_header: Default::default(),
197      extra_action: Default::default(),
198      no_auth: false,
199      transform_from: Default::default(),
200      cli_route: Default::default(),
201      cli_help: Default::default(),
202      cli_long_help: Default::default(),
203      cli_visible_aliases: Default::default(),
204      cli_long_flag_aliases: Default::default(),
205      cli_aliases: Default::default(),
206      cli_short_flag_aliases: Default::default(),
207      cli_output_formats: Default::default(),
208      cli_force_output_format: Default::default(),
209      cli_no_output: Default::default(),
210      config: Default::default(),
211    }
212  }
213}
214
215pub type Emap = HashMap<String, EpNode>;
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct EpNode {
219  pub endpoint: Vec<Endpoint>,
220  pub route: Emap,
221}
222
223fn endpoint_filename() -> PathBuf {
224  let mut dir = scratch::path("crud_api");
225  if let Err(why) = std::fs::create_dir_all(&dir) {
226    panic!(
227      "! Error while creating the endpoints temp dir: {kind:?}",
228      kind = why.kind()
229    );
230  }
231
232  dir.push(format!("endpoints-{}.json", std::process::id()));
233  dir
234}
235
236#[derive(Default, Serialize, Deserialize)]
237struct TmpStore {
238  ep: Emap,
239  inputs: HashMap<String, ApiInputSerde>,
240}
241
242fn load_store() -> TmpStore {
243  match File::open(endpoint_filename()) {
244    Ok(file) => {
245      let reader = BufReader::new(file);
246      let u: TmpStore = serde_json::from_reader(reader).expect("Error reading endpoints.json.");
247      u
248    }
249    Err(_) => TmpStore::default(),
250  }
251}
252
253pub fn input_map() -> HashMap<String, ApiInputSerde> {
254  load_store().inputs
255}
256
257pub fn store_input(input: String, field: impl Into<ApiInputSerde>) {
258  let mut store = load_store();
259  // let mut prefixes = store.inputs.get(&input).unwrap_or(&vec![]).to_owned();
260  // prefixes.push(prefix);
261  store.inputs.insert(input, field.into());
262  let file = File::create(endpoint_filename()).expect("Can't open file in write mode");
263  serde_json::to_writer_pretty(file, &store).unwrap();
264}
265
266pub fn endpoints() -> Emap {
267  load_store().ep
268}
269
270pub fn store_endpoint(epoint: Endpoint) {
271  // OK. That's the best piece of code I ever produce.
272  //  let map: Emap = endpoints();
273  let mut store = load_store();
274  let mut segments: Vec<&str> = epoint.cli_route.split('/').collect();
275  segments.reverse();
276
277  let map = insert_endpoint(store.ep, &epoint, segments);
278
279  let file = File::create(endpoint_filename()).expect("Can't open file in write mode");
280  store.ep = map;
281  serde_json::to_writer_pretty(file, &store).unwrap();
282}
283
284fn insert_endpoint(map: Emap, ep: &Endpoint, mut segments: Vec<&str>) -> Emap {
285  if let Some(segment) = segments.pop() {
286    if segment.is_empty() {
287      return insert_endpoint(map, ep, segments);
288    }
289    let mut map = map;
290    if segments.is_empty() {
291      // We find the leaf
292      if let Some(node) = map.get(segment) {
293        let mut node = node.to_owned();
294        node.endpoint.push(ep.to_owned());
295        map.insert(segment.to_string(), node);
296      } else {
297        let node = EpNode {
298          endpoint: vec![ep.to_owned()],
299          route: HashMap::new(),
300        };
301        map.insert(segment.to_string(), node);
302      }
303      map
304    } else if let Some(node) = map.get(segment) {
305      let mut node = node.to_owned();
306      node.route = insert_endpoint(node.route.to_owned(), ep, segments);
307      map.insert(segment.to_string(), node);
308      map
309    } else {
310      let node = EpNode {
311        endpoint: vec![],
312        route: insert_endpoint(HashMap::new(), ep, segments),
313      };
314      map.insert(segment.to_string(), node);
315      map
316    }
317  } else {
318    map
319  }
320}
321
322#[cfg(test)]
323mod tests {
324  use super::{insert_endpoint, EndpointBuilder};
325  use std::collections::HashMap;
326
327  #[test]
328  fn test_insert_simple_endpoint() {
329    let ep = EndpointBuilder::default()
330      .cli_route("/")
331      .route("/")
332      .build()
333      .unwrap();
334    let mut segments: Vec<&str> = ep.cli_route.split('/').collect::<Vec<&str>>();
335    segments.reverse();
336    let map = HashMap::new();
337    let result = insert_endpoint(map, &ep, segments);
338    assert_eq!(serde_json::to_string(&result).unwrap(), "{}");
339  }
340
341  #[test]
342  fn test_insert_one_endpoint_at_one_level_endpoint() {
343    let ep = EndpointBuilder::default()
344      .cli_route("/post")
345      .route("/post")
346      .build()
347      .unwrap();
348    let mut segments: Vec<&str> = ep.cli_route.split('/').collect();
349    segments.reverse();
350    let map = HashMap::new();
351    let result = insert_endpoint(map, &ep, segments);
352    assert_eq!(serde_json::to_string(&result).unwrap(),"{\"post\":{\"endpoint\":[{\"route\":\"/post\",\"method\":\"GET\",\"result_ok_status\":\"OK\",\"result_ko_status\":[],\"result_multiple\":false,\"result_is_stream\":false,\"extra_header\":[],\"no_auth\":false,\"transform_from\":null,\"cli_route\":\"/post\",\"cli_no_output\":false,\"cli_force_output_format\":false,\"config\":[]}],\"route\":{}}}");
353  }
354
355  #[test]
356  fn test_insert_two_endpoints_at_one_level() {
357    let ep = EndpointBuilder::default()
358      .cli_route("/post")
359      .route("/post")
360      .build()
361      .unwrap();
362    let mut segments: Vec<&str> = ep.cli_route.split('/').collect();
363    segments.reverse();
364    let map = HashMap::new();
365    let map = insert_endpoint(map, &ep, segments);
366
367    let ep = EndpointBuilder::default()
368      .cli_route("/post")
369      .route("/post")
370      .method("POST")
371      .build()
372      .unwrap();
373    let mut segments: Vec<&str> = ep.cli_route.split('/').collect();
374    segments.reverse();
375    let result = insert_endpoint(map, &ep, segments);
376
377    assert_eq!(serde_json::to_string(&result).unwrap(),"{\"post\":{\"endpoint\":[{\"route\":\"/post\",\"method\":\"GET\",\"result_ok_status\":\"OK\",\"result_ko_status\":[],\"result_multiple\":false,\"result_is_stream\":false,\"extra_header\":[],\"no_auth\":false,\"transform_from\":null,\"cli_route\":\"/post\",\"cli_no_output\":false,\"cli_force_output_format\":false,\"config\":[]},{\"route\":\"/post\",\"method\":\"POST\",\"result_ok_status\":\"OK\",\"result_ko_status\":[],\"result_multiple\":false,\"result_is_stream\":false,\"extra_header\":[],\"no_auth\":false,\"transform_from\":null,\"cli_route\":\"/post\",\"cli_no_output\":false,\"cli_force_output_format\":false,\"config\":[]}],\"route\":{}}}");
378  }
379
380  #[test]
381  fn test_insert_three_endpoints_at_two_levels() {
382    let map = HashMap::new();
383    let ep = EndpointBuilder::default()
384      .cli_route("/post")
385      .route("/post")
386      .build()
387      .unwrap();
388    let mut segments: Vec<&str> = ep.cli_route.split('/').collect();
389    segments.reverse();
390    let map = insert_endpoint(map, &ep, segments);
391
392    let ep = EndpointBuilder::default()
393      .cli_route("/post")
394      .route("/post")
395      .method("POST")
396      .build()
397      .unwrap();
398    let mut segments: Vec<&str> = ep.cli_route.split('/').collect();
399    segments.reverse();
400    let map = insert_endpoint(map, &ep, segments);
401
402    let ep = EndpointBuilder::default()
403      .cli_route("/post/user")
404      .route("/post/user")
405      .build()
406      .unwrap();
407    let mut segments: Vec<&str> = ep.cli_route.split('/').collect();
408    segments.reverse();
409    let map = insert_endpoint(map, &ep, segments);
410    assert_eq!(serde_json::to_string(&map).unwrap(),"{\"post\":{\"endpoint\":[{\"route\":\"/post\",\"method\":\"GET\",\"result_ok_status\":\"OK\",\"result_ko_status\":[],\"result_multiple\":false,\"result_is_stream\":false,\"extra_header\":[],\"no_auth\":false,\"transform_from\":null,\"cli_route\":\"/post\",\"cli_no_output\":false,\"cli_force_output_format\":false,\"config\":[]},{\"route\":\"/post\",\"method\":\"POST\",\"result_ok_status\":\"OK\",\"result_ko_status\":[],\"result_multiple\":false,\"result_is_stream\":false,\"extra_header\":[],\"no_auth\":false,\"transform_from\":null,\"cli_route\":\"/post\",\"cli_no_output\":false,\"cli_force_output_format\":false,\"config\":[]}],\"route\":{\"user\":{\"endpoint\":[{\"route\":\"/post/user\",\"method\":\"GET\",\"result_ok_status\":\"OK\",\"result_ko_status\":[],\"result_multiple\":false,\"result_is_stream\":false,\"extra_header\":[],\"no_auth\":false,\"transform_from\":null,\"cli_route\":\"/post/user\",\"cli_no_output\":false,\"cli_force_output_format\":false,\"config\":[]}],\"route\":{}}}}}");
411  }
412
413  #[test]
414  fn test_insert_one_endpoints_at_three_levels() {
415    let map = HashMap::new();
416    let ep = EndpointBuilder::default()
417      .cli_route("/post/comments/replies")
418      .route("/post")
419      .build()
420      .unwrap();
421    let mut segments: Vec<&str> = ep.cli_route.split('/').collect();
422    segments.reverse();
423    let map = insert_endpoint(map, &ep, segments);
424
425    assert_eq!(serde_json::to_string(&map).unwrap(),"{\"post\":{\"endpoint\":[],\"route\":{\"comments\":{\"endpoint\":[],\"route\":{\"replies\":{\"endpoint\":[{\"route\":\"/post\",\"method\":\"GET\",\"result_ok_status\":\"OK\",\"result_ko_status\":[],\"result_multiple\":false,\"result_is_stream\":false,\"extra_header\":[],\"no_auth\":false,\"transform_from\":null,\"cli_route\":\"/post/comments/replies\",\"cli_no_output\":false,\"cli_force_output_format\":false,\"config\":[]}],\"route\":{}}}}}}}");
426  }
427
428  #[test]
429  fn test_endpoint_default() {
430    let ep = EndpointBuilder::default().build().unwrap();
431    assert_eq!(ep.route, "".to_string());
432    assert_eq!(ep.method, "GET".to_string());
433  }
434  #[test]
435  fn test_endpoint_result_struct() {
436    let ep = EndpointBuilder::default()
437      .result_struct("Endpoint")
438      .build()
439      .unwrap();
440    assert_eq!(ep.route, "".to_string());
441    assert_eq!(ep.method, "GET".to_string());
442    assert_eq!(ep.result_struct, "Endpoint".to_string());
443  }
444}