oasert 0.1.3

A library for verifying in-flight requests against a provided OpenAPI 3.1.x or 3.0.x specification.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
use crate::types::Operation;
use crate::validator::{JsonPath, ValidationError, ValidationErrorKind};
use crate::{PATHS_FIELD, PATH_SEPARATOR, REF_FIELD};
use dashmap::{DashMap, Entry};
use serde_json::{Map, Value};
use std::collections::HashSet;
use std::sync::Arc;

type TraverseResult<'a> = Result<SearchResult<'a>, ValidationError>;

#[derive(Debug)]
pub(crate) enum SearchResult<'a> {
    Arc(Arc<Value>),
    Ref(&'a Value),
}

impl<'a> SearchResult<'a> {
    pub(crate) fn value(&'a self) -> &'a Value {
        match self {
            SearchResult::Arc(arc_val) => arc_val,
            SearchResult::Ref(val) => val,
        }
    }
}

pub struct OpenApiTraverser {
    specification: Value,
    resolved_references: DashMap<String, Arc<Value>>,
    resolved_operations: DashMap<(String, String), Arc<Operation>>,
}

impl OpenApiTraverser {
    pub(crate) fn new(specification: Value) -> Self {
        Self {
            specification,
            resolved_references: DashMap::new(),
            resolved_operations: DashMap::new(),
        }
    }

    pub fn specification(&self) -> &Value {
        &self.specification
    }

    pub fn get_operation(
        &self,
        request_path: &str,
        request_method: &str,
    ) -> Result<Arc<Operation>, ValidationError> {
        let binding = request_method.to_lowercase();
        let request_method = binding.as_str();
        println!(
            "Looking for path: {} and method {}",
            request_path, request_method
        );
        log::debug!("Looking for path '{request_path}' and method '{request_method}'");

        let entry = self
            .resolved_operations
            .entry((String::from(request_path), String::from(request_method)));
        match entry {
            Entry::Occupied(e) => Ok(e.get().clone()),
            Entry::Vacant(e) => {
                // Grab all paths from the spec
                if let Ok(spec_paths) =
                    self.get_required_spec_node(&self.specification, PATHS_FIELD)
                {
                    let spec_paths = Self::require_object(spec_paths.value())?;
                    for (spec_path, spec_path_methods) in spec_paths {
                        let operations = Self::require_object(spec_path_methods)?;

                        // Grab the operation matching our request method and test to see if the path matches our request path.
                        // If both method and path match, then we've found the operation associated with the request.
                        if let Some(operation) = operations.get(request_method) {
                            if Self::matches_spec_path(request_path, spec_path) {
                                log::debug!(
                                    "OpenAPI path '{spec_path}' and method '{request_method}' match provided request path '{request_path}' and method '{request_method}'."
                                );
                                let mut json_path = JsonPath::new();
                                json_path
                                    .add(PATHS_FIELD)
                                    .add(spec_path)
                                    .add(request_method);

                                let operation = Arc::new(Operation {
                                    data: operation.clone(),
                                    path: json_path,
                                });

                                if !Self::path_has_parameter(spec_path) {
                                    e.insert(operation.clone());
                                }
                                return Ok(operation);
                            }
                        }
                    }
                }
                Err(ValidationError::MissingOperation)
            }
        }
    }

    fn path_has_parameter(path: &str) -> bool {
        path.contains("{") && path.contains("}")
    }

    /// Determines if a given `path` matches an OpenAPI `specification` `path` pattern.
    ///
    /// This function checks if a request path matches a `specification` path, handling path parameters
    /// enclosed in curly braces (e.g., "/users/{id}").
    ///
    /// # Arguments
    ///
    /// * `path_to_match` - The actual request path to check against the `specification`.
    /// * `spec_path` - The `specification` path pattern that may contain path parameters in the format "{param_name}".
    ///
    /// # Returns
    ///
    /// * `true` if the path matches the `specification` pattern, accounting for path parameters.
    /// * `false` if the path does not match the pattern or has a different number of segments.
    fn matches_spec_path(path_to_match: &str, spec_path: &str) -> bool {
        // If the spec path we are checking contains no path parameters,
        // then we can simply compare path strings.
        if !Self::path_has_parameter(spec_path) {
            spec_path == path_to_match

        // if the request path contains path parameters, we need to compare each segment
        // When we reach a segment that is a parameter, compare the value in the path to the value in the spec.
        } else {
            let target_segments = path_to_match.split(PATH_SEPARATOR).collect::<Vec<&str>>();
            let spec_segments = spec_path.split(PATH_SEPARATOR).collect::<Vec<&str>>();

            if spec_segments.len() != target_segments.len() {
                return false;
            }

            let (matching_segments, segment_count) =
                spec_segments.iter().zip(target_segments.iter()).fold(
                    (0, 0),
                    |(mut matches, mut count), (spec_segment, target_segment)| {
                        count += 1;
                        if let Some(_) = spec_segment.find("{").and_then(|start| {
                            spec_segment
                                .find("}")
                                .map(|end| &spec_segment[start + 1..end])
                        }) {
                            // assume the path param type matches
                            matches += 1;
                        } else if spec_segment == target_segment {
                            matches += 1;
                        }

                        (matches, count)
                    },
                );

            matching_segments == segment_count
        }
    }

