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}