use crate::{
error::{Error, ErrorCode},
types::notification::Notification,
types::{ErrorDetails, IntoResponse, PropertyType, RequestId, Response, Schema},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::Value;
use std::collections::HashMap;
#[cfg(feature = "client")]
use std::{future::Future, pin::Pin, sync::Arc};
use crate::types::Uri;
#[cfg(feature = "tasks")]
use crate::types::{RelatedTaskMetadata, TaskMetadata};
pub mod commands {
pub const CREATE: &str = "elicitation/create";
pub const COMPLETE: &str = "notifications/elicitation/complete";
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ElicitRequestParams {
Form(ElicitRequestFormParams),
Url(ElicitRequestUrlParams),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitRequestFormParams {
pub message: String,
pub mode: Option<ElicitationMode>,
#[serde(rename = "requestedSchema")]
pub schema: RequestSchema,
#[cfg(feature = "tasks")]
#[serde(skip_serializing_if = "Option::is_none")]
pub task: Option<TaskMetadata>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitRequestUrlParams {
#[serde(rename = "elicitationId")]
pub id: String,
pub message: String,
pub mode: ElicitationMode,
pub url: Uri,
#[cfg(feature = "tasks")]
#[serde(skip_serializing_if = "Option::is_none")]
pub task: Option<TaskMetadata>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ElicitationMode {
Form,
Url,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestSchema {
#[serde(rename = "type", default)]
pub r#type: PropertyType,
pub properties: HashMap<String, Schema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitResult {
pub action: ElicitationAction,
pub content: Option<Value>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ElicitationAction {
Accept,
Cancel,
Decline,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UrlElicitationRequiredError {
pub elicitations: Vec<ElicitRequestUrlParams>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElicitationCompleteParams {
#[serde(rename = "elicitationId")]
pub id: String,
}
#[derive(Debug)]
pub struct Validator {
schema: RequestSchema,
}
impl From<ElicitRequestFormParams> for ElicitRequestParams {
#[inline]
fn from(value: ElicitRequestFormParams) -> Self {
Self::Form(value)
}
}
impl From<ElicitRequestUrlParams> for ElicitRequestParams {
#[inline]
fn from(value: ElicitRequestUrlParams) -> Self {
Self::Url(value)
}
}
impl Default for RequestSchema {
#[inline]
fn default() -> Self {
Self {
r#type: PropertyType::Object,
properties: HashMap::with_capacity(8),
required: None,
}
}
}
impl Validator {
#[inline]
pub fn new(params: ElicitRequestFormParams) -> Self {
Self {
schema: params.schema,
}
}
#[inline]
pub fn validate<T: Serialize + JsonSchema>(&self, content: T) -> Result<Value, Error> {
let source_schema = schemars::schema_for!(T);
self.validate_schema_compatibility(&source_schema)?;
serde_json::to_value(&content)
.map_err(Error::from)
.and_then(|c| self.validate_content_constraints(&c).map(|_| c))
}
fn validate_schema_compatibility(&self, source: &schemars::Schema) -> Result<(), Error> {
const PROP: &str = "properties";
const REQ: &str = "required";
let target = &self.schema;
let source_props = source
.get(PROP)
.and_then(|v| v.as_object())
.ok_or(Error::new(
ErrorCode::InvalidParams,
"Source schema missing properties",
))?;
let source_required = source
.get(REQ)
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
.unwrap_or_default();
for prop_name in target.properties.keys() {
if !source_props.contains_key(prop_name) {
return Err(Error::new(
ErrorCode::InvalidParams,
format!("Missing property: {prop_name}"),
));
}
}
if let Some(target_required) = &target.required {
for required_prop in target_required {
if !source_required.contains(&required_prop.as_str()) {
return Err(Error::new(
ErrorCode::InvalidParams,
format!("Required property not marked as required: {required_prop}"),
));
}
}
}
Ok(())
}
fn validate_content_constraints(&self, content: &Value) -> Result<(), Error> {
let schema = &self.schema;
let content_obj = content.as_object().ok_or(Error::new(
ErrorCode::InvalidParams,
"Content is not an object",
))?;
if let Some(required) = &schema.required {
for required_prop in required {
if !content_obj.contains_key(required_prop) {
return Err(Error::new(
ErrorCode::InvalidParams,
format!("Missing required property: {required_prop}"),
));
}
}
}
for (prop_name, prop_schema) in &schema.properties {
if let Some(prop_value) = content_obj.get(prop_name) {
self.validate_property_value(prop_value, prop_schema)?;
}
}
Ok(())
}
#[inline]
fn validate_property_value(&self, value: &Value, schema: &Schema) -> Result<(), Error> {
match schema {
Schema::String(string_schema) => string_schema.validate(value),
Schema::Number(number_schema) => number_schema.validate(value),
Schema::Boolean(boolean_schema) => boolean_schema.validate(value),
Schema::SingleUntitledEnum(e) => e.validate(value),
Schema::SingleTitledEnum(e) => e.validate(value),
Schema::MultiUntitledEnum(e) => e.validate(value),
Schema::MultiTitledEnum(e) => e.validate(value),
Schema::LegacyEnum(e) => e.validate(value),
}
}
}
impl ElicitRequestParams {
#[inline]
pub fn form(message: impl Into<String>) -> ElicitRequestFormParams {
ElicitRequestFormParams {
message: message.into(),
schema: RequestSchema::new(),
mode: None,
meta: None,
#[cfg(feature = "tasks")]
task: None,
}
}
#[inline]
pub fn url(url: impl Into<Uri>, message: impl Into<String>) -> ElicitRequestUrlParams {
ElicitRequestUrlParams {
id: uuid::Uuid::new_v4().to_string(),
message: message.into(),
url: url.into(),
mode: ElicitationMode::Url,
meta: None,
#[cfg(feature = "tasks")]
task: None,
}
}
#[inline]
pub fn as_form(&self) -> Option<&ElicitRequestFormParams> {
match self {
Self::Form(params) => Some(params),
_ => None,
}
}
#[inline]
pub fn as_url(&self) -> Option<&ElicitRequestUrlParams> {
match self {
Self::Url(params) => Some(params),
_ => None,
}
}
#[inline]
pub fn into_form(self) -> Result<ElicitRequestFormParams, Error> {
match self {
Self::Form(params) => Ok(params),
_ => Err(Error::new(
ErrorCode::InvalidRequest,
"Request is not a form request",
)),
}
}
#[inline]
pub fn into_url(self) -> Result<ElicitRequestUrlParams, Error> {
match self {
Self::Url(params) => Ok(params),
_ => Err(Error::new(
ErrorCode::InvalidRequest,
"Request is not a URL request",
)),
}
}
#[inline]
#[cfg(feature = "tasks")]
pub fn with_related_task(self, task_id: impl Into<String>) -> Self {
match self {
Self::Form(form) => form.with_related_task(task_id).into(),
Self::Url(url) => url.with_related_task(task_id).into(),
}
}
#[inline]
#[cfg(feature = "tasks")]
pub fn is_task_augmented(&self) -> bool {
self.as_url().is_some_and(|p| p.task.is_some())
}
#[inline]
#[cfg(feature = "tasks")]
pub fn related_task(&self) -> Option<RelatedTaskMetadata> {
match self {
Self::Form(form) => form.related_task(),
Self::Url(url) => url.related_task(),
}
}
}
impl ElicitRequestFormParams {
#[inline]
pub fn with_prop(mut self, prop: &str, schema: impl Into<Schema>) -> Self {
self.schema = self.schema.with_prop(prop, schema);
self
}
#[inline]
pub fn with_required(mut self, prop: &str, schema: impl Into<Schema>) -> Self {
self.schema = self.schema.with_required(prop, schema);
self
}
#[inline]
pub fn with_schema<T: JsonSchema>(mut self) -> Self {
self.schema = RequestSchema::of::<T>();
self
}
#[inline]
#[cfg(feature = "tasks")]
pub fn with_related_task(mut self, task: impl Into<RelatedTaskMetadata>) -> Self {
let meta: RelatedTaskMetadata = task.into();
let meta = serde_json::to_value(meta).unwrap();
self.meta
.get_or_insert_with(|| serde_json::json!({}))
.as_object_mut()
.unwrap()
.insert(crate::types::task::RELATED_TASK_KEY.into(), meta);
self
}
#[inline]
#[cfg(feature = "tasks")]
pub fn related_task(&self) -> Option<RelatedTaskMetadata> {
self.meta
.as_ref()
.and_then(|m| m.as_object())
.and_then(|m| m.get(crate::types::task::RELATED_TASK_KEY))
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
}
#[cfg(feature = "tasks")]
impl ElicitRequestUrlParams {
pub fn with_ttl(mut self, ttl: Option<usize>) -> Self {
self.task = Some(TaskMetadata { ttl });
self
}
#[inline]
#[cfg(feature = "tasks")]
pub fn with_related_task(mut self, task: impl Into<RelatedTaskMetadata>) -> Self {
let meta: RelatedTaskMetadata = task.into();
let meta = serde_json::to_value(meta).unwrap();
self.meta
.get_or_insert_with(|| serde_json::json!({}))
.as_object_mut()
.unwrap()
.insert(crate::types::task::RELATED_TASK_KEY.into(), meta);
self
}
#[inline]
#[cfg(feature = "tasks")]
pub fn related_task(&self) -> Option<RelatedTaskMetadata> {
self.meta
.as_ref()
.and_then(|m| m.as_object())
.and_then(|m| m.get(crate::types::task::RELATED_TASK_KEY))
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
}
impl RequestSchema {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn of<T: JsonSchema>() -> Self {
let mut schema = Self::default();
let json_schema = schemars::schema_for!(T);
let required = json_schema.get("required").and_then(|v| v.as_array());
if let Some(props) = json_schema.get("properties").and_then(|v| v.as_object()) {
for (field, def) in props {
let req = required
.map(|arr| !arr.iter().any(|v| v == field))
.unwrap_or(true);
schema = if req {
schema.with_required(field, Schema::from(def))
} else {
schema.with_prop(field, Schema::from(def))
}
}
}
schema
}
#[inline]
pub fn with_prop(mut self, prop: &str, schema: impl Into<Schema>) -> Self {
self.properties.insert(prop.into(), schema.into());
self
}
#[inline]
pub fn with_required(mut self, prop: &str, schema: impl Into<Schema>) -> Self {
self = self.with_prop(prop, schema);
self.required.get_or_insert_with(Vec::new).push(prop.into());
self
}
}
impl ElicitResult {
#[inline]
pub fn accept() -> Self {
Self {
action: ElicitationAction::Accept,
content: None,
meta: None,
}
}
#[inline]
pub fn decline() -> Self {
Self {
action: ElicitationAction::Decline,
content: None,
meta: None,
}
}
#[inline]
pub fn cancel() -> Self {
Self {
action: ElicitationAction::Cancel,
content: None,
meta: None,
}
}
#[inline]
pub fn with_content<T: Serialize>(mut self, content: T) -> Self {
self.content = Some(serde_json::to_value(&content).unwrap());
self
}
#[inline]
pub fn content<T: DeserializeOwned>(&self) -> Option<T> {
self.content
.as_ref()
.and_then(|content| serde_json::from_value(content.clone()).ok())
}
pub fn is_accepted(&self) -> bool {
self.action == ElicitationAction::Accept
}
pub fn is_canceled(&self) -> bool {
self.action == ElicitationAction::Cancel
}
pub fn is_declined(&self) -> bool {
self.action == ElicitationAction::Decline
}
pub fn map<T, U, F>(&self, f: F) -> Result<U, Error>
where
T: DeserializeOwned,
F: FnOnce(T) -> U,
{
if self.is_accepted() {
self.content::<T>()
.ok_or_else(|| Error::new(ErrorCode::ParseError, "Failed to parse content"))
.map(f)
} else {
Err(Error::new(
ErrorCode::InvalidRequest,
"User rejected the request",
))
}
}
pub fn map_err<T, F>(&self, f: F) -> Result<T, Error>
where
T: DeserializeOwned,
F: FnOnce() -> Error,
{
if self.is_accepted() {
self.content::<T>()
.ok_or_else(|| Error::new(ErrorCode::ParseError, "Failed to parse content"))
} else {
Err(f())
}
}
#[inline]
#[cfg(feature = "tasks")]
pub fn with_related_task(mut self, task: impl Into<RelatedTaskMetadata>) -> Self {
let meta: RelatedTaskMetadata = task.into();
let meta = serde_json::to_value(meta).unwrap();
self.meta
.get_or_insert_with(|| serde_json::json!({}))
.as_object_mut()
.unwrap()
.insert(crate::types::task::RELATED_TASK_KEY.into(), meta);
self
}
#[inline]
#[cfg(feature = "tasks")]
pub fn related_task(&self) -> Option<RelatedTaskMetadata> {
self.meta
.as_ref()
.and_then(|m| m.as_object())
.and_then(|m| m.get(crate::types::task::RELATED_TASK_KEY))
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
}
impl UrlElicitationRequiredError {
#[inline]
pub fn new(elicitations: impl IntoIterator<Item = ElicitRequestUrlParams>) -> Self {
Self {
elicitations: elicitations.into_iter().collect(),
}
}
#[inline]
pub fn to_error(self, message: impl Into<String>) -> Error {
let err = match serde_json::to_value(self) {
Ok(data) => ErrorDetails {
code: ErrorCode::UrlElicitationRequiredError,
message: message.into(),
data: Some(data),
},
Err(err) => ErrorDetails {
code: ErrorCode::InternalError,
message: err.to_string(),
data: None,
},
};
err.into()
}
}
impl ElicitationCompleteParams {
#[inline]
pub fn new(id: impl Into<String>) -> Self {
Self { id: id.into() }
}
}
impl TryFrom<Notification> for ElicitationCompleteParams {
type Error = Error;
#[inline]
fn try_from(value: Notification) -> Result<Self, Self::Error> {
let params = value
.params
.ok_or_else(|| Error::new(ErrorCode::InvalidParams, "Missing params"))?;
serde_json::from_value(params).map_err(Error::from)
}
}
impl From<Result<Value, Error>> for ElicitResult {
fn from(result: Result<Value, Error>) -> Self {
match result {
Ok(content) => ElicitResult::accept().with_content(content),
Err(err) => ElicitResult::decline().with_content(err.to_string()),
}
}
}
impl IntoResponse for ElicitResult {
#[inline]
fn into_response(self, req_id: RequestId) -> Response {
match serde_json::to_value(self) {
Ok(v) => Response::success(req_id, v),
Err(err) => Response::error(req_id, err.into()),
}
}
}
#[cfg(feature = "client")]
pub(crate) type ElicitationHandler = Arc<
dyn Fn(ElicitRequestParams) -> Pin<Box<dyn Future<Output = ElicitResult> + Send + 'static>>
+ Send
+ Sync,
>;
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{
BooleanSchema, NumberSchema, StringFormat, StringSchema, UntitledSingleSelectEnumSchema,
};
use schemars::JsonSchema;
#[derive(Serialize, JsonSchema)]
struct TestStruct {
name: String,
age: u32,
active: bool,
}
fn create_test_schema() -> RequestSchema {
let mut schema = RequestSchema::new();
schema.properties.insert(
"name".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: Some(2),
max_length: Some(50),
format: None,
}),
);
schema.properties.insert(
"age".to_string(),
Schema::Number(NumberSchema {
r#type: PropertyType::Number,
title: None,
descr: None,
min: Some(0.0),
max: Some(120.0),
}),
);
schema.properties.insert(
"active".to_string(),
Schema::Boolean(BooleanSchema::default()),
);
schema.required = Some(vec!["name".to_string(), "age".to_string()]);
schema
}
fn create_form_params_with_schema(schema: RequestSchema) -> ElicitRequestFormParams {
ElicitRequestFormParams {
message: "Test message".to_string(),
mode: None,
meta: None,
#[cfg(feature = "tasks")]
task: None,
schema,
}
}
#[test]
fn it_creates_validator_for_params_with_schema() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema.clone());
let validator = Validator::new(params);
assert_eq!(validator.schema.properties.len(), schema.properties.len());
assert_eq!(validator.schema.required, schema.required);
}
#[test]
fn it_validates_compatible_schema_success() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content = TestStruct {
name: "John Doe".to_string(),
age: 30,
active: true,
};
let result = validator.validate(content);
assert!(result.is_ok());
let json_value = result.unwrap();
assert_eq!(json_value["name"], "John Doe");
assert_eq!(json_value["age"], 30);
assert_eq!(json_value["active"], true);
}
#[test]
fn it_validates_missing_property_in_source() {
let mut schema = create_test_schema();
schema.properties.insert(
"missing_prop".to_string(),
Schema::String(StringSchema::default()),
);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content = TestStruct {
name: "John Doe".to_string(),
age: 30,
active: true,
};
let result = validator.validate(content);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.code, ErrorCode::InvalidParams);
assert!(error.to_string().contains("Missing property: missing_prop"));
}
#[test]
fn it_validates_missing_required_property() {
let mut schema = create_test_schema();
schema.required = Some(vec![
"name".to_string(),
"age".to_string(),
"missing_required".to_string(),
]);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content = TestStruct {
name: "John Doe".to_string(),
age: 30,
active: true,
};
let result = validator.validate(content);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.code, ErrorCode::InvalidParams);
assert!(
error
.to_string()
.contains("Required property not marked as required")
);
}
#[test]
fn it_validates_content_constraints_missing_required() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"active": true
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.code, ErrorCode::InvalidParams);
assert!(error.to_string().contains("Missing required property"));
}
#[test]
fn it_validates_content_constraints_not_object() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!("not an object");
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.code, ErrorCode::InvalidParams);
assert!(error.to_string().contains("Content is not an object"));
}
#[test]
fn it_validates_string_property_success() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"name": "John",
"age": 25,
"active": true
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_string_property_too_short() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"name": "J", "age": 25,
"active": true
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("String too short: 1 < 2"));
}
#[test]
fn it_validates_string_property_too_long() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let long_name = "a".repeat(51); let content_json = serde_json::json!({
"name": long_name,
"age": 25,
"active": true
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("String too long: 51 > 50"));
}
#[test]
fn it_validates_string_property_invalid_type() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"name": 123, "age": 25,
"active": true
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Expected string value"));
}
#[test]
fn it_validates_number_property_success() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"name": "John",
"age": 50, "active": true
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_number_property_too_small() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"name": "John",
"age": -5, "active": true
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Number too small: -5 < 0"));
}
#[test]
fn it_validates_number_property_too_large() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"name": "John",
"age": 150, "active": true
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Number too large: 150 > 120"));
}
#[test]
fn it_validatess_number_property_invalid_type() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"name": "John",
"age": "not a number", "active": true
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Expected number value"));
}
#[test]
fn it_validates_boolean_property_success() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"name": "John",
"age": 25,
"active": false
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_boolean_property_invalid_type() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"name": "John",
"age": 25,
"active": "not a boolean" });
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Expected boolean value"));
}
#[test]
fn it_validates_enum_property_success() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"status".to_string(),
Schema::SingleUntitledEnum(UntitledSingleSelectEnumSchema {
r#type: PropertyType::String,
title: None,
descr: None,
r#enum: vec![
"active".to_string(),
"inactive".to_string(),
"pending".to_string(),
],
default: None,
}),
);
schema.required = Some(vec!["status".to_string()]);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"status": "active"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_enum_property_invalid_value() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"status".to_string(),
Schema::SingleUntitledEnum(UntitledSingleSelectEnumSchema {
r#type: PropertyType::String,
title: None,
descr: None,
r#enum: vec!["active".to_string(), "inactive".to_string()],
default: None,
}),
);
schema.required = Some(vec!["status".to_string()]);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"status": "invalid_status"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(
error
.to_string()
.contains("Invalid enum value: invalid_status")
);
}
#[test]
fn it_validates_enum_property_invalid_type() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"status".to_string(),
Schema::SingleUntitledEnum(UntitledSingleSelectEnumSchema {
r#type: PropertyType::String,
title: None,
descr: None,
r#enum: vec!["active".to_string(), "inactive".to_string()],
default: None,
}),
);
schema.required = Some(vec!["status".to_string()]);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"status": 123 });
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Expected string value for enum"));
}
#[test]
fn it_validates_string_format_email_success() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"email".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: None,
max_length: None,
format: Some(StringFormat::Email),
}),
);
schema.required = Some(vec!["email".to_string()]);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"email": "test@example.com"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_string_format_email_invalid() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"email".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: None,
max_length: None,
format: Some(StringFormat::Email),
}),
);
schema.required = Some(vec!["email".to_string()]);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"email": "invalid-email"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Invalid email format"));
}
#[test]
fn it_validates_string_format_uri_success() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"website".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: None,
max_length: None,
format: Some(StringFormat::Uri),
}),
);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let test_cases = vec![
"http://example.com",
"https://example.com",
"file://path/to/file",
"res://resource_1",
];
for uri in test_cases {
let content_json = serde_json::json!({
"website": uri
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok(), "Failed for URI: {}", uri);
}
}
#[test]
fn it_validates_string_format_uri_invalid() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"website".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: None,
max_length: None,
format: Some(StringFormat::Uri),
}),
);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"website": "not-a-uri"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Invalid URI format"));
}
#[test]
fn it_validates_string_format_date_success() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"birth_date".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: None,
max_length: None,
format: Some(StringFormat::Date),
}),
);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"birth_date": "1990-05-15"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_string_format_date_invalid() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"birth_date".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: None,
max_length: None,
format: Some(StringFormat::Date),
}),
);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let test_cases = vec![
"1990/05/15", "90-05-15", "1990-5-15", "not-a-date", ];
for invalid_date in test_cases {
let content_json = serde_json::json!({
"birth_date": invalid_date
});
let result = validator.validate_content_constraints(&content_json);
assert!(
result.is_err(),
"Should fail for invalid date: {}",
invalid_date
);
let error = result.unwrap_err();
assert!(error.to_string().contains("Invalid date format"));
}
}
#[test]
fn it_validates_string_format_datetime_success() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"updated_at".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: None,
max_length: None,
format: Some(StringFormat::DateTime),
}),
);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"updated_at": "2023-05-15T14:30:00Z"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_string_format_datetime_invalid() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"updated_at".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: None,
max_length: None,
format: Some(StringFormat::DateTime),
}),
);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"updated_at": "2023-05-15 14:30:00" });
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Invalid date format"));
}
#[test]
fn it_validates_string_format_unknown_format() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"custom_field".to_string(),
Schema::String(StringSchema {
r#type: PropertyType::String,
title: None,
descr: None,
min_length: None,
max_length: None,
format: None,
}),
);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"custom_field": "any value should work"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_optional_properties() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"required_field".to_string(),
Schema::String(StringSchema::default()),
);
schema.properties.insert(
"optional_field".to_string(),
Schema::String(StringSchema::default()),
);
schema.required = Some(vec!["required_field".to_string()]);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"required_field": "value"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
let content_json = serde_json::json!({
"required_field": "value",
"optional_field": "optional_value"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_no_required_properties() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"optional_field".to_string(),
Schema::String(StringSchema::default()),
);
schema.required = None;
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_ok());
}
#[test]
fn it_validates_schema_compatibility_no_properties() {
let schema = RequestSchema::new(); let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content = TestStruct {
name: "John Doe".to_string(),
age: 30,
active: true,
};
let result = validator.validate(content);
assert!(result.is_ok());
}
#[test]
fn it_tests_serialize_error_handling() {
let schema = create_test_schema();
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content = TestStruct {
name: "John Doe".to_string(),
age: 30,
active: true,
};
let result = validator.validate(content);
assert!(result.is_ok());
}
#[test]
fn it_tests_request_schema_default() {
let schema = RequestSchema::default();
assert_eq!(schema.r#type, PropertyType::Object);
assert!(schema.properties.is_empty());
assert_eq!(schema.required, None);
}
#[test]
fn it_tests_edge_case_empty_enum() {
let mut schema = RequestSchema::new();
schema.properties.insert(
"status".to_string(),
Schema::SingleUntitledEnum(UntitledSingleSelectEnumSchema {
r#type: PropertyType::String,
title: None,
descr: None,
r#enum: vec![],
default: None,
}),
);
let params = create_form_params_with_schema(schema);
let validator = Validator::new(params);
let content_json = serde_json::json!({
"status": "any_value"
});
let result = validator.validate_content_constraints(&content_json);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Invalid enum value"));
}
}