1use indexmap::IndexMap;
2use itertools::Itertools;
3
4use crate::{
5 arena::Arena,
6 parse::{
7 self, Document, Info, Parameter, ParameterLocation, ParameterStyle as ParsedParameterStyle,
8 RefOrParameter, RefOrRequestBody, RefOrResponse, RefOrSchema, RequestBody, Response,
9 },
10};
11
12use super::{
13 error::IrError,
14 transform::transform,
15 types::{
16 InlineTypePath, InlineTypePathRoot, InlineTypePathSegment,
17 ParameterStyle as IrParameterStyle, SchemaTypeInfo, SpecInlineType, SpecOperation,
18 SpecParameter, SpecParameterInfo, SpecRequest, SpecResponse, SpecSchemaType, SpecType,
19 TypeInfo,
20 },
21};
22
23#[derive(Debug)]
31pub struct Spec<'a> {
32 pub info: &'a Info,
34 pub operations: Vec<SpecOperation<'a>>,
36 pub schemas: IndexMap<&'a str, SpecType<'a>>,
38}
39
40impl<'a> Spec<'a> {
41 pub fn from_doc(arena: &'a Arena, doc: &'a Document) -> Result<Self, IrError> {
47 let schemas = match &doc.components {
48 Some(components) => components
49 .schemas
50 .iter()
51 .map(|(name, schema)| {
52 let ty = transform(
53 arena,
54 doc,
55 TypeInfo::Schema(SchemaTypeInfo {
56 name,
57 resource: schema.extension("x-resourceId"),
58 }),
59 schema,
60 );
61 (name.as_str(), ty)
62 })
63 .collect(),
64 None => IndexMap::new(),
65 };
66
67 let operations = doc
68 .paths
69 .iter()
70 .map(|(path, item)| {
71 let segments = parse::path::parse(arena, path.as_str())?;
72 Ok(item
73 .operations()
74 .map(move |(method, op)| (method, segments.clone(), op)))
75 })
76 .flatten_ok()
77 .map_ok(|(method, path, op)| -> Result<_, IrError> {
78 let resource = op.extension("x-resource-name");
79 let id = op.operation_id.as_deref().ok_or(IrError::NoOperationId)?;
80 let params = arena.alloc_slice(op.parameters.iter().filter_map(|param_or_ref| {
81 let param = match param_or_ref {
82 RefOrParameter::Other(p) => p,
83 RefOrParameter::Ref(r) => {
84 r.path.pointer().follow::<&Parameter>(doc).ok()?
85 }
86 };
87 let ty: &_ = match ¶m.schema {
88 Some(RefOrSchema::Ref(r)) => arena.alloc(SpecType::Ref(&r.path)),
89 Some(RefOrSchema::Other(schema)) => arena.alloc(transform(
90 arena,
91 doc,
92 InlineTypePath {
93 root: InlineTypePathRoot::Resource(resource),
94 segments: arena.alloc_slice_copy(&[
95 InlineTypePathSegment::Operation(id),
96 InlineTypePathSegment::Parameter(param.name.as_str()),
97 ]),
98 },
99 schema,
100 )),
101 None => arena.alloc(
102 SpecInlineType::Any(InlineTypePath {
103 root: InlineTypePathRoot::Resource(resource),
104 segments: arena.alloc_slice_copy(&[
105 InlineTypePathSegment::Operation(id),
106 InlineTypePathSegment::Parameter(param.name.as_str()),
107 ]),
108 })
109 .into(),
110 ),
111 };
112 let style = match (param.style, param.explode) {
113 (Some(ParsedParameterStyle::DeepObject), Some(true) | None) => {
114 Some(IrParameterStyle::DeepObject)
115 }
116 (Some(ParsedParameterStyle::SpaceDelimited), Some(false) | None) => {
117 Some(IrParameterStyle::SpaceDelimited)
118 }
119 (Some(ParsedParameterStyle::PipeDelimited), Some(false) | None) => {
120 Some(IrParameterStyle::PipeDelimited)
121 }
122 (None, None) => None,
123 (Some(ParsedParameterStyle::Form) | None, Some(true) | None) => {
124 Some(IrParameterStyle::Form { exploded: true })
125 }
126 (Some(ParsedParameterStyle::Form) | None, Some(false)) => {
127 Some(IrParameterStyle::Form { exploded: false })
128 }
129 _ => None,
130 };
131 let info = SpecParameterInfo {
132 name: param.name.as_str(),
133 ty,
134 required: param.required,
135 description: param.description.as_deref(),
136 style,
137 };
138 Some(match param.location {
139 ParameterLocation::Path => SpecParameter::Path(info),
140 ParameterLocation::Query => SpecParameter::Query(info),
141 _ => return None,
142 })
143 }));
144
145 let request = op
146 .request_body
147 .as_ref()
148 .and_then(|request_or_ref| {
149 let request = match request_or_ref {
150 RefOrRequestBody::Other(rb) => rb,
151 RefOrRequestBody::Ref(r) => {
152 r.path.pointer().follow::<&RequestBody>(doc).ok()?
153 }
154 };
155
156 Some(if request.content.contains_key("multipart/form-data") {
157 RequestContent::Multipart
158 } else if let Some(content) = request.content.get("application/json")
159 && let Some(schema) = &content.schema
160 {
161 RequestContent::Json(schema)
162 } else if let Some(content) = request.content.get("*/*")
163 && let Some(schema) = &content.schema
164 {
165 RequestContent::Json(schema)
166 } else {
167 RequestContent::Any
168 })
169 })
170 .map(|content| match content {
171 RequestContent::Multipart => SpecRequest::Multipart,
172 RequestContent::Json(RefOrSchema::Ref(r)) => {
173 SpecRequest::Json(arena.alloc(SpecType::Ref(&r.path)))
174 }
175 RequestContent::Json(RefOrSchema::Other(schema)) => {
176 SpecRequest::Json(arena.alloc(transform(
177 arena,
178 doc,
179 InlineTypePath {
180 root: InlineTypePathRoot::Resource(resource),
181 segments: arena.alloc_slice_copy(&[
182 InlineTypePathSegment::Operation(id),
183 InlineTypePathSegment::Request,
184 ]),
185 },
186 schema,
187 )))
188 }
189 RequestContent::Any => SpecRequest::Json(
190 arena.alloc(
191 SpecInlineType::Any(InlineTypePath {
192 root: InlineTypePathRoot::Resource(resource),
193 segments: arena.alloc_slice_copy(&[
194 InlineTypePathSegment::Operation(id),
195 InlineTypePathSegment::Request,
196 ]),
197 })
198 .into(),
199 ),
200 ),
201 });
202
203 let response = {
204 let mut statuses = op
205 .responses
206 .keys()
207 .filter_map(|status| Some((status.as_str(), status.parse::<u16>().ok()?)))
208 .collect_vec();
209 statuses.sort_unstable_by_key(|&(_, code)| code);
210 let key = statuses
211 .iter()
212 .find(|&(_, code)| matches!(code, 200..300))
213 .map(|&(key, _)| key)
214 .unwrap_or("default");
215
216 op.responses
217 .get(key)
218 .and_then(|response_or_ref| {
219 let response = match response_or_ref {
220 RefOrResponse::Other(r) => r,
221 RefOrResponse::Ref(r) => {
222 r.path.pointer().follow::<&Response>(doc).ok()?
223 }
224 };
225 response.content.as_ref()
226 })
227 .map(|content| {
228 if let Some(content) = content.get("application/json")
229 && let Some(schema) = &content.schema
230 {
231 ResponseContent::Json(schema)
232 } else if let Some(content) = content.get("*/*")
233 && let Some(schema) = &content.schema
234 {
235 ResponseContent::Json(schema)
236 } else {
237 ResponseContent::Any
238 }
239 })
240 .map(|content| match content {
241 ResponseContent::Json(RefOrSchema::Ref(r)) => {
242 SpecResponse::Json(arena.alloc(SpecType::Ref(&r.path)))
243 }
244 ResponseContent::Json(RefOrSchema::Other(schema)) => {
245 SpecResponse::Json(arena.alloc(transform(
246 arena,
247 doc,
248 InlineTypePath {
249 root: InlineTypePathRoot::Resource(resource),
250 segments: arena.alloc_slice_copy(&[
251 InlineTypePathSegment::Operation(id),
252 InlineTypePathSegment::Response,
253 ]),
254 },
255 schema,
256 )))
257 }
258 ResponseContent::Any => SpecResponse::Json(
259 arena.alloc(
260 SpecInlineType::Any(InlineTypePath {
261 root: InlineTypePathRoot::Resource(resource),
262 segments: arena.alloc_slice_copy(&[
263 InlineTypePathSegment::Operation(id),
264 InlineTypePathSegment::Response,
265 ]),
266 })
267 .into(),
268 ),
269 ),
270 })
271 };
272
273 Ok(SpecOperation {
274 resource,
275 id,
276 method,
277 path: arena.alloc_slice_copy(&path),
278 description: op.description.as_deref(),
279 params,
280 request,
281 response,
282 })
283 })
284 .flatten_ok()
285 .collect::<Result<_, IrError>>()?;
286
287 Ok(Spec {
288 info: &doc.info,
289 operations,
290 schemas,
291 })
292 }
293
294 #[inline]
296 pub(super) fn resolve(&'a self, mut ty: &'a SpecType<'a>) -> ResolvedSpecType<'a> {
297 loop {
298 match ty {
299 SpecType::Schema(ty) => return ResolvedSpecType::Schema(ty),
300 SpecType::Inline(ty) => return ResolvedSpecType::Inline(ty),
301 SpecType::Ref(r) => ty = &self.schemas[&*r.name()],
302 }
303 }
304 }
305}
306
307#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
314pub(super) enum ResolvedSpecType<'a> {
315 Schema(&'a SpecSchemaType<'a>),
316 Inline(&'a SpecInlineType<'a>),
317}
318
319#[derive(Clone, Copy, Debug)]
320enum RequestContent<'a> {
321 Multipart,
322 Json(&'a RefOrSchema),
323 Any,
324}
325
326#[derive(Clone, Copy, Debug)]
327enum ResponseContent<'a> {
328 Json(&'a RefOrSchema),
329 Any,
330}