Skip to content
Open
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
9 changes: 8 additions & 1 deletion sandbox/api/tests/test_get_consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,17 @@ def test_get_consent_returns_expected_responses__mocked_get_consent(
200,
),
(
"performer:identifier=9000000017&status=proposed&status=active",
# OR semantics → resource can have either status
"performer:identifier=9000000017&status=proposed,active",
"./api/examples/GET_Consent/filtered-relationships-status-proposed-active.yaml",
200,
),
(
# AND semantics → empty intersection → no results because each resource can only have one status
"performer:identifier=9000000017&status=proposed&status=active",
"./api/examples/errors/invalidated-resource.yaml",
404,
),
(
"performer:identifier=9000000022",
"./api/examples/GET_Consent/multiple-relationships.yaml",
Expand Down
76 changes: 65 additions & 11 deletions sandbox/api/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from json import dumps, load
from logging import getLogger
from typing import Any, List, Optional
from typing import Any, List, Optional, Set

from flask import Request, Response
from yaml import CLoader as Loader
Expand All @@ -26,6 +26,42 @@
FHIR_MIMETYPE = "application/fhir+json"
logger = getLogger(__name__)

VALID_STATUS_VALUES = {"proposed", "active", "rejected", "inactive", "entered-in-error"}


def parse_fhir_status_params(status_list: List[str]) -> Optional[Set[str]]:
"""Parse FHIR search status parameters handling OR (comma-separated) and AND (repeated) semantics.

FHIR search conventions:
- OR: comma-separated values within a single param, e.g. status=active,proposed
- AND: repeated params, e.g. status=active&status=proposed
- AND of ORs: status=active,proposed&status=inactive

For a single-valued field like status, AND groups are intersected. If the intersection
is empty, no records can match (e.g. status=active&status=inactive is always empty).

Args:
status_list (List[str]): Raw list from request.args.getlist("status")

Returns:
Optional[Set[str]]: Effective set of status values to match, or None if any value is invalid.
An empty set means the AND intersection produced no possible matches.
"""
all_values = {value.strip() for item in status_list for value in item.split(",")}

if not all_values.issubset(VALID_STATUS_VALUES):
return None

# Each item in status_list is an AND group; values within each group are OR alternatives
and_groups = [set(value.strip() for value in item.split(",")) for item in status_list]

# Intersect across AND groups — for a single-valued field, only values common to all groups can match
result = and_groups[0]
for group in and_groups[1:]:
result = result & group

return result


def load_json_file(file_name: str) -> dict:
"""Get response from file. Expected file content is a JSON."""
Expand Down Expand Up @@ -273,28 +309,46 @@ def check_for_consent_filtering(
status_inactive_response_yaml: str,
status_proposed_and_active_response_yaml: str,
) -> Response:
"""Checks the GET consent request status params and provides related response
"""Checks the GET consent request status params and provides related response.

Supports FHIR search conventions:
- OR: comma-separated values within a single param, e.g. status=active,proposed
- AND: repeated params, e.g. status=active&status=proposed
For a single-valued field like status, AND groups are intersected; an empty intersection
means no records can match and a 404 is returned.

Args:
status (List[str]): The status parameters supplied to the request
_include (List[str]): The include parameters supplied to the request
status_active_with_details_response_yaml (str): Bundle to return when status param is 'active'
status_inactive_response_yaml (str): Bundle to return when status param is 'inactive'
status_proposed_and_active_response_yaml (str): Bundle to return when status param is 'proposed,inactive'
status_active_with_details_response_yaml (str): Bundle to return when effective status is {'active'}
status_inactive_response_yaml (str): Bundle to return when effective status is {'inactive'}
status_proposed_and_active_response_yaml (str): Bundle to return when effective status is {'proposed', 'active'}

Returns:
response: Resultant Response object based on input.
"""
if status == [] or status is None:
if not status:
return generate_response_from_example(INVALIDATED_RESOURCE, 404)
if status == ["active"]:
if len(_include) == 2 and CONSENT_GRANTEE in _include and CONSENT_GRANTEE in _include:

effective_statuses = parse_fhir_status_params(status)

if effective_statuses is None:
# One or more values were not in the allowed enum
return generate_response_from_example(GET_CONSENT__STATUS_PARAM_INVALID, 422)

if not effective_statuses: # equivalent to effective_statuses == set()
# AND intersection across repeated params produced no common values
return generate_response_from_example(INVALIDATED_RESOURCE, 404)

if effective_statuses == {"active"}:
if len(_include) == 2 and CONSENT_GRANTEE in _include and CONSENT_PATIENT in _include:
return generate_response_from_example(status_active_with_details_response_yaml, 200)
else:
return generate_response_from_example(INVALIDATED_RESOURCE, 404)
elif status == ["inactive"]:
elif effective_statuses == {"inactive"}:
return generate_response_from_example(status_inactive_response_yaml, 200)
elif len(status) == 2 and "active" in status and "proposed" in status:
elif effective_statuses == {"active", "proposed"}:
return generate_response_from_example(status_proposed_and_active_response_yaml, 200)
else:
return generate_response_from_example(GET_CONSENT__STATUS_PARAM_INVALID, 422)
# Valid enum values but no sandbox example exists for this combination
return generate_response_from_example(INVALIDATED_RESOURCE, 404)
27 changes: 18 additions & 9 deletions specification/validated-relationships-service-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2808,20 +2808,30 @@ components:
ConsentStatus:
in: query
name: status
description: Specify one or more `status` to filter the records returned in the bundle. For example you can filter to only `active` or `proposed` proxy relationships.
description: >
Specify one or more `status` values to filter the records returned in the bundle.


Supports FHIR search conventions:

- **OR** — comma-separated values within a single parameter, e.g. `status=active,proposed` returns records with status `active` _or_ `proposed`.

- **AND** — repeated parameters, e.g. `status=active&status=proposed`. For a single-valued field like `status`, AND groups are intersected; disjoint groups will return no results.

- **AND of ORs** — e.g. `status=active,proposed&status=inactive`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am i right in thinking this produces no results - because the AND will return no results as no proxy role has multiple statuses?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's my understanding. I've put logic in the get consent lambda validation to return a 422 in this instance. As you say, status is a singleton value for each proxy role, so any AND values that aren't an exact intersect of each other I've treated as invalid because it may give unexpected results.
i.e. for "status=active, proposed&status=active" the caller may be expecting active or proposed, but this could only return active roles because of the extra AND active clause.



Valid status values are bound to the http://hl7.org/fhir/consent-state-codes CodeSystem: `proposed`, `active`, `rejected`, `inactive`, `entered-in-error`.
required: false
style: form
explode: true
schema:
type: array
items:
type: string
enum:
- proposed
- active
- rejected
- inactive
- entered-in-error
description: >
A single status value, or a comma-separated list of status values representing an OR group.
Valid values are: `proposed`, `active`, `rejected`, `inactive`, `entered-in-error`.
examples:
Active:
summary: Only active authorised proxy relationships
Expand All @@ -2832,8 +2842,7 @@ components:
ActiveOrProposed:
summary: Active OR proposed (but not yet authorised) proxy relationships
value:
- proposed
- active
- proposed,active

BearerAuthorization:
in: header
Expand Down
Loading