Skip to main content
Glama

Convex MCP server

Official
by get-convex
validator.rs70.6 kB
#[cfg(any(test, feature = "testing"))] use std::collections::BTreeSet; use std::{ borrow::Borrow, collections::BTreeMap, fmt::{ self, Display, }, iter, }; use errors::ErrorMetadata; #[cfg(any(test, feature = "testing"))] use proptest::prelude::*; use serde_json::{ Number, Value as JsonValue, }; use shape_inference::{ Shape, ShapeConfig, ShapeCounter, ShapeEnum, }; use value::{ export::ValueFormat, id_v6::DeveloperDocumentId, sorting::TotalOrdF64, utils::{ display_map, display_sequence, }, ConvexObject, ConvexValue, FieldName, FieldPath, IdentifierFieldName, Namespace, NamespacedTableMapping, TableName, TableNumber, }; use super::DocumentSchema; use crate::{ document::{ CREATION_TIME_FIELD, ID_FIELD, }, json_schemas, virtual_system_mapping::{ all_tables_number_to_name, VirtualSystemMapping, }, }; /// Validates that a Convex value has the given type. /// /// These are used by both schema enforcement and argument validation. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Validator { Id(TableName), Null, Float64, Int64, Boolean, String, Bytes, Literal(LiteralValidator), Array(Box<Validator>), Set(Box<Validator>), Record(Box<Validator>, Box<Validator>), Map(Box<Validator>, Box<Validator>), Object(ObjectValidator), Union(Vec<Validator>), Any, } #[cfg(any(test, feature = "testing"))] impl proptest::arbitrary::Arbitrary for Validator { type Parameters = BTreeSet<TableName>; type Strategy = impl proptest::strategy::Strategy<Value = Validator>; fn arbitrary_with(table_names: Self::Parameters) -> Self::Strategy { use proptest::prelude::*; let id_validator = if table_names.is_empty() { any::<TableName>().prop_map(Validator::Id).boxed() } else { let table_names: Vec<_> = table_names.into_iter().collect(); proptest::sample::select(table_names) .prop_map(Validator::Id) .boxed() }; let leaf = prop_oneof![ Just(Validator::Null), id_validator, Just(Validator::Float64), Just(Validator::Int64), Just(Validator::Boolean), Just(Validator::String), Just(Validator::Bytes), any::<LiteralValidator>().prop_map(Validator::Literal), Just(Validator::Any), ]; leaf.prop_recursive(3, 8, 8, move |inner| { prop_oneof![ inner.clone().prop_map(Box::new).prop_map(Validator::Array), inner.clone().prop_map(Box::new).prop_map(Validator::Set), (inner.clone(), inner.clone()) .prop_map(|(s1, s2)| Validator::Map(Box::new(s1), Box::new(s2))), prop::collection::btree_map( any::<IdentifierFieldName>(), (inner.clone(), proptest::bool::ANY).prop_map(|(validator, optional)| { FieldValidator { validator, optional, } }), 0..8 ) .prop_map(ObjectValidator) .prop_map(Validator::Object), prop::collection::vec(inner, 1..8).prop_map(Validator::Union), ] }) } } impl Display for Validator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Validator::Id(table_name) => write!(f, "v.id(\"{table_name}\")"), Validator::Null => write!(f, "v.null()"), Validator::Float64 => write!(f, "v.float64()"), Validator::Int64 => write!(f, "v.int64()"), Validator::Boolean => write!(f, "v.boolean()"), Validator::String => write!(f, "v.string()"), Validator::Bytes => write!(f, "v.bytes()"), Validator::Literal(literal) => write!(f, "v.literal({literal})"), Validator::Array(validator) => write!(f, "v.array({validator})"), Validator::Set(validator) => write!(f, "v.set({validator})"), Validator::Map(keys, values) => write!(f, "v.map({keys}, {values})"), Validator::Record(keys, values) => write!(f, "v.record({keys}, {values})"), Validator::Object(object_validator) => write!(f, "{object_validator}"), Validator::Union(validators) => { display_sequence(f, ["v.union(", ")"], validators.iter()) }, Validator::Any => write!(f, "v.any()"), } } } impl Validator { pub fn check_value( &self, value: &ConvexValue, table_mapping: &NamespacedTableMapping, virtual_system_mapping: &VirtualSystemMapping, ) -> Result<(), ValidationError> { let all_tables_number_to_name = all_tables_number_to_name(table_mapping, virtual_system_mapping); self.check_value_internal(value, &all_tables_number_to_name, ValidationContext::new()) } fn check_value_internal( &self, value: &ConvexValue, all_tables_number_to_name: &impl Fn(TableNumber) -> anyhow::Result<TableName>, context: ValidationContext, ) -> Result<(), ValidationError> { match (self, value) { (Validator::Id(validator_table), ConvexValue::String(s)) => { if let Ok(id) = DeveloperDocumentId::decode(s) && let Ok(table_name) = all_tables_number_to_name(id.table()) { if &table_name != validator_table { if table_name.is_system() { let err = ValidationError::SystemTableReference { id, validator_table: validator_table.clone(), context, }; return Err(err); } else { let err = ValidationError::TableNamesDoNotMatch { id, found_table_name: table_name, validator_table: validator_table.clone(), context, }; return Err(err); } } } else { let err = ValidationError::NoMatch { value: value.clone(), validator: self.clone(), context, }; return Err(err); } }, (Validator::Null, ConvexValue::Null) | (Validator::Float64, ConvexValue::Float64(_)) | (Validator::Int64, ConvexValue::Int64(_)) | (Validator::Boolean, ConvexValue::Boolean(_)) | (Validator::String, ConvexValue::String(_)) | (Validator::Bytes, ConvexValue::Bytes(_)) => return Ok(()), (Validator::Literal(literal), value) => { let literal_as_value: ConvexValue = literal.clone().into(); if value != &literal_as_value { return Err(ValidationError::LiteralValuesDoNotMatch { value: value.clone(), literal_validator: literal.clone(), context, }); } }, (Validator::Array(t), ConvexValue::Array(v)) => { for (i, elt) in v.into_iter().enumerate() { t.check_value_internal( elt, all_tables_number_to_name, context.with(format!("[{i}]")), )?; } }, (Validator::Set(t), ConvexValue::Set(v)) => { for (i, elt) in v.into_iter().enumerate() { t.check_value_internal( elt, all_tables_number_to_name, context.with(format!(".keys()[{i}]")), )?; } }, (Validator::Map(key_type, value_type), ConvexValue::Map(map)) => { for (i, (key, value)) in map.into_iter().enumerate() { key_type.check_value_internal( key, all_tables_number_to_name, context.with(format!("keys()[{i}]")), )?; value_type.check_value_internal( value, all_tables_number_to_name, context.with(format!(".values()[{i}]")), )?; } }, (Validator::Record(key_type, value_type), ConvexValue::Object(object)) => { for (key, value) in object.iter() { key_type.check_value_internal( &ConvexValue::from(key.clone()), all_tables_number_to_name, context.with(format!(".keys()")), )?; value_type.check_value_internal( value, all_tables_number_to_name, context.with(format!(".values()")), )?; } }, (Validator::Object(object_validator), ConvexValue::Object(object)) => { for (field_name, field_type) in &object_validator.0 { let maybe_value = object.get::<str>(field_name.borrow()); if let Some(value) = maybe_value { field_type.validator.check_value_internal( value, all_tables_number_to_name, context.with(format!(".{field_name}")), )? } else if !field_type.optional { return Err(ValidationError::MissingRequiredField { object: object.clone(), field_name: field_name.clone(), object_validator: object_validator.clone(), context, }); } } for field in object.keys() { if !object_validator.0.contains_key::<str>(field.borrow()) { return Err(ValidationError::ExtraField { object: object.clone(), field_name: field.clone(), object_validator: object_validator.clone(), context, }); } } }, (Validator::Union(validators), value) => { if validators.len() == 1 { return validators[0].check_value_internal( value, all_tables_number_to_name, context, ); } // TODO: This is dropping the error messages from the individual // validators. Maybe we should combine them if this fails? for t in validators { if t.check_value_internal(value, all_tables_number_to_name, context.clone()) .is_ok() { return Ok(()); } } return Err(ValidationError::NoMatch { value: value.clone(), validator: self.clone(), context, }); }, (Validator::Any, _) => return Ok(()), (..) => { return Err(ValidationError::NoMatch { value: value.clone(), validator: self.clone(), context, }) }, }; Ok(()) } pub fn from_shape<C: ShapeConfig, S: ShapeCounter>( t: &Shape<C, S>, table_mapping: &NamespacedTableMapping, virtual_system_mapping: &VirtualSystemMapping, ) -> Self { match t.variant() { ShapeEnum::Never => Self::Union(vec![]), ShapeEnum::Null => Self::Null, ShapeEnum::Int64 => Self::Int64, ShapeEnum::Float64 => Self::Float64, ShapeEnum::NegativeInf => Self::Float64, ShapeEnum::PositiveInf => Self::Float64, ShapeEnum::NegativeZero => Self::Float64, ShapeEnum::NaN => Self::Float64, ShapeEnum::NormalFloat64 => Self::Float64, ShapeEnum::Boolean => Self::Boolean, ShapeEnum::StringLiteral(s) => { Self::Literal(LiteralValidator::String(s.literal.clone())) }, ShapeEnum::Id(table_number) => { match all_tables_number_to_name(table_mapping, virtual_system_mapping)( *table_number, ) { Ok(table_name) => Self::Id(table_name), Err(_) => Self::String, } }, ShapeEnum::FieldName => Self::String, ShapeEnum::String => Self::String, ShapeEnum::Bytes => Self::Bytes, ShapeEnum::Array(array_type) => Self::Array(Box::new(Self::from_shape( array_type.element(), table_mapping, virtual_system_mapping, ))), ShapeEnum::Set(set_type) => Self::Set(Box::new(Self::from_shape( set_type.element(), table_mapping, virtual_system_mapping, ))), ShapeEnum::Map(map_type) => Self::Map( Box::new(Self::from_shape( map_type.key(), table_mapping, virtual_system_mapping, )), Box::new(Self::from_shape( map_type.value(), table_mapping, virtual_system_mapping, )), ), ShapeEnum::Object(object_type) => { let object_fields = object_type .iter() .map(|(k, v)| { ( k.clone(), FieldValidator { validator: Self::from_shape( &v.value_shape, table_mapping, virtual_system_mapping, ), optional: v.optional, }, ) }) .collect(); Self::Object(ObjectValidator(object_fields)) }, ShapeEnum::Record(record_type) => Self::Record( Box::new(Self::from_shape( record_type.field(), table_mapping, virtual_system_mapping, )), Box::new(Self::from_shape( record_type.value(), table_mapping, virtual_system_mapping, )), ), ShapeEnum::Union(union_type) => Self::Union( union_type .iter() .map(|t| Self::from_shape(t, table_mapping, virtual_system_mapping)) .collect(), ), ShapeEnum::Unknown => Self::Any, } } /// A validator A is a subset of the validator B iff for every value that /// conforms to A, the value also conforms to B. /// /// This verification is used to know if a full table scan can be skipped /// when updating the schema. Hence, false negatives are permissible but /// false positives are not. pub fn is_subset(&self, superset: &Validator) -> bool { match (&self, &superset) { // Generic types (Validator::Array(left_contents), Validator::Array(right_contents)) | (Validator::Set(left_contents), Validator::Set(right_contents)) => { left_contents.is_subset(right_contents) }, (Validator::Map(left_keys, left_values), Validator::Map(right_keys, right_values)) => { left_keys.is_subset(right_keys) && left_values.is_subset(right_values) }, ( Validator::Object(ObjectValidator(left_fields)), Validator::Object(ObjectValidator(right_fields)), ) => { // No field disappears left_fields .keys() .all(|left_field_name| right_fields.contains_key(left_field_name)) && right_fields.iter().all(|(field, right_validator)| -> bool { match left_fields.get(field) { // Either a non-breaking change… Some(left_validator) => { (!left_validator.optional || right_validator.optional) // no mandatory → optional change && left_validator .validator .is_subset(&right_validator.validator) }, // …or a new optional field None => right_validator.optional, } }) }, // Identical types (v1, v2) if v1 == v2 => true, // Types that are subsets of other ones (_, Validator::Any) | (Validator::Literal(LiteralValidator::String(_)), Validator::String) | (Validator::Literal(LiteralValidator::Int64(_)), Validator::Int64) | (Validator::Literal(LiteralValidator::Float64(_)), Validator::Float64) | (Validator::Literal(LiteralValidator::Boolean(_)), Validator::Boolean) | (Validator::Id(_), Validator::String) => true, // Unions (Validator::Union(left_cases), _) => left_cases .iter() .all(|left_case| left_case.is_subset(superset)), (_, Validator::Union(cases)) => { if cases.iter().any(|case| self.is_subset(case)) { true } else if let Validator::Boolean = self { // Allow boolean ⊆ true | false Validator::Literal(LiteralValidator::Boolean(true)).is_subset(superset) && Validator::Literal(LiteralValidator::Boolean(false)).is_subset(superset) } else { false } }, _ => false, } } /// Is this something like `v.union(v.literal("foo"), v.literal("bar"))` /// These need to be treated differently if they are the key type for /// Validator::Record pub(crate) fn is_string_subtype_with_string_literal(&self) -> bool { match self { Validator::Id(_) | Validator::Null | Validator::Float64 | Validator::Int64 | Validator::Boolean | Validator::String | Validator::Bytes | Validator::Array(_) | Validator::Set(_) | Validator::Record(..) | Validator::Map(..) | Validator::Object(_) | Validator::Any => false, Validator::Literal(l) => match l { LiteralValidator::Float64(_) | LiteralValidator::Int64(_) | LiteralValidator::Boolean(_) => false, LiteralValidator::String(_) => true, }, Validator::Union(unions) => unions .iter() .any(|v| v.is_string_subtype_with_string_literal()), } } /// Returns `true` when it is sometimes possible to have a field with the /// given path on the document if this table definition is enforced, or /// `false` when it is never possible. pub fn can_contain_field(&self, field_path: &FieldPath) -> bool { self._can_contain_field(field_path.fields()) } fn _can_contain_field(&self, field_path_parts: &[IdentifierFieldName]) -> bool { let Some(first_part) = field_path_parts.first() else { return true; }; match &self { Validator::Any => true, Validator::Union(cases) => cases .iter() .any(|case| case._can_contain_field(field_path_parts)), Validator::Object(ObjectValidator(fields)) => fields .get(first_part) .map(|field_validator| { field_validator .validator ._can_contain_field(&field_path_parts[1..]) }) .unwrap_or(false), _ => false, } } /// Returns true if field_path points to a field where at least one allowed /// value for that field is could be Array<Float64>. /// /// Some weird cases - if any path in field_path is Any, we return true. If /// any path is a union and at least one of the unions has a path that /// matches our field_path that matches, we return true. If the field path /// points to an Array<Any> we also return true. pub fn overlaps_with_array_float64(&self, field_path: &FieldPath) -> bool { self._overlaps_with_array_float64(field_path.fields()) } fn is_valid_vector_validator(validator: &Validator) -> bool { match validator { Validator::Array(validator) => { matches!(**validator, Validator::Float64 | Validator::Any) }, Validator::Any => true, Validator::Union(validators) => validators.iter().any(Self::is_valid_vector_validator), _ => false, } } fn _overlaps_with_array_float64(&self, field_path_parts: &[IdentifierFieldName]) -> bool { let Some(first_part) = field_path_parts.first() else { return Self::is_valid_vector_validator(self); }; match &self { Validator::Any => true, Validator::Union(cases) => cases .iter() .any(|case| case._overlaps_with_array_float64(field_path_parts)), Validator::Object(ObjectValidator(fields)) => fields .get(first_part) .map(|field_validator| { field_validator .validator ._overlaps_with_array_float64(&field_path_parts[1..]) }) .unwrap_or(true), _ => false, } } pub fn ensure_supported_for_streaming_export(&self) -> anyhow::Result<()> { match self { // Leaf values Validator::Id(_) | Validator::Null | Validator::Float64 | Validator::Int64 | Validator::Boolean | Validator::String | Validator::Bytes | Validator::Literal(_) // Values that map to `any` | Validator::Record(_, _) | Validator::Any => Ok(()), Validator::Array(element_validator) => { element_validator.ensure_supported_for_streaming_export() }, Validator::Set(element_validator) => { element_validator.ensure_supported_for_streaming_export() }, Validator::Map(key_validator, value_validator) => { key_validator.ensure_supported_for_streaming_export()?; value_validator.ensure_supported_for_streaming_export() }, Validator::Object(object_validator) => { let fields = &object_validator.0; for field_validator in fields.values() { field_validator.validator.ensure_supported_for_streaming_export()? } Ok(()) }, Validator::Union(validators) => { let mut num_objects = 0; for validator in validators { if matches!(validator, Validator::Object(_)) { num_objects += 1; }; validator.ensure_supported_for_streaming_export()? }; if num_objects > 1 { Err(anyhow::anyhow!(ErrorMetadata::bad_request( "UnsupportedSchemaForExport", "Schema contains a union of objects, which is not supported for export" ))) } else { Ok(()) } } } } pub fn to_json_schema(&self, value_format: ValueFormat) -> JsonValue { let json_schema = match self { Validator::Id(table_name) => json_schemas::id(table_name), Validator::Null => json_schemas::null(), Validator::Float64 => json_schemas::float64(true, value_format), Validator::Int64 => json_schemas::int64(value_format), Validator::Boolean => json_schemas::boolean(), Validator::String => json_schemas::string(), Validator::Bytes => json_schemas::bytes(value_format), Validator::Literal(literal_validator) => match literal_validator { LiteralValidator::Float64(_) => json_schemas::float64(true, value_format), LiteralValidator::Int64(_) => json_schemas::int64(value_format), LiteralValidator::Boolean(_) => json_schemas::boolean(), LiteralValidator::String(_) => json_schemas::string(), }, Validator::Array(element_validator) => { json_schemas::array(element_validator.to_json_schema(value_format)) }, Validator::Set(element_validator) => { json_schemas::set(element_validator.to_json_schema(value_format)) }, Validator::Record(key_validator, value_validator) => json_schemas::record( key_validator.to_string(), value_validator.to_json_schema(value_format), ), Validator::Map(key_validator, value_validator) => json_schemas::map( key_validator.to_json_schema(value_format), value_validator.to_json_schema(value_format), ), Validator::Object(object_validator) => { object_validator.to_json_schema(AddTopLevelFields::False, value_format) }, Validator::Union(validators) => { let options = validators .iter() .map(|v| v.to_json_schema(value_format)) .collect(); json_schemas::union(options) }, Validator::Any => json_schemas::any(), }; json_schema } pub fn foreign_keys<'a>(&'a self) -> Box<dyn Iterator<Item = &'a TableName> + 'a> { Box::new(iter::from_coroutine( #[coroutine] move || match self { Self::Id(table_name) => yield table_name, Self::Object(object) => { for table_name in object.foreign_keys() { yield table_name; } }, Self::Array(item) | Self::Set(item) => { for table_name in item.foreign_keys() { yield table_name; } }, Self::Union(options) => { for table_name in options.iter().flat_map(|option| option.foreign_keys()) { yield table_name; } }, Self::Record(key, value) | Self::Map(key, value) => { for table_name in key.foreign_keys() { yield table_name; } for table_name in value.foreign_keys() { yield table_name; } }, Self::Any | Self::Boolean | Self::Bytes | Self::String | Self::Literal(_) | Self::Null | Self::Float64 | Self::Int64 => {}, }, )) } pub fn has_map_or_set(&self) -> bool { match self { Self::Id(_) | Self::Null | Self::Float64 | Self::Int64 | Self::Boolean | Self::String | Self::Bytes | Self::Literal(_) | Self::Any => false, Self::Set(_) | Self::Map(..) => true, Self::Array(a) => a.has_map_or_set(), Self::Record(k, v) => k.has_map_or_set() || v.has_map_or_set(), Self::Object(o) => o.has_map_or_set(), Self::Union(u) => u.iter().any(|o| o.has_map_or_set()), } } // Filter out `_id` and `_creationTime` at the top level pub fn filter_top_level_system_fields(self) -> Self { match self { Validator::Id(_) | Validator::Null | Validator::Float64 | Validator::Int64 | Validator::Boolean | Validator::String | Validator::Bytes | Validator::Literal(_) | Validator::Array(_) | Validator::Set(_) | Validator::Record(..) | Validator::Map(..) | Validator::Any => self, Validator::Object(o) => Validator::Object(o.filter_system_fields()), Validator::Union(validators) => Validator::Union( validators .into_iter() .map(|v| v.filter_top_level_system_fields()) .collect(), ), } } } impl From<DocumentSchema> for Validator { fn from(document_schema: DocumentSchema) -> Self { match document_schema { DocumentSchema::Any => Validator::Any, DocumentSchema::Union(validators) => { Validator::Union(validators.into_iter().map(Validator::Object).collect()) }, } } } impl From<Option<DocumentSchema>> for Validator { fn from(option: Option<DocumentSchema>) -> Self { match option { None => Validator::Any, Some(document_schema) => document_schema.into(), } } } #[derive(Clone, Debug, PartialEq)] pub struct ValidationContext(Option<String>); impl ValidationContext { pub fn new() -> Self { ValidationContext(None) } pub fn with(&self, new_context: String) -> Self { match &self.0 { Some(context) => Self(Some(format!("{context}{new_context}"))), None => Self(Some(new_context)), } } } impl Display for ValidationContext { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(context) = &self.0 { write!(f, "Path: {context}") } else { Ok(()) } } } #[derive(Clone, Debug, Eq, Ord, PartialOrd, PartialEq)] #[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] pub enum LiteralValidator { Float64(TotalOrdF64), Int64(i64), Boolean(bool), String(value::ConvexString), } impl Display for LiteralValidator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Attempt to display this with JSON. For the values that can't be // printed with JSON, fall back to the general type. let string = match self { LiteralValidator::Float64(float) => { if let Some(json_number) = Number::from_f64(f64::from(float.clone())) { serde_json::to_string(&JsonValue::Number(json_number)) } else { Ok("<number>".to_string()) } }, LiteralValidator::Int64(_) => Ok("<bigint>".to_string()), LiteralValidator::Boolean(bool) => serde_json::to_string(&JsonValue::Bool(*bool)), LiteralValidator::String(string) => { serde_json::to_string(&JsonValue::String(string.clone().into())) }, } .map_err(|_| fmt::Error)?; write!(f, "{string}") } } impl From<LiteralValidator> for ConvexValue { fn from(literal: LiteralValidator) -> Self { match literal { LiteralValidator::Float64(float) => ConvexValue::Float64(float.into()), LiteralValidator::Int64(int) => ConvexValue::Int64(int), LiteralValidator::Boolean(bool) => ConvexValue::Boolean(bool), LiteralValidator::String(string) => ConvexValue::String(string), } } } impl TryFrom<ConvexValue> for LiteralValidator { type Error = anyhow::Error; fn try_from(v: ConvexValue) -> anyhow::Result<Self> { match v { ConvexValue::Float64(f) => Ok(LiteralValidator::Float64(f.into())), ConvexValue::Int64(i) => Ok(LiteralValidator::Int64(i)), ConvexValue::Boolean(b) => Ok(LiteralValidator::Boolean(b)), ConvexValue::String(s) => Ok(LiteralValidator::String(s.to_string().try_into()?)), _ => Err(anyhow::anyhow!("Value {v} is not a valid literal.")), } } } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] #[cfg_attr( any(test, feature = "testing"), proptest(params = "BTreeSet<TableName>") )] pub struct ObjectValidator( #[cfg_attr( any(test, feature = "testing"), proptest( strategy = "prop::collection::btree_map(any::<IdentifierFieldName>(), \ any_with::<FieldValidator>(params), 0..8)" ) )] pub BTreeMap<IdentifierFieldName, FieldValidator>, ); #[macro_export] macro_rules! object_validator { ($($field_name:expr => $field_type:expr),* $(,)?) => { { use $crate::schemas::validator::ObjectValidator; use std::collections::BTreeMap; #[allow(unused_mut)] let mut fields = BTreeMap::new(); { $(fields.insert($field_name.to_string().parse()?, $field_type);)* } ObjectValidator(fields) } }; } impl Display for ObjectValidator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { display_map(f, ["v.object({", "})"], self.0.iter()) } } pub enum AddTopLevelFields { True(TableName), False, } impl ObjectValidator { pub fn has_validator_for_system_field(&self) -> bool { let fields = &self.0; fields.keys().any(|f| f.is_system()) } pub fn filter_system_fields(self) -> Self { if !self.has_validator_for_system_field() { return self; } let fields = self.0; let filtered_fields = fields.into_iter().filter(|(f, _)| !f.is_system()).collect(); Self(filtered_fields) } pub fn has_map_or_set(&self) -> bool { let fields = &self.0; fields.values().any(|f| f.has_map_or_set()) } pub fn to_json_schema( &self, add_top_level_fields: AddTopLevelFields, value_format: ValueFormat, ) -> JsonValue { let fields = &self.0; let mut field_infos: BTreeMap<String, json_schemas::FieldInfo> = fields .iter() .map(|(field_name, field_validator)| { ( field_name.to_string(), json_schemas::FieldInfo { schema: field_validator.validator.to_json_schema(value_format), optional: field_validator.optional, }, ) }) .collect(); if let AddTopLevelFields::True(table_name) = add_top_level_fields { field_infos.insert( ID_FIELD.to_string(), json_schemas::FieldInfo { schema: json_schemas::id(&table_name), optional: false, }, ); field_infos.insert( CREATION_TIME_FIELD.to_string(), json_schemas::FieldInfo { schema: json_schemas::float64(false, value_format), optional: false, }, ); }; json_schemas::object(field_infos) } pub fn foreign_keys(&self) -> impl Iterator<Item = &TableName> { self.0 .values() .flat_map(|field| field.validator.foreign_keys()) } } /// Object fields can be optional. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] #[cfg_attr( any(test, feature = "testing"), proptest(params = "BTreeSet<TableName>") )] pub struct FieldValidator { #[cfg_attr( any(test, feature = "testing"), proptest(strategy = "any_with::<Validator>(params)") )] pub validator: Validator, pub optional: bool, } impl FieldValidator { pub fn validator(&self) -> &Validator { &self.validator } pub fn required_field_type(validator: Validator) -> Self { Self { validator, optional: false, } } pub fn optional_field_type(validator: Validator) -> Self { Self { validator, optional: true, } } pub fn has_map_or_set(&self) -> bool { self.validator.has_map_or_set() } } impl Display for FieldValidator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.optional { write!(f, "v.optional({})", self.validator) } else { write!(f, "{}", self.validator) } } } #[derive(derive_more::Display, Debug, Clone, PartialEq)] pub enum ValidationError { #[display( "Found ID \"{id}\" from table `{found_table_name}`, which does not match the table name \ in validator `v.id(\"{validator_table}\")`.{context}" )] TableNamesDoNotMatch { id: DeveloperDocumentId, found_table_name: TableName, validator_table: TableName, context: ValidationContext, }, #[display( "Found ID \"{id}\" from a system table, which does not match the table name in validator \ `v.id(\"{validator_table}\")`.{context}" )] SystemTableReference { id: DeveloperDocumentId, validator_table: TableName, context: ValidationContext, }, #[display( "`{value}` does not match literal validator `v.literal({literal_validator})`.{context}" )] LiteralValuesDoNotMatch { value: ConvexValue, literal_validator: LiteralValidator, context: ValidationContext, }, #[display( "Object is missing the required field `{field_name}`. Consider wrapping the field \ validator in `v.optional(...)` if this is expected. {context} Object: {object} Validator: {object_validator}" )] MissingRequiredField { object: ConvexObject, field_name: IdentifierFieldName, object_validator: ObjectValidator, context: ValidationContext, }, #[display( "Object contains extra field `{field_name}` that is not in the validator. {context} Object: {object} Validator: {object_validator}" )] ExtraField { object: ConvexObject, field_name: FieldName, object_validator: ObjectValidator, context: ValidationContext, }, #[display( "Value does not match validator. {context} Value: {value} Validator: {validator}" )] NoMatch { value: ConvexValue, validator: Validator, context: ValidationContext, }, } #[cfg(test)] mod tests { use std::{ collections::BTreeMap, str::FromStr, }; use cmd_util::env::env_config; use errors::ErrorMetadataAnyhowExt; use json_trait::JsonForm as _; use maplit::{ btreemap, btreeset, }; use proptest::prelude::*; use serde_json::json; use shape_inference::{ testing::TestConfig, CountedShape, }; use value::{ array, assert_obj, assert_val, export::ValueFormat, id_v6::DeveloperDocumentId, proptest::{ RestrictNaNs, ValueBranching, }, ConvexObject, ConvexValue, ExcludeSetsAndMaps, FieldName, FieldType, InternalId, NamespacedTableMapping, TableMapping, TableName, TableNamespace, }; use super::Validator; use crate::{ schemas::{ validator::{ FieldValidator, LiteralValidator, ObjectValidator, ValidationContext, ValidationError, }, DocumentSchema, }, testing::TestIdGenerator, virtual_system_mapping::{ all_tables_name_to_number, VirtualSystemMapping, }, }; fn empty_table_mapping() -> NamespacedTableMapping { TableMapping::new().namespace(TableNamespace::test_user()) } // Arbitrary `TryFrom` implementation for testing `check_value`. fn value_from_validator( validator: Validator, id_generator: &TestIdGenerator, ) -> anyhow::Result<ConvexValue> { let value = match validator { Validator::Id(table_name) => { let id = InternalId::MIN; let table_number = all_tables_name_to_number( TableNamespace::test_user(), id_generator, &id_generator.virtual_system_mapping, )(table_name)?; let doc_idv6 = DeveloperDocumentId::new(table_number, id); ConvexValue::String(doc_idv6.encode().try_into()?) }, Validator::Null => assert_val!(null), Validator::Float64 => assert_val!(0.), Validator::Int64 => assert_val!(0), Validator::Boolean => assert_val!(false), Validator::String => assert_val!(""), Validator::Bytes => ConvexValue::Bytes(vec![1, 2, 3].try_into()?), Validator::Literal(literal) => literal.into(), Validator::Array(v) => { assert_val!([value_from_validator(*v, id_generator)?]) }, Validator::Set(v) => { ConvexValue::Set(btreeset! { value_from_validator(*v, id_generator)? }.try_into()?) }, Validator::Map(k, v) => { let key = value_from_validator(*k, id_generator)?; let map: BTreeMap<ConvexValue, ConvexValue> = btreemap! { key => value_from_validator(*v, id_generator)? }; ConvexValue::Map(map.try_into()?) }, Validator::Record(k, v) => { let key = value_from_validator(*k, id_generator)?; let field_name = match key { ConvexValue::String(s) => FieldName::from_str(&s)?, _ => anyhow::bail!("Record key was not a string"), }; assert_val!({field_name => value_from_validator(*v, id_generator)?}) }, Validator::Object(object) => { let map: BTreeMap<_, _> = object .0 .into_iter() .map(|(field_name, field_type)| { let value = value_from_validator(field_type.validator, id_generator)?; anyhow::Ok::<(FieldName, ConvexValue)>((field_name.into(), value)) }) .try_collect()?; ConvexValue::Object(map.try_into()?) }, Validator::Union(validators) => { let validator = validators.into_iter().next().ok_or_else(|| { anyhow::anyhow!("Union validator must have at least one validator") })?; value_from_validator(validator, id_generator)? }, Validator::Any => assert_val!(null), }; Ok(value) } // Arbitrary implementation for testing `check_object`. fn object_from_schema( schema: DocumentSchema, id_generator: &TestIdGenerator, ) -> anyhow::Result<ConvexObject> { match schema { DocumentSchema::Any => Ok(btreemap! {}.try_into()?), DocumentSchema::Union(objects) => { let object = objects.into_iter().next().ok_or_else(|| { anyhow::anyhow!("Union validator must have at least one validator") })?; value_from_validator(Validator::Object(object), id_generator)?.try_into() }, } } proptest! { #![proptest_config(ProptestConfig { cases: 64 * env_config("CONVEX_PROPTEST_MULTIPLIER", 1), failure_persistence: None, .. ProptestConfig::default() })] #[test] fn test_check_value(v in any_with::<DocumentSchema>(btreeset! { "table_name".parse::<TableName>().unwrap(), "table_name_2".parse::<TableName>().unwrap()}, )) { let mut id_generator = TestIdGenerator::new(); id_generator.user_generate(&"table_name".parse().unwrap()); id_generator.generate_virtual(&"table_name_2".parse().unwrap()); // Test that `check_value` succeeds for objects created from arbitrary document schemas. let object = object_from_schema(v.clone(), &id_generator).unwrap(); v.check_value( &object, &id_generator.namespace(TableNamespace::test_user()), &id_generator.virtual_system_mapping, ).unwrap(); } } #[test] fn test_record_can_have_string_keys() -> anyhow::Result<()> { // Tests validator with type // `v.object({ myArg: v.record(v.string(), v.number()) })` let validator_json = json!({ "type": "object", "value": { "myArg": { "fieldType": { "type": "record", "keys": { "type": "string", }, "values": { "fieldType": { "type": "number" }, "optional": false, } }, "optional": false }, } }); assert!(Validator::json_deserialize_value(validator_json).is_ok()); Ok(()) } #[test] fn test_record_can_have_string_subset_as_key() -> anyhow::Result<()> { // Tests validator with type // `v.object({ myArg: v.record(v.id("users"), v.number()) })` let validator_json = json!({ "type": "object", "value": { "myArg": { "fieldType": { "type": "record", "keys": { "type": "id", "tableName": "users", }, "values": { "fieldType": { "type": "number" }, "optional": false, } }, "optional": false }, } }); assert!(Validator::json_deserialize_value(validator_json).is_ok()); Ok(()) } #[test] fn test_record_key_any() -> anyhow::Result<()> { // Tests validator with type // `v.object({ myArg: v.record(v.any(), v.number()) })` let validator_json = json!({ "type": "object", "value": { "myArg": { "fieldType": { "type": "record", "keys": { "type": "any", }, "values": { "fieldType": { "type": "number" }, "optional": false, } }, "optional": false }, } }); must_let::must_let!(let Err(e) = Validator::json_deserialize_value(validator_json)); assert_eq!(e.short_msg(), "InvalidRecordType"); Ok(()) } #[test] fn test_record_cannot_have_number_keys() -> anyhow::Result<()> { // Tests validator with type // `v.object({ myArg: v.record(v.number(), v.number()) })` let validator_json = json!({ "type": "object", "value": { "myArg": { "fieldType": { "type": "record", "keys": { "type": "number", }, "values": { "fieldType": { "type": "number" }, "optional": false, } }, "optional": false }, } }); must_let::must_let!(let Err(e) = Validator::json_deserialize_value(validator_json)); assert_eq!(e.short_msg(), "InvalidRecordType"); Ok(()) } #[test] fn test_record_cannot_have_optional_values() -> anyhow::Result<()> { // Tests validator with type // `v.object({ myArg: v.record(v.id("users"), v.optional(v.number())) })` let validator_json = json!({ "type": "object", "value": { "myArg": { "fieldType": { "type": "record", "keys": { "type": "id", "tableName": "users", }, "values": { "fieldType": { "type": "number" }, "optional": true, } }, "optional": false }, } }); must_let::must_let!(let Err(e) = Validator::json_deserialize_value(validator_json)); assert_eq!(e.short_msg(), "InvalidRecordType"); Ok(()) } #[test] fn test_record_can_have_unions() -> anyhow::Result<()> { // Tests validator with type // `v.object({ myArg: v.record(v.union(v.id("users"), v.string()), // v.optional(v.number())) })` let validator_json = json!({ "type": "object", "value": { "myArg": { "fieldType": { "type": "record", "keys": { "type": "union", "value": [ { "type": "string" }, { "tableName": "users", "type": "id" } ] }, "values": { "fieldType": { "type": "number" }, "optional": false, } }, "optional": false }, } }); assert!(Validator::json_deserialize_value(validator_json).is_ok()); Ok(()) } #[test] fn test_record_cannot_have_unions_with_literals() -> anyhow::Result<()> { // Tests validator with type // `v.object({ myArg: v.record(v.union(v.literal("abc"), v.string()), // v.optional(v.number())) })` let validator_json = json!({ "type": "object", "value": { "myArg": { "fieldType": { "type": "record", "keys": { "type": "union", "value": [ { "type": "string" }, { "type": "literal", "value": "abc", } ] }, "values": { "fieldType": { "type": "number" }, "optional": false, } }, "optional": false }, } }); must_let::must_let!(let Err(e) = Validator::json_deserialize_value(validator_json)); assert_eq!(e.short_msg(), "InvalidRecordType"); Ok(()) } #[test] fn test_record_cannot_have_string_literal_keys() -> anyhow::Result<()> { let validator_json = json!({ "type": "object", "value": { "myArg": { "fieldType": { "type": "record", "keys": { "type": "literal", "value": "abc", }, "values": { "fieldType": { "type": "number" }, "optional":false, } }, "optional": false }, } }); must_let::must_let!(let Err(e) = Validator::json_deserialize_value(validator_json)); assert_eq!(e.short_msg(), "InvalidRecordType"); Ok(()) } #[test] fn test_record_check_value() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let table_name: TableName = "users".parse()?; let key_validator = Validator::Id(table_name.clone()); let value_validator = Validator::Float64; let validator = Validator::Record( Box::new(key_validator.clone()), Box::new(value_validator.clone()), ); let value_wrong_type = ConvexValue::String("hello".try_into()?); let err = validator .check_value( &value_wrong_type, &id_generator.namespace(TableNamespace::test_user()), &VirtualSystemMapping::default(), ) .unwrap_err(); assert_eq!( err, ValidationError::NoMatch { value: value_wrong_type, context: ValidationContext::new(), validator: validator.clone() } ); let user_id1 = id_generator.user_generate(&table_name); let user_id2 = id_generator.user_generate(&table_name); let value_wrong_key: ConvexValue = assert_obj!( user_id1.to_string() => ConvexValue::Float64(0.0), user_id2.to_string() => ConvexValue::Float64(0.0), "hello" => ConvexValue::Float64(0.0), ) .into(); let err: ValidationError = validator .check_value( &value_wrong_key, &id_generator.namespace(TableNamespace::test_user()), &VirtualSystemMapping::default(), ) .unwrap_err(); assert_eq!( err, ValidationError::NoMatch { value: ConvexValue::String("hello".try_into()?), context: ValidationContext::new().with(".keys()".to_string()), validator: key_validator } ); let value_wrong_value: ConvexValue = assert_obj!( user_id1.to_string() => ConvexValue::Boolean(true), user_id2.to_string() => ConvexValue::Float64(0.0), ) .into(); let err: ValidationError = validator .check_value( &value_wrong_value, &id_generator.namespace(TableNamespace::test_user()), &VirtualSystemMapping::default(), ) .unwrap_err(); assert_eq!( err, ValidationError::NoMatch { value: ConvexValue::Boolean(true), context: ValidationContext::new().with(".values()".to_string()), validator: value_validator } ); Ok(()) } #[test] fn test_record_check_value_with_virtual_ids() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let table_name: TableName = "users".parse()?; let key_validator = Validator::Id(table_name.clone()); let value_validator = Validator::Float64; let validator = Validator::Record( Box::new(key_validator.clone()), Box::new(value_validator.clone()), ); let value_wrong_type = ConvexValue::String("hello".try_into()?); let err = validator .check_value( &value_wrong_type, &id_generator.namespace(TableNamespace::test_user()), &id_generator.virtual_system_mapping, ) .unwrap_err(); assert_eq!( err, ValidationError::NoMatch { value: value_wrong_type, context: ValidationContext::new(), validator: validator.clone() } ); let user_id1 = id_generator.generate_virtual(&table_name); let user_id2 = id_generator.generate_virtual(&table_name); let value_wrong_key: ConvexValue = assert_obj!( user_id1.to_string() => ConvexValue::Float64(0.0), user_id2.to_string() => ConvexValue::Float64(0.0), "hello" => ConvexValue::Float64(0.0), ) .into(); let err: ValidationError = validator .check_value( &value_wrong_key, &id_generator.namespace(TableNamespace::test_user()), &id_generator.virtual_system_mapping, ) .unwrap_err(); assert_eq!( err, ValidationError::NoMatch { value: ConvexValue::String("hello".try_into()?), context: ValidationContext::new().with(".keys()".to_string()), validator: key_validator } ); let value_wrong_value: ConvexValue = assert_obj!( user_id1.to_string() => ConvexValue::Boolean(true), user_id2.to_string() => ConvexValue::Float64(0.0), ) .into(); let err: ValidationError = validator .check_value( &value_wrong_value, &id_generator.namespace(TableNamespace::test_user()), &id_generator.virtual_system_mapping, ) .unwrap_err(); assert_eq!( err, ValidationError::NoMatch { value: ConvexValue::Boolean(true), context: ValidationContext::new().with(".values()".to_string()), validator: value_validator } ); Ok(()) } #[test] fn test_display() -> anyhow::Result<()> { // Test the display of our complex validator types. let id_validator = Validator::Id("tableName".parse()?); assert_eq!(id_validator.to_string(), "v.id(\"tableName\")"); let float_literal = Validator::Literal(LiteralValidator::Float64(123f64.into())); assert_eq!(float_literal.to_string(), "v.literal(123.0)"); let int_literal = Validator::Literal(LiteralValidator::Int64(123)); assert_eq!(int_literal.to_string(), "v.literal(<bigint>)"); let string_literal = Validator::Literal(LiteralValidator::String("abc".to_string().try_into()?)); assert_eq!(string_literal.to_string(), "v.literal(\"abc\")"); let boolean_literal = Validator::Literal(LiteralValidator::Boolean(true)); assert_eq!(boolean_literal.to_string(), "v.literal(true)"); let array_validator = Validator::Array(Box::new(Validator::String)); assert_eq!(array_validator.to_string(), "v.array(v.string())"); let set_validator = Validator::Set(Box::new(Validator::Float64)); assert_eq!(set_validator.to_string(), "v.set(v.float64())"); let map_validator = Validator::Map(Box::new(Validator::Int64), Box::new(Validator::Boolean)); assert_eq!(map_validator.to_string(), "v.map(v.int64(), v.boolean())"); let object_validator = Validator::Object( object_validator!("required" => FieldValidator::required_field_type(Validator::String), "optional" => FieldValidator::optional_field_type(Validator::Float64)), ); assert_eq!( object_validator.to_string(), "v.object({optional: v.optional(v.float64()), required: v.string()})" ); let union_validator = Validator::Union(vec![Validator::String, Validator::Float64]); assert_eq!( union_validator.to_string(), "v.union(v.string(), v.float64())" ); Ok(()) } #[test] fn test_id_match() -> anyhow::Result<()> { let table1: TableName = "table1".parse()?; let table2: TableName = "table2".parse()?; let id_validator = Validator::Id(table1.clone()); let mut id_generator = TestIdGenerator::new(); // generate an ID so it's in the table mapping id_generator.user_generate(&table1); let document_id = id_generator.user_generate(&table2); let id_v6 = DeveloperDocumentId::from(document_id); let value: ConvexValue = id_v6.into(); let err = id_validator .check_value( &value, &id_generator.namespace(TableNamespace::test_user()), &id_generator.virtual_system_mapping, ) .unwrap_err(); assert_eq!( err, ValidationError::TableNamesDoNotMatch { validator_table: table1, context: ValidationContext::new(), id: id_v6, found_table_name: table2 } ); Ok(()) } #[test] fn test_id_match_with_virtual_ids() -> anyhow::Result<()> { let table1: TableName = "table1".parse()?; let table2: TableName = "table2".parse()?; let id_validator = Validator::Id(table1.clone()); let mut id_generator = TestIdGenerator::new(); // generate an ID so it's in the table mapping id_generator.user_generate(&table1); let id_v6 = id_generator.generate_virtual(&table2); let value: ConvexValue = id_v6.into(); let err = id_validator .check_value( &value, &id_generator.namespace(TableNamespace::test_user()), &id_generator.virtual_system_mapping, ) .unwrap_err(); assert_eq!( err, ValidationError::TableNamesDoNotMatch { validator_table: table1, context: ValidationContext::new(), id: id_v6, found_table_name: table2 } ); Ok(()) } #[test] fn test_schema_literal_match() -> anyhow::Result<()> { let schema_literal = LiteralValidator::String("hello".to_string().try_into()?); let string_literal = Validator::Literal(schema_literal.clone()); string_literal .check_value( &ConvexValue::String("hello".try_into()?), &empty_table_mapping(), &VirtualSystemMapping::default(), ) .unwrap(); let value = ConvexValue::Int64(0); let err = string_literal .check_value( &value, &empty_table_mapping(), &VirtualSystemMapping::default(), ) .unwrap_err(); assert_eq!( err, ValidationError::LiteralValuesDoNotMatch { value, literal_validator: schema_literal, context: ValidationContext::new() } ); Ok(()) } #[test] fn test_error_messages_include_context() -> anyhow::Result<()> { // The validator expects `property` to be an array of strings, // but it actually contains an int. let validator = Validator::Object(ObjectValidator(btreemap! { "property".parse()? => FieldValidator::required_field_type( Validator::Array(Box::new(Validator::String)) ) })); let object = ConvexValue::Object(assert_obj!("property" => ConvexValue::Array(array!(123.into())?))); // Check that the error message includes the path to the assert!(validator .check_value( &object, &empty_table_mapping(), &VirtualSystemMapping::default(), ) .unwrap_err() .to_string() .contains(".property[0]")); Ok(()) } #[test] fn test_ensure_supported_for_streaming_export() -> anyhow::Result<()> { let simple_object_validator = Validator::Object(ObjectValidator(btreemap! { "property".parse()? => FieldValidator::required_field_type(Validator::String) })); assert!(simple_object_validator .ensure_supported_for_streaming_export() .is_ok()); let any_validator = Validator::Any; assert!(any_validator .ensure_supported_for_streaming_export() .is_ok()); let union_object_validator = Validator::Union(vec![ Validator::Object(ObjectValidator(btreemap! { "propertyA".parse()? => FieldValidator::required_field_type(Validator::String) })), Validator::Object(ObjectValidator(btreemap! { "propertyB".parse()? => FieldValidator::required_field_type(Validator::String) })), ]); must_let::must_let!( let Err(e) = union_object_validator.ensure_supported_for_streaming_export() ); assert_eq!(e.short_msg(), "UnsupportedSchemaForExport"); let nested_union_object_validator = Validator::Array(Box::new(Validator::Union(vec![ Validator::Object(ObjectValidator(btreemap! { "propertyA".parse()? => FieldValidator::required_field_type(Validator::String) })), Validator::Object(ObjectValidator(btreemap! { "propertyB".parse()? => FieldValidator::required_field_type(Validator::String) })), ]))); must_let::must_let!( let Err(e) = nested_union_object_validator.ensure_supported_for_streaming_export() ); assert_eq!(e.short_msg(), "UnsupportedSchemaForExport"); Ok(()) } proptest! { #![proptest_config(ProptestConfig { cases: 64 * env_config("CONVEX_PROPTEST_MULTIPLIER", 1), failure_persistence: None, .. ProptestConfig::default() })] #[test] fn test_validators_are_subsets_of_themselves(validator in any_with::<Validator>(btreeset! { "table_name".parse::<TableName>().unwrap()})) { prop_assert!(validator.is_subset(&validator)); } #[test] fn test_all_types_are_a_subset_of_any(validator in any_with::<Validator>(btreeset! { "table_name".parse::<TableName>().unwrap()})) { prop_assert!(validator.is_subset(&Validator::Any)); } #[test] fn test_no_type_is_a_subset_of_never(validator in any_with::<Validator>(btreeset! { "table_name".parse::<TableName>().unwrap()})) { let never = Validator::Union(vec![]); prop_assert!(!validator.is_subset(&never)); } #[test] fn test_union_of_one_element_is_equivalent_to_this_element(validator in any_with::<Validator>(btreeset! { "table_name".parse::<TableName>().unwrap()})) { let union_of_one = Validator::Union(vec![validator.clone()]); prop_assert!(validator.is_subset(&union_of_one)); prop_assert!(union_of_one.is_subset(&validator)); } #[test] fn test_to_json_schema( v in any_with::<Validator>(btreeset! { "table_name".parse::<TableName>().unwrap()}), value_format in any::<ValueFormat>(), ) { jsonschema::validator_for(&v.to_json_schema(value_format)).unwrap(); } #[test] fn test_validator_from_a_shape_validates_it( resolved_value in any_with::<ConvexValue>( ( FieldType::User, ValueBranching::default(), ExcludeSetsAndMaps(false), RestrictNaNs(false), ) ) ) { let table_mapping = empty_table_mapping(); let virtual_system_mapping = VirtualSystemMapping::default(); let shape = CountedShape::<TestConfig>::empty().insert_value(&resolved_value); let validator = Validator::from_shape(&shape, &table_mapping, &virtual_system_mapping); prop_assert!(validator.check_value( &resolved_value, &table_mapping, &virtual_system_mapping ).is_ok()); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server