1use indexmap::IndexMap;
2use itertools::Itertools;
3use rustc_hash::FxHashSet;
4
5use crate::{
6 arena::Arena,
7 ir::OperationId,
8 parse::{
9 self, Document, Info, Method, Operation, Parameter, ParameterLocation,
10 ParameterStyle as ParsedParameterStyle, RefOrParameter, RefOrRequestBody, RefOrResponse,
11 RefOrSchema, RequestBody, Response,
12 path::{ParsedPath, PathFragment, PathSegment},
13 },
14};
15
16use super::{
17 error::IrError,
18 transform::{TransformContext, TypeInfo, transform_with_context},
19 types::{
20 InlineTypeIds, ParameterStyle as IrParameterStyle, SchemaTypeInfo, SpecInlineType,
21 SpecOperation, SpecParameter, SpecParameterInfo, SpecRequest, SpecResponse, SpecSchemaType,
22 SpecType,
23 },
24};
25
26#[derive(Debug)]
34pub struct Spec<'a> {
35 pub info: &'a Info,
37 pub operations: Vec<SpecOperation<'a>>,
39 pub schemas: IndexMap<&'a str, SpecType<'a>>,
41 pub(crate) ids: InlineTypeIds<'a>,
43}
44
45impl<'a> Spec<'a> {
46 pub fn from_doc(arena: &'a Arena, doc: &'a Document) -> Result<Self, IrError> {
52 let ids = InlineTypeIds::new(arena);
53 let context = TransformContext::new(arena, doc, ids);
54
55 let schemas = match &doc.components {
56 Some(components) => components
57 .schemas
58 .iter()
59 .map(|(name, schema)| {
60 let ty = transform_with_context(
61 &context,
62 TypeInfo::Schema(SchemaTypeInfo {
63 name,
64 resource: schema.extension("x-resourceId"),
65 }),
66 schema,
67 );
68 (name.as_str(), ty)
69 })
70 .collect(),
71 None => IndexMap::new(),
72 };
73
74 let operations = doc
75 .paths
76 .iter()
77 .map(|(path, item)| {
78 let path = parse::path::parse(arena, path.as_str())?;
79 Ok(item.operations().map(move |(method, op)| PathOperation {
80 path,
81 method,
82 params: &item.parameters,
83 op,
84 }))
85 })
86 .flatten_ok()
87 .map_ok(|item| -> Result<_, IrError> {
88 let resource = item.op.extension("x-resource-name");
89 let id = item
90 .op
91 .operation_id
92 .as_deref()
93 .ok_or(IrError::NoOperationId)?;
94
95 let params = {
96 enum Source<'a> {
97 Declared(&'a Parameter),
98 Synthesized(&'a str),
99 }
100
101 let mut declared = IndexMap::new();
104 for param in item
105 .params
106 .iter()
107 .chain(item.op.parameters.iter())
108 .filter_map(|p| match p {
109 RefOrParameter::Other(p) => Some(p),
110 RefOrParameter::Ref(r) => {
111 r.ref_.pointer().follow::<&Parameter>(doc).ok()
112 }
113 })
114 {
115 declared.insert((param.name.as_str(), param.location), param);
116 }
117
118 let mut sources = {
123 let mut seen = FxHashSet::default();
124 item.path
125 .segments
126 .iter()
127 .flat_map(|segment| match segment {
128 &PathSegment::Templated(fragments) => fragments,
129 PathSegment::Literal(_) => &[],
130 })
131 .filter_map(|fragment| match fragment {
132 &PathFragment::Param(name) => Some(name),
133 _ => None,
134 })
135 .filter(|&name| seen.insert(name))
136 .map(|name| {
137 match declared.shift_remove(&(name, ParameterLocation::Path)) {
138 Some(param) => Source::Declared(param),
139 None => Source::Synthesized(name),
140 }
141 })
142 .collect_vec()
143 };
144
145 sources.extend(declared.into_iter().filter_map(|((_, location), param)| {
147 match location {
148 ParameterLocation::Path => None,
151 _ => Some(Source::Declared(param)),
152 }
153 }));
154
155 let params = sources.into_iter().filter_map(|source| match source {
157 Source::Declared(param) => {
158 let ty: &_ = match ¶m.schema {
159 Some(RefOrSchema::Ref(r)) => arena.alloc(SpecType::Ref(r)),
160 Some(RefOrSchema::Inline(schema)) => arena
161 .alloc(transform_with_context(&context, ids.next(), schema)),
162 None => arena.alloc(SpecInlineType::Any(ids.next()).into()),
163 };
164 let style = match (param.style, param.explode) {
165 (Some(ParsedParameterStyle::DeepObject), Some(true) | None) => {
166 Some(IrParameterStyle::DeepObject)
167 }
168 (
169 Some(ParsedParameterStyle::SpaceDelimited),
170 Some(false) | None,
171 ) => Some(IrParameterStyle::SpaceDelimited),
172 (Some(ParsedParameterStyle::PipeDelimited), Some(false) | None) => {
173 Some(IrParameterStyle::PipeDelimited)
174 }
175 (None, None) => None,
176 (Some(ParsedParameterStyle::Form) | None, Some(true) | None) => {
177 Some(IrParameterStyle::Form { exploded: true })
178 }
179 (Some(ParsedParameterStyle::Form) | None, Some(false)) => {
180 Some(IrParameterStyle::Form { exploded: false })
181 }
182 _ => None,
183 };
184 let info = SpecParameterInfo {
185 name: param.name.as_str(),
186 ty,
187 required: param.required,
188 description: param.description.as_deref(),
189 style,
190 };
191 Some(match param.location {
192 ParameterLocation::Path => SpecParameter::Path(info),
193 ParameterLocation::Query => SpecParameter::Query(info),
194 _ => return None,
195 })
196 }
197 Source::Synthesized(name) => {
198 let ty: &_ = arena.alloc(SpecInlineType::Any(ids.next()).into());
199 Some(SpecParameter::Path(SpecParameterInfo {
200 name,
201 ty,
202 required: true,
203 description: None,
204 style: None,
205 }))
206 }
207 });
208
209 arena.alloc_slice(params)
210 };
211
212 let request = item
213 .op
214 .request_body
215 .as_ref()
216 .and_then(|request_or_ref| {
217 let request = match request_or_ref {
218 RefOrRequestBody::Other(rb) => rb,
219 RefOrRequestBody::Ref(r) => {
220 r.ref_.pointer().follow::<&RequestBody>(doc).ok()?
221 }
222 };
223
224 Some(if request.content.contains_key("multipart/form-data") {
225 RequestContent::Multipart
226 } else if let Some(content) = request.content.get("application/json")
227 && let Some(schema) = &content.schema
228 {
229 RequestContent::Json(schema)
230 } else if let Some(content) = request.content.get("*/*")
231 && let Some(schema) = &content.schema
232 {
233 RequestContent::Json(schema)
234 } else {
235 RequestContent::Any
236 })
237 })
238 .map(|content| match content {
239 RequestContent::Multipart => SpecRequest::Multipart,
240 RequestContent::Json(RefOrSchema::Ref(r)) => {
241 SpecRequest::Json(arena.alloc(SpecType::Ref(r)))
242 }
243 RequestContent::Json(RefOrSchema::Inline(schema)) => SpecRequest::Json(
244 arena.alloc(transform_with_context(&context, ids.next(), schema)),
245 ),
246 RequestContent::Any => {
247 SpecRequest::Json(arena.alloc(SpecInlineType::Any(ids.next()).into()))
248 }
249 });
250
251 let response = {
252 let mut statuses = item
253 .op
254 .responses
255 .keys()
256 .filter_map(|status| Some((status.as_str(), status.parse::<u16>().ok()?)))
257 .collect_vec();
258 statuses.sort_unstable_by_key(|&(_, code)| code);
259 let key = statuses
260 .iter()
261 .find(|&(_, code)| matches!(code, 200..300))
262 .map(|&(key, _)| key)
263 .unwrap_or("default");
264
265 item.op
266 .responses
267 .get(key)
268 .and_then(|response_or_ref| {
269 let response = match response_or_ref {
270 RefOrResponse::Other(r) => r,
271 RefOrResponse::Ref(r) => {
272 r.ref_.pointer().follow::<&Response>(doc).ok()?
273 }
274 };
275 response.content.as_ref()
276 })
277 .map(|content| {
278 if let Some(content) = content.get("application/json")
279 && let Some(schema) = &content.schema
280 {
281 ResponseContent::Json(schema)
282 } else if let Some(content) = content.get("*/*")
283 && let Some(schema) = &content.schema
284 {
285 ResponseContent::Json(schema)
286 } else {
287 ResponseContent::Any
288 }
289 })
290 .map(|content| match content {
291 ResponseContent::Json(RefOrSchema::Ref(r)) => {
292 SpecResponse::Json(arena.alloc(SpecType::Ref(r)))
293 }
294 ResponseContent::Json(RefOrSchema::Inline(schema)) => {
295 SpecResponse::Json(arena.alloc(transform_with_context(
296 &context,
297 ids.next(),
298 schema,
299 )))
300 }
301 ResponseContent::Any => SpecResponse::Json(
302 arena.alloc(SpecInlineType::Any(ids.next()).into()),
303 ),
304 })
305 };
306
307 Ok(SpecOperation {
308 resource,
309 id: OperationId::new(id),
310 method: item.method,
311 path: item.path,
312 description: item.op.description.as_deref(),
313 params,
314 request,
315 response,
316 })
317 })
318 .flatten_ok()
319 .collect::<Result<_, IrError>>()?;
320
321 Ok(Spec {
322 info: &doc.info,
323 operations,
324 schemas,
325 ids,
326 })
327 }
328
329 #[inline]
331 pub(super) fn resolve(&'a self, mut ty: &'a SpecType<'a>) -> ResolvedSpecType<'a> {
332 loop {
333 match ty {
334 SpecType::Schema(ty) => return ResolvedSpecType::Schema(ty),
335 SpecType::Inline(ty) => return ResolvedSpecType::Inline(ty),
336 SpecType::Ref(r) => ty = &self.schemas[&*r.name()],
337 }
338 }
339 }
340}
341
342#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
349pub(super) enum ResolvedSpecType<'a> {
350 Schema(&'a SpecSchemaType<'a>),
351 Inline(&'a SpecInlineType<'a>),
352}
353
354#[derive(Clone, Copy, Debug)]
355enum RequestContent<'a> {
356 Multipart,
357 Json(&'a RefOrSchema),
358 Any,
359}
360
361#[derive(Clone, Copy, Debug)]
362enum ResponseContent<'a> {
363 Json(&'a RefOrSchema),
364 Any,
365}
366
367#[derive(Clone, Copy, Debug)]
368struct PathOperation<'a> {
369 path: ParsedPath<'a>,
370 method: Method,
371 params: &'a [RefOrParameter],
372 op: &'a Operation,
373}