ploidy_core/ir/
spec.rs

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 &param.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}