From 36cbca65a8a40c8827c9a2fd1c937877d054003f Mon Sep 17 00:00:00 2001 From: Michael Buckley Date: Wed, 28 Aug 2024 15:02:26 -0400 Subject: [PATCH] New functions for Consent evaluation (#6250) Little tools for consent evaluation. --- .../consent/ConsentOperationStatusEnum.java | 69 +++++++++ .../ConsentOperationStatusEnumTest.java | 135 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/consent/ConsentOperationStatusEnumTest.java diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/ConsentOperationStatusEnum.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/ConsentOperationStatusEnum.java index 9875bcb5e90..81f70b61efe 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/ConsentOperationStatusEnum.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/consent/ConsentOperationStatusEnum.java @@ -19,6 +19,8 @@ */ package ca.uhn.fhir.rest.server.interceptor.consent; +import java.util.stream.Stream; + public enum ConsentOperationStatusEnum { /** @@ -39,4 +41,71 @@ public enum ConsentOperationStatusEnum { * counting/caching methods) */ AUTHORIZED, + ; + + /** + * Assigns ordinals to the verdicts by strength: + * REJECT > AUTHORIZED > PROCEED. + * @return 2/1/0 for REJECT/AUTHORIZED/PROCEED + */ + int getPrecedence() { + switch (this) { + case REJECT: + return 2; + case AUTHORIZED: + return 1; + case PROCEED: + default: + return 0; + } + } + /** + * Evaluate verdicts in order, taking the first "decision" (i.e. first non-PROCEED) verdict. + * + * @return the first decisive verdict, or PROCEED when empty or all PROCEED. + */ + public static ConsentOperationStatusEnum serialEvaluate(Stream theVoteStream) { + return theVoteStream.filter(verdict -> PROCEED != verdict).findFirst().orElse(PROCEED); + } + + /** + * Evaluate verdicts in order, taking the first "decision" (i.e. first non-PROCEED) verdict. + * + * @param theNextVerdict the next verdict to consider + * @return the combined verdict + */ + public ConsentOperationStatusEnum serialReduce(ConsentOperationStatusEnum theNextVerdict) { + if (this != PROCEED) { + return this; + } else { + return theNextVerdict; + } + } + + /** + * Evaluate all verdicts together, allowing any to veto (i.e. REJECT) the operation. + * + * + * @return REJECT if any reject, AUTHORIZED if no REJECT and some AUTHORIZED, PROCEED if empty or all PROCEED + */ + public static ConsentOperationStatusEnum parallelEvaluate(Stream theVoteStream) { + return theVoteStream.reduce(PROCEED, ConsentOperationStatusEnum::parallelReduce); + } + + /** + * Evaluate two verdicts together, allowing either to veto (i.e. REJECT) the operation. + * + * @return REJECT if either reject, AUTHORIZED if no REJECT and some AUTHORIZED, PROCEED otherwise + */ + public ConsentOperationStatusEnum parallelReduce(ConsentOperationStatusEnum theNextVerdict) { + if (theNextVerdict.getPrecedence() > this.getPrecedence()) { + return theNextVerdict; + } else { + return this; + } + } } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/consent/ConsentOperationStatusEnumTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/consent/ConsentOperationStatusEnumTest.java new file mode 100644 index 00000000000..ec046b6bba5 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/interceptor/consent/ConsentOperationStatusEnumTest.java @@ -0,0 +1,135 @@ +package ca.uhn.fhir.rest.server.interceptor.consent; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Arrays; +import java.util.stream.Stream; + +import static ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum.AUTHORIZED; +import static ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum.PROCEED; +import static ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum.REJECT; +import static org.junit.jupiter.api.Assertions.*; + +class ConsentOperationStatusEnumTest { + + /** + * With "serial" evaluation, the first non-PROCEED verdict wins. + */ + @ParameterizedTest + @CsvSource(textBlock = """ + REJECT REJECT REJECT , REJECT + REJECT REJECT PROCEED , REJECT + REJECT REJECT AUTHORIZED, REJECT + REJECT PROCEED REJECT , REJECT + REJECT PROCEED PROCEED , REJECT + REJECT PROCEED AUTHORIZED, REJECT + REJECT AUTHORIZED REJECT , REJECT + REJECT AUTHORIZED PROCEED , REJECT + REJECT AUTHORIZED AUTHORIZED, REJECT + PROCEED REJECT REJECT , REJECT + PROCEED REJECT PROCEED , REJECT + PROCEED REJECT AUTHORIZED, REJECT + PROCEED PROCEED REJECT , REJECT + PROCEED PROCEED PROCEED , PROCEED + PROCEED PROCEED AUTHORIZED, AUTHORIZED + PROCEED AUTHORIZED REJECT , AUTHORIZED + PROCEED AUTHORIZED PROCEED , AUTHORIZED + PROCEED AUTHORIZED AUTHORIZED, AUTHORIZED + AUTHORIZED REJECT REJECT , AUTHORIZED + AUTHORIZED REJECT PROCEED , AUTHORIZED + AUTHORIZED REJECT AUTHORIZED, AUTHORIZED + AUTHORIZED PROCEED REJECT , AUTHORIZED + AUTHORIZED PROCEED PROCEED , AUTHORIZED + AUTHORIZED PROCEED AUTHORIZED, AUTHORIZED + AUTHORIZED AUTHORIZED REJECT , AUTHORIZED + AUTHORIZED AUTHORIZED PROCEED , AUTHORIZED + AUTHORIZED AUTHORIZED AUTHORIZED, AUTHORIZED + """) + void testSerialEvaluation_choosesFirstVerdict(String theInput, ConsentOperationStatusEnum theExpectedResult) { + // given + Stream consentOperationStatusEnumStream = Arrays.stream(theInput.split(" +")) + .map(String::trim) + .map(ConsentOperationStatusEnum::valueOf); + + // when + ConsentOperationStatusEnum result = ConsentOperationStatusEnum.serialEvaluate(consentOperationStatusEnumStream); + + assertEquals(theExpectedResult, result); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + REJECT , REJECT , REJECT + REJECT , PROCEED , REJECT + REJECT , AUTHORIZED, REJECT + AUTHORIZED, REJECT , AUTHORIZED + AUTHORIZED, PROCEED , AUTHORIZED + AUTHORIZED, AUTHORIZED, AUTHORIZED + PROCEED , REJECT , REJECT + PROCEED , PROCEED , PROCEED + PROCEED , AUTHORIZED, AUTHORIZED + """) + void testSerialReduction_choosesFirstVerdict(ConsentOperationStatusEnum theFirst, ConsentOperationStatusEnum theSecond, ConsentOperationStatusEnum theExpectedResult) { + + // when + ConsentOperationStatusEnum result = theFirst.serialReduce(theSecond); + + assertEquals(theExpectedResult, result); + } + + + /** + * With "parallel" evaluation, the "strongest" verdict wins. + * REJECT > AUTHORIZED > PROCEED. + */ + @ParameterizedTest + @CsvSource(textBlock = """ + REJECT REJECT REJECT , REJECT + REJECT REJECT PROCEED , REJECT + REJECT REJECT AUTHORIZED, REJECT + REJECT PROCEED REJECT , REJECT + REJECT PROCEED PROCEED , REJECT + REJECT PROCEED AUTHORIZED, REJECT + REJECT AUTHORIZED REJECT , REJECT + REJECT AUTHORIZED PROCEED , REJECT + REJECT AUTHORIZED AUTHORIZED, REJECT + PROCEED REJECT REJECT , REJECT + PROCEED REJECT PROCEED , REJECT + PROCEED REJECT AUTHORIZED, REJECT + PROCEED PROCEED REJECT , REJECT + PROCEED PROCEED PROCEED , PROCEED + PROCEED PROCEED AUTHORIZED, AUTHORIZED + PROCEED AUTHORIZED REJECT , REJECT + PROCEED AUTHORIZED PROCEED , AUTHORIZED + PROCEED AUTHORIZED AUTHORIZED, AUTHORIZED + AUTHORIZED REJECT REJECT , REJECT + AUTHORIZED REJECT PROCEED , REJECT + AUTHORIZED REJECT AUTHORIZED, REJECT + AUTHORIZED PROCEED REJECT , REJECT + AUTHORIZED PROCEED PROCEED , AUTHORIZED + AUTHORIZED PROCEED AUTHORIZED, AUTHORIZED + AUTHORIZED AUTHORIZED REJECT , REJECT + AUTHORIZED AUTHORIZED PROCEED , AUTHORIZED + AUTHORIZED AUTHORIZED AUTHORIZED, AUTHORIZED + """) + void testParallelReduction_strongestVerdictWins(String theInput, ConsentOperationStatusEnum theExpectedResult) { + // given + Stream consentOperationStatusEnumStream = Arrays.stream(theInput.split(" +")) + .map(String::trim) + .map(ConsentOperationStatusEnum::valueOf); + + // when + ConsentOperationStatusEnum result = ConsentOperationStatusEnum.parallelEvaluate(consentOperationStatusEnumStream); + + assertEquals(theExpectedResult, result); + } + + @Test + void testStrengthOrder() { + assertTrue(REJECT.getPrecedence() > AUTHORIZED.getPrecedence()); + assertTrue(AUTHORIZED.getPrecedence() > PROCEED.getPrecedence()); + } + +}