Skip to content

Commit

Permalink
Merge pull request #437 from DBCG/feature-plugins-test-framework
Browse files Browse the repository at this point in the history
WIP Test Framework
  • Loading branch information
JPercival authored Jan 26, 2022
2 parents 64b40e7 + 30c9c4e commit cda1032
Show file tree
Hide file tree
Showing 116 changed files with 3,125 additions and 3,118 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"java",
"json",
"xml",
"md"
"markdown"
],
"cSpell.ignorePaths": [
".git/objects",
Expand All @@ -31,6 +31,7 @@
"editor.formatOnSave": true
},
"cSpell.words": [
"DEQM",
"Gson",
"numer",
"reindex"
Expand Down
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ To run the cqf-ruler directory from this project use:

`java -jar server/target/cqf-ruler-server-*.war`

### Module Structure

The cqf-ruler uses the hapi-fhir-jpaserver-starter project as a base. On top of that, it adds an extensiblity API and utility functions to allow creating plugins which contain functionality for a specific IG. This diagram shows how it's structured

![Module Diagram](docs/diagrams/modules.drawio.svg)

### Plugins

Plugins use Spring Boot [autoconfiguration](https://docs.spring.io/spring-boot/docs/2.0.0.M3/reference/html/boot-features-developing-auto-configuration.html) to be loaded at runtime. Spring searches for a `spring.factories` file in the meta-data of the jars on the classpath, and the `spring.factories` file points to the root Spring config for the plugin. For example, the content of the `resources/META-INF/spring.factories` file might be:
Expand Down Expand Up @@ -104,6 +110,62 @@ To this end:
* The CQF Ruler project has adopted the HAPI Coding Conventions: <https://github.com/hapifhir/hapi-fhir/wiki/Contributing>
* Plugins should generally use the "hapi.fhir" prefix for configuration properties

### Utility Guidelines

#### Types of Utilities

In general, reusable utilities are separated along two different dimensions, Classes and Behaviors.

Class specific utilities are functions that are associated with specific class or interface, and add functionality to that class.

Behavior specific utilities allow the reuse of behavior across many different classes.

#### Class Specific Utilities

Utility or Helper methods that are associated with a single class should go into a class that has the pluralized name of the associated class. For example, utilities for `Client` should go into the `Clients` class. This ensures that the utility class is focused on one class and allows for more readable code:

`Clients.forUrl("test.com")`

as opposed to:

`ClientUtilities.createClient("test.com")`

or, if you put unrelated code into the class, you might end up with something like:

`Clients.parseRegex()`

If the code doesn't read clearly after you've added an utility, consider that it may not be in the right place.

In general, all the functions for this type of utility should be `static`. No internal state should be maintained (`static final`, or immutable, state is ok). If you final that your utility class contains mutable state, consider an alternate design.

Examples

* Factory functions
* Adding behavior to a class you can't extend

#### Behavior Specific Utilities

If there is behavior you'd like to share across many classes, model that as an interface and use a name that follows the pattern `"ThingDoer"`. For example, all the classes that access a database might be `DatabaseReader`. Use `default` interface implementations to write logic that can be shared many places. The interfaces themselves shouldn't have mutable state (again `static final` is ok). If it's necessary for the for shared logic to have access to state, model that as an method without a default implementation. For example:

```java
interface DatabaseReader {
Database getDb();
default Entity read(Id id) {
return getDb().connect().find(id);
}
}
```

In the above example any class that has access to a `Database` can inherit the `read` behavior.

Examples

* Cross-cutting concerns

### Discovery

Following conventions such as these make it easier for the next developer to find code that's already been implemented as opposed to reinventing the wheel.

## Commit Policy

All new development takes place on `<feature>` branches off `master`. Once feature development on the branch is complete, the feature branch is submitted to `master` as a PR. The PR is reviewed by maintainers and regression testing by the CI build occurs.
Expand Down
87 changes: 82 additions & 5 deletions core/pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.opencds.cqf.ruler</groupId>
Expand All @@ -22,11 +23,80 @@
<version>0.5.0-SNAPSHOT</version>
</dependency>

<!-- The following dependencies are only needed for automated unit tests, you do not neccesarily need them to run the example. -->
<dependency>
<groupId>org.opencds.cqf.ruler</groupId>
<artifactId>cqf-ruler-test</artifactId>
<version>0.5.0-SNAPSHOT</version>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>

Expand All @@ -35,5 +105,12 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
</project>
3 changes: 2 additions & 1 deletion core/src/main/java/org/opencds/cqf/ruler/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
Expand All @@ -35,7 +36,7 @@
Application.class }, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {
FhirTesterConfig.class, org.opencds.cqf.ruler.external.Application.class, StarterCqlR4Config.class,
StarterCqlDstu3Config.class, CqlR4Config.class, CqlDstu3Config.class, BaseCqlConfig.class }))
@SpringBootApplication(exclude = { ElasticsearchRestClientAutoConfiguration.class })
@SpringBootApplication(exclude = { ElasticsearchRestClientAutoConfiguration.class, QuartzAutoConfiguration.class })
@Import({ SubscriptionSubmitterConfig.class, SubscriptionProcessorConfig.class, SubscriptionChannelConfig.class,
WebsocketDispatcherConfig.class, MdmConfig.class, TesterUIConfig.class, BeanFinderConfig.class })
public class Application extends SpringBootServletInitializer {
Expand Down
150 changes: 150 additions & 0 deletions core/src/main/java/org/opencds/cqf/ruler/behavior/DaoRegistryUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.opencds.cqf.ruler.behavior;

import static com.google.common.base.Preconditions.checkNotNull;

import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.opencds.cqf.ruler.utility.Ids;
import org.opencds.cqf.ruler.utility.TypedBundleProvider;

import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;

/**
* Simulate FhirDal operations until such time as that is fully baked
*/
public interface DaoRegistryUser {

public DaoRegistry getDaoRegistry();

default <T extends IBaseResource> T read(Class<T> theResourceClass, String theIdPart) {
checkNotNull(theResourceClass);
checkNotNull(theIdPart);

return read(theResourceClass, theIdPart, null);
}

default <T extends IBaseResource> T read(Class<T> theResourceClass, String theIdPart, RequestDetails requestDetails) {
checkNotNull(theResourceClass);
checkNotNull(theIdPart);
checkNotNull(requestDetails);

return getDaoRegistry().getResourceDao(theResourceClass).read(Ids.newId(theResourceClass, theIdPart), requestDetails);
}

default <T extends IBaseResource> T read(IIdType theId) {
checkNotNull(theId);

return read(theId, null);
}

@SuppressWarnings("unchecked")
default <T extends IBaseResource> T read(IIdType theId, RequestDetails requestDetails) {
checkNotNull(theId);

return (T) getDaoRegistry().getResourceDao(theId.getResourceType()).read(theId, requestDetails);
}

default <T extends IBaseResource> DaoMethodOutcome create(T theResource) {
checkNotNull(theResource);

return create(theResource, null);
}

@SuppressWarnings("unchecked")
default <T extends IBaseResource> DaoMethodOutcome create(T theResource, RequestDetails requestDetails) {
checkNotNull(theResource);

return ((IFhirResourceDao<T>) getDaoRegistry().getResourceDao(theResource.fhirType())).create(theResource,
requestDetails);
}

default <T extends IBaseResource> DaoMethodOutcome update(T theResource) {
checkNotNull(theResource);

return update(theResource, null);
}

@SuppressWarnings("unchecked")
default <T extends IBaseResource> DaoMethodOutcome update(T theResource, RequestDetails requestDetails) {
checkNotNull(theResource);

return ((IFhirResourceDao<T>) getDaoRegistry().getResourceDao(theResource.fhirType())).update(theResource,
requestDetails);
}

default <T extends IBaseResource> DaoMethodOutcome delete(T theResource) {
checkNotNull(theResource);

return delete(theResource, null);
}

@SuppressWarnings("unchecked")
default <T extends IBaseResource> DaoMethodOutcome delete(T theResource, RequestDetails requestDetails) {
checkNotNull(theResource);

return ((IFhirResourceDao<T>) getDaoRegistry().getResourceDao(theResource.fhirType()))
.delete(theResource.getIdElement(), requestDetails);
}

default DaoMethodOutcome delete(IIdType theIdType) {
checkNotNull(theIdType);

return delete(theIdType, null);
}

default DaoMethodOutcome delete(IIdType theIdType, RequestDetails requestDetails) {
checkNotNull(theIdType);

return getDaoRegistry().getResourceDao(theIdType.getResourceType()).delete(theIdType, requestDetails);
}

default <T extends IBaseBundle> T transaction(T theTransaction) {
checkNotNull(theTransaction);

return transaction(theTransaction, null);
}

@SuppressWarnings("unchecked")
default <T extends IBaseBundle> T transaction(T theTransaction, RequestDetails theRequestDetails) {
checkNotNull(theTransaction);

return (T) getDaoRegistry().getSystemDao().transaction(theRequestDetails, theTransaction);
}

default <T extends IBaseResource> TypedBundleProvider<T> search(Class<T> theResourceClass,
SearchParameterMap theSearchMap) {
checkNotNull(theResourceClass);
checkNotNull(theSearchMap);

return search(theResourceClass, theSearchMap, null);
}

default <T extends IBaseResource> TypedBundleProvider<T> search(Class<T> theResourceClass, SearchParameterMap theSearchMap,
RequestDetails theRequestDetails) {
checkNotNull(theResourceClass);
checkNotNull(theSearchMap);

return TypedBundleProvider.fromBundleProvider(getDaoRegistry().getResourceDao(theResourceClass).search(theSearchMap, theRequestDetails));
}

default IBundleProvider search(String theResourceName, SearchParameterMap theSearchMap) {
checkNotNull(theResourceName);
checkNotNull(theSearchMap);

return search(theResourceName, theSearchMap, null);
}

default IBundleProvider search(String theResourceName, SearchParameterMap theSearchMap,
RequestDetails theRequestDetails) {
checkNotNull(theResourceName);
checkNotNull(theSearchMap);

return getDaoRegistry().getResourceDao(theResourceName).search(theSearchMap);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.opencds.cqf.ruler.behavior;

import ca.uhn.fhir.context.FhirContext;

public interface FhirContextUser {
FhirContext getFhirContext();
}
23 changes: 23 additions & 0 deletions core/src/main/java/org/opencds/cqf/ruler/behavior/IdCreator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.opencds.cqf.ruler.behavior;

import static com.google.common.base.Preconditions.checkNotNull;

import org.hl7.fhir.instance.model.api.IIdType;
import org.opencds.cqf.ruler.utility.Ids;


public interface IdCreator extends FhirContextUser {

default <T extends IIdType> T newId(String theResourceName, String theResourceId) {
checkNotNull(theResourceName);
checkNotNull(theResourceId);

return Ids.newId(getFhirContext(), theResourceName, theResourceId);
}

default <T extends IIdType> T newId(String theResourceId) {
checkNotNull(theResourceId);

return Ids.newId(getFhirContext(), theResourceId);
}
}
Loading

0 comments on commit cda1032

Please sign in to comment.