Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
=========

Fixed
^^^^^

- Attributes are case insensitive #39

[0.1.10] - 2024-06-30
---------------------

Expand Down
29 changes: 28 additions & 1 deletion scim2_models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
from pydantic import field_validator
from pydantic import model_serializer
from pydantic import model_validator
from pydantic.alias_generators import to_camel
from pydantic_core import PydanticCustomError
from pydantic_core import core_schema
from typing_extensions import NewType
from typing_extensions import Self

from scim2_models.attributes import contains_attribute_or_subattributes
from scim2_models.attributes import validate_attribute_urn
from scim2_models.utils import to_camel

ReferenceTypes = TypeVar("ReferenceTypes")
URIReference = NewType("URIReference", str)
Expand Down Expand Up @@ -416,6 +416,33 @@ def check_request_attributes_mutability(

return value

@model_validator(mode="wrap")
@classmethod
def normalize_attribute_names(
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
) -> Self:
"""Normalize payload attribute names.

:rfc:`RFC7643 §2.1 <7653#section-2.1>` indicate that attribute
names should be case insensitive. Any attribute name is
camelized so any case is handled the same way.
"""

def camelize_attribute_name(attr_name: str) -> str:
is_extension_attribute = ":" in attr_name
return to_camel(attr_name) if not is_extension_attribute else attr_name

def camelize_value(value: Any) -> Any:
if isinstance(value, dict):
return {
camelize_attribute_name(k): camelize_value(v)
for k, v in value.items()
}
return value

camelized_value = camelize_value(value)
return handler(camelized_value)

@model_validator(mode="wrap")
@classmethod
def check_response_attributes_returnability(
Expand Down
16 changes: 16 additions & 0 deletions scim2_models/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import re
from typing import Optional

from pydantic.alias_generators import to_snake


def int_to_str(status: Optional[int]) -> Optional[str]:
return None if status is None else str(status)


def to_camel(string: str) -> str:
"""Transform strings to camelCase.

This is more or less the pydantic implementation, but it does not
add uppercase on alphanumerical characters after specials
characters. For instance '$ref' stays '$ref'.
"""

snake = to_snake(string)
camel = re.sub(r"_+([0-9A-Za-z]+)", lambda m: m.group(1).title(), snake)
return camel
30 changes: 30 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import uuid

from scim2_models import BulkRequest
from scim2_models import BulkResponse
Expand Down Expand Up @@ -133,3 +134,32 @@ def test_everything_is_optional():
]
for model in models:
model()


def test_case_sensitivity():
"""RFC7643 §2.1 indicates that attribute names should be case insensitive.

Attribute names are case insensitive and are often "camel-cased"
(e.g., "camelCase").

Reported by issue #39.
"""

payload = {
"UserName": "UserName123",
"Active": True,
"DisplayName": "BobIsAmazing",
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"externalId": uuid.uuid4().hex,
"name": {
"formatted": "Ryan Leenay",
"familyName": "Leenay",
"givenName": "Ryan",
},
"emails": [
{"Primary": True, "type": "work", "value": "testing@bob.com"},
{"Primary": False, "type": "home", "value": "testinghome@bob.com"},
],
}
user = User.model_validate(payload)
assert user.display_name == "BobIsAmazing"
14 changes: 14 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from scim2_models.utils import to_camel


def test_to_camel():
assert to_camel("foo") == "foo"
assert to_camel("Foo") == "foo"
assert to_camel("fooBar") == "fooBar"
assert to_camel("FooBar") == "fooBar"
assert to_camel("foo_bar") == "fooBar"
assert to_camel("Foo_bar") == "fooBar"
assert to_camel("foo_Bar") == "fooBar"
assert to_camel("Foo_Bar") == "fooBar"

assert to_camel("$foo$") == "$foo$"