Skip to content

Instantly share code, notes, and snippets.

@bion
Last active January 27, 2021 00:00
Show Gist options
  • Save bion/31311a05f9b433a48afaf6cc75b63b02 to your computer and use it in GitHub Desktop.
Save bion/31311a05f9b433a48afaf6cc75b63b02 to your computer and use it in GitHub Desktop.
Domain model design sketch -- ObjectMapper approach
@Entity
class Applicant extends Model {
@Id
Long id;
@DbJsonB
CustomObjectNode object;
// etc.
}
enum ApplicantDataNodeTypes {
ADDRESS,
ARRAY,
DATE,
FILE,
MONEY,
NUMBER,
CUSTOM_OBJECT,
PHONE,
SIGNATURE,
STRING,
}
interface ApplicantDataNode {
ApplicantDataNodeTypes getNodeType();
// get the wrapped node, throws if wrong type
AddressNode getAddress();
ArrayNode getArray();
DateNode getDate();
FileNode getFile();
MoneyNode getMoney();
NumberNode getNumber();
CustomObjectNode getObject();
PhoneNode getPhone();
SignatureNode getSignature();
StringNode getString();
}
class CustomObjectNode {
Map<String, ApplicantDataNode> contents;
// Get the object or answer primitive containing the field(s)
// to be updated
// e.g. applicant.first_name -> applicant
// e.g. applicant.address.zip -> address
// e.g. applicant.dependents[1].employers[2].address.zip -> address
ApplicantDataNode getContainerForUpdate(UpdatePath updatePath) {
// do some recursion through objects and their child fields
}
}
class Update {
UpdatePath path;
// basically a "one of"
Value value;
}
class UpdatePath {
// { applicant, pet's names, 1 }
// e.g. "applicant.dependents[1].employers[2].address.zip"
// becomes
// { applicant, dependents, 1, employers, 2, address, zip }
List<String> path;
String getPropertyName() {
return Iterables.last(path);
}
}
class UpdateResult {
boolean success;
Map<String, String> validationMessages;
UpdateResult(success, validationMessages) {
// etc.
}
boolean isSuccess() {
return success;
}
}
class CustomObjectNode {
Map<String, ApplicantDataNode> properties;
Map<String, ApplicantDataNodeTypes> propertyMap;
Map<String, NodeValidation> propertyValidations;
// returns a validation message if it fails
// throws is value is of the wrong type
public Optional<String> update(String propertyName, Value value) {
ApplicantDataNodeTypes propType = typeFor(propertyName);
switch (propType) {
case PropTypes.NUMBER: {
updateNumberProp(propertyName, value.getNumber());
break;
}
// etc
}
}
private ApplicantDataNodeTypes typeFor(String propertyName) {
return propertyMap.get(popertyName);
}
}
class CustomObjectUpdateStrategy extends UpdateStrategy {
CustomObjectValidator validator;
@Inject
CustomObjectUpdateStrategy(CustomObjectValidator validator) {
this.validator = validator;
}
UpdateResult update(ApplicantDataNode wrapperNode, Update update) {
CustomObjectNode object = wrapperNode.getCustomObject();
String fieldName = update.getPath().getFieldName();
object.update(fieldName, update.getValue());
}
}
class AddressUpdateStrategy extends UpdateStrategy {
AddressValidator validator;
@Inject
AddressUpdateStrategy(AddressValidator validator) {
this.validator = validator;
}
UpdateResult update(ApplicantDataNode wrapperNode, Update update) {
AddressNode address = wrapperNode.getAddress();
String fieldName = update.getPath().getFieldName();
switch (fieldName) {
case "zip": {
// we know the value is a string because that's what goes in zip.
// if it's not a string then getString() throws.
address.setZip(update.getValue().getString());
break;
}
// etc
}
// validator can call type-checked methods for accessing JSON-backed
// and computed properties, and doesn't need to check
// that the value its receiving for validation is actually an address
//
// returnas a map of field names to validation messages
Map<String, String> validationMessages = validator.validate(address);
return new UpdateResult(validationMessages.isEmpty(), validationMessages);
}
}
Applicant applicant = new Applicant()
.email.equalTo("rob@foo.bar")
.findOne();
ObjectNode applicantData = applicant.getObject();
ApplicantDataNode container = applicantData
.getContainerForUpdate(update.getPath());
// each node type has a corresponding update strategy
UpdateStrategy strategy = updateStrategyFactory
.strategyFor(container.getNodeType());
// type-specific update strategy allows for type-specific validation messages
UpdateResult result = strategy.update(container, update);
if (result.isSuccess()) {
applicant.save();
return ok(...);
} else {
// or something like this, messages will need to be mapped to form input elements
return badRequest(formState, result.errorMessages);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment