5

I am trying to migrate some code using a Repository pattern from Vapor 3 to Vapor 4. I have gone through the documentation of this specific pattern from the Vapor 4 documentation, and I think I understand it for the most part.

The one thing I am not getting, however, is the way that the repository factory gets set within the Application extension. The example from the documentation shows this:

extension Application {
    private struct UserRepositoryKey: StorageKey { 
        typealias Value = UserRepositoryFactory 
    }

    var users: UserRepositoryFactory {
        get {
            self.storage[UserRepositoryKey.self] ?? .init()
        }
        set {
            self.storage[UserRepositoryKey.self] = newValue
        }
    }
}

If I am reading the getter method correctly (and I might not be - I'm far from a Swift expert), a new instance of the UserRepositoryFactory structure will be created and returned when app.users is referenced. At that time, however, it does not appear that the contents of self.storage[UserRepositoryKey.self] is changed in any way. So if I happened to access app.users two times in a row, I would get 2 different instances returned to me and self.storage[UserRepositoryKey.self] would remain set to nil.

Following through the rest of the sample code in the document, it appears to define the make function that will be used by the factory when configuring the app as so:

app.users.use { req in
    DatabaseUserRepository(database: req.db)
}

Here it seems like app.users.use would get a new factory instance and call its use function to set the appropriate make method for that instance.

Later, when I go to handle a request, I use the request.users method that was defined by this Request extension:

extension Request {
    var users: UserRepository {
        self.application.users.make!(self)
    }
}

Here it seems like self.application.users.make would be invoked on a different repository factory instance that is referenced by self.application.users. It would therefore not apply the factory's make method that was set earlier when configuring the application.

So what am I missing here?

Tim Dean
  • 8,253
  • 2
  • 32
  • 59

2 Answers2

4

It looks like the docs are slightly out of date for that. You can have a look at how views or client is done, but somewhere you need to call initialize() to set the repository. Here's what my working repository looks like:

import Vapor

extension Application {
    struct Repositories {
        
        struct Provider {
            let run: (Application) -> ()
            
            public init(_ run: @escaping (Application) -> ()) {
                self.run = run
            }
        }
        
        final class Storage {
            var makeRepository: ((Application) -> APIRepository)?
            init() { }
        }
        
        struct Key: StorageKey {
            typealias Value = Storage
        }
        
        let application: Application
        
        var repository: APIRepository {
            guard let makeRepository = self.storage.makeRepository else {
                fatalError("No repository configured. Configure with app.repositories.use(...)")
            }
            return makeRepository(self.application)
        }
        
        func use(_ provider: Provider) {
            provider.run(self.application)
        }
        
        func use(_ makeRepository: @escaping (Application) -> APIRepository) {
            self.storage.makeRepository = makeRepository
        }
        
        func initialize() {
            self.application.storage[Key.self] = .init()
        }
        
        private var storage: Storage {
            if self.application.storage[Key.self] == nil {
                self.initialize()
            }
            return self.application.storage[Key.self]!
        }
    }
    
    var repositories: Repositories {
        .init(application: self)
    }
}

That autoinitializes itself the first time it's used. Note that APIRepository is the protocol used for my repostiory. FluentRepository is the Fluent implementation of that protocol. Then like you I have an extension on Request to use it in request handlers:

extension Request {
    var repository: APIRepository {
        self.application.repositories.repository.for(self)
    }
}

Finally, you need to configure it to use the right repository. So in my configure.swift I have:

app.repositories.use { application in
    FluentRepository(database: application.db)
}

and in tests I can switch it for the in-memory repository that doesn't touch the DB:

application.repositories.use { _ in
    return inMemoryRepository
}
0xTim
  • 5,146
  • 11
  • 23
  • Thank you @0xTim - Looking at your updated example, I have a couple of other questions: 1. In your `Request` extension you call a function called `for` on your `APIRepository` type. Is that function something special that all repositories should define, or is it just something you happen to use in this particular repository interface? 2. You define a `struct` called `Provider`, which you then reference in one of the `use` functions, but I can't figure out what that concept is for or how it is used. As far as I can tell it is not referenced elsewhere in your example. Can you clarify? – Tim Dean Aug 11 '20 at 11:54
  • `for` allows you to create the repository using the same event loop as the request. This helps avoid having to hop between different event loops (and avoids threading crashes). The Provider is used to allow you to switch different implementations, though you're correct it's not needed here. Most Vapor repositories (from Vapor itself) use it, so it's here because of copy and paste! It would allow you to do `application.repositories.use(.inMemoryRepository)` if set up correctly, but in this instance I want access to the in memory repository so I can have a reference to it to add data etc – 0xTim Aug 12 '20 at 15:55
  • So if I understand this approach correctly, `application.repositories.repository` returns an instance of your repository that is not, at that time, associated with an event loop. When you reference `request.repository` will retrieve that un-associated repository instance and then associate it with the event loop from the request. Is that correct? – Tim Dean Aug 22 '20 at 03:44
  • Almost - `app.repositories.repository` returns an instance of the repository associated with the application's event loop, `req.repository` with the request's event loop. This is useful if you need to do stuff when booting the app up outside of the lifecycle of a request – 0xTim Aug 23 '20 at 09:20
  • Thanks 0xTim - Now I just need to figure out how to properly use the requests event loop from controller/repository/model. Is there documentation somewhere that shows how that is done? If not, are there any good reference repositories I can use to pattern my web application after? – Tim Dean Aug 24 '20 at 01:02
  • There aren't any docs because it's both fairly advanced and a mixture of personal preference. But if you have a look at the `ViewRenderer` protocol and the `for(_:)` function that's a pretty common pattern. You then use that with the `Request` extension in my original answer to create a new repository instance with that event loop. For example mine looks like ```swift func `for`(_ request: Request) -> APIRepository { FluentRepository(database: request.db) } ``` – 0xTim Aug 25 '20 at 12:06
  • I am still struggling to get this to work. I have the production code working but I can't figure out how to create my in-memory version of the repository for testing. In order for it to comply with my repository protocol, it must have access to an event loop so it can return `EventLoopFuture` results. But to pass in an event loop from my XCTestCase, the only way I've found to do that is by calling `app.eventLoopGroup.next()`. That seems to build but when I run the test I get an error: `Fatal error: Sessions not configured. Configure with app.sessions.initialize() ` What am I missing? – Tim Dean Sep 07 '20 at 04:40
  • 1
    You need to create your own event loop group and control the lifecycle of that. See [this example](https://github.com/brokenhandsio/SteamPress/blob/vapor4/Tests/SteamPressTests/Helpers/TestWorld.swift#L7) for how to do that. You can then create your application using that event loop - [like so](https://github.com/brokenhandsio/SteamPress/blob/vapor4/Tests/SteamPressTests/Helpers/TestWorld%2BApplication.swift#L17) and create your in memory repository using it and then set up your app correctly. Remember to shut the event loop down at the end of each test – 0xTim Sep 07 '20 at 15:21
  • Thank you again @0xTim for all of your help. I now have managed to get my application and my tests working. It took quite a while but I finally have something I can build on. Now that I've gotten through this (difficult) learning curve I find myself wondering if I'm doing things the "right" way for my needs. I have posted another SO question to https://stackoverflow.com/questions/63866977/recommendations-for-structuring-a-complex-vapor-4-application-for-testability. – Tim Dean Sep 13 '20 at 03:51
2

I have managed to get the example from the docs working as-is.

Tracing through the execution with the debugger, there is the predictable call to get, as you say, and this returns the instance from .init() as the failover from not having a previously stored value. Included in the example you refer to is:

struct UserRepositoryFactory {
    var make: ((Request) -> UserRepository)?
    mutating func use(_ make: @escaping ((Request) -> UserRepository)) {
        self.make = make
    }
}

This use function is executed next, which is mutating and updates the variable make. I believe it is this change to make that then triggers a call to set. It certainly happens immediately after use and before execution moves on in configure.swift. So, by the time the server formally starts and you actually use the Repository in a route, there is a stored instance that is reused as required.

Nick
  • 4,820
  • 18
  • 31
  • 47
  • Thanks @Nick - That is really interesting. I had also noticed that this code worked as advertised in my own tests, and I just wasn't sure how. The fact that `set` is being called after `use` is invoked would explain the results - Although I'm not sure why that would be the case. Is there something in Swift that causes a mutated `struct` to be set back to the object that "owns" it? – Tim Dean Aug 10 '20 at 18:41
  • 1
    @TimDean, thanks. I think I have read about this behaviour somewhere but now can't find it. It seems logical that it would use the `set` mechanism to store changes (presumably triggering willSet and didSet along the way). Your question is a really interesting one. I'm surprised it hasn't picked up more votes. – Nick Aug 10 '20 at 18:56