1use indexmap::IndexMap;
2use itertools::Itertools;
3use ploidy_pointer::JsonPointee;
4
5use crate::parse::{
6 self, Document, Info, Parameter, ParameterLocation, ParameterStyle, RefOrParameter,
7 RefOrRequestBody, RefOrResponse, RefOrSchema, RequestBody, Response,
8};
9
10use super::{
11 error::IrError,
12 transform::transform,
13 types::{
14 InlineIrTypePath, InlineIrTypePathRoot, InlineIrTypePathSegment, IrOperation, IrParameter,
15 IrParameterInfo, IrParameterStyle, IrRequest, IrResponse, IrType, IrTypeName,
16 },
17};
18
19#[derive(Debug)]
20pub struct IrSpec<'a> {
21 pub info: &'a Info,
22 pub operations: Vec<IrOperation<'a>>,
23 pub schemas: IndexMap<&'a str, IrType<'a>>,
24}
25
26impl<'a> IrSpec<'a> {
27 pub fn from_doc(doc: &'a Document) -> Result<Self, IrError> {
28 let schemas = match &doc.components {
29 Some(components) => components
30 .schemas
31 .iter()
32 .map(|(name, schema)| {
33 let ty = transform(doc, IrTypeName::Schema(name), schema);
34 (name.as_str(), ty)
35 })
36 .collect(),
37 None => IndexMap::new(),
38 };
39
40 let operations = doc
41 .paths
42 .iter()
43 .map(|(path, item)| {
44 let segments = parse::path::parse(path.as_str())?;
45 Ok(item
46 .operations()
47 .map(move |(method, op)| (method, segments.clone(), op)))
48 })
49 .flatten_ok()
50 .map_ok(|(method, path, op)| -> Result<_, IrError> {
51 let resource = op.extension("x-resource-name").unwrap_or("full");
52 let id = op.operation_id.as_deref().ok_or(IrError::NoOperationId)?;
53 let params = op
54 .parameters
55 .iter()
56 .filter_map(|param_or_ref| {
57 let param = match param_or_ref {
58 RefOrParameter::Other(p) => p,
59 RefOrParameter::Ref(r) => doc
60 .resolve(r.path.pointer().clone())
61 .ok()
62 .and_then(|p| p.downcast_ref::<Parameter>())?,
63 };
64 let ty = match ¶m.schema {
65 Some(RefOrSchema::Ref(r)) => IrType::Ref(&r.path),
66 Some(RefOrSchema::Other(schema)) => transform(
67 doc,
68 InlineIrTypePath {
69 root: InlineIrTypePathRoot::Resource(resource),
70 segments: vec![
71 InlineIrTypePathSegment::Operation(id),
72 InlineIrTypePathSegment::Parameter(param.name.as_str()),
73 ],
74 },
75 schema,
76 ),
77 None => IrType::Any,
78 };
79 let style = match (param.style, param.explode) {
80 (Some(ParameterStyle::DeepObject), Some(true) | None) => {
81 Some(IrParameterStyle::DeepObject)
82 }
83 (Some(ParameterStyle::SpaceDelimited), Some(false) | None) => {
84 Some(IrParameterStyle::SpaceDelimited)
85 }
86 (Some(ParameterStyle::PipeDelimited), Some(false) | None) => {
87 Some(IrParameterStyle::PipeDelimited)
88 }
89 (Some(ParameterStyle::Form) | None, Some(true) | None) => {
90 Some(IrParameterStyle::Form { exploded: true })
91 }
92 (Some(ParameterStyle::Form) | None, Some(false)) => {
93 Some(IrParameterStyle::Form { exploded: false })
94 }
95 _ => None,
96 };
97 let info = IrParameterInfo {
98 name: param.name.as_str(),
99 ty,
100 required: param.required,
101 description: param.description.as_deref(),
102 style,
103 };
104 Some(match param.location {
105 ParameterLocation::Path => IrParameter::Path(info),
106 ParameterLocation::Query => IrParameter::Query(info),
107 _ => return None,
108 })
109 })
110 .collect_vec();
111
112 let request = op
113 .request_body
114 .as_ref()
115 .and_then(|request_or_ref| {
116 let request = match request_or_ref {
117 RefOrRequestBody::Other(rb) => rb,
118 RefOrRequestBody::Ref(r) => doc
119 .resolve(r.path.pointer().clone())
120 .ok()
121 .and_then(|p| p.downcast_ref::<RequestBody>())?,
122 };
123
124 Some(if request.content.contains_key("multipart/form-data") {
125 RequestContent::Multipart
126 } else if let Some(content) = request.content.get("application/json")
127 && let Some(schema) = &content.schema
128 {
129 RequestContent::Json(schema)
130 } else if let Some(content) = request.content.get("*/*")
131 && let Some(schema) = &content.schema
132 {
133 RequestContent::Json(schema)
134 } else {
135 RequestContent::Any
136 })
137 })
138 .map(|content| match content {
139 RequestContent::Multipart => IrRequest::Multipart,
140 RequestContent::Json(RefOrSchema::Ref(r)) => {
141 IrRequest::Json(IrType::Ref(&r.path))
142 }
143 RequestContent::Json(RefOrSchema::Other(schema)) => {
144 IrRequest::Json(transform(
145 doc,
146 InlineIrTypePath {
147 root: InlineIrTypePathRoot::Resource(resource),
148 segments: vec![
149 InlineIrTypePathSegment::Operation(id),
150 InlineIrTypePathSegment::Request,
151 ],
152 },
153 schema,
154 ))
155 }
156 RequestContent::Any => IrRequest::Json(IrType::Any),
157 });
158
159 let response = {
160 let mut statuses = op
161 .responses
162 .keys()
163 .filter_map(|status| Some((status.as_str(), status.parse::<u16>().ok()?)))
164 .collect_vec();
165 statuses.sort_unstable_by_key(|&(_, code)| code);
166 let key = statuses
167 .iter()
168 .find(|&(_, code)| matches!(code, 200..300))
169 .map(|&(key, _)| key)
170 .unwrap_or("default");
171
172 op.responses
173 .get(key)
174 .and_then(|response_or_ref| {
175 let response = match response_or_ref {
176 RefOrResponse::Other(r) => r,
177 RefOrResponse::Ref(r) => doc
178 .resolve(r.path.pointer().clone())
179 .ok()
180 .and_then(|p| p.downcast_ref::<Response>())?,
181 };
182 response.content.as_ref()
183 })
184 .map(|content| {
185 if let Some(content) = content.get("application/json")
186 && let Some(schema) = &content.schema
187 {
188 ResponseContent::Json(schema)
189 } else if let Some(content) = content.get("*/*")
190 && let Some(schema) = &content.schema
191 {
192 ResponseContent::Json(schema)
193 } else {
194 ResponseContent::Any
195 }
196 })
197 .map(|content| match content {
198 ResponseContent::Json(RefOrSchema::Ref(r)) => {
199 IrResponse::Json(IrType::Ref(&r.path))
200 }
201 ResponseContent::Json(RefOrSchema::Other(schema)) => {
202 IrResponse::Json(transform(
203 doc,
204 InlineIrTypePath {
205 root: InlineIrTypePathRoot::Resource(resource),
206 segments: vec![
207 InlineIrTypePathSegment::Operation(id),
208 InlineIrTypePathSegment::Response,
209 ],
210 },
211 schema,
212 ))
213 }
214 ResponseContent::Any => IrResponse::Json(IrType::Any),
215 })
216 };
217
218 Ok(IrOperation {
219 resource,
220 id,
221 method,
222 path,
223 description: op.description.as_deref(),
224 params,
225 request,
226 response,
227 })
228 })
229 .flatten_ok()
230 .collect::<Result<_, IrError>>()?;
231
232 Ok(IrSpec {
233 info: &doc.info,
234 operations,
235 schemas,
236 })
237 }
238}
239
240#[derive(Clone, Copy, Debug)]
241enum RequestContent<'a> {
242 Multipart,
243 Json(&'a RefOrSchema),
244 Any,
245}
246
247#[derive(Clone, Copy, Debug)]
248enum ResponseContent<'a> {
249 Json(&'a RefOrSchema),
250 Any,
251}