Skip to main content

openapi_forge/
types.rs

1//! `OpenAPI` types -- delegated to sekkei for canonical definitions,
2//! with a compatibility `SchemaOrRef` adapter for openapi-forge consumers.
3
4pub use sekkei::{Components, OpenApiSpec, Operation, PathItem};
5
6// Re-export takumi's FieldType as TypeInfo for backward compatibility.
7pub use takumi::FieldType as TypeInfo;
8
9/// Extract the last segment of a `$ref` path (e.g. `Foo` from `#/components/schemas/Foo`).
10#[must_use]
11pub(crate) fn ref_name_from_path(ref_path: &str) -> Option<&str> {
12    ref_path.rsplit('/').next()
13}
14
15/// Adapter: convert sekkei's flat `Schema` (with optional `ref_path`) to the
16/// `SchemaOrRef` enum pattern used by openapi-forge consumers.
17///
18/// This preserves API compatibility for code that pattern-matches on
19/// `SchemaOrRef::Ref` vs `SchemaOrRef::Schema`.
20#[derive(Debug, Clone)]
21#[non_exhaustive]
22pub enum SchemaOrRef {
23    Ref { ref_path: String },
24    Schema(Box<sekkei::Schema>),
25}
26
27impl SchemaOrRef {
28    /// Extract the schema name from a `$ref` like `#/components/schemas/Foo`.
29    #[must_use]
30    pub fn ref_name(&self) -> Option<&str> {
31        match self {
32            Self::Ref { ref_path } => ref_name_from_path(ref_path),
33            Self::Schema(_) => None,
34        }
35    }
36
37    /// Convert from a sekkei `Schema`.
38    ///
39    /// If the schema has a `ref_path`, it becomes `SchemaOrRef::Ref`;
40    /// otherwise it becomes `SchemaOrRef::Schema`.
41    #[must_use]
42    #[deprecated(since = "0.2.0", note = "use `SchemaOrRef::from(schema)` instead")]
43    pub fn from_schema(schema: &sekkei::Schema) -> Self {
44        Self::from(schema)
45    }
46}
47
48impl From<&sekkei::Schema> for SchemaOrRef {
49    fn from(schema: &sekkei::Schema) -> Self {
50        if let Some(ref_path) = &schema.ref_path {
51            Self::Ref {
52                ref_path: ref_path.clone(),
53            }
54        } else {
55            Self::Schema(Box::new(schema.clone()))
56        }
57    }
58}
59
60/// Alias: `SchemaObject` is now `sekkei::Schema`.
61pub type SchemaObject = sekkei::Schema;
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn ref_name_extracts_last_segment() {
69        let sor = SchemaOrRef::Ref {
70            ref_path: "#/components/schemas/Foo".to_string(),
71        };
72        assert_eq!(sor.ref_name(), Some("Foo"));
73    }
74
75    #[test]
76    fn ref_name_single_segment_ref() {
77        let sor = SchemaOrRef::Ref {
78            ref_path: "JustAName".to_string(),
79        };
80        assert_eq!(sor.ref_name(), Some("JustAName"));
81    }
82
83    #[test]
84    fn ref_name_empty_trailing_slash() {
85        let sor = SchemaOrRef::Ref {
86            ref_path: "#/components/schemas/".to_string(),
87        };
88        assert_eq!(sor.ref_name(), Some(""));
89    }
90
91    #[test]
92    fn ref_name_returns_none_for_schema_variant() {
93        let schema = sekkei::Schema::default();
94        let sor = SchemaOrRef::Schema(Box::new(schema));
95        assert_eq!(sor.ref_name(), None);
96    }
97
98    #[test]
99    #[allow(deprecated)]
100    fn from_schema_with_ref_path_produces_ref_variant() {
101        let schema = sekkei::Schema {
102            ref_path: Some("#/components/schemas/Bar".to_string()),
103            ..sekkei::Schema::default()
104        };
105        let sor = SchemaOrRef::from_schema(&schema);
106        match &sor {
107            SchemaOrRef::Ref { ref_path } => {
108                assert_eq!(ref_path, "#/components/schemas/Bar");
109            }
110            SchemaOrRef::Schema(_) => panic!("expected Ref variant"),
111        }
112    }
113
114    #[test]
115    #[allow(deprecated)]
116    fn from_schema_without_ref_path_produces_schema_variant() {
117        let schema = sekkei::Schema::default();
118        let sor = SchemaOrRef::from_schema(&schema);
119        assert!(matches!(sor, SchemaOrRef::Schema(_)));
120    }
121
122    #[test]
123    #[allow(deprecated)]
124    fn from_schema_preserves_schema_type() {
125        let schema = sekkei::Schema {
126            schema_type: Some("string".to_string()),
127            description: Some("a test field".to_string()),
128            ..sekkei::Schema::default()
129        };
130        let sor = SchemaOrRef::from_schema(&schema);
131        match sor {
132            SchemaOrRef::Schema(s) => {
133                assert_eq!(s.schema_type.as_deref(), Some("string"));
134                assert_eq!(s.description.as_deref(), Some("a test field"));
135            }
136            SchemaOrRef::Ref { .. } => panic!("expected Schema variant"),
137        }
138    }
139
140    #[test]
141    fn schema_or_ref_debug_impl() {
142        let sor = SchemaOrRef::Ref {
143            ref_path: "#/test".to_string(),
144        };
145        let dbg = format!("{sor:?}");
146        assert!(dbg.contains("Ref"));
147        assert!(dbg.contains("#/test"));
148    }
149
150    #[test]
151    fn schema_or_ref_clone() {
152        let sor = SchemaOrRef::Ref {
153            ref_path: "#/components/schemas/Cloned".to_string(),
154        };
155        let cloned = sor.clone();
156        assert_eq!(cloned.ref_name(), Some("Cloned"));
157    }
158
159    #[test]
160    fn ref_name_deeply_nested_path() {
161        let sor = SchemaOrRef::Ref {
162            ref_path: "a/b/c/d/e/DeepName".to_string(),
163        };
164        assert_eq!(sor.ref_name(), Some("DeepName"));
165    }
166
167    #[test]
168    fn from_trait_ref_variant() {
169        let schema = sekkei::Schema {
170            ref_path: Some("#/components/schemas/Baz".to_string()),
171            ..sekkei::Schema::default()
172        };
173        let sor = SchemaOrRef::from(&schema);
174        assert_eq!(sor.ref_name(), Some("Baz"));
175    }
176
177    #[test]
178    fn from_trait_schema_variant() {
179        let schema = sekkei::Schema::default();
180        let sor = SchemaOrRef::from(&schema);
181        assert!(matches!(sor, SchemaOrRef::Schema(_)));
182    }
183
184    #[test]
185    #[allow(deprecated)]
186    fn schema_or_ref_from_schema_ref_name_round_trip() {
187        let schema = sekkei::Schema {
188            ref_path: Some("#/components/schemas/RoundTrip".to_string()),
189            ..sekkei::Schema::default()
190        };
191        let sor = SchemaOrRef::from_schema(&schema);
192        assert_eq!(sor.ref_name(), Some("RoundTrip"));
193    }
194}