Skip to content

Commit 326fdb8

Browse files
committed
feat: add uuid validator
1 parent 3e7cc4f commit 326fdb8

File tree

18 files changed

+385
-1
lines changed

18 files changed

+385
-1
lines changed

.DS_Store

10 KB
Binary file not shown.

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ idna = "0.3.0"
4343
base64 = "0.13.1"
4444
num-bigint = "0.4.3"
4545
python3-dll-a = "0.2.7"
46+
uuid = "1.3.4"
4647

4748
[lib]
4849
name = "_pydantic_core"

python/pydantic_core/core_schema.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class CoreConfig(TypedDict, total=False):
7373
# to construct error `loc`s, default True
7474
loc_by_alias: bool
7575
# whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never'
76+
# whether instances of models and dataclasses (including subclass instances) should re-validate, default 'never'
7677
revalidate_instances: Literal['always', 'never', 'subclass-instances']
7778
# whether to validate default values during validation, default False
7879
validate_default: bool
@@ -206,6 +207,7 @@ def field_name(self) -> str:
206207
'url',
207208
'multi-host-url',
208209
'json',
210+
'uuid',
209211
]
210212

211213

@@ -1187,6 +1189,24 @@ def callable_schema(
11871189
return _dict_not_none(type='callable', ref=ref, metadata=metadata, serialization=serialization)
11881190

11891191

1192+
class UuidSchema(TypedDict, total=False):
1193+
type: Required[Literal['uuid']]
1194+
version: Literal[1, 3, 4, 5]
1195+
ref: str
1196+
metadata: Any
1197+
serialization: SerSchema
1198+
1199+
1200+
def uuid_schema(
1201+
*,
1202+
version: Literal[1, 3, 4, 5] | None = None,
1203+
ref: str | None = None,
1204+
metadata: Any = None,
1205+
serialization: SerSchema | None = None,
1206+
) -> UuidSchema:
1207+
return _dict_not_none(type='uuid', ref=ref, version=version, metadata=metadata, serialization=serialization)
1208+
1209+
11901210
class IncExSeqSerSchema(TypedDict, total=False):
11911211
type: Required[Literal['include-exclude-sequence']]
11921212
include: Set[int]
@@ -3688,6 +3708,7 @@ def definition_reference_schema(
36883708
IsInstanceSchema,
36893709
IsSubclassSchema,
36903710
CallableSchema,
3711+
UuidSchema,
36913712
ListSchema,
36923713
TuplePositionalSchema,
36933714
TupleVariableSchema,
@@ -3772,6 +3793,7 @@ def definition_reference_schema(
37723793
'multi-host-url',
37733794
'definitions',
37743795
'definition-ref',
3796+
'uuid',
37753797
]
37763798

37773799
CoreSchemaFieldType = Literal['model-field', 'dataclass-field', 'typed-dict-field', 'computed-field']
@@ -3867,6 +3889,9 @@ def definition_reference_schema(
38673889
'url_syntax_violation',
38683890
'url_too_long',
38693891
'url_scheme',
3892+
'uuid_type',
3893+
'uuid_parsing',
3894+
'uuid_version_mismatch',
38703895
]
38713896

38723897

src/errors/types.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,18 @@ pub enum ErrorType {
308308
UrlScheme {
309309
expected_schemes: String,
310310
},
311+
// UUID errors,
312+
UuidType,
313+
UuidExactType {
314+
class_name: String,
315+
},
316+
UuidParsing {
317+
error: String,
318+
},
319+
UuidVersionMismatch {
320+
version: usize,
321+
schema_version: usize,
322+
},
311323
}
312324

313325
macro_rules! render {
@@ -451,6 +463,11 @@ impl ErrorType {
451463
Self::UrlSyntaxViolation { .. } => extract_context!(Cow::Owned, UrlSyntaxViolation, ctx, error: String),
452464
Self::UrlTooLong { .. } => extract_context!(UrlTooLong, ctx, max_length: usize),
453465
Self::UrlScheme { .. } => extract_context!(UrlScheme, ctx, expected_schemes: String),
466+
Self::UuidExactType { .. } => extract_context!(UuidExactType, ctx, class_name: String),
467+
Self::UuidParsing { .. } => extract_context!(UuidParsing, ctx, error: String),
468+
Self::UuidVersionMismatch { .. } => {
469+
extract_context!(UuidVersionMismatch, ctx, version: usize, schema_version: usize)
470+
}
454471
_ => {
455472
if ctx.is_some() {
456473
py_err!(PyTypeError; "'{}' errors do not require context", value)
@@ -555,6 +572,11 @@ impl ErrorType {
555572
Self::UrlSyntaxViolation {..} => "Input violated strict URL syntax rules, {error}",
556573
Self::UrlTooLong {..} => "URL should have at most {max_length} characters",
557574
Self::UrlScheme {..} => "URL scheme should be {expected_schemes}",
575+
Self::UuidExactType { .. } => "Input should be an instance of {class_name}",
576+
Self::UuidType => "UUID input should be a string or UUID object",
577+
Self::UuidParsing { .. } => "Input should be a valid UUID, {error}",
578+
Self::UuidVersionMismatch { .. } => "UUID version {version} doest not match expected version: {schema_version}"
579+
558580
}
559581
}
560582

@@ -674,6 +696,12 @@ impl ErrorType {
674696
Self::UrlSyntaxViolation { error } => render!(tmpl, error),
675697
Self::UrlTooLong { max_length } => to_string_render!(tmpl, max_length),
676698
Self::UrlScheme { expected_schemes } => render!(tmpl, expected_schemes),
699+
Self::UuidExactType { class_name } => render!(tmpl, class_name),
700+
Self::UuidParsing { error } => render!(tmpl, error),
701+
Self::UuidVersionMismatch {
702+
version,
703+
schema_version,
704+
} => to_string_render!(tmpl, version, schema_version),
677705
_ => Ok(tmpl.to_string()),
678706
}
679707
}
@@ -734,6 +762,13 @@ impl ErrorType {
734762
Self::UrlSyntaxViolation { error } => py_dict!(py, error),
735763
Self::UrlTooLong { max_length } => py_dict!(py, max_length),
736764
Self::UrlScheme { expected_schemes } => py_dict!(py, expected_schemes),
765+
Self::UuidExactType { class_name } => py_dict!(py, class_name),
766+
767+
Self::UuidParsing { error } => py_dict!(py, error),
768+
Self::UuidVersionMismatch {
769+
version,
770+
schema_version,
771+
} => py_dict!(py, version, schema_version),
737772
_ => Ok(None),
738773
}
739774
}

src/input/input_abstract.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::fmt;
2+
use uuid::Uuid;
23

34
use pyo3::types::{PyDict, PyType};
45
use pyo3::{intern, prelude::*};

src/input/input_json.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ impl<'a> Input<'a> for JsonInput {
257257
_ => Err(ValError::new(ErrorType::DateType, self)),
258258
}
259259
}
260+
260261
// NO custom `lax_date` implementation, if strict_date fails, the validator will fallback to lax_datetime
261262
// then check there's no remainder
262263
#[cfg_attr(has_no_coverage, no_coverage)]

src/input/input_python.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::borrow::Cow;
22
use std::str::from_utf8;
3+
use uuid::Uuid;
34

45
use pyo3::prelude::*;
56
use pyo3::types::{

src/input/return_enums.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::slice::Iter as SliceIter;
55
use std::str::FromStr;
66

77
use num_bigint::BigInt;
8+
use uuid::Uuid;
89

910
use pyo3::exceptions::PyTypeError;
1011
use pyo3::prelude::*;

src/serializers/infer.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ pub(crate) fn infer_to_python_known(
187187
let py_url: PyMultiHostUrl = value.extract()?;
188188
py_url.__str__().into_py(py)
189189
}
190+
ObType::Uuid => {
191+
let v = value.getattr(intern!(py, "value"))?;
192+
infer_to_python(v, include, exclude, extra)?.into_py(py)
193+
}
190194
ObType::PydanticSerializable => serialize_with_serializer()?,
191195
ObType::Dataclass => serialize_dict(dataclass_to_dict(value)?)?,
192196
ObType::Enum => {
@@ -503,6 +507,10 @@ pub(crate) fn infer_serialize_known<S: Serializer>(
503507
}
504508
seq.end()
505509
}
510+
ObType::Uuid => {
511+
let s = value.str().map_err(py_err_se_err)?.to_str().map_err(py_err_se_err)?;
512+
serializer.serialize_str(s)
513+
}
506514
ObType::Path => {
507515
let s = value.str().map_err(py_err_se_err)?.to_str().map_err(py_err_se_err)?;
508516
serializer.serialize_str(s)
@@ -616,7 +624,7 @@ pub(crate) fn infer_json_key_known<'py>(ob_type: &ObType, key: &'py PyAny, extra
616624
let k = key.getattr(intern!(key.py(), "value"))?;
617625
infer_json_key(k, extra)
618626
}
619-
ObType::Path => Ok(key.str()?.to_string_lossy()),
627+
ObType::Path | ObType::Uuid => Ok(key.str()?.to_string_lossy()),
620628
ObType::Unknown => {
621629
if let Some(fallback) = extra.fallback {
622630
let next_key = fallback.call1((key,))?;

src/serializers/ob_type.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub struct ObTypeLookup {
3939
// types from this package
4040
url: usize,
4141
multi_host_url: usize,
42+
// uuid type
43+
uuid: usize,
4244
// enum type
4345
enum_type: usize,
4446
// generator
@@ -83,6 +85,7 @@ impl ObTypeLookup {
8385
enum_type: py.import("enum").unwrap().getattr("Enum").unwrap().get_type_ptr() as usize,
8486
generator: py.import("types").unwrap().getattr("GeneratorType").unwrap().as_ptr() as usize,
8587
path: py.import("pathlib").unwrap().getattr("Path").unwrap().as_ptr() as usize,
88+
uuid: py.import("uuid").unwrap().getattr("UUID").unwrap().get_type_ptr() as usize,
8689
}
8790
}
8891

@@ -130,6 +133,7 @@ impl ObTypeLookup {
130133
ObType::Enum => self.enum_type == ob_type,
131134
ObType::Generator => self.generator == ob_type,
132135
ObType::Path => self.path == ob_type,
136+
ObType::Uuid => self.uuid == ob_type,
133137
ObType::Unknown => false,
134138
};
135139

@@ -218,6 +222,8 @@ impl ObTypeLookup {
218222
ObType::Generator
219223
} else if ob_type == self.path {
220224
ObType::Path
225+
} else if ob_type == self.uuid {
226+
ObType::Uuid
221227
} else {
222228
// this allows for subtypes of the supported class types,
223229
// if `ob_type` didn't match any member of self, we try again with the next base type pointer
@@ -315,6 +321,8 @@ pub enum ObType {
315321
Generator,
316322
// Path
317323
Path,
324+
// Uuid
325+
Uuid,
318326
// unknown type
319327
Unknown,
320328
}

src/serializers/shared.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ combined_serializer! {
128128
Dataclass: super::type_serializers::dataclass::DataclassSerializer;
129129
Url: super::type_serializers::url::UrlSerializer;
130130
MultiHostUrl: super::type_serializers::url::MultiHostUrlSerializer;
131+
Uuid: super::type_serializers::uuid::UuidSerializer;
131132
Any: super::type_serializers::any::AnySerializer;
132133
Format: super::type_serializers::format::FormatSerializer;
133134
ToString: super::type_serializers::format::ToStringSerializer;

src/serializers/type_serializers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub mod tuple;
2222
pub mod typed_dict;
2323
pub mod union;
2424
pub mod url;
25+
pub mod uuid;
2526
pub mod with_default;
2627

2728
pub(self) use super::computed_fields::ComputedFields;

src/serializers/type_serializers/string.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ impl TypeSerializer for StrSerializer {
3434
extra: &Extra,
3535
) -> PyResult<PyObject> {
3636
let py = value.py();
37+
dbg!("to python");
3738
match extra.ob_type_lookup.is_type(value, ObType::Str) {
3839
IsType::Exact => Ok(value.into_py(py)),
3940
IsType::Subclass => match extra.mode {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use std::borrow::Cow;
2+
3+
use pyo3::prelude::*;
4+
use pyo3::types::{PyDict, PyString};
5+
6+
use crate::definitions::DefinitionsBuilder;
7+
8+
use super::{
9+
infer_json_key, infer_serialize, infer_to_python, py_err_se_err, BuildSerializer, CombinedSerializer, Extra,
10+
IsType, ObType, SerMode, TypeSerializer,
11+
};
12+
13+
#[derive(Debug, Clone)]
14+
pub struct UuidSerializer;
15+
16+
impl BuildSerializer for UuidSerializer {
17+
const EXPECTED_TYPE: &'static str = "uuid";
18+
19+
fn build(
20+
_schema: &PyDict,
21+
_config: Option<&PyDict>,
22+
_definitions: &mut DefinitionsBuilder<CombinedSerializer>,
23+
) -> PyResult<CombinedSerializer> {
24+
Ok(Self {}.into())
25+
}
26+
}
27+
28+
impl TypeSerializer for UuidSerializer {
29+
fn to_python(
30+
&self,
31+
value: &PyAny,
32+
include: Option<&PyAny>,
33+
exclude: Option<&PyAny>,
34+
extra: &Extra,
35+
) -> PyResult<PyObject> {
36+
let py = value.py();
37+
match extra.ob_type_lookup.is_type(value, ObType::Str) {
38+
IsType::Exact => Ok(value.into_py(py)),
39+
IsType::Subclass => match extra.mode {
40+
SerMode::Json => Ok(value.extract::<&str>()?.into_py(py)),
41+
_ => Ok(value.into_py(py)),
42+
},
43+
IsType::False => {
44+
extra.warnings.on_fallback_py(self.get_name(), value, extra)?;
45+
infer_to_python(value, include, exclude, extra)
46+
}
47+
}
48+
}
49+
50+
fn json_key<'py>(&self, key: &'py PyAny, extra: &Extra) -> PyResult<Cow<'py, str>> {
51+
if let Ok(py_str) = key.downcast::<PyString>() {
52+
Ok(py_str.to_string_lossy())
53+
} else {
54+
extra.warnings.on_fallback_py(self.get_name(), key, extra)?;
55+
infer_json_key(key, extra)
56+
}
57+
}
58+
59+
fn serde_serialize<S: serde::ser::Serializer>(
60+
&self,
61+
value: &PyAny,
62+
serializer: S,
63+
include: Option<&PyAny>,
64+
exclude: Option<&PyAny>,
65+
extra: &Extra,
66+
) -> Result<S::Ok, S::Error> {
67+
match value.downcast::<PyString>() {
68+
Ok(py_str) => serialize_py_str(py_str, serializer),
69+
Err(_) => {
70+
extra.warnings.on_fallback_ser::<S>(self.get_name(), value, extra)?;
71+
infer_serialize(value, serializer, include, exclude, extra)
72+
}
73+
}
74+
}
75+
76+
fn get_name(&self) -> &str {
77+
Self::EXPECTED_TYPE
78+
}
79+
}
80+
81+
pub fn serialize_py_str<S: serde::ser::Serializer>(py_str: &PyString, serializer: S) -> Result<S::Ok, S::Error> {
82+
let s = py_str.to_str().map_err(py_err_se_err)?;
83+
serializer.serialize_str(s)
84+
}

0 commit comments

Comments
 (0)