<?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: Thomas Couderc</title>
    <description>The latest articles on DEV Community by Thomas Couderc (@elzinko).</description>
    <link>https://dev.to/elzinko</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%2F3977615%2Fb2f8a546-3961-4f7c-9918-7fd24fe69bc7.jpeg</url>
      <title>DEV Community: Thomas Couderc</title>
      <link>https://dev.to/elzinko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elzinko"/>
    <language>en</language>
    <item>
      <title>I built a Kotlin DSL because I couldn't read my own regex anymore</title>
      <dc:creator>Thomas Couderc</dc:creator>
      <pubDate>Wed, 10 Jun 2026 11:39:48 +0000</pubDate>
      <link>https://dev.to/elzinko/i-built-a-kotlin-dsl-because-i-couldnt-read-my-own-regex-anymore-p0j</link>
      <guid>https://dev.to/elzinko/i-built-a-kotlin-dsl-because-i-couldnt-read-my-own-regex-anymore-p0j</guid>
      <description>&lt;h2&gt;
  
  
  The git blame moment
&lt;/h2&gt;

&lt;p&gt;You know that moment.&lt;/p&gt;

&lt;p&gt;You open a file. You see a regex. Eighty characters of pure line noise.&lt;br&gt;
You mutter &lt;em&gt;"who wrote this garbage?"&lt;/em&gt; and run &lt;code&gt;git blame&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was you. Three months ago.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the &lt;em&gt;readable&lt;/em&gt; kind. Multiply by five on real legacy code, then sprinkle in a few nested groups, a couple of lookaheads, and a comment that says &lt;code&gt;// don't touch this&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article is about &lt;strong&gt;kexpresso&lt;/strong&gt;, a small Kotlin library I built to stop hating my past self. It's open source, on Maven Central, and Kotlin Multiplatform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reveal
&lt;/h2&gt;

&lt;p&gt;Same regex, written with kexpresso:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kexpresso&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;startOfText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;endOfText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DSL reads top to bottom like English. The compiler catches typos that strings never would. And it compiles to a standard &lt;code&gt;kotlin.text.Regex&lt;/code&gt; at construction time — I measured &lt;strong&gt;0 % overhead at match time&lt;/strong&gt; vs raw &lt;code&gt;Regex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But the DSL is just the entry point. The features that earned the library its place in my own projects are the ones that come &lt;em&gt;after&lt;/em&gt; you have a pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;describe()&lt;/code&gt; — your past self's apology
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Kexpresso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;// → "one or more characters from [a-zA-Z0-9._%+-], then '@', then one or&lt;/span&gt;
&lt;span class="c1"&gt;//    more characters from [a-zA-Z0-9.-], then '.', then 2 or more letters"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Kexpresso.from(regex)&lt;/code&gt; reverse-engineers a raw regex into the DSL by parsing it into the same AST the builder produces. &lt;code&gt;describe()&lt;/code&gt; walks that AST and returns a plain explanation.&lt;/p&gt;

&lt;p&gt;Drop it into your tooling, your code reviews, your logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;analyze()&lt;/code&gt; — catch ReDoS before prod does
&lt;/h2&gt;

&lt;p&gt;Regular expressions can be &lt;strong&gt;catastrophically slow&lt;/strong&gt;. A pattern like &lt;code&gt;(a+)+b&lt;/code&gt; against &lt;code&gt;"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"&lt;/code&gt; will hang for seconds, sometimes minutes. This is the ReDoS family of vulnerabilities — and it has taken down major sites (Cloudflare in 2019, Stack Overflow in 2016, plenty of others).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kexpresso&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;capture&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;oneOrMore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'a'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;oneOrMore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;// → vulnerable: nested quantifier `(a+)+` exhibits exponential backtracking on input "a…ac"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The analyzer walks the AST looking for known dangerous shapes: nested quantifiers, ambiguous alternations, overlapping repeats. It runs in milliseconds and you can wire it into your tests or a pre-commit hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;examples(n)&lt;/code&gt; — actual test data, not your tired imagination
&lt;/h2&gt;

