fastapi_config/
lib.rs

1#![warn(missing_docs)]
2#![warn(rustdoc::broken_intra_doc_links)]
3#![cfg_attr(doc_cfg, feature(doc_cfg))]
4//! This crate provides global configuration capabilities for [`fastapi`](https://docs.rs/fastapi/latest/fastapi/).
5//!
6//! ## Config options
7//!
8//! * Define rust type aliases for `fastapi` with `.alias_for(...)` method.
9//! * Define schema collect mode for `fastapi` with `.schema_collect(...)` method.
10//!   * [`SchemaCollect::All`] will collect all schemas from usages including inlined with `inline(T)`
11//!   * [`SchemaCollect::NonInlined`] will only collect non inlined schemas from usages.
12//!
13//! <div class="warning">
14//!
15//! <b>Warning!</b><br>
16//! The build config will be stored to projects `OUTPUT` directory. It is then read from there via `OUTPUT` environment
17//! variable which will return **any instance** rust compiler might find at that time (Whatever the `OUTPUT` environment variable points to).
18//! **Be aware** that sometimes you might face a situation where the config is not aligned with your Rust aliases.
19//! This might need you to change something on your code before changed config might apply.
20//!
21//! </div>
22//!
23//! ## Install
24//!
25//! Add dependency declaration to `Cargo.toml`.
26//!
27//! ```toml
28//! [build-dependencies]
29//! fastapi-config = "0.1"
30//! ```
31//!
32//! ## Examples
33//!
34//! _**Create `build.rs` file with following content, then in your code you can just use `MyType` as
35//! alternative for `i32`.**_
36//!
37//! ```rust
38//! # #![allow(clippy::needless_doctest_main)]
39//! use fastapi_config::Config;
40//!
41//! fn main() {
42//!     Config::new()
43//!         .alias_for("MyType", "i32")
44//!         .write_to_file();
45//! }
46//! ```
47//!
48//! See full [example for fastapi-config](https://github.com/nxpkg/fastapi/tree/master/examples/fastapi-config-test/).
49
50use std::borrow::Cow;
51use std::collections::HashMap;
52use std::fs;
53use std::path::PathBuf;
54
55use serde::de::Visitor;
56use serde::{Deserialize, Serialize};
57
58/// Global configuration initialized in `build.rs` of user project.
59///
60/// This works similar fashion to what `hyperium/tonic` grpc library does with the project configuration. See
61/// the quick usage from [module documentation][module]
62///
63/// [module]: ./index.html
64#[derive(Default, Serialize, Deserialize)]
65#[non_exhaustive]
66pub struct Config<'c> {
67    /// A map of global aliases `fastapi` will recognize as types.
68    #[doc(hidden)]
69    pub aliases: HashMap<Cow<'c, str>, Cow<'c, str>>,
70    /// Schema collect mode for `fastapi`. By default only non inlined schemas are collected.
71    pub schema_collect: SchemaCollect,
72}
73
74/// Configures schema collect mode. By default only non explicitly inlined schemas are collected.
75/// but this behavior can be changed to collect also inlined schemas by setting
76/// [`SchemaCollect::All`].
77#[derive(Default)]
78pub enum SchemaCollect {
79    /// Makes sure that all schemas from usages are collected including inlined.
80    All,
81    /// Collect only non explicitly inlined schemas to the OpenAPI. This will result smaller schema
82    /// foot print in the OpenAPI if schemas are typically inlined with `inline(T)` on usage.
83    #[default]
84    NonInlined,
85}
86
87impl Serialize for SchemaCollect {
88    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89    where
90        S: serde::Serializer,
91    {
92        match self {
93            Self::All => serializer.serialize_str("all"),
94            Self::NonInlined => serializer.serialize_str("non_inlined"),
95        }
96    }
97}
98
99impl<'de> Deserialize<'de> for SchemaCollect {
100    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
101    where
102        D: serde::Deserializer<'de>,
103    {
104        struct SchemaCollectVisitor;
105        impl<'d> Visitor<'d> for SchemaCollectVisitor {
106            type Value = SchemaCollect;
107            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
108                formatter.write_str("expected str `all` or `non_inlined`")
109            }
110
111            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
112            where
113                E: serde::de::Error,
114            {
115                if v == "all" {
116                    Ok(SchemaCollect::All)
117                } else {
118                    Ok(SchemaCollect::NonInlined)
119                }
120            }
121        }
122
123        deserializer.deserialize_str(SchemaCollectVisitor)
124    }
125}
126
127impl<'c> Config<'c> {
128    const NAME: &'static str = "fastapi-config.json";
129
130    /// Construct a new [`Config`].
131    pub fn new() -> Self {
132        Self {
133            ..Default::default()
134        }
135    }
136
137    /// Add new global alias.
138    ///
139    /// This method accepts two arguments. First being identifier of the user's type alias.
140    /// Second is the type path definition to be used as alias value. The _`value`_ can be anything
141    /// that `fastapi` can parse as `TypeTree` and can be used as type for a value.
142    ///
143    /// Because of `TypeTree` the aliased value can also be a fairly complex type and not limited
144    /// to primitive types. This also allows users create custom types which can be treated as
145    /// primitive types. E.g. One could create custom date time type that is treated as chrono's
146    /// DateTime or a String.
147    ///
148    /// # Examples
149    ///
150    /// _**Create `MyType` alias for `i32`.**_
151    /// ```rust
152    /// use fastapi_config::Config;
153    ///
154    /// let _ = Config::new()
155    ///     .alias_for("MyType", "i32");
156    /// ```
157    ///
158    /// _**Create `Json` alias for `serde_json::Value`.**_
159    /// ```rust
160    /// use fastapi_config::Config;
161    ///
162    /// let _ = Config::new()
163    ///     .alias_for("Json", "Value");
164    /// ```
165    /// _**Create `NullableString` alias for `Option<String>`.**_
166    /// ```rust
167    /// use fastapi_config::Config;
168    ///
169    /// let _ = Config::new()
170    ///     .alias_for("NullableString", "Option<String>");
171    /// ```
172    pub fn alias_for(mut self, alias: &'c str, value: &'c str) -> Config<'c> {
173        self.aliases
174            .insert(Cow::Borrowed(alias), Cow::Borrowed(value));
175
176        self
177    }
178
179    /// Define schema collect mode for `fastapi`.
180    ///
181    /// Method accepts one argument [`SchemaCollect`] which defines the collect mode to be used by
182    /// `utiopa`. If none is defined [`SchemaCollect::NonInlined`] schemas will be collected by
183    /// default.
184    ///
185    /// This can be changed to [`SchemaCollect::All`] if schemas called with `inline(T)` is wished
186    /// to be collected to the resulting OpenAPI.
187    pub fn schema_collect(mut self, schema_collect: SchemaCollect) -> Self {
188        self.schema_collect = schema_collect;
189
190        self
191    }
192
193    fn get_out_dir() -> Option<String> {
194        match std::env::var("OUT_DIR") {
195            Ok(out_dir) => Some(out_dir),
196            Err(_) => None,
197        }
198    }
199
200    /// Write the current [`Config`] to a file. This persists the [`Config`] for `fastapi` to read
201    /// and use later.
202    pub fn write_to_file(&self) {
203        let json = serde_json::to_string(self).expect("Config must be JSON serializable");
204
205        let Some(out_dir) = Config::get_out_dir() else {
206            return;
207        };
208
209        match fs::write([&*out_dir, Config::NAME].iter().collect::<PathBuf>(), json) {
210            Ok(_) => (),
211            Err(error) => panic!("Failed to write config {}, error: {error}", Config::NAME),
212        };
213    }
214
215    /// Read a [`Config`] from a file. Used internally by `utiopa`.
216    #[doc(hidden)]
217    pub fn read_from_file() -> Config<'c> {
218        let Some(out_dir) = Config::get_out_dir() else {
219            return Config::default();
220        };
221
222        let str = match fs::read_to_string([&*out_dir, Config::NAME].iter().collect::<PathBuf>()) {
223            Ok(str) => str,
224            Err(error) => panic!("Failed to read config: {}, error: {error}", Config::NAME),
225        };
226
227        serde_json::from_str(&str).expect("Config muts be JSON deserializable")
228    }
229}