ModelKit 0.4.4

ModelKit 0.4.4

TestsTested
LangLanguage SwiftSwift
License MIT
ReleasedLast Release Mar 2017
SwiftSwift Version 3.0
SPMSupports SPM

Maintained by Sam Williams.



ModelKit 0.4.4

  • By
  • Sam Williams

ModelKit

This is a framework for working with model objects.

pod 'ModelKit'

Or, include only some components:

  • ‘ModelKit/Fields’
  • 'ModelKit/Models’
  • 'ModelKit/RemoteModelStore’ (Alamofire dependency)

Fields

Fields give you:

  • type-safe change observation
  • automatic timestamps
  • validations

Basic usage:

class Person {
  let age = Field<Int>()
}
person.age.value = 10

For multivalued fields, there is ArrayField, which wraps a field object describing the single-valued type.

let tags = ArrayField(Field<String>(), name: "Tags")

The inner field is responsible for validations, transformations, etc.. The ArrayField owns top-level attributes like name, key, etc. – but for convenience, it will copy them from the inner field at initialization.

The unary postfix operator * is provided to wrap a Field in an ArrayField. So you can also write the above declaration like this:

let tags = Field<String>(name: "Tags")*

Validations

Simple closure validations:

let age = Field<Int>().require { $0 > 0 }

Rules can be chained, too, implying an AND. Order is not important.

let age = Field<Int>().require { $0 > 0 }.require { $0 % 2 == 0 }

By default, nil values will be considered valid. To change that for a given rule, pass allowNil: false to require.

To validate a field value, call field.validate(), which returns a ValidationState enum:

public enum ValidationState:Equatable {
    case unknown
    case invalid([String])
    case valid
}

The associated value of the .invalid case is a list of error messages (e.g., ["must be greater than 0", "is required"]).

Timestamps

Fields will automatically have the following timestamps:

  • updatedAt: the last time any value was set
  • changedAt: the last time a new value was set (compared using ==)

Observers

This library includes the ValueObserver and ValueObservable protocols for generic, type-safe change observation. Fields implement both protocols.

A ValueObservable can have any number of registered ValueObserver objects. The --> operator is a shortcut for the addObserver method (<-- works the same, only with its arguments swapped). Observation events are triggered once when the observer is added, and after that whenever a field value is set.

Adding an observer

An observer can be added if it implements the ValueObserver protocol, which has a valueChanged(observable, value: value) method.

field --> observer

Or, a closure can be provided. In place of an observer object, an owner is used only to identify each closure.

field --> owner { value in
  print(value)
}

We can still register a closure even if no observer is given. This is effectively registering the closure with a null observer.

age --> { value in 
  print("Age was changed to \(value)")
}

Binding a field to another field

Since Field itself implements both ValueObservable and ValueObserver, the --> operator can be used to create a link between two field values.

sourceField --> destinationField

This will set the value of destinationField to that of sourceField immediately, and again whenever sourceField’s value changes.

The <--> operator is a shortcut for <-- followed by --> (and can only be used between two Fields).

field1 <--> field2

Since <-- is called first, both fields will initially have the value of field2.

Unregistering

Unregistering observers is done with the removeObserver method, or the -/-> operator. All observers can be removed with removeAllObservers().

Models

A Model object automatically converts to and from a dictionary representation of its Field properties.

person.dictionaryValue()
// --> ["name": "Bob", "tags": ["red", "blue", "green"]]

If your field’s value is a subclass of Model, you should use the ModelField subclass.

let companies = ModelField<Company>()*

ModelStore

This library provides a Promise-based interface for abstract data stores, specified in the ModelStore protocol, as well as several concrete implementations.

func create<T: Model>(model:T) -> Promise<T>
func update<T: Model>(model:T) -> Promise<T>
func delete<T: Model>(model:T) -> Promise<T>
func lookup<T: Model>(modelClass:T.Type, identifier:String) -> Promise<T>
func list<T: Model>(modelClass:T.Type) -> Promise<[T]>

MemoryModelStore

This stores models in memory. It adds a lookupImmediately method for synchronous identifier-based lookups.

RemoteModelStore

This is intended as a base class for your RESTful server interface. It includes a number of overridable hooks that you can customize for your particular needs, like:

  • defaultHeaders
  • handleError
  • constructResponse

A RESTRouter is responsible for generating paths for resource locations.

collectionPath(for: Person.self)
instancePath(for: person)

Nested routes are generated automatically if your model conforms to HasOwnerField, which requires it to specify an owning field. If a person’s owner field is its company field, for example, you might get "/companies/10/employees/45" for its instance path.

It also contains a ValueTransformerContext var that can be used to customize serialization. For example:

  • context.keyCase - specify the casing style of keys (.snake, .upperCamel, .lowerCamel)
  • context.explicitNull - decide whether keys for null values should be included
  • Specify custom transformers