A convenient way to instantiate your data objects with validation, default values, null checks, etc... Give your data a bit more class!
The documentation for the supplemental DataMap
is located in the wiki.
import haxe.ds.Option;
enum Color { Red; Blue; }
// Add @:publicFields to make all fields public
@:publicFields class Person implements DataClass
{
///// Basic usage /////
final id : Int; // Required field (cannot be null)
final name : Null<String>; // Null<T> allows null
@:trim final address : String = ""; // Automatically trim fields
///// Validation /////
@:validate(_.length >= 2) // Expression validation, "_" is replaced with the field
final city : String;
@:validate(~/[\w-.]+@[\w-.]+/) // Regexp validation
final email : String;
///// Default values /////
final color : Color = Blue;
final created : Date = Date.now(); // Works also for statements
///// Immutable properties /////
var isBlue(get, never) : Bool; // Only get/never properties are allowed.
function get_isBlue() return color.match(Blue);
}
class Main {
static function main() {
var p : Person;
// A Person can now be created like this:
p = new Person({
id: 1,
email: "[email protected]",
city: "Punxsutawney"
});
// This will not compile because
// the required id field is missing:
p = new Person({
name: "Test",
email: "[email protected]",
city: "Punxsutawney"
});
// This will throw an exception because of
// runtime validation:
p = new Person({
id: 1,
email: "nope",
city: "X"
});
}
}
Make sure to test for null
in validators when a field is nullable. Example:
@:validate(_ == null || _.length > 1)
final name : Null<String>;
Also remember that default values will not be tested against any validators (it creates issues with inheritance and error handling).
@:validate(_.length > 10)
final name : String = "Short"; // This will pass validation because it's a default value!
A constructor will be automatically generated, but if you want to add your own it should be in the following format. For this purpose you can also use @:exclude
on fields that you want to set in the constructor yourself.
class Custom implements DataClass {
public final id : Int;
@:exclude public final idStr : String;
// A parameter called 'data' is required
public function new(data) {
// [Generated code inserted here]
// === Your code below here ===
// (all fields will be validated at this point)
this.idStr = Std.string(this.id);
}
}
If you let a DataClass extend another class, fields in the superclass will be regarded as DataClass fields, so you will need to supply them when creating the object. Example.
class Parent implements DataClass {
@:validate(_ > 0)
public final id : Int;
}
class Person extends Parent {
public final name : String;
}
// Creating a Person
final p = new Person({name: "Test"}); // Doesn't work, requires id
final p = new Person({id: 1, name: "Test"}); // Ok
You can add validators to an interface, they will be used in the implementing DataClass.
interface IChapter extends DataClass // extending is optional, but convenient
{
@:validate(_.length > 0)
public final info : String;
}
All classes implementing DataClass
will get a static validate
method that can be used to test if some input data will pass validation:
class Main {
static function main() {
var errors : haxe.ds.Option<dataclass.DataClassErrors>;
// Will return Option.None, meaning that all data passed validation
errors = Person.validate({
id: 1,
email: "[email protected]",
city: "Punxsutawney"
});
// This will return Option.Some(errors), where errors is a Map<String, Option<Any>>, in this case
// ["email" => Some("no email"), "city" => None] (where None represents a null value)
errors = Person.validate({
id: 2,
email: "no email"
});
}
}
The validate
method requires a complete input set, which may not be ideal when checking a single value like a html input field. Therefore all fields with validators will generate a static validateFieldName(testValue) : Bool
method as well.
Since all fields must be final
, changing the DataClass object isn't possible, but a static copy
method is available which you can use to create new objects of the same type in a simple manner:
final p = new Person({id: 1, name: "Test"});
final p2 = Person.copy(p, {id: 2});
Or even fancier, add a using
statement:
using Person;
final p = new Person({id: 1, name: "Test"});
final p2 = p.copy({id: 2});
When handling browser form input, it could be tempting to make a DataClass
for the form, but for every keystroke or click the model will mutate, so it's more convenient to make a simpler data structure for the form:
@:publicFields @:structInit private class Form {
var firstName : String;
var lastName : String;
var email : String;
}
For validation, a DataClass
can be used. Here's how it would look like in Mithril, where Person
is the corresponding DataClass
for the above form:
m("input[placeholder='First name']", {
"class": if(Person.validateName(form.firstName)) null else "error",
value: form.firstName,
oninput: e -> form.firstName = e.target.value
})
When submitting the form, dataMap can then be used to create the actual DataClass
required by the business logic.
When a DataClass object is instantiated but the input fails validaton, a dataclass.DataClassException
is thrown:
try new Person({
id: 2,
email: "no email"
}) catch(e : DataClassException) {
trace(e.errors); // DataClassErrors
trace(e.dataClass); // The failed object
trace(e.data); // The failed data
}
Use a library like deep_equal for value comparison between DataClass
objects.
DataClass can ease the JSON conversion process, especially when using Date
. When defining -D dataclass-date-auto-conversion
, strings and numbers will be automatically converted to Date
, so you can basically create DataClass objects directly from JSON:
class Test implements DataClass {
public final id : Int;
public final created : Date;
}
final json = haxe.Json.parse('{"id":123,"created":"2019-05-05T06:10:24.428Z"}');
final t = new Test(json);
trace(t.created.getFullYear());
This works with strings in the javascript json format 2012-04-23T18:25:43.511Z
and numbers representing the number of milliseconds elapsed since 1st January 1970. An exception is when targeting javascript, where the native Date methods will be used, making it possible to store the date in many different formats.
haxelib install dataclass
, then put -lib dataclass
in your .hxml
file.
Simple objects are used in the Data part of the DCI architecture. They represent what the system is, and have no connections to other objects. They play Roles in DCI Contexts, where they become parts of Interactions between other objects, describing what the system does based on a user mental model. The haxedci-example repository has a thorough tutorial of the DCI paradigm in Haxe if you're interested.