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