    /// Retrieves an optional field from a JSON value in an OpenAPI specification.
    ///
    /// This function attempts to get a specified field from a JSON operation object,
    /// but unlike `get_required_spec_node`, it treats missing fields as valid
    /// (returns None) rather than errors.
    ///
    /// # Arguments
    /// * `operation` - The JSON value (typically an operation object) to search within
    /// * `field` - The name of the optional field to extract
    ///
    /// # Returns
    ///
    /// * `Ok(Some(SearchResult))` - If the field exists, returns a wrapped reference
    ///   to the field value, either as an owned `Arc<Value>` or a borrowed reference
    /// * `Ok(None)` - If the specified field doesn't exist in the value
    /// * `Err(ValidationError)` - For any error other than a missing field
    pub(crate) fn get_optional_spec_node<'a>(
        &'a self,
        node: &'a Value,
        field: &str,
    ) -> Result<Option<SearchResult<'a>>, ValidationError>
    where
        Self: 'a,
    {
        log::trace!(
            "Attempting to find optional field '{}' from '{}'",
            field,
            node.to_string()
        );
        match self.get_required_spec_node(node, field) {
            Ok(security) => Ok(Some(security)),
            Err(e) if e.kind() == ValidationErrorKind::MismatchingSchema => Ok(None),
            Err(e) => Err(e),
        }
    }