&lt;p&gt;Testing a regex by hand is the worst. You always pick the same five inputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kexpresso&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;exactly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;exactly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;examples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;// → ["847-19", "302-55", "613-88", "104-72", "975-33"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;examples(count, seed)&lt;/code&gt; walks the AST and generates strings that satisfy &lt;code&gt;matches()&lt;/code&gt;. Deterministic per seed — your tests stay reproducible. It supports &lt;code&gt;Sequence&lt;/code&gt;, &lt;code&gt;Literal&lt;/code&gt;, primitive tokens, quantifiers, groups, and alternations with guaranteed matches; it is best-effort on lookarounds, backreferences, and &lt;code&gt;Raw&lt;/code&gt; nodes (it still produces strings, just without a match guarantee).&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain helpers, because nobody enjoys writing IPv6
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;webhook&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kexpresso&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;startOfText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;endOfText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kexpresso&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;ipv4&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;whitespace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;exactly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sixteen helpers shipped: &lt;code&gt;email()&lt;/code&gt;, &lt;code&gt;url()&lt;/code&gt;, &lt;code&gt;ipv4()&lt;/code&gt;, &lt;code&gt;ipv6()&lt;/code&gt;, &lt;code&gt;uuid()&lt;/code&gt;, &lt;code&gt;macAddress()&lt;/code&gt;, &lt;code&gt;base64()&lt;/code&gt;, &lt;code&gt;jwt()&lt;/code&gt;, &lt;code&gt;iso8601Date()&lt;/code&gt;, and friends. All composable inside the DSL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kotlin Multiplatform: write once, regex everywhere
&lt;/h2&gt;

&lt;p&gt;The full DSL lives in &lt;code&gt;commonMain&lt;/code&gt;. Published targets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JVM&lt;/td&gt;
&lt;td&gt;published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS (IR, Node.js)&lt;/td&gt;
&lt;td&gt;published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasm (&lt;code&gt;wasmJs&lt;/code&gt;, Node.js)&lt;/td&gt;
&lt;td&gt;published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native — Linux, Windows&lt;/td&gt;
&lt;td&gt;published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native — macOS, iOS&lt;/td&gt;
&lt;td&gt;published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The portable test suite passes identically on every target. JVM-only regex constructs (&lt;code&gt;\A&lt;/code&gt;, &lt;code&gt;\z&lt;/code&gt;, atomic groups, possessive quantifiers) remain JVM-only and stay tested in &lt;code&gt;jvmTest&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Gradle Kotlin DSL&lt;/span&gt;
&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.elzinko:kexpresso:0.8.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's on Maven Central — no token, no extra repository configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest 0.x disclaimer
&lt;/h2&gt;

&lt;p&gt;This is 0.8. I'm aiming for 1.0 once the API has soaked with external users. If you try it and the API rubs you the wrong way, &lt;strong&gt;please open an issue&lt;/strong&gt; — that's exactly the signal I need before committing to SemVer stability.&lt;/p&gt;

&lt;p&gt;The roadmap, the contribution guide, and the publishing setup all live under &lt;code&gt;docs/&lt;/code&gt;. Pull requests welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/elzinko/kexpresso" rel="noopener noreferrer"&gt;github.com/elzinko/kexpresso&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Maven Central: &lt;a href="https://central.sonatype.com/artifact/io.github.elzinko/kexpresso" rel="noopener noreferrer"&gt;central.sonatype.com/artifact/io.github.elzinko/kexpresso&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API docs (Dokka): &lt;a href="https://elzinko.github.io/kexpresso/" rel="noopener noreferrer"&gt;elzinko.github.io/kexpresso&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;30-second guided tour: clone the repo and run &lt;code&gt;./gradlew :samples:run&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If kexpresso saves you time, a ⭐ on the repo means the world. ☕&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>regex</category>
      <category>opensource</category>
      <category>kmp</category>
    </item>
  </channel>
</rss>
