<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniele</title>
    <description>The latest articles on DEV Community by Daniele (@lupodevelop).</description>
    <link>https://dev.to/lupodevelop</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3672550%2F268b3a53-b26a-4ee9-9236-b8d7f2f2d3cc.png</url>
      <title>DEV Community: Daniele</title>
      <link>https://dev.to/lupodevelop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lupodevelop"/>
    <language>en</language>
    <item>
      <title>Testcontainer(s) for Gleam: yes, without the 's' : what it is, how it works, why it exists</title>
      <dc:creator>Daniele</dc:creator>
      <pubDate>Wed, 29 Apr 2026 06:33:00 +0000</pubDate>
      <link>https://dev.to/lupodevelop/testcontainers-for-gleam-yes-without-the-s-what-it-is-how-it-works-why-it-exists-2a20</link>
      <guid>https://dev.to/lupodevelop/testcontainers-for-gleam-yes-without-the-s-what-it-is-how-it-works-why-it-exists-2a20</guid>
      <description>&lt;h2&gt;
  
  
  I built testcontainers for Gleam and the name was already taken. Twice.
&lt;/h2&gt;

&lt;p&gt;Let me start with the name, because it tells you everything.&lt;/p&gt;

&lt;p&gt;The canonical testcontainers library for the JVM ecosystem is called &lt;code&gt;testcontainers&lt;/code&gt;. There is also one for Elixir, and it is called &lt;code&gt;testcontainers&lt;/code&gt;. Then someone built a Gleam wrapper around the Elixir one and published it as &lt;code&gt;testcontainers_gleam&lt;/code&gt;. By the time I showed up wanting to build something native for Gleam, both &lt;code&gt;testcontainers&lt;/code&gt; and &lt;code&gt;testcontainers_gleam&lt;/code&gt; were gone.&lt;/p&gt;

&lt;p&gt;So the library is called &lt;a href="https://hex.pm/packages/testcontainer" rel="noopener noreferrer"&gt;&lt;code&gt;testcontainer&lt;/code&gt;&lt;/a&gt;. Singular. Not because I think one container is enough. Because I arrived third.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, why build it at all
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;testcontainers_gleam&lt;/code&gt; is not a bad library. I want to be clear about that. It does what it says. It wraps the Elixir Testcontainers implementation and exposes it to Gleam code. If you are already in a mixed Elixir/Gleam project where that Elixir dep is sitting in your tree anyway, it is a completely reasonable choice and it probably saves you an afternoon.&lt;/p&gt;

&lt;p&gt;But it is a wrapper. It is Elixir-shaped. The API leaks the abstraction underneath. And if you want idiomatic Gleam, typed errors, opaque builders, the &lt;code&gt;use&lt;/code&gt; syntax doing its thing... you are fighting against the grain of it.&lt;/p&gt;

&lt;p&gt;Gleam deserved something native. Not a translation layer. A library that starts from what Gleam is good at.&lt;/p&gt;

&lt;p&gt;That is the itch. I scratched it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm5pyyrff3dsyvs8zyig3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm5pyyrff3dsyvs8zyig3.png" alt="pago - the mascotte" width="256" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet Pago
&lt;/h2&gt;

&lt;p&gt;Every good library deserves a mascot. &lt;code&gt;testcontainer&lt;/code&gt; has Pago.&lt;/p&gt;

&lt;p&gt;Pago is a paguro, a hermit crab. He carries a Docker container on his shell. He does not complain about it. He just carries it, cleans it up when the test is done, and goes home.&lt;/p&gt;

&lt;p&gt;That is the philosophy in one image. The container lifecycle is something your tests should carry without thinking about, not something they should wrestle with. You declare what you need, you write your assertions, you close the &lt;code&gt;use&lt;/code&gt; block, and Pago handles the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let's start: How it works
&lt;/h2&gt;

