diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java index 0d7e291374c..7bcede37df2 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBindingMethodParameterBuilder.java @@ -96,7 +96,6 @@ static Object[] tryBuildMethodParams(Method theMethod, Object[] theMethodParams) theMethod, parameterTypeWithOperationEmbeddedParam, theMethodParams); } - // LUKETODO: UNIT TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! private static Object[] determineMethodParamsForOperationEmbeddedParams( Method theMethod, Class theParameterTypeWithOperationEmbeddedParam, Object[] theMethodParams) throws InvocationTargetException, IllegalAccessException, InstantiationException { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java index e2268600514..dfe1e2eeaf4 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/EmbeddedParameterConverter.java @@ -30,6 +30,7 @@ public class EmbeddedParameterConverter { private final FhirContext myContext; private final Method myMethod; private final Operation myOperation; + // LUKETODO: warning? private final Class[] myParameterTypes; private final Class myOperationEmbeddedType; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index 4370c1cddc0..2c8b4ed4db2 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -146,7 +146,6 @@ public static List getResourceParameters( parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(methodToUse, paramIndex); declaredParameterType = parameterType; } - // LUKETODO: as a guard: if this is still a Collection, then throw because something went wrong if (Collection.class.isAssignableFrom(parameterType)) { throw new ConfigurationException( Msg.code(401) + "Argument #" + paramIndex + " of Method '" + methodToUse.getName() diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java index 50eb26e4260..c4f176fa553 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationEmbeddedParameter.java @@ -42,6 +42,7 @@ import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.ReflectionUtil; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.*; @@ -141,6 +142,11 @@ public String getParamType() { return myParamType; } + @VisibleForTesting + public Class getInnerCollectionType() { + return myInnerCollectionType; + } + public String getSearchParamType() { if (mySearchParameterBinding != null) { return mySearchParameterBinding.getParamType().getCode(); @@ -148,6 +154,11 @@ public String getSearchParamType() { return null; } + @VisibleForTesting + public String getOperationName() { + return myOperationName; + } + @SuppressWarnings("unchecked") @Override public void initializeTypes( @@ -197,7 +208,6 @@ public void initializeTypes( * should probably clean this up.. */ if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { - // LUKETODO: this is where we get the Exception: add an else if if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { myParamType = "Resource"; } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index 13f11945e3f..4e5e40f4a3f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -49,6 +49,7 @@ import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.ReflectionUtil; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseCoding; @@ -149,6 +150,11 @@ public String getParamType() { return myParamType; } + @VisibleForTesting + public Class getInnerCollectionType() { + return myInnerCollectionType; + } + public String getSearchParamType() { if (mySearchParameterBinding != null) { return mySearchParameterBinding.getParamType().getCode(); @@ -156,6 +162,11 @@ public String getSearchParamType() { return null; } + @VisibleForTesting + String getOperationName() { + return myOperationName; + } + @SuppressWarnings("unchecked") @Override public void initializeTypes( diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java index 0ed9646926e..e440d92b31e 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsOperationProvider.java @@ -47,7 +47,6 @@ public CareGapsOperationProvider( myStringTimePeriodHandler = theStringTimePeriodHandler; } - // LUKETODO: fix javadoc /** * Implements the $care-gaps @@ -74,19 +73,7 @@ public CareGapsOperationProvider( * * @param theRequestDetails generally auto-populated by the HAPI server * framework. - * @param thePeriodStart the start of the gaps through period - * @param thePeriodEnd the end of the gaps through period - * @param theSubject a reference to either a Patient or Group for which - * the gaps in care report(s) will be generated - * @param theStatus the status code of gaps in care reports that will be - * included in the result - * @param theMeasureId the id of Measure(s) for which the gaps in care - * report(s) will be calculated - * @param theMeasureIdentifier the identifier of Measure(s) for which the gaps in - * care report(s) will be calculated - * @param theMeasureUrl the canonical URL of Measure(s) for which the gaps - * in care report(s) will be calculated - * @param theNonDocument defaults to 'false' which returns standard 'document' bundle for `$care-gaps`. + * @param theParams Please refer to the javadoc for {@link CareGapsParams} for more information on the parameters. * If 'true', this will return summarized subject bundle with only detectedIssue resource. * @return Parameters of bundles of Care Gap Measure Reports */ diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java index cacb76a1a24..120669e462d 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/CareGapsParams.java @@ -5,8 +5,50 @@ import org.hl7.fhir.r4.model.CanonicalType; import java.util.List; +import java.util.Objects; import java.util.StringJoiner; +/** + * Non-RequestDetails parameters for the $care-gaps + * operation found in the + * Da Vinci DEQM + * FHIR Implementation Guide that overrides the $care-gaps + * operation found in the + * FHIR Clinical + * Reasoning Module. + *

