Skip to main content

openapi_schema_to_json_schema/
options.rs

1//! Conversion options and their resolution.
2
3use serde_json::Value;
4use std::fmt;
5use std::sync::Arc;
6
7use crate::consts::{NOT_SUPPORTED, STRUCTS};
8
9/// A hook that runs on each node before keyword processing.
10///
11/// It receives the node and the resolved options and returns the node to
12/// process. Returning a different value replaces the node.
13pub type BeforeTransform = Arc<dyn Fn(Value, &ResolvedOptions) -> Value + Send + Sync>;
14
15/// A hook that runs on each node after keyword processing.
16///
17/// Its return value becomes the converted node. At the root the return value
18/// receives the `$schema` member.
19pub type AfterTransform = Arc<dyn Fn(Value, &ResolvedOptions) -> Value + Send + Sync>;
20
21/// A handler that rewrites a node after `x-patternProperties` becomes
22/// `patternProperties`. Its return value replaces the node.
23pub type PatternPropertiesHandler = Arc<dyn Fn(Value) -> Value + Send + Sync>;
24
25/// Caller-facing options. Every field is optional. Unset fields take the
26/// defaults described on [`ResolvedOptions`].
27///
28/// Two ways to set options. Use struct-update syntax against [`Options::new`]:
29///
30/// ```
31/// # use openapi_schema_to_json_schema::Options;
32/// let options = Options {
33///     support_pattern_properties: Some(true),
34///     ..Options::new()
35/// };
36/// ```
37///
38/// Or chain the setters, which take plain values and wrap them for you:
39///
40/// ```
41/// # use openapi_schema_to_json_schema::Options;
42/// let options = Options::new()
43///     .support_pattern_properties(true)
44///     .strict_mode(false);
45/// ```
46#[derive(Clone, Default)]
47pub struct Options {
48    /// Rewrite `format: "date"` to `format: "date-time"`. Default false.
49    pub date_to_date_time: Option<bool>,
50    /// Accepted for compatibility and ignored. Conversion takes the input by
51    /// value and never mutates the caller's data, so the output is the same
52    /// whether this is set or unset. Default true.
53    pub clone_schema: Option<bool>,
54    /// Move `x-patternProperties` to `patternProperties` and run the handler.
55    /// Default false.
56    pub support_pattern_properties: Option<bool>,
57    /// Keywords to keep that would otherwise be stripped. Each entry is removed
58    /// from the strip list. Default empty.
59    pub keep_not_supported: Option<Vec<String>>,
60    /// Reject input `type` values outside the draft-04 type set. Default true.
61    pub strict_mode: Option<bool>,
62    /// Drop object properties marked `readOnly: true`. Default false.
63    pub remove_read_only: Option<bool>,
64    /// Drop object properties marked `writeOnly: true`. Default false.
65    pub remove_write_only: Option<bool>,
66    /// Replacement for the default `patternProperties` handler.
67    pub pattern_properties_handler: Option<PatternPropertiesHandler>,
68    /// Dotted paths whose values hold named sub-schemas to convert, for example
69    /// `"definitions"` or `"schema.definitions"`. Default empty.
70    pub definition_keywords: Option<Vec<String>>,
71    /// Hook run on each node before keyword processing.
72    pub before_transform: Option<BeforeTransform>,
73    /// Hook run on each node after keyword processing.
74    pub after_transform: Option<AfterTransform>,
75}
76
77impl Options {
78    /// Construct empty options. Equivalent to [`Options::default`].
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Set `date_to_date_time`. See the field for meaning.
84    pub fn date_to_date_time(mut self, value: bool) -> Self {
85        self.date_to_date_time = Some(value);
86        self
87    }
88
89    /// Set `clone_schema`. Accepted for compatibility and has no effect.
90    pub fn clone_schema(mut self, value: bool) -> Self {
91        self.clone_schema = Some(value);
92        self
93    }
94
95    /// Set `support_pattern_properties`. See the field for meaning.
96    pub fn support_pattern_properties(mut self, value: bool) -> Self {
97        self.support_pattern_properties = Some(value);
98        self
99    }
100
101    /// Set `keep_not_supported`. See the field for meaning.
102    pub fn keep_not_supported(mut self, value: Vec<String>) -> Self {
103        self.keep_not_supported = Some(value);
104        self
105    }
106
107    /// Set `strict_mode`. See the field for meaning.
108    pub fn strict_mode(mut self, value: bool) -> Self {
109        self.strict_mode = Some(value);
110        self
111    }
112
113    /// Set `remove_read_only`. See the field for meaning.
114    pub fn remove_read_only(mut self, value: bool) -> Self {
115        self.remove_read_only = Some(value);
116        self
117    }
118
119    /// Set `remove_write_only`. See the field for meaning.
120    pub fn remove_write_only(mut self, value: bool) -> Self {
121        self.remove_write_only = Some(value);
122        self
123    }
124
125    /// Set `definition_keywords`. See the field for meaning.
126    pub fn definition_keywords(mut self, value: Vec<String>) -> Self {
127        self.definition_keywords = Some(value);
128        self
129    }
130
131    /// Set the `patternProperties` handler from a plain closure. The closure is
132    /// boxed for you, so callers do not write `Arc::new`.
133    pub fn pattern_properties_handler<F>(mut self, handler: F) -> Self
134    where
135        F: Fn(Value) -> Value + Send + Sync + 'static,
136    {
137        self.pattern_properties_handler = Some(Arc::new(handler));
138        self
139    }
140
141    /// Set the before-transform hook from a plain closure. The closure is boxed
142    /// for you.
143    pub fn before_transform<F>(mut self, hook: F) -> Self
144    where
145        F: Fn(Value, &ResolvedOptions) -> Value + Send + Sync + 'static,
146    {
147        self.before_transform = Some(Arc::new(hook));
148        self
149    }
150
151    /// Set the after-transform hook from a plain closure. The closure is boxed
152    /// for you.
153    pub fn after_transform<F>(mut self, hook: F) -> Self
154    where
155        F: Fn(Value, &ResolvedOptions) -> Value + Send + Sync + 'static,
156    {
157        self.after_transform = Some(Arc::new(hook));
158        self
159    }
160}
161
162impl fmt::Debug for Options {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        f.debug_struct("Options")
165            .field("date_to_date_time", &self.date_to_date_time)
166            .field("clone_schema", &self.clone_schema)
167            .field(
168                "support_pattern_properties",
169                &self.support_pattern_properties,
170            )
171            .field("keep_not_supported", &self.keep_not_supported)
172            .field("strict_mode", &self.strict_mode)
173            .field("remove_read_only", &self.remove_read_only)
174            .field("remove_write_only", &self.remove_write_only)
175            .field(
176                "pattern_properties_handler",
177                &closure_field(self.pattern_properties_handler.is_some()),
178            )
179            .field("definition_keywords", &self.definition_keywords)
180            .field(
181                "before_transform",
182                &closure_field(self.before_transform.is_some()),
183            )
184            .field(
185                "after_transform",
186                &closure_field(self.after_transform.is_some()),
187            )
188            .finish()
189    }
190}
191
192/// Render a closure-bearing field as set or unset for `Debug`.
193fn closure_field(set: bool) -> &'static str {
194    if set {
195        "Some(<closure>)"
196    } else {
197        "None"
198    }
199}
200
201/// Options with defaults applied and internal fields derived.
202///
203/// The crate builds this from [`Options`] and passes a reference to the
204/// `before_transform` and `after_transform` hooks. A hook can read the resolved
205/// settings through the accessors. The fields stay private so the shape can
206/// change without breaking callers.
207///
208/// Defaults:
209///
210/// - `date_to_date_time` false, coerced with truthiness.
211/// - `support_pattern_properties` false, coerced with truthiness.
212/// - `strict_mode` true.
213/// - `definition_keywords` empty.
214#[derive(Clone)]
215pub struct ResolvedOptions {
216    pub(crate) date_to_date_time: bool,
217    pub(crate) support_pattern_properties: bool,
218    pub(crate) strict_mode: bool,
219    pub(crate) definition_keywords: Vec<String>,
220    pub(crate) pattern_properties_handler: Option<PatternPropertiesHandler>,
221    pub(crate) before_transform: Option<BeforeTransform>,
222    pub(crate) after_transform: Option<AfterTransform>,
223    /// Property flags that trigger property removal, in the order
224    /// `readOnly` then `writeOnly`.
225    pub(crate) remove_props: Vec<&'static str>,
226    /// Keywords stripped after transform, in `NOT_SUPPORTED` order minus
227    /// anything kept.
228    pub(crate) not_supported: Vec<&'static str>,
229}
230
231/// Apply defaults and derive internal fields.
232///
233/// Mirrors `resolveOptions`. `date_to_date_time` and
234/// `support_pattern_properties` use truthiness coercion. The rest use nullish
235/// defaults, so an explicit `false` survives.
236pub(crate) fn resolve_options(options: &Options) -> ResolvedOptions {
237    let date_to_date_time = options.date_to_date_time.unwrap_or(false);
238    let support_pattern_properties = options.support_pattern_properties.unwrap_or(false);
239    let keep_not_supported = options.keep_not_supported.clone().unwrap_or_default();
240    let definition_keywords = options.definition_keywords.clone().unwrap_or_default();
241    let strict_mode = options.strict_mode.unwrap_or(true);
242
243    let mut remove_props = Vec::new();
244    if options.remove_read_only.unwrap_or(false) {
245        remove_props.push("readOnly");
246    }
247    if options.remove_write_only.unwrap_or(false) {
248        remove_props.push("writeOnly");
249    }
250
251    let not_supported: Vec<&'static str> = NOT_SUPPORTED
252        .iter()
253        .copied()
254        .filter(|kw| !keep_not_supported.iter().any(|k| k == kw))
255        .collect();
256
257    ResolvedOptions {
258        date_to_date_time,
259        support_pattern_properties,
260        strict_mode,
261        definition_keywords,
262        pattern_properties_handler: options.pattern_properties_handler.clone(),
263        before_transform: options.before_transform.clone(),
264        after_transform: options.after_transform.clone(),
265        remove_props,
266        not_supported,
267    }
268}
269
270impl ResolvedOptions {
271    /// The struct keywords recursed during conversion.
272    pub(crate) const STRUCTS: &'static [&'static str] = STRUCTS;
273
274    /// Whether `format: "date"` is rewritten to `format: "date-time"`.
275    pub fn date_to_date_time(&self) -> bool {
276        self.date_to_date_time
277    }
278
279    /// Whether `x-patternProperties` is moved to `patternProperties`.
280    pub fn support_pattern_properties(&self) -> bool {
281        self.support_pattern_properties
282    }
283
284    /// Whether an invalid input `type` is rejected.
285    pub fn strict_mode(&self) -> bool {
286        self.strict_mode
287    }
288
289    /// The dotted paths whose values hold named sub-schemas to convert.
290    pub fn definition_keywords(&self) -> &[String] {
291        &self.definition_keywords
292    }
293}
294
295impl fmt::Debug for ResolvedOptions {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        f.debug_struct("ResolvedOptions")
298            .field("date_to_date_time", &self.date_to_date_time)
299            .field(
300                "support_pattern_properties",
301                &self.support_pattern_properties,
302            )
303            .field("strict_mode", &self.strict_mode)
304            .field("definition_keywords", &self.definition_keywords)
305            .field(
306                "pattern_properties_handler",
307                &closure_field(self.pattern_properties_handler.is_some()),
308            )
309            .field(
310                "before_transform",
311                &closure_field(self.before_transform.is_some()),
312            )
313            .field(
314                "after_transform",
315                &closure_field(self.after_transform.is_some()),
316            )
317            .field("remove_props", &self.remove_props)
318            .field("not_supported", &self.not_supported)
319            .finish()
320    }
321}