|
29 | 29 | from scim2_models.utils import _to_camel |
30 | 30 |
|
31 | 31 |
|
32 | | -def _is_attribute_requested(requested_urns: list[str], current_urn: str) -> bool: |
33 | | - """Check if an attribute should be included based on the requested URNs. |
| 32 | +def _short_attr_path(urn: str) -> str: |
| 33 | + """Extract the short attribute path from a full URN. |
| 34 | +
|
| 35 | + For URNs like ``urn:...:User:userName``, returns ``userName``. |
| 36 | + For URNs like ``urn:...:User:name.familyName``, returns ``name.familyName``. |
| 37 | + For short names like ``userName``, returns ``userName`` as-is. |
| 38 | + """ |
| 39 | + if ":" in urn: |
| 40 | + return urn.rsplit(":", 1)[1] |
| 41 | + return urn |
| 42 | + |
| 43 | + |
| 44 | +def _attr_matches(requested: str, current_urn: str) -> bool: |
| 45 | + """Check if a single requested attribute matches the current field URN. |
| 46 | +
|
| 47 | + Supports short names (``userName``), dotted paths (``name.familyName``), |
| 48 | + and full extension URNs. Handles parent/child relationships. |
| 49 | + """ |
| 50 | + req_lower = requested.lower() |
| 51 | + |
| 52 | + if ":" in requested: |
| 53 | + current_lower = current_urn.lower() |
| 54 | + return ( |
| 55 | + current_lower == req_lower |
| 56 | + or req_lower.startswith(current_lower + ":") |
| 57 | + or req_lower.startswith(current_lower + ".") |
| 58 | + or current_lower.startswith(req_lower + ".") |
| 59 | + or current_lower.startswith(req_lower + ":") |
| 60 | + ) |
| 61 | + |
| 62 | + current_short = _short_attr_path(current_urn).lower() |
| 63 | + return ( |
| 64 | + current_short == req_lower |
| 65 | + or current_short.startswith(req_lower + ".") |
| 66 | + or req_lower.startswith(current_short + ".") |
| 67 | + ) |
| 68 | + |
| 69 | + |
| 70 | +def _exact_attr_match(attrs: list[str], current_urn: str) -> bool: |
| 71 | + """Check if current_urn exactly matches any entry in attrs (case-insensitive). |
| 72 | +
|
| 73 | + Used for ``excludedAttributes`` matching and :attr:`Returned.request` checking, |
| 74 | + where parent/child relationship should not apply. |
| 75 | + """ |
| 76 | + current_short = _short_attr_path(current_urn).lower() |
| 77 | + for attr in attrs: |
| 78 | + attr_lower = attr.lower() |
| 79 | + if ":" in attr: |
| 80 | + if current_urn.lower() == attr_lower: |
| 81 | + return True |
| 82 | + else: |
| 83 | + if current_short == attr_lower: |
| 84 | + return True |
| 85 | + return False |
| 86 | + |
| 87 | + |
| 88 | +def _is_attribute_requested(requested_attrs: list[str], current_urn: str) -> bool: |
| 89 | + """Check if an attribute should be included based on the requested attributes. |
34 | 90 |
|
35 | 91 | Returns True if: |
36 | 92 | - The current attribute is explicitly requested |
37 | 93 | - A sub-attribute of the current attribute is requested |
38 | 94 | - The current attribute is a sub-attribute of a requested attribute |
39 | 95 | """ |
40 | | - return ( |
41 | | - current_urn in requested_urns |
42 | | - or any( |
43 | | - item.startswith(f"{current_urn}.") or item.startswith(f"{current_urn}:") |
44 | | - for item in requested_urns |
45 | | - ) |
46 | | - or any(current_urn.startswith(f"{item}.") for item in requested_urns) |
47 | | - ) |
| 96 | + return any(_attr_matches(req, current_urn) for req in requested_attrs) |
48 | 97 |
|
49 | 98 |
|
50 | 99 | class BaseModel(PydanticBaseModel): |
@@ -459,7 +508,11 @@ def _set_complex_attribute_urns(self) -> None: |
459 | 508 | if not attr_type or not is_complex_attribute(attr_type): |
460 | 509 | continue |
461 | 510 |
|
462 | | - schema = f"{main_schema}{separator}{field_name}" |
| 511 | + alias = ( |
| 512 | + self.__class__.model_fields[field_name].serialization_alias |
| 513 | + or field_name |
| 514 | + ) |
| 515 | + schema = f"{main_schema}{separator}{alias}" |
463 | 516 |
|
464 | 517 | if attr_value := getattr(self, field_name): |
465 | 518 | if isinstance(attr_value, list): |
@@ -517,28 +570,26 @@ def _scim_response_serializer( |
517 | 570 | """Serialize the fields according to returnability indications passed in the serialization context.""" |
518 | 571 | returnability = self.get_field_annotation(info.field_name, Returned) |
519 | 572 | attribute_urn = self.get_attribute_urn(info.field_name) |
520 | | - included_urns = info.context.get("scim_attributes", []) if info.context else [] |
521 | | - excluded_urns = ( |
| 573 | + included_attrs = info.context.get("scim_attributes", []) if info.context else [] |
| 574 | + excluded_attrs = ( |
522 | 575 | info.context.get("scim_excluded_attributes", []) if info.context else [] |
523 | 576 | ) |
524 | 577 |
|
525 | | - attribute_urn = _normalize_attribute_name(attribute_urn) |
526 | | - included_urns = [_normalize_attribute_name(urn) for urn in included_urns] |
527 | | - excluded_urns = [_normalize_attribute_name(urn) for urn in excluded_urns] |
528 | | - |
529 | 578 | if returnability == Returned.never: |
530 | 579 | return None |
531 | 580 |
|
532 | 581 | if returnability == Returned.default and ( |
533 | 582 | ( |
534 | | - included_urns |
535 | | - and not _is_attribute_requested(included_urns, attribute_urn) |
| 583 | + included_attrs |
| 584 | + and not _is_attribute_requested(included_attrs, attribute_urn) |
536 | 585 | ) |
537 | | - or attribute_urn in excluded_urns |
| 586 | + or _exact_attr_match(excluded_attrs, attribute_urn) |
538 | 587 | ): |
539 | 588 | return None |
540 | 589 |
|
541 | | - if returnability == Returned.request and attribute_urn not in included_urns: |
| 590 | + if returnability == Returned.request and not _exact_attr_match( |
| 591 | + included_attrs, attribute_urn |
| 592 | + ): |
542 | 593 | return None |
543 | 594 |
|
544 | 595 | return value |
|
0 commit comments