&lt;p&gt;The core API is a &lt;code&gt;use&lt;/code&gt; block. You describe a container with a builder, you start it with &lt;code&gt;with_container&lt;/code&gt;, and it is gone when the block ends. Automatic cleanup, even if your test process crashes, because a linked guard process is watching in the background.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import testcontainer
import testcontainer/container
import testcontainer/port
import testcontainer/wait

pub fn redis_test() {
  use redis &amp;lt;- testcontainer.with_container(
    container.new("redis:7-alpine")
    |&amp;gt; container.expose_port(port.tcp(6379))
    |&amp;gt; container.wait_for(wait.log("Ready to accept connections")),
  )

  let assert Ok(host_port) = container.host_port(redis, port.tcp(6379))
  // connect to 127.0.0.1:host_port
  Ok(Nil)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth pointing out here. Ports are typed: &lt;code&gt;port.tcp/1&lt;/code&gt; and &lt;code&gt;port.udp/1&lt;/code&gt; are different things. The builder is opaque, so you cannot pass a half-constructed &lt;code&gt;ContainerSpec&lt;/code&gt; somewhere it does not belong. Wait strategies are composable. Errors always carry context.&lt;/p&gt;

&lt;p&gt;The library also talks to Docker over the Unix socket directly via &lt;code&gt;gen_tcp&lt;/code&gt;, no HTTP client pulled in as a dependency. Fast. Lightweight. No surprises in your dep tree.&lt;/p&gt;




&lt;h2&gt;
  
  
  Formulas: the part that actually excited me
&lt;/h2&gt;

&lt;p&gt;Here is where things get interesting.&lt;/p&gt;

&lt;p&gt;A raw container gives you a running process and a mapped port. If you are starting Postgres, that means you get back a host and a port number. And then every single test file has to reassemble a connection URL from scratch: host, port, user, password, database, driver prefix. It is noise. It is copy-pasted noise.&lt;/p&gt;

&lt;p&gt;The solution is what I call a Formula.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub opaque type Formula(output)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;Formula(output)&lt;/code&gt; is two things combined: a &lt;code&gt;ContainerSpec&lt;/code&gt; that describes how to start the container, and an extraction function that takes the running &lt;code&gt;Container&lt;/code&gt; and produces a typed output. When you call &lt;code&gt;with_formula&lt;/code&gt;, the library starts the container, runs the extraction, and hands your test body a fully typed service record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import testcontainer
import testcontainer_formulas/postgres

pub fn user_repository_test() {
  use pg &amp;lt;- testcontainer.with_formula(
    postgres.new()
    |&amp;gt; postgres.with_database("myapp_test")
    |&amp;gt; postgres.with_password("secret")
    |&amp;gt; postgres.formula(),
  )

  // pg is a PostgresContainer
  // pg.connection_url, pg.host, pg.port: all there, already built
  Ok(Nil)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;output&lt;/code&gt; type parameter is the interesting bit. &lt;code&gt;Formula(PostgresContainer)&lt;/code&gt; and &lt;code&gt;Formula(RedisContainer)&lt;/code&gt; are different types. The compiler knows. You cannot accidentally pass one where the other is expected. No runtime surprise, no casting, just the type system doing its job.&lt;/p&gt;

&lt;p&gt;The extraction function is a small contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn new(
  spec: container.ContainerSpec,
  extract: fn(container.Container) -&amp;gt; Result(output, error.Error),
) -&amp;gt; Formula(output)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a running container. You return your typed output or an error. That is the entire surface of the abstraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why formulas live in a separate package
&lt;/h2&gt;

&lt;p&gt;The core library defines &lt;code&gt;Formula(output)&lt;/code&gt; and &lt;code&gt;with_formula&lt;/code&gt;. That is all it knows. It has no idea what Postgres is.&lt;/p&gt;

&lt;p&gt;The actual formulas live in &lt;a href="https://hex.pm/packages/testcontainer_formulas" rel="noopener noreferrer"&gt;&lt;code&gt;testcontainer_formulas&lt;/code&gt;&lt;/a&gt;, a companion package that ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;testcontainer_formulas/postgres&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;testcontainer_formulas/redis&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;testcontainer_formulas/mysql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;testcontainer_formulas/rabbitmq&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;testcontainer_formulas/mongo&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The separation is intentional, and the reason is not packaging convenience. It is about the community.&lt;/p&gt;

&lt;p&gt;The formula surface is small enough that anyone can write one. The pattern is clear. You define a config type, a builder pipeline, an extraction function, and you are done. That means if you need Kafka, Elasticsearch, LocalStack, a very specific internal service, something completely bizarre, you can open a PR against &lt;code&gt;testcontainer_formulas&lt;/code&gt; and it just fits. No changes needed to the core. No coordination required with me. The abstraction holds.&lt;/p&gt;

&lt;p&gt;I want &lt;code&gt;testcontainer_formulas&lt;/code&gt; to grow into a community-curated archive. Official formulas, opinionated formulas, weird ones. The contract is small enough that this is realistic. If you have an idea for one, open a PR. That is what the repository is for.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Formula Builder
&lt;/h2&gt;

&lt;p&gt;There is a third piece: &lt;a href="https://github.com/lupodevelop/testcontainer_formulas_builder" rel="noopener noreferrer"&gt;&lt;code&gt;testcontainer_formulas_builder&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is a block-based visual tool. You add blocks for the services you need (Postgres, Redis, MySQL, RabbitMQ, MongoDB, or a custom module), configure each one, set up the shared network if needed, and it generates the Gleam code as you go. You can copy it directly with &lt;code&gt;y&lt;/code&gt; in Vim navigation mode... So, &lt;strong&gt;YES&lt;/strong&gt; There is also a vim navigation mode, &lt;em&gt;because of course&lt;/em&gt; there is.&lt;/p&gt;

&lt;p&gt;There is a live version you can try right now, tagged as experimental: &lt;a href="https://lupodevelop.github.io/testcontainer_formulas_builder/" rel="noopener noreferrer"&gt;lupodevelop.github.io/testcontainer_formulas_builder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is aimed at people who want to get a working formula snippet without reading all the docs first, or who want to understand the structure before writing one from scratch. Either way it lowers the barrier for contribution, which is the whole point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-container setups
&lt;/h2&gt;

&lt;p&gt;For integration tests that need multiple services talking to each other, there are networks and stacks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use net &amp;lt;- testcontainer.with_stack(
  testcontainer.stack("app-test-net", fn(n) { Ok(n) }),
)
use pg &amp;lt;- testcontainer.with_formula(
  postgres.new()
  |&amp;gt; postgres.on_network(net)
  |&amp;gt; postgres.formula(),
)
// pg and any other container share the same Docker network
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stacks own the network lifecycle. Each container inside still gets its own guard process, so teardown is ordered and nothing leaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gleam add testcontainer
gleam add testcontainer_formulas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Core library: &lt;a href="https://hex.pm/packages/testcontainer" rel="noopener noreferrer"&gt;hex.pm/packages/testcontainer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Formulas: &lt;a href="https://hex.pm/packages/testcontainer_formulas" rel="noopener noreferrer"&gt;hex.pm/packages/testcontainer_formulas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Formula Builder: &lt;a href="https://github.com/lupodevelop/testcontainer_formulas_builder" rel="noopener noreferrer"&gt;github.com/lupodevelop/testcontainer_formulas_builder&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://hexdocs.pm/testcontainer/" rel="noopener noreferrer"&gt;hexdocs.pm/testcontainer&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you write a formula for something not in the package yet, open a PR. That is exactly what &lt;code&gt;testcontainer_formulas&lt;/code&gt; is there for.&lt;/p&gt;

&lt;p&gt;Pago is watching. Pago approves. &lt;/p&gt;

&lt;p&gt;So if you want to contribute, my &lt;a href="https://ko-fi.com/lupodevelop" rel="noopener noreferrer"&gt;Ko-fi&lt;/a&gt; is waiting for you.&lt;/p&gt;

</description>
      <category>gleam</category>
      <category>docker</category>
      <category>containers</category>
      <category>elixir</category>
    </item>
    <item>
      <title>Strings are hard. Unicode is harder.</title>
      <dc:creator>Daniele</dc:creator>
      <pubDate>Tue, 23 Dec 2025 11:36:52 +0000</pubDate>
      <link>https://dev.to/lupodevelop/strings-are-hard-unicode-is-harder-49cp</link>
      <guid>https://dev.to/lupodevelop/strings-are-hard-unicode-is-harder-49cp</guid>
      <description>&lt;p&gt;Strings should be simple.&lt;/p&gt;

&lt;p&gt;You call &lt;code&gt;.length()&lt;/code&gt;, slice off a few characters, maybe lowercase something, generate a URL-friendly slug. Basic stuff. The kind of thing you write once and never think about again.&lt;/p&gt;

&lt;p&gt;That's what I thought, too, until I shipped a feature where users could create custom display names, and someone named "José 👨‍💻" signed up.&lt;/p&gt;

&lt;p&gt;Everything broke in the weirdest ways. The slug generator mangled his name into gibberish. The "initial" function pulled the wrong letter. The character counter said his name was 11 characters long when it was obviously six.&lt;/p&gt;

&lt;p&gt;That's when I realized: strings aren't simple. They're one of those deceptively hard problems in programming that everyone assumes is solved, until it quietly corrupts your data in production.&lt;/p&gt;

&lt;p&gt;This post is about that realization, about why I ended up writing &lt;code&gt;str&lt;/code&gt; (a Unicode-aware string utility library for Gleam), and why I chose Gleam, specifically, to do it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The moment strings stopped being simple
&lt;/h2&gt;

&lt;p&gt;Let me show you the bug that started all of this.&lt;/p&gt;

&lt;p&gt;How many characters are in this string?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"👨‍👩‍👧‍👦"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your gut reaction is "one," congratulations, you're thinking like a user. That's a family emoji. One symbol. One visual unit.&lt;/p&gt;

&lt;p&gt;But ask your programming language, and you'll probably get a completely different answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript&lt;/strong&gt;: 11 (code units in UTF-16)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python 3&lt;/strong&gt;: 7 (Unicode code points)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most string libraries&lt;/strong&gt;: somewhere in between&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them say "1."&lt;/p&gt;

&lt;p&gt;And that's where the bugs start.&lt;/p&gt;

&lt;p&gt;Because what users &lt;em&gt;see&lt;/em&gt; isn't what the computer &lt;em&gt;counts&lt;/em&gt;. Modern text isn't just a flat array of characters. It's layered, contextual, and full of invisible combining marks, zero-width joiners, and modifier sequences.&lt;/p&gt;

&lt;p&gt;Here's another example that broke things for me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"🇮🇹"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the Italian flag emoji. Visually: one symbol. Internally: &lt;strong&gt;two regional indicator code points&lt;/strong&gt; (&lt;code&gt;🇮&lt;/code&gt; + &lt;code&gt;🇹&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Slice it in the wrong place, and you don't get "half a flag", you get broken, invalid Unicode. Worse, you might not even notice until a user files a bug report three months later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Unicode bugs are silent, and that's what makes them dangerous
&lt;/h2&gt;

&lt;p&gt;The worst thing about Unicode bugs? They rarely crash your program.&lt;/p&gt;

&lt;p&gt;They just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;truncate user input at weird positions&lt;/li&gt;
&lt;li&gt;generate slugs that look like &lt;code&gt;jos-null-programmer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;miscount lengths, breaking layout or validation&lt;/li&gt;
&lt;li&gt;corrupt data silently, everything &lt;em&gt;looks&lt;/em&gt; fine until you inspect it closely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I once built a "smart truncation" function that would cut text to exactly 50 characters and append &lt;code&gt;...&lt;/code&gt; if needed. It worked perfectly in tests—because I only tested with ASCII. Then someone entered &lt;code&gt;"Hello 👨‍👩‍👧‍👦 World"&lt;/code&gt;, and my function split the family emoji right in half. The result? Broken output that rendered as &lt;code&gt;�&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's when I started paying attention to how string libraries actually handle Unicode—or more accurately, how they &lt;em&gt;don't&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And I realized: most libraries treat "Unicode support" as an afterthought, a checkbox to tick. They'll handle ASCII perfectly, maybe support UTF-8 encoding, and call it a day.&lt;/p&gt;

&lt;p&gt;But real Unicode correctness? Grapheme cluster boundaries? Handling emoji sequences, combining diacritics, regional indicators?&lt;/p&gt;

&lt;p&gt;That stuff gets ignored until it causes problems.&lt;/p&gt;




&lt;h2&gt;
  
  
  The question that became a library
&lt;/h2&gt;

&lt;p&gt;At some point, frustrated with patching Unicode bugs in multiple projects, I asked myself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What would a string utility library look like if Unicode correctness was the &lt;em&gt;starting point&lt;/em&gt;, not an afterthought?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question eventually became &lt;code&gt;str&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But before I talk about the library itself, I need to explain why I wrote it in &lt;strong&gt;Gleam&lt;/strong&gt;, of all languages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Gleam?
&lt;/h2&gt;

&lt;p&gt;I didn't choose Gleam because it's popular. It's not, yet. The ecosystem is small, there's no massive community, and you can't just Google your way through every problem.&lt;/p&gt;

&lt;p&gt;I chose it &lt;em&gt;because&lt;/em&gt; of that.&lt;/p&gt;

&lt;p&gt;Coming from Elixir, where there are ten competing libraries for everything and mature tooling for every use case, Gleam felt refreshingly &lt;em&gt;constrained&lt;/em&gt;. And constraints are great when you want to focus on &lt;strong&gt;API design&lt;/strong&gt; and &lt;strong&gt;correctness&lt;/strong&gt; instead of fighting with dependency hell.&lt;/p&gt;

&lt;p&gt;Gleam also gave me things I really care about when working with something as finicky as Unicode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strong static typing&lt;/strong&gt;: no runtime surprises&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immutability by default&lt;/strong&gt;: no accidental state mutations breaking edge cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;null&lt;/code&gt;&lt;/strong&gt;: every function either succeeds or returns a &lt;code&gt;Result&lt;/code&gt;/&lt;code&gt;Option&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit error handling&lt;/strong&gt;: if something can fail, you &lt;em&gt;have&lt;/em&gt; to handle it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple, readable syntax&lt;/strong&gt;: I wanted the library to be approachable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But honestly, the biggest reason I chose Gleam?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because writing this library would force me to understand the language deeply.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And it did. I learned Gleam by building something real, something that had to work correctly, not just compile.&lt;/p&gt;




&lt;h2&gt;
  
  
  What &lt;code&gt;str&lt;/code&gt; is (and what it isn't)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;str&lt;/code&gt; isn't trying to be a "kitchen sink" library. It's not competing with massive string utilities that do everything from regex to natural language processing.&lt;/p&gt;

&lt;p&gt;The goals are deliberately modest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unicode-aware operations&lt;/strong&gt;: If a function works with "characters," it should mean &lt;em&gt;graphemes&lt;/em&gt;, not bytes or code points&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictable behavior&lt;/strong&gt;: Functions do what they say and nothing more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small, composable API&lt;/strong&gt;: Each function has one job&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correctness over cleverness&lt;/strong&gt;: I'd rather be boring and reliable than fast and wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly: &lt;strong&gt;if it says it operates on characters, it means graphemes&lt;/strong&gt;, the things users actually see.&lt;/p&gt;




&lt;h2&gt;
  
  
  A quick example of what goes wrong without grapheme awareness
&lt;/h2&gt;

&lt;p&gt;Consider this string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"👩🏽‍🚀"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visually: one astronaut emoji with medium skin tone.&lt;/p&gt;

&lt;p&gt;Internally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;👩 Base emoji (woman)&lt;/li&gt;
&lt;li&gt;🏽 Skin tone modifier&lt;/li&gt;
&lt;li&gt;ZWJ (zero-width joiner)&lt;/li&gt;
&lt;li&gt;🚀 Rocket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four separate Unicode code points, but the user sees &lt;em&gt;one symbol&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Now imagine trying to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Truncate it to "1 character"&lt;/li&gt;
&lt;li&gt;Capitalize it&lt;/li&gt;
&lt;li&gt;Reverse it&lt;/li&gt;
&lt;li&gt;Slice it in half&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your string library doesn't understand grapheme clusters, &lt;strong&gt;all of these operations produce broken output.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's why &lt;code&gt;str&lt;/code&gt; consistently operates at the grapheme level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import str/core

core.length("👩🏽‍🚀")  // → 1 (not 4, not 7)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One visible symbol. One count. That's the mental model users have, and the library should match it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The surprising complexity of case conversion
&lt;/h2&gt;

&lt;p&gt;Case conversion is one of those things everyone assumes is trivial.&lt;/p&gt;

&lt;p&gt;Until it isn't.&lt;/p&gt;

&lt;p&gt;Quick: what should this do?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;to_snake_case("Hello World")  // Easy: "hello_world"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, that's straightforward. Now try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;to_snake_case("Crème Brûlée 🍮")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What do you want here? Probably:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"creme_brulee"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Not:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"crème_brûlée"&lt;/code&gt; (preserving accents breaks URLs)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"cr_me_br_l_e"&lt;/code&gt; (dropped characters entirely)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"creme_brulee_"&lt;/code&gt; (weird trailing underscore from emoji)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;code&gt;str&lt;/code&gt;, the behavior is explicit and predictable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import str/extra

extra.to_snake_case("Crème Brûlée 🍮")
// → "creme_brulee"

extra.to_kebab_case("Crème Brûlée 🍮")
// → "creme-brulee"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It normalizes Unicode, folds accents to ASCII when needed, strips non-alphanumeric symbols, and produces stable, URL-safe output.&lt;/p&gt;

&lt;p&gt;No surprises. No edge cases breaking in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Slugs, URLs, and why slugification is harder than it looks
&lt;/h2&gt;

&lt;p&gt;Slugification is the perfect example of "simple functionality" that hides massive complexity underneath.&lt;/p&gt;

&lt;p&gt;People don't write URLs. They write &lt;em&gt;text&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Caffè ☕ e codice 👨‍💻"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you want to turn that into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"caffe-e-codice"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, that involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unicode normalization (NFD decomposition)&lt;/li&gt;
&lt;li&gt;ASCII transliteration (è → e, ä → a)&lt;/li&gt;
&lt;li&gt;Symbol removal (☕, 👨‍💻 stripped)&lt;/li&gt;
&lt;li&gt;Whitespace collapsing and replacement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the API stays simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;extra.slugify("Caffè ☕ e codice 👨‍💻")
// → "caffe-e-codice"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boring. Predictable. &lt;strong&gt;Correct.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the goal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Emoji-friendly by design
&lt;/h2&gt;

&lt;p&gt;I paid special attention to emoji handling, because emoji are where bad string handling is most &lt;em&gt;visible&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;core.length("👨‍👩‍👧‍👦")  // → 1 (not 7, not 11)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That family emoji is &lt;strong&gt;one visual symbol&lt;/strong&gt;, and &lt;code&gt;str&lt;/code&gt; treats it as one grapheme cluster.&lt;/p&gt;

&lt;p&gt;Same with operations like truncation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;core.truncate("Hello 👨‍👩‍👧‍👦 World", 8, "...")
// → "Hello 👨‍👩‍👧‍👦..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The emoji stays intact. No broken Unicode. No weird � characters.&lt;/p&gt;

&lt;p&gt;This might seem like a small thing, but it's the difference between software that &lt;em&gt;feels&lt;/em&gt; right and software that subtly breaks for international users.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design choices (and trade-offs)
&lt;/h2&gt;

&lt;p&gt;Some decisions in &lt;code&gt;str&lt;/code&gt; are opinionated.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I avoided depending directly on OTP's Unicode helpers, &lt;code&gt;str&lt;/code&gt; is pure Gleam&lt;/li&gt;
&lt;li&gt;Internal character tables are used for ASCII folding instead of runtime lookups&lt;/li&gt;
&lt;li&gt;Correctness is favored over clever micro-optimizations&lt;/li&gt;
&lt;li&gt;Fewer functions, but clearer ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No magic&lt;/strong&gt;: every function does exactly what it says&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No surprising behavior&lt;/strong&gt;: edge cases are handled explicitly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hidden costs&lt;/strong&gt;: you know what's happening under the hood&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Text bugs are &lt;em&gt;expensive&lt;/em&gt;. They corrupt data. They break user trust. They're hard to reproduce and harder to fix.&lt;/p&gt;

&lt;p&gt;I'd rather &lt;code&gt;str&lt;/code&gt; be boring and correct than clever and unpredictable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who is &lt;code&gt;str&lt;/code&gt; for?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;str&lt;/code&gt; is useful if you're:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building web backends and need URL-safe slugs&lt;/li&gt;
&lt;li&gt;Generating identifiers or usernames from user input&lt;/li&gt;
&lt;li&gt;Writing CLIs that display or format text&lt;/li&gt;
&lt;li&gt;Cleaning or transforming international text&lt;/li&gt;
&lt;li&gt;Working with emoji-heavy user-generated content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's &lt;em&gt;especially&lt;/em&gt; useful if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Care about Unicode correctness&lt;/li&gt;
&lt;li&gt;Want predictable, well-tested behavior&lt;/li&gt;
&lt;li&gt;Are exploring Gleam and want a solid utility library&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Writing &lt;code&gt;str&lt;/code&gt; was also about the ecosystem
&lt;/h2&gt;

&lt;p&gt;One reason I wanted to do this in Gleam was to &lt;strong&gt;contribute something tangible&lt;/strong&gt; to the ecosystem.&lt;/p&gt;

&lt;p&gt;In a smaller community:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every library matters&lt;/li&gt;
&lt;li&gt;Design decisions are more visible&lt;/li&gt;
&lt;li&gt;Feedback loops are shorter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writing &lt;code&gt;str&lt;/code&gt; wasn't just about solving string bugs, it was about learning Gleam by building something real, something people could actually use.&lt;/p&gt;

&lt;p&gt;And honestly? It was fun. The type system caught so many edge cases. The immutability forced me to think carefully about state. The explicit error handling made the API clearer.&lt;/p&gt;

&lt;p&gt;Gleam made me a better programmer while I was building this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Strings are hard.&lt;br&gt;&lt;br&gt;
Unicode is harder.&lt;br&gt;&lt;br&gt;
Emoji make everything visible.&lt;/p&gt;

&lt;p&gt;Gleam gave me the right balance of constraints and expressiveness to tackle this problem thoughtfully, without letting me cut corners.&lt;/p&gt;

&lt;p&gt;If you're exploring Gleam, or if you've ever been bitten by subtle Unicode bugs, I hope &lt;code&gt;str&lt;/code&gt; is useful to you.&lt;/p&gt;

&lt;p&gt;Feedback, issues, and contributions are always welcome.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;a href="https://hex.pm/packages/str" rel="noopener noreferrer"&gt;&lt;code&gt;str&lt;/code&gt; on Hex&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧠 &lt;a href="https://github.com/lupodevelop/str" rel="noopener noreferrer"&gt;Source on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✨ &lt;a href="https://gleam.run/" rel="noopener noreferrer"&gt;Gleam language&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. — If you've ever wondered why &lt;code&gt;"café".length !== "café".length&lt;/code&gt; in some languages, welcome to Unicode normalization hell. I have stories.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gleam</category>
      <category>opensource</category>
      <category>programming</category>
      <category>elixir</category>
    </item>
  </channel>
</rss>
