Entity
is the most elemental data protocol in Lucid. Most components derive their operating logic based on their Entity
type and how it is configured. In a way, the concrete implementation of Entity
is what puts all the pieces together and makes sure Lucid's data flow makes sense for your business logic.
Concrete implementations of Entity
are almost always generated by Lucid as they can easily become quite complex and it would be error prone to write them manually.
The Entity
protocol is split into several sub-protocols. An Entity
type gets different capabilities depending on which protocols are implemented.
EntityIdentifiable
: Can be identified using anEntityIdentifier
.EntityIndexing
: Can be searched using one or moreEntityIndexName
s.LocalEntity
: Can be used with a memory or diskStore
.MutableEntity
: Can be mutated locally.RemoteEntity
: Can be used withRemoteStore
.CoreDataEntity
: Can be used withCoreDataStore
.
Note: These protocols will automatically be applied to your entity when the code is generated based on each entity's description file.
Every Entity
has its own EntityIdentifier
type and property.
An EntityIdentifier
is a combination of two sub identifiers:
LocalID
RemoteID
(LocalID, RemoteID)
When an entity is created locally, it gets a unique local identifier assigned. When an entity comes from a server, it has a remote identifier assigned. This means that pushing a locally created Entity
then fetching that same Entity
from a server could create two versions of the same Entity
with two different unrelated identifiers.
In order to avoid those duplicates, Lucid merges both identifiers into one unique EntityIdentifier
, given the following rule:
LocalID == (LocalID, RemoteID)
||
RemoteID == (LocalID, RemoteID)
These rules have an impact on how EntityIdentifiers
can be used outside of Lucid. Since they are hashed based on two values, they implement DualHashable
instead of the typical Hashable
protocol. This means they can't be used as keys in regular dictionaries or sets. Instead, they can be used with DualHashDictionary
and DualHashSet
which are both provided by Lucid.
For every Entity
, Lucid generates two useful index enums:
EntityIndexName
: Used for writing queries, it has one case per property.EntityRelationshipIndexName
: Used for fetching entities' relationships, it has one case per relationship property.
The RemoteEntity
protocol is interesting because it gives you the opportunity to define how an Entity
type should be remotely accessed.
Even though those implementations are optional, it is recommended to implement them if the backend API you're using is resource oriented.
static func requestConfig(for remotePath: RemotePath<Self>) -> APIRequestConfig?
: Builds anAPIRequestConfig
based on theRemotePath
which is being requested.static func endpoint(for remotePath: RemotePath<Self>) -> ResultPayload.Endpoint?
: Selects which endpoint type to use for decoding the JSON payload coming back from the server, based on theRemotePath
which was used to send the request.
The following is an example of how to configure MyEntity
for a backend RESTful API:
public enum MyEntityContext: Equatable {
case discover
}
extension MyEntity {
public static func requestConfig(for remotePath: RemotePath<MyEntity>) -> APIRequestConfig? {
switch remotePath {
case .get(let identifier):
// Builds an URL like: https://my_server.com/api/my_entity/42
return APIRequestConfig(method: .get, path: .path("my_entity") / identifier)
case .search(let query) where query.context == .discover:
// Builds an URL like: https://my_server.com/api/discover/my_entity?page=1&order=asc
return APIRequestConfig(
method: .get,
path: .path("discover") / "my_entity",
query: [
("page", .value(query.page?.description)),
("order", .value(query.order.first?.requestValue))
]
)
default:
return nil
}
}
public static func endpoint(for remotePath: RemotePath<MyEntity>) -> EndpointResultPayload.Endpoint? {
switch remotePath {
case .get:
return .myEntity
case .search(let query) where query.context == .discover:
return .discoverMyEntity
default:
return nil
}
}
}
With the setup above, one can easily fetch an Entity
with the following code:
coreManagers.myEntityManager.get(
byID: myEntity.identifier,
in: .init(dataSource: .remoteOrLocal())
)
coreManagers.myEntityManager.search(
withQuery: Query.all
.order([.desc(by: .index(.popularity))])
.with(offset: offset)
.with(limit: 20)
.with(context: .discover),
in: ReadContext(dataSource: .remoteOrLocal())
)
Backend APIs are not all RESTful, and even when they are, they often have inconsistencies.
That's why Lucid also provides a way to specify a request based on the call site's context. The following code shows how to do so:
let request = APIRequestConfig(
method: .get,
path: .path("my_entity") / myEntity.identifier
)
let context = ReadContext<MyEntity>(
dataSource: .localOrRemote(endpoint: .request(request, resultPayload: .myEntity)
)
coreManagers.myEntityManager.get(
byID: myEntity.identifer,
in: context
)
Note that in the code above, the request might not be used, as it depends on whether or not it is found locally (because of the data source .localOrRemote
). However, in the eventuality it will be used, the request needs to match the CoreManager
's API being used. For instance, in this example, the fact we are using myEntity.identifier
in both places makes the code safe to use.