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},
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| segment.fragments())
128 .filter_map(|fragment| match fragment {
129 &PathFragment::Param(name) => Some(name),
130 _ => None,
131 })
132 .filter(|&name| seen.insert(name))
133 .map(|name| {
134 match declared.shift_remove(&(name, ParameterLocation::Path)) {
135 Some(param) => Source::Declared(param),
136 None => Source::Synthesized(name),
137 }
138 })
139 .collect_vec()
140 };
141
142 sources.extend(declared.into_iter().filter_map(|((_, location), param)| {
144 match location {
145 ParameterLocation::Path => None,
148 _ => Some(Source::Declared(param)),
149 }
150 }));
151
152 let params = sources.into_iter().filter_map(|source| match source {
154 Source::Declared(param) => {
155 let ty: &_ = match ¶m.schema {
156 Some(RefOrSchema::Ref(r)) => arena.alloc(SpecType::Ref(r)),
157 Some(RefOrSchema::Inline(schema)) => arena
158 .alloc(transform_with_context(&context, ids.next(), schema)),
159 None => arena.alloc(SpecInlineType::Any(ids.next()).into()),
160 };
161 let style = match (param.style, param.explode) {
162 (Some(ParsedParameterStyle::DeepObject), Some(true) | None) => {
163 Some(IrParameterStyle::DeepObject)
164 }
165 (
166 Some(ParsedParameterStyle::SpaceDelimited),
167 Some(false) | None,
168 ) => Some(IrParameterStyle::SpaceDelimited),
169 (Some(ParsedParameterStyle::PipeDelimited), Some(false) | None) => {
170 Some(IrParameterStyle::PipeDelimited)
171 }
172 (None, None) => None,
173 (Some(ParsedParameterStyle::Form) | None, Some(true) | None) => {
174 Some(IrParameterStyle::Form { exploded: true })
175 }
176 (Some(ParsedParameterStyle::Form) | None, Some(false)) => {
177 Some(IrParameterStyle::Form { exploded: false })
178 }
179 _ => None,
180 };
181 let info = SpecParameterInfo {
182 name: param.name.as_str(),
183 ty,
184 required: param.required,
185 description: param.description.as_deref(),
186 style,
187 };
188 Some(match param.location {
189 ParameterLocation::Path => SpecParameter::Path(info),
190 ParameterLocation::Query => SpecParameter::Query(info),
191 _ => return None,
192 })
193 }
194 Source::Synthesized(name) => {
195 let ty: &_ = arena.alloc(SpecInlineType::Any(ids.next()).into());
196 Some(SpecParameter::Path(SpecParameterInfo {
197 name,
198 ty,
199 required: true,
200 description: None,
201 style: None,
202 }))
203 }
204 });
205
206 arena.alloc_slice(params)
207 };
208
209 let request = item
210 .op
211 .request_body
212 .as_ref()
213 .and_then(|request_or_ref| {
214 let request = match request_or_ref {
215 RefOrRequestBody::Other(rb) => rb,
216 RefOrRequestBody::Ref(r) => {
217 r.ref_.pointer().follow::<&RequestBody>(doc).ok()?
218 }
219 };
220
221 Some(if request.content.contains_key("multipart/form-data") {
222 RequestContent::Multipart
223 } else if let Some(content) = request.content.get("application/json")
224 && let Some(schema) = &content.schema
225 {
226 RequestContent::Json(schema)
227 } else if let Some(content) = request.content.get("*/*")
228 && let Some(schema) = &content.schema
229 {
230 RequestContent::Json(schema)
231 } else {
232 RequestContent::Any
233 })
234 })
235 .map(|content| match content {
236 RequestContent::Multipart => SpecRequest::Multipart,
237 RequestContent::Json(RefOrSchema::Ref(r)) => {
238 SpecRequest::Json(arena.alloc(SpecType::Ref(r)))
239 }
240 RequestContent::Json(RefOrSchema::Inline(schema)) => SpecRequest::Json(
241 arena.alloc(transform_with_context(&context, ids.next(), schema)),
242 ),
243 RequestContent::Any => {
244 SpecRequest::Json(arena.alloc(SpecInlineType::Any(ids.next()).into()))
245 }
246 });
247
248 let response = {
249 let mut statuses = item
250 .op
251 .responses
252 .keys()
253 .filter_map(|status| Some((status.as_str(), status.parse::<u16>().ok()?)))
254 .collect_vec();
255 statuses.sort_unstable_by_key(|&(_, code)| code);
256 let key = statuses
257 .iter()
258 .find(|&(_, code)| matches!(code, 200..300))
259 .map(|&(key, _)| key)
260 .unwrap_or("default");
261
262 item.op
263 .responses
264 .get(key)
265 .and_then(|response_or_ref| {
266 let response = match response_or_ref {
267 RefOrResponse::Other(r) => r,
268 RefOrResponse::Ref(r) => {
269 r.ref_.pointer().follow::<&Response>(doc).ok()?
270 }
271 };
272 response.content.as_ref()
273 })
274 .map(|content| {
275 if let Some(content) = content.get("application/json")
276 && let Some(schema) = &content.schema
277 {
278 ResponseContent::Json(schema)
279 } else if let Some(content) = content.get("*/*")
280 && let Some(schema) = &content.schema
281 {
282 ResponseContent::Json(schema)
283 } else {
284 ResponseContent::Any
285 }
286 })
287 .map(|content| match content {
288 ResponseContent::Json(RefOrSchema::Ref(r)) => {
289 SpecResponse::Json(arena.alloc(SpecType::Ref(r)))
290 }
291 ResponseContent::Json(RefOrSchema::Inline(schema)) => {
292 SpecResponse::Json(arena.alloc(transform_with_context(
293 &context,
294 ids.next(),
295 schema,
296 )))
297 }
298 ResponseContent::Any => SpecResponse::Json(
299 arena.alloc(SpecInlineType::Any(ids.next()).into()),
300 ),
301 })
302 };
303
304 Ok(SpecOperation {
305 resource,
306 id: OperationId::new(id),
307 method: item.method,
308 path: item.path,
309 description: item.op.description.as_deref(),
310 params,
311 request,
312 response,
313 })
314 })
315 .flatten_ok()
316 .collect::<Result<_, IrError>>()?;
317
318 Ok(Spec {
319 info: &doc.info,
320 operations,
321 schemas,
322 ids,
323 })
324 }
325
326 #[inline]
328 pub(super) fn resolve(&'a self, mut ty: &'a SpecType<'a>) -> ResolvedSpecType<'a> {
329 loop {
330 match ty {
331 SpecType::Schema(ty) => return ResolvedSpecType::Schema(ty),
332 SpecType::Inline(ty) => return ResolvedSpecType::Inline(ty),
333 SpecType::Ref(r) => ty = &self.schemas[&*r.name()],
334 }
335 }
336 }
337}
338
339#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
346pub(super) enum ResolvedSpecType<'a> {
347 Schema(&'a SpecSchemaType<'a>),
348 Inline(&'a SpecInlineType<'a>),
349}
350
351#[derive(Clone, Copy, Debug)]
352enum RequestContent<'a> {
353 Multipart,
354 Json(&'a RefOrSchema),
355 Any,
356}
357
358#[derive(Clone, Copy, Debug)]
359enum ResponseContent<'a> {
360 Json(&'a RefOrSchema),
361 Any,
362}
363
364#[derive(Clone, Copy, Debug)]
365struct PathOperation<'a> {
366 path: ParsedPath<'a>,
367 method: Method,
368 params: &'a [RefOrParameter],
369 op: &'a Operation,
370}