Skip to content

Latest commit

 

History

History
142 lines (104 loc) · 5.91 KB

Entity.md

File metadata and controls

142 lines (104 loc) · 5.91 KB

Lucid - Entity

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 an EntityIdentifier.
  • EntityIndexing: Can be searched using one or more EntityIndexNames.
  • LocalEntity: Can be used with a memory or disk Store.
  • MutableEntity: Can be mutated locally.
  • RemoteEntity: Can be used with RemoteStore.
  • CoreDataEntity: Can be used with CoreDataStore.

Note: These protocols will automatically be applied to your entity when the code is generated based on each entity's description file.

Entity Identifier

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.

Entity Indices

For every Entity, Lucid generates two useful index enums:

Remote Entity

RESTful Configuration

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 an APIRequestConfig based on the RemotePath 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 the RemotePath 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())
)

Contextual API Requests

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.