Data Persistence

Summarized using AI

Hanami 2 - New Framework, New You

Tim Riley • February 17, 2023 • Melbourne, Australia

In the video titled "Hanami 2 - New Framework, New You," presented by Tim Riley at RubyConf AU 2023, the speaker discusses the release of Hanami 2.0, a refined and powerful framework designed for Ruby applications of varied scopes. After years of collaborative development, Hanami 2.0 aims to enhance maintainability and flexibility in Ruby programming. Key points covered include:

  • Introduction to Hanami: Tim Riley, a core member of Hanami, starts with an overview of the framework, highlighting its focus on maintenance and simplicity.

  • Evolution of Hanami: The framework has existed since 2014 but recently made significant progress with version 2.0, which integrates concepts from the Dry-RB and ROM-RB projects to provide a comprehensive development experience.

  • Key features: Hanami 2.0 introduces components, dependency injection (DI), and an improved testing experience, making it easy to create independent and maintainable applications. The speaker demonstrates how to set up a new Hanami application, using a "Bookshelf" app as an example.

  • Component-based structure: Each component, including actions and repositories, is designed for reusability and separation of concerns. This allows developers to maintain a clear organization in their code.

  • Slices: A new feature that helps organize applications into smaller domains or technical concerns, promoting modularity.

  • Expanded framework capabilities: Hanami 2 is positioned as an all-encompassing framework, suitable for more than just web applications. The speaker mentions the potential for building chatbots, CLI tools, and more using its architecture.

  • Future developments: The video hints at upcoming features, including database persistence using the ROM-RB toolkit for managing data storage and processing. This will include ways to easily separate business logic from persistence logic, enhancing application quality and clarity.

  • Encouraging developer growth: The speaker emphasizes that working with Hanami encourages developers to understand their application's domain better and invites them to explore the Ruby ecosystem with curiosity.

  • Call to action: Tim Riley encourages developers to embrace new tools and concepts within Hanami and to remain open to learning through experimentation, suggesting that this can lead to significant professional growth.

In conclusion, Hanami 2.0 presents a significant leap in the Ruby framework landscape, inviting developers to adopt new practices that enhance application design and structure, ultimately aiming to reshape how they engage with Ruby programming.

Hanami 2 - New Framework, New You
Tim Riley • February 17, 2023 • Melbourne, Australia

Years in the making, Hanami 2.0 is out! This release brings new levels of polish and power to a framework you can use for Ruby apps of all shapes and sizes.

Come along to discover what goes into building a new Hanami app, the principles that underpin the framework, and how they just might change the way you work with Ruby altogether.

RubyConf AU 2023

