Elementals is a gameified to-do application skinned like a fantasy RPG.
Hold on, isn't that just Habitica?
Yes! But different.
The various components of the system are represented by top-level directories (for example, "players" and "tasks"). Inside each component directory, you'll find a variety of modules:
- There will always be a
coremodule - If the component can be delivered by an API, there will be an
apimodule - If the component is delivered by a batch process, there will be some kind of
schedulermodule - There may be other modules for other adapters related to the component (for example, database plugins and wrappers for third-party libraries)
The core module of a component contains the component's high-level policy.
If you open up the core module, you'll find a collection of classes that are
named with verbs (for example, CreatePlayer and CompleteTask).
These are the use cases associated with the component, and are the heart of the application.
Each use case has one public method called perform. Each use case also defines an Outcome
interface which describes the possible outcomes of running the use case.
For example, the LevelUpPlayer.Outcome interface describes the three possible outcomes
of performing the LevelUpPlayer use case:
public class LevelUpPlayer {
public interface Outcome<T> {
T successfullyUpdatedPlayer(SavedPlayer updatedPlayer);
T playerCannotLevel();
T noSuchPlayer();
}
public <T> T perform(PlayerId playerId, Outcome<T> outcome) {
// ...
}When you invoke the use case, you provide an implementation of the Outcome interface describing
what you want to do in each of the possible scenarios.
For example, when API controllers call the use cases,
they provide Outcome instances that create the appropriate ResponseEntitys for each scenario.
The api modules define API endpoints for accessing the use cases.
While these are controller classes, I've named them like SomeUseCaseEndpoint
to highlight the fact that each class defines just one endpoint.
The first thing you'll find in each endpoint class is
a request object defining the shape of the request body the endpoint expects (if there is one),
and a PossibleResponses class describing all the different responses the endpoint could send back.
For example, the CreateHabitEndpoint looks like this:
@RestController
public class CreateHabitEndpoint {
private static class CreateHabitRequest {
@JsonProperty
private String title;
@JsonProperty
private String sides;
@JsonProperty
private String createdAt;
}
private static class PossibleResponses extends ResourceCreatedResponses<SavedHabit>
implements CreateHabit.Outcome<ResponseEntity> {
@Override
public ResponseEntity successfullyCreatedHabit(SavedHabit createdHabit) {
return ResponseEntity
.created(resourceLocation(createdHabit))
.body(new HabitResponse(createdHabit));
}
ResponseEntity malformedRequest(String error) {
return ResponseEntity
.badRequest()
.body(new ErrorResponse(error));
}
// ...
}
// ...Some api modules contain Request and Response objects describing JSON structures
that are used by multiple endpoints (for example, HabitResponse in the above example).
The component modules are not standalone applications.
Deployable applications are found in the applications directory.
Modules in applications import any component modules with functionality they want;
other than that, they should just consist of deployment configuration.