Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adding support to parse discriminator field in ObjectSchema #138

Merged
merged 9 commits into from
Dec 29, 2024
Merged
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"refpath",
"reqwest",
"rustfmt",
"rustup",
"semver",
"serde",
"spdx",
Expand Down
2 changes: 2 additions & 0 deletions crates/oas3/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- Add `spec::ObjectSchema::deprecated` field.
- Add `spec::ObjectSchema::examples` field.
- Add `spec::Contact::validate_email()` method.
- Add `spec::Discriminator` type.
- Add `spec::ObjectSchema::discriminator` field.
- Expose the `spec::ClientCredentialsFlow::token_url` field.
- The type of the `spec::ObjectSchema::enum` field is now `Vec<serde_json::Value>`.
- The type of the `spec::ObjectSchema::const` field is now `Option<serde_json::Value>`.
Expand Down
60 changes: 60 additions & 0 deletions crates/oas3/src/spec/discriminator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Schema specification for [OpenAPI 3.1](/~https://github.com/OAI/OpenAPI-Specification/blob/HEAD/versions/3.1.0.md)

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

/// A discriminator object can be used to aid in serialization, deserialization, and validation when
/// payloads may be one of a number of different schemas.
///
/// The discriminator is a specific object in a schema which is used to inform the consumer of the
/// document of an alternative schema based on the value associated with it.
///
/// See </~https://github.com/OAI/OpenAPI-Specification/blob/HEAD/versions/3.1.0.md#discriminator-object>.
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Discriminator {
/// Name of the property in the payload that will hold the discriminator value.
pub property_name: String,

/// Object to hold mappings between payload values and schema names or references.
///
/// When using the discriminator, inline schemas will not be considered.
#[serde(skip_serializing_if = "Option::is_none")]
pub mapping: Option<BTreeMap<String, String>>,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn discriminator_property_name_parsed_correctly() {
let spec = "propertyName: testName";
let discriminator = serde_yml::from_str::<Discriminator>(spec).unwrap();
assert_eq!("testName", discriminator.property_name);
assert!(discriminator.mapping.is_none());
}

#[test]
fn discriminator_mapping_parsed_correctly() {
let spec = indoc::indoc! {"
propertyName: petType
mapping:
dog: '#/components/schemas/Dog'
cat: '#/components/schemas/Cat'
monster: 'https://gigantic-server.com/schemas/Monster/schema.json'
"};
let discriminator = serde_yml::from_str::<Discriminator>(spec).unwrap();

assert_eq!("petType", discriminator.property_name);
let mapping = discriminator.mapping.unwrap();

assert_eq!("#/components/schemas/Dog", mapping.get("dog").unwrap());
assert_eq!("#/components/schemas/Cat", mapping.get("cat").unwrap());
assert_eq!(
"https://gigantic-server.com/schemas/Monster/schema.json",
mapping.get("monster").unwrap()
);
}
}
2 changes: 2 additions & 0 deletions crates/oas3/src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod components;
mod contact;
mod encoding;

mod discriminator;
mod error;
mod example;
mod external_doc;
Expand All @@ -38,6 +39,7 @@ mod tag;
pub use self::{
components::*,
contact::*,
discriminator::*,
encoding::*,
error::Error,
example::*,
Expand Down
31 changes: 30 additions & 1 deletion crates/oas3/src/spec/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use std::{collections::BTreeMap, fmt};
use derive_more::derive::{Display, Error};
use serde::{Deserialize, Deserializer, Serialize};

use super::{spec_extensions, FromRef, ObjectOrReference, Ref, RefError, RefType, Spec};
use super::{
discriminator::Discriminator, spec_extensions, FromRef, ObjectOrReference, Ref, RefError,
RefType, Spec,
};

/// Schema errors.
#[derive(Debug, Clone, PartialEq, Display, Error)]
Expand Down Expand Up @@ -498,6 +501,12 @@ pub struct ObjectSchema {
// #########################################################################

//
/// Discriminator for object selection based on propertyName
///
/// See </~https://github.com/OAI/OpenAPI-Specification/blob/HEAD/versions/3.1.0.md#discriminator-object>
#[serde(default, skip_serializing_if = "Option::is_none")]
pub discriminator: Option<Discriminator>,

/// A free-form property to include an example of an instance for this schema.
///
/// To represent examples that cannot be naturally represented in JSON or YAML, a string value
Expand Down Expand Up @@ -628,4 +637,24 @@ mod tests {
let schema = serde_yml::from_str::<ObjectSchema>(spec).unwrap();
assert_eq!(schema.example, Some(serde_json::Value::Null));
}

#[test]
fn discriminator_example_is_parsed_correctly() {
let spec = indoc::indoc! {"
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
- $ref: '#/components/schemas/Lizard'
- $ref: 'https://gigantic-server.com/schemas/Monster/schema.json'
discriminator:
propertyName: petType
mapping:
dog: '#/components/schemas/Dog'
monster: 'https://gigantic-server.com/schemas/Monster/schema.json'
"};
let schema = serde_yml::from_str::<ObjectSchema>(spec).unwrap();

assert!(schema.discriminator.is_some());
assert_eq!(2, schema.discriminator.unwrap().mapping.unwrap().len());
}
}
2 changes: 1 addition & 1 deletion tests/samples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fn validate_passing_samples() {
#[test]
fn validate_failing_samples() {
// TODO: implement validation for one-of: [paths, components, webhooks]
// see https://spec.openapis.org/oas/v3.1.0#openapi-document
// see https://github.com/OAI/OpenAPI-Specification/blob/HEAD/versions/3.1.0.md#openapi-document
// oas3::from_str(include_str!("samples/fail/no_containers.yaml")).unwrap_err();

// TODO: implement validation for non-empty server enum
Expand Down
Loading