Skip to content

Latest commit

 

History

History
395 lines (273 loc) · 19.1 KB

Appendix A.md

File metadata and controls

395 lines (273 loc) · 19.1 KB

Logo

Appendix A: Write your own custom functions

Using the default CRUD functions that Jayme provides is useful and convenient. However, it's very likely that you will often need to create your own functions for custom scenarios that are not included in the standards that Jayme works with.

Typical examples of non-standard scenarios are:

  • Compound endpoints. For instance: GET /users/:id/posts
  • Non-standard parsing. For example: A GET endpoint which instead of returning a JSON with an array of items, returns a JSON containing a dictionary with an "items" key where the items are contained.
  • Different pagination standards. For instance: A scrolled-based pagination where instead of getting the pagination info in the X-Total, X-Page and X-Per-Page headers, you get it within the JSON body response, with next and prev fields that include links to where you can continue fetching items.

In this document, you will learn how to take advantage of The Future Pattern to write your own custom functions in the same way the standard CRUD functions from Jayme are written.

Learning how to write these functions has a steep learning curve, but, once you get the hang of it, you'll see how easy it is to take advantage of the library to design well-architectured codebases while finding Jayme as a very powerful companion for any Swift project that hits RESTful APIs.


Understand how .read() works

First, let's try to grasp how this "apparently" simple function works internally:

/// Fetches the only entity from this repository.
/// Returns a `Future` containing the only entity in the repository, or the relevant `JaymeError` that could occur.
/// Watch out for a `.failure` case with `JaymeError.entityNotFound`.

public func read() -> Future<EntityType, JaymeError> {
    let path = self.name
    return self.backend.future(path: path, method: .GET, parameters: nil)
        .andThen { DataParser().dictionary(from: $0.0) }
        .andThen { EntityParser().entity(from: $0) }
}

This function is mean to work for single-entity repositories, and it's expected to retrieve the only entity living in the repository, or a JaymeError.notFound error case in case there's no entity.

Now, let's analyze it line by line:

1. The function declaration

