Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Static Value objects #43

Closed
NathanFlurry opened this issue May 25, 2016 · 3 comments
Closed

Static Value objects #43

NathanFlurry opened this issue May 25, 2016 · 3 comments
Labels
enhancement New feature or request

Comments

@NathanFlurry
Copy link
Contributor

NathanFlurry commented May 25, 2016

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.

@tanner0101 tanner0101 added enhancement New feature or request reviewing labels May 25, 2016
@tanner0101
Copy link
Member

I'd like to get @loganwright's thoughts on this since he implemented a very similar Validation library in Vapor.

My concern here would be forcing too much coupling between the database and the model. Users of Fluent should be able to choose whether or not they want the Model to closely resemble the database schema. As @Evertt mentioned, it could be possible to create a layer between this called Entity that does match the database schema, and Model could or could not match the entity.

For example, say my database has 2 columns: first_name and last_name. And say I want my model to have var name: Name where

struct Name {
   var first: String
   var last: String
   var full: String { first + "  " + last }
}

This simple functionality would be a headache to implement if the database and Model were forced to be coupled.

@NathanFlurry
Copy link
Contributor Author

@tannernelson That sounds like a good idea. To clarify, the class Person could be written as either:

class Person: Model {
    let id: Value?
    let age: Valid<Count<Int>>
    let first: String
    let last: String

    init(id: Value? = nil, age: Int, first: String, last: String) throws {
        self.id = id
        self.age = try Valid(age, by: Count<Int>.min(18))
        self.first = first
        self.last = last
    }

    convenience required init?(serialized: [String : Value]) throws {
        try self.init(id: serialized["id"], age: serialized["age"]?.int ?? 18, first: serialized["first"]?.string ?? "", last: serialized["last"]?.string ?? "")
    }

    func serialize() -> [String : Value?] {
        return [
            "age": age.value,
            "first": first,
            "last": last
        ]
    }

    class var table: String {
        return "people"
    }
}

or

class Person: Entity {
    let id = Field<Int?>()
    let age = Field<Int>(validators: Validators<Int>.greaterThanEqual(other: 18))
    let first = Field<String>(sqlType: .string(20), unique: true, validators: Validators<String>.length(10...20)) { print("Name changed \($0)") }
    let first = Field<String>(sqlType: .string(20), unique: true, validators: Validators<String>.length(10...20)) { print("Name changed \($0)") }

    init(age: Int, name: String) {
        self.age.value = age
        self.name.value = name
    }

    func values() -> [String: Value<Any>] {
        return ["id": id, "age": age, "name": name]
    }

    class var table: String {
        return "users"
    }
}

@NathanFlurry NathanFlurry self-assigned this May 27, 2016
@NathanFlurry NathanFlurry removed their assignment Jul 28, 2016
@tanner0101
Copy link
Member

This should be achievable with Node + Vapor validators now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants