Static Value
objects
Introduction
This change would allow for easier serialization and deserialization, provide better implementation of validators, enable a more eloquent relational mapping method, and allow for easier database generation.
Motivation
As it stands now, reading and writing to objects in Fluent isn't as fluent as it could be. Developers have to manually read the object's variables from a dictionary when deserializing data and then put all of that data back into a dictionary when serializing it again. That's a lot of unnecessary work for the user.
In addition, the possibility of automatically generation tables with the way the framework is currently designed isn't very feasible without even more code from the user of the framework. (See #305 on qutheory/vapor)
Finally, validating data should not be done by the user of the framework every time they change a variable; the validators should be executed automatically when changing the value of an object, and Fluent currently does not provide an easy way of doing that.
Proposed solution
Here is an example model that implements my proposed method:
class Person: Model {
let id = Value<Int?>()
let age = Value<Int>(validators: Validators<Int>.greaterThanEqual(other: 18))
let name = Value<String>(sqlType: .string(20), unique: true, validators: Validators<String>.length(10...20)) { print("Name changed \($0)") }
let gender = Value<Gender>()
let birthday = Value<Date>(nullable: true)
let friends = Value<[Person]>(relationship: Relationship.manyToMany)
init(age: Int, name: String, gender: Gender, birthday: Date) {
self.age.value = age
self.name.value = name
self.gender.value = gender
self.birthday.value = birthday
self.birthday.changeCallback = birthdayChanged
}
func values() -> [String: Value<Any>] {
return ["id": id, "age": age]
}
class var table: String {
return "users"
}
func birthdayChanged(value: Value<Date>) {
print("Hmm... That's odd.")
}
}
As you can see, the Value<>
objects are stored in constants, so they can not be replaced. This allows for them to be referenced and modified anywhere. That means it's specifically good for relationships where the database needs to be able to load more items into the array on an as-needed basis and can't just give the developer all the data at once and not retain a reference to it. To implement this functionality, we'd simply have to extend any Value<MutableCollection>
objects to have these methods that can selectively load items into the array:
extension Value where T: MutableCollection, T.Iterator.Element: Model {
init(relation: Relation, sqlType: SQLType? = nil, unique: Bool = false, nullable: Bool = false, validators: Validators<T>.ValueValidator..., changeCallback: ChangeCallback? = nil)
func all() throws -> [T.Iterator.Element]
func find(_ id: Value) throws -> T.Iterator.Element?
static var query: Fluent.Query<T.Iterator.Element>
}
In addition, instead of requiring a serialize() -> [String: Value?]
method and an initializer, init?(serialized: [String : Value])
, there's one method, values() -> [String: Value<Any>]
. When the data from the database is deserialized into the object, the database simply calls this method to get a reference to all the values in the object and reads in the data appropriately. Ideally, this method for serialization and deserialization would be built on top of the current Model
serialization requirements, so if a developer needs more control of the serialization for some reason, they can simply use those methods instead.
This method of storing values would also allow for validators that check the validity of a value any time it's changed. That way, the developer does not need to continuously check the values for validity over and over again every time they're changed; the Value<>
object itself will check them for you. Validators are available for used based on the type of value stored in Value<>
and highly modular, as seen in a couple example validators below:
struct Validators<Value> {
typealias ValueValidator = (inout value: Value) throws -> Void
}
extension Validators where Value: String {
static func length(isIn: Range<Int>) -> ValueValidator {
return {
guard $0.characters.count > 0 else {
throw ValidatorError()
}
}
}
}
extension Validators where Value: Comparable {
static func lessThan(other: Value) -> ValueValidator {
return { guard $0 < other else { throw ValidatorError() } }
}
static func greaterThan(other: Value) -> ValueValidator {
return { guard $0 > other else { throw ValidatorError() } }
}
static func lessThanEqual(other: Value) -> ValueValidator {
return { guard $0 <= other else { throw ValidatorError() } }
}
static func greaterThanEqual(other: Value) -> ValueValidator {
return { guard $0 >= other else { throw ValidatorError() } }
}
static func between(values range: Range<Value>) -> ValueValidator {
return { guard range.contains($0) else { throw ValidatorError() } }
}
}
You might recognize that the value passed into the ValueValidator
is flagged as inout
. This allows for values to be modified by the validator in order to fix salvageable inputs.
Finally, generating an empty database would be very easy with this, since everything is structured and validated rigorously. In addition, as seen in the declaration for a couple of the fields, the developer would be able to declare special SQL properties for values like the specific type of column, nullability, uniqueness, etc.
Impact
This will break all current Fluent projects. There's not much we can do to retain functionality of old codebases. Better sooner than later, if we're going to do it.
Alternatives considered
The alternative is to continue using Fluent as it works today, yet I feel that not adopting this change would require a lot more workarounds and cause many caveats in the future.
Decision (For Moderator Use)
On [Date], the community decided to (TBD) this proposal. When the community makes a decision regarding this proposal, their rationale for the decision will be written here.