Back in the mists of time, I used a tool called rvm. It was more than just a Ruby version manager, it was also a gemset manager: instead of using bundle exec or a binstub, you could just type a command and it would “work” because every version+gemset was a pristine environment. Cool in a way, but also very complicated.
I eventually migrated to the simpler rbenv and have used that for years now. It only manages Ruby versions and expects you to make use of Bundler to keep gems and executables in line. Y’know. Modern Ruby.
But what if you could use something even more modern than rbenv? Enter rv.
Inspired by uv in the Python ecosystem, rv aims to become your singular solution for total management of the Ruby environment. Right now this is primarily focused on installing Ruby versions, but in the future it will support installing gems and running executables. “Wait, would this then replace Bundler?” you might ask. That’s a distinct possibility, although my experience interfacing with Bundler as part of the Bridgetown framework leads me to believe Bundler isn’t going anywhere any time soon. (rv would need to offer a host of Ruby APIs to replace similar APIs provided by Bundler.)
One of the reasons rv is so fast is because it’s written in Rust. It seems to me the Rust is quickly becoming the new C when it comes to lower-level system languages operating alongside Ruby. A number of Rubyists are brushing up on their Rust and contributing to new tools like this, and I’m excited to see where that leads the ecosystem.
I’m using rv now in a couple of environments instead of rbenv, and it works quite well. Once the gem installation feature lands, you can be sure I’ll give that a thorough workout and report back. Bonus: many of the folks working on rv and other experimental tools like Ruby Butler are also involved in the gem.coop initiative I wrote about which is equally exciting.
A shorthand for how I think about the design of new APIs, whether I’m working on the Bridgetown web framework or another library, is a principle I’ve come to call Don’t Make Me Think. (DMMT)
DMMT plays out in many ways. Sometimes you might compare it to the Principle of Least Astonishment (or Surprise), but ultimately I think it’s more about the vibes, man and taste born of experience than any pat technical explanation. The aforementioned Wikipedia article also links to some other interesting and related principles such as Do What I Mean (DWIM) and that old chestnut Convention Over Configuration.
When it comes to software testing, I believe the DMMT principle needs to kick into overdrive. Let me set the context for my entreaty:
I do not, have not, and will not practice TDD (Test-Driven Development). Does that shock you? I hope not. While I have no problem with people who are adherents to the TDD philosophy, I do have a massive problem with the notion that one must practice TDD to be a Good Programmer. Here’s what I do: I write tests when I need to write tests. Sometimes that’s before I work on a problem, in which case it looks like I’m practicing TDD. Other times I write tests after I’ve solved a problem. Still other times, I write no tests at all. Again, does that shock you? I hope not.
I do not enjoy writing tests. I enjoy solving problems. Test-writing is a “side effect” of wanting to author robust and reliable code, just as dish-washing is a “side effect” of keeping your kitchen clean and useful for the real task of cooking and eating delicious food. So while I might sometimes tolerate a certain degree of fussiness and ceremony in the process of authoring user-focused code because the end results are worth it, I have no such tolerance in my authoring of tests. Get in, get green, get out as fast as humanly possible is my motto!
Given all that, I have spent the bulk of my career as a programmer wondering why some testing frameworks and techniques can be such a right PITA. Because if the primary goal is to get me to write more tests (a somewhat noble goal I might also poke holes in as the contrarian that I am), your testing framework is not doing its job!
From RSpec to Minitest Assertions to Expectations to Intuitive Expectations #
Like many Rubyists, my first memories of working on Ruby applications and writing tests was in relation to the RSpec testing framework. Now I’m not here to cast aspersions on the creators of RSpec nor its many fans. Let’s just say RSpec has never tickled my fancy.
I mean,
expect(actual).tobe_within(delta).of(expected)
superficially looks like a cool Ruby DSL, and we all love a good DSL right? Unfortunately, RSpec routinely falls down pretty hard on the Don’t Make Me Think (DMMT) principle. I have to hold all of this syntax in my head:
expect: this is straightforward enough.
.to: why is this here? Why can’t I just write expect and then a matcher like eq? Because there’s also not_to which is the opposite of to. I have to remember this now.
be_*: oh great, now I have to remember all of these various matchers starting with be…er, except that many matchers don’t! Can I write expect(something).to be_equal_to(3)? No! I can’t even though that sounds like a full English phrase. Some matchers literally make no grammatical sense to me at all. Like this one:
expect(10).tosatisfy("be a multiple of 5")do|v|v%5==0end
Huh? 10 should satisfy being a multiple of 5? I would never talk like this.
of: is this just a one-off? Or are there other of cases in the matchers API? Answer: it’s a one-off! This definitely is a violation of the Principle of Least Astonishment. I don’t think it’s reasonable that someone learning a testing framework should have to know that for only one specific matcher there is a unique piece of DSL to learn and use.
Now it’s true I would probably not use this particular matcher very often, as I don’t typically work on math problems requiring these calculations. But I think it’s a good illustration for the point I’m making.
There’s also a lot about the RSpec code styles and ecosystem I don’t particularly care for. I don’t like all the indirections of multiple lets and how mocks look and just a whole host of issues. Again, if you personally dig all that, good for you! I always found it to be a miserable experience.
As time passed, I started to run into increased usage of Minitest and a breezy style of writing very simple test methods with a small set of possible assertions. Coming from RSpec, it’s almost unbearably terse, but once you get used to it, it’s hard to beat. For some cases like simply testing if something is true or false, it’s a revelation:
While that sort of syntax might not feel wildly Ruby-esque, I think it’s an amazing example of the DMMT principle.
However, as time passed I started to grow a bit frustrated with simply writing assertions all the time. Not only is there the strange “backwards” arguments issue of Minitest’s assert_equal with the expected rather than the actual coming first (not a design flaw of Minitest per se, it has a long history predating Minitest and even the Ruby community), I don’t particularly care for the way my tests look with dozens or hundreds of assert_* or refute_* statements. I also am a big fan of spec-style test writing with blocks of describe/it or context/should or however you want to define those terms, and often you find the spec-style tests written not with assertions but with “expectations”.
Minitest offers its own expectations syntax which I like to use with expect. The matchers all start with either must or wont:
Yet, me being me, it wasn’t long before I started to ask myself the following question: why do I need to think about all of these must_* and wont_* matchers? DMMT! I should be able to write the most standard, most obvious Ruby language code possible without needing to learn much of anything at all.
For example, if I wanted to check the equality of two values, couldn’t I simply write this?
expect(foo)=="bar"
And if not, why not?
With that question in mind, I set out to experiment with the most Don’t Make Me Think testing paradigm possible…and I succeeded. 😎
How Intuitive are Intuitive Expectations? Very Intuitive! #
A new feature provided by the Bridgetown Foundation gem (and one you can use in any Ruby app, not just a Bridgetown project), Intuitive Expectations builds upon Minitest’s exceptions and provides simpler and chainable variations.
Here are some examples from the docs:
expect(some_int)!=123expect(some_big_str)<<"howdy"# or expect(...).include? ...expect(some_bool).true?# aliased to truthy?expect(2..4).within?(1..6)expect(food_tastes)=~/g(r+)eat/
I also am a huge fan of chainable DSLs (aka where the methods return self so you can reuse that context), so naturally I had to make sure that works as well:
We’re really excited on the Bridgetown Core team to be moving away from an older testing style based originally on a fork of Jekyll using the Shoulda gem and Minitest assertions to Minitest spec-style and Intuitive Expectations (not in all but in many cases). Here’s an example excerpt from such a test:
classTestComponents<BridgetownUnitTestdescribe"Bridgetown::Component"doit"renders with captured block content"do# lots of funky whitespace from all the erb captures!spaces=" "morespaces=" "expect(@erb_page.output)<<<<~HTML
<app-card>
<header>I'M A CARD</header>
<app-card-inner>
#{spaces}
<p>I'm the body of the card</p>
#{morespaces}<img src="test.jpg" />
#{spaces}NOTHING
</app-card-inner>
<footer>I'm a footer</footer>
</app-card>
HTMLendit"does not render if render? is false"doexpect(@erb_page.output).exclude?("NOPE").exclude?("Canceled!")endit"handles same-file namespaced components"doexpect(@erb_page.output)<<"<card-section>blurb contents</card-section>"endendend
Beauty is in the eye of the beholder and all art is subjective, but I truly believe Minitest Spec + Intuitive Expectations is the most elegant test syntax I’ve ever laid eyes on. Once this was available to me, I grew used to it so quickly I now feel like any project is “broken” if I can’t use this syntax. It’s so familiar to me that I quickly wrote a JavaScript version built on top of Node’s built-in assertions and testing features so I can write this in JavaScript:
content=awaitpage.evaluate(()=>document.querySelector("div").shadowRoot.innerHTML)expect(content).include("<p>I'm in the shadow DOM!</p>")expect(content).notMatch(/s\w*z/)constfunkyTown=awaitpage.evaluate(()=>document.documentElement.dataset.results)expect(funkyTown).equal("won't you take me 2")constcount=awaitpage.evaluate(()=>document.querySelectorAll("#delete-me").length)expect(count).notEqual(1)
I guess once you expect Intuitive Expectations, you never go back.
Assuming you haven’t been living under a rock these past few weeks, the Ruby community has been embroiled in quite a bit of drama. I won’t recap it here…there are plenty of other sources to go (Joel Drapper for one), and I also have my own pointed take on the matter on my personal blog. But here on Fullstack Ruby I like to maintain a positive, can-do attitude, and to that end, let’s talk about some very exciting developments!
Most Rubyists are familiar with rubygems.org and the reason that you see source "https://rubygems.org" at the top of every Gemfile is so Bundler can download and install gems from the rubygems server.
So Bundler is very flexible in this regard, as is the gem command (more on that in a moment). Which is why this news matters: The Gem Cooperative has announced a new community-minded gem server is now available, currently mirroring all the gems from rubygems. Martin Emde says that “all Ruby developers are welcome to switch to using this new server immediately.”
And here’s how you do it. Edit your project’s Gemfile and replace the source line at the top with this:
source"https://gem.coop"
Now you can bundle install and bundle update and support this new community effort.
Who is behind The Gem Cooperative, you may ask? Basically it’s all those folks who had previously been working on rubygems before they were unceremoniously kicked out of Ruby Central during the takeover. Ouch. From gem.coop, they are:
And Mike McQuaid of Homebrew fame is also helping out with establishing governance for the project (and some technical advisory from the looks of things).
Work is currently underway on an updated version of Bundler that will support namespaces, and once that happens and gem.coop is updated to support gem pushes, we could see folks decide to publish new/updated gems only to gem.coop under new namespaces. This is all in service of moving away from Ruby Central as a single (and very problematic) point of failure.
All right, so updating your Gemfile is easy enough, but what about when you need to install new gems from scratch using the gem command? You will use gem sources for that. First, you can get a list of which sources you currently use by running gem sources --list. Typically you’ll just see the rubygems server listed. To add gem.coop, run:
gem sources --add https://gem.coop
and to remove rubygems, run:
gem sources --remove https://rubygems.org/
You can verify when you install a new gem which server is used by including the -V flag, e.g.:
gem install solargraph -V
Otherwise just use gem as per usual.
With news like this and the previous round of news regarding rv which is an attempt to create next-generation unified Ruby tooling (install Ruby, install dependencies, run app commands, create new gems, etc.), I think we may be on the cusp of a big leap forward for the language both in terms of technical prowess as well as acceptable forms of community governance.
Well my Ruby friends, a new day has dawned with the release of the Ruby web framework Bridgetown 2, and that means I can start to enjoy the fruits of our labor by sharing useful code examples and architectural explanations here on Fullstack Ruby. Yay! 🎉
On a Bridgetown client project, we wanted to be able to drop in links to the client’s many videos hosted on Vimeo. I didn’t want to have to deal with the hassle of grabbing <iframe> tags for every single video, so my first inclination was to write a helper method and use those calls in the markup where needed. But then I realized I could go a step further: just paste in the damn link and get an embed on the other side! 😂
It needs to go from this Markdown source:
Ever wonder what it's like to dance under the sea? Here's your chance to experience lights that simulate moving water. These are customizable with different color variations and ripple speeds.
https://vimeo.com/390917842
to this HTML output:
<p>Ever wonder what it’s like to dance under the sea? Here’s your chance to experience lights that simulate moving water. These are customizable with different color variations and ripple speeds.</p><p><iframesrc="https://player.vimeo.com/video/390917842"width="640"height="360"frameborder="0"allow="autoplay; fullscreen; picture-in-picture"allowfullscreenloading="lazy"></iframe></p>
And using a bit of string substitution in a builder hook, the solution is straightforward indeed:
In your case you might be using YouTube, or PeerTube, or some other form of video hosting, but the concept would be just the same. You could even layer up several gsub calls to handle them all.
For better frontend performance, due to the large number of images we display on some of the content pages, I wanted to ensure that images added in the Markdown would output with a loading="lazy" attribute. This tells browsers to hold off on loading the image until the reader scrolls down to that place in the document.
After making sure I had gem "nokolexbor" installed, and had added html_inspector_parser "nokolexbor" to my Bridgetown configuration in config/initializers.rb, I proceeded to write an HTML inspector plugin to do the job:
On another project, I wanted to have some smarts where the image used for open graph previews could be pulled directly out of the content, rather than me having to set an image front matter variable by hand. I decided to solve this with a bit of regex wizardry:
# plugins/builders/image_extractions.rbclassBuilders::ImageExtractions<SiteBuilderdefbuildhook:posts,:pre_renderdo|resource|nextifresource.data.image&&!resource.data.image.end_with?("an-image-i-wanted-to-skip-here.png")md_img=resource.content.match%r!\!\[.*?\]\((.*?)\)!img_url,_=md_img&.capturesunlessimg_urlhtml_img=resource.content.match%r!<img src="(.*?)"!img_url,_=html_img&.capturesendifimg_url&&!img_url.end_with?(".gif")img_url=img_url.start_with?("http")?img_url:"#{site.config.url}#{img_url}"# Set the image front matter to the found URLresource.data.image=img_urlendendendend
You could simplify this if you’re only dealing with Markdown content…in my case I have a lot of old HTML-based content predating the age of modern Markdown files, so I need to support both input formats.
And that’s it for today’s round of Bridgetown tips! To stay in touch for the next installment, make sure that you follow us on Mastodon and subscribe to the newsletter. What would you like to learn about next for building websites with Bridgetown? Let us know! ☺️
I always hate writing posts like this, which is why I rarely do it and tend to let content destinations linger on the interwebs indefinitely.
But I’m in the midst of spring summer cleaning regarding all things content creation, so I figured it’s best to be upfront about these things and give folks a heads up what I’m currently working on.
TL;DR: I’m bidding the Fullstack Ruby podcast a bittersweet farewell and gearing up to launch a new podcast centered on current events in the software & internet technology space, because we’ve reached a crisis point and future of the open web is more fragile than ever.
Here’s the truth. There’s a lot that’s fucked up about Big Tech and software development right now. Pardon my language, but I have struggled mightily with burnout for going on two years now; not because I don’t like writing software (oddly enough, I care as much about the actual work I do as a developer as I ever have!), but because I don’t like the software industry. ☹️
And sadly, I have been particularly disappointed with what’s going on with Ruby. I don’t want to rehash past consternation (you can read about my attempt to fully reboot Fullstack Ruby a year ago for more background, and listen to the followup podcast episode). Here’s the summary:
There are two devastating downward pressures on the software industry right now: the unholy alliance of far-right bigotry/propaganda & Big Tech, and the atomic bomb-level threat to the open web that is Generative AI. And the crazy part is, there’s actually a cultural connection between fascism and genAI so in a sense, these aren’t two separate problems. They’re the same problem.
Unfortunately, the Ruby community taken as a whole has done NOTHING to fight these problems. Certain individuals have, yes. Good for them. It’s not moving the needle though.
Ruby already has suffered in recent years from the brain-drain problem and the lack of mainstream awareness for new project development. I have always felt that problem alone is one we can surmount. But when you pile on top of that the fascism problem (personified in the founder & figurehead of Ruby on Rails, DHH) and the AI problem (which major voices in the Ruby space have not only failed to combat but they’re actively advocating for and encouraging genAI use), I find it increasingly difficult to remain an active cheerleader going forward.
Don’t get me wrong, I’m not saying it’s any better per se in other programming language communities. But if I have to deal with fighting off fascism and the evils of Big Tech on a daily basis, I might as well be writing JavaScript while I’m doing it. JavaScript is the lingua franca of the web. It just is. And it’s already what we use for frontend, out of technical necessity.
I still prefer writing Ruby on the backend. I do, I really do! And I’m not sure yet I’m ready to give that up, even now. But it does mean I find my enthusiasm for talking about Ruby and recommending it to others fading into the background. Damn.
I haven’t yet decided what the ultimate fate of this blog is. But I do know I’m ready to scale my ambitions way down in this particular community, so to start, I must bid the podcast farewell. 🫡
As I alluded to above, I’m actually gearing up to launch a brand new podcast! You may be interested it in, you may not. Regardless, the easiest way to stay notified on the launch is to follow me on Mastodon and subscribe to the Cycles Hyped No More newsletter (the podcast will be a companion product if you will to that newsletter).
It should come to no surprise by now that the purpose of the new podcast is to take the twin perils of fascism-adjacent Big Tech and genAI head on. I will be speaking about this early, often, for as long as it takes for people to wake up and realize the open web is under assault. I don’t mean to sound unnecessarily dramatic, but while we’re over here arguing about programming languages and coding patterns and architectural choices for our web apps, the very web itself is getting pillaged and dismantled brick by brick by hostile forces.
I’m not going to let that happen without a fight.
I hope you’re interested in joining me in this fight. Stay tuned.
Thus I figured it was high time I took Hanami for a spin, so after running gem install hanami, along with a few setup commands and a few files to edit, I had a working Ruby-powered website running with Hanami! Yay!! 🎉
But then I started to miss the familiar comforts of Serbea, a Ruby template language based on ERB but with a few extra tricks up its sleeve to make it feel more like “brace-style” template languages such as Liquid, Nunjucks, Twig, Jinja, Mustache, etc. I’ve been using Serbea on nearly all of my Bridgetown sites as well as a substantial Rails client project, so it’s second nature to write my HTML in this syntax. (Plus, y’know, Serbea is a gem I wrote. 😋)
After feeling sad for a moment, it occurred to me that I’d read that Hanami—like many Ruby frameworks—uses Tilt under the hood to load templates. Ah ha! Serbea is also built on top of Tilt, so it shouldn’t be too difficult to get things working. One small hurdle I knew I’d have to overcome is that I don’t auto-register Serbea as a handler for “.serb” files, as Serbea requires a mixin for its “pipeline” syntax support as an initial setup step. So I’d need to figure out where that registration should go and how to apply the mixin.
Turns out, there was a pretty straightforward solution. (Thanks Hanami!) I found that templates are rendered within a Scope object, and while a new Hanami application doesn’t include a dedicated “base scope” class out of the box, it’s very easy to create one. Here’s what mine looks like with the relevant Serbea setup code:
# this file is located at app/views/scope.rbrequire"serbea"Tilt.registerTilt::SerbeaTemplate,"serb"moduleYayHanamimoduleViewsclassScope<Hanami::View::ScopeincludeSerbea::Helpersendendend
Just be sure to replace YayHanami with your application or slice constant. That and a bundle add serbea should be all that’s required to get Serbea up and running!
Once this was all in place, I was able to convert my .html.erb templates to .html.serb. I don’t have anything whiz-bang to show off yet, but for your edification here’s one of Hanami’s ERB examples rewritten in Serbea:
<h1>What's on the Bookshelf</h1><ul>{%books.eachdo|book|%}<li>{{book.title}}</li>{%end%}</ul><h2>Don't miss these best selling titles</h2><ul>{%best_sellers.eachdo|book|%}<li>{{book.title}}</li>{%end%}</ul>
This may not look super thrilling, but imagine you wanted to write a helper that automatically creates a search link for a book title and author to a service like BookWyrm. You could add a method to your Scope class like so:
Anyway, I’m totally jazzed that I got Hanami and Serbea playing nicely together, and I can’t wait to see what I might try building next in Hanami! This will be an ongoing series here on Fullstack Ruby (loosely titled “Jared Tries to Do Unusual Things with Hanami”), so make sure that you follow us on Mastodon and subscribe to the newsletter to keep abreast of further developments.
This is a right humdinger of an episode of Fullstack Ruby! I got the chance to talk with Karl Oscar Weber all about the Camping web framework, as well as his Grilled Cheese livestream, working as a freelancer, and how to criticize by creating as a programmer in a world fraught with political upheaval. Great craic, as the Irish say.
I don’t know about you, but after a while, I just get tired of the same arguments. Wouldn’t it be great if I could simply forward them instead? Let somebody else handle those arguments!
OK I kid, I kid…but it’s definitely true that argument forwarding is an important aspects of API design in Ruby, and anonymous argument forwarding is a pretty awesome feature of recent versions of Ruby.
Let’s first step through a history of argument forwarding in Ruby.
In the days before Ruby 2.0, Ruby didn’t actually have a language construct for what we call keyword arguments at the method definition level. All we had were positional arguments. So to “simulate” keyword arguments, you could call a method with what looked like keyword arguments (really, it was akin to Hash syntax), and all those key/value pairs would be added to a Hash argument at the end.
Fun fact, you can still do this even today! But it’s not recommended. Instead, we were graced with true language-level keyword arguments in Ruby 2. To build on the above:
Here we’re specifying hello as a true keyword argument, but also allowing additional keyword arguments to get passed in via an argument splat.
Back to the past though. When we just had positional arguments, it was “easy” to forward arguments because there was only one type of argument:
defpass_it_along(*args)you_take_care_of_it(*args)enddefpass_the_block_too(*args,&block)# if you want the forward a blockdo_all_the_things(*args,&block)end
There is also a way to say “ignore all arguments, I don’t need them” which is handy when a subclass wants to override a superclass method and really doesn’t care about arguments for some reason:
Things became complicated when we first got keyword arguments, because the question becomes: when you forward arguments the traditional way, do you get real keyword arguments forwarded as well, or do you just get a boring Hash?
For the life of Ruby 2, this worked one way, and then we got a big change in Ruby 3 (and really it took a few iterations before a fully clean break).
In Ruby 2, forwarding positional arguments only would automatically convert keywords over to keyword arguments in the receiving method:
However, in the Ruby of today, this works differently. There’s a special ruby2_keywords method decorator that lets you simulate how things used to be, but it’s well past its sell date. What you should do instead is forward keyword arguments separately:
The first thing you can do is use triple-dots notation, which we’ve had since Ruby 2.7:
defpass_it_along(...)you_take_care_of_it(...)enddefyou_take_care_of_it(*args,**kwargs,&block)puts[args,kwargs,block.()]endpass_it_along("hello",abc: 123){"I'm not a blockhead!"}
This did limit the ability to add anything extra in method definitions or invocations, but since Ruby 3.0 you can prefix with positional arguments if you wish:
defpass_it_along(str,...)you_take_care_of_it(str.upcase,...)enddefyou_take_care_of_it(*args,**kwargs,&block)puts[args,kwargs,block.()]endpass_it_along("hello",abc: 123){"I'm not a blockhead!"}
So at this point, you can mix ‘n’ match all of those anonymous operators however you see fit.
The reason you’d still want to use syntax like *args, **kwargs, or &block in a method definition is if you need to do something with those values before forwarding them, or in some metaprogramming cases. Otherwise, using anonymous arguments (or just a basic ...) is likely the best solution going, uh, forward. 😎
There are also higher-level constructs available in Ruby to forward, or delegate, logic to other objects:
The Forwardable module is a stdlib mixin which lets you specify one or more methods to forward using class methods def_delegator or def_delegators.
The Delegator class is part of the stdlib and lets you wrap a another class and then add on some additional features.
So depending on your needs, it may make more sense to rely on those additional stdlib features rather than handle argument forwarding yourself at the syntax level.
No matter what though, it’s clear we have many good options for defining an API where one part of the system can hand logic off to another part of the system. This isn’t perhaps as common when you’re writing application-level code, but if you’re working on a gem or a framework, it can come up quite often. It’s nice to know that what was once rather cumbersome is now more streamlined in recent releases of Ruby.