00:00:00 This is worth trying on a side project. Yeah, tell me about Hanami. Hanami, ah, I hear it's hit 2.0.
00:00:07 Hanami 2.0 has emerged from the depths of the internet for Ruby apps of all shapes and sizes. After years in the making, it brings new levels of polish and power to a framework near you.
00:00:20 Tim, our next speaker, is a core member of Hanami, Dry-RB, and ROM-RB, and is a principal engineer at Buildkite, one of our sponsors here today.
00:00:28 He's been writing Ruby for over 20 years, and he still loves it. Tim works to bring the joy of Ruby to writing real maintainable apps.
00:00:34 With 20 years of experience in Ruby, I'm sure Tim is preparing for his open source Hanami work. He's collaborating asynchronously with teams in Rome and Krakow.
00:00:47 He says the collaboration is extremely asynchronous and feels like it's happening through a straw, but what they've released is Hanami 2.0, which is a testament to how well-aligned the team is.
00:01:16 Hi everyone, I'm Tim, and I am really happy to be here today. It's an exciting time to be gathered again.
00:01:30 Just a bit about me: I work at Buildkite, where we make fast and flexible CI/CD for all kinds of developers. We operate here in Australia and New Zealand, and we do it using Ruby, which is really cool.
00:01:48 I also contribute to several open source projects in Ruby, namely Hanami, Dry-RB, and ROM-RB. You can probably guess what we'll be talking about today—Hanami.
00:02:00 In a nutshell, Hanami is an application framework for Ruby, and it’s the perfect time to share this because just at the end of last year, we released our 2.0 version.
00:02:14 I’m really excited for the possibilities it might unlock, not just for the shape of Ruby apps, but also for each of us as Ruby developers.
00:02:20 I conceived of this talk at the end of last year while thinking about the new year and New Year's resolutions. We have a new framework and a new title, but it’s already mid-February.
00:02:39 I thought, well, I’m up for resolutions at any time of year. Maybe this is a chance to come up with a different tagline or title.
00:02:53 So, what would be the theme? Well, we’re back! We’re gathered here again after a three-year break—RubyConf is back!
00:03:03 I can stop pinching myself! It’s day two, and getting on the beers is optional, of course. I may have one after this.
00:03:10 Let’s see how we can introduce Hanami while injecting a bit of Australian flavor at the same time. I took a run at a few Aussie-inspired titles.
00:03:39 For starters, we could talk about the run towards Hanami 2.0. Here we have a seminal moment in Australian sporting history. Maybe this release can be a similar moment for this project.
00:04:03 I think we can do more! Parents in the room might know what's coming next. Yes! We can talk about Hanami 2 for real life. Bluey is currently Australia's number one cultural export, and I could only hope for some small measure of the success they're enjoying.
00:04:24 At the same time, I would love to share some of the joy and passion that we see in Bluey and Bingo. We can stay on the small screen for a bit—perhaps more from my own childhood. We can flip the channel to celebrate Hanami 2 and Friends.
00:04:50 Mr. Squiggle had a real ability to take inscrutable patterns and turn them into something cohesive and surprising. Well, maybe Hanami could help us do that for our Ruby code.
00:05:10 One more, and I have to thank Pat for bringing some political vibes yesterday. This title is for the Oz poll crowd—Hanami 2: It’s Time. Whitlam brought in progressive and reformist energy to Australia after 20 years of, well, not much.
00:05:38 This sentiment feels pretty strong. I can vibe with this one. As Pat made very clear, we should not rest on any laurels in the Ruby community. Maybe it's time for something new.
00:06:06 But ultimately, what I want to get across is that with this framework, we might find a place that will help us continue to call Ruby home, remain in this community, and use this technology that we know and love while still growing and learning.
00:06:30 So let’s get into it. Let’s learn a bit about how we got here with Hanami. For starters, the project has been around since 2014, and when it was first released, it was advertised as a simple, fast, lightweight web framework.
00:07:00 It focused on maintainability and testability. Then, just a year later, in 2015, the Dry-RB project began, where I was working at the time, and we, too, had a strong emphasis on helping Ruby developers build more maintainable applications.
00:07:19 It wasn’t long before both teams realized they were working toward the same goal, so we joined forces. After a busy four to five years, we’ve shipped 2.0.
00:07:38 Now we have something that brings the power and flexibility of Dry-RB with a friendly and streamlined out-of-the-box experience. I'm talking today, but this was a real team effort.
00:07:55 I want to give a shout-out to these legends, particularly Luca and Peter, my teammates on the core team.
00:08:03 Here’s our plan for today: Three phases. We’re going to do some building to get to know Hanami. From there, we’ll learn more about what the framework can offer.
00:08:20 Lastly, we’ll take a look at how that might help us grow and see Ruby in a new light. So let's get started!
00:08:35 We can gem install Hanami, and from here we can create a new app for today. We’ll call ours 'Bookshelf.'
00:08:47 Let's start our tour by checking out the app class itself. This is nice and simple—we won't need to dwell too long here. But there is one thing I want to point out.
00:09:00 In Hanami apps, all your code lives within a single namespace that matches the app’s name. So, here we have 'module Bookshelf.' This is where all our code will live.
00:09:15 Next, we can check out the routes, which are also very simple. In a new Hanami app, we just have this basic starter route, giving us this welcome string.
00:09:30 From this beginning, we need to make our app real. The first thing we need to do is generate an action, and Hanami provides a nice generator for that. For starters, we’ll create a 'BooksIndex' action.
00:10:00 This generates a new route for us. It looks pretty simple: a GET request to '/books' will take us to that 'BooksIndex' action.
00:10:18 Here’s the action that the generator made for us—it's a basic starter action. The job of an action in a Hanami app is to handle all of our HTTP interactions.
00:10:38 We have exactly one action per endpoint in our app. The 'handle' method is the most important part of each action because that's where we tell it how to behave.
00:10:50 We are given separate request and response objects, and with these objects, we can do things like inspect request parameters or set the response body.
00:11:09 Speaking of those request parameters, Hanami actions support flexible parameter validation schemas, which help us ensure our parameters meet our expectations in terms of both structure and type.
00:11:26 Since this is an index action, we’ll want a couple of optional parameters: a page number and a per-page value. For both of these, we want to make sure they are integers greater than zero.
00:11:44 This gives us a chance to return an error response if any of those parameters are not valid. We can see here that actions give us a range of helpful features for handling HTTP interactions.
00:12:00 Now, where are we going to put the job of our business logic? How can we fetch the books that we want to display? This is where we get to know one of Hanami's most important features.
00:12:14 In Hanami, our app is also a container, which serves as a central organizing object responsible for loading and managing access to all the components in our app.
00:12:36 Our action is already a component, and we can fetch it from the app using a key that matches its name.
00:12:48 What we get back is an instance of that action ready to be used. In fact, any class that we put into 'app' will be made available to us as a component.
00:13:00 So, in our case, let's create a component for a 'BookRepo,' something that can return a list of books for our action.
00:13:16 It could look something like this: since we put it in the 'app/repos' directory, we'll give it a matching namespace inside 'Bookshelf.'
00:13:31 Then we can add a method for some of the latest books we've read. This is clearly a stand-in for something that connects to a real data source, but for the sake of our little starter app, it will do.
00:13:49 Now we have two components: our action and our repo. But how do we get them to know about each other? We really want our repo to be available inside the action to retrieve our books.
00:14:06 For this, we can use Hanami's dependency injection (DI) mixin. We can include this mixin inside any class in a Hanami app, whether it is an action or some other type of class.
00:14:24 Then we pass in a list of components we want as dependencies, and those components become available as instance methods backed by instance variables in all shapes and sizes.
00:14:39 We can use them wherever we need inside our classes. Using DI like this is central to how we can create higher-level behavior by bringing together a range of smaller, more focused components.
00:14:56 In this case, we can now use the latest books from our 'BookRepo' to create a JSON response that we return from our action.
00:15:17 After all of that, we can now run our Hanami server—good old Puma—where we can see that it works. We can make a request and view a list of our books.
00:15:35 So, well done, everybody! We've made our first working feature with Hanami, which has helped us to get to know the core of the framework.
00:15:51 This has given us a good spot from which to jump off and explore more features. We’ll start by looking at the testing experience.
00:16:06 How can we test our action? Every part of Hanami is designed to be directly testable. In this case, we can call 'new' directly on the action and even pass in a test double for the 'BookRepo' dependency.
00:16:22 This will let us control its behavior for the purposes of this test. After that, everything's set up for us to call that action, test its response body, and ensure it meets our expectations.
00:16:39 What we see here is a low-level isolated test. For most actions, we won't want to go this far; a more appropriate kind of test would be an outside-in test, which makes a request to our app from the outside.
00:16:52 This runs all the way through to ensure it behaves as we want. Those request specs are available in every new Hanami app. The takeaway is that because all Hanami components are designed to be directly testable, we get to make the best decision for each testing situation.
00:17:14 We can also explore our code through a console. We can launch the console with the 'Hanami console' command, and once we’re in, we can use this 'app' shortcut to get access to our app.
00:17:32 From there, we can access all of our components, which again come back as instances ready for us to work with. This console always starts quickly, no matter the size of the app.
00:17:49 In Hanami 2, we offer two levels of boot mode. The first we call 'prepare,' which is a lightweight way of booting our app. It minimizes what it loads upfront, and we use this in all aspects of development.
00:18:13 From tests to the console, to even running our development server. This means that as large as our app may grow, the development experience remains snappy.
00:18:27 Along with this, we have a more traditional boot process that loads everything upfront, which is perfect for pre-forking web servers like Puma.
00:18:43 One thing that happens across both levels of boot is that Hanami will load our settings—these are the important values we give our app to ensure it behaves correctly in every environment.
00:19:09 Things like keys, secrets, and flags. We can define our settings, for example, if we wanted to send emails whenever new books are added.
00:19:25 To achieve this, we would use a third-party email API and add a setting for its API key, specifying that we want this to be a string. Hanami will then load these settings from matching environment variables.
00:19:45 In development, you can also use '.env' files to supply them. From there, we can access our settings as components in the app.
00:20:01 It comes with methods for each of the settings we've just defined so we can retrieve their values. We can also include it as a dependency of any object, thanks to our DI mixin.
00:20:16 Aside from these settings, so far we've only seen components that we've loaded by defining classes in our app directory.
00:20:34 But what about components that might need special handling as part of their setup? This is where we can use providers.
00:20:52 We need to make one for our email service. We've already created the setting for its API key, and now we need to use it. Providers have their own folder in config.
00:21:09 Here, we're registering our email service provider. In every provider, we have a couple of lifecycle steps we can use.
00:21:25 For starters, here we're going to require the gem for the email API client. Then we'll grab our API key from the settings component that we just set up.
00:21:43 We'll pass it in to configure that client and register that client object as a component in our app.
00:21:57 Now this means that any class using the email service as a dependency will get that fully configured client, ready for them to work with.
00:22:08 By now, we've covered nearly everything we need to know about Hanami apps, but there's one more important concept for us to discover: slices.
00:22:27 Slices help us organize our app into separate domains or technical concerns. They're a fantastic way to introduce modularity and clearer internal boundaries as our app grows.
00:22:45 I was really glad that Julian talked yesterday about Rails engines. As I listened, I thought, this guy's in my brain. Everything he said could be done with slices in Hanami.
00:23:01 The difference is that with Hanami, slices are the heart of the framework. They form part of the recommended path as people build their apps.
00:23:18 They appear as soon as you add a file into the slices directory. We have a helpful generator that we can use to get started.
00:23:35 Let’s make an admin slice. This will prepare an 'admin/' directory for us, and we can create a new action in it, such as a 'BooksCreate' action.
00:23:50 Just like our app, every slice maps to a single Ruby namespace, meaning we're using 'Admin.' Hanami will also create a slice class for us, which works just like our app.
00:24:06 It offers access to components, but with the limitation that it's confined to the components inside its slice directory—this is how we create boundaries.
00:24:25 Also, just like the app, slices have their own DI mixin with the same limitation—they only load components from inside the slice.
00:24:39 Now we've seen that both the app and slices act as containers for our individual components. With the DI mixin, we can recognize the dependencies between components and their relationships.
00:25:00 From here, we can think of them as part of a big directed graph. We can take things one step further since slices can import components from other slices.
00:25:17 This means we can envision an equally clear graph of all our app's high-level concerns. This is a really powerful approach; we have the tools built right into the framework to help us better organize our code at all levels.
00:25:33 This leads to easier understanding and maintainability. Slices provide operational flexibility, too, by allowing us to configure the slices we want to load for specific deployments.
00:25:53 One key goal with Hanami 2 is to extend this power to all types of apps, not just web apps. Let’s look at the gem file of a brand new Hanami app.
00:26:14 What we see is that this is indeed a web app because we have the Hanami router and the Hanami controller gems included.
00:26:31 However, these are not fixed dependencies of the framework, which is why they're listed right here in the gem file.
00:26:39 If we wanted to build something different, we could remove those extra lines. With only the Hanami gem remaining, we would have all the quality-of-life features we've just explored.
00:27:03 Containers, components, dependency injection, settings, the console, and even slices. This means we can keep everything we need and nothing we don’t.
00:27:21 We can utilize all these features for any type of app. Therefore, Hanami 2 is no longer just a web framework; it is the 'everything' framework.
00:27:37 Say we want to build a chatbot, a CLI tool, a serverless function, or a stream consumer. We can do it all with the conveniences of a full framework.
00:27:56 I don’t think Ruby has ever seen anything quite like this, and I'm really excited about where people might take it.
00:28:05 With all this flexibility we've shared and that we've given to Hanami, I’m sure some of you may have noticed a few things missing.
00:28:23 Like that connection to a database that I just smoothly glossed over, or a way to interact with it. I want to be real about this for a minute.
00:28:44 Working on a project for four to five years without a release is pretty tough. We're a small team; this is a pure passion project with no commercial backing.
00:29:02 I'm sure you all know the feeling of a project that drags on for too long—it can be draining. But it’s 2023, and if there’s one thing I’ve learned recently...
00:29:12 Well, maybe it's okay for things to take their time. By some measures, we were doing okay.
00:29:27 To bring back another Aussie vibe—life would be pretty straight without a complex, multi-year open source project to keep you busy.
00:29:42 Working across that time period, I think it worked out pretty well in the end. It gave us time for things to take their twists and turns, helping us discover the type of experience we wanted to offer.
00:30:01 In the end, we had to ship something, so we did the one thing we could: we cut scope. Everything we’ve seen so far is in the 2.0 release on rubygems.org.
00:30:14 This has gotten us, as a team, into a good place to finish the remaining parts. Because you're a few hundred of my closest friends, I want to offer you a bit of a preview.
00:30:31 We can start with database persistence, and to introduce this, I’ll share a little more about the relationship between Hanami and its sibling projects.
00:30:45 All the logos I showed you before, everything we’ve seen in Hanami 2, has been built around the Dry-RB gems.
00:31:04 When someone picks up Hanami and creates their new app, they work with useful gems that are themselves standalone and independently useful.
00:31:19 They are part of a Ruby ecosystem that prioritizes flexibility and support for apps of all kinds. What Hanami brings is a curated experience that combines these gems into a cohesive structure.
00:31:38 For database persistence, we are going to do the same just with ROM-RB. ROM is a powerful, flexible standalone database persistence toolkit with a long pedigree.
00:31:59 For Hanami 2, we’re not going to hide it behind some simplified interface. Instead, we’re going to elevate it so you’ll have access to every feature of ROM, along with a nice on-ramp and helpful conventions.
00:32:16 This way, it fits beautifully within a larger app. Let’s take a look at how that will work.
00:32:31 We can start by revisiting our 'BookRepo,' where we were returning an array of static data before.
00:32:44 To make this repo communicate with the database, all we need to do is have it inherit from our app's base repo class, which will be generated for us when we create our app.
00:33:04 From there, we can now change our 'latest' method to select what we want from the books table in our database. Here, we're using ROM's full-powered query API.
00:33:21 In repos like this, we can add methods for both reads and writes, turning them into our own interface to the database.
00:33:37 This helps us separate our persistence logic from our business logic, ensuring our database uses are considered and intentional.
00:33:54 When we call these methods, the records from the database are returned as instances of our own matching struct classes.
00:34:09 These are simple value objects; they don’t carry any connection back to the database. This means we can pass them throughout our app with confidence.
00:34:24 Our repos remain the key interface for interacting with that persistence layer. That's persistence!
00:34:38 We can also look at views, and here we’ll see an approach that's fairly similar to what we've already looked at. Every view in Hanami has its own class.
00:34:54 So here's one for a 'BooksIndex.' Just like everywhere else, we can include dependencies here. So since this is our 'BooksIndex,' let's bring back that 'BookRepo' we've just updated.
00:35:08 In our view, we can create exposures to prepare the values that we want to pass to our template. In this case, we'll pass the latest books from our database.
00:35:20 In the template, we can work with those values to prepare the HTML that we want to render. Views themselves are components too.
00:35:37 This means we can include them as dependencies in other classes like here in our 'BooksIndex' action, which was the first thing we made.
00:35:51 Inside our 'handle' method, we can now render the view in our response. At this point, our action is no longer a JSON action; it is returning all our latest books as HTML.
00:36:07 Since rendering an action's matching view is a common task, Hanami will find that view for us, so we don’t even need the DI mixin in cases like this.
00:36:23 That's a preview of persistence and views! We’re still figuring out all the minor details, but it should give you a good idea of how these elements will broadly fit together.
00:36:41 With our views, when we release these, we’ll include everything you need to create a full-stack web application, including integration with asset bundlers for front-end assets.
00:36:58 So there we go, we’ve seen every part of Hanami now, including the bits that are coming soon. Work on those will happen in the next couple of months.
00:37:16 In the meantime, this is a great chance to get a head start and familiarize yourself with Hanami's core features.
00:37:31 I’m really excited about how it can help us grow as Rubyists, because I think Hanami brings more to the table than just a collection of framework features.
00:37:44 It provides a new perspective on what constitutes good app design, and it embodies these principles in its design.
00:37:58 From this, we can learn and use these insights to foster our growth as developers. So how can we grow? Firstly, we can deepen our understanding of our app’s domain.
00:38:18 Hanami encourages us in a number of ways, and the most important of these is that it clarifies: the framework is not your app.
00:38:37 These two things are separate. The framework should work in service of our app, not the other way around. Therefore, Hanami routes and actions are designed to be as thin as possible.
00:38:53 They act as a gateway into our app, which is where our core business logic resides. This core business logic is ours and is meant to be separate from framework concerns.
00:39:09 If our business logic is separate, we need to think extra about how to name and organize that code. We want to discover the right arrangement of concepts manifested as components.
00:39:29 Let's make that code a useful expression of our business domain. Because Hanami makes it easy to decompose our logic into independent, focused components, we can take control over the shape of our code.
00:39:44 So much of this is about growing our modularity muscle—boiling things down into focused components and composing them to create the high-level behavior we want to offer our users.
00:39:59 This largely revolves around figuring out boundaries. Let’s be honest, figuring out boundaries is critical to every aspect of our lives, and it's essential for us to practice as developers.
00:40:14 We never stop working on this. We'll do ourselves a big favor by choosing to work with a framework that acknowledges this crucial activity and equips us with the tools to address it, like Hanami does with its components and slices.
00:40:30 This gives us a tremendous opportunity to grow as Ruby developers. The framework is not our app, which means that much of our app can be written in what is effectively plain old Ruby.
00:40:54 That’s glorious! When we build confidence with Ruby, it does wonders for our ability to think and problem-solve fluidly in this language.
00:41:09 You might even begin to see Ruby through a new lens. What you might discover is that when we create objects with injected dependencies, which offer one or more methods that receive arguments and return a value—all while not modifying the internal state of that object.
00:41:25 Well, then that begins to look a little bit like functional programming, which can bring greater clarity to the flow of our code. Every core Hanami component is built along these lines.
00:41:45 You have brilliant examples to follow. When you explore Hanami, you start experiencing a broader part of the Ruby ecosystem.
00:42:01 It builds upon the rich heritage of gems that support Ruby apps of all types, including Dry-RB and ROM-RB. You may find inspiration for your own code from how these gems solve their challenges.
00:42:21 I think we have many opportunities for growth. If we go back to our original premise: a new framework, new you. I want to reflect on that for a moment.
00:42:39 And I know we've seen many new ideas, which can be overwhelming, but I want to reassure you through my own story.
00:42:55 I know that a new framework leading to a new you is possible. I lived it myself, starting around seven years ago.
00:43:11 At that time, I had been writing Ruby for a long time, but I was still dissatisfied with how my apps were turning out. I knew I wanted something different, but I didn’t know how it should look.
00:43:26 So instead of searching for a different language, I looked around the Ruby ecosystem. I found the ROM gem and, later, another gem called 'Router,' which is a standalone routing toolkit.
00:43:41 From these gems, their ideas, and the community behind them, opened up a new world for me. It led to one thing after another.
00:43:56 I tried the gems, incorporated them into my apps, and gradually my apps began to reshape as I better understood the concepts.
00:44:08 Now, here I am, helping to build a range of gems to make these insights more accessible than ever.
00:44:20 These seven years have been the most fulfilling of my Ruby journey, and I hope to make it clear that no matter where you are on your developer path, there is always an opportunity to forge a new direction.
00:44:36 So with that in mind, I want to encourage you all to finish off with a resolution. Let’s try something different.
00:44:50 I would suggest: be curious! There’s a lot out there to learn—the Ruby ecosystem is larger than you think!
00:45:04 Maybe Hanami can be your gateway to something new. Be bold! If you find something interesting, give it a try.
00:45:18 If that feels uncomfortable—well, that's probably a good sign, which means you’re learning. Be excited! I know I am.
00:45:34 I think we are witnessing the beginning of another special moment for Ruby— a resurgence of new ideas. This is your chance to be part of it.
00:45:48 Lastly, be persistent—you can always try and try again! Just to leave you with one final Aussie reference, think of your opportunities like an endless packet of Tim Tams: you can always grab another and try things again.
00:46:04 That is everything from me! If you’d like to learn more, check out the Hanami site. We’ve got a great getting started guide for this release.
00:46:17 I'm also developing a complete open-source example app called 'Decaf Sucks,' which is on GitHub. I’m just getting started, so if you’d like to follow along, now’s a great chance.
00:46:34 And lastly, I’m always up for a chat! Find me at the conference, mention me, or reach out on Mastodon. Let’s have a conversation. Thank you very much!
Explore all talks recorded at RubyConf AU 2023
+11