public func read() -> Future<EntityType, JaymeError> {

Pay special attention to the return type. It's a Future. A future represents an operation that doesn't happen immediately, i.e. an asynchronous operation. And futures are generalized: they expect you to specify 2 types they have to work with.

Explaining how Futures work is out of the scope of this document. If you want further reference on the topic, you can check out this talk.

Let's have a quick look at Future:

public struct Future<T, E: Error> { 
   public typealias FutureResultType = Result<T, E>
    // ...
}

What you need to know: Futures work very close to another type called Result. The generic types T and E: Error specified in Future are, in the end, what the Result type is going to use for representing the result of the operation.

So, let's have a quick look at Result:

/// Represents the result of an asynchronous operation.
public enum Result<T, E: Error> {
    
    /// Indicates that the operation has been completed successfully.
    /// Contains the relevant data associated to the operation response.
    case success(T)
    
    /// Indicates that the operation could not be completed or has been completed but unsuccessfully.
    /// Contains the relevant error associated to the failure cause.
    case failure(E)
    
}

What you need to know:

  • T is the type that represents what you are looking for in the function. If the asynchronous operation completes successfully, you will get a T.
  • E is the type used for representing an error. If the asynchronous operation fails, you will get an E. Also, E has to conform to the Error protocol.

Back to our function:

public func read() -> Future<EntityType, JaymeError> {
  • T is EntityType. If the operation succeeds, you will get an EntityType. Notice that EntityType has to be tied to a particular type when defining the repository. In the README example, EntityType is a User.
  • E is JaymeError. If the operation fails, you will get a JaymeError.

That's why, when you call this function, your code will look like this:

SomeRepository().read().start() { result in
    switch result {
    case .success(let value):
        // value is a T
    case .failure(let error):
        // error is a E
    }
}

Or, more specifically:

UsersRepository().read().start() { result in
    switch result {
    case .success(let user):
        // user is an EntityType, which is a User in UsersRepository
    case .failure(let error):
        // error is a JaymeError
    }
}

Now, if you analyze the function declaration of readAll(), you'll find:

public func readAll() -> Future<[EntityType], JaymeError> {

Which should make more sense to you now: You will get a result with either a success case with an array of entities [EntityType], or a failure case with a JaymeError.

Now you should kind of get the magic formula in your mind on how these function signatures are written depending on what's needed.

2. The call to the backend

Let's analyze this part now:

self.backend.future(path: path, method: .GET, parameters: nil)

If you look what this function returns, you'll find this in URLSessionBackend:

/// Returns a `Future` containing either:
/// - A tuple with possible `NSData` relevant to the HTTP response and a possible `PageInfo` object if there is pagination-related info associated to it.
/// - A `JaymeError` holding the error that occurred.
open func future(path: Path, method: HTTPMethodName, parameters: [AnyHashable: Any]? = nil) -> Future<(Data?, PageInfo?), JaymeError> {
    return self.createFuture(path: path, method: method, parameters: parameters)
}

Notice the following facts:

  • The return type of this function is Future <(Data?, PageInfo?), JaymeError>.
  • What this function do is execute the networking code necessary to build a request, send it to the server, and parse the HTTP response turning it into a possible raw Data object. It also parses pagination information that may come in the response's headers. If such pagination info exists, you will get a PageInfo object.

Up to here, you got the data from the response. However, you can't return that data as a Data object directly from the read() function, because it has to return an EntityType. Here is when the parsing comes into the scenario. The idea is that your view controllers should never worry about parsing objects from JSONs and all that sauce. Instead, the parsing is done at the repository level, in these functions that we are learning to write.

3. The parsing

Let's move on to the next line:

    .andThen { DataParser().dictionary(from: $0.0) }

You are probably wondering what this line means and how it's supposed to work. Simply put, .andThen is a function declared in Future that allows you to chain functions that return Futures.

Instead of having to unwrap the result as you did in your view controller, specifying what to do in each case, you can use the .andThen function and just pass in a transforming function that specifies what to do with the successful case of the asynchronous operation. As for the failure scenarios, they are just forwarded.

Notice that when you pass in a closure by using the braces at the end, Swift puts the arguments in annonymous variables named $0, $1, $2, and so on. What $0 represents in this line is the (Data?, PageInfo?) tuple that is returned inside the Future object that we had obtained from the previous line. Since this tuple doesn't have labels, you can access its elements by using .0 and .1. Therefore, $0.0 contains the Data? object, and $0.1 contains the PageInfo? object.

Now, notice how $0.0 is used. Jayme provides you with elemental classes for extracting raw data into dictionaries or arrays.

If you check out DataParser, you'll find:

open func dictionary(from possibleData: Data?) -> Future<[AnyHashable: Any], JaymeError> { ... }

Notice that when you call this function, you're turning your possible Data object into a [AnyHashable: Any] dictionary, or JaymeError if it can't be parsed. Also, notice that the return type is... a Future!

Having this return type as a Future allows you to keep on chaining more operations using the .andThen function.

We're almost there. We've got a Future<[AnyHashable: Any], JaymeError> so far, but we need to return a Future<EntityType, JaymeError> out from our read() function.

There's one step left, which is turning this dictionary into an actual entity.

Let's observe the next, and last, line:

    .andThen { EntityParser().entity(from: $0) }

Now you can guess what this function does. The $0 variable, in this case, corresponds to the [AnyHashable: Any] object obtained from the Future returned in the previous line.

Just in case, let's take a look at this definition, from EntityParser:

open func entity(from dictionary: [AnyHashable: Any]) -> Future<EntityType, JaymeError> { ... }

This function turns [AnyHashable: Any] into EntityType, while conserving the Future structure for chaining operations.

Notice that the only restriction for EntityType in EntityParser is the following:

open class EntityParser<EntityType: DictionaryInitializable> {

Yes, EntityType has to be DictionaryInitializable so that it can be initialized from a dictionary.

4. Wrap up

In summary, keep on mind the following piece of code:

public func read() -> Future<EntityType, JaymeError> { 
// 1. You will get an EntityType if everything goes well

    let path = self.name
    
    return self.backend.future(path: path, method: .GET, parameters: nil)
    // 2. Up to here, you get (Data?, PageInfo?) if everything goes well
    
        .andThen { DataParser().dictionary(from: $0.0) }
        // 3. Up to here, you get [AnyHashable: Any] if everything goes well
        
        .andThen { EntityParser().entity(from: $0) }
        // 4. Finally, you get EntityType if everything goes well
        
        // 5. If something goes bad at any point, you will get a JaymeError
}

Write your own function for a compound endpoint

Let's suppose you have set up your PostsRepository as following:

class PostsRepository: Readable {
    typealias EntityType = Post
    let backend = URLSessionBackend.myAppBackend()
    let name = "posts" 
}

And let's suppose you're performing readAll() and read(id:) operations successfully, hitting the endpoints GET /posts and GET /posts/:id respectively, without much effort.

Now, let's pretend your API allows you to get all the posts for a specific user only, through the GET /users/:user_id/posts endpoint. How are you supposed to hit this endpoint?

There is no default way you can do it with Jayme. You need to write your own function that hits that endpoint. Now that you've learned how these functions work internally, you should be good to go and write your own function.

This one is simple. Let's analyze the key points:

  • It makes sense for this function to live in the PostsRepository, as you expect to get posts from it
  • You need to fetch an array of entities, i.e. you expect to get [EntityType]
  • You need the user_id to build the path to hit the endpoint
  • You need to use the GET HTTP method
  • You don't need to send any parameters in the request's body

With those key points in mind, we can create this function:

extension PostsRepository {

    func read(userId: String) -> Future<[EntityType], JaymeError> {
        let path = "users/\(userId)/\(self.name)"
        return self.backend.future(path: path, method: .GET)
            .andThen { DataParser().dictionaries(from: $0.0) }
            .andThen { EntityParser().entities(from: $0) }
        }

}

Pay attention to the usage of DataParser().dictionaries and EntityParser().entities, which differ to the DataParser().dictionary and EntityParser().entity that we used in our previous example. This is because in this case you are expecting to read the JSON object as an array, not as a dictionary.

Now, you are ready to use this function in your view controller:

PostsRepository().read(userId: "123").start() { result in
    switch result {
    case .success(let posts):
        // you got posts from user with id "123"
    case .failure(let error):
        // JaymeError indicating what happened
    }
}

This is it! Once you understand how to take advantage of Future, DataParser and EntityParser, writing your own custom functions for hitting compound endpoints becomes a piece of cake.


Write your own CREATE functions

Another common scenario that you will find is the one in which you need to create entities, but you expect their id's to be generated server-side. You'll find yourself in trouble if you want to use the default create(entity) method that Jayme's Creatable provides you with, given that this method requires a full entity to be passed in, and normally a full entity includes an id field, and you don't have a value for it yet, since you expect it to come in the response from the server.

As you start trying to come up with a solution, you realize that making id variables optional in your entities just makes things worse: You end up having a non-sense guard let nightmare everywhere in your code.

You may think of sending a garbage id value, like "", or "_", or "provisory_id", or a random UUID string, until you get a valid one from the server. But, this way, your entities would be dirty at some point. So, this is not the most elegant solution there could be.

You may also think of another crazy approach where you have entity types with id, and entity types without id, but you soon realize this kind of strategy just leads your code to hell.

So, a simple solution for this scneario is implementing your own create methods for your repositories, by passing in the necessary parameters that the entity needs to be constructed, except for the id.

Let's say that you have a User entity that looks like this:

struct User: Identifiable {
    let id: String
    let name: String
    let email: String
}

extension User: DictionaryInitializable {
    init(dictionary: [AnyHashable: Any]) throws {
        // Parse the entity here
        guard 
            let id = dictionary["id"] as? String,
            let name = dictionary["name"] as? String,
            let email = dictionary["email"] as? String
            else { throw JaymeError.parsingError }
        self.id = id
        self.name = name
        self.email = email
    }
}

Then, your UsersRepository, with your custom create method, should look like this:

class UsersRepository: Readable {
    typealias EntityType = User
    let backend = URLSessionBackend.myAppBackend()
    let name = "users"

    func create(name: String, email: String) -> Future<User, JaymeError> {
        let path = self.name
        let parameters = ["name": name, "email": email] // see? no id!
        return self.backend.future(path: path, method: .POST, parameters: parameters)
            .andThen { DataParser().dictionary(from: $0.0) }
            .andThen { EntityParser().entity(from: $0) }
    }
}

Notice that you can't create a User instance until you don't have an id. That occurs right after a successful response, that includes the created entity in its body, is parsed properly. At that point, it's the .andThen { EntityParser().entity(from: $0) } code what will create a User instance, which you'll be able to play with in your view controller:

let future = UsersRepository().create(name: "Paul", email: "paul@me.com")
future.start { result in
    switch result {
    case .success(let user):
        // You've got your User instance here, including the id that came from the server
    case .failure(let error):
        // JaymeError indicating what happened
}

Be prepared, it will become very normal that you have to write your own create implementation for any entity you need to be able to create. Unfortunately, there's no magic bullet for this.


Write your own parsers

Not every API works the same. Not every API returns your objects of interest at the root level of the JSON response.

For instance, this JSON response is Jayme compliant:

[ {"id": "1", "name": "Paul"},
  {"id": "2", "name": "Kate"},
  {"id": "3", "name": "Grant"} ]

But, what if the entities come within an envelope in the JSON response, like this:

{ "items": [ {"id": "1", "name": "Paul"},
             {"id": "2", "name": "Kate"},
             {"id": "3", "name": "Grant"} ]
}

Here, you need to parse your JSON objects differently. If all the endpoints of your API behave like this, it's convenient that you create your own DataParser and EntityParser classes that have this structure in mind. Then, you can use those in any CRUD function that you write.

Have in mind that all the default functions declared in Creatable, Readable, Updatable and Deletable use the default DataParser and EntityParser classes. So, if you want to keep on using these default functions, you have to re-write them to use your parsers instead.

You may be wondering why aren't DataParser and EntityParser instances injected when creating a repository? This way, you would be able to still use the default Creatable, Readable, Updatable and Deletable functions with your custom parsers by just passing them in. This was a though architectural decision when developing Jayme. See, in most of scenarios where you get the entities within an envelope field, you also get extra information that's useful for parsing, like, for instance, pagination information. Extracting DataParser and EntityParser into variables at the repository level forces you to define interfaces to define what their functions are supposed to return. In Jayme's DataParser and EntityParser default classes, these return types only care about the entities. If you need different stuff, this dependency injection would be useless.


This is it

Now that you've learned how to write your custom functions, which is a very powerful tool in the Jayme's world, you're encouraged to move on to the next level and apply unit-tests to any custom function that you write. This is explained in the Appendix B: Unit-test your repositories.