    /// Attempts to retrieve a required field from a JSON value, following any references if present.
    ///
    /// # Arguments
    /// * `value` - The JSON value to search within
    /// * `field` - The name of the required field to extract
    ///
    /// # Returns
    /// * `Ok(SearchResult)` - A wrapped reference to the requested field value, either as an owned `Arc<Value>`
    ///   or a borrowed reference
    /// * `Err(ValidationError::FieldMissing)` - If the specified field doesn't exist in the value
    pub(crate) fn get_required_spec_node<'a>(
        &'a self,
        node: &'a Value,
        field: &str,
    ) -> Result<SearchResult<'a>, ValidationError> {
        log::trace!(
            "Attempting to find required field '{}' from '{}'",
            field,
            node.to_string()
        );
        let ref_result = self.resolve_possible_ref(node)?;
        match ref_result {
            SearchResult::Arc(val) => match val.get(field) {
                None => Err(ValidationError::FieldMissing),
                Some(v) => Ok(SearchResult::Arc(Arc::new(v.clone()))),
            },
            SearchResult::Ref(val) => match val.get(field) {
                None => Err(ValidationError::FieldMissing),
                Some(v) => Ok(SearchResult::Ref(v)),
            },
        }
    }

    /// Resolves a JSON node that might contain a `reference` (via "$ref" field).
    ///
    /// # Arguments
    /// * `self` - The OpenApiTraverser instance that contains the `reference` resolution context
    /// * `node` - The JSON value that might contain a `reference` to resolve
    ///
    /// # Returns
    /// * `Ok(SearchResult::Arc)` - If the node contains a `reference` that has been previously resolved
    /// * `Ok(SearchResult::Ref)` - If the node does not contain a `reference`
    /// * `Err(ValidationError)` - If `reference` resolution fails (e.g., circular `reference` or missing field)
    fn resolve_possible_ref<'a>(&'a self, node: &'a Value) -> TraverseResult<'a> {
        if let Ok(ref_string) = Self::get_as_str(node, REF_FIELD) {
            println!("Checking for: {}", ref_string);
            let entry = self.resolved_references.entry(String::from(ref_string));
            return match entry {
                Entry::Occupied(entry) => {
                    println!("Found: {}", ref_string);
                    Ok(SearchResult::Arc(entry.get().clone()))
                }
                Entry::Vacant(entry) => {
                    println!("Ref string {} not found, solving", ref_string);
                    let mut seen_references = HashSet::new();
                    let res = self.get_reference_path(ref_string, &mut seen_references)?;
                    let res = match res {
                        SearchResult::Arc(val) => {
                            let ret = val;
                            entry.insert(ret.clone());
                            ret
                        }
                        SearchResult::Ref(val) => {
                            let res = Arc::new(val.clone());
                            entry.insert(res.clone());
                            res
                        }
                    };
                    println!("Resolved {} with value of {}", ref_string, res);
                    return Ok(SearchResult::Arc(res));
                }
            };
        }

        Ok(SearchResult::Ref(node))
    }

    /// Resolves a `reference` string by navigating through the specification object to find the referenced schema.
    ///
    /// # Arguments
    /// * `ref_string` - A string containing a JSON `reference` path (e.g., "#/components/schemas/Pet")
    /// * `seen_references` - A mutable HashSet tracking references already encountered to detect circular references
    ///
    /// # Returns
    /// * `Ok(SearchResult)` - The resolved schema if the `reference` was successfully resolved
    /// * `Err(ValidationError::CircularReference)` - If a circular `reference` is detected
    /// * `Err(ValidationError::FieldMissing)` - If a path cannot be found in the specification
    fn get_reference_path<'a, 'b>(
        &'a self,
        ref_string: &'a str,
        seen_references: &mut HashSet<&'a str>,
    ) -> TraverseResult<'b>
    where
        'a: 'b,
    {
        if seen_references.contains(ref_string) {
            return Err(ValidationError::CircularReference);
        }
        seen_references.insert(ref_string);
        let mut complete_path = String::from("/");
        let path = ref_string
            .split(PATH_SEPARATOR)
            .filter(|node| !(*node).is_empty() && (*node != "#"))
            .collect::<Vec<&str>>()
            .join("/");
        complete_path.push_str(&path);

        let current_schema = match &self.specification.pointer(&complete_path) {
            None => {
                println!(
                    "Could not find pointer path: {}, {}",
                    path, &self.specification
                );
                return Err(ValidationError::FieldMissing);
            }
            Some(v) => self.resolve_possible_ref(v)?,
        };
        Ok(current_schema)
    }

    /// Retrieves the value of a specified field from a `Value` and attempts to return it as a string.
    ///
    /// # Arguments
    ///
    /// * `node` - A reference to a `Value` object representing the data structure to be queried.
    /// * `field` - A string slice representing the key (field name) to retrieve from the `node`.
    ///
    /// # Returns
    ///
    /// * `Ok(&str)` - If the field exists in the `node` and its value can successfully be interpreted as a string.
    /// * `Err(ValidationError::FieldMissing)` - If the specified field does not exist in the `node`.
    /// * `Err(ValidationError::UnexpectedType)` - If the field exists but its value is not a string.
    fn get_as_str<'a, 'b>(node: &'a Value, field: &str) -> Result<&'b str, ValidationError>
    where
        'a: 'b,
    {
        log::trace!("Grabbing {} from {} as a str.", field, node.to_string());
        match node.get(field) {
            None => Err(ValidationError::FieldMissing),
            Some(found) => Self::require_str(found),
        }
    }

    /// Attempts to convert the provided JSON value into a boolean.
    ///
    /// # Arguments
    ///
    /// * `node` - A reference to a `Value` (from `serde_json`) representing a JSON structure.
    ///   It is expected to be a valid JSON value of type boolean.
    ///
    /// # Returns
    ///
    /// * `Ok(bool)` - If the `Value` provided is a boolean
    /// * `Err(ValidationError::UnexpectedType)` - If the `Value` provided is not a boolean
    pub(crate) fn require_bool<'a, 'b>(node: &'a Value) -> Result<bool, ValidationError>
    where
        'a: 'b,
    {
        match node.as_bool() {
            None => Err(ValidationError::UnexpectedType),
            Some(bool) => Ok(bool),
        }
    }

    /// Attempts to extract a `string` (`&str`) from a JSON `Value`.
    ///
    /// # Arguments
    /// * `node` - A reference to a JSON `Value` from which the function will attempt to extract a `string`.
    ///
    /// # Returns
    /// * `Ok(&str)` - If the `Value` is a `string`.
    /// * `Err(ValidationError::UnexpectedType` - If the `Value` is not a `string`
    pub(crate) fn require_str<'a, 'b>(node: &'a Value) -> Result<&'b str, ValidationError>
    where
        'a: 'b,
    {
        match node.as_str() {
            None => Err(ValidationError::UnexpectedType),
            Some(string) => Ok(string),
        }
    }

    /// Validates and extracts an object from a JSON `Value`.
    ///
    /// # Arguments
    ///
    /// * `node` - A reference to a `Value`, which is expected to be a JSON object.
    ///
    /// # Returns
    ///
    /// * `Ok(&Map<String, Value>)` - If the `Value` is an object
    /// * `Err(ValidationError::UnexpectedType)` - If the `Value` is not an object.
    pub(crate) fn require_object<'a, 'b>(
        node: &'a Value,
    ) -> Result<&'b Map<String, Value>, ValidationError>
    where
        'a: 'b,
    {
        match node.as_object() {
            None => Err(ValidationError::UnexpectedType),
            Some(map) => Ok(map),
        }
    }

    /// Attempts to ensure that a given JSON `Value` is of `array` type.
    ///
    /// # Arguments
    ///
    /// * `node` - A reference to a `Value` (from `serde_json`) that is evaluated to check whether it is an `array`.
    ///
    /// # Returns
    ///
    /// * `Ok(&Vec<Value>)` - If the `node` is an `array`
    /// * `Err(ValidationError::UnexpectedType)` - If the `node` is not an `array`.
    pub(crate) fn require_array<'a, 'b>(node: &'a Value) -> Result<&'b Vec<Value>, ValidationError>
    where
        'a: 'b,
    {
        match node.as_array() {
            None => Err(ValidationError::UnexpectedType),
            Some(array) => Ok(array),
        }
    }
}