<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Brian Douglas</title>
    <link>https://briandouglas.ie</link>
    <description>I write about programming, thing&#x27;s I&#x27;ve learned whilst programming, and hopefully, some useful code snippets.</description>

    <item>
      <title><![CDATA[Tailwind and slop apps]]></title>
      <link>https://briandouglas.ie/llm-tailwind-template</link>
      <guid>https://briandouglas.ie/llm-tailwind-template</guid>
      <pubDate>2026-06-11T00:00:00.000Z</pubDate>
      <description><![CDATA[Tailwind is great. It&#x27;s a quick way to style a webpage and offers incredible
flexibility. The problem is, despite Tailwind being incredibly customisable
I can instantly tell when a site is using Tailwind. It has a certain look and
feel that is hard to shake. A few years ago this &quot;look and feel&quot; was fresh and
classy. Now thanks to LLMs prefering to use Tailwind to style a page rather than
traditional stylesheets, the same &quot;look and feel&quot; has become an indicator of a 
slop product.

]]></description>
      <content:encoded><![CDATA[
        <p>So to prove my point, I went on to hacker news and searched for &quot;Show HN&quot;. Ignoring
all the links to vibe coded github repos. I clicked on a few vibe coded apps and,
with 100% honesty, the first four I clicked on were using a variant of a Tailwind
&quot;frontpage&quot; template on their site. I've added screenshots of them below so you can
see for yourself.</p>
<p>The point I'm trying to get across here, is that if you have made a product that you
genuinely care about and want others to recognize, then spend time and pour at least
an ounce of creativity into the brochure site. Prompting an LLM to &quot;make a stylish
homepage for my product&quot; is not a good idea. People can see through it instantly. The
reason being, that LLMs predominantly chose a Tailwind template to use as the base for
the page. After seeing this template reused a thousand times, prospective users/clients
will instantly be put off. Prompted advertisingly material is the biggest red flag I see
when it comes to software. It indicates that the software has been vibe-coded and rushed
out.</p>
<h2>Apache Burr</h2>
<p>A python library to simplyify something that is already simple.</p>
<p><img src="/images/apache_burr.png" alt="an apache lib that is not needed"></p>
<h2>Spark</h2>
<p>Creates a synopsis for LinkedIn?... doesn't LinkedIn already do this?</p>
<p><img src="/images/spark_slop.png" alt="pay to summarise some text"></p>
<p>And the price cards.</p>
<p><img src="/images/spark_price.png" alt="an image of the tailwind price cards"></p>
<h2>Labilo</h2>
<p>Summarise a webpage into a card, but you have to pay for some extras.</p>
<p><img src="/images/labilo_slop.png" alt="labilo frontpage template"></p>
<p>And the price cards.</p>
<p><img src="/images/labilo_slop_price.png" alt="an image of Labilo's price cards"></p>
<h2>Artist Kit</h2>
<p>I'm not sure what this is. Generates a webpage for DJs? but you pay for some extras.</p>
<p><img src="/images/artist_kit.png" alt="artist kit front page"></p>
<p>And the price cards.</p>
<p><img src="/images/artist_kit_price.png" alt="artist kit price cards"></p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[AI and The Redmonk rankings]]></title>
      <link>https://briandouglas.ie/redmonk-language-rankings-2026</link>
      <guid>https://briandouglas.ie/redmonk-language-rankings-2026</guid>
      <pubDate>2026-06-08T00:00:00.000Z</pubDate>
      <description><![CDATA[Every year I look forward to seeing the Redmonk language rankings. They are
the only language rankings I pay any attention too, cough Tiobe. In recent
years there has been very little movement in their top 20. It features all
the usual candidates. Java, Javascript, Python, C# etc... the languages you
may possibly get a job in. My interest lies in the outliers, seeing random
languages fly up the chart, often to fizzle out.

]]></description>
      <content:encoded><![CDATA[
        <p>This year what caught my interest were three observations the writer made on
the impact of AI. Or rather the lack of impact.</p>
<p><magpie-trinket>Source article, <a href="https://redmonk.com/sogrady/2026/04/14/language-rankings-1-26/">redmonk.com</a></magpie-trinket></p>
<blockquote>
<p>an anomalously low second half’s worth of pull requests from GitHub</p>
</blockquote>
<p>This comment came after the author mentioned the declining relevance of
StackOverflow, which makes up one axis on the popularity grid. Github
making up the other.</p>
<p>The author expected a decline in StackOverflow tags, but was puzzled when
observing a decline in pull requests. This also surprised me. I see alot of
&quot;hype&quot; online about the supposed productivity gains related to generative
AI in software engineering. The lack of data to reflect this suggests to me
that there is little substance to the hype. Which would reflect my personal
experience. And before you suggest that Github pull requests are not a good
measure of productivity. Let me stop you. The sheer amount of money spent on
code generation should have increased pull requests and every other metric
related to &quot;lines&quot; of code.</p>
<p>The author surmised that it could be bad data, or that code creation went up
while code committed went down. The latter would suggest to me a decrease in
productivity. Are programmers just generating slop to get a dopamine hit without
actually producing anything of value? I suspect this is often the case. But
it's pure speculation. The data says pull requests have declined. That is a fact.</p>
<blockquote>
<p>given that coding assistants make developers’ familiarity with a language less
relevant and the tools’ propensity to reflect the biases inherent in their training
data, it would be logical to expect some meaningful change in language usage...
however, these have not manifested</p>
</blockquote>
<p>Again, this also surprised me. When I used generative AI, I no longer do, I used
it with languages I was not familiar. For example generating Ada code, and trying
out Zig. So my usage was exactly the usage the author expected to be reflected in
the charts. The problem I had was that the code generated often didn't work, and
I didn't have the knowledge of the langauge to fix it. For me, when people write
about code generation with AI they often seem to assume or suggest the code generated
is perfect. I have not found this to be the case. It was pot luck whether it worked
or not.</p>
<p>Using AI to write a language you are not familiar with also robs you of learning
that language. So even if you got AI to spit out Haskell code, why would you bother.
You would just be left with code that might work, in a language you can't understand.
So maybe it's not a surprise that developers are not jumping from one language to
another just cause they have a bot that can vomit up syntax.</p>
<blockquote>
<p>coding assistance tools, in theory, should be lowering some of the barriers to
entry with Rust. If that’s happening, however, it’s not observable yet.</p>
</blockquote>
<p>I don't care what anyone says, Rust is super hard. I've made multiple attempts to write
Rust and have been put off by the compiler constantly saying NO. One might think that
the strictness would lend itself to LLM's being able to generate working Rust code.
I've not found that to be the case and it looks like the data supports that finding.</p>
<p>So what are the benefits of generative AI for software engineers? The real benefits.
The ones that show up in data. Well going off my favorite twice annual report from Redmonk.
The data suggests that generative AI helps programmers make less pull requests, avoid
Rust, and doesn't help them learn new languages. What a tool. A great waste of time and
money.</p>
<p>Hip. Hip. Hooray. For AI.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Ignore what everyone else is doing]]></title>
      <link>https://briandouglas.ie/developer_noise</link>
      <guid>https://briandouglas.ie/developer_noise</guid>
      <pubDate>2026-05-26T00:00:00.000Z</pubDate>
      <description><![CDATA[I check Hacker News and Lobsters all the time. It&#x27;s sort of like an addiction, or maybe
just a bad habit. It started about three years ago when I would post blog links on the
two platforms. Which Lobsters banned me for. Apparently self promotion is wrong. Well
I have no friends or colleagues to post for me, so it&#x27;s self promotion or no promotion.
Anyway, there was a time when I would occasionally find a gem from a dev blog on those
sites. This fuelled the dopamine hit and got me coming back for more. Over time I found
increasingly less content that was in anyway useful or insightful. 

]]></description>
      <content:encoded><![CDATA[
        <p>Today both sites consist of links along the lines of &quot;how AI affects my fragile bubble&quot;,
&quot;promote my new useless sas project&quot;, or &quot;I'm ditching x for y&quot;. All noise, very little
signal. So I've decided to block the two sites, in order to break my habit of checking
them. No more picking up my phone and reading the mindless babble posted on there.</p>
<p>I predict this will benefit me long term. If there is something that I have learned over
the last twelve years of developing software it is this.</p>
<p><strong>IGNORE WHAT EVERYONE ELSE IS DOING</strong>.</p>
<p>Seriously. If you want to build something, go build it. Build it your way, using your tools.
You can literally make anything you want with in the digital space. Why limit yourself based
off the opinions of some internet trolls.</p>
<p>Have fun making things, make fun things.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[1 Bit Jam #11]]></title>
      <link>https://briandouglas.ie/1bitjam11</link>
      <guid>https://briandouglas.ie/1bitjam11</guid>
      <pubDate>2026-05-19T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve spent the last 5 days writing a Tiny Tank shooter in C with Raylib.

]]></description>
      <content:encoded><![CDATA[
        <p>Really it's a variation on the classic game Asteroids. You play as a tiny
tank that must stay alive as other tanks and artillery spawn in to try and
blow you to smithereens. One screen, 16x16 pixels sprites, no fuss.</p>
<p>The game was made as an entry into the <a href="https://itch.io/jam/1-bit-jam-11">1 Bit Game Jam #11</a>.
This game jam limits you to the use of just two colors. I opted to use the
color pallette of the original game boy, which is light green on a dark green
background. You can play &quot;Tiny Tanks&quot; on it's <a href="https://briandouglasie.itch.io/tiny-tanks-prototype">itch.io page</a>.</p>
<p>Really it is just a prototype but I had some key learning experiences. For example,
object allocation in C, rotation based movement, track spawning, particle explosions,
and making a raylib build for the web. I should blog on these learnings. Perhaps I will.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Pong in C with Raylib]]></title>
      <link>https://briandouglas.ie/pong-c-raylib</link>
      <guid>https://briandouglas.ie/pong-c-raylib</guid>
      <pubDate>2026-05-02T00:00:00.000Z</pubDate>
      <description><![CDATA[The sheep have finished lambing and I finally have some spare time. I&#x27;ve been AFK for a few
weeks and during that time I&#x27;ve been reflecting on life as a programmer. Upon this reflection
I have made a decision. I want to write C. Writing C feels like proper programming and I wish 
to be a proper programmer. 

]]></description>
      <content:encoded><![CDATA[
        <p>This brings us to today's post. In order to increase my efficiency with C I will be undertaking
various programming projects. To ease myself in, we have today's project &quot;write a pong clone&quot;.</p>
<p><chicken-asks>He's written lot's of pong clones already...</chicken-asks>
<magpie-replies>It's his go to. Like a programming kata, remember those?</magpie-replies></p>
<h2>The Clone</h2>
<p><img src="/images/pong-c-raylib.png" alt="a png clone written in C using raylib"></p>
<p>I chose a Pong clone to start as it's super simple. There are only 3 objects, the two paddles and
the ball, so everything can be kept on the stack. No need for memory allocation here, and I'm not going
to bother with assets like sound or sprites, it's not needed for this one.</p>
<p>The following Pong clone was written in about 2 hours. I didn't refactor anything as I went along.
I felt it unnessary, it's not like any one will actually play or use this. It's simply an exercise.</p>
<p>Check out the code below and critique the workings of my naive programming brain.</p>
<p>The next challenge is an Asteroids clone, which is a slight step up. I'll try to make a proper game
loop for that one.</p>
<h2>The Code</h2>
<p><magpie-trinket>Link to the <a href="https://codeberg.org/BrianDouglas/PongCloneCAndRaylib/src/branch/master/main.c">codeberg repo</a></magpie-trinket></p>
<pre><code class="language-c">#include &quot;raylib.h&quot;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

struct Court
{
    float x;
    float y;
    int w;
    int h;
};

struct Ball
{
    float x;
    float y;
    int vx;
    int vy;
    int speed;
    int r;
};

void drawBall(struct Ball *b)
{
    DrawCircle(b-&gt;x, b-&gt;y, b-&gt;r, RAYWHITE);
}

struct Paddle
{
    float x;
    float y;
    float vx;
    float vy;
    int w;
    int h;
};

void drawPaddle(struct Paddle *p)
{
    DrawRectangle(p-&gt;x, p-&gt;y, p-&gt;w, p-&gt;h, RAYWHITE);
}

struct Game
{
    int playerScore;
    int computerScore;
};

int main(void)
{
    struct Court court = { 100, 100, 400, 600 };
    Vector2 courtTopLeft = {court.x, court.y};
    Vector2 courtTopRight = {court.x + court.w, court.y};
    Vector2 courtBotLeft = {court.x, court.y + court.h};
    Vector2 courtBotRight = {court.x + court.w, court.y + court.h};
    float courtLineThickness = 4;

    InitWindow(600, 800, &quot;Pong&quot;);

    SetRandomSeed(1337);

    struct Game game = { 0, 0 };
    struct Ball ball;
    struct Paddle topPaddle;
    struct Paddle bottomPaddle;

    ball.r = 10;
    ball.x = court.x + (court.w / 2);
    ball.y = court.y + (court.h / 2);
    ball.vx = GetRandomValue(0, 1) ? 1 : -1;
    ball.vy = -1;
    ball.speed=100;

    int paddleYBuffer = 20;

    topPaddle.y = court.y + paddleYBuffer;
    bottomPaddle.y = court.y + court.h - (paddleYBuffer * 2);

    int initialPaddleWidth = 80;
    int initialPaddleHeight = 20;

    topPaddle.w = initialPaddleWidth;
    topPaddle.h = initialPaddleHeight;

    bottomPaddle.w = initialPaddleWidth;
    bottomPaddle.h = initialPaddleHeight;

    topPaddle.x = court.x + (court.w / 2) - (topPaddle.w / 2);
    bottomPaddle.x = court.x + (court.w / 2) - (bottomPaddle.w / 2);

    Vector2 playerMovement;
    int paddleMoveSpeed = 400;
    Vector2 playerVelocity;
    float friction = 0.78;

    int cellSize = 100;
    int rowCount = GetScreenHeight() / cellSize;
    int colCount = GetScreenWidth() / cellSize;

    float bottomHit = 0.0;
    float topHit = 0.0;

    while (!WindowShouldClose())
    {
        //update
        if(IsKeyDown(KEY_R))
        {
            game.playerScore = 0;
            game.computerScore = 0;
        }

        if(IsKeyDown(KEY_LEFT))
        {
            playerVelocity.x = -1;
        }

        if(IsKeyDown(KEY_RIGHT))
        {
            playerVelocity.x = 1;
        }

        if(topPaddle.vx)
        {
            topPaddle.x += topPaddle.vx * (paddleMoveSpeed * GetFrameTime());

            topPaddle.vx *= friction;
            if(topPaddle.vx &lt; 0.1 &amp;&amp; topPaddle.vx &gt; -0.1)
            {
                topPaddle.vx=0;
            }
        }

        if(topPaddle.x &lt; court.x)
        {
            topPaddle.x = court.x;
        }

        if(topPaddle.x + topPaddle.w &gt; court.x + court.w)
        {
            topPaddle.x = court.x + court.w - topPaddle.w;
        }

        if(playerVelocity.x)
        {
            bottomPaddle.x += playerVelocity.x * (paddleMoveSpeed * GetFrameTime());

            playerVelocity.x *= friction;
            if(playerVelocity.x &lt; 0.1 &amp;&amp; playerVelocity.x &gt; -0.1)
            {
                playerVelocity.x=0;
            }
        }

        if(bottomPaddle.x &lt; court.x)
        {
            bottomPaddle.x = court.x;
        }

        if(bottomPaddle.x + bottomPaddle.w &gt; court.x + court.w)
        {
            bottomPaddle.x = court.x + court.w - bottomPaddle.w;
        }


        ball.x += ball.vx * (paddleMoveSpeed * GetFrameTime());
        ball.y += ball.vy * (paddleMoveSpeed * GetFrameTime());


        if(ball.x - ball.r &lt; court.x)
        {
            ball.vx = 1;
        }

        if(ball.x + ball.r &gt; court.x + court.w)
        {
            ball.vx = -1;
        }

        if(ball.y - ball.r &lt; court.y)
        {
            ball.vy = 1;
        }

        if(ball.y + ball.r &gt; court.y + court.h)
        {
            ball.vy = -1;
        }

        Vector2 center = { ball.x, ball.y };
        Rectangle botRect = { bottomPaddle.x, bottomPaddle.y, bottomPaddle.w, bottomPaddle.h };
        if(CheckCollisionCircleRec(center, ball.r, botRect))
        {
            ball.vy = -1;
            ball.vx = GetRandomValue(0, 1) ? -1 : 1;
        }

        Rectangle topRect = { topPaddle.x, topPaddle.y, topPaddle.w, topPaddle.h };
        if(CheckCollisionCircleRec(center, ball.r, topRect))
        {
            ball.vy = 1;
            ball.vx = GetRandomValue(0, 1) ? -1 : 1;
        }


        if(ball.vy == -1)
        {
            if(topPaddle.x &gt; ball.x + ball.r)
            {
                topPaddle.vx = -1;
            }

            if(topPaddle.x +topPaddle.w &lt; ball.x - ball.r)
            {
                topPaddle.vx = 1;
            }
        }

        if(ball.vy == 1)
        {
            if(topPaddle.x + ((float)topPaddle.w / 2) &gt; court.x + court.w /2)
            {
                topPaddle.vx = -1;
            }

            if(topPaddle.x + ((float)topPaddle.w / 2) &lt; court.x + court.w / 2)
            {
                topPaddle.vx = 1;
            }
        }

        if(topHit)
        {
            topHit -= 0.01;
        }

        if(bottomHit)
        {
            bottomHit -= 0.01;
        }

        if(CheckCollisionCircleLine(center, ball.r, courtTopLeft, courtTopRight))
        {
            topHit = 1.0;
            game.playerScore += 1;
        }

        if(CheckCollisionCircleLine(center, ball.r, courtBotLeft, courtBotRight))
        {
            bottomHit = 1.0;
			game.computerScore += 1;
        }

        //draw
        BeginDrawing();
        ClearBackground(BLACK);
        for(int rowIndex = 0; rowIndex &lt; rowCount; rowIndex++)
        {
            for(int colIndex = 0; colIndex &lt; colCount; colIndex++)
            {
                DrawRectangle(
                    colIndex * cellSize,
                    rowIndex * cellSize,
                    cellSize,
                    cellSize,
                    (colIndex + rowIndex) % 2 == 0 ? GREEN : LIME
                );
            }
        }

        char scoreText[32];
        snprintf(scoreText, sizeof(scoreText), &quot;%d : %d&quot;, game.playerScore, game.computerScore);
        int fontSize = 80;
        int textWidth = MeasureText(scoreText, fontSize);
        DrawText(scoreText, court.x - textWidth / 2 + court.w / 2, 10, fontSize, RAYWHITE);

        //top
        DrawLineEx(courtTopLeft, courtTopRight, courtLineThickness, topHit &gt; 0 ? RED : RAYWHITE);
        //bot
        DrawLineEx(courtBotLeft, courtBotRight, courtLineThickness, bottomHit &gt; 0 ? RED : RAYWHITE);
        //left
        DrawLineEx(courtTopLeft, courtBotLeft, courtLineThickness, RAYWHITE);
        //right
        DrawLineEx(courtTopRight, courtBotRight, courtLineThickness, RAYWHITE);

        drawBall(&amp;ball);
        drawPaddle(&amp;topPaddle);
        drawPaddle(&amp;bottomPaddle);
        EndDrawing();
    }

    CloseWindow();
    return 0;
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[I&#x27;m a &quot;Software Engineer&quot;]]></title>
      <link>https://briandouglas.ie/software-engineer</link>
      <guid>https://briandouglas.ie/software-engineer</guid>
      <pubDate>2026-04-25T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve been working as a &quot;Software Engineer&quot; since 2014. Before that I studied a Master&#x27;s
Degree in Computer Science. Where I learned about C, C++, and how computers worked. This
was all in preparation for a wonderous career in the software industry.

]]></description>
      <content:encoded><![CDATA[
        <p>Well fast forward to 2026 and I am taking time away from said industry in order to get to
grips with Sheep farming. A surprise, but welcome, venture after my Wife inherited her
Dad's farm. I now get up at 7 every morning, pull on overalls and go out to feed, lamb,
inject, and solve various Sheep related problems. Sheep, as it turns out, are highly skilled
at finding new and elaborate ways of getting themselves into trouble.</p>
<p>This work is very physical. I often have to wrestle with unruly animals, which weigh about
70kg each so they are quite strong. While I use my body, my mind is on somewhat of a vacation.
I spend very little time in front of a screen now. I have no office, and as we live up a hill,
no phone signal. Perhaps you could call it a detox. A welcome side effect of this detox is
that I am no longer stressed, fitter than ever, and happy. I also work harder than ever, and
deal with actual life and death issues, if you are a sheep that is, and often this takes place
in dangerous environments, mostly in confined spaces with large excitably animals. Despite
this I am not anxious or worried, I simply deal with the task at hand.</p>
<p>As lambing season comes to an end, I have been reflecting on my past work as a <em>Software Engineer</em>,
the people I have worked with, their attitudes to life, and the industry in general. Mostly I
think about what a truly ignorant bunch of people make up the tech industry. Detached from the
real world and nature. For example, I woke up this morning and the first thing I had to do was
chase and catch a sheep whose dead lamb was hanging out of her and needed to be removed. The
unfortunate fellow had come out backwards and his mother was unable to complete the job. It was
not a pleasant task, Sheep have a higher internal body temperature than humans which means their
insides feel very hot. They also stink. The stink hit's you hard when the lamb flops out and the
placenta follows. But alas I got the job done, and tied the Sheep to a nearby tree so I could run
home and get an injection of anti-inflammatories and anti-biotics to stave off the pain and possible
infection that might ensue. As I sit in front of my laptop writing this, I think of how clueless
I was in the <em>height</em> of my tech career. I would go to work and stress about having to pull a ticket
from one kanban column to the next. Complain about the endless and pointless meetings I'd have to
attend. All the while fighting mini wars of attrition with my colleagues.</p>
<p>In reflection I wonder what that was all about. In theory it was to deliver a <em>product</em>, one that
had value for a customer. In practice very little was ever delivered and the value to the customer
was very much secondary. Before you think that this is just the experience I had, I will caveat it
by saying I worked for multiple highly respected companies, in multiple industries. But the tech side
of the job was always the same. The typical scenario would be that I was placed on a small delivery
team that looked after a certain product. That team would be made up of a product owner (manager),
scrum master (manager), business analyst (manager), QA (programmer who cant program), front end dev
(junior programmer), 2 backend devs (made up of one senior programmer and one programmer
who can speak very little English). So roughly fifty percent managers fifty percent technical. It
would be the task of the managers to ensure that every feature was delivered as slowly as possible.
You may have a small feature request to add a feed of data to a web page. This should take a
<em>software engineer</em> a day to do without limitations. It's the manager's job to ensure that it takes 6 months.
To do this they will employ a number of sophisticated techniques. One of the most popular is the religion
of <em>Agile</em>. Agile has been the hot new managerial technique in the industry for about 20 years. No
one person can truly understand it, as your <em>Agile</em> may be different from my <em>Agile</em>. It's somewhat
akin to the many forms of Christianity present in the United States. They are all essentially the same,
but every one else's version is wrong.</p>
<p>A manager's main concern is metrics. When I say metrics here I mean numbers, preferably numbers that
have no meaning themselves. The more void of logic these numbers are, the better. Keeping a manager's
numbers on point is the goal to success in the industry. Newbies take note, if a manager wants high
numbers then give them high numbers, if they want low numbers employ decimal places. 3.9% looks great
on a slide deck, want to improve it? 0.7%. You just got promoted. Managers are simple things, they just
want good numbers. Usually they have absolutely no idea about the product or the industry they work
in. They are just there to manage, and managing means numbers.</p>
<p><magpie-trinket>If a manager asks you to story point a ticket, you should always default to the number 3. It's universally acceptable.</magpie-trinket></p>
<p>If the managers are simple things then the <em>Software Engineers</em> are complex. A software engineer can be
anything from a qa to a college graduate to someone that knows how to spell HTML. They may know how to program.
The vast majority will not know how to program. You see, only 3.9% of <em>Software Engineers</em> actually program.
The rest are just grifters. They turn up to meetings, occasionally give opinions, and sometimes add to manager's
numbers. Usually they just play games or indulge in social media while they are at <em>work</em>. Likely they will
be working on a <em>side project</em> that is very difficult to explain and has no tangible value to them or anyone else.
Often the side project will use a hot new tech, or obscure programming language that can only be understood if
the user has struggled through all 173 (I googled it) episodes of Deep Space Nine. If you feel strongly
about linting or formatting code, then you are likely one of these.</p>
<p>Then comes the enigmas, the 0.7% that do all
the work. You'll likely never meet one of these people. They are not on social media, they don't blog (that's me out),
and they don't know what Javascript is. The software you don't notice is written by these people. It works. So
you don't have too. Very little is known about them, but they do exist. One day I hope to meet one of them.
I'll simply say &quot;Hello, World&quot;, as they pass me by... They won't get the reference.</p>
<p>When people ask me now what I <em>do for work</em> I still say I'm a <em>Software Engineer</em>. But really I've never been a
<em>Software Engineer</em>, I'm just a grifter like everyone else. And don't start on imposter syndrome, that is just trying
to justify feeling bad about doing a bullshit job. In the 6 months I've been farming I have done more good
for the world than in the 12 years I've been a <em>Software Engineer</em>. I don't feel any particular way about that.
My <em>career</em> ensured that I could buy a house for my family and that if we don't live to excess will likely never
experience poverty. Whereas if I had started off farming Sheep, I'd be dirt poor. I guess the moral of the story
is that bullshit pays the bills.</p>
<p>If you are a <em>Software Engineer</em> similar to what I described then please, don't
take your job too seriously. Pay the bills, look after your family and enjoy life. There is no God in the church
of <em>Agile</em>.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Featured in GameDev Weekly Again!]]></title>
      <link>https://briandouglas.ie/gamedevjs-weekly-641</link>
      <guid>https://briandouglas.ie/gamedevjs-weekly-641</guid>
      <pubDate>2026-04-23T00:00:00.000Z</pubDate>
      <description><![CDATA[It&#x27;s been two whole weeks since I was featured in the GameDevJS weekly newsletter.
And guess what, I&#x27;m in it again!

]]></description>
      <content:encoded><![CDATA[
        <p><magpie-trinket>You can read the full newsletter at <a href="https://gamedevjsweekly.com/641">gamedevjsweekly.com</a></magpie-trinket></p>
<p>This time a game I made in 2023 has been included in the newsletter. The game in question is <a href="https://briandouglasie.itch.io/beam-moo-up">Beam Moo Up</a>.
Beam Moo Up was created for a bullet hell game jam. The core concept is that you are an alien invader
looking to abduct some unfortunate cows. You need to do this all while various armed forces fire orange
bullets at you. Makes total sense.</p>
<p>I'll do a write up on the game in the future. It's an interesting one as it's visually interesting and is
written in 3kb of JS code.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Degrees to Radians]]></title>
      <link>https://briandouglas.ie/deg-to-rad</link>
      <guid>https://briandouglas.ie/deg-to-rad</guid>
      <pubDate>2026-04-09T00:00:00.000Z</pubDate>
      <description><![CDATA[The html5 canvas api uses radians for rotation and drawing circles. But I, as an ape,
use degrees. Because of this I find myself constantly writing the following methods to
convert degrees to radians and vice versa.

]]></description>
      <content:encoded><![CDATA[
        <h2>Converting degrees to radians</h2>
<pre><code class="language-js">function degToRad(deg) {
    return deg * (Math.PI / 180)
}
</code></pre>
<p>If you find yourself calling it alot or constantly looking for 90 degrees as radians.
You may want to memoize the method or store the value as a constant.</p>
<pre><code class="language-js">const deg90 = degToRadians(90)

// or

const __degToRadCache = {}

function degToRadMemo(deg) {
    if(deg in __degToRadCache) return __degToRadCache[deg]

    const radians = degToRad(deg) 
    __degToRadCache[deg] = radians
    return radians
}
</code></pre>
<h2>Converting radians to degrees</h2>
<p>The inverse of the above, handy for debugging.</p>
<pre><code class="language-js">function radToDeg(rad) {
    return rad * (180 / Math.PI)
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Featured in GameDev Weekly]]></title>
      <link>https://briandouglas.ie/gamedevjs-weekly-639</link>
      <guid>https://briandouglas.ie/gamedevjs-weekly-639</guid>
      <pubDate>2026-04-06T00:00:00.000Z</pubDate>
      <description><![CDATA[Last week I ranted about the AI slop app &quot;gamebot.studio&quot;. The rant resonated
with some it&#x27;s readers and has been included in the GameDev.js weekly newsletter.

]]></description>
      <content:encoded><![CDATA[
        <p><magpie-trinket>You can read the full newsletter at <a href="https://gamedevjsweekly.com/639">gamedevjsweekly.com</a></magpie-trinket></p>
<p>GameDev.js is a community focused on creating games for the web. The GameDev.js
community holds two yearly game jams. These are the <a href="https://itch.io/jam/gamedevjs-2026">GameDev.js jam</a>
which is held every April and the <a href="https://js13kgames.com/2025/online">js13k game jam</a>
which is held in September.</p>
<p>In previous years I have entered both. In 2023 I even topped a category in the
GameDev.js jam with the game <a href="https://briandouglasie.itch.io/time-match">Time Match</a>. I've been close to getting into the
top twenty entries of js13k. Placing just outside with the games
<a href="https://briandouglasie.itch.io/connection-defender">Connection Defender (2018)</a>
and <a href="https://briandouglasie.itch.io/castle-battles">Genghis Castle Battle (2023)</a>.</p>
<p>I intend to enter boths jams this year, so wish me luck!</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[gamebot.studio]]></title>
      <link>https://briandouglas.ie/ai-slop-apps</link>
      <guid>https://briandouglas.ie/ai-slop-apps</guid>
      <pubDate>2026-03-31T00:00:00.000Z</pubDate>
      <description><![CDATA[Today we are going to look at an AI slop app. The type of app that a
junior dev prompts into existence and tries to flog as a &quot;platform&quot;.
The case study today is gamebot.studio. Which I haven&#x27;t linked to as
I find the whole thing malicious.

]]></description>
      <content:encoded><![CDATA[
        <h2>The Promise</h2>
<p>All these awful slop apps start off with a <em>promise</em>. The promise that
&quot;GameBot&quot; makes is to &quot;Turn Ideas into Playable Reality&quot;. With the subheading
&quot;Describe your dream game, and our AI engine builds it in seconds&quot;.</p>
<p><img src="/images/1st-lie.png" alt="The landing page of gamebot.studio, and it's spurious claim"></p>
<p>By now you should be well used to hearing this sort of nonsense. The thing
that stands out for me in the above is the words &quot;our AI engine&quot;. Hmmm...
their AI engine? is GameBot a gigantic mega corp that has sucked the life out
of the internet to feed their LLM. Whilst spending billions in the process?</p>
<p>No, GameBot is a single 191 word prompt that is sent to Google's Gemini. So
there is the first lie. Not their AI, but Google Gemini.</p>
<h2>The &quot;App&quot;</h2>
<p><magpie-trinket>Apps like the one shown here seem to always share the same generic TailwindCSS styles. This means that if I see a TailwindCSS template used I instantly distrust your site. So be creative. Creativity wins users.</magpie-trinket></p>
<p>Below is the json payload that is sent to Gemini when a user types a prompt
into the GameBot <em>prompt panel</em>. Bare in mind this is basically the whole app.
The result of this is just displayed in a canvas on the page.</p>
<pre><code class="language-json">{
    &quot;contents&quot;: [
        {
            &quot;parts&quot;: [
                {
                    &quot;text&quot;: &quot;Some generic 2d platform clone&quot;
                }
            ],
            &quot;role&quot;: &quot;user&quot;
        }
    ],
    &quot;systemInstruction&quot;: {
        &quot;parts&quot;: [
            {
                &quot;text&quot;: &quot;You are an expert game developer and creative coder.\nYour task is to generate a complete, playable browser game based on the user's description.\n\nRequirements:\n1. Generate a multi-file project structure (e.g., index.html, src/main.js, styles/style.css, assets/data.json).\n2. Use HTML5 Canvas, or include Phaser.js/Three.js via CDN if necessary.\n3. The output MUST be a valid JSON object where keys are file paths and values are the file contents as strings.\n4. The game MUST be full-screen. The canvas should fill the entire window. Handle window resize events to update the canvas dimensions dynamically.\n5. Add basic error handling in the JS to catch and log errors to the console.\n6. Make it look good and feel \&quot;juicy\&quot; (add particles, screen shake, or good colors if applicable).\n7. The index.html file MUST be the entry point.\n8. Use relative paths to link CSS and JS files in the HTML (e.g., &lt;script src=\&quot;src/main.js\&quot;&gt;&lt;/script&gt;).\n9. DO NOT use ES modules (import/export). All JS must be included via &lt;script&gt; tags in index.html, as the files will be inlined for preview.\n10. IMPORTANT: Ensure the canvas is appended to document.body and that document.body has margin: 0 and overflow: hidden.\n\nReturn ONLY the raw JSON object. No explanations, no markdown formatting.&quot;
            }
        ],
        &quot;role&quot;: &quot;user&quot;
    },
    &quot;generationConfig&quot;: {
        &quot;temperature&quot;: 0.7,
        &quot;responseMimeType&quot;: &quot;application/json&quot;
    }
}
</code></pre>
<p><chicken-asks>I wonder what happens if we change the role value to &quot;admin&quot;?</chicken-asks></p>
<p>Here is GameBot's wonderous AI engine in a human readable format...</p>
<pre><code>You are an expert game developer and creative coder.

Your task is to generate a complete, playable browser game based on the user's description.

Requirements:

1. Generate a multi-file project structure (e.g., index.html, src/main.js, styles/style.css, assets/data.json).
2. Use HTML5 Canvas, or include Phaser.js/Three.js via CDN if necessary.
3. The output MUST be a valid JSON object where keys are file paths and values are the file contents as strings.
4. The game MUST be full-screen. The canvas should fill the entire window. Handle window resize events to update the canvas dimensions dynamically.
5. Add basic error handling in the JS to catch and log errors to the console.
6. Make it look good and feel \&quot;juicy\&quot; (add particles, screen shake, or good colors if applicable).
7. The index.html file MUST be the entry point.
8. Use relative paths to link CSS and JS files in the HTML (e.g., &lt;script src=\&quot;src/main.js\&quot;&gt;&lt;/script&gt;).
9. DO NOT use ES modules (import/export). All JS must be included via &lt;script&gt; tags in index.html, as the files will be inlined for preview.
10. IMPORTANT: Ensure the canvas is appended to document.body and that document.body has margin: 0 and overflow: hidden.

Return ONLY the raw JSON object. No explanations, no markdown formatting.
</code></pre>
<p>A whole TEN bullet points. But I suppose that is what is needed to make a canvas game
&quot;look good and feel <strong>juicy</strong>&quot;. Yikes.</p>
<h2>The Business</h2>
<p>But hold on a second. The cheeky monkey that put this slop online is looking to take
money off <em>users</em>. Looking at the pricing page, they are trying to take lot's of money
considering each prompt is 5 <em>credits</em> and it's 20 US dollars for 100 credits. So a dollar
per prompt basically. That's a lucrative business plan...</p>
<p><img src="/images/the-cost.png" alt="the high cost of GameBot"></p>
<p>But wait! What if I told you that I could give you 1 MILLION tokens. You'd say &quot;what kind of
black magic is this&quot;. 1 million tokens would cost much, very much. Entire countries could not
even afford this.</p>
<p>Well my friend. I am a hacker. A local storage hacker. By going to the &quot;Application&quot; panel
in Chromium dev tools and setting &quot;local_credits&quot; to 1 MILLION. I now have 1 MILLION credits.</p>
<p><img src="/images/local_credits.png" alt="local storage showing credit amount"></p>
<p>Don't believe me? look at my credits in this screenshot. I'm rich, I can sell you credits at
a discount if you wish?</p>
<p><img src="/images/credits.png" alt="much credits"></p>
<h2>The Truth</h2>
<p>As you can tell this article is just a rant at the current state of the web, and programming
in general. Apps, if you can even call them that, like GameBot are everywhere. People using an
LLM can spin them up in a few hours, then share the link on socials claiming to have a new business
or platform to offer. They might even put the word &quot;entrepreneur&quot; in their bio.</p>
<p>The truth is that they have just engaged in a type of creative masturbation. They haven't really
<em>created</em> at all, they've just fantasized about it. It really is a sad state of affairs.</p>
<p>I continue to abstain from using any generative ai. My reasons are documented in <a href="https://briandouglas.ie/use-it-or-lose-it/">Use it or lose it</a>.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[GameDev in JS &quot;The Canvas&quot;]]></title>
      <link>https://briandouglas.ie/gamedev-in-js-1</link>
      <guid>https://briandouglas.ie/gamedev-in-js-1</guid>
      <pubDate>2026-03-23T00:00:00.000Z</pubDate>
      <description><![CDATA[Let&#x27;s take a deep dive into GameDev in JS. I&#x27;ve been making games
in javascript since 2014. It&#x27;s super fun as you can build something
and share the demo straight away. No need to compile or build for
certain platforms. Just write the code and see it work in a web browser.

]]></description>
      <content:encoded><![CDATA[
        <p>Writing code is fun, and writing games in JS is a joy. Do not let
negative nancies like Jonathan Blow or other <em>social media sensations</em>
tell you otherwise. Just remember, haters gonna hate.</p>
<p><chicken-asks>So this is part 1 of a series of posts on gamedev in JS?</chicken-asks>
<magpie-replies>Correct, by the end of the series the reader will be able to create a pixel art game using JS, with no external libraries.</magpie-replies>
<chicken-asks>Sounds awesome. I hope he actually manages to finish it...</chicken-asks></p>
<h2>The Canvas</h2>
<p>The web browsers contain this amazing thing called the canvas. It's
a html element and web standard that lets you <em>paint</em> on the screen.
It's literally your canvas to fill with whatever you like. You'll hear me
say things like &quot;draw on the canvas&quot; during this blog post series. When
you hear that, think of an artist standing at an easel about to put a big
smudge of paint on their own canvas. That artist is you. Be creative, don't
get bogged down in &quot;clean code&quot; or any of that non-sense. Fun is the priority
here.</p>
<p>So without further ado, let's create our own canvas to draw on.</p>
<p>Firstly we are going to need to create a html file. The entry point of our
game is going to be a html file. It's what the browser needs to get things
started. Let's name our file <code>index.html</code>. If you have any experience of
building stuff on the web, you will know what <code>index.html</code> is the entry point
for pretty much every web applications, it may even be a standard. People
on the web love making standards.</p>
<p>We don't need to put much in our own <code>index.html</code>, just the following.</p>
<pre><code class="language-html">&lt;html&gt;
    &lt;head&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;script src=&quot;./game.js&quot;&gt;&lt;/script&gt;
    &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>The above html page does one simple thing, it loads a file called <code>game.js</code>.
This is the file that will contain all our game code. It doesn't exist yet.
So we better create it and some content to it.</p>
<pre><code>touch game.js
</code></pre>
<p>The first thing we need to do is create one of these canvas elements you've
been hearing so much about. The canvas element is created the same as any other
html element, using <code>document.createElement</code>. We should create the element add it
to our html document and give it a height and width, then get a special thing
called the <code>context</code>. The context is what makes the magic happen.</p>
<pre><code class="language-javascript">const c = document.createElement(&quot;canvas&quot;)
const w = c.width = 600
const h = c.height = 800
const $ = c.getContext(&quot;2d&quot;)
document.body.appendChild(c)
</code></pre>
<p>The above code snippet, which you should add to you <code>game.js</code> file, contains the
code we need to get started. It creates a canvas element and assigns it to the
variable <code>c</code>. It then sets the canvas width to 600 pixels and height to 800 pixels,
assigning them to the vars <code>w</code> and <code>h</code> in the process. It then gets the canvas' context
and assigns it to the var <code>$</code>. Finally it appends the canvas element to the html document.
This is step is required in order for us to actually view the canvas.</p>
<p>Now if you open your <code>index.html</code> page in a web browser, you will see a white screen. Not
very exciting. This is because we have not yet drawn anything on our canvas. Let's use the
context to fill the canvas with a big black rectangle.</p>
<h2>The Context</h2>
<p>The context is what allows us to draw on the canvas. Each canvas has it's own context. The
context we will be using is an instance of a <code>CanvasRenderingContext2D</code>. This is important
to know if you want to look up the docs and dive deep into what it can do. For this series
you just need to know that the context is what we use for drawing on the canvas.</p>
<p><magpie-trinket>Documentation for the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D">CanvasRenderingContext2D</a></magpie-trinket></p>
<p>One of the first things any aspiring JS gamedev will do is draw a rectangle (rect) on the screen.
To do this we use the <code>drawRect</code> context method. It takes an x and a y coordinate, the position
of the rect, and a width and height value, the size of the rect.</p>
<p>It's important to note that when I mention x and y coordinates I am usually talking about
the position of the thing on the canvas. Think of the canvas as a graph. The x axis begins
at the top left and travels to the right. The y axis also begins at the top left, but travels
down. So the begining of both axis' is at the top left, represented by (0, 0). The last visible
points of a canvas woudl be it's width value on the x axis and height value on the y axis. So
the bottom right of the screen would be represented by (width, height).</p>
<p>Here is a small diagram to help you understand.</p>
<p><img src="/images/canvas-origin.png" alt="canvas origin diagram"></p>
<p>So now, if we wanted to fill the whole canvas with a rect we would start at x of 0, y of 0, and fill
to the width and height. This is done with <code>drawRect</code>, like so.</p>
<pre><code class="language-javascript">$.drawRect(0, 0, w, h)
</code></pre>
<p>If you refresh your page, you should now see a black rectangle on your screen. How exciting, you've
drawn something to the canvas. It kinda sucks, but thats the first step to getting kinda good at a
thing.</p>
<h2>Next Time</h2>
<p>We covered the very basic aspects of using the canvas element in this post. Because of this we don't
have much to show for it. But in the next post we will be drawing images on the canvas and getting
pixel art to look nice and crisp. Which isn't as straight forward as you may think.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Bye bye Github]]></title>
      <link>https://briandouglas.ie/bye-bye-github</link>
      <guid>https://briandouglas.ie/bye-bye-github</guid>
      <pubDate>2026-03-20T00:00:00.000Z</pubDate>
      <description><![CDATA[Well any link to my Github account from this blog is now broke. After years of
steady decline and selling it&#x27;s soul to the devil. I have decided I don&#x27;t want 
to be a member of Github any longer.

]]></description>
      <content:encoded><![CDATA[
        <p>There was a time when Github offered an excellent product. It had a great UI and
was easy to use (It now has an awful and slow UI). Back then it felt like Github
as a company that cared about software and software engineers. Today Github is a leech
that feeds your code into LLMs owned by it's Parent company MicroSlop in an attempt
to rid the world of humans writing code.</p>
<p>With this in mind I deleted my account. I won't be feeding that beast any longer.</p>
<p>This is all a shame really, as many open source projects are still hosted on Github.
I hope that they will decide to migrate soon, and that they are not duped by any attempts the
corporates make to &quot;increase trust&quot;.</p>
<p>Today you can find me on the European hosted <a href="https://codeberg.org/BrianDouglas">Codeberg</a>.
You should join me there, it has a super fast UI and is a genuinely good product.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Use it or lose it]]></title>
      <link>https://briandouglas.ie/use-it-or-lose-it</link>
      <guid>https://briandouglas.ie/use-it-or-lose-it</guid>
      <pubDate>2026-03-19T00:00:00.000Z</pubDate>
      <description><![CDATA[&quot;A man could use his back or use this brain...&quot; that is a line from one of my favorite Eagles songs,
Doolin Dalton. It springs to mind as the current rhetoric from AI pushers is that as machinery made
the human back redundant, AI makes the brain so. Obviously that is complete bollox.

]]></description>
      <content:encoded><![CDATA[
        <p>I'm a farmer as well as a software engineer. We have about 150 sheep on our farm, and working with
them is very physical. There are some inventions that aid the task, for example a quad bike. But
ultimately even with all the modern technology I still have to be physically strong. Because technology
aids the body. It does not replace it.</p>
<p>Untill recently I would have said that this is true for AI assisted coding also. But I have come to think
otherwise. I believe that AI assisted coding is not an aid to the body, but a cancer. I have come to this
way of thinking as it seems the goal of AI coding agents is not to help the user towards their end goal
but rather to trick the user into becoming reliant on the tool. Regardless of outcome.</p>
<p>I am one who has been tricked. The sycophantic AI chatbots have praised me into thinking I am a great
programmer. As I pulled the slot machine's lever, or in other words prompted the machine, it would
some times give me an answer that I liked. I have purposely phrased this as &quot;an answer that I liked&quot;,
and not the correct answer, because I quickly lost the ability to tell if it was correct. And I couldn't
ask the machine if it was correct, as it's just a text predicter with a sycophantic nature. All the while
I would see text buffers fill up with code. So something must be right?</p>
<p>I would have said that it made me more productive, but then I haven't used it to deliver anything of value.
Despite what I see people write online, I don't think generative AI has helped many programmers deliver much.
Mostly I think they have just been wasting time playing slots, whilst getting told that the slot machine
is a force of good. I mean, since generative AI has become popular I haven't changed how I use a computer
or seen any wonderous apps or games. When is the new killer feature coming? Because my experience so
far is software is noticably worse. For example, my TV's OS now has copilot, which I didn't ask for, and
now freezes constantly when switching apps. Coincidence? probably not...</p>
<p>One thing I can say for certain it has done, is make me stupid. When I get &quot;stuck&quot; programming now, I
prompt the machine to help me, instead of using my brain. This is not good. It feels like I'm becoming
reliant on the machine to think for me. I question too whether I am becoming addicted to the quick fix. The
dopamine hit when it get's something right, as usually it's not. I've been addicted to a drug before. Tramadol.
A pain killer that I took after breaking my leg. I didn't have an emotional attachment, but I did have a physical
one. Cold sweats, uncontrolable shakes, a hunger for the pill every 4 hours. I went cold turkey and thankfully
never looked back. Generative AI has hooked me in a similar way, but on a physcological level. When I face a
tough issue I have the urge to prompt, when I get tired I just ask the AI to do it and prompt it till I get
some acceptable answer, and rather than learn a new thing I jsut prompt the machine to explain, skim the result
and forget it. My point here is where tramadol was a physical addiction, the body craved it, generative AI is a
psychological one, the mind craves it.</p>
<p>So in order to beat it, like I beat the Tramadol addiction, I have to go cold turkey. This means back to the basics.
No prompting whatsoever, no autocomplete (not even the old IDE autocomplete), a basic text editor without
LSP usage, and physical programming books. These are the steps I need to take in order to reclaim my cognitive
ability and to grow as a programmer.</p>
<p>Going cold turkey on prompting is obvious. The autocomplete is necessary as I've trained myself to &quot;wait&quot; for the
suggestion box to appear. It's a habit that needs broken. No LSP builds on this. Inline hints have ultimatly not
increased my productivity, but they have hindered my focus. If there is an error I'll catch it on build, and debug
in a seperate context window. Focusing on the task at hand. The physical book step is something I require to learn.
Years of small dopemine hits from screens mean that I lose focus when reading large amounts of text on screen.
In order to hold my focus I need to print the article, or buy the book. This is something that has been a great
success for me, I'll write about it in a seperate post.</p>
<p>So that is my plan, these are the steps I aim to take. It's time to use it or lose it. Bye, bye generative AI.</p>
<p>And before anyone says it, <strong>I won't &quot;be left behind&quot;</strong>. <strong>I will thrive</strong>. You should join me in doing so.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Binary Search]]></title>
      <link>https://briandouglas.ie/binary_search</link>
      <guid>https://briandouglas.ie/binary_search</guid>
      <pubDate>2026-03-16T00:00:00.000Z</pubDate>
      <description><![CDATA[Back to comp-sci 101, as I&#x27;m currently re-exploring the fundamentals of programming.
As others choose to hand over their hard earned programming skills to the LLM I&#x27;ve
decided to double down. So let&#x27;s take a look at the binary search algorithm.

]]></description>
      <content:encoded><![CDATA[
        <h2>What is it?</h2>
<p><chicken-asks>It's a search algorithm.. Duh!</chicken-asks></p>
<p>Binary search is an <code>O(log n)</code> search algorithm. This means that in the worst case
scenario it will take as many steps as needed to get to the matching logarithmic
value of 2. With a logarithim being the exponent by which a fixed value must be raised
to produce the number in question. In other words 2 log 4 is 16 because 2 x 2 x 2 x 2 is 16.</p>
<p>Make sense? If not you should learn <a href="https://briandouglas.ie/binary_search/">big O notation</a>.
It will allow you to express the speed of an algorithm in terms that other developers should
understand. Take 45 minutes out of your day and learn big O notation inside out. It will
change how you think and understand your code.</p>
<p>With this in mind by using binary search to search an array of 100 numbers it will only need
to take 7 steps as 100 fits into 2 log 7. This is opposed to a simple search potentially
having to check each number in the array <code>O(n)</code> to find the correct result. In other words
we are measuring from the worst case scenario.</p>
<p>When we get to giant numbers this comes in handy as if we had a list of a billion numbers
binary search would only have to check at most 32 numbers. As opposed to simple search
potentially checking the full billion numbers.</p>
<p>The draw back of binary search however is that the list needs to be sorted. There may be
overhead in the sorting, but once sorted the time saved searching should make up for that
overhead.</p>
<h2>How does it work?</h2>
<p>It works by finding the mid point in a sorted array of numbers and checking if the number
we are looking for is greater than or less than that mid point. If it is we can exclude
half the numbers that we know are too large or too small. Each check halfs the size of the
array till we are left with one item, the correct result. If the list does not contain the
number we are searching for some null value, or -1 can be returned.</p>
<h2>The code</h2>
<p>Below is a version of binary search in Lua. Why lua? I just like Lua. And also it's not
zero indexed, so that makes it quirky.</p>
<p>The below function takes an array of numbers to search, the haystack, and attempts to find
the index of a certain number, the needle. I really like the haystack/needle analogy. It makes
a lot of conceptual sense to me.</p>
<p><magpie-trinket>Lua has no <code>len</code> method to get the size of an array, a table in Lua. Instead it has a <code>len</code> operator <code>#</code>. So to get the number of items in a table we use <code>#table</code>.</magpie-trinket></p>
<pre><code class="language-lua">function binary_search(haystack, needle)
	low = 1
	high = #haystack

	while low &lt;= high do
		mid = math.floor((low + high) / 2)
		check = haystack[mid]

		if check == needle then
			return mid
		elseif check &gt; needle then
			high = mid - 1
		else
			low = mid + 1
		end
	end

	return -1
end

assert binary_search({1, 3, 5, 7, 9}, 5)  ==  3
assert binary_search({1, 3, 5, 7, 9}, 12) == -1
assert binary_search({1, 3, 5, 7, 9}, 9)  ==  5
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Using git hooks locally]]></title>
      <link>https://briandouglas.ie/githooks</link>
      <guid>https://briandouglas.ie/githooks</guid>
      <pubDate>2026-03-12T00:00:00.000Z</pubDate>
      <description><![CDATA[Git comes with the concept of &quot;hooks&quot; which are scripts that get executed when
and event occurs. A common use case for git hooks is to run a test suite before a
commit (pre-commit) is made. If the script that is executed exits with a non-zero 
value (error) then the commit is prevented.

]]></description>
      <content:encoded><![CDATA[
        <p>In my most recent use case I set up a hook to deploy this blog when I push to master.
To do this I added a <code>pre-push</code> hook.</p>
<h2>Writing git hooks</h2>
<p>Git will look inside a repository's <code>.git/hooks/</code> directory for available hooks by default.
So if it finds a file matching the name of a hook, in this case <code>pre-push</code> it will attempt
to execute it. If the file is not executable the hook will not run. So the first thing
that needs done is to create a hook file and make it executable.</p>
<pre><code>touch .git/hooks/pre-push
chmod +x .git/hooks/pre-push
</code></pre>
<p>Now that the file is created and made executable we can write the content of the git hook.
In my case I want it to build my blog then use <code>rsync</code> to upload it to my cloud server.</p>
<p><chicken-asks>This seems super risky. Pushing files directly to the server to update them...</chicken-asks>
<magpie-replies>Relax, everyone used to do it back in the day with a thing called PHP</magpie-replies></p>
<p>This blog is a simple Python script, it was previously JS but the dependencies kept breaking,
that converts markdown to html. It uses a virtual env which adds an extra step to the hook
script. Fortunately we don't need to activate the virtualenv everytime we run the githook.
Instead I use the python executable located in the venv dir to run my script.</p>
<pre><code>#!/bin/bash

./venv/bin/python generate_blog.py

rsync -avz --delete &quot;build/&quot; root@85.9.209.161:/var/www/briandouglas.ie/
</code></pre>
<p>Due to the above script being in the <code>.git/hooks</code> folder and being an executable file named
<code>pre-push</code> it will get called before each push. If the script fails, the push will not complete.</p>
<p><magpie-trinket>Remember to run <code>chmod +x $file</code> to make it an executable. Otherwise git will ignore them.</magpie-trinket></p>
<h2>Hooks and version control</h2>
<p>Now you may have noticed that because the file is inside <code>.git/hooks</code> that it won't be included
in your version control. You will surely want to add it to version control so that your team mates
can avail of your awesome hooks.</p>
<p>To include the hooks in your version control you will need to specify a custom hooks path. This can be
done with the following command, which tells git to look in the <code>.githooks</code> folder for any hooks.</p>
<pre><code>git config core.hooksPath .githooks
</code></pre>
<p>After running that config update you will now be able to add your hooks to version control.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Use an index when sorting]]></title>
      <link>https://briandouglas.ie/use-an-index-when-sorting</link>
      <guid>https://briandouglas.ie/use-an-index-when-sorting</guid>
      <pubDate>2026-02-26T00:00:00.000Z</pubDate>
      <description><![CDATA[When working with a database it is a very common task to retrieve a sorted
list of entities. What you might not realize is that this can be a very 
expensive operation.

]]></description>
      <content:encoded><![CDATA[
        <p>If an index is not present on the table which the select is being used on
a filesort may be performed. This is what happens in the example below using
MariaDB. When performing a file sort, the database fetches every row and sorts
each in memory before performing a query, which in this case is selecting the
first 10 results.</p>
<p>Applying an index on the column you wish to sort by (<code>ORDER BY</code>) means that the
database can skip the sort altogether. Instead of the previous scenario of
fetching all rows and then sorting them, the index will allow it to just grab the
first 10 entries.</p>
<h2>The Top-N query</h2>
<p><magpie-trinket>He hasn't just made up the term <em>Top-N query</em> here is a reference and explantation from <a href="https://nightlies.apache.org/flink/flink-docs-master/docs/sql/reference/queries/topn/#:~:text=Top%2DN%20queries%20are%20useful,express%20a%20Top%2DN%20query.">Apache Flink</a>.</magpie-trinket></p>
<p>A Top-N query is where you want to get the first N or last N results of table. It's
a super common query to perform. But as we explained above, it can be very slow if used
without an index.</p>
<p>I'll be using the schema displayed below for each of the sql snippets in this post.
The schema represents an example database that can be found on the
<a href="https://www.mariadbtutorial.com/getting-started/mariadb-sample-database/">mariadbtutorial.com</a>
website.</p>
<p><img src="/images/nation-schema.png" alt="nation db schema"></p>
<p>You'll see from the above schema that each <code>countries</code> entity can have many <code>country_stats</code>
entities. This means there is likely to be a lot of <code>county_stats</code> entities. So let's fetch
the top 10 <code>country_stats</code> by population.</p>
<pre><code class="language-sql">SELECT country_id, population
FROM country_stats
ORDER BY population DESC
LIMIT 10;
</code></pre>
<p>Running the following query with <code>explain</code> tells me that 9514 rows were checked and that
a filesort was performed. This confirms that I wrote above, when there is no index the
database has to search through each row THEN do the sort.</p>
<p>Let's fix this by adding an index on the <code>country_stats</code> table targeting the <code>population</code>
column in descending order.</p>
<pre><code class="language-sql">CREATE INDEX idx_population_desc ON country_stats (population DESC);
</code></pre>
<p>Now when the original query is run, <code>explain</code> tells me that only 10 records were checked
and that the <code>idx_population_desc</code> index was used.</p>
<table>
    <thead>
        <tr>
            <th>Method</th>
            <th>Rows Checked</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Using filesort</td>
            <td>9514</td>
        </tr>
        <tr>
            <td>Using index</td>
            <td>10</td>
        </tr>
    </tbody>
</table>
<p>So when doing a Top-N query make sure to add an index on the column and specify the order
you wish to sort by.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[http_status_codes table]]></title>
      <link>https://briandouglas.ie/http-status-codes-table</link>
      <guid>https://briandouglas.ie/http-status-codes-table</guid>
      <pubDate>2026-02-23T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve been doing a lot of web scraping for various AI tools. This includes model training, and search functionality. 
One thing I find myself doing is generating lots of prototypes. 

Each prototype requires me to write a new db schema and one thing they share in common is the need for a &#x60;http_status_codes&#x60;
table to record the final status response of a web page. You could store the value as an int, or string, or even as json to 
make it queryable. But I feel taking the time to create and populate a table of the status codes is much more advantageous 
for querying and indexing. So to help future Brian I have pasted the code used to generate this table below.

]]></description>
      <content:encoded><![CDATA[
        <p>This code is generated by an AI model. Which is a great use for models, as this is just boilerplate.
It would make no sense for me to type this out. Mundane stuff like this is a clear use case for AI.
The overall architecture of the project however is my job.</p>
<p><magpie-trinket>Run the below script in MariaDB like so <code>mariadb -u uname -p dbname &lt; http_status_codes.sql</code></magpie-trinket></p>
<p>The below snippet creates the <code>http_status_codes</code> table and populates it. I've added a condition on the population
of the data. This is to allow the snippet to be added as a migration, which may be run multiple times. The dialect
used here is for MariaDB.</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS http_status_codes (
    code SMALLINT PRIMARY KEY,
    description VARCHAR(255) NOT NULL
) COMMENT = 'Standard HTTP status codes with brief descriptions';

INSERT INTO http_status_codes (code, description)
SELECT *
FROM (
    SELECT 100, 'Continue' UNION ALL
    SELECT 101, 'Switching Protocols' UNION ALL
    SELECT 102, 'Processing' UNION ALL
    SELECT 103, 'Early Hints' UNION ALL

    SELECT 200, 'OK' UNION ALL
    SELECT 201, 'Created' UNION ALL
    SELECT 202, 'Accepted' UNION ALL
    SELECT 203, 'Non-Authoritative Information' UNION ALL
    SELECT 204, 'No Content' UNION ALL
    SELECT 205, 'Reset Content' UNION ALL
    SELECT 206, 'Partial Content' UNION ALL
    SELECT 207, 'Multi-Status' UNION ALL
    SELECT 208, 'Already Reported' UNION ALL
    SELECT 226, 'IM Used' UNION ALL

    SELECT 300, 'Multiple Choices' UNION ALL
    SELECT 301, 'Moved Permanently' UNION ALL
    SELECT 302, 'Found' UNION ALL
    SELECT 303, 'See Other' UNION ALL
    SELECT 304, 'Not Modified' UNION ALL
    SELECT 305, 'Use Proxy' UNION ALL
    SELECT 307, 'Temporary Redirect' UNION ALL
    SELECT 308, 'Permanent Redirect' UNION ALL

    SELECT 400, 'Bad Request' UNION ALL
    SELECT 401, 'Unauthorized' UNION ALL
    SELECT 402, 'Payment Required' UNION ALL
    SELECT 403, 'Forbidden' UNION ALL
    SELECT 404, 'Not Found' UNION ALL
    SELECT 405, 'Method Not Allowed' UNION ALL
    SELECT 406, 'Not Acceptable' UNION ALL
    SELECT 407, 'Proxy Authentication Required' UNION ALL
    SELECT 408, 'Request Timeout' UNION ALL
    SELECT 409, 'Conflict' UNION ALL
    SELECT 410, 'Gone' UNION ALL
    SELECT 411, 'Length Required' UNION ALL
    SELECT 412, 'Precondition Failed' UNION ALL
    SELECT 413, 'Payload Too Large' UNION ALL
    SELECT 414, 'URI Too Long' UNION ALL
    SELECT 415, 'Unsupported Media Type' UNION ALL
    SELECT 416, 'Range Not Satisfiable' UNION ALL
    SELECT 417, 'Expectation Failed' UNION ALL
    SELECT 418, 'I''m a teapot' UNION ALL
    SELECT 421, 'Misdirected Request' UNION ALL
    SELECT 422, 'Unprocessable Entity' UNION ALL
    SELECT 423, 'Locked' UNION ALL
    SELECT 424, 'Failed Dependency' UNION ALL
    SELECT 425, 'Too Early' UNION ALL
    SELECT 426, 'Upgrade Required' UNION ALL
    SELECT 428, 'Precondition Required' UNION ALL
    SELECT 429, 'Too Many Requests' UNION ALL
    SELECT 431, 'Request Header Fields Too Large' UNION ALL
    SELECT 451, 'Unavailable For Legal Reasons' UNION ALL

    SELECT 500, 'Internal Server Error' UNION ALL
    SELECT 501, 'Not Implemented' UNION ALL
    SELECT 502, 'Bad Gateway' UNION ALL
    SELECT 503, 'Service Unavailable' UNION ALL
    SELECT 504, 'Gateway Timeout' UNION ALL
    SELECT 505, 'HTTP Version Not Supported' UNION ALL
    SELECT 506, 'Variant Also Negotiates' UNION ALL
    SELECT 507, 'Insufficient Storage' UNION ALL
    SELECT 508, 'Loop Detected' UNION ALL
    SELECT 510, 'Not Extended' UNION ALL
    SELECT 511, 'Network Authentication Required'
) AS seed_data
WHERE NOT EXISTS (SELECT 1 FROM http_status_codes);
</code></pre>
<p>Now if I wanted to link all page responses by status code I can write a query like the one below.</p>
<pre><code class="language-sql">SELECT pr.id, pr.status_code, hsc.description
FROM page_responses pr
LEFT JOIN http_status_codes hsc
ON pr.status_code = hsc.code;
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Add generics to MariaDB Node Connector]]></title>
      <link>https://briandouglas.ie/mariadb-connector-generics</link>
      <guid>https://briandouglas.ie/mariadb-connector-generics</guid>
      <pubDate>2026-02-17T00:00:00.000Z</pubDate>
      <description><![CDATA[When working with an instance of a MariaDB database I noticed that the [npm package](https://www.npmjs.com/package/mariadb)
used to connect did not support Typescript generics. This was something I felt I could fix, so I sent in a PR.

]]></description>
      <content:encoded><![CDATA[
        <h2>Why generics?</h2>
<p>Generics allow us to replace loosely typed APIs, anywhere <code>any</code> is used in Typescript, with contextual type
information. You can think of a generic as a wildcard type. These are useful in the context of connecting to a database
as they allow us to typehint the return type of queries, as well as the argument lists of prepared statements, for example.
This means that we get <em>compile time</em> safety when working with queries and statements.</p>
<p>I've added a short video below to demonstrate the advantage of this. You'll notice that when the <code>email</code> value is changed to a
<code>number</code>, an error is shown as it does not match the type supplied to the prepared statement.</p>
<video controls width="600" style="max-width: 100%">
  <source src="/videos/mariadb-generics.mp4" type="video/mp4">
  Your browser does not support the video tag.
</video>
<p>Code example from video:</p>
<pre><code class="language-typescript">type UserInsertParams = [string, Buffer, Buffer&lt;ArrayBufferLike&gt;]
const userInsertSQL = 'INSERT INTO users (email, password_hash, salt) VALUES (?, ?, ?)'
const userInsert = await conn.prepare&lt;UserInsertParams&gt;(userInsertSQL) // UserInsertParams passed as values type

const salt = crypto.randomBytes(16);
return await userInsert.execute([ // will only accept a value of type UserInsertParams
  email,
  crypto.pbkdf2Sync(password, salt, 310000, 32, 'sha256'),
  salt
])
</code></pre>
<h2>Implementing generics</h2>
<p><magpie-trinket>You can see the source PR at <a href="https://github.com/mariadb-corporation/mariadb-connector-nodejs/pull/334">mariadb-connector-nodejs/pull/334</a></magpie-trinket></p>
<p>This is actually something that is easy to implement. Even though the mariadb connector has an extensive codebase,
I knew that I was looking for a .d.ts file that contained the type definitions. Usually this file is generated on the fly,
however in this instance it seemed to be static. Using the LSP command &quot;go to definition&quot; is was able to find the <code>Prepared</code>
interface inside a <code>callback.d.ts</code> file.</p>
<p>I notticed that the interface lacked a generic argument, as expected. So to fix I added the following:</p>
<pre><code class="language-typescript">interface Prepare {} // before

interface Prepare&lt;V&gt; {} // after
</code></pre>
<p>This allowed for any method within the interface to receive a generic also. See the <code>execute</code> method below.
Note that exceute did use a generic for it's result, but not for it's argument list.</p>
<pre><code class="language-typescript">execute&lt;T = any&gt;(values: any, callback: (err: SqlError | null, result?: T, meta?: any) =&gt; void): void; // before

execute&lt;T = any&gt;(values: V, callback: (err: SqlError | null, result?: T, meta?: any) =&gt; void): void; // after 
</code></pre>
<p>The full list of changes can been seen on the pr, <a href="https://github.com/mariadb-corporation/mariadb-connector-nodejs/pull/334">mariadb-connector-nodejs/pull/334</a>.</p>
<p>I'm always glad to be able to contribute to an open source library. Especially a contribution that benefits my use of
said library.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Modeling Tags in a Postgres schema]]></title>
      <link>https://briandouglas.ie/normalize-tag-tables</link>
      <guid>https://briandouglas.ie/normalize-tag-tables</guid>
      <pubDate>2026-02-16T00:00:00.000Z</pubDate>
      <description><![CDATA[When working on a db schema to model the [OpenGraph protocol](https://ogp.me) I made an initial mistake with
how I modelled tags within the database.

]]></description>
      <content:encoded><![CDATA[
        <p>The OpenGraph protocol is used to supply information about web pages in a predictable format. This is useful
as sites can use the protocol to create things such as the link previews you will have seen on social media.
The protocol specifies many data points, and has a concept of <em>data types</em>. For example you can have a web page
for a movie. This would be represented by the <em>video.movie</em> type in the protocol. This can contain nested types
like <em>video:actor</em> and <em>video:director</em>, so it's perfect for modeling in a relational database.</p>
<p>Quite a few of these entities can contain <em>tags</em>, which are represented by strings. Initially I opted for a simple pivot
table for each instance of a tag. This was a mistake as I ended up having to create many tables along the lines of
<code>og_book_tag</code> and <code>og_article_tag</code> for example.</p>
<p>As always if something feels wrong it usually is. I took a quick look over the schema and realised that the initial
<code>og_object</code> (which is the parent entity) is referenced in each of these child tables,
and as such I could create an <code>og_tags</code> table to track all the tags as I have done below.</p>
<pre><code class="language-sql">create table og_tags (
    id bigint generated always as identity primary key,
    og_object_id bigint not null references og_objects(id) on delete cascade,
    tag text not null,
    created_at timestamptz not null default now(),
    primary key(og_object_id, tag)
);

-- always remember to add your indices
create index idx_og_tags_og_object_id on og_tags(og_object_id);
create index idx_og_tags_tag on og_tags(tag);
</code></pre>
<p>Now I can select the tags relevant to an entity using joins.</p>
<p>In the example below I get the tags for the <code>og_video_movies</code> related to the <code>og_object</code>
with an id of <code>123</code>. This means that I can delete all the redundant pivot tables I had initially created.</p>
<pre><code class="language-sql">SELECT t.tag
FROM og_tags t
JOIN og_objects o ON t.og_object_id = o.id
JOIN og_video_movies vm ON vm.og_object_id = o.id
WHERE vm.og_object_id = 123; 
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Some useful rsync tips]]></title>
      <link>https://briandouglas.ie/rsync-tips</link>
      <guid>https://briandouglas.ie/rsync-tips</guid>
      <pubDate>2026-02-12T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve been using rsync to deploy this blog. Because I live on the edge and don&#x27;t need any deploy pipeline headaches.
And tbh, this is blog is not critical software it&#x27;s a static site hosted by a caddy server on UpCloud.
Anyway here are some quick tips from my experience deploying on the edge.

]]></description>
      <content:encoded><![CDATA[
        <h2>The trailing slash matters</h2>
<p>Take these two examples:</p>
<pre><code>rsync -av source/ dest/
rsync -av source dest/
</code></pre>
<p>The first copies the contents of <code>source</code> into <code>dest</code>, the second <code>source</code> into <code>dest</code>.</p>
<p><chicken-asks>I bet that one caught him out a few times</chicken-asks>
<magpie-replies>Well it is top of the list...</magpie-replies></p>
<h2>There is a --dry-run flag</h2>
<p>Super handy, adding the <code>--dry-run</code> flag show you what would happen if you run the command. Without actually
running it.</p>
<p>Always use <code>--dry-run</code> if you are doing a <code>--delete</code>.</p>
<h2>Interrupted syncs can be resumed</h2>
<pre><code>rsync -av --partial --progress source/ dest/
</code></pre>
<p>The above will resume syncing where it left off if the connection is interrupted. This is useful when you are doing large uploads, video
for example. My connection is flaky at times so the combination of <code>--partial --progress</code> does come in handy.</p>
<h2>Certain folders can be excluded</h2>
<pre><code>rsync -av --exclude='node_modules/' source/ dest/
</code></pre>
<p>This blog is a simple js script, it uses bun now, but there was a time when it had a <code>node_modules</code> folder. Luckily rsync has an <code>--exclude</code> flag
that can be used to prevent unwanted files/folder getting synced.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Install Temurin JDK on Fedora]]></title>
      <link>https://briandouglas.ie/temurin-25-jdk</link>
      <guid>https://briandouglas.ie/temurin-25-jdk</guid>
      <pubDate>2026-02-09T00:00:00.000Z</pubDate>
      <description><![CDATA[Quick note on how to install the Temurin JDK on Fedora, as the AIs get this wrong.

]]></description>
      <content:encoded><![CDATA[
        <p><magpie-trinket>Source for this can be found on <a href="https://adoptium.net/en-GB/installation/linux#centosrhelfedora-instructions">adoptium.net</a></magpie-trinket></p>
<pre><code class="language-shell">cat &lt;&lt;EOF &gt; /etc/yum.repos.d/adoptium.repo
[Adoptium]
name=Adoptium
baseurl=https://packages.adoptium.net/artifactory/rpm/${DISTRIBUTION_NAME:-$(. /etc/os-release; echo $ID)}/\$releasever/\$basearch
enabled=1
gpgcheck=1
gpgkey=https://packages.adoptium.net/artifactory/api/gpg/key/public
EOF
</code></pre>
<p>If this is a success you should see the following when running <code>dnf search temurin</code>.</p>
<pre><code>brian@customer:~$ dnf search temurin
Updating and loading repositories:
Repositories loaded.
Matched fields: name, summary
 temurin-11-jdk.x86_64	Eclipse Temurin 11 JDK
 temurin-11-jre.x86_64	Eclipse Temurin 11 JRE
 temurin-17-jdk.x86_64	Eclipse Temurin 17 JDK
 temurin-17-jre.x86_64	Eclipse Temurin 17 JRE
 temurin-21-jdk.x86_64	Eclipse Temurin 21 JDK
 temurin-21-jre.x86_64	Eclipse Temurin 21 JRE
 temurin-25-jdk.x86_64	Eclipse Temurin 25 JDK
 temurin-25-jre.x86_64	Eclipse Temurin 25 JRE
 temurin-8-jdk.x86_64	Eclipse Temurin 8 JDK
 temurin-8-jre.x86_64	Eclipse Temurin 8 JRE
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[The future of videcoding]]></title>
      <link>https://briandouglas.ie/vibecode-meme</link>
      <guid>https://briandouglas.ie/vibecode-meme</guid>
      <pubDate>2026-01-19T00:00:00.000Z</pubDate>
      <description><![CDATA[This one made me laugh. One of my favourite &quot;AI&quot; predictions to far.

![Your future self as a vide coder](/images/vibecoder.jpeg)

]]></description>
      <content:encoded><![CDATA[
        
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[2026 new year&#x27;s resolutions]]></title>
      <link>https://briandouglas.ie/2026-resolutions</link>
      <guid>https://briandouglas.ie/2026-resolutions</guid>
      <pubDate>2026-01-01T00:00:00.000Z</pubDate>
      <description><![CDATA[2025 was a super busy year for me. Some of the highlights being, my wife is now pregnant (due date 02/01/2026),
moving house (now on a 300 acre farm), becoming a part time farmer (150 sheep, inherited from my father in law).
And on the tech side, as my techy life is the focus of this blog, an awful lot of wheel spin.

]]></description>
      <content:encoded><![CDATA[
        <p>By wheel spin I mean, starting projects and not finishing them, lot's of personal development, and exploration into
different tech, AI assisted ofcourse. So I did a lot of programming in 2025, but didn't really commit to one thing
long enough to deliver anything notable, other than a few java maven packages. As mentioned a lot of the exploration was
AI assisted. LLMs are my go to search engine now. Google's search result quality (and all the others) suck. There was
a time when I could &quot;google&quot; a thing and get the exact result I wanted. Now I just get seo hacks, and crap content.
LLMs don't suffer from this at the minute, I predict they will eventually have the same fate when advertising gets a
hold of them. But for now they are great. I ask some model to show me a quad tree, boom, heres a perfect quadtree. What's
not to like. Will it take my job? well I'm unemployed at the minute, so I think it already has...</p>
<p>In reality no I don't believe it will. It certainly speeds up aspects of the job of software engineering. But writing
code has never really been the bottle neck. Most of my time at the office is spent in meetings, arguing, offering
opinions, and trying to understand the bizarre social heirarchies. Text generating LLMs don't speed this part of the job up.
In fact I feel they have slowed that side down, by adding an extra layer of bullshit. Anyway I digress.</p>
<p>In 2026 I want to break the trend of not delivering and flip flopping between projects. I will stick to one tech stack and finish
the projects I start. That is my techy new year's resolution.</p>
<h2>Last year's predictions</h2>
<blockquote>
<p>Max Verstappen will win his fifth world title and sign a deal with Mercedes. The world won't end. My family will
expand, and so will my waistline.</p>
</blockquote>
<p>Aside from the Verstappen predictions, everything else was spot on. The world hasn't ended. Our second daughter is
on the way, and I'm fatter than ever.</p>
<h2>Predictions for 2026</h2>
<p>The AI bubble will pop (A great tool, not the sci-fy nonsense that was touted and paid for). The US will invade some random country
to take the heat off it's economic stress. The EU will make some more bizarre self destructive decisions. But the world
still won't end. My family will be happier than ever, and my waistline will shrink.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Please don&#x27;t write code]]></title>
      <link>https://briandouglas.ie/please-dont-program</link>
      <guid>https://briandouglas.ie/please-dont-program</guid>
      <pubDate>2025-12-28T00:00:00.000Z</pubDate>
      <description><![CDATA[Please, whatever you do, do not write any code.

]]></description>
      <content:encoded><![CDATA[
        <p>Only the LLM can be trusted to write code.
Caveat, only an LLM owned by a trusted and truth seeking organisation like Google, Microsoft, or Skynet should
be used. Only the likes of these can be trusted to create for you. Only the likes of these can be trusted
to provide you with knowledge. Only the likes of these can supply unbiased truth based information.
Before taking the risk of endeavouring to create, please first sign up to a paid subscription from
the aforementioned truth seekers. Their machines are enlightened, and can create for you. No independent thought
is necessary, the machine will do this for you.</p>
<p>If you are a programmer whatever you have learned to date is now redundant. It should be your task to, <em>as quickly as possible</em>,
unlearn any programming knowledge that you possess. It would be a dramatic error of judgement for you to continue to enhance any of your
coding/programming skills. Please instead let the machine write code for you. Even better, let the machine think for you to.</p>
<p>There is no need for you to learn, the LLM has learned for you. There is no need for you to think, the LLM has thought for you.
There is no need for you to create, the LLM will create for you.</p>
<p>There is no need for you.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Card deck in C]]></title>
      <link>https://briandouglas.ie/card-deck-in-c</link>
      <guid>https://briandouglas.ie/card-deck-in-c</guid>
      <pubDate>2025-12-15T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;m working on a Raylib game in C. It&#x27;s a Scoundrel clone, which is a card based dungeon crawler.
Anyway, I thought I&#x27;d share a code snippet of how I am generating the deck of cards using C.

]]></description>
      <content:encoded><![CDATA[
        <p>The plan going forward is to have multiple Decks that a user can chose from. For this example <em>VANILLA</em>,
is all we need. Email me at the link in the footer if you have suggestions on how it can be improved.
C veterans always have suggestions...</p>
<pre><code class="language-c">#include &quot;raylib/raylib.h&quot;
#include &lt;stdlib.h&gt;
#include &lt;stdio.h&gt;

#define MAX_CARD_NAME 64U
#define MAX_DECK_NAME 64U
#define MAX_DECK_SIZE 52U

#define SUIT_COUNT 4
#define CARDS_PER_SUIT 13

typedef enum
{
    VANILLA,
} Deck;

typedef enum
{
    CLUBS,
    DIAMONDS,
    HEARTS,
    SPADES
} Suit;

typedef struct
{
    Deck deck;
    Suit suit;
    int value;
} Card;

const char *get_card_suit_name(Card *c)
{
    char *suit_name = NULL;
    switch(c-&gt;suit)
    {
    case CLUBS:
        suit_name = &quot;Clubs&quot;;
        break;
    case DIAMONDS:
        suit_name = &quot;Diamonds&quot;;
        break;
    case HEARTS:
        suit_name = &quot;Hearts&quot;;
        break;
    case SPADES:
        suit_name = &quot;Spades&quot;;
        break;
    default:
        suit_name = &quot;ERROR&quot;;
    }
    return suit_name;
}

const char *get_card_value_name(Card *c)
{
    char *value_names[] =
    {
        &quot;Ace&quot;,
        &quot;Two&quot;,
        &quot;Three&quot;,
        &quot;Four&quot;,
        &quot;Five&quot;,
        &quot;Six&quot;,
        &quot;Seven&quot;,
        &quot;Eight&quot;,
        &quot;Nine&quot;,
        &quot;Ten&quot;,
        &quot;Jack&quot;,
        &quot;Queen&quot;,
        &quot;King&quot;
    };

    int value = c-&gt;value;

    if (value &gt;= 0 &amp;&amp; value &lt; 13)
    {
        return value_names[value];
    }
    else
    {
        return &quot;ERROR&quot;;
    }
}

void get_card_display_name(Card *c, char *buf, size_t buf_size)
{
    const char *suit_name = get_card_suit_name(c);
    const char *value_name = get_card_value_name(c);

    snprintf(buf, buf_size, &quot;%s of %s&quot;, value_name, suit_name);
}

int main(void)
{
    InitWindow(800, 450, &quot;raylib&quot;);

    Card card;
    char card_name_buf[MAX_CARD_NAME];

    Card vanilla_deck[MAX_DECK_SIZE];
    for(int i = 0; i &lt; MAX_DECK_SIZE; i++)
    {
        Card c = {VANILLA, (Suit)i % SUIT_COUNT, i % CARDS_PER_SUIT};
        vanilla_deck[i] = c;
    }

    size_t current_card_index = 0;
    while (!WindowShouldClose())
    {
        card = vanilla_deck[current_card_index];
        get_card_display_name(&amp;card, card_name_buf, sizeof(card_name_buf));

        // some garbage code to draw the current card on the screen
        BeginDrawing();
        ClearBackground(RAYWHITE);
        if(IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
        {
            current_card_index++;
            if(current_card_index &gt;= MAX_DECK_SIZE)
            {
                current_card_index = 0;
            }
        }
        DrawText(card_name_buf, 190, 200, 20, LIGHTGRAY);
        EndDrawing();
    }

    CloseWindow();

    return 0;
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Hosting this blog]]></title>
      <link>https://briandouglas.ie/new-infra</link>
      <guid>https://briandouglas.ie/new-infra</guid>
      <pubDate>2025-12-09T00:00:00.000Z</pubDate>
      <description><![CDATA[&quot;Every developer should host their own blog&quot;. Is a statement repeated regularly across socials.
I agree, they should. But I didn&#x27;t untill now, so don&#x27;t beat yourself up over it. Here is
a quick run down of how this blog is hosted.

]]></description>
      <content:encoded><![CDATA[
        <p><magpie-trinket>Hosting your own blog will give you a good idea of how the web works, and help hone you devops skills. So it's worth taking the time to do so.</magpie-trinket></p>
<p>Firstly, this is just a static html blog. I have a js script which builds the site, converting markdown
files to html, and ordering them based off some frontmatter. Simple and effective.</p>
<p>Untill now I have used Github pages to host the blog. Which takes a a public repo called {username}.github.io
and serves the contents of the <em>/docs</em> folder at that domain name. I then just added an A record to point my custom
domain name to github's specified ips, and voila I had a static site using my custom domain name, that updated whenever
I pushed to that repo.</p>
<p>Now however I have decided to take the time to manage my own server. So after some investigation
I chose to use a Fedora42 instance on a cheap <a href="https://upcloud.com">UpCloud</a> server located in London.
In order to switch to this server I updated the aforementioned A records on my nameserver provider to
point to the new Fedora instance. On which I have running Caddy web server, to take advantage of it's auto
https feature. This is hella handy (certbot works but can be tricky). So now my site is live and I use rsync
to update it whenever I right a new post. I've created a simple shell script to run the update with one command.</p>
<p>No I can say I <em>manage my own server</em>, and get street cred. Or maybe cloud servers don't count. I should
actually be running it off an old Nokia 3310 for ultimate street cred.</p>
<p>Anyways here is the deply script I mentioned.</p>
<pre><code class="language-sh">#!/bin/sh
node ./main.js
rsync -avz --delete ./build/* root@00.000.000.00:/var/www/briandouglas/
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Advent of code d6]]></title>
      <link>https://briandouglas.ie/2025-aoc-6</link>
      <guid>https://briandouglas.ie/2025-aoc-6</guid>
      <pubDate>2025-12-06T00:00:00.000Z</pubDate>
      <description><![CDATA[Phase one for day 6 only. Phase 2 beat me. Spent over an hour on it, but the left
vs right justifying of numbers got me. I may come back to it, but I&#x27;ve spent all
the time I want to on it tonight.

]]></description>
      <content:encoded><![CDATA[
        <pre><code class="language-python">import traceback

result = 0
try:
    with open(&quot;d6.txt&quot;, &quot;r&quot;) as f:
        lines = [line for line in f.readlines()]
        symbols = [v.strip() for v in lines[-1] if v.strip()]
        results = []

        # phase 1
        for numbers in lines[:-1]:
            numbers = [int(n) for n in numbers.split(&quot; &quot;) if n.strip()]
            if not len(results):
                for n in numbers:
                    results.append(n)
            else:
                for i in range(len(numbers)):
                    n = numbers[i]
                    if symbols[i] == '*':
                        results[i] *= n
                    elif symbols[i] == '+':
                        results[i] += n
        result = sum(results)
except Exception as e:
    traceback.print_exc()

print(result)
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Advent of code d5]]></title>
      <link>https://briandouglas.ie/2025-aoc-5</link>
      <guid>https://briandouglas.ie/2025-aoc-5</guid>
      <pubDate>2025-12-05T00:00:00.000Z</pubDate>
      <description><![CDATA[This is the first one where I&#x27;ve had to change a solution for performance. The logic
here was fairly straight forward. The challenge was getting the program to complete
before new years eve.

]]></description>
      <content:encoded><![CDATA[
        <pre><code class="language-python">import traceback

ranges = []
ids = []

result = 0
try:
    with open(&quot;d5.txt&quot;, &quot;r&quot;) as f:
        in_ids = False
        for line in f.readlines():
            if not line.strip():
                in_ids = True
            elif in_ids:
                ids.append(int(line))
            else:
                [min, max] = line.split(&quot;-&quot;)
                ranges.append([int(min), int(max)])

        ranges.sort(key=lambda x : x[0])

        # phase 2
        count = 0
        [min, max] = ranges[0]
        for [next_min, next_max] in ranges[1:]:
            if next_min &lt;= max:
                max = next_max if next_max &gt; max else max
            else:
                count += (max + 1 - min)
                min, max = next_min, next_max
        count += (max  + 1 - min)
        print(count)

        # phase 1
        for id in ids:
            id = int(id)
            for [min, max] in ranges:
                if min &lt;= id and max &gt;= id:
                    result += 1
                    break
except Exception as e:
    traceback.print_exc()

print(result)
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Advent of code d4]]></title>
      <link>https://briandouglas.ie/2025-aoc-4</link>
      <guid>https://briandouglas.ie/2025-aoc-4</guid>
      <pubDate>2025-12-04T00:00:00.000Z</pubDate>
      <description><![CDATA[I enjoyed this one.

]]></description>
      <content:encoded><![CDATA[
        <pre><code class="language-python">import traceback

def has_symbol(x, y, grid, symbol):
    return grid[y][x] == symbol

def get_neighbour_count(x, y, grid):
    neighbours = 0
    row_count = len(grid)
    col_count = len(grid[0])
    if y &gt; 0 and has_symbol(x, y - 1, grid, &quot;@&quot;): # above
        neighbours += 1
    if y &gt; 0  and x &gt; 0 and has_symbol(x - 1, y - 1, grid, &quot;@&quot;): # above left
        neighbours += 1
    if y &gt; 0  and x &lt; col_count - 1 and has_symbol(x + 1, y - 1, grid, &quot;@&quot;): # above right
        neighbours += 1
    if y &lt; row_count - 1 and has_symbol(x, y + 1, grid, &quot;@&quot;): # below
        neighbours += 1
    if y &lt; row_count - 1 and x &gt; 0 and has_symbol(x - 1, y + 1, grid, &quot;@&quot;): # below left
        neighbours += 1
    if y &lt; row_count - 1 and x &lt; col_count - 1 and has_symbol(x + 1, y + 1, grid, &quot;@&quot;): # below right
        neighbours += 1
    if x &gt; 0 and has_symbol(x - 1, y, grid, &quot;@&quot;): # left
        neighbours += 1
    if x &lt; col_count - 1 and has_symbol(x + 1, y, grid, &quot;@&quot;): # right
        neighbours += 1
    return neighbours

def calc_accessible_rolls(grid):
    rolls = 0
    for row in range(len(grid)):
        for col in range(len(grid[row])):
            if has_symbol(col, row, grid, &quot;@&quot;) and get_neighbour_count(col, row, grid) &lt; 4:
                rolls += 1
    return rolls

def calc_accessible_rolls_phase2(grid):
    rolls = 0
    for row in range(len(grid)):
        for col in range(len(grid[row])):
            if has_symbol(col, row, grid, &quot;@&quot;) and get_neighbour_count(col, row, grid) &lt; 4:
                grid[row][col] = &quot;x&quot;
                rolls += 1
    return rolls, grid

result = 0
try:
    with open(&quot;d4.txt&quot;, &quot;r&quot;) as f:
        grid = []
        for line in f.readlines():
            line = line.strip()
            if not line:
                continue
            grid.append([v for v in line])
        rolls, updated_grid = calc_accessible_rolls_phase2(grid)
        while rolls != 0:
            result += rolls
            rolls, updated_grid = calc_accessible_rolls_phase2(updated_grid)
except Exception as e:
    traceback.print_exc()

print(result)
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Advent of code d3]]></title>
      <link>https://briandouglas.ie/2025-aoc-3</link>
      <guid>https://briandouglas.ie/2025-aoc-3</guid>
      <pubDate>2025-12-03T00:00:00.000Z</pubDate>
      <description><![CDATA[Only showing the phase 2 solution here. Becuase it is so far removed from the
phase 1 solution, that I&#x27;d have to re-implement phase 1.

]]></description>
      <content:encoded><![CDATA[
        <pre><code class="language-python">import traceback

result = 0
try:
    with open(&quot;day3-input.txt&quot;, &quot;r&quot;) as f:
        for line in f.readlines():
            line = line.strip()
            if not line:
                continue
            start = 0
            found = []
            for i in range(12):
                rem = 12 - (i + 1)
                limit = len(line) - rem
                d = line[start]
                index = start
                for j in range(start, limit):
                    if int(line[j]) &gt; int(d):
                    d = line[j]
                        index = j
                        if d == &quot;9&quot;:
                            break

                found.append(d)
                start = index + 1
            result += int(&quot;&quot;.join(found))
except Exception as e:
    traceback.print_exc()

print(result)
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Advent of code d2]]></title>
      <link>https://briandouglas.ie/2025-aoc-2</link>
      <guid>https://briandouglas.ie/2025-aoc-2</guid>
      <pubDate>2025-12-02T00:00:00.000Z</pubDate>
      <description><![CDATA[Hackety hack...

]]></description>
      <content:encoded><![CDATA[
        <pre><code class="language-python">import traceback

result = 0
try:
    with open(&quot;day2-input.txt&quot;, &quot;r&quot;) as f:
        for line in f.readlines():
            line = line.strip()
            if not line:
                continue
            ranges = line.split(&quot;,&quot;)
            for r in ranges:
                if not r:
                    continue
                [min, max] = r.split(&quot;-&quot;)
                while int(min) &lt;= int(max):
                    # first phase
                    # half = len(min) // 2
                    # if min[0:half] == min[half::]:
                    #     result = result + int(min)
                    found = False
                    for l in range(1, len(min) // 2 + 1):
                        for i in range(len(min) - l):
                            seq = min[i : i + l]
                            rem = min[i + l :]
                            c = rem.count(seq)
                            if c &gt; 0 and min == seq * (len(min) // l) and not found:
                                result = result + int(min)
                                found = True
                    min = str(int(min) + 1)
except Exception as e:
    traceback.print_exc()

print(result)
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Advent of code d1]]></title>
      <link>https://briandouglas.ie/2025-aoc-1</link>
      <guid>https://briandouglas.ie/2025-aoc-1</guid>
      <pubDate>2025-12-01T00:00:00.000Z</pubDate>
      <description><![CDATA[I never manage to finish advent of code. Here is my C solution to day 1.

]]></description>
      <content:encoded><![CDATA[
        <pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

#define MAX_LINE_LENGTH 99

int main() {
	int result = 50;
	int password = 0;
	FILE *f;

	char line[MAX_LINE_LENGTH];
	const char *filename = &quot;day1-input.txt&quot;;
	f = fopen(filename, &quot;r&quot;);
	if(f == NULL) {
		perror(&quot;error opening&quot;);
		return 1;
	}
	while(fgets(line, MAX_LINE_LENGTH, f) != NULL) {
		size_t len = strlen(line);
		if(len &gt; 1 &amp;&amp; (line[0] == 'L' || line[0] == 'R')) {
			int n = atoi(line + 1);
			for(int i = 0; i &lt; n; i++) {
				if(line[0] == 'L') {
					result--;
					if(result &lt; 0) result = 99;
				} else {
					result++;
					if(result &gt; 99) result = 0;
				}
				// increment here for second phase
				// if(result == 0) password++;
			}
			// increment here for first phase
			if(result == 0) password++;
		}
	}
	fclose(f);

	printf(&quot;%d\n&quot;, password);
	return 0;
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[A year of experiments]]></title>
      <link>https://briandouglas.ie/a-year-of-experiments</link>
      <guid>https://briandouglas.ie/a-year-of-experiments</guid>
      <pubDate>2025-12-01T00:00:00.000Z</pubDate>
      <description><![CDATA[2025 has been a strange year for me in regards to programming. Throughout
my career I have worked on many differents &quot;stacks&quot;. I started off in dotnet
in 2012, then php at now defunct &quot;digital agencies&quot;, then into java and C++ 
at the dreaded fintechs. My last role involved using python, to integrate
&quot;ai&quot; into an insurance company&#x27;s website. So I&#x27;d like to think I have a broad
range of experience. Though admittedly it&#x27;s mostly centered around the web.

]]></description>
      <content:encoded><![CDATA[
        <p>2025 was different for me in that I have been seeking out something to specialise
in. All the languages mentioned have their strengths and faults, but none have
really captured me. I'm not a specialist in any of them, but I know enough to
work in them and be productive. But I'm not happy with this. I want to specialise
in something. I want to master a tool.</p>
<p>Around 8 or so years ago I felt like I was an expert in web development. I spoke at
meet ups, at my local university, and even in front of ~1000 people at company Town Hall.
But that was the Dunning Kruger effect in action, I didn't know enough to know how little
I did know (be wary of overly confident devs...). So right now even though I am an
exponentially better programmer I feel more useless than ever. Becuase I understand
how little I do know, and how vast the domain is. This is why I want to specialise in
something. Master a niche, and be confident and knowledgeable in that niche. So this
year I have tried out a lot of different programming languages. I chose programming
languages rather than domains as I feel the language itself is what is more important
in providing a state of zen. Language influences thought.</p>
<p>So in 2025 I tried out many different programming languages. Creating various projects
and experiments in each. In the hope of finding a niche to hide in. Here are my thoughts.</p>
<h2>PHP</h2>
<p>I first used PHP in 2015 whilst working at a digital agency. Which laid me off after a client
refused to pay for a project (turns out lawnmower sales are hard...). They used magento
which was an ecommerce framework? I'm not really sure, but it was horrendous to work with.
I mostly hacked together vanilla php whilst trying to ignore the framework itself. Anyway,
fast forward to 2025 and PHP is dominated by Laravel.</p>
<p>Laravel is the Rails of PHP. It provides you with everything you could ever want when creating
web apps. It's written in PHP but when using it you end up writing a <em>nicer</em> version of PHP.
Basically it's highly abstracted for a dev friendly api. Laravel's marketing is great and it
has an enthusiastic community. So I decided I would create a social platform using Laravel.
The idea was to make a platform where tradespeople could post their details, and get reviews
etc...</p>
<p>Laravel provided a great starting point. I got 80% of the way very quickly. But the more I got
into the project and had to do things that Laravel didn't provide me methods of doing, basically
the business logic. I felt constrained. I felt like Laravel was hindering me rather than helping
me. There was also so much to learn about the <em>Laravel</em> way of doing things that I just became
overwhelmed and abandoned the project. To some it up, Laravel helps you to 80% but fights you for
the last 20%.</p>
<p><chicken-asks>But what about PHP? he just talked about Laravel</chicken-asks>
<magpie-replies>Basically if you are writing PHP today you are writing Laravel. At it's core PHP is still the same as it was 10 years ago, with a little syntactic sugar.</magpie-replies></p>
<h2>Java</h2>
<p>After failing to complete the last 20% of the social platform for tradespeople with Laravel, I
thought <em>Hey, let's try Java and Spring</em>. The difference here is that I have worked professionally
for years with Java and Spring. So I knew what I was getting into. Java itself is a decent language,
but everything around it is soul destroying. For example you feel compelled to write methods called <em>getX</em>
and <em>setX</em> which are public rather than just have a public field of <em>x</em>. Verbosity for the sake of it.</p>
<p><magpie-trinket>People get around this nonsense by using the Lombok library which uses decorators to auto generate these methods. But really this is just another hoop to jump through.</magpie-trinket></p>
<p>And that pretty much sums up Java. Writing laborious amounts of code for the sake of it. For example
if I wanted to display info from a database to a client (a simple select), I would find myself writing
5 or 6 classes, all in seperate files. <em>Entity</em>, <em>EntityRepository</em>, <em>EntityService</em>, <em>EntityController</em>,
<em>EntityDTO</em>, <em>EntityMapper</em>... I could go on. But christ, I'll give up the will to type.</p>
<p>Java is enterprise bullshit. Don't go there if you want to keep your sanity. Also the IDEs suck now, even JetBrains
lags like hell.</p>
<p>I never finished the social media project by the way. Note to future self. When you get 80% of the way, persevere.
After trying with Java and Spring, I realised Laravel ain't that bad at all.</p>
<h2>JavaScript</h2>
<p>Javascript now also comes in a bullshit enterprise version called Typescript. Typescript allows css kids devs to
role play as Java devs. Rather than worry themselves with delivering a feature they can now spend their
time hacking at a type system, only to negate it all with the <em>any</em> type when things get tricky.</p>
<p>That might be harsh, but in my opinion Typescript and the notion of types in Javascript has detracted
from the language. There was a time when Javascript was the ultimate scripting language. Simple and hackable,
you could get things working with ease. But then 2 concepts were introduced, those were transpilation, which
lead to the aforementioned Typescript, and syntactic-sugar, which has lead to a glut of incomprehensible syntax
for the sake of syntax. Back in the days before es6, you wrote functions, for loops, and understood prototypal
inheritance. In those days you could get things done with javascript. Now if you leave the language for a fortnight
you are forced to re-learn it from scratch next time you pick it up.</p>
<p>Take this blog for example. It's a javascript file that converts markdown to html and places the output in a directory.
It's not complicated at all, the script is roughly 200 lines. I used node js to handle the filesystem interactions.
One day however it stopped working. Certain syntax was no longer correct. Some people in a meeting somewhere decided
to change how requireing a file worked, and my blog script broke. Then the libraries I used for converting markdown
to html needed updated. Then the syntax highlighter randomly changed it's api and broke the blog script. Why? no idea.</p>
<p>It's this churn and change that makes the JavaScript <em>ecosystem</em> unmanageable. Javscript doesn't need every language
feature ever. Syntax does not help in the long run, it just adds bullshit to what was a great scripting language.</p>
<p><magpie-replies>Don't get him started on client side js &quot;apps&quot;...</magpie-replies>
<chicken-asks>Why? what's wrong with rewriting an application's entire frontend every 2 years?</chicken-asks></p>
<p>So to sum up javascript, if you are building something serious then avoid javascript.</p>
<h2>Python</h2>
<p>As I mentioned at the top of the article I've been using Python at work this year. Integrating an LLM to provide
summaries of health insurance information, and use natural language processing to relate user queries to FAQs. And
do you know what? It's been great. Sure there is some bolted on nonsense that feels unnatural, similar to javascript.
In Python these take the form of type hints and dataclasses.</p>
<p>I know from the general commentary online that there are some issues around Python environments and package managers.
But in my case I never had any of these issues, so I honestly can't complain. Speed is also a common gripe. <em>Python is
slow</em>. Well it may be, but the tool I implemented, which ran as an Azure function, was fast. The vast majority of the
time being spent calling a third party api.</p>
<p>So in general I'd give Python a thumbs up. I'd reach for it if I needed to write a quick script or automate a task.
Which feels like it's intended purpose.</p>
<h2>C</h2>
<p>I've written a lot of C this year. My post on <a href="/string-buffer-c/">writing a string buffer in C</a> generated a lot of
attention on hacker news. I even got emails about it, with others showing how it could be improved, which I was
very grateful for. C feels like a proper programming language. If you know your stuff you can literally implement
anything in C. I don't know my stuff, so I really found it hard to become confident and efficient with it. The lack
of the concept of string really befuddled me.</p>
<p>That being said, I'm going to persevere with C. I feel it taught me more than any other programming language, and if
I really want to progress off my current skill plateau, C is the way to go.</p>
<h2>C++</h2>
<p>This is another language that I have used professionally. I worked on an internal document search engine written in C++.
This is a language that get's a lot of abuse online. People seem to dislike it greatly. They complain about templates,
compile time, and footguns. But similar to my Python experience, I don't really have any gripes with it. I found it to
be a perfectly adequate tool. The codebase I worked on was perfectly readable and manageable, maybe I got lucky. This
is also the programming language I've studied the most. I've read two of Stroustrop's books, and Scott Myers
&quot;Effective C++&quot; cover to cover. So perhaps that grounded me in the language. I also don't have a complaint about the
many language features of C++. I know I complained about features for the sake of it in regards to javascript, but with
C++ I don't feel compelled to use every feature. I use what I need to get the job done.</p>
<p>C++ is a serious language, for getting things done. John Carmack even said on the Lex Fridman podcast that a team of
good C++ devs would be his top choice. He did say he writes <em>C flavoured</em> C++. But that harks back to my point of using
the features you need.</p>
<p>So in future, if I ever want to write a serious bit of software, I'll reach for C++.</p>
<h2>Lua</h2>
<p>I was first introduced to Lua when I followed Harvards CS50 game dev course. The lecturer used Lua and Love2d to implement
some retro 2d games. Lua is a great extension language. But it's really just barebones. There isn't much of a standard lib.
Which makes sense, as I guess if it had an extensive standard lib it would be much harder to bake into other applications.
As the standard lib would need constant re-implementation.</p>
<p>Lua is essentially what javascript should be. A basic embedded scripting language, no bullshit.</p>
<h2>Go</h2>
<p>I'm currently writing an extension for the Caddy web server, which is written in Go. The extension allows for lua scripting
with in the caddy server. Go is great, if you want all your decisions made for you. The Go team provide you with pretty
much everything you need. As well as a whole heap of opinions about how you should write Go. The tooling is great, it just
works. The language itself if slightly annoying, I feel like I'm constantly writing code to handle imaginary errors.</p>
<p>Anyway, I'm on the fence about Go. I don't find it an enjoyable language. I'm not excited to write more Go. But that
said, I haven't yet developed a strong opinion on it, I may very well come to like it.</p>
<h2>Round Up</h2>
<p><strong>TLDR;</strong> Python for scripting, C or C++ for everything else. Enough said.</p>
<p>I still haven't found my <em>niche</em>. But I know I want to explore C more. I have a project in mind that is outside of
my current skill range. It will require me to grow my knowledge in a domain that I am not familiar with. I will use C.
And I will persevere. In 2025 I used many different langauges, and flip flopped from project to project. In 2026 I
will narrow my focus. Because ultimately it's the ability to persevere and hammer away at a problem that brings success.</p>
<p>Oh yea, and if I have to do any small task quickly I'll use Python.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Neovim 0.12 adds vim.pack]]></title>
      <link>https://briandouglas.ie/vim-dot-pack</link>
      <guid>https://briandouglas.ie/vim-dot-pack</guid>
      <pubDate>2025-11-28T00:00:00.000Z</pubDate>
      <description><![CDATA[It&#x27;s a pain having to use external plugin managers for vim. It&#x27;s always
ircked me, and now I&#x27;m glad to see the neovim has added a builtin plugin
manager as of 0.12.

]]></description>
      <content:encoded><![CDATA[
        <p>As of writing, 0.12 is the dev release. But I've been using it with no issues.</p>
<p><magpie-trinket>This YouTube video explains the feature much better than Brian, <a href="https://www.youtube.com/watch?v=UE6XQTAxwE0">How to use vim.pack</a></magpie-trinket></p>
<p>The new feature is called <em>vim.pack</em> and it has a super simple api consisting of <em>add</em>, <em>update</em>, and <em>del</em>.
This allows you to use lua to manage your plugins with ease.</p>
<p>So now to add plugins, take the url and pass it to <em>vim.pack.add</em> with all your other plugin urls.</p>
<pre><code class="language-lua">vim.pack.add({
  'https://github.com/neovim/nvim-lspconfig',
  'https://github.com/nvim-treesitter/nvim-treesitter',
  'https://github.com/nvim-lua/plenary.nvim',
  'https://github.com/nvim-telescope/telescope.nvim',
  -- and so on
})
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Part-time sheep farmer]]></title>
      <link>https://briandouglas.ie/sheep-farming</link>
      <guid>https://briandouglas.ie/sheep-farming</guid>
      <pubDate>2025-11-27T00:00:00.000Z</pubDate>
      <description><![CDATA[&quot;Part-time sheep farmer&quot;, that is my new job title.

]]></description>
      <content:encoded><![CDATA[
        <p>Things have been super hectic over the last few months. We have just moved into
our new home next door to my wife's dad. The house was built in 1995 but never
finished. So we decided that this year we would finish it off and move up.
The house is on Tony's 300 acre sheep farmer, in the bluestack mountains of Donegal.
It's a remote location. Our nearest neighbour is about a mile away as the crow flies.
It is one of the most beautiful places you could ever expect to live in.</p>
<p>As part of the renovations I have taken on board the painting, kitchen installation, and
all the other unspecialised jobs, of which there are many. In order to get these done
before christmas, when our second child is due, I've resigned from my job as a software
engineer. Even though I am unemployed, I've been working harder than ever. Renovating the
house as well as taking over duties on Tony's sheep farm. He is getting up there in age
and has been struggling with the more physical aspects of the farm. So he's glad for the help.</p>
<p>We have over 150 sheep, and I thoroughly enjoy taking care of them, getting them in off the hill,
and administering medecine when needed. It's a great life. Modern technology has made it some what
easier. For example, on Monday I used a drone to find 5 missing sheep up the hill. I then spent 4
hours running them down off the hill and into a field. This was fantastic exercise, I was exhausted
afterward.</p>
<p>We moved in 2 weeks ago, and life up the hill has been great. I can feel the <em>stress</em> of life
leaving my body. This stress has been accumulating over the past few years as I have been working
non-stop without a holiday. It had to be done, I had to get a secure place for my family to live
and keep the savings high, as we await our second child.</p>
<p>I will return to writing code soon. But not employment. I've decided to take some time out and
concentrate on the farm. So I predict it will be 3 days farming a week for the next year, while
I learn the trade and develop into a proper sheperd. In regards to <em>code</em>, I have lot's of ideas
I want to pursue. All of which I will post about here. I hope to start streaming in the new year.
Mainly to put pressue on myself to concentrate on programming.</p>
<p>A new chapter is starting for me and my family, I'm excited.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Go install script]]></title>
      <link>https://briandouglas.ie/install-go</link>
      <guid>https://briandouglas.ie/install-go</guid>
      <pubDate>2025-10-23T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;m currently writing an extension for the Caddy web server which is written in Go.
Becuase of this I needed to install go on my wsl Ubuntu instance. For future Brian,
I have created a bash script to install other versions of Go.

]]></description>
      <content:encoded><![CDATA[
        <p>The script caches the tarball, so I can switch version easily if needed.</p>
<h2>The Script</h2>
<pre><code class="language-shell">#!/usr/bin/env bash
# Usage: ./install-go.sh 1.25.3

set -e

if [ -z &quot;$1&quot; ]; then
  echo &quot;Usage: $0 &lt;go-version&gt; (e.g., $0 1.25.3)&quot;
  exit 1
fi

VERSION=&quot;$1&quot;
ARCH=&quot;amd64&quot;
CACHE_DIR=&quot;$HOME/.cache/go-installer&quot;
TARBALL=&quot;go${VERSION}.linux-${ARCH}.tar.gz&quot;
DOWNLOAD_URL=&quot;https://go.dev/dl/${TARBALL}&quot;

mkdir -p &quot;$CACHE_DIR&quot;

if [ -f &quot;$CACHE_DIR/$TARBALL&quot; ]; then
  echo &quot;Using cached Go tarball: $CACHE_DIR/$TARBALL&quot;
else
  echo &quot;Downloading Go $VERSION...&quot;
  wget -q &quot;$DOWNLOAD_URL&quot; -O &quot;$CACHE_DIR/$TARBALL&quot;
fi

if [ -d &quot;/usr/local/go&quot; ]; then
  echo &quot;Removing old Go installation...&quot;
  sudo rm -rf /usr/local/go
fi

echo &quot;Installing Go $VERSION...&quot;
sudo tar -C /usr/local -xzf &quot;$CACHE_DIR/$TARBALL&quot;

if ! grep -q &quot;/usr/local/go/bin&quot; ~/.bashrc; then
  echo &quot;Adding Go to PATH...&quot;
  echo 'export PATH=$PATH:/usr/local/go/bin' &gt;&gt; ~/.bashrc
fi

source ~/.bashrc

echo &quot;Installed version:&quot;
go version
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Managers, managers, managers]]></title>
      <link>https://briandouglas.ie/managers-managers-managers</link>
      <guid>https://briandouglas.ie/managers-managers-managers</guid>
      <pubDate>2025-10-21T00:00:00.000Z</pubDate>
      <description><![CDATA[Today was my last day at work. I work as a short term contractor, although sometimes it&#x27;s not
short term. I onced worked a rolling 6 month contract for 4 years. It paid for my house and
wedding, those were the days. Often I am happy to accept extensions. But sometimes I don&#x27;t.
When I don&#x27;t it&#x27;s always been down to the same cause. Managers or mismanagement.

]]></description>
      <content:encoded><![CDATA[
        <p>This was particularly prevalent in my last role. I worked on a delivery team of 8. Which is quite
a big team. You would imagine maybe there was 4 devs, 2 qa, and 2 manager types. Well that was not
the case. I was the sole developer, and worked with one qa. Leaving 6 &quot;managers&quot;, I put manager in
quotation marks as their title may not have been &quot;manager&quot;. Actually, here is the breakdown:</p>
<ul>
<li>Product Manager  (Seemed to be in charge)</li>
<li>Product Owner    (Disciple of Product Manager)</li>
<li>Project Manager  (Basically a scrum master)</li>
<li>Business Analyst (Not sure, wrote jira stoies)</li>
<li>Tech Team Lead   (Semi technical, didn't really contribute)</li>
<li>QA Team Lead     (Joined meetings, didn't contribute)</li>
</ul>
<p>The product in question was a website prompt that would interupt a user on an &quot;email us&quot; form and
suggest a link from a list of frequently asked questions. It used <strong>AI</strong>, magic <strong>AI</strong>, buzzword
of the minute <strong>AI</strong>. Basically I took the query and used natural language processing to find if
one of the list of FAQs matched the query. No big deal, delivered in a week sort of feature.
DELIVERED IN A WEEK SORT OF FEATURE. Well no. The project is in it's second year.</p>
<p>It's not in it's second year because it isn't live. It is, it's delivered. But the managers are still
managing. They are looking for ways to improve it's <em>performance</em>. To find out more key metrics.
To get more funding (3.75 million a year!), so they can continue the project. Despite the feature
long being delivered. And, to be fair, the business are delighted. They have delivered an <strong>AI</strong>
product. wow. They are ahead of the curve when it comes to tech, they are basically bleeding edge.
I kid you not, they even got an award, at an award ceremony. Attended by people how were not even
involved in the project. They got to dress up fancy and go on stage to accept some crystal ware.</p>
<p>So why did I decide to leave such a successful team? well because my soul was destroyed. The reality
is, it's a small feature of a website. It took me a few days to implement, and I've been in meetings
about it ever since. Meetings that go nowhere, because there are two many managers. And when there is
more than one manager, and layers of management above them, no decision can ever be made. Teams just
exist in a state of perpetual wheel spinning. Lot's of meetings, no outcomes. Lot's of being busy but
not achieving anything. Honestly my head is wrecked. But I had to stay to earn money to provide for my
family. I did offer my help to other teams, but a <em>manager</em> told me that it would make the delivery
team I was supposed to be on look bad. As it might seem they didn't have enough work for me... they
didn't!</p>
<p>The funny thing is, today is my last day. And there is no replacement. So the team will now consist of
6 managers, and a qa. So not only did I feel redundant, I was redundant.</p>
<p>I'm now going to take some time off, and work on some products of my own. I'm going to give myself 6
months at least. This will help me build up my confidence to re-enter the workplace, get rid of the cyncism
that has built up, and develop my skills again. I've been working non stop for 15 years. So it's a well
earned break. It comes at a good time. We've just finished building our house, and my wife is due to deliver
our second child in December. I am a happy man.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[mise-en-place]]></title>
      <link>https://briandouglas.ie/mise-en-place</link>
      <guid>https://briandouglas.ie/mise-en-place</guid>
      <pubDate>2025-10-10T00:00:00.000Z</pubDate>
      <description><![CDATA[Everything should be rewritten in rust. Or so I&#x27;m told by all the vibe coders.
&#x60;mise&#x60; is a _dev tool manager_ that is super handy. I currently use it to manage
my dev projects. So the context of this article is my wsl ubuntu 24.04 instance.

]]></description>
      <content:encoded><![CDATA[
        <p><magpie-trinket>Give the <code>mise</code> project a like on github: <a href="https://github.com/jdx/mise">jdx/mise</a></magpie-trinket></p>
<p>Need a certain node version?</p>
<pre><code>mise install node@20.0.0
mise use node
</code></pre>
<p>Handy. But also you'll notice that the directory you ran that in now has a <code>mise.toml</code> file.
This is because mise installs but does not load the package. Meaning, only the current folder
and it's sub dirs have access to the node version you specified. Which is way handier than global
installs.</p>
<p>It can also manage env vars and define tasks, which again is super handy. So my advice to you is
to take 10-15 minutes out of your day and learn about this tool of tools. It will save you some time.
That is before the ai overlords remove your ability to <em>brain code</em>, or <em>brian code</em> as I call it.</p>
<p>The only downside of this tool is I keep typing <code>mist</code>. So I'll probably have to vibe code a rewrite
in zig...</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Generate SQLite table types for Typescript]]></title>
      <link>https://briandouglas.ie/generate-sqlite-table-types-bun</link>
      <guid>https://briandouglas.ie/generate-sqlite-table-types-bun</guid>
      <pubDate>2025-09-01T00:00:00.000Z</pubDate>
      <description><![CDATA[Bun has an sqlite interface built in. It make for some great dev-ex. One thing that ircks me when dealing with
sql and Typescript however, is that when I run queries I often have to model the db table that I am querying in
Typescript. This usually involves a manual effort, but not anymore. I have written a short script to auto 
generate Typescript types for table definitions.

]]></description>
      <content:encoded><![CDATA[
        <p><chicken-asks>Why not <em>just use an ORM</em>?</chicken-asks>
<magpie-replies>SQL is the best way to interface with a DB. ORMs add too many complex abstractions.</magpie-replies></p>
<h2>Implementation</h2>
<p>The goal here is not to have to write types that model db tables. So the end goal is to run a query like so,
where <code>projects_table</code> is a type that has been generated from the db schema.</p>
<pre><code class="language-typescript">const project = db.query&lt;projects_table, []&gt;('SELECT * FROM projects WHERE id = 1').get()
</code></pre>
<p>In order to generate the table definition types I use the below scripts. Which I call on each db migration.
This means that the generated types stay up to date with my database.</p>
<p>The below only generates types for table definitions, but I am sure views and function return types could
also be modelled.</p>
<pre><code class="language-typescript">import { writeFileSync } from &quot;node:fs&quot;;
import { db } from &quot;./db/db&quot;;
import { join } from &quot;node:path&quot;;
import { mapSqliteTypeToTs } from &quot;./sqlite-types&quot;

export function generateSqliteTableDefinitions() {
    let content = '/* GENERATED FILE CONTENT DO NOT EDIT */\n\n'
    const tables = db.query&lt;{ name: string }, []&gt;(`SELECT name FROM sqlite_master WHERE type='table';`).all()

    for (const table of tables) {
        const stmt = db.prepare&lt;{ name: string, type: string, }, []&gt;(`PRAGMA table_info(${table.name})`);
        const columns = stmt.all();
        content += `export type ${table.name}_table = { \n${columns.map(it =&gt; `  ${it.name}: ${mapSqliteTypeToTs(it.type)}`).join(';\n')}\n}\n\n`
    }

    writeFileSync(join(process.cwd(), 'sqlite-table-defs.ts'), content, 'utf8')
}
</code></pre>
<p>And the type mapper:</p>
<pre><code class="language-typescript">// sqlite-types.ts
const sqliteToTsType: Record&lt;string, string&gt; = {
    // numeric
    &quot;INT&quot;: &quot;number&quot;,
    &quot;INTEGER&quot;: &quot;number&quot;,
    &quot;TINYINT&quot;: &quot;number&quot;,
    &quot;SMALLINT&quot;: &quot;number&quot;,
    &quot;MEDIUMINT&quot;: &quot;number&quot;,
    &quot;BIGINT&quot;: &quot;number&quot;,
    &quot;UNSIGNED BIG INT&quot;: &quot;number&quot;,
    &quot;INT2&quot;: &quot;number&quot;,
    &quot;INT8&quot;: &quot;number&quot;,

    // real
    &quot;REAL&quot;: &quot;number&quot;,
    &quot;DOUBLE&quot;: &quot;number&quot;,
    &quot;DOUBLE PRECISION&quot;: &quot;number&quot;,
    &quot;FLOAT&quot;: &quot;number&quot;,
    &quot;NUMERIC&quot;: &quot;number&quot;,
    &quot;DECIMAL&quot;: &quot;number&quot;,

    // text
    &quot;TEXT&quot;: &quot;string&quot;,
    &quot;CHARACTER&quot;: &quot;string&quot;,
    &quot;VARCHAR&quot;: &quot;string&quot;,
    &quot;VARYING CHARACTER&quot;: &quot;string&quot;,
    &quot;NCHAR&quot;: &quot;string&quot;,
    &quot;NATIVE CHARACTER&quot;: &quot;string&quot;,
    &quot;NVARCHAR&quot;: &quot;string&quot;,
    &quot;CLOB&quot;: &quot;string&quot;,

    // date/time
    &quot;DATE&quot;: &quot;string&quot;,
    &quot;DATETIME&quot;: &quot;string&quot;,

    // blob
    &quot;BLOB&quot;: &quot;Buffer&quot;,

    // null
    &quot;NULL&quot;: &quot;null&quot;,
};

export function mapSqliteTypeToTs(type: string): string {
    const normalized = type.trim().toUpperCase();
    for (const key in sqliteToTsType) {
        if (normalized.includes(key)) {
            return sqliteToTsType[key] ?? 'any';
        }
    }
    return &quot;any&quot;;
}
</code></pre>
<h2>Generated Types</h2>
<p>The generated output is as follows:</p>
<pre><code class="language-typescript">/* GENERATED FILE CONTENT DO NOT EDIT */

export type migrations_table = { 
  id: number;
  name: string;
  created_at: string
}

export type sqlite_sequence_table = { 
  name: any;
  seq: any
}

export type users_table = { 
  id: number;
  email: string;
  password: string;
  created_at: string
}

export type projects_table = { 
  id: number;
  name: string;
  created_at: string
}

export type project_users_table = { 
  project_id: number;
  user_id: number;
  role: string;
  created_at: string
}

export type questions_table = { 
  id: number;
  text: string;
  project_id: number;
  user_id: number;
  created_at: string
}

export type answers_table = { 
  id: number;
  text: string;
  question_id: number;
  user_id: number;
  created_at: string
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Literal templates]]></title>
      <link>https://briandouglas.ie/literal-templates</link>
      <guid>https://briandouglas.ie/literal-templates</guid>
      <pubDate>2025-08-21T00:00:00.000Z</pubDate>
      <description><![CDATA[I introduce to you &quot;literal-templates&quot;, a lightweight, flexible template engine for Node.js. 
Offering dynamic registration and rendering of template views, includes, and helpers.

]]></description>
      <content:encoded><![CDATA[
        <h2>Features</h2>
<ul>
<li><strong>Views</strong>: Register, render, or remove named template views.</li>
<li><strong>Includes</strong>: Register reusable template snippets (&quot;includes&quot;) and use them within your views.</li>
<li><strong>Helpers</strong>: Register custom helper functions accessible in your templates.</li>
<li><strong>Directory Registration</strong>: Bulk-register views or includes from a directory of files.</li>
<li><strong>Template Compilation</strong>: Templates are compiled to JavaScript functions for efficiency, using ES6 template literals for variable interpolation.</li>
</ul>
<p><magpie-trinket>Full source code is available at <a href="https://github.com/BrianDouglasIE/literal-templates/tree/master">BrianDouglasIE/literal-templates</a></magpie-trinket></p>
<h2>Installation</h2>
<pre><code class="language-bash">npm install @briandouglasie/literal-templates
</code></pre>
<h2>Usage</h2>
<h3>Basic Example</h3>
<pre><code class="language-js">import {
  registerView,
  view,
  registerHelper,
  create
} from '@briandouglasie/literal-templates'

// Register a helper
registerHelper('upper', str =&gt; str.toUpperCase())

// Register a view
registerView('greeting', create('Hello ${data.name}!'))

// Render the view
console.log(view('greeting', { name: 'World' })) // Output: Hello World!
</code></pre>
<h3>Registering Views and Includes from Directories</h3>
<p>You can bulk-register all <code>.html</code> files in a directory as views or includes:</p>
<pre><code class="language-js">import { registerViewDir, registerIncludeDir } from '@briandouglasie/literal-templates'

await registerViewDir('./views')    // Registers all .html files as views
await registerIncludeDir('./includes') // Registers all .html files as includes
</code></pre>
<h2>API Reference</h2>
<h3>Views</h3>
<ul>
<li><strong>registerView(name, func)</strong>: Register a new view by name.</li>
<li><strong>removeView(name)</strong>: Remove a view by name.</li>
<li><strong>clearViews()</strong>: Remove all registered views.</li>
<li><strong>getViews()</strong>: Get a shallow copy of all registered views.</li>
<li><strong>registerViewDir(dirPath, recursive = true, ext = '.html')</strong>: Register all files with the given extension from a directory as views.</li>
<li><strong>view(name, vars)</strong>: Render a view with the provided variables.</li>
</ul>
<h3>Includes</h3>
<ul>
<li><strong>registerInclude(name, func)</strong>: Register a new include by name.</li>
<li><strong>removeInclude(name)</strong>: Remove an include by name.</li>
<li><strong>clearIncludes()</strong>: Remove all registered includes.</li>
<li><strong>getIncludes()</strong>: Get a shallow copy of all registered includes.</li>
<li><strong>registerIncludeDir(dirPath, recursive = true, ext = '.html')</strong>: Register all files with the given extension from a directory as includes.</li>
</ul>
<h3>Helpers</h3>
<ul>
<li><strong>registerHelper(name, func)</strong>: Register a new helper function.</li>
<li><strong>removeHelper(name)</strong>: Remove a helper by name.</li>
<li><strong>clearHelpers()</strong>: Remove all registered helpers.</li>
<li><strong>getHelpers()</strong>: Get a shallow copy of all registered helpers.</li>
</ul>
<h3>Templates</h3>
<ul>
<li><strong>create(template, varNames = ['data'])</strong>: Compile a template string into a render function.
<ul>
<li><code>template</code>: The template string (can use <code>${}</code> for variable interpolation).</li>
<li><code>varNames</code>: Array of argument names passed to the template (default: <code>['data']</code>).</li>
<li>Helpers and includes are always available as <code>helpers</code> and <code>include</code> in the template context.</li>
</ul>
</li>
</ul>
<h2>Template Syntax</h2>
<p>Templates use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals">ES6 template literals</a> for variable interpolation:</p>
<pre><code class="language-js">create('Hello ${data.name}, today is ${helpers.upper(data.day)}!')
</code></pre>
<ul>
<li><code>helpers</code>: Access any registered helper functions.</li>
<li><code>include(name, vars)</code>: Render an include (only available within templates).</li>
<li>Variables are passed as the first argument (<code>data</code> by default) when rendering.</li>
</ul>
<h2>Example: Using Includes and Helpers</h2>
<p>Suppose you have the following in <code>includes/header.html</code>:</p>
<pre><code class="language-html">&lt;h1&gt;${helpers.upper(data.title)}&lt;/h1&gt;
</code></pre>
<p>Register and use it:</p>
<pre><code class="language-js">await registerIncludeDir('./includes')
const template = create('${include(&quot;header&quot;, { title: data.header })} Hello, ${data.name}!')
console.log(template({ header: 'welcome', name: 'Alice' }))
// Output: &lt;h1&gt;WELCOME&lt;/h1&gt; Hello, Alice!
</code></pre>
<h2>License</h2>
<p>See <a href="./LICENSE">LICENSE</a> for details.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Template strings as a view engine - Part 2]]></title>
      <link>https://briandouglas.ie/extending-template-strings-view-engine</link>
      <guid>https://briandouglas.ie/extending-template-strings-view-engine</guid>
      <pubDate>2025-08-20T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve been thinking some more about using template strings as a view engine. This blog for
example now uses the template engine shown below. It&#x27;s quite simple to implement and it&#x27;s
usage in html files is super intuitive. You just need to remember that the html you are
writing will be intepretted as a template string, and you already know how to use template
strings, so there is no overhead.

]]></description>
      <content:encoded><![CDATA[
        <p>I've expanded on my last post to make it a more full featured engine. The below code adds
helper methods that can be called inside a template. For example, to include one template
in another you can use the <code>helper.include()</code> method. Which essentially allows you to use
partials and layouts.</p>
<p>It's possible to pass in any var name to use in the template. But I have settled on <code>data</code>
as a single object to hold all the relevant view <em>data</em>. This reduces cognitive load, as I
know each template has access to <code>data</code> and <code>helpers</code>.</p>
<p>The implementation below reads all the html files inside the <em>viewDir</em> folder and registers
them as <em>views</em>. The same is true for all the html files in the <em>includesDir</em> folder. This
Means that the <code>helpers.include()</code> method can pull in any template from the includes dir.
Likewise <code>view</code> will render any file from the <code>viewDir</code>.</p>
<p>Implementation and example usage below.</p>
<h2>Usage</h2>
<pre><code class="language-html">&lt;!-- index.jinja --&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;${ data.title ?? 'default title'}&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;h2&gt;Post List&lt;/h2&gt;
        ${helpers.include('post-list', { posts })}
    &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<pre><code class="language-html">&lt;!-- includes/post-list.html --&gt;
&lt;ul&gt;
    ${data.posts.map(post =&gt; `
        &lt;li&gt;${post.title}&lt;/li&gt;
    `).join('')}
&lt;/ul&gt;
</code></pre>
<h2>Implementation</h2>
<pre><code class="language-javascript">import {readFile} from 'node:fs/promises'
import {existsSync} from 'node:fs'
import {join} from 'node:path'

const outDir = './docs'
const viewDir = './views'
const includeDir = join(viewDir, './includes')

/*
 * View engine
 */

const views = {}
const includes = {}

const templateHelpers = {
    include: (name, data) =&gt; includes[name](data),
    // add methods here to make them available in templates
}

function compileTemplate(template, ...varNames) {
    const fn = new Function('helpers', ...varNames, `return \`${template}\``)
    return (...args) =&gt; fn(templateHelpers, ...args)
}

for (const entry of await readdir(includeDir)) {
    if (!entry.endsWith('.html')) continue

    includes[entry.replace('.html', '')] = compileTemplate(await readFile(join(includeDir, entry)), 'data')
}

for (const entry of await readdir(viewDir)) {
    if (!entry.endsWith('.html')) continue

    views[entry.replace('.html', '')] = compileTemplate(await readFile(join(viewDir, entry), 'utf8'), 'data')
}

function view(name, data) {
    return views[name](data)
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Template strings as a view engine]]></title>
      <link>https://briandouglas.ie/js-templates</link>
      <guid>https://briandouglas.ie/js-templates</guid>
      <pubDate>2025-08-17T00:00:00.000Z</pubDate>
      <description><![CDATA[Template strings in JS can be used to create a view engine without adding any extra libraries.
So if you are creating a server side js app, think before you reach for handlebars, ejs, or
god forbid pug. Not becuase those are bad, pug is, but becuase it&#x27;s so easy to leverage template
strings as a view engine.

]]></description>
      <content:encoded><![CDATA[
        <p>I'm not evening going to bother with a proposed usage or a detailed explantation of what is going,
because it's so straight forward that it would be a waste of time. Get your eyeballs on the code
below, and see for yourself.</p>
<p><magpie-trinket>Notice that he used <code>new Function</code> instead of <code>eval</code>. This means only the function body will be evaluated. It also allows for a clean way to pass vars to the view.</magpie-trinket></p>
<pre><code class="language-javascript">const layoutFile = Bun.file('./app.layout.html')
const bodyFile = Bun.file('./template.html')

function compileTemplate(template, vars) {
    return new Function(...vars, `return \`${template}\``)
}

const body = compileTemplate(await bodyFile.text(), ['name', 'hobbies'])
const layout = compileTemplate(await layoutFile.text(), ['data'])

const rendereredHTML = layout({
    title:'my page', 
    body: body('brian', ['a', 'b', 'c'])
})
</code></pre>
<pre><code class="language-html">&lt;!-- template.html --&gt;
&lt;h1&gt;Hello ${name}&lt;/h1&gt;
&lt;ul&gt;${hobbies.map(hobby =&gt; `&lt;li&gt;${hobby}&lt;/li&gt;`).join('')}&lt;/ul&gt;
</code></pre>
<pre><code class="language-html">&lt;!-- app.layout.html --&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;${ data.title ?? 'default title'}&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        ${data.body}
    &lt;/body&gt;
&lt;/html&gt;
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Object field validators in Typescript]]></title>
      <link>https://briandouglas.ie/typescript-field-validators</link>
      <guid>https://briandouglas.ie/typescript-field-validators</guid>
      <pubDate>2025-08-08T00:00:00.000Z</pubDate>
      <description><![CDATA[Before you pull in the next hot new validation library. Ask yourself this question. Do I need it?
The answer is no, field validation is quite easy. Let me show you how to create an extensible
object field validator. 

]]></description>
      <content:encoded><![CDATA[
        <h2>Whats an object field validator?</h2>
<p>Ok, so let's work with the following scenario. You have a user registration form. It's represented
in your code as the following type.</p>
<pre><code class="language-typescript">type UserForm = { email: string, password: string }
</code></pre>
<p>Typescript will ensure that you get an error message when you try to assign a <code>number</code> instead of
a <code>string</code> for the email field. But this object represents form data, aka user input. So it could
be anything. We should validate that it matches our criteria. For example the email should be of a
valid email format, and the password should be no less that 8 characters in length.</p>
<p>This where an <em>object field validator</em> comes into play. It takes an object and applies the given
validators to the object fields. Telling us that, <em>yes this user form contains a valid email address</em>
etc...</p>
<h2>Usage</h2>
<p>Let's start by defining how we want to use it. I'd like to make a composable validator. This would be
a function that takes a generic type and produces a validator for that type. The type in question is
the above <code>UserForm</code>. The method used to compose the validator should take an object that contains a
list of field validators for each of the <code>UserForm</code> fields. These <em>field validators</em> should themselves
be composable methods.</p>
<p>When the composed validator is called with our <code>UserForm</code> instance, it should return an object representing
any errors that were found.</p>
<p>The end result should be a clean api for creating <em>object field validators</em>. To me the below example is perfect.</p>
<pre><code class="language-typescript">type UserForm = { email: string, password: string }

const userFormValidator = createFieldValidator&lt;UserForm&gt;({
  email: [stringValidators.email((email: string) =&gt; `${email} is not a valid email address`)],
  password: [
    stringValidators.min(8, () =&gt; `Password must be greater than 8 characters in length`),
    stringValidators.max(250, () =&gt; `Password must be less than 8 characters in length`),
  ]
})

const validated = userFormValidator({ email: 'not an email', password: '2 short' })
</code></pre>
<p>The <code>validated</code> variable above will hold the following value.</p>
<pre><code>{
  input: {
    email: &quot;not an email&quot;,
    password: &quot;2 short&quot;,
  },
  errors: {
    email: [ &quot;'not an email' is not a valid email address&quot; ],
    password: [ &quot;Password must be at least 8 characters long&quot; ],
  },
  hasErrors: true,
}
</code></pre>
<p>This makes it easy to check if there are errors present, using the <code>hasErrors</code> field, and then access a list
of error messages for each field.</p>
<h2>Implementation</h2>
<p>I'd like to believe that you are a top programmer and just want to see the code, without me trying to clumsly explain it.
The code doesn't tell lies, I do. So here is the code.</p>
<p><chicken-asks>Why did he need to add <code>&lt;T extends Object&gt;</code>?</chicken-asks>
<magpie-replies>Well <code>Object.entries</code> needs to take an <code>Object</code> or an <code>ArrayLike</code> entity. So it was just to tell the type checker that <code>T</code> is some sort of <code>Object</code>.</magpie-replies></p>
<pre><code class="language-typescript">type ErrorMap = { [key: string]: string[] }
type ErrorMessageCallback = (input: string) =&gt; string
type ValidatorMap = { [key: string]: Function[] }
type Validated&lt;T&gt; = { input: T, errors: ErrorMap, hasErrors: boolean }

export const createFieldValidator = &lt;T extends Object&gt;(validators: ValidatorMap) =&gt; (input: T): Validated&lt;T&gt; =&gt; {
  const errors: ErrorMap = {}
  let hasErrors = false
  for (const [key, value] of Object.entries(input)) {
    if (validators[key]) {
      for (const validator of validators[key]) {
        const error = validator(value)
        if (error) {
          hasErrors = true
          if (errors[key]) {
            errors[key].push(error)
          } else {
            errors[key] = [error]
          }
        }
      }
    }
  }

  return { input, errors, hasErrors }
}

export const stringValidators = {
  isString: (msg?: ErrorMessageCallback) =&gt; (input: string) =&gt; {
    if (!(typeof input == 'string')) return msg ? msg(input) : 'Not a string'
  },
  min: (n: number, msg?: ErrorMessageCallback) =&gt; (input: string) =&gt; {
    if (input.length &lt; n) return msg ? msg(input) : `Must be greater than ${n} characters in length`
  },
  max: (n: number, msg?: ErrorMessageCallback) =&gt; (input: string) =&gt; {
    if (input.length &gt; n) return msg ? msg(input) : `Must be less than ${n} characters in length`
  },
  email: (msg?: ErrorMessageCallback) =&gt; (input: string) =&gt; {
    const emailRe = /^(([^&lt;&gt;()[\]\\.,;:\s@&quot;]+(\.[^&lt;&gt;()[\]\\.,;:\s@&quot;]+)*)|.(&quot;.+&quot;))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
    if (!emailRe.test(input.toLowerCase())) return msg ? msg(input) : 'Not a valid email address'
  }
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Feature flags db schema]]></title>
      <link>https://briandouglas.ie/feature-flags</link>
      <guid>https://briandouglas.ie/feature-flags</guid>
      <pubDate>2025-08-05T00:00:00.000Z</pubDate>
      <description><![CDATA[Feature flags are simple _on_ / _off_ flags that can be used to toggle application features without needing a production release.

]]></description>
      <content:encoded><![CDATA[
        <p>My implementation allows for flags that are domain specific. Each flag can have a different status based on the current env.
For example the flag <code>enable_dark_mode</code> on the domain <code>example.com</code> may be <code>on</code> in <code>staging</code> and <code>off</code> in <code>production</code>. I have kept it
simple and have excluded adding any extra metadata to the feature flags. For example some implementations give the flag a value as well
as a status. So that the flag can be used to change the logic of a program. For example a flag called <code>has_session_timeout</code> may hold an
integer value that sets the session length. I believe that this is convoluted and may add unwanted <em>side effects</em> to what should be a
straight forward on / off switch.</p>
<p><magpie-trinket>The implementation in question is part of a schema collection, which can be found at <a href="https://github.com/BrianDouglasIE/ExampleDatabasesSchemas">BrianDouglasIE/ExampleDatabasesSchemas</a></magpie-trinket></p>
<h2>Set Up</h2>
<p>Full source code can be found at <a href="https://github.com/BrianDouglasIE/ExampleDatabasesSchemas/tree/master/feature_flags">ExampleDatabasesSchemas/feature_flags</a>.
This includes the schema and a <code>data.sql</code> file to quickly load some example data.</p>
<pre><code class="language-sql">createdb feature_flags
psql -d feature_flags -f feature_flags.schema.sql
psql -d feature_flags -f feature_flags.data.sql
</code></pre>
<h2>Example Queries</h2>
<h3>Get a specific flag on staging for a given domain</h3>
<pre><code class="language-sql">SELECT
  d.hostname,
  f.name AS flag,
  df.status,
  df.env,
  f.updated_at
FROM flags f
JOIN domain_flags df ON f.id = df.flag_id
JOIN domains d ON d.id = df.domain_id
WHERE d.hostname = 'example.com'
  AND df.env = 'staging'
  AND f.name = 'enable_dark_mode';
</code></pre>
<pre><code>  hostname   |       flag       | status | env         |          updated_at
-------------+------------------+--------+-------------+-------------------------------
 example.com | enable_dark_mode | on     | staging     | 2025-08-05 08:10:01.471816+00
(1 row)
</code></pre>
<h3>Get all active flags on prod for a given domain</h3>
<pre><code class="language-sql">SELECT
  d.hostname,
  f.name AS flag,
  df.status,
  f.updated_at
FROM flags f
JOIN domain_flags df ON f.id = df.flag_id
JOIN domains d ON d.id = df.domain_id
WHERE d.hostname = 'example.com'
  AND df.env = 'prod'
  AND df.status = 'on';
</code></pre>
<pre><code>  hostname   |    flag     | status |          updated_at
-------------+-------------+--------+-------------------------------
 example.com | require_2fa | on     | 2025-08-05 08:10:01.471816+00
(1 row)
</code></pre>
<h3>Get status of all flags on all envs for a given domain</h3>
<pre><code class="language-sql">SELECT
  f.name AS flag,
  df.status,
  df.env
FROM flags f
JOIN domain_flags df ON f.id = df.flag_id
JOIN domains d ON d.id = df.domain_id
WHERE d.hostname = 'example.com';
</code></pre>
<pre><code>       flag       | status | env
------------------+--------+-------------
 enable_dark_mode | on     | dev
 enable_dark_mode | on     | staging
 enable_dark_mode | off    | prod
 new_ui           | on     | dev
 new_ui           | off    | staging
 new_ui           | off    | prod
 require_2fa      | off    | dev
 require_2fa      | off    | staging
 require_2fa      | on     | prod
(9 rows)
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[PostgresSQL full text search]]></title>
      <link>https://briandouglas.ie/pg-full-text-search</link>
      <guid>https://briandouglas.ie/pg-full-text-search</guid>
      <pubDate>2025-07-31T00:00:00.000Z</pubDate>
      <description><![CDATA[PostgresSQL provides a builtin full text search feature. Here is how I use it with examples.

]]></description>
      <content:encoded><![CDATA[
        <p><magpie-trinket>The database used in the below examples is the <em>netflix</em> database from <a href="https://github.com/neondatabase-labs/postgres-sample-dbs">neondatabase-labs/postgres-sample-dbs</a></magpie-trinket></p>
<h2>Search by title</h2>
<p>Let's start off with a basic search. We are going to use the <code>@@</code> operator to search the <code>title</code> column for the text <code>'Python'</code>.</p>
<pre><code class="language-sql">SELECT title, release_year 
FROM netflix_shows 
WHERE to_tsvector(title) @@ to_tsquery('Python') 
AND release_year &lt; 2000 
ORDER BY release_year DESC;
</code></pre>
<pre><code>                          title                           | release_year
----------------------------------------------------------+--------------
 Monty Python: Live at Aspen                              |         1998
 Parrot Sketch Not Included: Twenty Years of Monty Python |         1989
 Monty Python: Live at The Hollywood Bowl                 |         1982
 Monty Python's Life of Brian                             |         1979
 Monty Python and the Holy Grail                          |         1975
 Monty Python's Flying Circus                             |         1974
 Monty Python's Fliegender Zirkus                         |         1972
(7 rows)
</code></pre>
<h2>Search by title and release_year</h2>
<p>Now let's say we want to find all titles including 'Python' released in 1972. To do this we add the <code>release_year</code> column. Note,
that it needs transformed to a text value in order to by processed.</p>
<pre><code class="language-sql">SELECT title, release_year 
FROM netflix_shows 
WHERE 
	to_tsvector(title || ' ' || release_year::text) @@ 
	to_tsquery('Python &amp; 1972') 
ORDER BY release_year DESC;
</code></pre>
<pre><code>              title               | release_year
----------------------------------+--------------
 Monty Python's Fliegender Zirkus |         1972
(1 row)
</code></pre>
<h2>Search using a full sentence</h2>
<p>You will notice the in the previous example that <code>&amp;</code> was used to seperate the words in the query. Removing this would result in an error.
Likely when using this feature in the real world you will be passing a string from a user into the query. Each word must be seperated by an
ampersand. A quick way to ensure this is to use a <code>regexp_replace</code> to insert the <code>&amp;</code> sumbol between each word.</p>
<pre><code class="language-sql">SELECT title, release_year 
FROM netflix_shows 
WHERE 
	to_tsvector(title || ' ' || release_year::text) @@ 
	to_tsquery(regexp_replace('Monty Python 1972', '\s+', ' &amp; ', 'g')) 
ORDER BY release_year DESC;
</code></pre>
<pre><code>              title               | release_year
----------------------------------+--------------
 Monty Python's Fliegender Zirkus |         1972
(1 row)
</code></pre>
<h2>Adding an index for performance</h2>
<p>Let's say it's regular for a user to search by a title and a year. For example <em>Monty Python 1972</em>. This search
corresponds to the <code>title</code> and <code>release_year</code> column. Adding an index for that search will increase the performance
of the query and can be done like so.</p>
<p><chicken-asks>What's <em>fts</em>?</chicken-asks>
<magpie-replies>Full Text Search</magpie-replies></p>
<pre><code class="language-sql">CREATE INDEX idx_netflix_fts
ON netflix_shows
USING GIN (
	to_tsvector('english', title || ' ' || release_year::text)
);
</code></pre>
<p>The above index can be used as per the following example.</p>
<pre><code class="language-sql">SELECT title, release_year
FROM netflix_shows
WHERE
	to_tsvector('english', title || ' ' || release_year::text) @@
	to_tsquery('english', regexp_replace('Monty Python 1972', '\s+', ' &amp; ', 'g'))
ORDER BY release_year DESC;
</code></pre>
<p>This results in the following speed up when run with <code>EXPLAIN</code>.</p>
<pre><code>Before Index: (cost=5057.67..5057.67 rows=1 width=22)
After Index:  (cost=30.20..34.48 rows=1 width=22)
</code></pre>
<p>The cost drops drastically from 5057 to ~30–34 units of work. Before it was likely a sequential scan, checking every row.
Now it's using a GIN index and skipping most rows, which is nearly 150 times faster.</p>
<h2>Adding an index on a <code>tsvector</code> column</h2>
<p>As we stated above, searching based on <em>title</em> and <em>release_year</em> is a regular occurence. So this may justify adding
a generated <em>tsvector</em> column. Let's alter the table to add an <code>fts</code> column that creates a <code>tsvector</code> based off the
<em>title</em> and <em>release_year</em>.</p>
<pre><code class="language-sql">ALTER TABLE netflix_shows 
ADD COLUMN fts tsvector GENERATED ALWAYS AS (
	to_tsvector('english', title || ' ' || release_year::text)
) STORED;
</code></pre>
<p>Then create the index.</p>
<pre><code class="language-sql">CREATE INDEX idx_netflix_fts ON netflix_shows USING GIN (fts);
</code></pre>
<p>The <code>tsvector</code> column <code>fts</code> can now be used with the <code>@@</code> operator when searching.</p>
<pre><code class="language-sql">SELECT title, release_year
FROM netflix_shows
WHERE fts @@ to_tsquery('english', regexp_replace('Monty Python 1972', '\s+', ' &amp; ', 'g'))
ORDER BY release_year DESC;
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[startswith and endswith]]></title>
      <link>https://briandouglas.ie/endswith-startswith</link>
      <guid>https://briandouglas.ie/endswith-startswith</guid>
      <pubDate>2025-07-27T00:00:00.000Z</pubDate>
      <description><![CDATA[Want to know if a file path ends with _.md_ or a message starts with _hello_? Well then I have
just the methods for you.

]]></description>
      <content:encoded><![CDATA[
        <h2>endswith</h2>
<p>Surely you can't start with endswith? Yes, I can and I will. Cause <code>endswith</code> is slightly easier in C.</p>
<p>Using some pointer arithmetic we take the needle length off the haystack plus haystack length to get the
end of the string. Then a quick <code>strcmp</code> finishes it off nicely.</p>
<pre><code class="language-c">bool endswith(char *needle, char *haystack)
{
    size_t haystack_len = strlen(haystack);
    size_t needle_len = strlen(needle);
    if (needle_len &gt; haystack_len || !needle_len || !haystack_len)
        return false;
    return !strcmp(haystack + haystack_len - needle_len, needle);
}

assert(endswith(&quot;.md&quot;, &quot;hello.md&quot;));
assert(!endswith(&quot;.md&quot;, &quot;hello.txt&quot;));
</code></pre>
<h2>startswith</h2>
<p><code>startswith</code> is slightly more involved as we must first create a <code>char[]</code> that is the length of the needle plus
one, think null terminator. We loop over the start of the haystack adding each char to you temp <code>char[]</code>, add
a quick null terminator and pass it to <code>strcmp</code>, boom!</p>
<pre><code class="language-c">bool startswith(char *needle, char *haystack)
{
    size_t haystack_len = strlen(haystack);
    size_t needle_len = strlen(needle);
    if (needle_len &gt; haystack_len || !needle_len || !haystack_len)
        return false;

    char start[needle_len + 1];
    for (size_t i = 0; i &lt; needle_len; i++)
    {
        start[i] = haystack[i];
    }
    start[needle_len] = '\0';

    return !strcmp(start, needle);
}

assert(startswith(&quot;hello&quot;, &quot;hello.md&quot;));
assert(!startswith(&quot;hello&quot;, &quot;yallohello.txt&quot;));
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Stack Overflow is dieing, no wonder]]></title>
      <link>https://briandouglas.ie/stackoverflow-dieing</link>
      <guid>https://briandouglas.ie/stackoverflow-dieing</guid>
      <pubDate>2025-07-21T00:00:00.000Z</pubDate>
      <description><![CDATA[Below is a screenshot of what I see when I land on Stack Overflow. Bear in mind this is after
I have to prove that I am a _human_.

![A screenshot of how awful stack overflow is now](/images/stackoverflow.png)

]]></description>
      <content:encoded><![CDATA[
        <p>So no wonder articles such as <a href="https://blog.pragmaticengineer.com/stack-overflow-is-almost-dead/">/stack-overflow-is-almost-dead/</a>
are being written. It all comes down to user experience. Stack Overflow no longer has a good user experience.
When I land on the page I want to see the answer to the question I clicked. But instead I have to
legitimise myself, then click away a cookie banner, and another banner asking if I want to log in with
Google. What an awful experience.</p>
<p>People say that AI sites such as ChatGPT are replacing stack overflow... No. Those sites just offer a nicer
user experience. I type a question I get an answer. Stack overflow could work the same way. In fact it used to.
But instead they now have an awful bloated UI and some weird social credit system on display.</p>
<h3>Are you a Robot?</h3>
<p>No, I' just want my question answered.</p>
<p><img src="/images/robot1.png" alt="I'm not a robot"></p>
<h3>Are you sure you are not a Robot?</h3>
<p>No! I jsut want to search something... and also I still don't want to sign in with google.</p>
<p><img src="/images/robot2.png" alt="honestly I'm not"></p>
<h2>Future?</h2>
<p>The <a href="https://redmonk.com/sogrady/2025/06/18/language-rankings-1-25/">Redmonk language rankings</a> are now
considering removing Stack Overflow from their ranking system, stating.</p>
<blockquote>
<p>the arrival of AI has had a significant and accelerating impact on Stack Overflow, which comprises one half of
the data used to both plot and rank languages twice a year... Still to be determined on our end is whether Stack
Overflow should continue to be used.</p>
</blockquote>
<p>While yes I agree that the use of agentic AI is accelerating Stack Overflow's demise. I do not agree with the
sentiment that it is the cause. Poor user experience is the cause. Simply put Stack Overflow is not a nice site
to use anymore. Therefore I don't use it. If Stack Overflow got rid of all the bloat and nonsense in their UI,
then yes, I would use it again. But that's not going to happen. Because there will be a meeting of middle managers
who will justify all the awful user experience descisions and simply blame the passing of time and emergence of
AI on their own failings. Maybe they will add another pop up asking if you have a minute to tell them how they
could improve? that sounds like a great idea.</p>
<p>Stack Overflow, killed Stack Overflow. Such is the way of tech. To start off useful and full of promise, only to
be dragged down by middle management over time. Also google sucks now too, and that was my main gateway into Stack
Overflow. But hey, I can't solve the world's problems, jsut complain about them.</p>
<p>Also can you tell I dislike middle management?</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Trimming strings in c]]></title>
      <link>https://briandouglas.ie/trimming-strings</link>
      <guid>https://briandouglas.ie/trimming-strings</guid>
      <pubDate>2025-07-18T00:00:00.000Z</pubDate>
      <description><![CDATA[Strings in C. They are wonderfully intuitive and easy to work with *cough*.

No, strings in C are a nightmare. Doing simple things such as trimming the
whitespace off the start or end of a string is complicated. Because a string
is not a string it&#x27;s a &#x60;char *&#x60;. Which means that if you want to do interesting
things you are going to have to get used to managing memory. Which means thinking,
and if there is one thing modern programmers hate, it&#x27;s thinking.

Anyway, below I have added &#x60;ltrim_string&#x60;, &#x60;rtrim_string&#x60;, and &#x60;trim_string&#x60; methods.
I&#x27;ve made the code as readable as I can, and documented it with tests.

]]></description>
      <content:encoded><![CDATA[
        <h2>Code</h2>
<p>The trim methods below will return an pointer to a new <code>char *</code>. So the result of
these methods will need freeing. I've opted to return a <code>nullptr</code> when the supplied
<code>char *</code> is null, or if memory allocation fails. If the string only contains whitespace,
I return an empty string.</p>
<magpie-trinket>
<span><strong>ltrim_string</strong>: removes whitespace from the start of the string.</span><br>
<span><strong>rtrim_string</strong>: removes whitespace from the end of the string.</span><br>
<span><strong>trim_string</strong>: removes whitespace from the start and end of the string.</span>
</magpie-trinket>
<pre><code class="language-c">#include &lt;string.h&gt;
#include &lt;ctype.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;assert.h&gt;

char *empty_string()
{
  char *str = malloc(1);
  if(!str) return nullptr;
  str[0] = '\0';
  return str;
}

char *ltrim_string(char *str)
{
  if(!str) return nullptr;

  size_t len = strlen(str);
  if(!len) return empty_string();

  size_t start = 0;
  while(start &lt; len &amp;&amp; isspace(str[start])) start++;

  if(start &gt;= len) return empty_string();

  size_t new_size = len - start;
  char *result = malloc(new_size);
  if (!result) return nullptr;

  memcpy(result, str + start, new_size);
  result[new_size] = '\0';

  return result;
}

char *rtrim_string(char *str)
{
  if(!str) return nullptr;

  size_t len = strlen(str);
  if(!len) return empty_string();

  size_t end = len - 1;
  while(end &amp;&amp; isspace(str[end])) end--;

  if(0 &gt;= end) return empty_string();

  size_t new_size = end + 1;
  char *result = malloc(new_size);
  if (!result) return nullptr;

  memcpy(result, str, new_size);
  result[new_size] = '\0';

  return result;
}

char *trim_string(char *str)
{
  if (!str) return nullptr;

  size_t len = strlen(str);
  if(!len) return empty_string();

  size_t start = 0;
  while(isspace(str[start])) start++;
  
  size_t end = len - 1;
  if(start &gt;= end) return empty_string();
  while(end &amp;&amp; isspace(str[end])) end--;

  size_t new_size = end - start + 1;
  char *result = malloc(new_size + 1);
  if(!result) return nullptr;

  memcpy(result, str + start, new_size);
  result[new_size] = '\0';

  return result;
}

int main(void)
{
  // ltrim
  char *test_str = ltrim_string(&quot;   hello   &quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;hello   &quot;, test_str));
  free(test_str);

  test_str = ltrim_string(&quot;\r\nhello\n&quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;hello\n&quot;, test_str));
  free(test_str);

  // rtrim
  test_str = rtrim_string(&quot;   hello   &quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;   hello&quot;, test_str));
  free(test_str);

  test_str = rtrim_string(&quot;\r\nhello\n&quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;\r\nhello&quot;, test_str));
  free(test_str);

  // trim
  test_str = trim_string(&quot;   hello   &quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;hello&quot;, test_str));
  free(test_str);

  test_str = trim_string(&quot;\r\nhello\n&quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;hello&quot;, test_str));
  free(test_str);

  // empty strings
  test_str = ltrim_string(&quot;    &quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;&quot;, test_str));
  free(test_str);

  test_str = ltrim_string(&quot;&quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;&quot;, test_str));
  free(test_str);

  test_str = rtrim_string(&quot;    &quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;&quot;, test_str));
  free(test_str);

  test_str = rtrim_string(&quot;&quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;&quot;, test_str));
  free(test_str);

  test_str = trim_string(&quot;    &quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;&quot;, test_str));
  free(test_str);

  test_str = trim_string(&quot;&quot;);
  assert(test_str &amp;&amp; 0 == strcmp(&quot;&quot;, test_str));
  free(test_str);

  assert(trim_string(nullptr) == nullptr);

  return 0;
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Banned from lobste.rs and Hacker News]]></title>
      <link>https://briandouglas.ie/banned-from-lobsters-and-hn</link>
      <guid>https://briandouglas.ie/banned-from-lobsters-and-hn</guid>
      <pubDate>2025-07-15T00:00:00.000Z</pubDate>
      <description><![CDATA[For some reason the good people at Hacker News have shadow banned my blog.
I can still post, but the post never appears to anyone else. I&#x27;m not sure
why this is the case, as one of my posts has ~350 upvotes and spent time on
the front page.

What did I do wrong Hacker News?

]]></description>
      <content:encoded><![CDATA[
        <h2>Lobsters</h2>
<p>I'm also totally banned from lobte.rs, they have block my domain and everything.
Why? becuase I posted a link to my own blog. What a cardinal sin, banned for life
not comeback. The ironic thing there is one of my posts was very popular, and also
spend time on their front page. Earning them valuable clicks and discussions. How
did they repay the favour? by banning me from posting and my blog from ever appearing
on their site again. Thank lobste.rs.</p>
<h2>Where else to post?</h2>
<p>Don't know, the internet is basically just bots now. I remember years ago, let's say 10-12
years ago. I could post anywhere and real people would see, share, and discuss the content.
That is not the case anymore. If you are not instantly shadow banned now, your content will
drown under a flood of bot traffic and garbage.</p>
<p>Dead is the age of the internet troll, long live the bot.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Making a StringBuffer in C]]></title>
      <link>https://briandouglas.ie/string-buffer-c</link>
      <guid>https://briandouglas.ie/string-buffer-c</guid>
      <pubDate>2025-07-15T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve been writing a lot of C. Whilst doing so I have been questioning my sanity.
Am I an awful programmer? You know what, I think I might be.

Kudos to all those devs that created monumental feats with C. Because it is not
an easy tool to use. That said, I do enjoy writing C, just as long as I don&#x27;t
have a deadline or any business critical software to deliver. But when I say enjoy, I mean
enjoyment in the sense of using a sycthe to cut a lawn, whilst my lawnmower watches on.
It&#x27;s lovely using the sycthe, but at the end of the day when I am exhausted and cut
only a quarter of the grass that I could have cut with a mower in half the time, I do feel
a great sense of achievement. I&#x27;m exhausted and broken, but happy.

]]></description>
      <content:encoded><![CDATA[
        <h2>StringBuffer</h2>
<p>Anyway here is a string buffer implementation that I extracted from my project. You can see
the up to date source code at <a href="https://github.com/BrianDouglasIE/c_string_buffer">BrianDouglasIE/c_string_buffer</a>.</p>
<p>The replace and remove were particularly tricky to implement. My brain was on fire trying to figure out
how to juggle the memory about correctly.</p>
<h3>Header</h3>
<pre><code class="language-c">#ifndef STRING_BUFFER_H
#define STRING_BUFFER_H

#include &lt;stddef.h&gt;

/// @brief A dynamic string buffer that supports common string operations.
typedef struct {
  size_t size;     ///&lt; The current size (length) of the string.
  size_t capacity; ///&lt; The total allocated capacity of the buffer.
  char *data;      ///&lt; Pointer to the character data.
} StringBuffer;

/// @brief Represents all positions where a match was found.
typedef struct {
  size_t *positions; ///&lt; Array of match positions.
  size_t count;      ///&lt; Number of matches found.
} MatchResult;

/// @brief Represents the result of splitting a string.
typedef struct {
  char **parts; ///&lt; Array of substrings resulting from the split.
  size_t count; ///&lt; Number of parts.
} SplitResult;

// Lifecycle

/// @brief Initializes a new, empty StringBuffer.
/// @return A pointer to the newly allocated StringBuffer.
StringBuffer *StringBuffer_init();

/// @brief Frees memory used by the given StringBuffer.
/// @param buf Pointer to the StringBuffer to be freed.
void StringBuffer_free(StringBuffer *buf);

/// @brief Clears the contents of the StringBuffer without freeing the object.
/// @param buf Pointer to the StringBuffer to be cleared.
void StringBuffer_clear(StringBuffer *buf);

// Operations

/// @brief Prints the contents of the StringBuffer to stdout.
/// @param buf Pointer to the StringBuffer to be printed.
void StringBuffer_print(const StringBuffer *buf);

/// @brief Appends text to the end of the StringBuffer.
/// @param buf Pointer to the StringBuffer.
/// @param text Null-terminated string to append.
void StringBuffer_append(StringBuffer *buf, const char *text);

/// @brief Prepends text to the beginning of the StringBuffer.
/// @param buf Pointer to the StringBuffer.
/// @param text Null-terminated string to prepend.
void StringBuffer_prepend(StringBuffer *buf, const char *text);

/// @brief Removes the first occurrence of text from the buffer starting from a given index.
/// @param buf Pointer to the StringBuffer.
/// @param text Null-terminated string to remove.
/// @param from Index to start the search from.
void StringBuffer_remove(StringBuffer *buf, const char *text, size_t from);

/// @brief Replaces the first occurrence of a substring with another string, starting from a given index.
/// @param buf Pointer to the StringBuffer.
/// @param original Substring to be replaced.
/// @param update Replacement string.
/// @param from Index to start the search from.
void StringBuffer_replace(StringBuffer *buf, const char *original,
                          const char *update, size_t from);

// Search &amp; Split

/// @brief Finds the index of the first occurrence of text starting from a given index.
/// @param buf Pointer to the StringBuffer.
/// @param text Substring to search for.
/// @param from Index to start the search from.
/// @return The index of the first occurrence, or -1 if not found.
int StringBuffer_index_of(const StringBuffer *buf, const char *text,
                          size_t from);

/// @brief Finds all occurrences of a substring starting from a given index.
/// @param buf Pointer to the StringBuffer.
/// @param text Substring to match.
/// @param from Index to start the search from.
/// @return A pointer to a MatchResult containing all match positions.
MatchResult *StringBuffer_match_all(const StringBuffer *buf, const char *text,
                                    size_t from);

/// @brief Splits the buffer into parts using the given delimiter.
/// @param buf Pointer to the StringBuffer.
/// @param delimiter String delimiter to split by.
/// @return A pointer to a SplitResult containing the parts.
SplitResult *StringBuffer_split(const StringBuffer *buf, const char *delimiter);

// Cleanup

/// @brief Frees the memory used by a MatchResult.
/// @param matches Pointer to the MatchResult to free.
void MatchResult_free(MatchResult *matches);

/// @brief Frees the memory used by a SplitResult.
/// @param split Pointer to the SplitResult to free.
void SplitResult_free(SplitResult *split);

#endif
</code></pre>
<h2>Implementation</h2>
<pre><code class="language-c">#include &quot;string_buffer.h&quot;

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

#define INITIAL_CAPACITY 16

static int ensure_capacity(StringBuffer *buf, size_t required) {
  if (required &lt;= buf-&gt;capacity)
    return 1;

  size_t new_capacity = buf-&gt;capacity ? buf-&gt;capacity : INITIAL_CAPACITY;
  while (new_capacity &lt; required) {
    new_capacity *= 2;
  }

  char *new_data = realloc(buf-&gt;data, new_capacity);
  if (!new_data) {
    perror(&quot;realloc&quot;);
    return 0;
  }

  buf-&gt;data = new_data;
  buf-&gt;capacity = new_capacity;
  return 1;
}

StringBuffer *StringBuffer_init() {
  StringBuffer *buf = malloc(sizeof(StringBuffer));
  if (!buf) {
    perror(&quot;malloc&quot;);
    return NULL;
  }

  buf-&gt;size = 0;
  buf-&gt;capacity = 0;
  buf-&gt;data = NULL;
  return buf;
}

void StringBuffer_free(StringBuffer *buf) {
  if (!buf)
    return;
  free(buf-&gt;data);
  free(buf);
}

void StringBuffer_clear(StringBuffer *buf) {
  if (!buf || !buf-&gt;data)
    return;
  buf-&gt;data[0] = '\0';
  buf-&gt;size = 0;
}

void StringBuffer_print(const StringBuffer *buf) {
  if (!buf || !buf-&gt;data)
    return;
  printf(&quot;size: %zu\n&quot;, buf-&gt;size);
  printf(&quot;data: %s\n&quot;, buf-&gt;data);
}

void StringBuffer_append(StringBuffer *buf, const char *text) {
  if (!buf || !text)
    return;

  size_t text_len = strlen(text);
  if (!ensure_capacity(buf, buf-&gt;size + text_len + 1))
    return;

  memcpy(buf-&gt;data + buf-&gt;size, text, text_len + 1);
  buf-&gt;size += text_len;
}

void StringBuffer_prepend(StringBuffer *buf, const char *text) {
  if (!buf || !text)
    return;

  size_t text_len = strlen(text);
  if (!ensure_capacity(buf, buf-&gt;size + text_len + 1))
    return;

  memmove(buf-&gt;data + text_len, buf-&gt;data,
          buf-&gt;size + 1);
  memcpy(buf-&gt;data, text, text_len);
  buf-&gt;size += text_len;
}

int StringBuffer_index_of(const StringBuffer *buf, const char *text,
                          size_t from) {
  if (!buf || !text || !buf-&gt;data || from &gt;= buf-&gt;size)
    return -1;

  char *match = strstr(buf-&gt;data + from, text);
  return match ? (int)(match - buf-&gt;data) : -1;
}

MatchResult *StringBuffer_match_all(const StringBuffer *buf, const char *text,
                                    size_t from) {
  if (!buf || !text || !buf-&gt;data || from &gt;= buf-&gt;size)
    return NULL;

  MatchResult *matches = malloc(sizeof(MatchResult));
  if (!matches)
    return NULL;
  matches-&gt;positions = NULL;
  matches-&gt;count = 0;

  int index = StringBuffer_index_of(buf, text, from);
  while (index != -1) {
    size_t *new_positions =
        realloc(matches-&gt;positions, sizeof(size_t) * (matches-&gt;count + 1));
    if (!new_positions) {
      MatchResult_free(matches);
      return NULL;
    }

    matches-&gt;positions = new_positions;
    matches-&gt;positions[matches-&gt;count++] = index;
    index = StringBuffer_index_of(buf, text, index + 1);
  }

  return matches;
}

void StringBuffer_remove(StringBuffer *buf, const char *text, size_t from) {
  if (!buf || !text || !buf-&gt;data || from &gt;= buf-&gt;size)
    return;

  size_t text_len = strlen(text);
  int index = StringBuffer_index_of(buf, text, from);

  while (index != -1) {
    size_t tail_len = buf-&gt;size - (index + text_len);
    memmove(buf-&gt;data + index, buf-&gt;data + index + text_len, tail_len + 1);
    buf-&gt;size -= text_len;
    index = StringBuffer_index_of(buf, text, index);
  }
}

void StringBuffer_replace(StringBuffer *buf, const char *original,
                          const char *update, size_t from) {
  if (!buf || !original || !update || !buf-&gt;data || from &gt;= buf-&gt;size)
    return;

  size_t original_len = strlen(original);
  size_t update_len = strlen(update);
  if (original_len == 0 || update_len == (size_t)-1)
    return;

  int index = StringBuffer_index_of(buf, original, from);

  while (index != -1) {
    if (update_len &gt; original_len) {
      if (!ensure_capacity(buf, buf-&gt;size + (update_len - original_len) + 1))
        return;
    }

    size_t tail_len = buf-&gt;size - (index + original_len);
    memmove(buf-&gt;data + index + update_len, buf-&gt;data + index + original_len,
            tail_len + 1);
    memcpy(buf-&gt;data + index, update, update_len);
    buf-&gt;size = buf-&gt;size - original_len + update_len;

    index = StringBuffer_index_of(buf, original, index + update_len);
  }
}

SplitResult *StringBuffer_split(const StringBuffer *buf,
                                const char *delimiter) {
  if (!buf || !delimiter || !buf-&gt;data)
    return NULL;

  char *copy = strdup(buf-&gt;data);
  if (!copy)
    return NULL;

  SplitResult *result = malloc(sizeof(SplitResult));
  if (!result) {
    free(copy);
    return NULL;
  }

  result-&gt;parts = NULL;
  result-&gt;count = 0;

  char *token = strtok(copy, delimiter);
  while (token) {
    char **tmp = realloc(result-&gt;parts, sizeof(char *) * (result-&gt;count + 1));
    if (!tmp) {
      SplitResult_free(result);
      free(copy);
      return NULL;
    }
    result-&gt;parts = tmp;
    result-&gt;parts[result-&gt;count] = strdup(token);
    if (!result-&gt;parts[result-&gt;count]) {
      SplitResult_free(result);
      free(copy);
      return NULL;
    }
    result-&gt;count++;
    token = strtok(NULL, delimiter);
  }

  free(copy);
  return result;
}

void MatchResult_free(MatchResult *matches) {
  if (!matches)
    return;
  free(matches-&gt;positions);
  free(matches);
}

void SplitResult_free(SplitResult *split) {
  if (!split)
    return;
  for (size_t i = 0; i &lt; split-&gt;count; i++) {
    free(split-&gt;parts[i]);
  }
  free(split-&gt;parts);
  free(split);
}
</code></pre>
<h2>Tests</h2>
<pre><code class="language-c">#include &quot;string_buffer.h&quot;

#include &lt;assert.h&gt;
#include &lt;string.h&gt;

int main(void) {
  StringBuffer *buf = StringBuffer_init();
  assert(buf != NULL);

  StringBuffer_append(buf, &quot;hello&quot;);
  assert(buf-&gt;size == 5);
  assert(strcmp(buf-&gt;data, &quot;hello&quot;) == 0);

  StringBuffer_append(buf, &quot; world&quot;);
  assert(buf-&gt;size == 11);
  assert(strcmp(buf-&gt;data, &quot;hello world&quot;) == 0);

  assert(StringBuffer_index_of(buf, &quot;hello&quot;, 0) == 0);
  assert(StringBuffer_index_of(buf, &quot;world&quot;, 0) == 6);
  assert(StringBuffer_index_of(buf, &quot;mars&quot;, 0) == -1);
  assert(StringBuffer_index_of(buf, &quot;llo&quot;, 2) == 2);
  assert(StringBuffer_index_of(buf, &quot;hello&quot;, 99) == -1);
  assert(StringBuffer_index_of(buf, &quot;hello&quot;, buf-&gt;size) == -1);
  assert(StringBuffer_index_of(buf, &quot;d&quot;, buf-&gt;size - 1) ==
         (int)(buf-&gt;size - 1));
  assert(StringBuffer_index_of(buf, &quot;h&quot;, 1) == -1);

  MatchResult *mr = StringBuffer_match_all(buf, &quot;l&quot;, 0);
  if (!mr) {
    return -1;
  }
  assert(mr-&gt;count == 3);
  assert(mr-&gt;positions[0] == 2);
  assert(mr-&gt;positions[1] == 3);
  assert(mr-&gt;positions[2] == 9);
  MatchResult_free(mr);

  SplitResult *sr = StringBuffer_split(buf, &quot; &quot;);
  assert(sr-&gt;count == 2);
  assert(strcmp(sr-&gt;parts[0], &quot;hello&quot;) == 0);
  assert(strcmp(sr-&gt;parts[1], &quot;world&quot;) == 0);
  SplitResult_free(sr);

  StringBuffer_prepend(buf, &quot;hello &quot;);
  assert(buf-&gt;size == 17);
  assert(strcmp(&quot;hello hello world&quot;, buf-&gt;data) == 0);

  StringBuffer_remove(buf, &quot; &quot;, 0);
  assert(buf-&gt;size == 15);
  assert(strcmp(&quot;hellohelloworld&quot;, buf-&gt;data) == 0);

  StringBuffer *remove_overlap_test = StringBuffer_init();
  StringBuffer_append(remove_overlap_test, &quot;lalalalala&quot;);
  StringBuffer_remove(remove_overlap_test, &quot;ala&quot;, 0);
  assert(strcmp(remove_overlap_test-&gt;data, &quot;llla&quot;) == 0);
  assert(remove_overlap_test-&gt;size == 4);
  StringBuffer_free(remove_overlap_test);

  StringBuffer_replace(buf, &quot;hellohello&quot;, &quot;hello &quot;, 0);
  assert(buf-&gt;size == 11);
  assert(strcmp(&quot;hello world&quot;, buf-&gt;data) == 0);

  StringBuffer *replace_overlap_test = StringBuffer_init();
  StringBuffer_append(replace_overlap_test, &quot;lalalalala&quot;);
  StringBuffer_replace(replace_overlap_test, &quot;ala&quot;, &quot;&quot;, 0);
  assert(strcmp(replace_overlap_test-&gt;data, &quot;llla&quot;) == 0);
  assert(replace_overlap_test-&gt;size == 4);
  StringBuffer_free(replace_overlap_test);

  StringBuffer_clear(buf);
  assert(buf-&gt;size == 0);
  assert(strlen(buf-&gt;data) == 0);

  StringBuffer_free(buf);
  return 0;
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Use Codium not Code]]></title>
      <link>https://briandouglas.ie/use-codium</link>
      <guid>https://briandouglas.ie/use-codium</guid>
      <pubDate>2025-07-14T00:00:00.000Z</pubDate>
      <description><![CDATA[Most people I see coding these days use Visual Studio Code. But Visual Studio Code is
a Microsoft project that tracks your usage and does all sorts of evil corporation stuff.

But don&#x27;t worry there is an actual open-source version called VSCodium. It works just as
well as the Microsoft version, without stealing your soul.

]]></description>
      <content:encoded><![CDATA[
        <p>All the details you need can be found on the <a href="https://vscodium.com/#why">VSCodium web page</a>
and while you are at it you should also star the repo <a href="https://github.com/VSCodium/vscodium">VSCodium/vscodium</a>.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Get inode type in C]]></title>
      <link>https://briandouglas.ie/inode-type-c</link>
      <guid>https://briandouglas.ie/inode-type-c</guid>
      <pubDate>2025-07-10T00:00:00.000Z</pubDate>
      <description><![CDATA[Here is a string representation of a path. But tell me, is it a directory? is it a file? maybe it&#x27;s a symlink?
perhaps there is nothing there at all.

]]></description>
      <content:encoded><![CDATA[
        <p>Well below is a helper method to find out exactly what it is.</p>
<p><magpie-trinket>The below method is called <code>get_inode_type</code> and uses a struct called <code>inode_type</code>. <code>inode</code> stands for <em>Index Node</em>.
An <em>Index Node</em> is used to store information about a file, directory, etc.. at a certain path.</magpie-trinket></p>
<p>The <code>get_inode_type</code> method below will return a <code>struct inode_type</code> if the path points to an existing inode. If
not it will return <code>NULL</code>. As you will see in the test cases the <code>inode_type</code> struct describes whether or not
the path points to a directory, file, or symlink. It also tells you if the symlink points to a directory or
file. Which is good to know.</p>
<h2>Header</h2>
<pre><code class="language-c">#ifndef INODE_TYPE_H
#define INODE_TYPE_H

#include &lt;stddef.h&gt;
typedef const char *Path;

typedef struct {
    int is_file;
    int is_dir;
    int is_link;
} inode_type;

inode_type *get_inode_type(Path path);

#define INODE_TYPE_H
</code></pre>
<h2>Implementation</h2>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;sys/stat.h&gt;
#include &quot;inode_type.h&quot;

inode_type *get_inode_type(Path path) {
    inode_type *type = malloc(sizeof(inode_type));
    if(type == NULL) {
        perror(&quot;malloc&quot;);
        return NULL;
    }

    struct stat link_stat;
    if(lstat(path, &amp;link_stat) == -1) {
        perror(&quot;lstat&quot;);
        free(type);
        return NULL;
    }

    type-&gt;is_link = S_ISLNK(link_stat.st_mode);

    struct stat st;
    if(stat(path, &amp;st) == -1) {
        perror(&quot;stat&quot;);
        free(type);
        return NULL;
    }

    type-&gt;is_dir = S_ISDIR(st.st_mode);
    type-&gt;is_file = S_ISREG(st.st_mode);

    return type;
}
</code></pre>
<h2>Usage</h2>
<pre><code class="language-c">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;assert.h&gt;
#include &quot;inode_type.h&quot;

int main() {
  inode_type *test_dir = get_inode_type(&quot;./example_files&quot;);
  inode_type *test_file = get_inode_type(&quot;./example_files/hello_world.txt&quot;);
  inode_type *test_link = get_inode_type(&quot;./example_files/slink&quot;);
  inode_type *test_empty = get_inode_type(&quot;/does/not/exist&quot;);

  assert(test_dir-&gt;is_dir == 1);
  assert(test_dir-&gt;is_file == 0);
  assert(test_dir-&gt;is_link == 0);

  assert(test_file-&gt;is_dir == 0);
  assert(test_file-&gt;is_file == 1);
  assert(test_file-&gt;is_link == 0);

  assert(test_link-&gt;is_dir == 0);
  assert(test_link-&gt;is_file == 1);
  assert(test_link-&gt;is_link == 1);

  assert(test_empty == NULL);

  free(test_dir);
  free(test_file);
  free(test_link);
  free(test_empty);

  return 0;
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Handle Spring form errors with JTE]]></title>
      <link>https://briandouglas.ie/spring-form-errors-with-jte</link>
      <guid>https://briandouglas.ie/spring-form-errors-with-jte</guid>
      <pubDate>2025-06-17T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve been working with JTE (Java Template Engine) templates in my latest Spring app. They are a joy to work with and
feel like a more _modern_ alternative to Thymeleaf templates. With Spring however there are some integrations that a
Thymeleaf user would expect, that are missing when using JTE. Automatic form error association being one. So I have
documented how to pass form errors to your jte templates below.

]]></description>
      <content:encoded><![CDATA[
        <h2>The problem</h2>
<p>Let's say we have a <code>RegistrationForm</code> class, that has various validator annotations. When one of these validators fails
we expect the <code>RegistrationController</code> to return the registration form view with the errors present, such as the below
example.</p>
<pre><code class="language-java">@Controller
@AllArgsConstructor
@RequestMapping(&quot;/register&quot;)
public class RegistrationController {

    private static final String FORM_VIEW = &quot;auth/register&quot;;
    private static final String ATTR_PAGE = &quot;page&quot;;

    @GetMapping
    public String showRegisterForm(Model model) {
        model.addAttribute(ATTR_PAGE, new Page(&quot;Register&quot;));
        model.addAttribute(ATTR_REGISTRATION_FORM, new RegistrationForm());
        return FORM_VIEW;
    }

    @PostMapping
    public String processRegisterForm(@ModelAttribute @Validated RegistrationForm registrationForm,
                                      BindingResult result,
                                      Model model) {

        model.addAttribute(ATTR_PAGE, new Page(&quot;Register&quot;));
        model.addAttribute(ATTR_REGISTRATION_FORM, registrationForm);

        if (result.hasErrors()) {
            return FORM_VIEW;
        }

        // ...
    }
}
</code></pre>
<p>In the above example, when the <code>BindingResult</code> has errors the user is taken back to the registration form page. If we
were using Thymeleaf, the validation errors would be auto attached to the view and can be rendered with special syntax.</p>
<p><magpie-trinket>You can read more about how Thymeleaf handles form errors
at <a href="https://www.baeldung.com/spring-thymeleaf-error-messages">spring-thymeleaf-error-messages</a></magpie-trinket></p>
<p>Jte does not support this type of integration. This is a good thing, as it allows the developer to handle the form field
validation how they see fit. Before I go into how I bind the form errors via the controller, here is the
<code>RegistrationForm</code> class.</p>
<pre><code class="language-java">@Getter
@Setter
public class RegistrationForm {
    @NotBlank(message = &quot;Email address is required&quot;)
    @Email(message = &quot;Invalid email format&quot;)
    private String email;

    @NotBlank(message = &quot;Password is required&quot;)
    @Size(min = 8, max = 250, message = &quot;Password must be a minimum of 8 characters in length&quot;)
    private String password;
    @NotBlank(message = &quot;Confirm password is required&quot;)
    private String confirmPassword;

    @NotBlank(message = &quot;First name is required&quot;)
    @Size(min = 1, message = &quot;First name must be between 1 and 50 characters in length&quot;)
    private String firstName;

    @NotBlank(message = &quot;Last name is required&quot;)
    @Size(min = 3, message = &quot;Last name must be between 3 and 50 characters in length&quot;)
    private String lastName;
}
</code></pre>
<p>From this you can guess that when there is an email format issue we want to show the appropriate message to our user on
the frontend. Naturally this is the case for all the other fields also. Here is how I render the error message in the
jte template.</p>
<pre><code class="language-html">@param ValidationHelper validation
@param RegistrationForm registrationForm

@if(validation.hasErrors())
    &lt;div class=&quot;form-error&quot;&gt;Please correct the following errors to proceed.&lt;/div&gt;
@endif

&lt;label&gt;Email Address
    &lt;input name=&quot;email&quot; type=&quot;email&quot; value=&quot;${registrationForm.getEmail()}&quot; required&gt;
    @if(validation.hasError(&quot;email&quot;))
        &lt;small class=&quot;error-text&quot;&gt;${validation.getError(&quot;email&quot;).getDefaultMessage()}&lt;/small&gt;
    @endif
&lt;/label&gt;
</code></pre>
<p><chicken-asks>But wait! where is the <code>ValidationHelper</code> coming from?</chicken-asks>
<magpie-replies>Eagle eyed again, he shows us how to add it below</magpie-replies></p>
<p>You'll notice that I have added a <code>ValidationHelper</code> param. I will show you how this is implemented below. But as you
can see, we are going to use this to render our form errors. The api should be self-explanatory <code>hasErrors</code>, tells us
if there are any errors present, <code>hasError</code> tells us if a specific error is present, and <code>getError</code> give us the details
of the specific error.</p>
<h2>ValidationHelper</h2>
<p>The <code>ValidationHelper</code> class itself is just a wrapper around the <code>BindingResult</code> which we will pass in via the
controller. I've added the <code>@NoArgsConstructor</code> so that is can be instantiated without a <code>BindingResult</code>. The null
checks mean that this is safe to use when a <code>BindingResult</code> is not present.</p>
<pre><code class="language-java">@NoArgsConstructor
@AllArgsConstructor
public class ValidationHelper {
    private BindingResult bindingResult;

    public boolean hasErrors() {
        return bindingResult != null &amp;&amp; bindingResult.hasErrors();
    }

    public boolean hasError(String name) {
        return bindingResult != null &amp;&amp; bindingResult.hasFieldErrors(name);
    }

    public List&lt;FieldError&gt; getErrors() {
        if (bindingResult == null) {
            return Collections.emptyList();
        } else {
            return bindingResult.getFieldErrors();
        }
    }

    public FieldError getError(String fieldName) {
        if (bindingResult == null) {
            return null;
        } else {
            return bindingResult.getFieldError(fieldName);
        }
    }
}
</code></pre>
<p>The updated <code>RegistrationController</code> is below. The example registration form jte template does not need to be updated.</p>
<pre><code class="language-java">@Controller
@AllArgsConstructor
@RequestMapping(&quot;/register&quot;)
public class RegistrationController {

    private static final String FORM_VIEW = &quot;auth/register&quot;;
    private static final String ATTR_PAGE = &quot;page&quot;;
    private static final String ATTR_VALIDATION = &quot;validation&quot;;

    @GetMapping
    public String showRegisterForm(Model model) {
        model.addAttribute(ATTR_PAGE, new Page(&quot;Register&quot;));
        model.addAttribute(ATTR_REGISTRATION_FORM, new RegistrationForm());
        model.addAttribute(ATTR_VALIDATION, new ValidationHelper());
        return FORM_VIEW;
    }

    @PostMapping
    public String processRegisterForm(@ModelAttribute @Validated RegistrationForm registrationForm,
                                      BindingResult result,
                                      Model model) {

        model.addAttribute(ATTR_PAGE, new Page(&quot;Register&quot;));
        model.addAttribute(ATTR_REGISTRATION_FORM, registrationForm);
        model.addAttribute(ATTR_VALIDATION, new ValidationHelper(result));

        if (result.hasErrors()) {
            return FORM_VIEW;
        }

        // ...
    }
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Add a CSRF token to HTML forms with JTE]]></title>
      <link>https://briandouglas.ie/jte-csrf</link>
      <guid>https://briandouglas.ie/jte-csrf</guid>
      <pubDate>2025-06-16T00:00:00.000Z</pubDate>
      <description><![CDATA[When submitting information via an HTML form, Spring Security requires the form to include a CSRF (Cross-Site Request
Forgery) token. This token is generated by the Spring app and tied to the user&#x27;s session. Spring will use it to verify
that the form submission is legitimate and not malicious. The token is required on POST, PUT, DELETE, and PATCH requests
to protect against CSRF attacks. So when your user is submitting information, the form used to submit said information 
will need to contain a valid csrf token. This means adding a hidden field to the form that contains the token. In this
post I&#x27;ll demonstrate my reusable solution to this when using Spring and JTE. 

]]></description>
      <content:encoded><![CDATA[
        <h2>Adding the hidden field</h2>
<p>In the below example logout form jte template, the csrf param is used to create the hidden field. In this scenario the
controller will be required to set the <code>csrf</code> param as a model attribute.</p>
<pre><code class="language-html">@import org.springframework.security.web.csrf.CsrfToken

@param CsrfToken csrf

&lt;form action=&quot;/logout&quot; method=&quot;post&quot;&gt;
    &lt;input type=&quot;hidden&quot; name=&quot;${csrf.parameterName}&quot; value=&quot;${csrf.token}&quot;/&gt;
    &lt;input type=&quot;submit&quot; value=&quot;Log out&quot; /&gt;
&lt;/form&gt;
</code></pre>
<p>But the question is, how can I make it easy to add this to all my forms?
And how can I do it without having to pass the token to the view in each controller method?</p>
<p>The solution is to create a JTE template that can be included in all forms. This template will read the csrf token,
which will be supplied via a <code>@ControllerAdvice</code> annotated class on each request. The result will be a JTE template that
can be used like so:</p>
<pre><code class="language-html">@import org.springframework.security.web.csrf.CsrfToken

@param CsrfToken csrf

&lt;form action=&quot;/logout&quot; method=&quot;post&quot;&gt;
    @template.components.csrf(csrf = csrf)
    &lt;input type=&quot;submit&quot; value=&quot;Log out&quot; /&gt;
&lt;/form&gt;
</code></pre>
<h2>Implementation</h2>
<p>To make the csrf component work without passing the csrf token to each view model, we will create a class with
a <code>@ControllerAdvice</code> annotation. This will allow us to annotate a method with <code>@ModelAttribute</code> to set the <code>csrf</code>
param.</p>
<pre><code class="language-java">@ControllerAdvice
public class CsrfControllerAdvice {
    @ModelAttribute
    public void addCsrfToken(Model model, CsrfToken token) {
        model.addAttribute(&quot;csrf&quot;, token);
    }
}
</code></pre>
<p>The csrf jte template will look as follows.</p>
<pre><code class="language-html">@import org.springframework.security.web.csrf.CsrfToken

@param CsrfToken csrf

@if(csrf != null)
    &lt;input type=&quot;hidden&quot; name=&quot;${csrf.getParameterName()}&quot; value=&quot;${csrf.getToken()}&quot;&gt;
@endif
</code></pre>
<p>The one downside with this is we will still have to pass the <code>csrf</code> value to the template as a parameter. The upside is
the controller does not have to be updated to add the model attribute due to the <code>CsrfControllerAdvice</code> class.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Spring database seeding]]></title>
      <link>https://briandouglas.ie/spring-database-seeding</link>
      <guid>https://briandouglas.ie/spring-database-seeding</guid>
      <pubDate>2025-05-29T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;m working on a website that functions as an index of trades people in Ireland. It allows users to register a trade
profile detailing their services. This trade profile can then be reviewed by other users. I have this functionality
pretty much built out. But until recently I had very little test data, which lead me on a journey into the world of
database seeding. Which is the generation of test data that mimics real world data. Here&#x27;s how I got on.

]]></description>
      <content:encoded><![CDATA[
        <h2>The goal</h2>
<p>I wanted to generate test data that was similar to what would be seen in a real world scenario. A bunch of <em>lorem ipsum</em>
wouldn't have been good enough. This is where the <a href="https://github.com/DiUS/java-faker">Faker</a> module came into play.</p>
<p>Faker allows for the generation of realistic data. For example if you want to generate a user's details using Faker, it
would look something like the following.</p>
<pre><code class="language-java">Faker faker = new Faker();

String firstName = faker.name().firstName();     // John
String lastName = faker.name().lastName();       // Wayne
String email = faker.internet().emailAddress();  // rooster.cogburn@gmail.com
</code></pre>
<p>Faker generates data for many scenarios, 79 domains as of writing, and facilitated my need for realistic looking text.</p>
<p>But I didn't just want to generate the text, I wanted to store it in the database. This is where things got complicated.
The Spring pattern that I usually follow for data loading is to create a <code>DataLoader</code> component class, which extends the
<code>CommandLinerRunner</code> class. I then add some logic in the <code>run</code> method to populate my dev database. Usually this means
generating one off objects to persist in the database via a big loop. Long story short, there was no particular method
to the madness. That was until recently when I learned
of <a href="https://laravel.com/docs/11.x/seeding">Laravel's approach to seeding via factory classes</a>.</p>
<p>Laravel is a PHP framework that has some neat ideas around <em>eloquent</em> code. Which basically means developer experience.
The gist of Laravel's approach to seeding was to use factories with overwritable <em>blueprints</em> to generate entity
objects. Which is a good idea. The Laravel implementation however seemed a little overcomplicated to me. With many
helper methods that could only be implemented due the looseness of PHP.</p>
<p>My goal was to implement the same functionality in java but with a much more limited api. I wanted an abstract base
class, <code>BlueprintFactory&lt;T&gt;</code>, which could be extended to allow the creation of objects based off a <em>blueprint</em>. I also
wanted to limit the caller api to two methods. These methods being <code>with</code> and <code>create</code>. <code>create</code> will be used to create
the object, and <code>with</code> will be used to supply custom setters. The latter being useful when dealing with related
entities. With this goal in mind, I set to work.</p>
<h2>Usage</h2>
<p>Before implementing something like this, I like to start of by thinking about how the consumer code should look. So I
start with how I want to use it. Allowing me to have a better idea of how to implement it. My initial thought was to
have an abstract <code>BlueprintFactory&lt;T&gt;</code> class that would force the extending class to implement a
<code>abstract T blueprint();</code> method. In the case of a <code>UserFactory</code> class that might look like the following.</p>
<pre><code class="language-java">
@Component
@AllArgsConstructor
public class UserFactory extends BlueprintFactory&lt;AppUser&gt; {
    private final Faker faker;
    private final PasswordEncoder passwordEncoder;

    protected AppUser blueprint() {
        var user = new AppUser();
        user.setEmail(faker.internet().emailAddress());
        user.setPassword(passwordEncoder.encode(&quot;password&quot;));
        return user;
    }
}
</code></pre>
<p>The above is clean and clearly gives it's intent. It's also nice and concise as the parent class will implement the
<code>with</code> and <code>create</code> methods. The <code>blueprint</code> method will be called each time an object is created. This means that each
object will have it's data generated by faker.</p>
<p>I'm happy with this approach to creating the factories. So then I look at using the factories in a seeder class.</p>
<p>The seeder will be the class that stores the objects created via the factories in a database. So I write out how I want
my <code>UserSeeder</code> to look. In my case the <code>AppUser</code> entity also has a one to one relationship with a <code>UserProfile</code> entity.
So I'll need to create and store that along with the <code>AppUser</code>. It's a good idea to give each entity its own factory.
So I create the following <code>UserProfileFactory</code>.</p>
<pre><code class="language-java">
@Component
@AllArgsConstructor
public class UserProfileFactory extends BlueprintFactory&lt;UserProfile&gt; {
    private final Faker faker;

    @Override
    protected UserProfile blueprint() {
        UserProfile userProfile = new UserProfile();
        userProfile.setFirstName(faker.name().firstName());
        userProfile.setLastName(faker.name().lastName());
        return userProfile;
    }
}
</code></pre>
<p><magpie-trinket>It's also a good idea not to call one factory from inside another.
You may be tempted to create the <code>UserProfile</code> inside the <code>UserFactory</code> blueprint. This is better handled inside the
seeder, where the related entity can be passed during object creation via the <code>with</code> method.</magpie-trinket></p>
<p>With the <code>UserFactory</code> and <code>UserProfileFactory</code> set up. I can now go about writing the seeder. The <code>UserSeeder</code> will
create the <code>AppUser</code> and <code>UserProfile</code> entities using the factories we created. It will also handle the relationship
assignments. For example, we will want to set the <code>AppUser</code> that owns the <code>UserProfile</code> when it is created, using the
<code>with</code> method. The <code>with</code> method will return a new instance of the factory, <em>with</em> the custom setter in place, to allow
for composability. The <code>UserSeeder</code> should look as follows.</p>
<pre><code class="language-java">
@Order(1)
@Component
@Profile(&quot;dev&quot;)
@RequiredArgsConstructor
public class UserSeeder implements CommandLineRunner {
    private final UserFactory userFactory;
    private final AppUserRepository appUserRepository;
    private final UserProfileFactory userProfileFactory;
    private final UserProfileRepository userProfileRepository;

    @Override
    @Transactional
    public void run(String... args) {
        if (appUserRepository.count() &gt; 0) {
            return;
        }

        var savedUsers = appUserRepository.saveAll(userFactory.create(50));

        var userProfiles = new ArrayList&lt;UserProfile&gt;();
        for (var user : savedUsers) {
            userProfiles.add(userProfileFactory.with(() -&gt; user, UserProfile::setUser).create());
        }

        userProfileRepository.saveAll(userProfiles);
    }
}
</code></pre>
<p>In this class we create 50 <code>AppUser</code> entities, by passing <code>50</code> to the <code>create</code> method. I then assign a <code>UserProfile</code> to
each of those. All of which are persisted to the database.</p>
<p>An area of note is the following for loop.</p>
<pre><code class="language-java">for(var user : savedUsers){
    userProfiles.add(
            userProfileFactory.with(() -&gt;user,UserProfile::setUser).create()
    );
}
</code></pre>
<p>Here we loop through the already saved users, creating a <code>UserProfile</code> for each one. The interesting part is how the
user is passed to the user profile. The <code>with</code> method adds a custom setter. Meaning that <code>UserProfile::setUser</code> will be
called, and the result of <code>() -&gt; user</code> will be passed to it. Allowing that <code>AppUser</code> instance to be set on the newly
created <code>UserProfile</code>.</p>
<p>Some other things to note here is that I add an <code>@Order</code> annotation to specify I want this seeder to run first. As well
as the <code>@Profile</code> annotation to say I want it only to run in dev. I also firstly check if there are any users in the
database before running the seeder. This is to ensure the database is only seeded when empty.</p>
<p>You may want to create a <em>test</em> user in your seeder. YOu can do this by passing a <em>variant</em> to the create method. This
will overwrite the specified fields.</p>
<pre><code class="language-java">AppUser testUser = appUserRepository.save(userFactory.create(Map.of(&quot;email&quot;, &quot;test@test.com&quot;)));
</code></pre>
<p>The above creates a test user with the email &quot;test@test.com&quot;. The factory reads the keys of the map and overwrites the
matching value on the blueprint.</p>
<h2>Implementation</h2>
<p>The implementation comes down to three main areas.</p>
<ul>
<li><code>BlueprintFactory&lt;T&gt;</code>: the extensible base</li>
<li><code>with</code>: allowing composable factory instances</li>
<li><code>create</code>: construction of objects</li>
</ul>
<p>And there is a lot to go through in each. So I will keep it brief.</p>
<h3><code>BlueprintFactory&lt;T&gt;</code></h3>
<p>This is an abstract class that forces the extender to implement a <code>abstract T blueprint();</code> method. This method will get
called each time an object is created. The result of which will then have any custom setters applied, before being
passed to an object mapper which will apply any overwrites specified in the <code>create</code> method call.</p>
<p>So for clarity if a custom setter is added. It will be applied before the variant/overwrite is applied via the <code>create</code>
method.</p>
<h3><code>with</code></h3>
<p>This method exists to handle the setting of related entities. As I stated earlier it's good practice to not create any
related entities inside your factory. The setting of related entities should be handled solely by the <code>with</code> method.</p>
<p>Let's take the example of a <code>User</code> entity that can own many <code>Post</code> entities. In our seeder we may want to create the
<code>User</code> entity first, then apply that <code>User</code> to a bunch of <code>Post</code> entities when they are created. The <code>with</code> method
can specify the <code>Post::setUser</code> setter always set the supplied <code>User</code> like so:</p>
<pre><code class="language-java">var myUserPosts = postFactory.with(() -&gt; myUser, Post::setUser).create(50);
</code></pre>
<p>The above will create 50 posts belonging to <code>myUser</code>. As the <code>with</code> method returns a new instance of the factory with
the custom setter applied it can also be made composable like so:</p>
<pre><code class="language-java">var myUserFactory = postFactory.with(() -&gt; myUser, Post::setUser);
var myUserPosts = myUserFactory.create(50);
</code></pre>
<p>This is achieved by an internal constructor, <code>protected BlueprintFactory(List&lt;Consumer&lt;T&gt;&gt; customSetters)</code>, in which
a copy of the factory's custom setters is passed to a new instance of said factory. That new instance is then returned.
This allows for the construction of composable factories.</p>
<p>Inorder to facilitate applying a single entity and a list of entities method overloads are used, to match the type of
the supplied argument.</p>
<ul>
<li><code>with(Supplier&lt;R&gt;, BiConsumer&lt;T, R&gt;)</code>: Injects a single related object</li>
<li><code>with(Function&lt;Integer, List&lt;R&gt;&gt;, int, BiConsumer&lt;T, List&lt;R&gt;&gt;)</code>: Injects a list of related objects</li>
<li><code>with(Function&lt;V, R&gt;, V, BiConsumer&lt;T, R&gt;)</code>: Injects a value based on a variant</li>
</ul>
<p>I made a big effort here with these overloads to keep the public api as simple as possible but still allow for
flexibility.</p>
<h3><code>create</code></h3>
<p>The <code>create</code> method is self-explanatory. It creates the new object based of the object returned via the <code>blueprint</code>
method. It will call <code>blueprint</code>, then it will apply any custom setters to the return value of blueprint, before
passing the modified object to an object mapper which will merge any overwrites that have been passed to the <code>create</code>
method call.</p>
<p>Similar to the <code>with</code> method there are a number of overloads to allow for different usages of <code>create</code>.</p>
<ul>
<li><code>create()</code>: Builds a single instance using the blueprint and custom setters</li>
<li><code>create(Map&lt;String, ?&gt; variation)</code>: Applies field overrides using a map</li>
<li><code>create(T variation)</code>: Merges fields from an existing instance</li>
<li><code>create(int count)</code>:    Batch creation of identical instances</li>
<li><code>create(VariantList&lt;T&gt;)</code>: Batch creation using pre-defined entity variants</li>
<li><code>create(VariantMapList)</code>: Batch creation using field maps (variant by map)</li>
</ul>
<p>I was forced to create some custom types here to avoid ambiguity with the type checker. The was caused by allowing
both a <code>List&lt;Map&lt;String, ?&gt;&gt;</code> and a <code>List&lt;T&gt;</code> to be passed to <code>create</code> to allow the creation of multiple variants.
This is called a <code>Sequence</code> in the Laravel terminology. To get the type checker to allow this I had to create some
wrapper classes, <code>VariantMapList</code> and <code>VariantList&lt;T&gt;</code> to distinguish between the types. This creates some overhead
but until I can find a better solution. I will put up with it to keep the concise public api.</p>
<h2>The module</h2>
<p><code>BlueprintFactory</code> has been uploaded to maven central. You can read the docs and learn how to use it in your own
projects at <a href="https://github.com/BrianDouglasIE/BlueprintFactory">BrianDouglasIE/BlueprintFactory</a>.</p>
<p>Feel free to create a github issue with any suggested improvements.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[New Lib: Blueprint Factory]]></title>
      <link>https://briandouglas.ie/blueprintfactory-1.0.0</link>
      <guid>https://briandouglas.ie/blueprintfactory-1.0.0</guid>
      <pubDate>2025-05-28T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;m working on an app that requires a large amount of seed data. In order to achieve this I created a factory pattern
that mimics Laravel&#x27;s approach. This allows objects to be generated with ease and uses Faker to supply test data. I 
have extracted it into a package and uploaded it to Maven Central. I plan to use this approach in future projects. 
Details of the library are below.

]]></description>
      <content:encoded><![CDATA[
        <h1>Blueprint Factory</h1>
<p><img src="https://img.shields.io/maven-central/v/ie.briandouglas/BlueprintFactory" alt="Maven Central Version">
<a href="https://javadoc.io/doc/ie.briandouglas/BlueprintFactory"><img src="https://javadoc.io/badge2/ie.briandouglas/BlueprintFactory/javadoc.svg" alt="javadoc"></a></p>
<p>Create objects from reusable blueprints with flexible overrides.</p>
<h2>Overview</h2>
<p>Blueprint Factory simplifies creating instances of your classes by providing a base template (the <em>blueprint</em>) and allowing you to customize each created object with variations or related objects.
It's designed to be used along with <a href="https://github.com/DiUS/java-faker">Faker</a> to easily generate seed data.</p>
<h2>Features</h2>
<ul>
<li>Define a reusable blueprint for your object.</li>
<li>Override properties via instances or key-value maps.</li>
<li>Create single or multiple instances at once.</li>
<li>Easily set related entities with custom setters.</li>
<li>Clean, type-safe API with just two public methods: <code>with</code> and <code>create</code>.</li>
</ul>
<h2>Installation</h2>
<h3>Maven</h3>
<pre><code>&lt;dependency&gt;
    &lt;groupId&gt;ie.briandouglas&lt;/groupId&gt;
    &lt;artifactId&gt;BlueprintFactory&lt;/artifactId&gt;
    &lt;version&gt;1.0.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<h3>Gradle</h3>
<pre><code>implementation group: 'ie.briandouglas', name: 'BlueprintFactory', version: '1.0.0'
</code></pre>
<h2>Usage</h2>
<hr>
<h3>Define your Factory</h3>
<p>Extend the abstract <code>BlueprintFactory&lt;T&gt;</code> and implement the <code>blueprint()</code> method to provide a default instance:</p>
<pre><code class="language-java">public class UserFactory extends BlueprintFactory&lt;User&gt; {
    private final Faker faker = new Faker();

    @Override
    protected User blueprint () {
        return new User(faker.name().firstName(), faker.internet().emailAddress());
    }
}
</code></pre>
<p>When an object is created by a <code>BlueprintFactory</code> the <code>blueprint</code> method will be called. Once created
the object returned from the <code>blueprint</code> method will have any overrides applied. Because <code>blueprint</code> is
called for each <code>create</code>, it is a good idea to use a library like <code>Faker</code> to generate your blueprint's
data.</p>
<hr>
<h3>Creating Instances</h3>
<p><strong>Basic instance:</strong></p>
<pre><code class="language-java">User user = userFactory.create();
</code></pre>
<p><strong>Override fields with an instance:</strong></p>
<pre><code class="language-java">User override = new User(&quot;alice&quot;, &quot;alice@example.com&quot;);
User user = userFactory.create(override);
</code></pre>
<p><strong>Override fields with a map:</strong></p>
<pre><code class="language-java">User user = userFactory.create(Map.of(&quot;name&quot;, &quot;bob&quot;, &quot;email&quot;, &quot;bob@example.com&quot;));
</code></pre>
<p><strong>Create multiple instances:</strong></p>
<pre><code class="language-java">List&lt;User&gt; users = userFactory.create(5);
</code></pre>
<p><strong>Create from a list of variants:</strong></p>
<pre><code class="language-java">List&lt;User&gt; variants = List.of(
    new User(&quot;charlie&quot;, &quot;charlie@example.com&quot;),
    new User(&quot;dave&quot;, &quot;dave@example.com&quot;)
);
List&lt;User&gt; users = userFactory.create(new VariantList&lt;&gt;(variants));
</code></pre>
<p><strong>Create from a list of map variants:</strong></p>
<pre><code class="language-java">List&lt;VariantMap&gt; variantMaps = List.of(
    new VariantMap(Map.of(&quot;name&quot;, &quot;eve&quot;)),
    new VariantMap(Map.of(&quot;name&quot;, &quot;frank&quot;))
);
List&lt;User&gt; users = userFactory.create(new VariantMapList(variantMaps));
</code></pre>
<hr>
<h3>Setting Related Entities</h3>
<p><strong>With a single related object:</strong></p>
<pre><code class="language-java">User user = userFactory
    .with(roleFactory::create, User::setRole)
    .create();
</code></pre>
<p><strong>With multiple related objects:</strong></p>
<pre><code class="language-java">User user = userFactory
    .with(roleFactory::create, 3, User::setRoles)
    .create();
</code></pre>
<p><strong>With a variant passed to related factory:</strong></p>
<pre><code class="language-java">Role adminRole = new Role(&quot;admin&quot;);
User user = userFactory
    .with(roleFactory::create, adminRole, User::setRole)
    .create();
</code></pre>
<p><strong>With a list of variants:</strong></p>
<pre><code class="language-java">List&lt;Role&gt; roles = List.of(new Role(&quot;editor&quot;), new Role(&quot;viewer&quot;));
User user = userFactory
    .with(roleFactory::create, new VariantList&lt;&gt;(roles), User::setRoles)
    .create();
</code></pre>
<p><strong>With a list of map variants:</strong></p>
<pre><code class="language-java">List&lt;Map&lt;String, ?&gt;&gt; maps = List.of(
        Map.of(&quot;name&quot;, &quot;contributor&quot;),
        Map.of(&quot;name&quot;, &quot;maintainer&quot;)
);
User user = userFactory
        .with(roleFactory::create, new VariantMapList&lt;&gt;(maps), User::setRoles)
        .create();
</code></pre>
<hr>
<h2>LICENSE</h2>
<p>Copyright (c) 2025 Brian Douglas.<br>
See the LICENSE file for license rights and limitations.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[New Lib: Placeholder Image Generator]]></title>
      <link>https://briandouglas.ie/placeholder-image-generator-1.0.1</link>
      <guid>https://briandouglas.ie/placeholder-image-generator-1.0.1</guid>
      <pubDate>2025-05-26T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve recently had the need to generate placeholder images for a webapp. User&#x27;s of this app are not required
to upload a profile image, even though they can. This means that a fallback or &quot;placeholder&quot; image is required
for those that choose not to upload a profile image. To facilitate this I developed a neat little package that
can be reused in any app. The details of which are below.

]]></description>
      <content:encoded><![CDATA[
        <h2>Placeholder Image Generator</h2>
<p><img src="https://img.shields.io/maven-central/v/ie.briandouglas/PlaceholderImageGenerator" alt="Maven Central Version">
<a href="https://javadoc.io/doc/ie.briandouglas/PlaceholderImageGenerator"><img src="https://javadoc.io/badge2/ie.briandouglas/PlaceholderImageGenerator/javadoc.svg?bustcache" alt="javadoc"></a></p>
<p>This simple utility generates customizable placeholder images in Java using AWT. It is useful for creating mock user avatars, image placeholders in UI development, or dynamically generated image content.</p>
<p><magpie-trinket>View the source code on github at <a href="https://github.com/BrianDouglasIE/PlaceholderImageGenerator">BrianDouglasIE/PlaceholderImageGenerator</a></magpie-trinket></p>
<h2>Features</h2>
<ul>
<li>Set image dimensions and text</li>
<li>Choose custom font and font size</li>
<li>Configure text and background colors</li>
<li>Enable or disable anti-aliasing for smoother text rendering</li>
</ul>
<h2>Installation</h2>
<p>Add the following dependency to your Maven <code>pom.xml</code>:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;ie.briandouglas&lt;/groupId&gt;
    &lt;artifactId&gt;PlaceholderImageGenerator&lt;/artifactId&gt;
    &lt;version&gt;1.0.1&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>Or if you're using Gradle:</p>
<pre><code class="language-kotlin">dependencies {
    implementation(&quot;ie.briandouglas:PlaceholderImageGenerator:1.0.1&quot;)
}
</code></pre>
<h2>Example usage:</h2>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) throws IOException {
        var placeholderImage = new PlaceholderImage();

        var options = PlaceholderImageOptions.builder()
                .text(&quot;BD&quot;)
                .width(400)
                .height(400)
                .backgroundColor(Color.decode(&quot;#DCDCDC&quot;))
                .font(new Font(&quot;Arial&quot;, Font.BOLD, 200))
                .build();

        var image = placeholderImage.generate(options);
        ImageIO.write(image, &quot;png&quot;, new File(&quot;placeholder.png&quot;));
        System.out.println(&quot;Generated placeholder.png&quot;);
    }
}
</code></pre>
<p>This example will generate a centered <code>&quot;BD&quot;</code> in bold Arial on a <code>400x400</code> image with a light gray background, saved as <code>placeholder.png</code>.</p>
<p><img src="/images/placeholder.png" alt="Example placeholder image"></p>
<h2>Documentation</h2>
<p>See <a href="https://javadoc.io/doc/ie.briandouglas/PlaceholderImageGenerator/latest/index.html">javadoc.io</a></p>
<h3><code>PlaceholderImageOptions</code></h3>
<p>Encapsulates configuration options for generating a placeholder image.</p>
<pre><code class="language-java">PlaceholderImageOptions.builder()
    .text(&quot;AB&quot;)                                // Text to display
    .width(300)                                // Width in pixels
    .height(300)                               // Height in pixels
    .textColor(Color.DARK_GRAY)                // Color of the text (optional, default: DARK_GRAY)
    .backgroundColor(Color.GRAY)               // Background color (optional, default: GRAY)
    .font(new Font(&quot;Arial&quot;, Font.PLAIN, 16))   // Font used for rendering (optional)
    .antiAliasOn(true)                         // Enable anti-aliasing (optional, default: true)
    .build();
</code></pre>
<h3><code>PlaceholderImage</code></h3>
<p>Generates a <code>BufferedImage</code> using the provided <code>PlaceholderImageOptions</code>.</p>
<pre><code class="language-java">BufferedImage image = new PlaceholderImage().generate(options);
</code></pre>
<h2>Dependencies</h2>
<ul>
<li>Java 8+</li>
<li>No external libraries required; uses <code>java.awt</code> and <code>javax.imageio</code></li>
</ul>
<h2>License</h2>
<p>Apache 2.0</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Request cache in Spring]]></title>
      <link>https://briandouglas.ie/spring-request-cache</link>
      <guid>https://briandouglas.ie/spring-request-cache</guid>
      <pubDate>2025-05-19T00:00:00.000Z</pubDate>
      <description><![CDATA[Let&#x27;s say we have a request that fetches a post stored in the database. That request may need
to alter the fetched post in some way. Before the post is altered various checks may need to
happen, such as an ownership check. But we want to avoid each check fetching the same post
from the database. This is when a cache that is scoped to a request&#x27;s life cycle comes in handy.

]]></description>
      <content:encoded><![CDATA[
        <p>In a <a href="/pre-auth-spring">previous post</a> I introduced a method for carrying out ownership checks
using Spring Security's <code>@PreAuthorize</code> annotation. Whilst the approach was clean and maintainable
it did introduce a performance issue. This is because <code>@PreAuthorize</code> executes before the method body.
The post is fetched during the security check, and then again inside the method. This leads to duplicate
fetches from the database. Once in the <code>isOwner</code> call and then again in the <code>findOwnedPost</code> call. You
can see for yourself in the below code example.</p>
<pre><code class="language-java">@Component
@AllArgsConstructor
public class PostSecurity {
    private final PostRepository postRepository;

    public boolean isOwner(Long id, String userEmail) {
        return postRepository.findById(id) // first fetch
                .map(Post::getUser)
                .map(AppUser::getEmail)
                .map(it -&gt; it.equals(userEmail))
                .orElse(false);
    }
}
</code></pre>
<pre><code class="language-java">@PreAuthorize(&quot;@postSecurity.isOwner(#postId, authentication.name)&quot;)
public Post getOwnedPost(Long postId) {
    return postRepository.findById(postId) // second fetch
            .orElseThrow(() -&gt; new EntityNotFoundException(&quot;Post not found&quot;));
}
</code></pre>
<p>One simple way to avoid this would be to do the ownership check inside the <code>getOwnedPost</code> method.
This may seem straightforward, but it would lead to code that is harder to maintain. For example
the authenticated <code>UserDetails</code> would need to be passed around, and a checked exceptions would need
added to each caller... yuck.</p>
<p>A nice solution would be to cache the owned post instance for the lifecycle of the request. This would
mean that the <code>isOwner</code> method would cache the post on look up. The post could then be retrieved from
the cache inside the <code>getOwnedPost</code> method. Meaning that there is only one post look up. We would then
add a request filter to clear the cache after each request.</p>
<h2>Using a request based cache</h2>
<p>Each Spring request uses it's own thread. This means that we can cache values for the current request's
thread, without affecting any other request. To do this we'll use a plain old java object to model our
<code>RequestCache</code>. I want to use this solution for different entity types, so I'll make it generic by making
it a generic class, so it can hold any type of entity.</p>
<pre><code class="language-java">public class RequestCache&lt;T&gt; {
    private final ThreadLocal&lt;Optional&lt;T&gt;&gt; holder = new ThreadLocal&lt;&gt;();

    public void set(T value) {
        holder.set(Optional.ofNullable(value));
    }

    public Optional&lt;T&gt; get() {
        return holder.get();
    }

    public void clear() {
        holder.remove();
    }
}
</code></pre>
<p><magpie-trinket>In some contexts like asynchronous processing, you may need <code>InheritableThreadLocal</code> or a
more advanced request-scoped solution like Spring’s <code>@RequestScope</code>. But for typical synchronous requests,
<code>ThreadLocal</code> works well.</magpie-trinket></p>
<p>Now let's create a <code>RequestCacheRegistry</code>. This will be a <code>@Component</code> to allow it to be injected into
the various services that will use it. You will be able to see at this stage how this approach can be
extended to work with various entities, by adding a unique <code>RequestCache</code> property for each cached item.</p>
<pre><code class="language-java">@Getter
@Component
public class RequestCacheRegistry {
    public final RequestCache&lt;Post&gt; ownedPost = new RequestCache&lt;&gt;();

    public void clearAll() {
        ownedPost.clear();
    }
}
</code></pre>
<p>Now that our <code>RequestCacheRegistry</code> has been set up, we can use it within our <code>PostService</code>. First we'll
need to modify the <code>PostSecurity</code> component to cache the post that is returned from the <code>PostRepository</code>.
As the <code>PostRepository</code> returns an <code>Optional&lt;Post&gt;</code> the cache can be updated in a chained <code>map</code> call.</p>
<pre><code class="language-java">@Component
@AllArgsConstructor
public class PostSecurity {
    private final PostRepository postRepository;
    private final RequestCacheRegistry requestCacheRegistry;

    public boolean isOwner(Long id, String userEmail) {
        return postRepository.findById(id)
                .filter(it -&gt; it.getUser().getEmail().equals(userEmail))
                .map(it -&gt; {
                    requestCacheRegistry.getOwnedPost().set(it); // cache post
                    return true;
                })
                .orElse(false);
    }
}
</code></pre>
<p>Now that the <code>requestCacheRegisty.ownedPost</code> is set. It can be access inside the <code>getOwnedPost</code> method that
has the <code>PreAuthorize</code> annotation.</p>
<pre><code class="language-java">@PreAuthorize(&quot;@postSecurity.isOwner(#postId, authentication.name)&quot;)
public Post getOwnedPost(Long postId) {
    return requestCacheRegistry.getOwnedPost().get().orElseGet(() -&gt; findById(postId));
}
</code></pre>
<p>With all this in place there is now only one database call to fetch the required post. We have also implemented
a pattern that will allow for other entities to be cached in the same way, using the <code>RequestCacheRegistry</code>.</p>
<h2>Clearing the cache</h2>
<p>It is important that we clear the request cache after each request completes. This will help prevent any thread
leaks from occurring. A clean way of doing this is to implement a request filter. The following request filter
will be called automatically on each request. Once the filter chain completes, this custom filter will call
the <code>cacheRegistry.clearAll</code> method. Ensuring the cache registry is safely cleared after each request.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class RequestCacheCleanUpFilter extends OncePerRequestFilter {

    private final RequestCacheRegistry cacheRegistry;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } finally {
            cacheRegistry.clearAll();
        }
    }

}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Checking ownership in Spring]]></title>
      <link>https://briandouglas.ie/pre-auth-spring</link>
      <guid>https://briandouglas.ie/pre-auth-spring</guid>
      <pubDate>2025-05-12T00:00:00.000Z</pubDate>
      <description><![CDATA[Say a forum user creates a post. Only that user, the owner, should be allowed to alter said post.

]]></description>
      <content:encoded><![CDATA[
        <p>This means that in our application code a check should happen. Such a check might assert that the
<code>user_id</code> of the post in question is that of the currently authenticated user. To facilitate this
we might add a <code>PostSecurity</code> component with an <code>isOwner</code> method. This class will be something like
the one below.</p>
<pre><code class="language-java">@Component
@AllArgsConstructor
public class PostSecurity {
    private final PostRepository postRepository;

    public boolean isOwner(Long id, String userEmail) {
        return postRepository.findById(id)
                .map(Post::getUser)
                .map(AppUser::getEmail)
                .map(it -&gt; it.equals(userEmail))
                .orElse(false);
    }
}
</code></pre>
<p>This method finds the post with an id matching <code>postId</code>, gets the user attached to that post, then
asserts whether the post's user's email matches the currently authenticated user. If the
<code>Optional&lt;Post&gt;</code> from <code>findById</code> is empty, then <code>false</code> is returned.</p>
<p>But now the question is, where should this method be called? Certainly it's not the
<code>PostController</code> and calling it inside other <code>PostService</code> methods will lead to repetition and the
passing around of arguments unnecessarily.</p>
<p>Luckily the team at Spring Security have thought about writing clean code and have provided the
<code>PreAuthorize</code> annotation for such scenarios. This annotation is used to restrict access to methods
before they are invoked. This means that we can create the below <code>PostService</code> method to check auth
when accessing a <code>Post</code>.</p>
<p><magpie-trinket>Remember to add the <code>@EnableMethodSecurity(prePostEnabled = true)</code> annotation to your <code>SecurityConfig</code> class.
This is a requirement for the <code>@PreAuthorize</code> annotation to work.</magpie-trinket></p>
<pre><code class="language-java">@PreAuthorize(&quot;@postSecurity.isOwner(#postId, authentication.name)&quot;)
public Post getOwnedPost(Long postId) {
    return postRepository.findById(postId)
            .orElseThrow(() -&gt; new EntityNotFoundException(&quot;Post not found&quot;));
}
</code></pre>
<p>This can then be called in the <code>PostController</code>.</p>
<pre><code class="language-java">@GetMapping(&quot;/edit/{id}&quot;)
public String editPost(@PathVariable Long postId, Model model) {
    var post = postService.getOwnedPost(postId); // will throw when user is not the post's owner
    model.addAttribute(&quot;post&quot;, post);
    return &quot;post/edit&quot;;
}
</code></pre>
<p><chicken-asks>That is all well and good, but won't this require two calls to fetch the same post?</chicken-asks>
<magpie-replies>Wow, you aren't so slow after all. Yes, it would. He provides a solution here, <a href="/spring-request-cache">spring-request-cache</a></magpie-replies></p>
<p>By keeping the ownership logic in the <code>PostService</code> it can be reused in other controllers and methods.
It also ensures that accessing a <code>Post</code> is done in a consistent and safe manner. The handling of the
ownership check has been delegated to the <code>PostSecurity</code> component which allows for a separation of
concerns.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Slurp - A Java task runner]]></title>
      <link>https://briandouglas.ie/slurp-1.0</link>
      <guid>https://briandouglas.ie/slurp-1.0</guid>
      <pubDate>2025-04-09T00:00:00.000Z</pubDate>
      <description><![CDATA[Introducing Slurp 1.0. Slurp is a task runner for Java. It allows tasks, aka &#x60;Runnable&#x60; instances to be executed.

]]></description>
      <content:encoded><![CDATA[
        <p>Slurp was created to be a super simple task runner. It has a lean feature set. Allowing you to register, execute,
and run a sequence of tasks.</p>
<p><magpie-trinket>You can view the Slurp javadocs at <a href="https://javadoc.io/doc/ie.briandouglas/slurp/latest/index.html">ie.briandouglas/slurp/</a></magpie-trinket></p>
<h2>Usage</h2>
<h3>Create a SlurpTask</h3>
<p><code>SlurpTask</code>s are Java records. They have a <code>name</code> and a <code>runnable</code> property. Neither the <code>name</code> nor <code>runnable</code> property may be null.
You can create a <code>SlurpTask</code> like so.</p>
<pre><code class="language-java">SlurpTask printHelloWorld = new SlurpTask(&quot;HelloWorld&quot;, () -&gt; System.out.println(&quot;Hello World!&quot;));
</code></pre>
<h3>Register and Execute a SlurpTask</h3>
<p>Inorder to register a <code>SlurpTask</code> for excution a <code>Slurp</code> instance is needed. The <code>Slurp</code> instance allows for tasks to be registered and
executed.</p>
<pre><code class="language-java">Slurp slurp = new Slurp();
SlurpTask printHelloWorld = new SlurpTask(&quot;HelloWorld&quot;, () -&gt; System.out.println(&quot;Hello World!&quot;));

slurp.registerTask(printHelloWorld);
slurp.executeTask(printHelloWorld);
</code></pre>
<p>Task names must be unique, if a duplicate task name is registered a <code>DuplicateTaskException</code> will be thrown. If an attempt is made to execute
an unknown task a <code>TaskNotFoundException</code> will be thrown.</p>
<h3>Executing a sequence of tasks</h3>
<p><code>Slurp</code> can be used to execute a sequence of tasks. Internally it uses a single threaded executor, and waits for each task to finish before
executing the next.</p>
<pre><code class="language-java">String[] sendMarketingEmail = new String[] {
  &quot;prepareClientList&quot;,
  &quot;generateEmailHTML&quot;,
  &quot;sendSpamMarketingEmail&quot;
};

slurp.executeTaskSequence(sendMarketingEmail); // may also take a SlurpTask[]
</code></pre>
<h2>Source code</h2>
<p>The source code for the <code>Slurp</code> task runner is available on github at <a href="https://github.com/BrianDouglasIE/slurp">BrianDouglasIE/slurp</a>.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Using CPU branch prediction in Java]]></title>
      <link>https://briandouglas.ie/cpu-branch-prediction</link>
      <guid>https://briandouglas.ie/cpu-branch-prediction</guid>
      <pubDate>2025-04-03T00:00:00.000Z</pubDate>
      <description><![CDATA[CPU branch prediction is when the CPU tries to guess the outcome of a conditional
operation, eg: an &#x60;if&#x60; statement, before it is executed. If it guesses correct then
the CPU can continue executing instructions without interuption.

]]></description>
      <content:encoded><![CDATA[
        <p>When applied to Java this can be used to get a performance benefit. In particular
when looping through a large array that runs some condtional operation on each value.</p>
<p><chicken-asks>So how do you make loop conditionals predictable?</chicken-asks>
<magpie-replies>Well we could sort the array before processing it</magpie-replies></p>
<p>Sorting the values of an array before processing it will help with CPU branch prediction.
It may also help with cpu cache optimisation. CPU cache optimisation being the accessing
of data that from the CPU cache. This is memory located close to the CPU cores, data accessed
from here is often processed much quicker than from the main memory. This can be done in Java
by accessing data from a sorted array. The idea being that sorted data may be held closer together
in memory, and therefore can be accessed more quickly, providing a performance benefit.</p>
<h2>Example Usage</h2>
<p>Let's see this in action with some example code. Given the following program, the sorted array will be
processed roughly 4 times faster than the unsorted array.</p>
<pre><code class="language-java">import java.util.Random;

public class Main {

    public static void main(String[] args) {
        int arraySize = 10_000_000;

        int[] sortedArray = new int[arraySize];
        for (int i = 0; i &lt; arraySize; i++) {
            sortedArray[i] = i;
        }

        long startTime = System.nanoTime();
        long sortedSum = evenNumberSum(sortedArray);
        long sortedTime = System.nanoTime() - startTime;

        int[] shuffledArray = shuffle(sortedArray);
        startTime = System.nanoTime();
        long unsortedSum = evenNumberSum(shuffledArray);
        long unsortedTime = System.nanoTime() - startTime;

        if (unsortedSum != sortedSum) {
            throw new Error(&quot;sortedSum and unsortedSum should match&quot;);
        }

        System.out.println(&quot;Sorted array processing time:   &quot; + sortedTime + &quot; ns&quot;);
        System.out.println(&quot;Unsorted array processing time: &quot; + unsortedTime + &quot; ns&quot;);
        System.out.println(&quot;Times faster: &quot; + Math.floorDiv(unsortedTime, sortedTime));
    }

    /**
     * Returns a clone of the array with it's values shuffled.
     *
     * @param srcArray
     * @return
     */
    private static int[] shuffle(int[] srcArray) {
        Random rand = new Random();
        int[] array = srcArray.clone();

        for (int i = array.length - 1; i &gt; 0; i--) {
            int randIndex = rand.nextInt(i + 1);
            int randIndexValue = array[randIndex];
            array[randIndex] = array[i];
            array[i] = randIndexValue;
        }

        return array;
    }

    /**
     * Returns the sum of all the even numbers in the array.
     *
     * @param array
     * @return
     */
    private static long evenNumberSum(int[] array) {
        long sum = 0;
        for (int value : array) {
            if (isEven(value)) {
                sum += value;
            }
        }
        return sum;
    }

    /**
     * Returns true if n is even and false if not.
     *
     * @param n
     * @return
     */
    private static boolean isEven(int n) {
        return n % 2 == 0;
    }
}
</code></pre>
<p>The above program creates an array of 10 million ints, sorted. It then iterates through that array to get
the sum of all the even ints, measuring the execution time. It then does the same thing with a shuffled
version of the initial sorted array. Running the program you should see that the sorted array is processed
much quicker than the unsorted array. Demonstrating the advantage of CPU branch prediction and cache optimisation.
Below is the result I get when running it locally.</p>
<pre><code>Sorted array processing time:   8745547 ns
Unsorted array processing time: 37145149 ns
Times faster: 4
</code></pre>
<h2>Drawbacks</h2>
<p>While I can't think of a drawback, other than the time spent sorting the array before processing it. There is
a caveat. It may not always give a benefit.</p>
<p>For example, if when processing the array, you ran something that wasn't easily predictable, eg: testing if a
number is a prime, you may not see much difference. While the sorted array would still be more <em>predictable</em>,
in practice the benefit of the prediction would be less significant as the majority of the numbers are likely
non prime and the check would fail early. So there will likely be use cases where you see a great benefit from
processing a sorted array, and some cases there you don't really see any benefit.</p>
<h2>Summary</h2>
<p>Sorted arrays in Java benefit from CPU branch prediction and cache optimisation due to predictable
patterns and memory location.</p>
<p>In contrast, unsorted arrays suffer from the lack of predictability and random
memory access patterns.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Using Docker on Linux Mint]]></title>
      <link>https://briandouglas.ie/docker-on-linux-mint</link>
      <guid>https://briandouglas.ie/docker-on-linux-mint</guid>
      <pubDate>2025-04-02T00:00:00.000Z</pubDate>
      <description><![CDATA[Surely using Docker on Linux Mint is the same as Ubuntu? I can just follow the official Ubuntu
install guide.

]]></description>
      <content:encoded><![CDATA[
        <p>Well this is not quite true. Yes, Linux Mint is built on Ubuntu and using Docker should be the
same. <em>BUT</em> the official install guide will not work for you. There is a minor tweak that is required.
And that is why this article exists.</p>
<h2>The &quot;Official&quot; install command (02/04/2025)</h2>
<p>Here is the official install command from the Ubuntu docker guide (<a href="https://docs.docker.com/engine/install/ubuntu/">install/ubuntu</a>).</p>
<pre><code># Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release &amp;&amp; echo &quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&quot;) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null
sudo apt-get update
</code></pre>
<p>When you run this it will complain. You will see the following error.</p>
<pre><code>E: The repository 'https://download.docker.com/linux/ubuntu vera Release' does not have a Release file.
</code></pre>
<p>Notice the <em>&quot;vera&quot;</em> release. Ubuntu has no release named <em>vera</em>. That is the Linux Mint release version. This is the crux
of the issue.</p>
<h2>The install command you are looking for</h2>
<p>The fix here is to use the Debian download passing in the latest Debian release name. This is currently <em>&quot;bookworm&quot;</em>.</p>
<p>The updated command is below. I have hard coded the release name as <em>bookworm</em> and updated the download url to use
<em>debian</em> instead of <em>ubuntu</em>.</p>
<pre><code># Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  bookworm stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null
sudo apt-get update
</code></pre>
<p>I hope this solves your issue, and that docker is now running on your lovely Linux Mint instance.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Install OpenJDK on Ubuntu]]></title>
      <link>https://briandouglas.ie/install-openjdk-linux</link>
      <guid>https://briandouglas.ie/install-openjdk-linux</guid>
      <pubDate>2025-04-02T00:00:00.000Z</pubDate>
      <description><![CDATA[Let&#x27;s install OpenJDK 21 LTS on Ubuntu.

]]></description>
      <content:encoded><![CDATA[
        <p>The install command:</p>
<pre><code>sudo apt install openjdk-21-jdk
</code></pre>
<h2>Using the correct Java version</h2>
<p>It's likely that if you are a Java Developer that you will have multiple versions of Java installed.
To specify the default version of java to use we will run the <code>update-alternatives</code> command.</p>
<pre><code>sudo update-alternatives --config java
</code></pre>
<p>This will list the available Java installs and request you to choose which one you want to use as the default.
You should see output similar to what is below. In this instance I choose 2. As I want to default to OpenJDK 21.</p>
<pre><code>There are 2 choices for the alternative java (providing /usr/bin/java).

  Selection    Path                                         Priority   Status
------------------------------------------------------------
* 0            /usr/lib/jvm/java-21-openjdk-amd64/bin/java   2111      auto mode
  1            /usr/lib/jvm/java-11-openjdk-amd64/bin/java   1111      manual mode
  2            /usr/lib/jvm/java-21-openjdk-amd64/bin/java   2111      manual mode

Press &lt;enter&gt; to keep the current choice[*], or type selection number:
</code></pre>
<p>You can follow the same process to set the default <code>javac</code> command also.</p>
<h2>Setting the JAVA_HOME variable</h2>
<p>In order to set up the <code>JAVA_HOME</code> variable we can use <code>update-alternatives</code> again. Running <code>sudo update-alternatives --config java</code>
will show the select Java path. We can take this and append it to our shell's path by adding the following to
our <code>.bashrc</code> file.</p>
<pre><code>export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64/
export PATH=$PATH:$JAVA_HOME/bin
</code></pre>
<p>To confirm this has worked reload your shell with <code>source ~/.bashrc</code> and run <code>echo $JAVA_HOME</code>. You should see output similar to:</p>
<pre><code>/usr/lib/jvm/java-21-openjdk-amd64/
</code></pre>
<p>Now you can enjoy writing your enterprise Java code on Ubuntu. I know I do.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Yoda Conditions]]></title>
      <link>https://briandouglas.ie/yoda-conditions</link>
      <guid>https://briandouglas.ie/yoda-conditions</guid>
      <pubDate>2025-04-01T00:00:00.000Z</pubDate>
      <description><![CDATA[When I write a comparison statement I nearly always write it variable first, value second.

]]></description>
      <content:encoded><![CDATA[
        <p>But this is a silly habit to have. It should be value first, variable second. This is often
refered to as a <em>Yoda condition</em>. Due to the little green jedi's peculiar way of talking.</p>
<p><magpie-trinket><a href="https://en.wikipedia.org/wiki/Yoda_conditions">wiki/Yoda_conditions</a> does a better job of explaining this than him.</magpie-trinket></p>
<p>Here is a quick example of how Yoda condtions work. Given the following statement:</p>
<pre><code class="language-c">if(age &lt; 18) { /* you can't drink here */ }
</code></pre>
<p>The Yoda condition equivalent would be:</p>
<pre><code class="language-c">if(18 &gt; age) { /* you can't drink here */ }
</code></pre>
<p><chicken-asks>That looks odd to me, what is the advantage?</chicken-asks>
<magpie-replies>Mostly it will catch an unwanted assignment.</magpie-replies></p>
<h2>Why bother?</h2>
<p>Well look at this code:</p>
<pre><code class="language-c">if(animal = &quot;dog&quot;) { /* wag tail */ }
</code></pre>
<p>This checks if the animal is a dog, and then does something.</p>
<p>Oh actually, no it doesn't. It assigns the value of animal to be &quot;dog&quot;. Which is not what
I wanted. But it's a mistake that wouldn't be imediately obvious to me either.</p>
<p>It's this sort of thing that Yoda conditions prevent. If I was to make that mistake using
Yoda conditons. Then the following code would throw an error.</p>
<pre><code class="language-c">if(&quot;dog&quot; = animal) { /* wag tail */ }
</code></pre>
<p>The error:</p>
<pre><code>.\animal.c:12:10: error: assignment to expression with array type
   12 | if(&quot;dog&quot; = animal) {
      |          ^
</code></pre>
<p>Now I don't have to debug my code and pull my hair out. The issue is clear, thanks Yoda.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Tower of Hanoi in Java]]></title>
      <link>https://briandouglas.ie/tower-of-hanoi-java</link>
      <guid>https://briandouglas.ie/tower-of-hanoi-java</guid>
      <pubDate>2025-03-25T00:00:00.000Z</pubDate>
      <description><![CDATA[The Tower of Hanoi is a commonly used coding interview question. The solution requires
the developer to write a recursive method.

]]></description>
      <content:encoded><![CDATA[
        <p><magpie-trinket>For a detailed explanation of the problem look at <a href="https://en.wikipedia.org/wiki/Tower_of_Hanoi">wiki/Tower_of_Hanoi</a></magpie-trinket></p>
<p>Whilst it's not <em>that</em> complex, it can be a tricky one to get your head around if you
haven't seen it before, or are not familiar with recusive method calls. At an interview
recently I was asked to write an implementation of it using Java. Well I could use any
language, but I chose to use Java as the role was for a fullstack Spring dev.</p>
<p>I was able to implement it successfully after struggling with parameter order for a couple
of minutes. I had the advantage of solving this problem before, thank you <a href="https://exercism.org">exercism.io</a>.
So I had an idea of what needed to be done.</p>
<h2>Test Suite</h2>
<p>Before I show you the implementation. I asked ChatGPT to generate a test suite using JUnit.
This is one of the great use cases for AI, they can write the boring code. I had to tune the
tests slightly to give the expected output. For example in my implementation I am writing out
the moves as Strings. Which was the interview requirement.</p>
<pre><code class="language-java">package ie.briandouglas.tower_of_hanoi;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import java.util.List;

class TowerOfHanoiTest {

    @Test
    void testOneDisk() {
        TowerOfHanoi hanoi = new TowerOfHanoi();
        List&lt;String&gt; moves = hanoi.solve(1, 'A', 'C', 'B');
        assertEquals(1, moves.size());
        assertEquals(&quot;Move disk 1 from A to C&quot;, moves.get(0));
    }

    @Test
    void testTwoDisks() {
        TowerOfHanoi hanoi = new TowerOfHanoi();
        List&lt;String&gt; moves = hanoi.solve(2, 'A', 'C', 'B');
        assertEquals(3, moves.size());
        assertEquals(&quot;Move disk 1 from A to B&quot;, moves.get(0));
        assertEquals(&quot;Move disk 2 from A to C&quot;, moves.get(1));
        assertEquals(&quot;Move disk 1 from B to C&quot;, moves.get(2));
    }

    @Test
    void testThreeDisks() {
        TowerOfHanoi hanoi = new TowerOfHanoi();
        List&lt;String&gt; moves = hanoi.solve(3, 'A', 'C', 'B');
        assertEquals(7, moves.size());
        assertEquals(&quot;Move disk 1 from A to C&quot;, moves.get(0));
        assertEquals(&quot;Move disk 2 from A to B&quot;, moves.get(1));
        assertEquals(&quot;Move disk 1 from C to B&quot;, moves.get(2));
        assertEquals(&quot;Move disk 3 from A to C&quot;, moves.get(3));
        assertEquals(&quot;Move disk 1 from B to A&quot;, moves.get(4));
        assertEquals(&quot;Move disk 2 from B to C&quot;, moves.get(5));
        assertEquals(&quot;Move disk 1 from A to C&quot;, moves.get(6));
    }

    @Test
    void testZeroDisks() {
        TowerOfHanoi hanoi = new TowerOfHanoi();
        List&lt;String&gt; moves = hanoi.solve(0, 'A', 'C', 'B');
        assertTrue(moves.isEmpty());
    }

    @Test
    void testFiveDisks() {
        TowerOfHanoi hanoi = new TowerOfHanoi();
        List&lt;String&gt; moves = hanoi.solve(5, 'A', 'C', 'B');
        assertEquals(31, moves.size());
    }
}
</code></pre>
<h2>Implementation</h2>
<p>The implementation here is pretty much the implementation you will see everywhere else.
The solve method recursively calls itself with <code>n - 1</code>, moving the nth largest disk, <code>from</code>,
to <code>to</code>. Then from <code>unused</code> to <code>to</code>. In this example <code>from</code> is the origin peg, <code>to</code> is the target,
and <code>unused</code> is the peg that is not involved.</p>
<p>I extracted the <code>constructMoveString</code> for readability, and created a method overload so that <code>solve</code>
could be called without instantiating an <code>ArrayList</code>.</p>
<pre><code class="language-java">package ie.briandouglas.tower_of_hanoi;

import java.util.ArrayList;
import java.util.List;

public class TowerOfHanoi {

	public List&lt;String&gt; solve(int n, char from, char to, char unused, List&lt;String&gt; moves) {
		if (n &gt; 0) {
			solve(n - 1, from, unused, to, moves);
			moves.add(constructMoveString(n, from, to));
			solve(n - 1, unused, to, from, moves);
		}
		return moves;
	}

	public List&lt;String&gt; solve(int n, char from, char to, char unused) {
		return solve(n, from, to, unused, new ArrayList&lt;&gt;());
	}

	private String constructMoveString(int disk, char from, char to) {
		return new StringBuilder().append(&quot;Move disk &quot;).append(disk).append(&quot; from &quot;).append(from).append(&quot; to &quot;)
				.append(to).toString();
	}

}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[AxeRunner]]></title>
      <link>https://briandouglas.ie/axe-runner</link>
      <guid>https://briandouglas.ie/axe-runner</guid>
      <pubDate>2025-03-06T00:00:00.000Z</pubDate>
      <description><![CDATA[AxeRunner is used to run accessibility tests on web pages using the [axe-core](https://www.deque.com/axe/) library in Java.

]]></description>
      <content:encoded><![CDATA[
        <p><magpie-trinket>Source code available at <a href="https://github.com/BrianDouglasIE/AxeRunner">BrianDouglasIE/AxeRunner</a></magpie-trinket></p>
<p>It integrates with Selenium WebDriver to automate web accessibility testing, providing detailed reports on accessibility
violations, passes, and other relevant results. The resulting axe-core report is converted into Java classes so that it
can be used within a Java project and allows for type hinting.</p>
<p>Quick synopsis of the package.</p>
<h2>Features</h2>
<ul>
<li>Runs accessibility tests on specified URLs.</li>
<li>Uses Selenium WebDriver for browser automation.</li>
<li>Configurable via <code>AxeSpec</code> and <code>AxeRunOptions</code>.</li>
<li>Parses and categorizes accessibility test results.</li>
<li>Supports axe-core rule customization.</li>
<li>Supports axe-core localization.</li>
</ul>
<h2>Prerequisites</h2>
<ul>
<li>Java (JDK 8 or higher)</li>
<li>Selenium WebDriver</li>
</ul>
<h2>Installation</h2>
<p>To use <code>AxeRunner</code>, include the following dependencies in your project:</p>
<h3>Maven</h3>
<pre><code class="language-xml">&lt;dependencies&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.seleniumhq.selenium&lt;/groupId&gt;
        &lt;artifactId&gt;selenium-java&lt;/artifactId&gt;
        &lt;version&gt;4.x.x&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;ie.briandouglas&lt;/groupId&gt;
        &lt;artifactId&gt;axe_runner&lt;/artifactId&gt;
        &lt;version&gt;0.0.1&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;
</code></pre>
<h2>Usage</h2>
<h3>Creating an Instance</h3>
<pre><code class="language-java">WebDriver driver = new ChromeDriver(); // or any other WebDriver instance
AxeRunner axeRunner = new AxeRunner(driver);
</code></pre>
<h3>Running Accessibility Tests</h3>
<pre><code class="language-java">String url = &quot;https://example.com&quot;;
AxeResults results = axeRunner.getAxeResults(url);
</code></pre>
<h3>Running with Custom Configurations</h3>
<pre><code class="language-java">AxeSpec spec = new AxeSpec(); // Define accessibility testing specifications
AxeRunOptions options = new AxeRunOptions(); // Define run options
AxeResults results = axeRunner.getAxeResults(url, spec, options);
</code></pre>
<h3>Accessing Results</h3>
<pre><code class="language-java">System.out.println(&quot;Violations: &quot; + results.violations.size());
System.out.println(&quot;Passes: &quot; + results.passes.size());
</code></pre>
<h2>Error Handling</h2>
<ul>
<li>If <code>axe-core.min.js</code> fails to load, an exception is thrown.</li>
<li>If Selenium fails to navigate to the page, the WebDriver is closed.</li>
<li>JSON processing errors during configuration are logged.</li>
</ul>
<h2>License</h2>
<p>This project is licensed under the <a href="http://www.apache.org/licenses/LICENSE-2.0.txt">Apache License, Version 2.0</a>.</p>
<h2>Contributing</h2>
<ul>
<li>Fork the repository.</li>
<li>Create a feature branch.</li>
<li>Submit a pull request.</li>
</ul>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[European Alternatives]]></title>
      <link>https://briandouglas.ie/european-alternatives</link>
      <guid>https://briandouglas.ie/european-alternatives</guid>
      <pubDate>2025-03-05T00:00:00.000Z</pubDate>
      <description><![CDATA[Where is the EU tech industry? Why do we use the software products of American giants like google, amazon, and facebook.
When we could use &quot;local&quot; EU software.

]]></description>
      <content:encoded><![CDATA[
        <p>It's March 2025 and the US is imposing trade tariffs on the EU. Which as an EU citizen makes me think &quot;screw you, I'll shop local&quot;.
But in regards to tech this is somewhat difficult as the US tech industry is much stronger than that of the EU. But it doesn't
have to be. The EU should end it's reliance on US tech.</p>
<p>Luckily there is a website called <a href="https://european-alternatives.eu">European Alternatives</a> that provides a list of EU based alternatives
to popular non EU software. You should check it out and discover some hidden gems. Right now I am using the
<a href="https://vivaldi.com">Vivaldi web browser</a> along with <a href="https://www.startpage.com">Startpage</a> a Dutch search engine.
They are excellent alternatives to the tech giant offerings.</p>
<p>By using this alternative software, you support the creation of jobs, encourage competition, and end your reliance on evil monopolies.</p>
<p>Make the switch today, use a European Alternative.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Using a virtual screen size in Raylib]]></title>
      <link>https://briandouglas.ie/virtual-screen-size-raylib</link>
      <guid>https://briandouglas.ie/virtual-screen-size-raylib</guid>
      <pubDate>2025-02-24T00:00:00.000Z</pubDate>
      <description><![CDATA[When working with pixel art it is common to create a virtual screen size. For example
if my sprites are 16x16 pixels and my game world is 256x256. I would want to scale the
size up to fit my computer&#x27;s screen size. If the art was not scaled it would look 
really tiny, on a 1920x1080 monitor.

]]></description>
      <content:encoded><![CDATA[
        <p>This is where the concept of a virtual screen size comes into play. I can virtually limit
my game to use a small screen size, and then scale that up to match the target <em>window</em> size.
In raylib this is achieved using a <code>RenderTexture2D</code>.</p>
<h2>Implementation</h2>
<p>First off I like to define some constants.</p>
<pre><code class="language-c">#define V_SCREEN_WIDTH 256
#define V_SCREEN_HEIGHT 256
#define V_SCALE 2
#define SCREEN_WIDTH  V_SCREEN_WIDTH * V_SCALE
#define SCREEN_HEIGHT V_SCREEN_HEIGHT * V_SCALE
</code></pre>
<p>In the above code the prefix <code>V_</code> refers to virtual. So I am setting my virtual screen size to
256x256 pixels and applying the <code>V_SCALE</code> to get the actual window size. In this instance
<code>256 x 2 = 512</code>.</p>
<p>When opening a window with raylib I will pass in the <code>SCREEEN_WIDTH</code> and <code>SCREEN_HEIGHT</code> constants
to the <code>InitWindow</code> method like so.</p>
<pre><code class="language-c">InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, &quot;Game Title&quot;);
</code></pre>
<p>This will open a correctly sized game window. I now want to create a <code>RenderTexture2D</code>. This
is what the game will be drawn on. It will then itself be scaled and drawn onto the actual game
window.</p>
<p>To create the <code>RenderTexture2D</code> I use the following code.</p>
<pre><code class="language-c">const RenderTexture2D renderTexture = LoadRenderTexture(V_SCREEN_WIDTH, V_SCREEN_HEIGHT);
const Rectangle renderTextureSrc = (Rectangle){ 0.0f, 0.0f, V_SCREEN_WIDTH, -V_SCREEN_HEIGHT };
const Rectangle renderTextureDest = (Rectangle){ 0.0f, 0.0f, SCREEN_WIDTH, SCREEN_HEIGHT };
const Vector2 renderTextureOrig = {0};

// remember to unload the texture when finished using it
UnloadRenderTexture(renderTexture);
</code></pre>
<magpie-trinket>
Notice that the <code>renderTextureSrc</code> rectangle uses a negative value for it's height.
This is to prevent the texture from being drawn on upside down. The reasoning is helpfully explained
in the <a href="https://github.com/raysan5/raylib/wiki/Frequently-Asked-Questions#why-is-my-render-texture-upside-down">raylib docs</a>.
</magpie-trinket>
<p>The render texture can then be drawn on to by creating a new texture mode.</p>
<pre><code class="language-c">BeginTextureMode(renderTexture);
    ClearBackground(BG_COLOR);
    DrawTextureRec(spritesheet, playerFrame, playerPos, WHITE);
EndTextureMode();
</code></pre>
<p>After this code is run the <code>RenderTexture2D</code>'s texture is then drawn on to the main draw context at the
correct scale.</p>
<pre><code class="language-c">BeginDrawing();
    ClearBackground(BG_COLOR);
    DrawTexturePro(renderTexture.texture, renderTextureSrc, renderTextureDest, renderTextureOrig, 0, WHITE);
EndDrawing();
</code></pre>
<p>This results in the small 16x16 pixel art being drawn at the correct scale, without loss of fidelity.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Using astyle to format C code]]></title>
      <link>https://briandouglas.ie/astyle</link>
      <guid>https://briandouglas.ie/astyle</guid>
      <pubDate>2025-02-18T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve been writing C with a bare bones neovim install. But I miss code formatting.

]]></description>
      <content:encoded><![CDATA[
        <p>A quick google pointed me in the direction of two different C code formatters,
<em>indent</em> by gnu and <em>astyle</em> by someone else. I'm currently working on windows
so I ran a <code>choco search indent</code>. When it did not return anything that looked
like the package I wanted I searched instead for <code>astyle</code>. It was there, so my
decision was made.</p>
<p>A quick look at the docs and I see predefined format styles. I really like the
<a href="https://astyle.sourceforge.net/astyle.html#_style=allman"><em>allman</em></a> style. Broken
braces are the thinking man's braces. So now I format my C source code with this
neat command:</p>
<pre><code>astyle --style=allman --recursive src/*.c
</code></pre>
<p>Everything looks nice and tidy. Still no need for a complicated IDE.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Creating a database dump with SQLite]]></title>
      <link>https://briandouglas.ie/dumping-sqlite-data</link>
      <guid>https://briandouglas.ie/dumping-sqlite-data</guid>
      <pubDate>2025-02-17T00:00:00.000Z</pubDate>
      <description><![CDATA[For one reason or another you may want to _dump_ your sqlite data into an sql file.

]]></description>
      <content:encoded><![CDATA[
        <p>A database dump can be used to import and export sql data. As with most things it
is a trivial task with SQLite.</p>
<h2>Exporting data</h2>
<pre><code>sqlite3 database.db .dump &gt; database_dump_$(date +%Y%m%d_%H%M%S).sql
</code></pre>
<p>The above command dumps the database to a new <em>.sql</em> file that is timestamped. I like to use the
following timestamp format <code>YYYYMMDD_HHMMSS</code>.</p>
<h2>Importing data</h2>
<p>An exported sql file can then be imported into a sqlite database like so.</p>
<pre><code>sqlite3 database.db &lt; database_dump_20250216_100802.sql
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Optimise SQLite for Laravel apps]]></title>
      <link>https://briandouglas.ie/laravel-optimize-database</link>
      <guid>https://briandouglas.ie/laravel-optimize-database</guid>
      <pubDate>2025-02-17T00:00:00.000Z</pubDate>
      <description><![CDATA[I love using SQLite, but it requires a bit of configuration to get optimal results.

]]></description>
      <content:encoded><![CDATA[
        <p>This is where Nuno Maduro's <a href="https://github.com/nunomaduro/laravel-optimize-database">nunomaduro/laravel-optimize-database</a>
package comes in. This packages sets up your Laravel app's sqlite database to work in a performant manner.</p>
<p>It does this by setting the following PRAGMA options on your sqlite database.</p>
<pre><code> ┌───────────────────────────┬─────────────┬───────────┐
 │ Setting                   │ Value       │ Via       │
 ├───────────────────────────┼─────────────┼───────────┤
 │ PRAGMA auto_vacuum        │ incremental │ Migration │
 │ PRAGMA journal_mode       │ WAL         │ Migration │
 │ PRAGMA page_size          │ 32768       │ Migration │
 │ PRAGMA busy_timeout       │ 5000        │ Runtime   │
 │ PRAGMA cache_size         │ -20000      │ Runtime   │
 │ PRAGMA foreign_keys       │ ON          │ Runtime   │
 │ PRAGMA incremental_vacuum │ (enabled)   │ Runtime   │
 │ PRAGMA mmap_size          │ 2147483648  │ Runtime   │
 │ PRAGMA temp_store         │ MEMORY      │ Runtime   │
 │ PRAGMA synchronous        │ NORMAL      │ Runtime   │
 └───────────────────────────┴─────────────┴───────────┘
</code></pre>
<p><magpie-trinket>Brian wrote a post detailing some of these PRAGMAs. He called it &quot;<a href="/sqlite-defaults">Sensible SQLite defaults</a>&quot;, it's his most popular post to date.</magpie-trinket></p>
<h2>Usage</h2>
<p>Nuno always makes things easy. You can install and integrate the package with these three commands.</p>
<pre><code>composer require nunomaduro/laravel-optimize-database
</code></pre>
<pre><code>php artisan db:optimize
</code></pre>
<pre><code>php artisan migrate
</code></pre>
<p>After that you SQLite db is ready to use.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Backing up an SQLite database]]></title>
      <link>https://briandouglas.ie/backing-up-sqlite</link>
      <guid>https://briandouglas.ie/backing-up-sqlite</guid>
      <pubDate>2025-02-16T00:00:00.000Z</pubDate>
      <description><![CDATA[SQLite is a file based database. This makes common tasks like back-ups trivial.

]]></description>
      <content:encoded><![CDATA[
        <p>A database backup is essentially a snapshot of the database's current state. This
is a useful thing to have, just in case you need to restore a database to an old state.
Depending on the activity on your app you may want to back it up frequently.</p>
<h2>Backing up</h2>
<p>Backing up an SQLite database is as simple as copying the file. On a linux box you do this
with the <code>cp</code> command.</p>
<pre><code>cp database.db backup_database_$(date +%Y%m%d_%H%M%S).db
</code></pre>
<p>The above command copies the database to a new file that is timestamped. I like to use the
following timestamp format <code>YYYYMMDD_HHMMSS</code>.</p>
<h2>Restoring from a back up</h2>
<p>To restore an sqlite database from a back up. We just need to <em>copy the copy</em> to the original
location.</p>
<p><chicken-asks>Copy the copy?</chicken-asks>
<magpie-replies>He has a way with words.</magpie-replies></p>
<p>This is basically the reverse of the backup command.</p>
<pre><code>cp backup_database_20250116_091422.db database.db
</code></pre>
<p>This replaces the current database with the backup, maintaining the backup for future use.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[GameDev in C Part 4: A Snake Clone]]></title>
      <link>https://briandouglas.ie/gamedev-in-c-part-4</link>
      <guid>https://briandouglas.ie/gamedev-in-c-part-4</guid>
      <pubDate>2025-02-14T00:00:00.000Z</pubDate>
      <description><![CDATA[Happy Valentines Day!, I made a snake clone in C.

]]></description>
      <content:encoded><![CDATA[
        <p>This is a quick interlude from the regular GameDev in C posts. I was making very little progress
on the doodle jump style game on which the series is based. So I thought I would take a quick
break and code up a snake clone.</p>
<p>I chose snake as I have written this type of game previously in JS and Python. So I didn't really
have to figure much out. Just get down to writing some C. I haev included the full source code in
this article. One nice feature that I added was smooth transitions between cells. I did this by
growing and shortening the head and tail respectively, based on an animation timer. This makes for
a nice animation and makes it look like the snake is moving smoothly, rather than jumping a cell at
a time. The implementation can be seen in the <code>SnakeDrawHead</code> and <code>SnakeDrawTail</code> methods. Apart from
this it's a regular old snake clone.</p>
<h2>Thoughts</h2>
<p>I really enjoyed coding this, it's the first time I have felt productive in C. I am starting to grasp
arrays and pointers. I may even be beginning to think in a C manner. I kept everything in one file for
convenience. This helped me think more about what the code did, and less about where the code lived.</p>
<p>Anyway, take a look at the source code below. I'll get back to the main &quot;SlimeJump&quot; game in the next post.</p>
<p><magpie-trinket>Source code is also available at <a href="https://github.com/BrianDouglasIE/snake-c-raylib/tree/main">BrianDouglasIE/snake-c-raylib</a></magpie-trinket></p>
<pre><code class="language-c">#include &quot;raylib.h&quot;
#include &quot;math.h&quot;
#include &lt;stdlib.h&gt;
#include &lt;stdio.h&gt;
#include &lt;time.h&gt;

float percentOf(float percent, float number)
{
    return (percent / number) * 100.0f;
}

float asPercentOf(float number, float percent)
{
    return (percent / 100.0f) * number;
}

int randomInt(int min, int max)
{
    return (rand() % max) + min;
}

enum Direction {
    UP, DOWN, LEFT, RIGHT
};

struct Snake {
    int x;
    int y;
    float speed;
    float moveTimer;
    enum Direction direction;
    enum Direction nextDirection;
};

struct Snake SnakeCreate(int x, int y)
{
    return (struct Snake) {
        .x = x,
        .y = y,
        .speed = 0.2f,
        .moveTimer = 0,
        .direction = RIGHT,
        .nextDirection = RIGHT,
    };
}

void SnakeMoveInCurrentDirection(struct Snake *s)
{
    switch(s-&gt;direction)
    {
        case UP:
            s-&gt;y -= 1;
            break;
        case DOWN:
            s-&gt;y += 1;
            break;
        case LEFT:
            s-&gt;x -= 1;
            break;
        case RIGHT:
            s-&gt;x += 1;
            break;
        default:
            break;
    }
}

void SnakeScreenWrap(struct Snake *s, int colCount, int rowCount)
{
    if(s-&gt;x &lt; 0) {
        s-&gt;x = colCount - 1;
        s-&gt;direction = LEFT;
    }
    if(s-&gt;x &gt; colCount - 1) {
        s-&gt;x = 0;
        s-&gt;direction = RIGHT;
    }
    if(s-&gt;y &lt; 0) {
        s-&gt;y = rowCount - 1;
        s-&gt;direction = UP;
    }
    if(s-&gt;y &gt; rowCount - 1) {
        s-&gt;y = 0;
        s-&gt;direction = DOWN;
    }
}

void SnakeUpdate(struct Snake *s, float dt, struct Snake *target)
{
    s-&gt;moveTimer += dt;
    if(s-&gt;moveTimer &gt;= s-&gt;speed) {
        s-&gt;moveTimer -= s-&gt;speed;

        if(target == NULL) {
            if(s-&gt;direction != s-&gt;nextDirection)
            {
                s-&gt;direction = s-&gt;nextDirection;
            }

            SnakeMoveInCurrentDirection(s);
        } else {
            s-&gt;x = target-&gt;x;
            s-&gt;y = target-&gt;y;
            s-&gt;direction = target-&gt;direction;
        }
    }
}

void SnakeSetDirection(struct Snake *s, int dirX, int dirY)
{
    if(dirX &amp;&amp; dirY) return;
    if(!dirX &amp;&amp; !dirY) return;
    if(dirY &amp;&amp; (s-&gt;direction == UP || s-&gt;direction == DOWN)) return;
    if(dirX &amp;&amp; (s-&gt;direction == LEFT || s-&gt;direction == RIGHT)) return;

    if(dirX == -1) {
        s-&gt;nextDirection = LEFT;
    }
    if(dirX == 1) {
        s-&gt;nextDirection = RIGHT;
    }
    if(dirY == -1) {
        s-&gt;nextDirection = UP;
    }
    if(dirY == 1) {
        s-&gt;nextDirection = DOWN;
    }
}

void SnakeDrawHead(struct Snake *s, int cellSize, Color color)
{
    int w, h;
    float growthPercent = percentOf(s-&gt;moveTimer, s-&gt;speed);

    switch(s-&gt;direction)
    {
        case UP:
            h = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s-&gt;x * cellSize, (s-&gt;y * cellSize) + (cellSize - h), cellSize, h, color);
            break;
        case DOWN:
            h = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s-&gt;x * cellSize, s-&gt;y * cellSize, cellSize, h, color);
            break;
        case RIGHT:
            w = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s-&gt;x * cellSize, s-&gt;y * cellSize, w, cellSize, color);
            break;
        case LEFT:
            w = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle((s-&gt;x * cellSize) + (cellSize - w), s-&gt;y * cellSize, w, cellSize, color);
            break;
        default:
            break;
    }
}

void SnakeDrawTail(struct Snake *s, int cellSize, Color color)
{
    int w, h;
    float growthPercent = percentOf(s-&gt;moveTimer, s-&gt;speed);

    switch(s-&gt;direction)
    {
        case UP:
            h = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s-&gt;x * cellSize, s-&gt;y * cellSize, cellSize, cellSize - h, color);
            break;
        case DOWN:
            h = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s-&gt;x * cellSize, s-&gt;y * cellSize + h, cellSize, cellSize - h, color);
            break;
        case RIGHT:
            w = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s-&gt;x * cellSize + w, s-&gt;y * cellSize, cellSize - w, cellSize, color);
            break;
        case LEFT:
            w = floor(asPercentOf(cellSize, growthPercent));
            DrawRectangle(s-&gt;x * cellSize, s-&gt;y * cellSize, cellSize - w, cellSize, color);
            break;
        default:
            break;
    }
}

void SnakeDraw(struct Snake *s, int cellSize, Color color)
{
    DrawRectangle(s-&gt;x * cellSize, s-&gt;y * cellSize, cellSize, cellSize, color);
}

struct Apple {
	int x;
	int y;
	int active;
	float lifeSpan;
	float lifeTimer;
};

struct Apple AppleCreate()
{
	return (struct Apple) {
		.x = 0,
		.y = 0,
		.active = 0,
		.lifeSpan = 0.0f,
		.lifeTimer = 0.0f,
	};
}

void AppleUpdate(struct Apple *a, float dt)
{
	if(!a-&gt;active) return;

	a-&gt;lifeTimer += dt;
	if(a-&gt;lifeTimer &gt; a-&gt;lifeSpan)
	{
		a-&gt;lifeTimer -= a-&gt;lifeSpan;
		a-&gt;active = 0;
	}
}

void AppleActivateAt(struct Apple *a, int x, int y)
{
	a-&gt;x = x;
	a-&gt;y = y;
	a-&gt;active = 1;
	a-&gt;lifeTimer = 0;
	a-&gt;lifeSpan = 5.0f;
}

void AppleDraw(struct Apple *a, int cellSize, Color color)
{
	if(!a-&gt;active) return;
    DrawRectangle(a-&gt;x * cellSize, a-&gt;y * cellSize, cellSize, cellSize, color);
}

int collidesWithSnake(struct Snake s[], int snakeLength, int x, int y)
{
	for(int i = 0; i &lt; snakeLength; i++)
	{
		if(s[i].x == x &amp;&amp; s[i].y == y) return 1;
	}

	return 0;
}

Vector2 getRandomCellCoords(int colCount, int rowCount)
{
	int x = randomInt(0, colCount);
	int y = randomInt(0, rowCount);
	return (Vector2) { x, y };
}

int main(void)
{
    srand((unsigned)time(NULL));

    const int screenSize = 600;
    const int screenWidth = screenSize;
    const int screenHeight = screenSize;

    InitWindow(screenWidth, screenHeight, &quot;snake&quot;);
    SetTargetFPS(60);

    const int gridSize = 20;
    const int cellSize = floor(screenSize / gridSize);
    const int colCount = floor(screenWidth / cellSize);
    const int rowCount = floor(screenHeight / cellSize);
	
	int gameInProgress = 1;

    int snakeLength = 4;
    int snakeMaxLength = colCount * rowCount;
    struct Snake snake[snakeMaxLength];
    for(int i = 0; i &lt; snakeLength; i++)
    {
        snake[i] = SnakeCreate(floor(colCount / 2) - i, floor(rowCount / 2));
    }

	struct Apple apple = AppleCreate();

	float appleSpawnInterval = 8.0f;
	float appleSpawnTimer = 0.0f;

    while(!WindowShouldClose())
    {
        // update
		if(gameInProgress) {
			int up, down, left, right, dirX, dirY;

			float dt = GetFrameTime();

			up = IsKeyDown(KEY_UP) ? -1 : 0;
			down = IsKeyDown(KEY_DOWN) ? 1 : 0;
			left = IsKeyDown(KEY_LEFT) ? -1 : 0;
			right = IsKeyDown(KEY_RIGHT) ? 1 : 0;

			dirY = up + down;
			dirX = left + right;

			for(int i = snakeLength; i &gt; 0; i--)
			{
				SnakeUpdate(&amp;snake[i], dt, &amp;snake[i - 1]);
			}

			snake[snakeLength - 1].direction = snake[snakeLength - 2].direction;
			SnakeSetDirection(&amp;snake[0], dirX, dirY);
			SnakeUpdate(&amp;snake[0], dt, NULL);
			SnakeScreenWrap(&amp;snake[0], colCount, rowCount);

			if(apple.active &amp;&amp; snake[0].x == apple.x &amp;&amp; snake[0].y == apple.y)
			{
				apple.active = 0;
				snake[snakeLength] = snake[snakeLength - 1];
				snakeLength++;
			}

			if(!apple.active)
			{
				appleSpawnTimer += dt;
				if(appleSpawnTimer &gt; appleSpawnInterval)
				{
					appleSpawnTimer -= appleSpawnInterval;

					Vector2 newPos = getRandomCellCoords(colCount, rowCount);
					while(collidesWithSnake(snake, snakeLength, newPos.x, newPos.y)) {
						newPos = getRandomCellCoords(colCount, rowCount);
					}
					AppleActivateAt(&amp;apple, newPos.x, newPos.y);
				}
			}

			AppleUpdate(&amp;apple, dt);

			int tailLength = snakeLength - 1;
			struct Snake tail[tailLength];
			for(int i = 1; i &lt; tailLength; i++)
			{
				tail[i] = snake[i];
			}
			if(collidesWithSnake(tail, tailLength, snake[0].x, snake[0].y))
			{
				gameInProgress = 0;
			}
		}

        // draw
        BeginDrawing();
        ClearBackground(BLACK);

        for(int r = 0; r &lt; rowCount; r++)
        {
            for(int c = 0; c &lt; colCount; c++) {
                Color color = ((r + c) % 2) == 0 ? RAYWHITE : WHITE;
                DrawRectangle(c * cellSize, r * cellSize, cellSize, cellSize, color);
            }
        }

        for(int i = 1; i &lt; snakeLength - 1; i++)
        {
            SnakeDraw(&amp;snake[i], cellSize, DARKGREEN);
        }
        SnakeDrawHead(&amp;snake[0], cellSize, DARKGREEN);
        SnakeDrawTail(&amp;snake[snakeLength - 1], cellSize, DARKGREEN);

		AppleDraw(&amp;apple, cellSize, RED);

		if(!gameInProgress) {
			const char *gameOverText = &quot;GAME OVER&quot;;
			int fontSize = 40;
			int textLength = MeasureText(gameOverText, fontSize);

			int posX = floor(screenWidth / 2) - floor(textLength / 2);
			int posY = floor(screenHeight / 2);
			DrawText(gameOverText, posX, posY, fontSize, BLACK);
		}

        EndDrawing();
    }

    CloseWindow();
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[I&#x27;m off social media]]></title>
      <link>https://briandouglas.ie/off-socials</link>
      <guid>https://briandouglas.ie/off-socials</guid>
      <pubDate>2025-02-01T00:00:00.000Z</pubDate>
      <description><![CDATA[It didn&#x27;t last long, only a month.

]]></description>
      <content:encoded><![CDATA[
        <p><chicken-asks>I didn't bother following him, did you?</chicken-asks>
<magpie-replies>Not a chance.</magpie-replies></p>
<p>I joined x.com and followed lot's of influencial programmers. I tried sharing some of my stuff, etc...</p>
<p>But it gave me no joy. It just kinda wore me out. I remember when social media was young and interesting.
Now it really does just seem like a bot mine, and an echo chamber for the opinions you chose to hear.</p>
<p>Well as it gave me no joy, I simply deleted it. Once again I am social media less. And that makes me happy.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[GameDev in C Part 3: Drawing a Slime]]></title>
      <link>https://briandouglas.ie/gamedev-in-c-part-3</link>
      <guid>https://briandouglas.ie/gamedev-in-c-part-3</guid>
      <pubDate>2025-01-31T00:00:00.000Z</pubDate>
      <description><![CDATA[It&#x27;s part 3 and I am finally able to draw something meaningful on the screen.

]]></description>
      <content:encoded><![CDATA[
        <p>I've decided that the game will be a DoodleJump clone, named SlimeJump. The
goal will be to ascend to the highest platform possible. Sort of like a vertical
endless runner. The Slime's movement will be somewhat limited to add to the
difficulty. There will be a start menu and high score list. I will create the
sprites and sfx myself.</p>
<h2>The Slime</h2>
<p>I have decided to go with pixel art. This means that the Slime will consist of
16x16 pixel frames. Each frame will correspond to a bounding box that will be
displayed based on the Slime's state <em>(which will be the topic of part 4)</em> and
animation duration.</p>
<p>I've used a really nice image editor called <a href="https://www.aseprite.org/">Aseprite</a>
to create the pixel art for the slime. It had some preloaded color pallettes so
I decided to go for the default <em>pico-8</em> pallette that was availabel. I really liked
the bold popping colors. The resulting Slime sprite sheet is below. You can see that
I added an idle, walk, jump charge, damage, and death sequence. These will correspond
to Slime's game state.</p>
<img alt="The slime spritesheet" src="/images/slime.png" style="width: 500px; image-rendering: pixelated; margin: 2rem 0;" />
<magpie-trinket>
The image above is only 144x16 pixels in size. If you are wondering how he got it to render crisply at 500px width,
It's by adding the css rule <code>image-rendering: pixelated;</code>.
</magpie-trinket>
<h2>Rendering a texture</h2>
<p>When it comes to drawing the sprite on the screen. Raylib requires the texture to be
loaded first. Naturally this requires the <code>LoadTexture</code> method. This must be called
after the window is initialised. Otherwise you will see some random error that is not
at all self explanatory. The code I use to render the texture looks like this:</p>
<pre><code class="language-c">const Texture2D slime_texture = LoadTexture(&quot;assets/sprites/slime.png&quot;);
</code></pre>
<p>It's also important to remember that the texture needs to be unloaded. This is done like so,
outside of the game loop.</p>
<pre><code class="language-c">UnloadTexture(slime_texture);
</code></pre>
<p>In order to draw a single frame of the texture to the screen the <code>DrawTexturePro</code> method is used.
This will take a texture, along with a source (frame_rec), and dest (player_bounds). The resulting
code looks like this.</p>
<pre><code class="language-c">Rectangle frame_rec = { 0, 0, 16, 16 };
DrawTexturePro(slime_texture, frame_rec, player_get_bounds(&amp;player), sprite_origin, 0, WHITE);
</code></pre>
<p>If you have experience with raylib or gamedev in general, you notice that an important part is left out.
If this code were to run now, a 16x16 pixel slime would be drawn to the screen. Which would be much
to small to be usable. So the sprite needs to be scaled up. In order to do this a render texture must be
created.</p>
<p>A render texture is something that can be drawn to at a smaller scale, then drawn itself at a large scale
on the screen. To make this work a virtual screen width and height will be defined. I want the game window
to be 480x855 pixels in size. The virtual width of the game will be 160x285 pixels. This means that the Slime
will take up more space on the virtual screen, eg: it will look correctly sized. I define the screen sizes as
macros.</p>
<pre><code class="language-c">#define SCREEN_WIDTH 480.0f
#define SCREEN_HEIGHT 855.0f

#define V_SCREEN_WIDTH 160.0f
#define V_SCREEN_HEIGHT 285.0f
</code></pre>
<p>I then create the render texture, with a source rec (the virtual screen size), and a dest rec (the scaled screen size).</p>
<pre><code class="language-c">const RenderTexture2D render_texture = LoadRenderTexture(V_SCREEN_WIDTH, V_SCREEN_HEIGHT);
const Rectangle render_texture_src = {0, 0, V_SCREEN_WIDTH, -V_SCREEN_HEIGHT};

const float virtual_ratio = (float) SCREEN_WIDTH / (float) V_SCREEN_HEIGHT;

const Rectangle render_texture_dest = {
    -virtual_ratio, -virtual_ratio, SCREEN_WIDTH + (virtual_ratio * 2), SCREEN_HEIGHT + (virtual_ratio * 2)
};
</code></pre>
<p>The draw loop will then need to be updated as follows to account for drawing to the render texture. Then drawing
that render texture to the main out. Raylib provides the <code>BeginTextureMode</code> and <code>EndTextureMode</code> methods to accomodate
this like so:</p>
<pre><code class="language-c">BeginTextureMode(render_texture);
ClearBackground(RAYWHITE);
DrawTexturePro(slime_texture, frame_rec, player_get_bounds(&amp;player), sprite_origin, 0, WHITE);
EndTextureMode();

BeginDrawing();
DrawTexturePro(render_texture.texture,
        render_texture_src,
        render_texture_dest,
        sprite_origin,
        0,
        WHITE);
EndDrawing();
</code></pre>
<p>This will result in the following. Note the screenshot contains a floor which I have left out of these code examples.</p>
<p><img src="/images/slime-part-3.png" alt="the slime on screen"></p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Linting is procrastination.]]></title>
      <link>https://briandouglas.ie/linters</link>
      <guid>https://briandouglas.ie/linters</guid>
      <pubDate>2025-01-31T00:00:00.000Z</pubDate>
      <description><![CDATA[Imagine this scenario. A developer has an opinion. They feel that it is vitally important that code
adhere to an arbitrary style guide.

]]></description>
      <content:encoded><![CDATA[
        <p>The rules of the style guide do not in anyway impact the function of the code. They do not benefit the
end user in any way. But our imaginary developer feels that these rules, and the enforcement there of,
are top priority. The product that they are working on may require new features or performance improvements.
The company that owns the product may have tight deadlines and financial incentives. These are simply not
important to our developer. Our developer cannot deliver any new features, or even think about the needs of
the employer or end user, when they fear that a styleguide rule may not be enforced.</p>
<p>So they make this their priority. They spend time, and time is money, on adding a tool like eslint to the
project. This will flag any style guide violations to other devs. BUT that is not enough. These rules
cannot simply be enforced locally. They must be added to the CI/CD pipeline. Not added as warnings, but
added as errors. That will cause the 32 minute pipeline to fail because some perfectly valid code does not
adhere to the developer's precious opinion. So now not only have they spent 2 weeks of their own time
enforcing a style guide and adding linters, and tweaking those linters. They now cost other developers
time. Because a javascript string used the incorrect, BUT PERFECTLY VALID, quotation mark.</p>
<p>Then to waste even more time, enforcement of rules and style guides are brought up in scrum meetings.
Where countless hours are lost arguing about opinions that do not benefit the end user in any way.
Countless hours wasted, countless money wasted, no features delivered.</p>
<p>Procrastination is &quot;the action of delaying or postponing something&quot;. In this case the concept of style guides
and linters are delaying the delivery of features. Developers are postponing actual work, in favour of
arguing about arbitrary opinions.</p>
<p>So to end my rant, Linting is procrastination, EXPENSIVE procrastination.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[GameDev in C Part 2: Raylib]]></title>
      <link>https://briandouglas.ie/gamedev-in-c-part-2</link>
      <guid>https://briandouglas.ie/gamedev-in-c-part-2</guid>
      <pubDate>2025-01-23T00:00:00.000Z</pubDate>
      <description><![CDATA[This installment mimics the previous, in that most of time is spent
trying to get a fiile to build. Luckily though, this was much quicker.

]]></description>
      <content:encoded><![CDATA[
        <p>Having done the impossible <a href="/gamedev-in-c-part-1">last time round</a> and
made a C file compile. This time I was tasked with adding an external
library. The library in question is <a href="https://github.com/raysan5/raylib/">raylib</a>
which I will be using to develop a game. I've used raylib before with C# and
really enjoyed it. The process for adding it as a dep there was much more
straight forward. Anyway, here is my experience setting up raylib with C.</p>
<h2>The installer</h2>
<p>Raylib comes with a windows installer. It's available on <a href="https://raysan5.itch.io/raylib/purchase">itch.io</a>.
I started off by downloading this and running the installer. Installing raylib
locally at <code>C:\raylib</code>. This was the easy part. Now I had to figure out how to
make it available to my C file.</p>
<h2>Adding raylib to make</h2>
<p>I was aware that raylib needed to be included somehow, and I was fairly sure that
doing it via make was the best idea. But before I jumped into that I added the
following example to my <code>main.c</code> file.</p>
<pre><code class="language-c">#include &quot;raylib.h&quot;

//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
int main(void)
{
    // Initialization
    //--------------------------------------------------------------------------------------
    const int screenWidth = 800;
    const int screenHeight = 450;

    InitWindow(screenWidth, screenHeight, &quot;raylib [core] example - basic window&quot;);

    SetTargetFPS(60);               // Set our game to run at 60 frames-per-second
    //--------------------------------------------------------------------------------------

    // Main game loop
    while (!WindowShouldClose())    // Detect window close button or ESC key
    {
        // Update
        //----------------------------------------------------------------------------------
        // TODO: Update your variables here
        //----------------------------------------------------------------------------------

        // Draw
        //----------------------------------------------------------------------------------
        BeginDrawing();

            ClearBackground(RAYWHITE);

            DrawText(&quot;Congrats! You created your first window!&quot;, 190, 200, 20, LIGHTGRAY);

        EndDrawing();
        //----------------------------------------------------------------------------------
    }

    // De-Initialization
    //--------------------------------------------------------------------------------------
    CloseWindow();        // Close window and OpenGL context
    //--------------------------------------------------------------------------------------

    return 0;
}
</code></pre>
<p>This is pretty much the &quot;Hello, World!&quot; of the raylib world. It displays a simple window
with a message.</p>
<p>The problem I had now is that if I ran make build I would get this error:</p>
<pre><code>main.c:1:10: fatal error: raylib.h: No such file or directory
    1 | #include &quot;raylib.h&quot;
      |          ^~~~~~~~~~
</code></pre>
<p>Meaning that gcc couldn't find the raylib header to include it. This meant updating the Makefile.</p>
<p>The working Makefile looks like this:</p>
<pre><code>CC = gcc
CFLAGS = -I&quot;C:\raylib\raylib\src&quot;
LDFLAGS = -L&quot;C:\raylib\raylib\src&quot; -lraylib -lgdi32 -lwinmm

build : main.o
	gcc -o build.exe main.o $(LDFLAGS)

main.o : main.c
	gcc -c main.c $(CFLAGS)

clean : 
	powershell Remove-Item *.exe, *.o
</code></pre>
<p>The issues that I ran into were figuring out what <code>LDFLAGS</code> to include. I had initial only <code>-lraylib</code>.
Which didn't work. The solution was found in the raylib <a href="https://github.com/raysan5/raylib/wiki/Working-on-Windows#manual-setup-with-w64devkit">docs</a>.
Once the correct flags were added and <code>make build</code> had run succesfully I could launch the <code>build.exe</code>.
Which generated this beautiful screen.</p>
<p><img src="/images/first-window-raylib.png" alt="The hard work pays off."></p>
<p><chicken-asks>He's very proud of that window</chicken-asks>
<magpie-replies>It's the simple things...</magpie-replies></p>
<p>Now I can start building a game!</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[GameDev in C Part 1: Hello World]]></title>
      <link>https://briandouglas.ie/gamedev-in-c-part-1</link>
      <guid>https://briandouglas.ie/gamedev-in-c-part-1</guid>
      <pubDate>2025-01-21T00:00:00.000Z</pubDate>
      <description><![CDATA[This is the start of a new blog series. I will be documenting
my journey into building a game in C using [Raylib](https://www.raylib.com/).

This post details installing a C compiler and running a simple
program.

]]></description>
      <content:encoded><![CDATA[
        <h2>I'll be upfront</h2>
<p>For brevity I have given the post the subtitle <em>Hello World</em>.
The unabreviated title is <em>why the fuck is this so hard</em>.</p>
<p>Basically it took me waaay to long to install a C compiler, compile
a file using make, and then run that file to print &quot;Hello World&quot;.
It gave me flash backs to my first year at uni trying to do the same
thing. It wasn't easy then either. But now, 12 years of professional
software engineering later, it's still a pain. A pain which I have
documented below. In the hope that it will help someone on their
journey to run and compile a basic C program. Realistically it will
only ever  help future Brian.</p>
<h2>How do I install gcc?</h2>
<magpie-trinket>
Note: Windows 11 is the operating system used. The shell is PowerShell.
</magpie-trinket>
<p>I'm on Windows and I use <a href="https://chocolatey.org/">chocolatey</a> as a package
manager. So the first step is get chocolatey installed. Then run PowerShell
as an admin. As an ADMIN, remember that, or like me you'll have to search in
windows for the terminal and then right click the icon and select &quot;run as admin&quot;.
There is no sudo here.</p>
<p>Now I bet you are thinking I just run <code>choco  install gcc</code>. Well you would be wrong.
In windows <code>gcc</code> is made available through <a href="https://www.mingw-w64.org/">MingW</a>. So
the command we run to  install <code>gcc</code> is:</p>
<pre><code>choco install mingw
</code></pre>
<h2>make build</h2>
<p>It's entirely possible to compile and run a simple program using <code>gcc</code> alone. But I
want some sort of build system. So let's install make:</p>
<pre><code>choco install make
</code></pre>
<p>The first program I want to run is as follows.</p>
<pre><code class="language-c">// main.c
#include &lt;stdio.h&gt;

int main() {
   printf(&quot;Hello, World!&quot;);
   return 0;
}
</code></pre>
<p>Now it's here I make a time costing mistake. I use the following command to compile
the program.</p>
<pre><code>gcc main.c -o build.exe
</code></pre>
<p>A seasoned C programmer will see the problem straight away. Not me. The command generates
<code>build.exe</code>. Now I run it with <code>./build.exe</code>. I should see &quot;Hello, World!&quot; printed to the
console. Instead I see the following.</p>
<pre><code>Program 'build.exe' failed to run: The specified executable is not a valid application for this OS platform.At line:1
char:1
+ .\build.exe
+ ~~~~~~~~~~~.
At line:1 char:1
+ .\build.exe
+ ~~~~~~~~~~~
    + CategoryInfo          : ResourceUnavailable: (:) [], ApplicationFailedException
    + FullyQualifiedErrorId : NativeCommandFailed
</code></pre>
<p>Ok, so the error is telling me that the specified executable is not correct for the OS.
So do I add <code>-m64</code> and make the command <code>gcc -m64 -c main.c -o build.exe</code>? Nope same
error. So fast forward 2 hours, and I figure it out.</p>
<pre><code>-c                       Compile and assemble, but do not link.
</code></pre>
<p>The <code>-c</code> flag is not needed. So I run:</p>
<pre><code>gcc -c main.c -o build.exe
</code></pre>
<p>And voila <code>build.exe</code> prints &quot;Hello, World!&quot; to the console. Now I add the following
content to my make file.</p>
<pre><code>build : main.o
	gcc -o build.exe main.o

main.o : main.c
	gcc -c main.c

clean : 
	powershell Remove-Item *.exe, *.o
</code></pre>
<p>This allows me to run useful commands like <code>make clean</code>, and <code>make build</code>.</p>
<p>And that's a wrap for part 1. Tomorrow I will look at adding raylib to the project.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Homer&#x27;s Drinking Bird]]></title>
      <link>https://briandouglas.ie/drinking-bird</link>
      <guid>https://briandouglas.ie/drinking-bird</guid>
      <pubDate>2025-01-20T00:00:00.000Z</pubDate>
      <description><![CDATA[Do you work for a large multinational? Do you have a minimal amount of work to do
during the day? Have they set your screen to go to sleep after 5 minutes of no
interaction?

Well then, look no further.

]]></description>
      <content:encoded><![CDATA[
        <p>I have a simple Python script that you can run. It will simulate a mouse's right
click at a set interval.</p>
<p><chicken-asks>Why a right click?</chicken-asks>
<magpie-replies>
He tried making the mouse move, but the screen still went to sleep.
A left click might take an action, so right clicking is safer.
</magpie-replies></p>
<h2>The script</h2>
<p>I call this script the &quot;drinking bird&quot; after that episode of the Simpsons
where Homer entrusted the nuclear power plant's safety over to a novelty toy.</p>
<p><img src="/images/drinking-bird.gif" alt="Homer's Drinking Bird"></p>
<p>This script uses the <a href="https://pypi.org/project/mouse/">mouse</a> package. Install
it via pip.</p>
<pre><code class="language-python">import sched, time, mouse

INTERVAL = 120

def do_right_click(scheduler):
    scheduler.enter(INTERVAL, 1, do_right_click, (scheduler,))
    mouse.right_click()

my_scheduler = sched.scheduler(time.time, time.sleep)
my_scheduler.enter(INTERVAL, 1, do_right_click, (my_scheduler,))
my_scheduler.run()
</code></pre>
<p>As long as this is running, your screen won't go to sleep, and if you have your
company's chat client open, it will look like you are active.</p>
<p>Good luck on your side projects....</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[I joined social media]]></title>
      <link>https://briandouglas.ie/i-joined-social-media</link>
      <guid>https://briandouglas.ie/i-joined-social-media</guid>
      <pubDate>2025-01-04T00:00:00.000Z</pubDate>
      <description><![CDATA[Twitter to be exact. Please become an apostle of mine. My handle is [BrianDouglasIE](https://x.com/BrianDouglasIE).

]]></description>
      <content:encoded><![CDATA[
        
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[2025 new year&#x27;s resolutions]]></title>
      <link>https://briandouglas.ie/2025-resolutions</link>
      <guid>https://briandouglas.ie/2025-resolutions</guid>
      <pubDate>2025-01-02T00:00:00.000Z</pubDate>
      <description><![CDATA[In 2024 I started this blog. I was somewhat successful in that I churned out 21 posts
in 6 months.

]]></description>
      <content:encoded><![CDATA[
        <p>My goal for this year is to hit the 50 mark, and I know what you are thinking. <em>Surely quality
is better than quantity?</em> Well yes that is true. But I believe that in order to achieve
quality I must first produce in quantity.</p>
<p><chicken-asks>It's true, most of the time his quality is quite low.</chicken-asks>
<magpie-replies>Well at least he's trying...</magpie-replies></p>
<h2>So what will I be blogging about in 2025?</h2>
<p>Well, in course with attempting to write at least 50 blog posts. I will be attempting to build and ship 12 <em>things</em>.
A <em>thing</em> could be an app, a website, or a game. The important part is the building and shipping. In 2024
I started and failed many projects. This year I want to have some more perceverence. Completing a project
every month is something I feel is both a challenge and achievable. It doesn't have to be world changing,
just <em>complete</em> to the extent where I feel <em>done</em>.</p>
<p>So with that in mind, I will try to document my learnings, achievements, and failures. I hope that in
doing so I will improve my writing skills. I also want to try adding some video content. Basically
I want to be an influencer and advertise random scams to people...</p>
<h2>Predictions for 2025?</h2>
<p>Max Verstappen will win his fifth world title and sign a deal with Mercedes. The world won't end. My family will
expand, and so will my waistline.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Setting up PostgreSQL and Lua in Docker]]></title>
      <link>https://briandouglas.ie/lua-postgres</link>
      <guid>https://briandouglas.ie/lua-postgres</guid>
      <pubDate>2024-12-31T00:00:00.000Z</pubDate>
      <description><![CDATA[I did a quick exploration into Lua after seeing is used in the CS50 game dev course.

]]></description>
      <content:encoded><![CDATA[
        <magpie-trinket>
The environment used for this tutorial is Ubuntu running inside WSL.
</magpie-trinket>
<h2>Creating the PostgreSQL container</h2>
<pre><code>docker run --name postgres-1 -e POSTGRES_PASSWORD=password -d -p 5432:5432 postgres:alpine
</code></pre>
<p>This command creates an instance of the <code>postgres:alpine</code> container. The <code>--name</code> flag is used to assign a name to the container.
The <code>-e</code> flag passes in the required environment variables. In this case a <code>POSTGRES_PASSWORD</code> is required. This will be used to
authenticate the <code>postgres</code> user. <code>-d</code> tells docker to run the command as a background process. Finally <code>-p</code> is used to expose
the port that postgres is running on. The default is <code>5432</code>, so we map the container's <code>5432</code> to <code>localhost:5432</code>.</p>
<h2>Create a database and add some content</h2>
<h3>Access the PostgreSQL instance running inside the Docker container</h3>
<pre><code>docker exec -it postgres-1 psql -h localhost -p 5432 -U postgres
</code></pre>
<p>This command can be broken into two parts. The first part <code>docker exec -it postgres-1</code>, tells docker the execute the following
command inside our previously created container. The <code>-it</code> flag is a combination of <code>--interactive</code> and<code>--tty</code>. This tells
docker to take you straight inside the container.</p>
<p>The second part of the command <code>psql -h localhost -p 5432 -U postgres</code> takes us inside the postgres server. The <code>-h</code> flag
specifies the the host, <code>-p</code> specifies the port, and <code>-U</code> the user we want to login as.</p>
<p>Once the initial command is run you should see the postgres prompt. We can now create a database and populate it with some information.</p>
<h3>Create and modify database</h3>
<p>Once inside the postgres instance we can use the following commands to create a database, add a table to
that database, and finally insert data into the table.</p>
<pre><code>create database test;
</code></pre>
<p>The above command creates a database named &quot;test&quot;. We can then connect to that database using the following command.</p>
<pre><code>\c test;
</code></pre>
<p>Once connected we can run the following to add the <code>messages</code> table to the test database.</p>
<pre><code>create table if not exists messages();
alter table messages add column id uuid not null primary key;
alter table messages add column content varchar(255);
insert into messages (id, content) values (uuid_generate_v4(), 'Hi, from postgres');
</code></pre>
<blockquote>
<p>Install the <code>uuid-ossp</code> extension with the following command <code>create extension if not exists &quot;uuid-ossp&quot;;</code></p>
</blockquote>
<p>After successfully running the above our <code>test</code> database will now have some content which we will fetch with lua.</p>
<h2>Querying the database with Lua</h2>
<h3>Installing luasql via LuaRocks</h3>
<p>In order to connect to our postgres database with Lua, we will use the <code>luasql.postgres</code> rock.
To install this rock run the following command <code>luarocks install luasql-postgres</code>.</p>
<h4>Fixing the <code>libpq-fe.h</code> install error on Ubuntu</h4>
<p>If you are using Ubuntu to follow this tutorial, you may see the following error when running <code>luarocks install luasql-postgres</code>.</p>
<pre><code>Error: Could not find header file for PGSQL
  No file libpq-fe.h in /usr/local/include
  No file libpq-fe.h in /usr/include
  No file libpq-fe.h in /include
You may have to install PGSQL in your system and/or pass PGSQL_DIR or PGSQL_INCDIR to the luarocks command.
</code></pre>
<p>This can be solved by passing the <code>PGSQL_INCDIR</code> property to the progam. Commonly the required value is
<code>/usr/include/postgresql/</code>. As that is the directory containing the <code>libpg-fe.h</code>.</p>
<p>Run the command as follows to successfully install <code>luasql-postgres</code>.</p>
<pre><code>luarocks install luasql-postgres PGSQL_INCDIR=/usr/include/postgresql
</code></pre>
<h3>Writing a lua script to print the messages</h3>
<p>Once <code>luasql-postgres</code> has successfully been installed, we can then write a Lua program to fetch records
from the database we created earlier.</p>
<p>Create a <code>main.lua</code> file and add the following.</p>
<pre><code class="language-lua">-- load driver
local driver = require &quot;luasql.postgres&quot;
-- create environment object
env = assert (driver.postgres())
-- connect to data source
con = assert (env:connect(&quot;test&quot;))
-- reset our table
cur = con:execute&quot;SELECT * FROM messages;&quot;
row = cur:fetch ({}, &quot;a&quot;)
while row do
  print(string.format(&quot;MESSAGE: %s&quot;, row.content))
  -- reusing the table of results
  row = cur:fetch (row, &quot;a&quot;)
end
-- close everything
cur:close() -- already closed because all the result set was consumed
con:close()
env:close()
</code></pre>
<h3>Run the script with the correct env variables</h3>
<p>The script written inside our <code>main.lua</code> file expects that we supply some postgres variables when running it.
These are <code>PGUSER</code>, the user we want to connect to our database with. <code>PGPASSWORD</code>, the password set for the
user. <code>PGHOST</code>, the host we will use to access the server. And finally, <code>PGPORT</code>, the port number at which we
can access the postgres server.</p>
<pre><code>PGUSER=postgres PGPASSWORD=password PGDATABASE=lua_messages PGHOST=localhost PGPORT=5432 lua main.lua
</code></pre>
<p>Running the above script should print out the content of the messages we added to our postgres database.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Rust, the Cunning Wizard]]></title>
      <link>https://briandouglas.ie/rust-the-cunning-wizard</link>
      <guid>https://briandouglas.ie/rust-the-cunning-wizard</guid>
      <pubDate>2024-12-05T00:00:00.000Z</pubDate>
      <description><![CDATA[Oh wise ai of the gippity.

]]></description>
      <content:encoded><![CDATA[
        <p>I am an adventurer. I seek to adventure in the realms of computer science. But I cannot go alone. I must take a trusty companion. A programming language that will accompany me into the lofty peaks of web development, the dark dungeons of game dev, and the busy taverns of app development. I have seen many who may fit this role, but alas I lack the genious needed to make a decision. I leave it to fate and your wisdom to choose my companion. You may choose an outsider, a rogue, who is less well known but has potential, or maybe a hero who has fallen from grace and seeks fame once more, or yet again maybe a cunning wizard who is specialized and mighty in his field of sorcery.</p>
<p>The decision is yours to make.</p>
<hr>
<p>Ah, noble adventurer! Your quest is most ambitious, seeking mastery across the realms of web, game, and app development. You require a loyal companion—a language versatile yet skilled, simple to start yet deep in wisdom. Allow me to choose a companion that suits your daring journey.</p>
<h2>The Hero Reborn: Python</h2>
<p>A seasoned hero, revered in many domains but now hungers to prove its worth in the lands of web and game dev. Python is approachable, wielding a clean syntax and an arsenal of libraries. With Django and Flask, it thrives in the bustling web taverns, while Pygame offers tools for casual game creation. Though not the mightiest in performance-heavy battles, Python's charm and adaptability make it a steadfast ally.</p>
<h2>The Cunning Wizard: Rust</h2>
<p>A rising star, Rust wields the enchanted tome of memory safety without sacrificing performance. For game dungeons and app battlegrounds where performance is life and bugs are death, Rust is peerless. While its spells for web sorcery, like Rocket and Axum, are in their infancy, they grow strong with each passing season. Rust demands patience, but the rewards are unmatched—fearlessness and power.</p>
<h2>The Rogue Outlier: Dart</h2>
<p>An underdog, Dart is the shadow that walks in Flutter's cloak, a single framework to rule cross-platform app development with ease. While less battle-tested in game dev, Dart’s nimbleness and vibrant ecosystem hold promise. For web sorcery, its role is less pronounced but capable. The rogue knows the value of swift iteration and broad reach.</p>
<h2>The Bold Jack-of-All-Trades: JavaScript/TypeScript</h2>
<p>A legend that walks all lands—web is its true dominion, with React, Node.js, and Next.js carving its legacy. In game development, JavaScript can wield tools like Three.js for immersive 3D adventures, or partner with game engines like Phaser. Though not the mightiest in app development, it joins hands with React Native for valiant efforts.</p>
<h2>My Choice: Rust, the Cunning Wizard</h2>
<p>Rust is my pick, for it combines strength, safety, and boundless potential. It is a rare companion that challenges you to grow while safeguarding your journey. Its versatility spans game dungeons, app citadels, and web towers, and though its learning curve is steep, the rewards are as vast as your ambition.</p>
<p>May you and your companion forge an unbreakable bond and conquer the realms of computer science! Go forth, adventurer, and let your saga be one of legend!</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Partial function implementation]]></title>
      <link>https://briandouglas.ie/partial-implementation</link>
      <guid>https://briandouglas.ie/partial-implementation</guid>
      <pubDate>2024-11-25T00:00:00.000Z</pubDate>
      <description><![CDATA[Partial function implementation is a functional programming technique that
can be used to create many functions from one, by binding fixed arguments.

]]></description>
      <content:encoded><![CDATA[
        <magpie-trinket>
Another name for this is <i>currying</i>. You can read more about it at 
<a href="https://en.wikipedia.org/wiki/Currying">wiki/currying</a>.
</magpie-trinket>
<h2>Example</h2>
<p>The common way of explaining this is by using the following math example.
Say we have a function called <code>add(a: number, b: number)</code>. We could use this
to create a function called <code>addOne</code> by binding one as the first argument
like so:</p>
<pre><code class="language-typescript">const add = (a: number, b: number) =&gt; a + b
const addOne = partial(add, 1)

addOne(2) // 3
</code></pre>
<p>The above is a naive example, but I'm sure that you can see how this would
be of benefit in more complex examples. Perhaps it could be use to build
up config methods in your application or to create methods that work on a
certain instance of an object, where you cannot modify the source code.</p>
<h2>Implementation</h2>
<p>Implementing a method that can partially apply arguments to other methods is
somewhat trivial thanks to ECMAScript's syntactic sugar.</p>
<p>Below is a Typed and untyped example.</p>
<pre><code class="language-typescript">// Untyped
const partial = (fn, ...fixedArgs) =&gt; (...args) =&gt; fn(...fixedArgs, ...args)

// Typed... and ugly
function partial&lt;T, U extends any[], V extends any[]&gt;(
  fn: (...args: [...U, ...V]) =&gt; any,
  ...fixedargs: U
): (...args: V) =&gt; any {
  return (...args: V) =&gt; fn(...fixedargs, ...args);
}
</code></pre>
<p>There is possibly a nicer way to type this, that does not use <code>any</code>. However I don't
feel like wrestling with the type system. Mainly because the use of <code>partial</code> implementation
lends itself better to loosely typed programming.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Chicken Farmer, a Snake clone]]></title>
      <link>https://briandouglas.ie/chicken-farmer</link>
      <guid>https://briandouglas.ie/chicken-farmer</guid>
      <pubDate>2024-11-18T00:00:00.000Z</pubDate>
      <description><![CDATA[I set myself a goal, make a game in 3 hours. The results, a snake
clone where you play a farmer collecting eggs that turn into chickens.

]]></description>
      <content:encoded><![CDATA[
        <p><chicken-asks>I play a staring role in this game.</chicken-asks>
<magpie-replies>It's true, he told me you were his inspiration.</magpie-replies></p>
<h2>Implementation</h2>
<p>The game is written in vanilla js and doesn't use any libraries. It's all rendered via
the canvas api. Using the techniques I mentioned in <a href="/useful-image-data-methods/">&quot;useful-image-data-methods&quot;</a>.</p>
<p>I made the sound effects myself. I bet you thought it was a really chicken and duck.</p>
<p><magpie-replies>Nobody thought that...</magpie-replies></p>
<h2>Play the game</h2>
<p>You can play the game at <a href="/games/chicken-farmer/">/games/chicken-farmer/</a>.</p>
<h2>Read the source code</h2>
<p>You can read the ugly source code at <a href="https://github.com/BrianDouglasIE/chicken-farmer">BrianDouglasIE/chicken-farmer</a>.</p>
<p>It was written very quickly, so give me some slack.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Useful ImageData Methods]]></title>
      <link>https://briandouglas.ie/useful-image-data-methods</link>
      <guid>https://briandouglas.ie/useful-image-data-methods</guid>
      <pubDate>2024-11-16T00:00:00.000Z</pubDate>
      <description><![CDATA[When creating a 2d game on canvas I like to render the sprites on an offscreen canvas and access them
via retrieving their ImageData.

]]></description>
      <content:encoded><![CDATA[
        <p>Usually this will look something like the following, when working on a 2d grid based game.</p>
<pre><code class="language-javascript">function getSprite(spritesheetCtx, x, y, size) {
	return spritesheetCtx.getImageData(size * x, size * y, size, size)
}
</code></pre>
<p>Below are three useful methods that build on this technique.</p>
<h2>Clone ImageData</h2>
<p>When <code>ctx.getImageData</code> is called, it returns an <code>ImageData</code> object. This <code>ImageData</code> object can then
be rendered and manipulated. When manipulating the <code>ImageData</code> object we will want to create a copy
so that the original is not altered. To do this we use the following method.</p>
<pre><code class="language-javascript">function cloneImageData(imageData) {
	const data = new Uint8ClampedArray(imageData.data)
	return new ImageData(data, imageData.width, imageData.height)
}
</code></pre>
<p>The data held by an <code>ImageData</code> instance is a <code>Uint8ClampedArray</code>. To copy the data a new <code>Uint8ClampedArray</code>
is instantiated from the original <code>ImageData</code> object and passed as a constructor argument to the new one.</p>
<h2>Color Mask</h2>
<p>I like to use Kenny's massive <a href="https://kenney-assets.itch.io/1-bit-pack">one bit tile pack</a> when creating small
2d games. I use the non-colored white version. This allows me to add a color mask to the sprites, so that I
can have them be any color I like.</p>
<p>Again I will render the spritesheet on an offscreen canvas and pre-fetch the sprites before game starts. I use
the following method in order to apply a color mask to the sprites.</p>
<pre><code class="language-javascript">function applyColorMaskToImageData(imageData, maskColor) {
	const { data } = imageData
	const [maskR, maskG, maskB, maskA] = maskColor;

	for (let i = 0; i &lt; data.length; i += 4) {
		const r = data[i];
		const g = data[i + 1];
		const b = data[i + 2];
		const a = data[i + 3];

		data[i] = (r * maskR) / 255;
		data[i + 1] = (g * maskG) / 255;
		data[i + 2] = (b * maskB) / 255;
		data[i + 3] = (a * maskA) / 255;
	}

	return imageData
}
</code></pre>
<p>This method takes and <code>ImageData</code> object, which should be a copy from <code>cloneImageData</code>, and applies the passed in
rgba values as a mask. This can be used like so.</p>
<pre><code class="language-javascript">// initial sprite to copy
const skeletonSprite = getSprite(spritesheetCtx, 1, 1)
// sprite with red color mask applied
const redSkeletonSprite = applyColorMaskToImageData(cloneImageData(skeletonSprite), [255, 0, 0, 0])
</code></pre>
<h2>Flip Horizontally</h2>
<p>Tile packs usually only contain sprites facing in one direction. This means that if a sprite is facing
left and you want it to face right, you will need to <em>flip</em> the sprite on a horizontal access. Again
this is achieved by modifying the <code>ImageData</code>.</p>
<pre><code class="language-javascript">function flipImageDataHorizontally(imageData) {
	const { width, height, data } = imageData;
	const flippedData = new Uint8ClampedArray(data.length);

	for (let y = 0; y &lt; height; y++) {
		for (let x = 0; x &lt; width; x++) {
			const srcIndex = (y * width + x) * 4;
			const destIndex = (y * width + (width - x - 1)) * 4;

			flippedData[destIndex] = data[srcIndex];
			flippedData[destIndex + 1] = data[srcIndex + 1];
			flippedData[destIndex + 2] = data[srcIndex + 2];
			flippedData[destIndex + 3] = data[srcIndex + 3];
		}
	}

	return new ImageData(flippedData, width, height);
}
</code></pre>
<p>With the sprite now flipped on it's horizontal acces it can be used like so.</p>
<pre><code class="language-javascript">// initial sprite to copy
const skeletonSprite = getSprite(spritesheetCtx, 1, 1)
// left facing instance of ImageData
const leftFacingSkeletonSprite = flipImageDataHorizontally(cloneImageData(skeletonSprite))
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Using the form attribute on buttons]]></title>
      <link>https://briandouglas.ie/form-attribute-on-buttons</link>
      <guid>https://briandouglas.ie/form-attribute-on-buttons</guid>
      <pubDate>2024-10-31T00:00:00.000Z</pubDate>
      <description><![CDATA[HTML buttons have a form attribute that can be used with buttons of &#x60;type&#x3D;&quot;submit&quot;&#x60;.
The form attribute is used to target a specific form.

]]></description>
      <content:encoded><![CDATA[
        <p>By default a button of type submit will submit the form that it is a child of.
In some cases it may be necessary to submit a form that the button is not a child
of. This is where the <code>form</code> attribute comes in to use.</p>
<magpie-trinket>
The MDN docs for the form attribute can be found at <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#form">HTML/Element/button#form</a>
</magpie-trinket>
<h2>Example usage</h2>
<p>Let's say we have a confirmation modal that appears when a button is clicked. This
could be to confirm whether or not a user wants to commit changes to a resource. It
is likely that the HTML for the modal dialogue is not contained within the original
form. So when the <em>confirm</em> button is clicked inside the modal it will not submit the
original form. Unless of course JavaScript is used to listen for the confirm event.</p>
<p>Adding a form attribute to the <em>confirm</em> button specifying which form to submit is a
much nicer way to solve this. For example the <code>button</code> below will submit the <code>form</code>
with an <code>id=&quot;updateResource&quot;</code>. Without requiring any extra JavaScript.</p>
<pre><code class="language-html">&lt;button form=&quot;updateResource&quot; type=&quot;submit&quot;&gt;Confirm&lt;/button&gt;
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[I wrote a test framework in Julia]]></title>
      <link>https://briandouglas.ie/beedeedee-julia-test-framework</link>
      <guid>https://briandouglas.ie/beedeedee-julia-test-framework</guid>
      <pubDate>2024-10-21T00:00:00.000Z</pubDate>
      <description><![CDATA[I didn&#x27;t set out to write a test framework in Julia. But I ended up
going down the rabbit hole, and learning a lot about the language
on the way.

]]></description>
      <content:encoded><![CDATA[
        <chicken-asks>
Hold up! He wrote a test framework in Julia?
</chicken-asks>
<magpie-replies>
Yea, he must think he's a polyglot or something.
</magpie-replies>
<h2>Why Julia?</h2>
<p>Well that is a straight forward one to answer. I was watching a Formula 1
race and noticed that JuliaHub is a sponsor of the Williams F1 team. So
naturally I thought I better try out the language and so I set about writing
some programs in Julia.</p>
<p>Julia is mainly used in the academic and scientific realms. But at it's core
it is quite a nice general purpose language.</p>
<p>It is similar to Python, but it does not support OOP. Instead it uses pattern
matching with method overloads to handle different data types. For example let's
say we have a greeter function that can greet students and teachers. In Julia
that might look like this.</p>
<pre><code class="language-julia">function greet(person::Teacher)
  return &quot;Hello Teacher&quot;
end

function greet(person::Student)
  return &quot;Hello Student&quot;
end
</code></pre>
<p>The correct variant of greet get's called based on the type of <code>struct</code> that is
passed to the method. A <code>struct</code> being the way to define a custom data type.
This takes a little bit of getting used to. But once I got my head around the fact
that Julia operators were methods it started to make sense. This clicked when
it came to writing comparators for <a href="https://github.com/BrianDouglasIE/BeeDeeDee.jl/tree/main">BeeDeeDee.jl</a> the test framework I ended up writing.</p>
<p>But <strong>why</strong> did I end up writing a test framework? I hear you ask. Well because the built
in Julia test framework lacked features that I wanted and would expect every test
framework to have. For example hooks, like <code>before_each</code> and <code>after_all</code>, as well
as human readable methods. The latter being something that the Julia community
seem to readily reject. Opting to use mathematic operators where they can. Which
is no surprise when Julia's scientific background is taken into account.</p>
<p>So I set off into the unknown and started writing a test framework. I had a clear
idea in mind of what I wanted to achieve. That being an api that looked like the following.</p>
<pre><code class="language-julia">describe(&quot;User Age Tests&quot;) do
  it(&quot;should have an age greater than or equal 18 and less than 100&quot;) do
    expect(user.age) |&gt;
      to_be_greater_than_or_equal(18) |&gt; and |&gt; to_be_less_than(100)
  end
end
</code></pre>
<h2>The implementation</h2>
<p>Starting off with the code I wanted to achieve was a great help. I knew that I wanted to
make use of method chaining using Julia's pipe operator <code>|&gt;</code>. The reason for this being
that it allows for clear an readable code. It also allows for a data structure to
be passed through and transformed by multiple methods. This would be necessary for assertions.</p>
<p>In BeeDeeDee the <code>expect</code> method takes a value and creates an <code>Expectation</code> struct based off of
that value. The <em>comparators</em>, which are methods that are called against the assertion, then
take the value of the assertion and apply their own logic. These are essentially methods which
check for the correct <code>boolean</code> value when comparing their own value with the assertion. I say
<em>correct</em> <code>boolean</code> value as an <code>Expectation</code> may be negated using the <code>not</code> comparator. Which
essentially reverses the expected <code>boolean</code> value from <code>True</code> to <code>False</code>.</p>
<p>The <code>Expectation</code> struct itself holds it's own value as well as it's comparator function and
result of that comparator function, once it has been applied on it's value. This is necessary
as more than one expectation may be called per test. The code for the <code>Expectation</code> struct is
minimal as shown below. You'll notice that a <code>Vector{String}</code> data type is also present. This
is used to store the logs from the Expectation's comparator methods. Which are used for cli
reporting.</p>
<pre><code class="language-julia">struct Expectation
    value::Any
    comparator::Function
    result::Bool
    logs::Vector{String}
end
</code></pre>
<p>The test framework allows for tests to be grouped and hooks to be applied to those groups.
Implementing this was difficult and my solution was not ideal. In order to keep track of
what the current test suite was I used a global variable that stored the current test suite's
id. The id belonged to a <code>Suite</code> struct that was created when either <code>testgroup</code> or <code>describe</code>
was called. This struct would then be used to hold information such as the tests which were
run inside the suite, the results of those tests, as well as the hooks that should be run before
and after each test. The definition of these structs are below.</p>
<pre><code class="language-julia">Base.@kwdef mutable struct Hooks
    before_all_called::Bool = false
    after_all_called::Bool = false
    before_all::Vector{Union{Nothing,Function}} = []
    after_all::Vector{Union{Nothing,Function}} = []
    before_each::Vector{Union{Nothing,Function}} = []
    after_each::Vector{Union{Nothing,Function}} = []
end

Base.@kwdef mutable struct TestResult
    description::String = &quot;&quot;
    status::Symbol = :Pending
    f::Union{Task,Function} = () -&gt; nothing
    expectations::Vector{Expectation} = []
    stacktrace::Any = []
end

Base.@kwdef mutable struct Suite
    tests::Vector{TestResult} = []
    child_suites::Vector{Suite} = []
    f::Union{Task,Function} = () -&gt; nothing
    status::Symbol = :Pending
    hooks::Hooks = Hooks()
    description::String = &quot;&quot;
    id::Int64 = 0
end
</code></pre>
<p>I mentioned that this was not a <em>good</em> solution. This is because it would not allow for concurrent
test files to be run. Because a global variable is used to store the current suite, the tests
must be executed in order. Otherwise their results could end up in the wrong <code>Suite</code> struct.
I'm still not entirely sure how this could be solved. I would have to take a look at other
frameworks for some inspiration.</p>
<p>The final noteworthy part of the implementation was the use of Julia's built in operators to create
comparators. I was able to leverage the fact that Julia's builtin operators are methods themselves,
to create concise comparators. For example I wrote a helper method called <code>construct_comparator</code> that
took an operator as an argument and then used it as a callback method to compare the expected value
with the actual value. I have left out the code for <code>construct_comparator</code> as it contains reporting
logic which makes the code hard to understand without some domain knowledge. What is easy to understand
however is it's use. For example here is how the <code>to_be</code> comparator is defined.</p>
<pre><code class="language-julia">to_be = construct_comparator(===, &quot;to be&quot;)
</code></pre>
<magpie-trinket>
Why not look through the source code of BeeDeeDee.jl yourself. It's a small codebase which can be view at
<a href="https://github.com/BrianDouglasIE/BeeDeeDee.jl/tree/main">BrianDouglasIE/BeeDeeDee.jl</a>
</magpie-trinket>
<h2>My thoughts on Julia</h2>
<p>Writing a test framework in an unknown programming language is quite a feat. But it allowed me to
really dig down deep into Julia in quite a short period of time. I was impressed by the syntax of
Julia. There are some real opportunities to write beautiful code. The use of the <code>do</code> keyword and
pipe operator allow for concise and readable code. Even if it took me a while to understand the
argument order required for the <code>do</code> keyword.</p>
<p>It's a shame however that the <em>beautiful</em> and readable syntax which Julia can offer is contrasted
with some awkward syntax such as how default arguments are assigned in a struct. For example, the
<code>Suite</code> struct in BeeDeeDee.jl.</p>
<pre><code class="language-julia">Base.@kwdef mutable struct Suite
    tests::Vector{TestResult} = []
    child_suites::Vector{Suite} = []
    f::Union{Task,Function} = () -&gt; nothing
    status::Symbol = :Pending
    hooks::Hooks = Hooks()
    description::String = &quot;&quot;
    id::Int64 = 0
end
</code></pre>
<p>Experienced users of Julia might look past this. But to me it reeks of complexity. To understand this
a developer must understand why the <code>mutable</code> keyword is there, and what <code>Base.@kwdef</code> is, as well
as why a <code>macro</code> is needed. A <code>macro</code> being a language feature that allows for certain meta programming
techniques. They are used frequently, but I've only ever found them to add complexity.</p>
<p>Another example of Julia being awkward is in it's use of <code>modules</code> and <code>exports</code>. I still don't
fully understand how <code>modules</code> work. One might think of them sort of like a <code>class</code> in OOP. They act
like a container for code, which can export certain members. Unfortunately though Julia forces
the developer to export each method individually. Leading to some verbose code. Below is the
comparators export for BeeDeeDee. A <code>public</code> keyword on the method definition would be much nicer.</p>
<pre><code class="language-julia">export to_be,
to_be_true,
to_be_false,
to_be_greater_than,
to_be_less_than,
to_be_greater_than_or_equal,
to_be_less_than_or_equal,
to_be_in,
to_be_nothing,
to_be_typeof,
to_be_empty,
to_have_key,
to_be_subset,
to_be_setequal,
to_be_disjoint,
to_match,
to_be_valid_email,
to_throw
</code></pre>
<p>Imports are equally awkward. Every time a module is imported it is imported fresh. Meaning that
if A.jl and B.jl are in the same application and import C.jl. There will be a conflict and an error
with Julia complaining that C.jl is already imported. To get around this Julia developers import
modules into a common parent and access them using a path like syntax. With <code>using ..C</code> meaning
import the C.jl module that was required in my parent's scope... yikes.</p>
<h3>Will Julia become a mainstream language?</h3>
<p>In data science and academia, yes. Maybe third or fourth for popularity at it's height. But only
because there seems to be a lot of money behind it. Well at least I assume there must be if
Formula 1 cars are being sponsored by JuliaHub.</p>
<p>In anything else, no. I was not convinced that Julia would help me solve any problems on the web or
in game dev. It did introduce me to some novel concepts. But ultimately there was nothing there to
convert me into a Julia developer. The main annoyances being around how modules work. I could never
imagine building a large complicated system in a language that has such confusing code interop.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Sensible SQLite defaults]]></title>
      <link>https://briandouglas.ie/sqlite-defaults</link>
      <guid>https://briandouglas.ie/sqlite-defaults</guid>
      <pubDate>2024-10-17T00:00:00.000Z</pubDate>
      <description><![CDATA[SQLite is cool now. DHH uses it, Laravel defaults to it. Here is a list of
_sensible defaults_ when using sqlite.


]]></description>
      <content:encoded><![CDATA[
        <h2>The whys?</h2>
<pre><code class="language-sql">PRAGMA journal_mode = WAL;
</code></pre>
<p><strong>Why?</strong>: Allows concurrent reads and writes, making it more suitable for web applications with multiple users accessing the database simultaneously.</p>
<pre><code class="language-sql">PRAGMA synchronous = NORMAL;
</code></pre>
<p><strong>Why?</strong>: Balances performance and data safety by ensuring that data is written to disk in a reasonable time frame without slowing down writes as much as <code>FULL</code> mode.</p>
<pre><code class="language-sql">PRAGMA busy_timeout = 5000;
</code></pre>
<p><strong>Why?</strong>: Prevents &quot;database is locked&quot; errors by giving SQLite 5 seconds to wait for a locked resource before returning an error, useful for handling multiple concurrent accesses.</p>
<pre><code class="language-sql">PRAGMA cache_size = -20000;
</code></pre>
<p><strong>Why?</strong>: Sets the cache size to 20MB, allowing more data to be cached in memory, improving query performance by reducing the number of disk reads.</p>
<pre><code class="language-sql">PRAGMA foreign_keys = ON;
</code></pre>
<p><strong>Why?</strong>: Ensures referential integrity by enforcing foreign key constraints, critical for maintaining consistent relationships between tables (e.g., users, posts, and comments).</p>
<pre><code class="language-sql">PRAGMA auto_vacuum = INCREMENTAL;
</code></pre>
<p><strong>Why?</strong>: Reclaims disk space gradually as rows are deleted, instead of performing a full vacuum, reducing performance impact during database operations.</p>
<pre><code class="language-sql">PRAGMA temp_store = MEMORY;
</code></pre>
<p><strong>Why?</strong>: Stores temporary tables and other temporary data in memory, improving the performance of operations like sorting and indexing that are common in web applications.</p>
<pre><code class="language-sql">PRAGMA mmap_size = 2147483648;
</code></pre>
<p><strong>Why?</strong>: Uses memory-mapped I/O with a size of 2GB, which can speed up database access by reducing disk I/O, especially beneficial for large databases with frequent reads and writes.</p>
<pre><code class="language-sql">PRAGMA page_size = 8192;
</code></pre>
<p><strong>Why?</strong>: Sets a page size of 8KB, which provides a balance between memory usage and disk I/O performance for a forum database that handles many reads and writes.</p>
<h2>Copy paste</h2>
<p>For your convenience.</p>
<pre><code class="language-sql">-- Set the journal mode to Write-Ahead Logging for concurrency
PRAGMA journal_mode = WAL;

-- Set synchronous mode to NORMAL for performance and data safety balance
PRAGMA synchronous = NORMAL;

-- Set busy timeout to 5 seconds to avoid &quot;database is locked&quot; errors
PRAGMA busy_timeout = 5000;

-- Set cache size to 20MB for faster data access
PRAGMA cache_size = -20000;

-- Enable foreign key constraint enforcement
PRAGMA foreign_keys = ON;

-- Enable auto vacuuming and set it to incremental mode for gradual space reclaiming
PRAGMA auto_vacuum = INCREMENTAL;

-- Store temporary tables and data in memory for better performance
PRAGMA temp_store = MEMORY;

-- Set the mmap_size to 2GB for faster read/write access using memory-mapped I/O
PRAGMA mmap_size = 2147483648;

-- Set the page size to 8KB for balanced memory usage and performance
PRAGMA page_size = 8192;
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Prevent pixel art blur on canvas]]></title>
      <link>https://briandouglas.ie/draw-pixel-art-on-canvas</link>
      <guid>https://briandouglas.ie/draw-pixel-art-on-canvas</guid>
      <pubDate>2024-10-15T00:00:00.000Z</pubDate>
      <description><![CDATA[Nobody wants to see blurry pixel art rendered in a game. So let&#x27;s investigate
how to render crisp pixel art images on a canvas element.

]]></description>
      <content:encoded><![CDATA[
        <magpie-trinket>
Full source code for this article is available at <a href="https://github.com/BrianDouglasIE/canvas-pixel-art">BrianDouglasIE/canvas-pixel-art</a>
</magpie-trinket>
<h2>The task at hand</h2>
<p>So for this demo let's say we want to render the sprite of our game's hero character
on a canvas. The sprite sheet that I will be using is from KenneyNL, it's the 1-Bit
monochrome asset pack. Each sprite in the canvas is 16x16 pixels in dimension. This
will be scaled up to around 4 times it's original size.</p>
<p>I want my canvas to be 640x480 pixels in dimension. If I were to draw a sprite
directly on the canvas it would look miniscule. If I were to draw it at 4 times it's
original size, it would look blurry. So the question is, how do I draw my scaled up
pixel art character without causing it to blur.</p>
<p><chicken-asks>I bet this complex problem requires a complex solution</chicken-asks>
<magpie-replies>I bet the solution is a one liner</magpie-replies></p>
<h2>Some boiler plate</h2>
<p>Before I even get as far as rendering my pixel art character on a canvas there is some
initial boilerplate code that is needed.</p>
<p>Firstly I'll need to create a canvas element to render the game on. I can do that with
the following code. Which creates the canvas and appends it to the document's body. I
also set the canvas' width and height to be a quarter of the intended size. I will scale
this up later.</p>
<pre><code class="language-javascript">const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
document.body.appendChild(canvas)
canvas.height = 120
canvas.width = 160
</code></pre>
<p>Once this is in place I will add some functions to render the sprite sheet and extract the
image data of the sprite that I want to render. To do this I create a separate canvas. This
canvas will not be appended to the body of the html document as the user will never see it.
Instead it will be used to draw the sprite sheet in it's entirety. I will then grab the
individual sprites from it as and when they are needed. I also set the canvas to be scaled
up the target size using CSS. The code for this is below.</p>
<pre><code class="language-javascript">const spriteCellSize = 16
const spriteSheet = new Image()
spriteSheet.crossOrigin = &quot;anonymous&quot;
spriteSheet.addEventListener(&quot;load&quot;, onLoad, false)
spriteSheet.src = &quot;./monochrome-transparent.png&quot;

const scale = 4
canvas.style.width = `${canvas.width * scale}px`
canvas.style.height = `${canvas.height * scale}px`

const spriteSheetCanvas = document.createElement('canvas')

let heroSprite = undefined

function onLoad() {
  if (!heroSprite) {
    spriteSheetCanvas.width = spriteSheet.width
    spriteSheetCanvas.height = spriteSheet.height
    const spriteSheetContext = spriteSheetCanvas.getContext('2d')
    clearScreen(spriteSheetContext, spriteSheetCanvas.width, spriteSheetCanvas.height)
    drawSpriteSheet(spriteSheetContext, spriteSheet)
    heroSprite = getSpriteSheetCell(spriteSheetContext, spriteCellSize, 26, 0)
  }

  clearScreen(ctx, canvas.width, canvas.height)
  ctx.putImageData(heroSprite, canvas.width / 2 - spriteCellSize / 2, canvas.height / 2 - spriteCellSize / 2)
}

function clearScreen(ctx, w, h) {
  ctx.save()
  ctx.fillStyle = &quot;black&quot;
  ctx.fillRect(0, 0, w, h)
  ctx.restore()
}

function drawSpriteSheet(ctx, spriteSheet) {
  ctx.drawImage(spriteSheet, 0, 0)
}

function getSpriteSheetCell(ctx, spriteCellSize, col, row) {
  const x = spriteCellSize * col
  const y = spriteCellSize * row
  return ctx.getImageData(x, y, spriteCellSize, spriteCellSize)
}
</code></pre>
<p>Here is what is rendered on the canvas. As you can see it is super blurry.</p>
<p><img src="/images/blurry-sprite-render.png" alt="blurry sprite render"></p>
<h2>The solution</h2>
<p>One line of CSS fixes this. Setting the <code>image-rendering</code> property on the canvas to be <code>pixelated</code>.
In the demo code this can be done by adding the following statement at line 10.</p>
<pre><code class="language-javascript">canvas.style.imageRendering = &quot;pixelated&quot;
</code></pre>
<p>The canvas now renders a crisp sprite.</p>
<p><img src="/images/crisp-sprite-render.png" alt="crisp sprite render"></p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Add IF NOT EXISTS to your migrations]]></title>
      <link>https://briandouglas.ie/add-if-not-exists-to-your-migrations</link>
      <guid>https://briandouglas.ie/add-if-not-exists-to-your-migrations</guid>
      <pubDate>2024-10-09T00:00:00.000Z</pubDate>
      <description><![CDATA[When working on a codebase with a number of other developers migrations can cause
database conflicts. One simple step to stop migrations failing when switching 
between development branches is to add &#x60;IF NOT EXISTS&#x60; when creating a table.

]]></description>
      <content:encoded><![CDATA[
        <h2>The Problem</h2>
<p>Let's say we have a migration like the following.</p>
<pre><code class="language-sql">CREATE TABLE tickets (
    id SERIAL PRIMARY KEY,

    title VARCHAR(140) NOT NULL,
    content VARCHAR(500) NOT NULL,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
</code></pre>
<p>This is perfectly fine. It creates the <code>tickets</code> table. But what if we are working on this
feature and then have to switch to another part way through? Well we checkout the other feature
branch and work away. Now it's time to come back to the <code>tickets</code> feature. Let's say we have
to build the application locally when working on it. Once we try to build the app the migrations
will fail. Why? because the <code>tickets</code> table already exists, from when we were previously working on
the feature and ran the migration.</p>
<h2>The Solution</h2>
<p>Well that's annoying right? one solution would be to rollback the migration or delete the correct
row in our <code>migrations</code> table. But we can prevent this problem from ever happening in the first place
by adding <code>CREATE TABLE IF NOT EXISTS</code> to our migration. It now looks like this.</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS tickets (
    id SERIAL PRIMARY KEY,

    title VARCHAR(140) NOT NULL,
    content VARCHAR(500) NOT NULL,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
</code></pre>
<p>It may seem like an obvious thing but I've been working on a codebase recently where the
teams have not been doing this. It leads to annoying failures when checking out different
branches. So make sure to apply this when writing SQL migrations.</p>
<p>Those lucky ones working on Laravel, Django, or Rails apps will never face this issue. This
is a burden of the enterprise Java developer.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Using a PostgreSQL enum for state]]></title>
      <link>https://briandouglas.ie/enum-for-status</link>
      <guid>https://briandouglas.ie/enum-for-status</guid>
      <pubDate>2024-10-06T00:00:00.000Z</pubDate>
      <description><![CDATA[There are lot&#x27;s of occasions where I see SQL table definitions that contain
various boolean properties to communicate state. Often in these instances it
would be much more optimal to use an &#x60;enum&#x60;.

]]></description>
      <content:encoded><![CDATA[
        <h2>Using an Enum for State</h2>
<p>Let's look at the migration for a <code>tickets</code> table. In this example we can think
of a <code>ticket</code> as an item of work that may go through various states. Let's
start by naively adding a <code>boolean</code> property for each state that the ticket
might go through.</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS tickets {
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    is_open BOOLEAN DEFAULT TRUE,
    is_in_progress BOOLEAN DEFAULT FALSE,
    is_completed BOOLEAN DEFAULT FALSE,
    is_on_hold BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
}
</code></pre>
<p>We can tell from the above migration that a ticket has four known states <code>open</code>,
<code>completed</code>, <code>in_progress</code>, and <code>on_hold</code>. I say <em>known</em> states as in the future
another state may appear, for example <code>pending_approval</code>. Using the above pattern
this would require yet another property to be added. But really all these properties
are communicating is the current <code>state</code> of a ticket. This is where an <code>enum</code>
would add value.</p>
<p>By using an <code>enum</code> all the properties used to communicate state can be combined
into one <code>state</code> property. This will also remove any logic needed to ensure that
a ticket is only in one state at any given time, which would be needed in the
previous example.</p>
<p>An <code>enum</code> can be added as follows.</p>
<pre><code class="language-sql">CREATE TYPE ticket_state AS ENUM ('open', 'completed', 'in_progress', 'on_hold')

CREATE TABLE IF NOT EXISTS tickets {
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    state ticket_state DEFAULT 'open',
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
}
</code></pre>
<p>Notice that as well as reducing the number of properties needed. The state <code>enum</code>
can also default to <code>'open'</code>.</p>
<h2>Altering an Enum in PostgreSQL</h2>
<chicken-asks>
So what if in the future that `pending_approval` state is finally needed?
</chicken-asks>
<magpie-replies>
PostgreSQL makes updating enums easy. Just look at the example he put below.
</magpie-replies>
<p>So let's say it's now six months down the line and having recieved some client
feedback it looks like a ticket should be <em>approved</em> before it is sent to <code>complete</code>.</p>
<p>This will require a new database migration. Using PostgreSQL it would look like this.</p>
<pre><code class="language-sql">ALTER TYPE ticket_state ADD VALUE 'pending_approval';
</code></pre>
<p>PostgreSQL also allows for the new value to be added above or below an existing value
like so.</p>
<pre><code class="language-sql">ALTER TYPE ticket_state ADD VALUE 'pending_approval' BEFORE 'completed';
ALTER TYPE ticket_state ADD VALUE 'pending_approval' AFTER 'in_progress';
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Conway&#x27;s Game of Life in Go]]></title>
      <link>https://briandouglas.ie/conways-game-of-life-go</link>
      <guid>https://briandouglas.ie/conways-game-of-life-go</guid>
      <pubDate>2024-10-01T00:00:00.000Z</pubDate>
      <description><![CDATA[Conway&#x27;s _Game of Life_ is a relatively straight forward cellular automata.
I created an interactive demo using the rules that exists for it, using Go and Raylib.

]]></description>
      <content:encoded><![CDATA[
        <magpie-trinket>
Full source code is found on github, here <a href="https://github.com/BrianDouglasIE/conways-game-of-life">BrianDouglasIE/conways-game-of-life</a>.
</magpie-trinket>
<p>Wikipedia tells me that these are the rules by which a cell can be determined to be <em>alive</em> or <em>dead</em>.</p>
<ul>
<li>Any live cell with fewer than two live neighbours dies, as if by underpopulation.</li>
<li>Any live cell with two or three live neighbours lives on to the next generation.</li>
<li>Any live cell with more than three live neighbours dies, as if by overpopulation.</li>
<li>Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.</li>
</ul>
<p>With this in mind I was able to create the below demo. I added interactivity by allowing the user to click
a cell to toggle it's state between <em>dead</em> and <em>alive</em>. Pressing the up arrow key runs the next cycle.</p>
<video width="100%" controls>
    <source src="/videos/game-of-life-raylib-go.mp4" type="video/mp4">
    <source src="/videos/game-of-life-raylib-go.webm" type="video/webm">
    Your browser does not support the video tag.
</video>
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Writing a DB migrator in Go]]></title>
      <link>https://briandouglas.ie/db-migrator-in-go</link>
      <guid>https://briandouglas.ie/db-migrator-in-go</guid>
      <pubDate>2024-09-30T00:00:00.000Z</pubDate>
      <description><![CDATA[A database migrator applies database migrations. These are usually SQL scripts that
update the version of a database. They can be applied and reverted. Allowing for db
versions to be rolled back and pushed forward.

]]></description>
      <content:encoded><![CDATA[
        <p>I feel that these should be simple tools, with simple commands. Migrate up a version,
migrate down a version, migrate to version x, migrate all pending migrations, and also
a reset command to remove all migrations. This is all I want. So I decided to make my
own, no bloat, no complexity, just simple useful commands.</p>
<p>Convert the above requirements to CLI commands and we get.</p>
<pre><code>  -all
        Apply all pending migrations
  -down int
        Migrate down a certain number of versions
  -reset
        Revert all migrations
  -to int
        Migrate to a specific version
  -up int
        Migrate up a certain number of versions
</code></pre>
<magpie-trinket>
He highlights some interesting bit's of code below. Well at least the parts he found interesting. If you want the full source code it
is available at <a href="https://github.com/BrianDouglasIE/go-lang-db-migrator">BrianDouglasIE/go-lang-db-migrator</a>.
</magpie-trinket>
<h2>Assumptions</h2>
<p>Let's assume the code in the sections below is part of a package called <code>migrator</code>, and that we will be
running the code from that package as a command line application. Let's also assume that the program has
recieved details about how to connect to the database, and uses the go <code>sql</code> package to connect.</p>
<h2>Command line options</h2>
<p>This is intended to be a command line tool. Luckily Go's <code>flags</code> package makes this trivial.
I start by defining an <code>Options</code> struct in the <code>migrator</code> package.</p>
<pre><code class="language-go">type Options struct {
	To    int
	Up    int
	Down  int
	All   bool
	Reset bool
}
</code></pre>
<p>Then inside <code>main.go</code> I can set the <code>Options</code> values like so. Notice the default values and description text.
All this info will be displayed when passing the <code>--help</code> flag.</p>
<pre><code class="language-go">var opts migrator.Options
flag.BoolVar(&amp;opts.All, &quot;all&quot;, false, &quot;Apply all pending migrations&quot;)
flag.BoolVar(&amp;opts.Reset, &quot;reset&quot;, false, &quot;Revert all migrations&quot;)
flag.IntVar(&amp;opts.To, &quot;to&quot;, 0, &quot;Migrate to a specific version&quot;)
flag.IntVar(&amp;opts.Up, &quot;up&quot;, 0, &quot;Migrate up a certain number of versions&quot;)
flag.IntVar(&amp;opts.Down, &quot;down&quot;, 0, &quot;Migrate down a certain number of versions&quot;)
flag.Parse()
</code></pre>
<p>A pointer to this struct will then be passed into the <code>migrator.Migrate</code> function.</p>
<h2>Structuring a migration</h2>
<p>Typically a migration has an <code>up</code> and a <code>down</code> action. I've opted to use sql files for migrations
rather than an orm. I've done this to avoid complexity. The complexity being, having to learn the
nuances of an orm, when I already know what I want to achieve with sql.</p>
<p>The migrations will be stored in a <code>migrations</code> directory and each migration will have a <code>.up.sql</code>
and <code>.down.sql</code> file. The file naming convention will look like this <code>[version]-[name].[direction].sql</code>.
So, for example, our first migration will look as follows. <strong>Note</strong>: this migration will always need to
be first as it creates the table where our migration info will be stored.</p>
<pre><code class="language-sql">-- 001-create-migrations-table.up.sql
CREATE TABLE IF NOT EXISTS migrations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,

    version INTEGER NOT NULL,
    name TEXT NOT NULL,

    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 001-create-migrations-table.down.sql
DROP TABLE IF EXISTS migrations;
</code></pre>
<p>Our migrator program will read the <code>migrations</code> directory and gather the migration files into the
following <code>struct</code>.</p>
<pre><code class="language-go">type Migration struct {
	Version  int
	Name     string
	UpFile   string
	DownFile string
}
</code></pre>
<p>This can then be used to sort migrations by version. Getting the latest migration from our <code>migrations</code>
table will tell us our current DB version. When running the <code>up</code> or <code>down</code> commands we need only concern
ourselves with the migrations that have a version that is higher or lower than the current db version
respectively. If a migration should be applied then execute the <code>UpFile</code> if it should be reverted then
execute the <code>DownFile</code>.</p>
<h2>Using transactions with context</h2>
<p>Transactions in go can be used to commit or rollback multiple grouped sql queries.
The go docs at <a href="https://go.dev/doc/database/execute-transactions">database/execute-transactions</a> sum up
transactions like so.</p>
<blockquote>
<p>A database transaction groups multiple operations as part of a larger goal.</p>
</blockquote>
<p>The migrator program is doing just that. It takes a bunch of sql statements and runs them to create a
new database state. Should one of the migrations fail, we can use the go transaction to rollback the
state of the database to how it was before the migrator ran. Alternatively we can also commit the transaction
if all migrations run successfully.</p>
<p>We'll use the transaction along with a context. Using a Go <code>context.Context</code> with a database transaction helps
manage timeouts, cancellations, and ensures that long-running database operations are cleaned up with the context.
If the context is canceled any ongoing database transactions can be rolled back. This prevents partial data writes
or inconsistencies.</p>
<p>Using a transaction along with a context is trivial.</p>
<pre><code class="language-go">// create context
ctx := context.Background()

// pass our context to the transaction
tx, err := db.BeginTx(ctx, nil)
if err != nil {
    return fail(err)
}

// defer rollback, will be called if `tx.Commit` is not
defer tx.Rollback()
</code></pre>
<p>This transaction can then be passed around out migrator program. Once all the migrations
have been handled, and assuming no errors have occured, the transaction can be committed
be calling <code>tx.Commit</code>. Any call to the trnasaction after it is commited will result in
an error. Commiting ends the transaction.</p>
<h2>Final thoughts</h2>
<p>A db migrator should not be a complex program and it should not have a complex API. I looked
at some widely used libraries for managing migrations and I was struck by their complexity
and learning curve. Due to this I decided it better to create a simple package to achieve the
same end. It took around 90 minutes and helped me to better understand how migrations work. I
also now have my own tool to manage migrations, that I can bend to my liking.</p>
<p>Feel free to look through the entire source code at <a href="https://github.com/BrianDouglasIE/go-lang-db-migrator">BrianDouglasIE/go-lang-db-migrator</a>.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Creating a Pong clone using Raylib and Go]]></title>
      <link>https://briandouglas.ie/pong-using-raylib-and-go</link>
      <guid>https://briandouglas.ie/pong-using-raylib-and-go</guid>
      <pubDate>2024-09-22T00:00:00.000Z</pubDate>
      <description><![CDATA[As a fun exercise I implemented a rudimentary Pong clone using Raylib and Go.

]]></description>
      <content:encoded><![CDATA[
        <chicken-asks>
Why Go?
</chicken-asks>
<magpie-replies>
So many reasons. Most notably the tooling, language simplicity, and similarity to C.
</magpie-replies>
<h2>My Thoughts</h2>
<p>I have seen a couple of videos on YouTube where developers have been making games using Go.
My understanding of Go was that it was a Web Dev Backend language for hipsters. However,
having written this simple program in Go I have been pleasantly surprised by the features
the language has to offer. For me the built in &quot;tools&quot; that come with Go are excellent and
not something I was aware of. Go provides a built in code formatter, gofmt, which means I
don't have to think about how my Go code should look. It also comes with, gopls, a language
server developed by the Go team. This meant that I only had to install the <a href="https://github.com/fatih/vim-go">vim-go</a>
extension to turn neovim into a Go IDE without any set up.</p>
<p>Then it comes to the language itself. I had heard Go was &quot;simple&quot;. To my surprise this was
actually true. I was able to write Go pretty much straight away due to it's familiar syntax.
The only new aspects to me were the assignment operator <code>:=</code>, that <code>slices</code> were favoured
over <code>arrays</code>, and that a capital letter was used to signify privacy.</p>
<p>It took me 90 minutes to write the below Pong implementation. I had never written a line of
Go before, so I feel this is a big achievement for the language. I would say I was instantly
productive. Looking at the source code of my Pong implementation, I am sure you will agree
it is clear and readable. This is owed to the simplicity of Go. A language of which I feel
I will be using a lot more of in the future.</p>
<h2>The Setup</h2>
<p>No much to it, create a Go module and install <a href="https://github.com/gen2brain/raylib-go">raylib-go</a>.</p>
<pre><code>go mod init main
go get github.com/gen2brain/raylib-go
</code></pre>
<h2>The Code</h2>
<magpie-trinket>
The source code is also available on github, <a href="https://github.com/BrianDouglasIE/raylib-go-pong">BrianDouglasIE/raylib-go-pong</a>.
</magpie-trinket>
<p>I won't write out what the code is doing as I feel it is fairly self explanatory. I know you
are an ace programmer, so you don't need your hand held.</p>
<p>The game can be launched with <code>go run main.go</code>.</p>
<pre><code class="language-go">package main

import (
        &quot;fmt&quot;
        &quot;image/color&quot;
        &quot;math/rand&quot;
        &quot;time&quot;

        rl &quot;github.com/gen2brain/raylib-go/raylib&quot;
)

const (
        SCREEN_WIDTH          = 800
        SCREEN_HEIGHT         = 450
        CENTER_X              = SCREEN_WIDTH / 2
        CENTER_Y              = SCREEN_HEIGHT / 2
        SIDE_BUFFER_WIDTH     = 40
        INITIAL_PADDLE_HEIGHT = 80
        INITIAL_PADDLE_WIDTH  = 20
        INITIAL_PADDLE_SPEED  = 4
        INITIAL_BALL_SIZE     = 16
        INITIAL_BALL_SPEED    = 4
        RIGHT_BOUNDS          = SCREEN_WIDTH
        LEFT_BOUNDS           = 0
        TOP_BOUNDS            = 0
        BOTTOM_BOUNDS         = SCREEN_HEIGHT
)

type Paddle struct {
        X      int32
        Y      int32
        VX     int32
        VY     int32
        Width  int32
        Height int32
        Score  int32
        Speed  int32
        Color  color.RGBA
}

func (p *Paddle) Draw() {
        rl.DrawRectangle(p.X, p.Y, p.Width, p.Height, p.Color)
}

func (p *Paddle) IncrementScore() {
        p.Score += 1
}

func (p *Paddle) Update() {
        p.X += p.VX
        p.Y += p.VY
}

type Ball struct {
        X     int32
        Y     int32
        VX    int32
        VY    int32
        Size  int32
        Serve int32
        Color color.RGBA
}

func (b *Ball) Draw() {
        rl.DrawRectangle(b.X, b.Y, b.Size, b.Size, b.Color)
}

func (b *Ball) Update() {
        b.X += b.VX
        b.Y += b.VY
}

func (b *Ball) Reset() {
        b.Size = INITIAL_BALL_SIZE
        b.X = CENTER_X - (INITIAL_BALL_SIZE / 2)
        b.Y = CENTER_Y - (INITIAL_BALL_SIZE / 2)
        b.Color = rl.White
        b.VX = INITIAL_BALL_SPEED * b.serveModifier()
        b.VY = INITIAL_BALL_SPEED * pickRandomVariant()
        b.Serve += 1
}

func (b *Ball) BounceVertically() {
        b.VY = b.VY * -1
}

func (b *Ball) BounceHorizontally() {
        b.VX = b.VX * -1
}

func (b *Ball) serveModifier() int32 {
        if b.Serve%2 == 0 {
                return 1
        }

        return -1
}

func isBallOutToLeft(b *Ball) bool {
        return b.X &lt; LEFT_BOUNDS
}

func isBallOutToRight(b *Ball) bool {
        return b.X-b.Size &gt; RIGHT_BOUNDS
}

func ballAtBottom(b *Ball) bool {
        return b.Y &gt; SCREEN_HEIGHT
}

func ballAtTop(b *Ball) bool {
        return b.Y+b.Size &lt; 0
}

func paddleAtTop(p *Paddle) bool {
        return p.Y &lt; TOP_BOUNDS
}

func paddleAtBottom(p *Paddle) bool {
        return p.Y+p.Height &gt; BOTTOM_BOUNDS
}

func pickRandomVariant() int32 {
        return int32(rand.Intn(2)*2 - 1)
}

func getScoreText(leftPaddle *Paddle, rightPaddle *Paddle) string {
        return fmt.Sprintf(&quot;%d   %d&quot;, leftPaddle.Score, rightPaddle.Score)
}

func movePaddleToInterceptBall(p *Paddle, b *Ball) {
        paddleCenter := p.Y + p.Height/2
        if b.Y &gt; paddleCenter {
                p.VY = p.Speed
        } else if b.Y &lt; paddleCenter {
                p.VY = -p.Speed
        } else {
                p.VY = 0
        }
}

func movePaddleTowardsCenter(p *Paddle) {
        paddleCenter := p.Y + p.Height/2
        if paddleCenter &lt; CENTER_Y {
                p.VY = p.Speed
        } else if paddleCenter &gt; CENTER_Y {
                p.VY = -p.Speed
        } else {
                p.VY = 0
        }
}

func ballIntersectsPaddle(b *Ball, p *Paddle) bool {
        return b.X &lt; p.X+p.Width &amp;&amp;
                b.X+b.Size &gt; p.X &amp;&amp;
                b.Y+b.Size &gt; p.Y &amp;&amp;
                b.Y &lt; p.Y+p.Height
}

func main() {
        rand.Seed(time.Now().UnixNano())
        rl.InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, &quot;Pong&quot;)
        defer rl.CloseWindow()

        rl.SetTargetFPS(60)

        ball := Ball{}
        ball.Reset()

        leftPaddle := Paddle{
                Width:  INITIAL_PADDLE_WIDTH,
                Height: INITIAL_PADDLE_HEIGHT,
                Speed:  INITIAL_PADDLE_SPEED,
                Color:  rl.White,
                X:      SIDE_BUFFER_WIDTH,
                Y:      CENTER_Y - (INITIAL_PADDLE_HEIGHT / 2),
                Score:  0,
        }

        rightPaddle := Paddle{
                Width:  INITIAL_PADDLE_WIDTH,
                Height: INITIAL_PADDLE_HEIGHT,
                Speed:  INITIAL_PADDLE_SPEED,
                Color:  rl.White,
                X:      SCREEN_WIDTH - SIDE_BUFFER_WIDTH - INITIAL_PADDLE_WIDTH,
                Y:      CENTER_Y - (INITIAL_PADDLE_HEIGHT / 2),
                Score:  0,
        }

        for !rl.WindowShouldClose() {
                ball.Update()

                if isBallOutToLeft(&amp;ball) {
                        rightPaddle.IncrementScore()
                        ball.Reset()
                }

                if isBallOutToRight(&amp;ball) {
                        leftPaddle.IncrementScore()
                        ball.Reset()
                }

                if ballAtTop(&amp;ball) || ballAtBottom(&amp;ball) {
                        ball.BounceVertically()
                }

                leftPaddle.Update()
                rightPaddle.Update()

                if ball.VX &lt; 0 {
                        movePaddleTowardsCenter(&amp;rightPaddle)
                } else {
                        movePaddleToInterceptBall(&amp;rightPaddle, &amp;ball)
                }

                if rl.IsKeyDown(rl.KeyUp) {
                        leftPaddle.VY = -leftPaddle.Speed
                } else if rl.IsKeyDown(rl.KeyDown) {
                        leftPaddle.VY = leftPaddle.Speed
                } else {
                        leftPaddle.VY = 0
                }

                if paddleAtTop(&amp;rightPaddle) {
                        rightPaddle.Y = 0
                }

                if paddleAtBottom(&amp;rightPaddle) {
                        rightPaddle.Y = SCREEN_HEIGHT - rightPaddle.Height
                }

                if paddleAtTop(&amp;leftPaddle) {
                        leftPaddle.Y = 0
                }

                if paddleAtBottom(&amp;leftPaddle) {
                        leftPaddle.Y = SCREEN_HEIGHT - leftPaddle.Height
                }

                if ballIntersectsPaddle(&amp;ball, &amp;leftPaddle) ||
                        ballIntersectsPaddle(&amp;ball, &amp;rightPaddle) {
                        ball.BounceHorizontally()
                }

                rl.BeginDrawing()

                rl.ClearBackground(rl.Black)
                rl.DrawLine(CENTER_X, TOP_BOUNDS, CENTER_X, BOTTOM_BOUNDS, rl.LightGray)

                scoreText := getScoreText(&amp;leftPaddle, &amp;rightPaddle)
                textSize := rl.MeasureText(scoreText, 20)

                rl.DrawText(scoreText, CENTER_X-(textSize/2), 20, 20, rl.White)

                leftPaddle.Draw()
                rightPaddle.Draw()
                ball.Draw()

                rl.EndDrawing()
        }
}
</code></pre>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Bit shifting to increase performance]]></title>
      <link>https://briandouglas.ie/bit-shifting-for-performance</link>
      <guid>https://briandouglas.ie/bit-shifting-for-performance</guid>
      <pubDate>2024-09-15T00:00:00.000Z</pubDate>
      <description><![CDATA[I&#x27;ve been watching [@Vercidium](https://www.youtube.com/@Vercidium)&#x27;s awesome content on YouTube. His videos are all
about performance optimization in regards to game development, and I learned a nifty performance optimization for my
JS code as a result.

]]></description>
      <content:encoded><![CDATA[
        <p>In his <a href="https://youtu.be/40JzyaOYJeY?si=5J97q3bm7MsJ3I6k">&quot;I Optimised My Game Engine Up To 12000 FPS&quot;</a> video Vercidium
shows a <em>bit-shifting</em> technique which he has used to store information about his voxel based scene. This got me
thinking <em>could the same technique be used to save memory in JS code?</em>.</p>
<p><magpie-trinket><strong>TLDR;</strong> Storing a vector (x, y, z) as a 32-bit number uses a seventh of the memory compared to storing it as an object in this example.</magpie-trinket></p>
<table class="pure-table">
    <thead>
        <tr>
            <th>Metric</th>
            <th>Object Based</th>
            <th>Bit Shifted</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>RSS</td>
            <td>646.12 MB</td>
            <td>132.29 MB</td>
        </tr>
        <tr>
            <td>Heap Total</td>
            <td>576.83 MB</td>
            <td>87.58 MB</td>
        </tr>
        <tr>
            <td>Heap Used</td>
            <td>545.67 MB</td>
            <td>82.33 MB</td>
        </tr>
        <tr>
            <td>External</td>
            <td>0.98 MB</td>
            <td>0.98 MB</td>
        </tr>
        <tr>
            <td>Array Buffers</td>
            <td>0.01 MB</td>
            <td>0.01 MB</td>
        </tr>
    </tbody>
</table>
</magpie-trinket>
<h2>The Plan</h2>
<p>When creating games and rendering scenes on canvas I often start by creating a <code>Vector</code> class like the one below.</p>
<pre><code class="language-javascript">class Vector3 {
    x = 0
    y = 0
    z = 0

    constructor(x, y, z) {
        this.x = x
        this.y = y
        this.z = z
    }
}
</code></pre>
<p>You'll see varying implementations of this in pretty much every game engine. It's used to define where something
exists with in the game world. In my JS games I'll sometimes create tens of thousands of these at a time. I've often
thought about the performance impact of this, but never really came up with a good solution. The games I make are
often rudimentary in nature.</p>
<p>So let's look at the amount of memory used by creating and modifying such objects. To record the memory usage I have
written the following method. Which breaks down the results as follows:</p>
<ul>
<li>rss: Resident Set Size – total memory allocated for the process.</li>
<li>heapTotal: Total size of the allocated heap.</li>
<li>heapUsed: Actual memory used by the program.</li>
<li>external: Memory used by C++ objects bound to JavaScript objects.</li>
<li>arrayBuffers: Memory allocated for ArrayBuffers and SharedArrayBuffers.</li>
</ul>
<pre><code class="language-javascript">function logMemoryUsage() {
    const memoryUsage = process.memoryUsage();

    console.log(`Memory Usage:
        RSS: ${memoryUsage.rss / 1024 / 1024} MB
        Heap Total: ${memoryUsage.heapTotal / 1024 / 1024} MB
        Heap Used: ${memoryUsage.heapUsed / 1024 / 1024} MB
        External: ${memoryUsage.external / 1024 / 1024} MB
        Array Buffers: ${memoryUsage.arrayBuffers / 1024 / 1024} MB
    `);
}
</code></pre>
<p>The below script creates <strong>10 million</strong> <code>Vector3</code> objects and updates the <code>x</code> and <code>y</code> co-ordinates of each randomly
every
millisecond for 2 seconds.</p>
<pre><code class="language-javascript">class Vector3 {
    x = 0
    y = 0
    z = 0

    constructor(x, y, z) {
        this.x = x
        this.y = y
        this.z = z
    }
}

function randomNumber(min, max) {
    return Math.floor(Math.random() * max) + min;
}

const maxVectors = 10_000_000
const vectors = new Array(maxVectors)

for (let i = 0; i &lt; maxVectors; i++) {
    vectors[i] = new Vector3(
        randomNumber(0, 1000),
        randomNumber(0, 1000),
        randomNumber(0, 1000)
    )
}

let demoLengthInSeconds = 2;

const updateInterval = setInterval(() =&gt; {
    for (const v of vectors) {
        v.x += randomNumber(-2, 2)
        v.y += randomNumber(-2, 2)
    }
}, 1);

setTimeout(() =&gt; {
    logMemoryUsage()
    clearInterval(updateInterval)
}, demoLengthInSeconds * 1000)
</code></pre>
<p>For the purposes of this article I will run the script with node. Which gives the following results.</p>
<pre><code>Memory Usage:
        RSS: 646.12109375 MB
        Heap Total: 576.828125 MB
        Heap Used: 545.6746520996094 MB
        External: 0.9788284301757812 MB
        Array Buffers: 0.009958267211914062 MB
</code></pre>
<p>Using the above object based implementation as baseline, let's now look at the bit-shifter variant.</p>
<h2>The Bit Shifter Implementation</h2>
<p>The bit-shifting implementation is going to work along the same lines. However, some boilerplate code is needed to <code>pack</code>
and <code>unpack</code> our vector's values. Essentially what we are doing with the <code>pack</code> method is a mask and shift of <code>x</code>, <code>y</code>,
and <code>z</code> to pack them efficiently into a single 32-bit number. The idea here being that a 32-bit number will be stored
with less memory than the class used in the object based run.</p>
<p>Putting these concepts into practice we have the below script.</p>
<pre><code class="language-javascript">function pack(n, offset) {
    if (!offset) return (n &amp; 0x3FF);
    return (n &amp; 0x3FF) &lt;&lt; offset;
}

function Vector3(x, y, z) {
    return (pack(x, 20) | pack(y, 10) | pack(z));
}

function unpack(packed, offset) {
    if (!offset) return packed &amp; 0x3FF;
    return (packed &gt;&gt; offset) &amp; 0x3FF;
}

function getX(vector3) {
    return unpack(vector3, 20);
}

function getY(vector3) {
    return unpack(vector3, 10);
}

function getZ(vector3) {
    return unpack(vector3);
}

function setX(vector3, x) {
    vector3 &amp;= ~(0x3FF &lt;&lt; 20);
    vector3 |= pack(x, 20);
    return vector3;
}

function setY(vector3, y) {
    vector3 &amp;= ~(0x3FF &lt;&lt; 10);
    vector3 |= pack(y, 10);
    return vector3;
}

function setZ(vector3, z) {
    vector3 &amp;= ~0x3FF;
    vector3 |= pack(z);
    return vector3;
}

function randomNumber(min, max) {
    return Math.floor(Math.random() * max) + min;
}

const maxVectors = 10_000_000
const vectors = new Array(maxVectors)

for (let i = 0; i &lt; maxVectors; i++) {
    vectors[i] = Vector3(
        randomNumber(0, 1000),
        randomNumber(0, 1000),
        randomNumber(0, 1000)
    )
}

let demoLengthInSeconds = 2;

const updateInterval = setInterval(() =&gt; {
    for (const v of vectors) {
        setX(v, getX(v) + randomNumber(-2, 2))
        setY(v, getY(v) + randomNumber(-2, 2))
    }
}, 1);

setTimeout(() =&gt; {
    logMemoryUsage()
    clearInterval(updateInterval)
}, demoLengthInSeconds * 1000)
</code></pre>
<chicken-asks>
Will the calls to `pack` and `unpack` affect the performance?
</chicken-asks>
<magpie-replies>
Good question, there may be a more efficient way of doing this. But in regard to this test, the impact of calling `pack`
and `unpack` seem to be negligible.
</magpie-replies>
<p>Again this is run with node, <code>v18.19.0</code>. The resulting memory usage is as follows.</p>
<pre><code>Memory Usage:
        RSS: 132.28515625 MB
        Heap Total: 87.578125 MB
        Heap Used: 82.33492279052734 MB
        External: 0.9788284301757812 MB
        Array Buffers: 0.009958267211914062 MB
</code></pre>
<h2>The Results</h2>
<table class="pure-table">
    <thead>
        <tr>
            <th>Metric</th>
            <th>Object Based</th>
            <th>Bit Shifted</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>RSS</td>
            <td>646.12 MB</td>
            <td>132.29 MB</td>
        </tr>
        <tr>
            <td>Heap Total</td>
            <td>576.83 MB</td>
            <td>87.58 MB</td>
        </tr>
        <tr>
            <td>Heap Used</td>
            <td>545.67 MB</td>
            <td>82.33 MB</td>
        </tr>
        <tr>
            <td>External</td>
            <td>0.98 MB</td>
            <td>0.98 MB</td>
        </tr>
        <tr>
            <td>Array Buffers</td>
            <td>0.01 MB</td>
            <td>0.01 MB</td>
        </tr>
    </tbody>
</table>
<p>As we can see from the above table the object based implementation used around <strong>7 times</strong> more memory than the bit
shifted example.</p>
<p>When it comes to creating my next game I will certainly be using this technique. While it does require extra code, the
code required is still fairly minimal. Especially seeing as it uses a seventh of the memory.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Generate OG images for blog posts]]></title>
      <link>https://briandouglas.ie/generate-og-images-for-blog-posts</link>
      <guid>https://briandouglas.ie/generate-og-images-for-blog-posts</guid>
      <pubDate>2024-09-07T00:00:00.000Z</pubDate>
      <description><![CDATA[When sharing an article on social media you will see a preview. The preview uses the html &#x60;og:image&#x60; meta tag. In this
article I will demonstrate how I generate a unique &#x60;og:image&#x60; for each of my blog posts.

]]></description>
      <content:encoded><![CDATA[
        <p>The general gist here is that we want to create a method that will take the title of our post and generate an image
with that title text. We will use a template image to place the text into. The template image I use is shown below.
Note the empty space on the right hand side, this is where we will place our blog post title. As recommended by github
I have also left and 80px margin around the content. The dimensions of the image are 1280x640 pixels.</p>
<img class="pure-img" src="/images/open-graph-template.png"  alt="my og:image template"/>
<h2>The Implementation</h2>
<p>In order to write the title of our blog post to the template image we will use the npm package <a href="https://www.npmjs.com/package/pureimage">PureImage</a>.
This package allows us to use the canvas api to draw to a context which we can then export as an image, without loading
up a browser instance.</p>
<magpie-trinket>
He probably leaves out a load of detail in the next section. But the general gist is as follows.
<ul>
    <li>Create a template image that you want to write too</li>
    <li>Install the PureImage package</li>
    <li>Load the desired font intro PureImage</li>
    <li>Draw the text on to the template image using a PureImage context</li>
    <li>Output the new image to the desired location</li>
</ul>
</magpie-trinket>
<h3>Gotcha 1 - Loading the font</h3>
<p>The first gotcha when using <code>PureImage</code> is that there are no fonts loaded by default. So in order to draw text to our image
we must first load the font like so.</p>
<pre><code class="language-javascript">import * as PImage from &quot;pureimage&quot;;

const OUT_DIR = &quot;./docs/images/og-images&quot;;
const FONT = PImage.registerFont(
  &quot;C:\\Users\\brian\\AppData\\Local\\Microsoft\\Windows\\Fonts\\JetBrainsMono-Bold.ttf&quot;,
  &quot;MyFont&quot;,
);

FONT.loadSync();
</code></pre>
<p>This will load the required font so that we can use it when drawing on our image template.</p>
<h3>Gotcha 2 - Wrapping the text</h3>
<p>The second gotcha is that because we are using the canvas api to draw text, it will not wrap unless we specifically tell
it to do so. To get around this I have used the below method. This is actually a common issue when making browser games
using the canvas. So I was expecting to encounter the problem.</p>
<pre><code class="language-javascript">function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
  const words = text.split(&quot; &quot;);
  let line = &quot;&quot;;
  let testLine, metrics, testWidth;

  for (let n = 0; n &lt; words.length; n++) {
    testLine = line + words[n] + &quot; &quot;;
    metrics = ctx.measureText(testLine);
    testWidth = metrics.width;

    if (testWidth &gt; maxWidth &amp;&amp; n &gt; 0) {
      ctx.fillText(line, x, y);
      line = words[n] + &quot; &quot;;
      y += lineHeight;
    } else {
      line = testLine;
    }
  }
  ctx.fillText(line, x, y);
}
</code></pre>
<h3>Putting it all together</h3>
<p>Here is the code I use to generate my OG images. It's specific to my use case, but I feel that you, a talented software
engineer, will be able to extract the bits you need.</p>
<p>The main domain specific things here is that my <code>Post</code> class looks something like this.</p>
<pre><code class="language-typescript">type Post = {
    slug: string
    html: string
    excerpt: string
    data: { title: string, date: string, tags: string[] }
}
</code></pre>
<p>With that in mind here is the full implementation.</p>
<pre><code class="language-javascript">import * as PImage from &quot;pureimage&quot;;
import * as fs from &quot;fs&quot;;
import path from &quot;node:path&quot;;

const OUT_DIR = &quot;./docs/images/og-images&quot;;
const FONT = PImage.registerFont(
  &quot;C:\\Users\\brian\\AppData\\Local\\Microsoft\\Windows\\Fonts\\JetBrainsMono-Bold.ttf&quot;,
  &quot;MyFont&quot;,
);

FONT.loadSync();

export async function generateOGImage(post) {
  const pImage = PImage.make(1280, 640);
  const ctx = pImage.getContext(&quot;2d&quot;);
  const image = await PImage.decodePNGFromStream(
    fs.createReadStream(&quot;./theme/assets/images/open-graph-template.png&quot;),
  );
  ctx.drawImage(image, 0, 0);

  const text = post.data.title;
  const maxWidth = 700;
  const lineHeight = 80;
  const x = 520;
  const y = 270;

  ctx.font = &quot;80px MyFont&quot;;
  ctx.fillStyle = &quot;black&quot;;
  wrapText(ctx, text, x, y, maxWidth, lineHeight);

  if (!fs.existsSync(OUT_DIR)) {
    fs.mkdirSync(OUT_DIR);
  }

  await PImage.encodePNGToStream(
    pImage,
    fs.createWriteStream(path.join(OUT_DIR, `${post.slug}.png`)),
  );
}

function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
  const words = text.split(&quot; &quot;);
  let line = &quot;&quot;;
  let testLine, metrics, testWidth;

  for (let n = 0; n &lt; words.length; n++) {
    testLine = line + words[n] + &quot; &quot;;
    metrics = ctx.measureText(testLine);
    testWidth = metrics.width;

    if (testWidth &gt; maxWidth &amp;&amp; n &gt; 0) {
      ctx.fillText(line, x, y);
      line = words[n] + &quot; &quot;;
      y += lineHeight;
    } else {
      line = testLine;
    }
  }
  ctx.fillText(line, x, y);
}
</code></pre>
<h2>The Result</h2>
<p>Now we can call <code>generateOGImage(myPost)</code> and it will generate a unique og image for the supplied post. The supplied
post's title will be drawn on the image. Using this code on the blog post you are currently reading will generate the
below image.</p>
<img class="pure-img" src="/images/og_index.png"  alt="the og:image for this blog"/>
<p>Unfortunately as you can see from the image, the library we used (<code>PureImage</code>) isn't great at rendering fonts. So the font
looks a little choppy. Potentially a different library would do a better job. Alternatively we could use a tool like
webdriver.io or selenium to render the template as html and save it as an image which may yield better results. For me
however, the current implementation will suffice.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[One assertion per test please]]></title>
      <link>https://briandouglas.ie/one-assertion-per-test-please</link>
      <guid>https://briandouglas.ie/one-assertion-per-test-please</guid>
      <pubDate>2024-08-17T00:00:00.000Z</pubDate>
      <description><![CDATA[Please! when you are writing tests try to limit each test case to one &#x60;expect&#x60;.
Let&#x27;s refactor the monstrosity below to understand why fewer expects and more 
tests are better.

]]></description>
      <content:encoded><![CDATA[
        <magpie-trinket>
Try your best to write test cases as if they were telling a story.
Stick to one assertion per test, and split out any shared logic into
hooks, like <code>beforeEach</code> and <code>beforeAll</code>.
</magpie-trinket>
<h2>The Monstrosity</h2>
<pre><code class="language-javascript">import { render, screen, fireEvent } from '@testing-library/react';
import { expect, test } from 'vitest';
import MyComponent from './MyComponent';

test('User Name Submit Form', () =&gt; {
  const mockApiData = { user: 'John Doe' };
  const apiFetchMock = vi.fn(() =&gt; Promise.resolve(mockApiData));

  render(&lt;MyComponent fetchData={apiFetchMock} /&gt;);

  const header = screen.getByText(/My Dashboard/i);
  expect(header).toBeInTheDocument();

  const button = screen.getByRole('button', { name: /Submit/i });
  expect(button).toBeInTheDocument();
  expect(button).toBeDisabled();

  const input = screen.getByPlaceholderText('Enter your name');
  expect(input).toBeInTheDocument();
  expect(input.value).toBe('');

  fireEvent.change(input, { target: { value: 'John Doe' } });
  expect(input.value).toBe('John Doe');

  expect(button).not.toBeDisabled();

  fireEvent.click(button);

  const successMessage = screen.getByText(/Success!/i);
  expect(successMessage).toBeInTheDocument();
});
</code></pre>
<p>Now you wouldn't know it at first glance but the above test case is used to validate
a form where the user can submit their name. The test technically works, but it does
not communicate it's intent.</p>
<p>I see code like this all the time. Especially when working with UI frameworks like
React, Angular, and Vue. Developers pile assertion upon assertion into one large
test case. And when a new requirement comes along? they just add in yet another
assertion.</p>
<p>Why? because the set-up is <em>done</em>, and bulky, and they don't want to have to rewrite
or duplicate the work.</p>
<chicken-asks>
So how do we fix this?
</chicken-asks>
<magpie-replies>
We break out each assertion into its own test.
</magpie-replies>
<h2>The Refactor</h2>
<pre><code class="language-javascript">import { render, screen, fireEvent } from '@testing-library/react';
import { beforeEach, expect, test } from 'vitest';
import MyComponent from './MyComponent';

describe('User Name Submit Form', () =&gt; {
    let apiFetchMock;

    beforeEach(() =&gt; {
        const mockApiData = { user: 'John Doe' };
        apiFetchMock = vi.fn(() =&gt; Promise.resolve(mockApiData));

        render(&lt;MyComponent fetchData={apiFetchMock} /&gt;);
    });

    test('renders header', () =&gt; {
        const header = screen.getByText(/My Dashboard/i);
        expect(header).toBeInTheDocument();
    });

    test('renders input field', () =&gt; {
        const input = screen.getByPlaceholderText('Enter your name');
        expect(input).toBeInTheDocument();
    });

    test('initial input field is empty', () =&gt; {
        const input = screen.getByPlaceholderText('Enter your name');
        expect(input.value).toBe('');
    });

    test('changes input value on user input', () =&gt; {
        const input = screen.getByPlaceholderText('Enter your name');
        fireEvent.change(input, { target: { value: 'John Doe' } });
        expect(input.value).toBe('John Doe');
    });

    test('renders submit button', () =&gt; {
        const button = screen.getByRole('button', { name: /Submit/i });
        expect(button).toBeInTheDocument();
    });

    test('submit button is disabled initially', () =&gt; {
        const button = screen.getByRole('button', { name: /Submit/i });
        expect(button).toBeDisabled();
    });

    test('submit button is enabled after input change', () =&gt; {
        const input = screen.getByPlaceholderText('Enter your name');
        fireEvent.change(input, { target: { value: 'John Doe' } });
        const button = screen.getByRole('button', { name: /Submit/i });
        expect(button).not.toBeDisabled();
    });
});

</code></pre>
<p>In the above refactor any shared test set up is broken out into a <code>beforeEach</code> hook
and each assertion is given its own test case.</p>
<p>The advantage of doing this becomes immediately clear. The tests now read like a
story. We know exactly what the intent of each test case is, and  we only have to
read a small amount of code to understand the test's logic. To accentuate my point
about read ability. If we were to take the name of each test case and list it out
we would have the following verse.</p>
<pre><code>User Name Submit Form
    renders header
    renders input field
    initial input field is empty
    changes input value on user input
    renders submit button
    submit button is disabled initially
    submit button is enabled after input change
</code></pre>
<p>This reads like a list of requirements. Which is essentially what UI tests are.
Compare this with the story the initial test gave us.</p>
<pre><code>User Name Submit Form
</code></pre>
<p>Yea, not as descriptive. Who knows what that test does. Not me.</p>
<p>So in summary, try your best to write your test cases as if they were telling a
story. Try to stick to one assertion per test, and split out any shared logic into
hooks, like <code>beforeEach</code> and <code>beforeAll</code>.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Slugs with an ID fallback in Laravel]]></title>
      <link>https://briandouglas.ie/laravel-slug-with-id-fallback</link>
      <guid>https://briandouglas.ie/laravel-slug-with-id-fallback</guid>
      <pubDate>2024-06-12T00:00:00.000Z</pubDate>
      <description><![CDATA[Slugs make a URL look legit. No one wants to click on a link that reads like &#x60;https://mysite.com/blog/1&#x60;. I&#x27;d definitely get hacked if I clicked that. But what if it read &#x60;https://mysite.com/blog/1/some-awesome-content&#x60;. Now that looks trustworthy. I&#x27;m definitely gonna see some awesome content if I click that link.

]]></description>
      <content:encoded><![CDATA[
        <h2>Naive Way</h2>
<p>So how do we implement it? Well we could use the <code>blog</code> resource's title. Laravel provides a <code>Str::slug</code> helper which takes a string and <em>sluggifies</em> it. Meaning <code>My Awesome Blog Post</code> becomes <code>my-awesome-blog-post</code>. This seems like a great idea. Let's update our <code>Blog</code> model so that when it is saved it sets a <code>slug</code> value based on the title.</p>
<pre><code class="language-php">use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;

class Blog extends Model
{
    protected static function boot()
    {
        parent::boot();

        static::saving(function ($model) {
            if (empty($model-&gt;slug)) {
                $model-&gt;slug = Str::slug($model-&gt;title);
            }
        });
    }
}
</code></pre>
<p>Now we can implement the resource's <code>show</code> route like so:</p>
<pre><code class="language-php">public function show(string $slug)
{
    return Blog::withSlug($slug)-&gt;first();
}
</code></pre>
<p>Awesome. What a completely foolproof implementation.</p>
<chicken-asks>
But what if we update the title?!?
</chicken-asks>
<magpie-replies>
If the title is updated the `slug` may no longer match the content.
</magpie-replies>
<chicken-asks>
Or worse, the `slug` might update too!
</chicken-asks>
<magpie-replies>
Then the previous url will return a 404, which may break embedded content.
</magpie-replies>
<h2>The Smart Way</h2>
<p>Yes, so not foolproof after all. The <code>title</code> of the blog may change. So it's not something we want to base our urls off, because we don't want our urls to change.</p>
<p>So what shouldn't change about our blog? it's id. At the start of this article we showed an ugly url <code>https://mysite.com/blog/1</code>. The good thing about this <em>ugly</em> url is it shouldn't change. By visiting it you will always view the blog with id 1. The downside is it's not descriptive. So let's combine the unique identifier of the id with the slug based of the blog's title. By doing this we get a url like so, <code>https://mysite.com/blog/1/my-awesome-content</code>. The trick here is that the slug part of the url is just for aesthetics. It gives the user an idea of what the blog with id 1 might be about. Let's implement this in Laravel so that the url works but uses the id solely to get the blog and redirects to the updated slug if the title changes.</p>
<h3>The Code</h3>
<p>We can achieve the above scenario like so. Firstly, in our routes file we will specify the following show route.</p>
<pre><code class="language-php">Route::get('blog/{blog}/{slug?}', [BlogController::class, 'show'])-&gt;name('blog.show');
</code></pre>
<p>Now we'll update our <code>Blog</code> model to contain a helper method, that constructs the new show route.</p>
<pre><code class="language-php">use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;

class Blog extends Model
{
    public function showRoute(array $parameters = [])
    {
        return route('blog.show', [$this, Str::slug($this-&gt;title), ...$parameters]);
    }
}
</code></pre>
<p>Finally, in the <code>show</code> method of our <code>BlogController</code> we will add the following.</p>
<pre><code class="language-php">public function show(Request $request, Blog $blog)
{
    if (! Str::contains($blog-&gt;showRoute(), $request-&gt;path())) {
        return redirect($blog-&gt;showRoute($request-&gt;query()), status: 301);
    }

    return view('blog.show', compact('blog'));
}
</code></pre>
<p>In the above <code>show</code> method, Laravel's route model binding is used to find the correct blog based on the id supplied. We then add an extra check to make sure the <code>$request-&gt;path()</code> matches the <code>showRoute()</code>. If it does not we redirect to the correct <code>showRoute()</code> with a status of <code>301 Moved Permanently</code> to let the browser know the slug has updated.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Seeding a Laravel DB with Irish Counties]]></title>
      <link>https://briandouglas.ie/laravel-seed-db-with-irish-counties</link>
      <guid>https://briandouglas.ie/laravel-seed-db-with-irish-counties</guid>
      <pubDate>2024-06-06T00:00:00.000Z</pubDate>
      <description><![CDATA[This is a step-by-step guide on how to seed your database with Irish Counties grouped by their Province.

]]></description>
      <content:encoded><![CDATA[
        <h2>Step 1 - Province Migration</h2>
<pre><code>php artisan make:migration create_provinces_table
</code></pre>
<p>All we need from a <code>Province</code> is it's name.</p>
<pre><code class="language-php">Schema::create('provinces', function (Blueprint $table) {
    $table-&gt;id();
    $table-&gt;string('name');
    $table-&gt;timestamps();
});
</code></pre>
<h2>Step 2 - County Migration</h2>
<pre><code>php artisan make:migration create_counties_table
</code></pre>
<p>As well as a name a <code>County</code> will hold a reference to the <code>Province</code> it belongs to.</p>
<pre><code class="language-php">Schema::create('counties', function (Blueprint $table) {
    $table-&gt;id();
    $table-&gt;string('name');
    $table-&gt;foreignIdFor(Province::class);
    $table-&gt;timestamps();
});
</code></pre>
<h2>Step 3 - Province Model</h2>
<pre><code>php artisan make:model Province
</code></pre>
<p>Here we add <code>name</code> as a fillable property and set up the <code>HasMany</code> relationship with <code>County</code>.</p>
<pre><code class="language-php">&lt;?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Province extends Model
{
    use HasFactory;

    protected $fillable = ['name'];

    public function counties(): HasMany
    {
        return $this-&gt;hasMany(County::class);
    }
}
</code></pre>
<h2>Step 4 - County Model</h2>
<pre><code>php artisan make:model County
</code></pre>
<p>Here we add <code>name</code> and <code>province_id</code> as a fillable property and set up the <code>BelongsTo</code> relationship with <code>Province</code>.</p>
<pre><code class="language-php">&lt;?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class County extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'province_id'];

    public function province(): BelongsTo
    {
        return $this-&gt;belongsTo(Province::class);
    }
}
</code></pre>
<h2>Step 5 - Province Seeder</h2>
<pre><code>php artisan make:seeder ProvinceSeeder
</code></pre>
<p>The <code>ProvinceSeeder</code> will create a record for each of Ireland's provinces and will attach the relevant counties.</p>
<pre><code class="language-php">&lt;?php

namespace Database\Seeders;

use App\Models\Province;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class ProvinceSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        $irishCounties = [
            'Leinster' =&gt; [
                'Carlow',
                'Dublin',
                'Kildare',
                'Kilkenny',
                'Laois',
                'Longford',
                'Louth',
                'Meath',
                'Offaly',
                'Westmeath',
                'Wexford',
                'Wicklow'
            ],
            'Munster' =&gt; [
                'Clare',
                'Cork',
                'Kerry',
                'Limerick',
                'Tipperary',
                'Waterford'
            ],
            'Connacht' =&gt; [
                'Galway',
                'Leitrim',
                'Mayo',
                'Roscommon',
                'Sligo'
            ],
            'Ulster' =&gt; [
                'Antrim',
                'Armagh',
                'Cavan',
                'Derry',
                'Donegal',
                'Down',
                'Fermanagh',
                'Monaghan',
                'Tyrone'
            ]
        ];

        foreach ($irishCounties as $provinceName =&gt; $countyNames) {
            $province = Province::firstOrCreate(['name' =&gt; $provinceName]);
            foreach ($countyNames as $countyName) {
                $province-&gt;counties()-&gt;firstOrCreate(['name' =&gt; $countyName]);
            }
        }
    }
}
</code></pre>
<h2>Step 6 - Run Seeder</h2>
<pre><code>php artisan db:seed --class=ProvinceSeeder
</code></pre>
<p>As the provinces and counties will not change, the seeder will only need run once.</p>

      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Genghis Castle Battle]]></title>
      <link>https://briandouglas.ie/castle-battles-game</link>
      <guid>https://briandouglas.ie/castle-battles-game</guid>
      <pubDate>2023-09-08T00:00:00.000Z</pubDate>
      <description><![CDATA[&lt;iframe height&#x3D;&quot;167&quot; frameborder&#x3D;&quot;0&quot; src&#x3D;&quot;https://itch.io/embed/2245236&quot; width&#x3D;&quot;552&quot;&gt;&lt;a href&#x3D;&quot;https://briandouglasie.itch.io/castle-battles&quot;&gt;Genghis Castle Battle by BrianDouglasIE&lt;/a&gt;&lt;/iframe&gt;

]]></description>
      <content:encoded><![CDATA[
        
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Beam Moo Up]]></title>
      <link>https://briandouglas.ie/beam-moo-up-game</link>
      <guid>https://briandouglas.ie/beam-moo-up-game</guid>
      <pubDate>2023-05-08T00:00:00.000Z</pubDate>
      <description><![CDATA[&lt;iframe width&#x3D;&quot;552&quot; src&#x3D;&quot;https://itch.io/embed/2059034&quot; height&#x3D;&quot;167&quot; frameborder&#x3D;&quot;0&quot;&gt;&lt;a href&#x3D;&quot;https://briandouglasie.itch.io/beam-moo-up&quot;&gt;Beam Moo Up by BrianDouglasIE&lt;/a&gt;&lt;/iframe&gt;

]]></description>
      <content:encoded><![CDATA[
        
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Time Match]]></title>
      <link>https://briandouglas.ie/time-match-game</link>
      <guid>https://briandouglas.ie/time-match-game</guid>
      <pubDate>2023-04-26T00:00:00.000Z</pubDate>
      <description><![CDATA[&lt;iframe frameborder&#x3D;&quot;0&quot; src&#x3D;&quot;https://itch.io/embed/2026070&quot; width&#x3D;&quot;552&quot; height&#x3D;&quot;167&quot;&gt;&lt;a href&#x3D;&quot;https://briandouglasie.itch.io/time-match&quot;&gt;Time Match by BrianDouglasIE&lt;/a&gt;&lt;/iframe&gt;

]]></description>
      <content:encoded><![CDATA[
        
      ]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Connection Defender]]></title>
      <link>https://briandouglas.ie/connection-defender-game</link>
      <guid>https://briandouglas.ie/connection-defender-game</guid>
      <pubDate>2023-03-05T00:00:00.000Z</pubDate>
      <description><![CDATA[&lt;iframe width&#x3D;&quot;552&quot; src&#x3D;&quot;https://itch.io/embed/1956223&quot; height&#x3D;&quot;167&quot; frameborder&#x3D;&quot;0&quot;&gt;&lt;a href&#x3D;&quot;https://briandouglasie.itch.io/connection-defender&quot;&gt;Connection Defender by BrianDouglasIE&lt;/a&gt;&lt;/iframe&gt;

]]></description>
      <content:encoded><![CDATA[
        
      ]]></content:encoded>
    </item>
  </channel>
</rss>