+ * The operation calculates measures describing gaps in care. For more details, + * reference the Gaps + * in Care Reporting section of the + * Da Vinci DEQM + * FHIR Implementation Guide. + *

+ * A Parameters resource that includes zero to many document bundles that + * include Care Gap Measure Reports will be returned. + *

+ * Usage: + * URL: [base]/Measure/$care-gaps + *

+ * myRequestDetails generally auto-populated by the HAPI server + * framework. + * myPeriodStart the start of the gaps through period + * myPeriodEnd the end of the gaps through period + * mySubject a reference to either a Patient or Group for which + * the gaps in care report(s) will be generated + * myStatus the status code of gaps in care reports that will be + * included in the result + * myMeasureId the id of Measure(s) for which the gaps in care + * report(s) will be calculated + * myMeasureIdentifier the identifier of Measure(s) for which the gaps in + * care report(s) will be calculated + * myMeasureUrl the canonical URL of Measure(s) for which the gaps + * in care report(s) will be calculated + * myNonDocument defaults to 'false' which returns standard 'document' bundle for `$care-gaps`. + * If 'true', this will return summarized subject bundle with only detectedIssue resource. + */ public class CareGapsParams { @OperationEmbeddedParam(name = "periodStart") private final String myPeriodStart; @@ -51,6 +93,17 @@ public CareGapsParams( this.myNonDocument = myNonDocument; } + private CareGapsParams(Builder builder) { + this.myPeriodStart = builder.myPeriodStart; + this.myPeriodEnd = builder.myPeriodEnd; + this.mySubject = builder.mySubject; + this.myStatus = builder.myStatus; + this.myMeasureId = builder.myMeasureId; + this.myMeasureIdentifier = builder.myMeasureIdentifier; + this.myMeasureUrl = builder.myMeasureUrl; + this.myNonDocument = builder.myNonDocument; + } + public String getPeriodStart() { return myPeriodStart; } @@ -83,6 +136,33 @@ public BooleanType getNonDocument() { return myNonDocument; } + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + CareGapsParams that = (CareGapsParams) o; + return Objects.equals(myPeriodStart, that.myPeriodStart) + && Objects.equals(myPeriodEnd, that.myPeriodEnd) + && Objects.equals(mySubject, that.mySubject) + && Objects.equals(myStatus, that.myStatus) + && Objects.equals(myMeasureId, that.myMeasureId) + && Objects.equals(myMeasureIdentifier, that.myMeasureIdentifier) + && Objects.equals(myMeasureUrl, that.myMeasureUrl) + && Objects.equals(myNonDocument, that.myNonDocument); + } + + @Override + public int hashCode() { + return Objects.hash( + myPeriodStart, + myPeriodEnd, + mySubject, + myStatus, + myMeasureId, + myMeasureIdentifier, + myMeasureUrl, + myNonDocument); + } + @Override public String toString() { return new StringJoiner(", ", CareGapsParams.class.getSimpleName() + "[", "]") @@ -96,4 +176,63 @@ public String toString() { .add("myNonDocument=" + myNonDocument) .toString(); } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String myPeriodStart; + private String myPeriodEnd; + private String mySubject; + private List myStatus; + private List myMeasureId; + private List myMeasureIdentifier; + private List myMeasureUrl; + private BooleanType myNonDocument; + + public Builder setPeriodStart(String myPeriodStart) { + this.myPeriodStart = myPeriodStart; + return this; + } + + public Builder setPeriodEnd(String myPeriodEnd) { + this.myPeriodEnd = myPeriodEnd; + return this; + } + + public Builder setSubject(String mySubject) { + this.mySubject = mySubject; + return this; + } + + public Builder setStatus(List myStatus) { + this.myStatus = myStatus; + return this; + } + + public Builder setMeasureId(List myMeasureId) { + this.myMeasureId = myMeasureId; + return this; + } + + public Builder setMeasureIdentifier(List myMeasureIdentifier) { + this.myMeasureIdentifier = myMeasureIdentifier; + return this; + } + + public Builder setMeasureUrl(List myMeasureUrl) { + this.myMeasureUrl = myMeasureUrl; + return this; + } + + public Builder setNonDocument(BooleanType myNonDocument) { + this.myNonDocument = myNonDocument; + return this; + } + + public CareGapsParams build() { + return new CareGapsParams(this); + } + } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java index 491baadddd2..c94320e3781 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/EvaluateMeasureSingleParams.java @@ -7,9 +7,31 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters; +import java.util.Objects; import java.util.StringJoiner; +/** + * Non-RequestDetails parameters for the $evaluate-measure + * operation found in the + * FHIR Clinical + * Reasoning Module. This implementation aims to be compatible with the CQF + * IG. + *

+ * myeId the id of the Measure to evaluate + * myPeriodStart The start of the reporting period + * myPeriodEnd The end of the reporting period + * myReportType The type of MeasureReport to generate + * mySubject the subject to use for the evaluation + * myPractitioner the practitioner to use for the evaluation + * myLastReceivedOn the date the results of this measure were last + * received. + * myProductLine the productLine (e.g. Medicare, Medicaid, etc) to use + * for the evaluation. This is a non-standard parameter. + * myAdditionalData the data bundle containing additional data + */ public class EvaluateMeasureSingleParams { + // LUKETODO: should we defined a new @IdEmbeddedParam annotation? @IdParam private final IdType myId; @@ -68,6 +90,20 @@ public EvaluateMeasureSingleParams( this.myParameters = myParameters; } + private EvaluateMeasureSingleParams(Builder builder) { + this.myId = builder.myId; + this.myPeriodStart = builder.myPeriodStart; + this.myPeriodEnd = builder.myPeriodEnd; + this.myReportType = builder.myReportType; + this.mySubject = builder.mySubject; + this.myPractitioner = builder.myPractitioner; + this.myLastReceivedOn = builder.myLastReceivedOn; + this.myProductLine = builder.myProductLine; + this.myAdditionalData = builder.myAdditionalData; + this.myTerminologyEndpoint = builder.myTerminologyEndpoint; + this.myParameters = builder.myParameters; + } + public IdType getId() { return myId; } @@ -112,6 +148,41 @@ public Parameters getParameters() { return myParameters; } + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + EvaluateMeasureSingleParams that = (EvaluateMeasureSingleParams) o; + return Objects.equals(myId, that.myId) + && Objects.equals(myPeriodStart, that.myPeriodStart) + && Objects.equals(myPeriodEnd, that.myPeriodEnd) + && Objects.equals(myReportType, that.myReportType) + && Objects.equals(mySubject, that.mySubject) + && Objects.equals(myPractitioner, that.myPractitioner) + && Objects.equals(myLastReceivedOn, that.myLastReceivedOn) + && Objects.equals(myProductLine, that.myProductLine) + && Objects.equals(myAdditionalData, that.myAdditionalData) + && Objects.equals(myTerminologyEndpoint, that.myTerminologyEndpoint) + && Objects.equals(myParameters, that.myParameters); + } + + @Override + public int hashCode() { + return Objects.hash( + myId, + myPeriodStart, + myPeriodEnd, + myReportType, + mySubject, + myPractitioner, + myLastReceivedOn, + myProductLine, + myAdditionalData, + myTerminologyEndpoint, + myParameters); + } + @Override public String toString() { return new StringJoiner(", ", EvaluateMeasureSingleParams.class.getSimpleName() + "[", "]") @@ -128,4 +199,81 @@ public String toString() { .add("myParameters=" + myParameters) .toString(); } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private IdType myId; + private String myPeriodStart; + private String myPeriodEnd; + private String myReportType; + private String mySubject; + private String myPractitioner; + private String myLastReceivedOn; + private String myProductLine; + private Bundle myAdditionalData; + private Endpoint myTerminologyEndpoint; + private Parameters myParameters; + + public Builder setId(IdType myId) { + this.myId = myId; + return this; + } + + public Builder setPeriodStart(String myPeriodStart) { + this.myPeriodStart = myPeriodStart; + return this; + } + + public Builder setPeriodEnd(String myPeriodEnd) { + this.myPeriodEnd = myPeriodEnd; + return this; + } + + public Builder setReportType(String myReportType) { + this.myReportType = myReportType; + return this; + } + + public Builder setSubject(String mySubject) { + this.mySubject = mySubject; + return this; + } + + public Builder setPractitioner(String myPractitioner) { + this.myPractitioner = myPractitioner; + return this; + } + + public Builder setLastReceivedOn(String myLastReceivedOn) { + this.myLastReceivedOn = myLastReceivedOn; + return this; + } + + public Builder setProductLine(String myProductLine) { + this.myProductLine = myProductLine; + return this; + } + + public Builder setAdditionalData(Bundle myAdditionalData) { + this.myAdditionalData = myAdditionalData; + return this; + } + + public Builder setTerminologyEndpoint(Endpoint myTerminologyEndpoint) { + this.myTerminologyEndpoint = myTerminologyEndpoint; + return this; + } + + public Builder setParameters(Parameters myParameters) { + this.myParameters = myParameters; + return this; + } + + public EvaluateMeasureSingleParams build() { + return new EvaluateMeasureSingleParams(this); + } + } } diff --git a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java index 0841fd56f10..36a9ae04bdb 100644 --- a/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java +++ b/hapi-fhir-storage-cr/src/main/java/ca/uhn/fhir/cr/r4/measure/MeasureOperationsProvider.java @@ -45,7 +45,6 @@ public MeasureOperationsProvider( myStringTimePeriodHandler = theStringTimePeriodHandler; } - // LUKETODO: fix javadoc /** * Implements the $evaluate-measure @@ -54,17 +53,7 @@ public MeasureOperationsProvider( * Reasoning Module. This implementation aims to be compatible with the CQF * IG. * - * @param theId the id of the Measure to evaluate - * @param thePeriodStart The start of the reporting period - * @param thePeriodEnd The end of the reporting period - * @param theReportType The type of MeasureReport to generate - * @param theSubject the subject to use for the evaluation - * @param thePractitioner the practitioner to use for the evaluation - * @param theLastReceivedOn the date the results of this measure were last - * received. - * @param theProductLine the productLine (e.g. Medicare, Medicaid, etc) to use - * for the evaluation. This is a non-standard parameter. - * @param theAdditionalData the data bundle containing additional data + * @param theParams Please refer to the javadoc for {@link EvaluateMeasureSingleParams} for more information on the parameters. * @param theRequestDetails The details (such as tenant) of this request. Usually * autopopulated HAPI. * @return the calculated MeasureReport @@ -79,6 +68,10 @@ public MeasureReport evaluateMeasure(EvaluateMeasureSingleParams theParams, Requ // LUKETODO: 2. can we modify OperationParam to support the concept of mututally exclusive // params // LUKETODO: 3. code gen from operation definition + // LUKETODO: 4. is there such as thing as a MUTUALLY EXCLUSIVE ANNotation?. is there such as + // thing as a MUTUALLY EXCLUSIVE ANNotation?4. is there such as thing as a MUTUALLY EXCLUSIVE + // ANNotation?4. is there such as thing as a MUTUALLY EXCLUSIVE ANNotation? + // so 3 different params : try annotations Eithers.forMiddle3(theParams.getId()), // LUKETODO: push this into the hapi-fhir REST framework code myStringTimePeriodHandler.getStartZonedDateTime(theParams.getPeriodStart(), theRequestDetails), diff --git a/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md b/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md new file mode 100644 index 00000000000..223543668d5 --- /dev/null +++ b/hapi-fhir-storage-cr/src/main/resources/docs/operatration-embedded-parameters.md @@ -0,0 +1,66 @@ + + +# Rules + * Provider method must be annotated with @Operation + * Provider method may contain 0 to 1 RequestDetails parameters + * Provider method must contain one and only one embedded parameters class + * The method parameter for this class must NOT have any annotations, especially NOT @OperationParam + * The parameters class must not have any top-level annotations + * The parameters class must be immutable: setters will not be respected by the reflection code + * The parameters class must contain one and only one public constructor for all fields + * The parameters class may contain a Builder class for developer convenience + * The parameters class must contain at least one field annotated with @OperationEmbeddedParam + * The parameters class must contain 0 to 1 @IdParam fields, with the same rules as @IdParam variables + * The parameters class may not contain fields with any annotations other than @IdParam or @OperationEmbeddedParam + * Parameters fields otherwise follow the same rules for @IdParam and @OperationParam fields, namely the types that are allowed, including Collections and IPrimitiveType + * An @OperationEmbeddedParam is equivalent to an OperationEmbeddedParameter + * REST method parameters will be passed as separate values and converted by reflection code at runtime to a single instance of the embedded parameters class before being passed to the operation provider + +## Example + +Here is a simplified example of how this would work in practice for $evaluate-measure: + +```java +public class EvaluateMeasureSingleParams { + @IdParam + private final IdType myId; + + @OperationEmbeddedParam(name = "periodStart") + private final String myPeriodStart; + + @OperationEmbeddedParam(name = "periodEnd") + private final String myPeriodEnd; + + @OperationEmbeddedParam(name = "reportType") + private final String myReportType; + + @OperationEmbeddedParam(name = "subject") + private final String mySubject; + + public EvaluateMeasureSingleParams( + IdType myId, + String myPeriodStart, + String myPeriodEnd, + String myReportType, + String mySubject ) { + this.myId = myId; + this.myPeriodStart = myPeriodStart; + this.myPeriodEnd = myPeriodEnd; + this.myReportType = myReportType; + this.mySubject = mySubject; + } +} +``` + +```java + @Operation(name = ProviderConstants.CR_OPERATION_EVALUATE_MEASURE, idempotent = true, type = Measure.class) + public MeasureReport evaluateMeasure(EvaluateMeasureSingleParams theParams, RequestDetails theRequestDetails) + throws InternalErrorException, FHIRException { + return myR4MeasureServiceFactory + .create(theRequestDetails) + .evaluate(theParams); +} +``` + + + diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java index 46ded8e063f..d4e1fc01dd0 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/method/MethodUtilTest.java @@ -14,7 +14,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.LoggerFactory; -import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.INVALID_METHOD_OPERATION_PARAMS_NO_OPERATION; @@ -26,13 +27,12 @@ import static ca.uhn.fhir.rest.server.method.InnerClassesAndMethods.SUPER_SIMPLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; // LUKETODO: try to test for every case in embedded params where there's a throws // This test lives in hapi-fhir-structures-r4 because if we introduce it in hapi-fhir-server, there will be a // circular dependency -// LUKETODO: do we need mocks at all? -@ExtendWith(MockitoExtension.class) class MethodUtilTest { private static final org.slf4j.Logger ourLog = LoggerFactory.getLogger(MethodUtilTest.class); @@ -41,8 +41,7 @@ class MethodUtilTest { private final InnerClassesAndMethods myInnerClassesAndMethods = new InnerClassesAndMethods(); - @Mock - private Object myProvider; + private final Object myProvider = new Object(); @Test void simpleMethodNoParams() { @@ -68,7 +67,16 @@ void sampleMethodOperationParams() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); - // LUKETODO: assert the actual OperationParameter values + final List expectedParameters = List.of( + new NullParameterToAssert(), + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean") + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -79,7 +87,16 @@ void sampleMethodOperationParamsWithFhirTypes() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationParameter.class, OperationParameter.class, OperationParameter.class); - // LUKETODO: assert the actual OperationParameter values + final List expectedParameters = List.of( + new NullParameterToAssert(), + new OperationParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null), + new OperationParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class,null), + new OperationParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, "boolean") + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -90,7 +107,14 @@ void sampleMethodEmbeddedParams() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); - // LUKETODO: assert the actual OperationEmbeddedParameter values + final List expectedParameters = List.of( + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, null, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null) + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -101,7 +125,15 @@ void sampleMethodEmbeddedParamsRequestDetailsFirst() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(RequestDetailsParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); - // LUKETODO: assert the actual OperationEmbeddedParameter values + final List expectedParameters = List.of( + new RequestDetailsParameterToAssert(), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,ArrayList.class, String.class, null) + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -112,7 +144,15 @@ void sampleMethodEmbeddedParamsRequestDetailsLast() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(OperationEmbeddedParameter.class, OperationEmbeddedParameter.class, RequestDetailsParameter.class); - // LUKETODO: assert the actual OperationEmbeddedParameter values + final List expectedParameters = List.of( + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null), + new RequestDetailsParameterToAssert() + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -123,7 +163,16 @@ void sampleMethodEmbeddedParamsWithFhirTypes() { assertThat(resourceParameters).isNotEmpty(); assertThat(resourceParameters).hasExactlyElementsOfTypes(NullParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class, OperationEmbeddedParameter.class); - // LUKETODO: assert the actual OperationEmbeddedParameter values + final List expectedParameters = List.of( + new NullParameterToAssert(), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param1", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param2", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS, ArrayList.class, String.class, null), + new OperationEmbeddedParameterToAssert(ourFhirContext, "param3", SAMPLE_METHOD_EMBEDDED_TYPE_NO_REQUEST_DETAILS,null, String.class, "boolean") + ); + + assertThat(resourceParameters) + .matches(theActualParameters -> assertParametersEqual(expectedParameters, theActualParameters), + "Expected parameters do not match actual parameters"); } @Test @@ -281,7 +330,79 @@ private List getMethodAndExecute(String theMethodName, Class... t myProvider); } - private List getResourceParameters(Method theMethod) { - return MethodUtil.getResourceParameters(ourFhirContext, theMethod, myProvider); + private boolean assertParametersEqual(List theExpectedParameters, List theActualParameters) { + if (theActualParameters.size() != theExpectedParameters.size()) { + fail("Expected parameters size does not match actual parameters size"); + return false; + } + + for (int i = 0; i < theActualParameters.size(); i++) { + final IParameterToAssert expectedParameter = theExpectedParameters.get(i); + final IParameter actualParameter = theActualParameters.get(i); + + if (! assertParametersEqual(expectedParameter, actualParameter)) { + return false; + } + } + + return true; + } + + private boolean assertParametersEqual(IParameterToAssert theExpectedParameter, IParameter theActualParameter) { + if (theExpectedParameter instanceof NullParameterToAssert && theActualParameter instanceof NullParameter) { + return true; + } + + if (theExpectedParameter instanceof RequestDetailsParameterToAssert && theActualParameter instanceof RequestDetailsParameter) { + return true; + } + + if (theExpectedParameter instanceof OperationParameterToAssert expectedOperationParameter && theActualParameter instanceof OperationParameter actualOperationParameter) { + assertThat(actualOperationParameter.getContext().getVersion().getVersion()).isEqualTo(expectedOperationParameter.myContext().getVersion().getVersion()); + assertThat(actualOperationParameter.getName()).isEqualTo(expectedOperationParameter.myName()); + assertThat(actualOperationParameter.getParamType()).isEqualTo(expectedOperationParameter.myParamType()); + assertThat(actualOperationParameter.getInnerCollectionType()).isEqualTo(expectedOperationParameter.myInnerCollectionType()); + + return true; + } + + if (theExpectedParameter instanceof OperationEmbeddedParameterToAssert expectedOperationEmbeddedParameter && theActualParameter instanceof OperationEmbeddedParameter actualOperationEmbeddedParameter) { + assertThat(actualOperationEmbeddedParameter.getContext().getVersion().getVersion()).isEqualTo(expectedOperationEmbeddedParameter.myContext().getVersion().getVersion()); + assertThat(actualOperationEmbeddedParameter.getName()).isEqualTo(expectedOperationEmbeddedParameter.myName()); + assertThat(actualOperationEmbeddedParameter.getParamType()).isEqualTo(expectedOperationEmbeddedParameter.myParamType()); + assertThat(actualOperationEmbeddedParameter.getInnerCollectionType()).isEqualTo(expectedOperationEmbeddedParameter.myInnerCollectionType()); + + return true; + } + + return false; + } + + private interface IParameterToAssert {} + + private record NullParameterToAssert() implements IParameterToAssert { + } + + private record RequestDetailsParameterToAssert() implements IParameterToAssert { + } + + private record OperationParameterToAssert( + FhirContext myContext, + String myName, + String myOperationName, + @SuppressWarnings("rawtypes") + Class myInnerCollectionType, + Class myParameterType, + String myParamType) implements IParameterToAssert { + } + + private record OperationEmbeddedParameterToAssert( + FhirContext myContext, + String myName, + String myOperationName, + @SuppressWarnings("rawtypes") + Class myInnerCollectionType, + Class myParameterType, + String myParamType) implements IParameterToAssert { } }