<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Zikani's Blog]]></title><description><![CDATA[Zikani's Blog]]></description><link>https://code.zikani.me</link><generator>RSS for Node</generator><lastBuildDate>Sat, 18 Apr 2026 07:14:44 GMT</lastBuildDate><atom:link href="https://code.zikani.me/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Generating test data in JSON, CSV and SQL formats using Go and Faker]]></title><description><![CDATA[Sometimes, you may need to generate fake data for use for testing behavior of your systems or for load or volume testing, for example to test that pagination behavior works well with hundreds or hundr]]></description><link>https://code.zikani.me/generating-test-data-in-json-csv-and-sql-formats-using-go-and-faker</link><guid isPermaLink="true">https://code.zikani.me/generating-test-data-in-json-csv-and-sql-formats-using-go-and-faker</guid><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Fri, 20 Feb 2026 08:39:08 GMT</pubDate><content:encoded><![CDATA[<p>Sometimes, you may need to generate fake data for use for testing behavior of your systems or for load or volume testing, for example to test that pagination behavior works well with hundreds or hundreds of thousands of records .</p>
<p>In this tutorial, you will learn how to accomplish that using Go using the Faker package to generate data and then see how to export that data as JSON, CSV and as SQL insert statements, ready for inserting into your RDBMS.</p>
<p>Let's get started by creating a new project and initializing a Go module.</p>
<pre><code class="language-sh">$ mkdir datagen

$ cd datagen

$ go mod init datagen
</code></pre>
<p>Next, use <code>go get</code> to add the faker package</p>
<pre><code class="language-sh">$ go get -u github.com/go-faker/faker/v4
</code></pre>
<p>Now create a new file named <code>main.go</code> which will house the code.</p>
<h3>Generating random people</h3>
<p>This next part is where it gets interesting and the real action begins. But, first and foremost you must know your domain model or the shape of the data you want to work with.</p>
<blockquote>
<p>You must know your domain model or the shape of the data you want to work with in order to generate good test data.</p>
</blockquote>
<p>In the examples below, we will pretend we need to create data for a CRM-like product, and for ease of showing the concepts we will only deal with a <code>Person</code> entity, which we will also keep lean and add only a few fields. However, know that the techniques shown here work for types or entities with lots of fields</p>
<pre><code class="language-go">package main

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/go-faker/faker/v4"
)

type Person struct {
	FirstName    string
	LastName     string
	DateOfBirth  *time.Time
	Email        string
	Phone        string
	OtherNumbers []string
	Address      string
}

func main() {
	dateOfBirth, err := time.Parse(time.DateOnly, faker.Date())
	if err != nil {
		panic(err)
	}
	person := Person{
		FirstName:   faker.FirstName(),
		LastName:    faker.LastName(),
		DateOfBirth: &amp;dateOfBirth,
		Email:       faker.Email(),
		Phone:       faker.Phonenumber(),
		OtherNumbers: []string{
			faker.Phonenumber(),
		},
		Address: faker.GetRealAddress().Address,
	}

	fmt.Printf("%v", person)
	// okay that doesn't look great

	// let's use json
	data, err := json.Marshal(person)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data))
}
</code></pre>
<p>We can run this with the following</p>
<pre><code class="language-bash">$ go run main.go
</code></pre>
<h3>Generating 1,000 random people</h3>
<p>Now, usually you will want to generate a lot of data for stress testing, or testing out different use cases. So let's generate a lot more data - it’s as simple as just adding in a loop.</p>
<p>You can replace the contents of main with the following which generates 1,000 individuals, the limit can be tuned by changing the value of the constant <code>N_PEOPLE</code></p>
<pre><code class="language-go">package main

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/go-faker/faker/v4"
)

type Person struct {
	FirstName    string
	LastName     string
	DateOfBirth  *time.Time
	Email        string
	Phone        string
	OtherNumbers []string
	Address      string
}

const N_PEOPLE = 1000

func main() {
	people := make([]Person, 0)
	for range N_PEOPLE {
		dateOfBirth, err := time.Parse(time.DateOnly, faker.Date())
		if err != nil {
			panic(err)
		}
		person := Person{
			FirstName:   faker.FirstName(),
			LastName:    faker.LastName(),
			DateOfBirth: &amp;dateOfBirth,
			Email:       faker.Email(),
			Phone:       faker.Phonenumber(),
			OtherNumbers: []string{
				faker.Phonenumber(),
			},
			Address: faker.GetRealAddress().Address,
		}
		people = append(people, person)
	}

	// let's use json
	data, err := json.Marshal(people)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data))
}
</code></pre>
<p>Again, you can run this with <code>go run main.go</code></p>
<p>What you may have noted is that this writes the result to the Terminal (or rather to stdout). In order to write to a file, we can pipe the output to a JSON file. Running the following will give you a <code>people.json</code> file that contains people records.</p>
<pre><code class="language-go">$ go run main.go &gt; people.json
</code></pre>
<h3>Exporting generated data to CSV</h3>
<p>Sometimes you may need data for testing import functionality that takes in a CSV file, lets see how to generate CSV file. Replace main with the following:</p>
<pre><code class="language-go">package main

import (
	"encoding/csv"
	"os"
	"strings"
	"time"

	"github.com/go-faker/faker/v4"
)

type Person struct {
	FirstName    string
	LastName     string
	DateOfBirth  *time.Time
	Email        string
	Phone        string
	OtherNumbers []string
	Address      string
}

const N_PEOPLE = 1000

func (p Person) toRecord() []string {
	return []string{
		p.FirstName,
		p.LastName,
		p.DateOfBirth.Format(time.DateOnly),
		p.Email,
		p.Phone,
		strings.Join(p.OtherNumbers, ";"),
		p.Address,
	}
}

func toRecords(people []Person) [][]string {
	records := make([][]string, 0)
	for _, p := range people {
		records = append(records, p.toRecord())
	}
	return records
}

func main() {
	people := make([]Person, 0)
	for range N_PEOPLE {
		dateOfBirth, err := time.Parse(time.DateOnly, faker.Date())
		if err != nil {
			panic(err)
		}
		person := Person{
			FirstName:   faker.FirstName(),
			LastName:    faker.LastName(),
			DateOfBirth: &amp;dateOfBirth,
			Email:       faker.Email(),
			Phone:       faker.Phonenumber(),
			OtherNumbers: []string{
				faker.Phonenumber(),
			},
			Address: faker.GetRealAddress().Address,
		}
		people = append(people, person)
	}

	writer := csv.NewWriter(os.Stdout)
	writer.WriteAll(toRecords(people))
}
</code></pre>
<p>We can run this and write to CSV file with the following command</p>
<pre><code class="language-sh">$ go run main.go &gt; people.csv
</code></pre>
<h3>Generating data as SQL insert statements</h3>
<p>For most systems, you may be using a relational database of some kind. Here is the same code adapted to generate SQL insert statements that can be used to import data into a <code>people</code> table. Again, this is an example and your table names, columns, etc.. depend on the structure of your data.</p>
<pre><code class="language-go">package main

import (
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/go-faker/faker/v4"
)

type Person struct {
	ID           int64
	FirstName    string
	LastName     string
	DateOfBirth  *time.Time
	Email        string
	Phone        string
	OtherNumbers []string
	Address      string
}

const N_PEOPLE = 1000

func quote(unquoted string) string {
	return fmt.Sprintf(`'%s'`, strings.ReplaceAll(unquoted, "'", `''`))
}

func (p Person) toValuesRow() string {
	values := strings.Join([]string{
		quote(p.FirstName),
		quote(p.LastName),
		quote(p.DateOfBirth.Format(time.DateOnly)),
		quote(p.Email),
		quote(p.Phone),
		quote(strings.Join(p.OtherNumbers, ";")),
		quote(p.Address),
	}, ",")

	return fmt.Sprintf("(%s)", values)
}

func main() {
	people := make([]Person, 0)
	for i := range N_PEOPLE {
		dateOfBirth, err := time.Parse(time.DateOnly, faker.Date())
		if err != nil {
			panic(err)
		}
		person := Person{
			ID:          int64(i + 1),
			FirstName:   faker.FirstName(),
			LastName:    faker.LastName(),
			DateOfBirth: &amp;dateOfBirth,
			Email:       faker.Email(),
			Phone:       faker.Phonenumber(),
			OtherNumbers: []string{
				faker.Phonenumber(),
			},
			Address: faker.GetRealAddress().Address,
		}
		people = append(people, person)
	}

	sb := strings.Builder{}
	sb.WriteString("INSERT INTO people(first_name, last_name, date_of_birth, email, phone, other_numbers, address) VALUES \n")

	for i, p := range people {
		sb.WriteString(p.toValuesRow())
		if i == len(people)-1 {
			sb.WriteString(";\n")
		} else {
			sb.WriteString(",\n")
		}
	}

	fmt.Fprint(os.Stdout, sb.String())
}
</code></pre>
<p>Here is how to run the code to generate the SQL script, using the shell pipe operator again.</p>
<pre><code class="language-sh">$ go run main.go &gt; people.sql
</code></pre>
<h2>Conclusion</h2>
<p>Generating test data is important and typical for testing systems to verify behavior and for load/volume testing. We have seen how to use Go and the Faker package to generate such data and write to JSON, CSV and SQL files. We used a simple example, but the techniques here can be adapted for generating data for different domains and scenarios. The limit is your imagination. You can even adapt and evolve the ideas into CLI tool for your specific projects or teams.  </p>
<p>I hope you found this useful.</p>
]]></content:encoded></item><item><title><![CDATA[Machine Learning based SPAM detection using ONNX in Java]]></title><description><![CDATA[In this article I go over how I implemented a Spring Boot API for Spam Detection using an advanced anti-spam model from the Hugging Face onnx-community and Microsoft’s ONNX Runtime for Java. We will package the API up as a Docker image which we can r...]]></description><link>https://code.zikani.me/machine-learning-based-spam-detection-using-onnx-in-java</link><guid isPermaLink="true">https://code.zikani.me/machine-learning-based-spam-detection-using-onnx-in-java</guid><category><![CDATA[Java]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[spam detection]]></category><category><![CDATA[ONNX]]></category><category><![CDATA[onnxruntime]]></category><category><![CDATA[huggingface]]></category><category><![CDATA[nlp]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Sun, 08 Feb 2026 21:45:02 GMT</pubDate><content:encoded><![CDATA[<p>In this article I go over how I implemented a Spring Boot API for Spam Detection using an advanced anti-spam model from the <a target="_blank" href="https://huggingface.co/onnx-community/models">Hugging Face onnx-community</a> and Microsoft’s <a target="_blank" href="https://onnxruntime.ai/docs/get-started/with-java.html">ONNX Runtime for Java</a>. We will package the API up as a Docker image which we can run a container from using docker or podman, and I guess in theory you could deploy on your Kubernetes cluster, if you (are) fancy.</p>
<p>The code for this project is on a GitHub repo: <a target="_blank" href="https://github.com/zikani03/spam-detection-with-onnx">https://github.com/zikani03/spam-detection-with-onnx</a></p>
<h2 id="heading-which-model-to-use">Which model to use?</h2>
<p>SPAM detection is a very important part of modern digital communications especially if your running platforms that accept User Generated Content (UGC). Implementing SPAM detection is one of the classic machine learning problems, and there are many approaches to doing so.</p>
<p>Fortunately, it is possible to find an open SPAM detection model now on Hugging Face and use it without much ado, even for commercial use. As I was looking around on Huggingface I came across <a target="_blank" href="https://huggingface.co/Titeiiko/OTIS-Official-Spam-Model">OTIS</a>, from the description of the project it says</p>
<blockquote>
<p>Otis is an advanced anti-spam artificial intelligence model designed to mitigate and combat the proliferation of unwanted and malicious content within digital communication channels.</p>
</blockquote>
<p>Sounds interesting enough, so I looked to see if there was an ONNX version of this model and was glad to find that the onnx-community organization has exactly that, <a target="_blank" href="https://huggingface.co/onnx-community/OTIS-Official-Spam-Model-ONNX">here</a>.</p>
<p>So the next step was to download the <code>model.onnx</code> and <code>tokenizer.json</code> files and include them in the project. Otis is licensed under BSD 3-Clause license for the curious.</p>
<h2 id="heading-the-controller">The Controller</h2>
<p>The controller isn’t much but here it is for reference, as you can see we have defined our API endpoint at the path: <code>/api/spam/check</code> which is intended to be called via a POST request. We rely on Spring’s internal content negotiation for the request and responses meaning we can expect to be able to send and receive JSON.</p>
<pre><code class="lang-java"><span class="hljs-meta">@RequestMapping("/api/spam/check")</span>
<span class="hljs-meta">@RestController</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SpamCheckerController</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> SpamDetectionService spamDetectionService;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">SpamCheckerController</span><span class="hljs-params">(<span class="hljs-meta">@Autowired</span>  SpamDetectionService spamDetectionService)</span> </span>{
        <span class="hljs-keyword">this</span>.spamDetectionService = spamDetectionService;
    }

    <span class="hljs-meta">@PostMapping</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> ResponseEntity&lt;SpamCheckResponse&gt; <span class="hljs-title">checkSpam</span><span class="hljs-params">(<span class="hljs-meta">@RequestBody</span> SpamCheckRequest request)</span> <span class="hljs-keyword">throws</span> Exception </span>{
        <span class="hljs-keyword">return</span> ok(spamDetectionService.detectSpam(request));
    }
}
</code></pre>
<h2 id="heading-the-spam-detection-service">The Spam Detection Service</h2>
<p>The end goal is to have an API that can be called from HTTP client. But In order to separate concerns, we place the inference code for the Spam detection in a class named <code>SpamDetectionService</code> with an appropriate <code>@Service</code> annotation.</p>
<p>Inside this class we leverage the ONNX runtime for Java, passing the paths to the model and tokenizer files to initiate a <a target="_blank" href="https://djl.ai/extensions/tokenizers/">HuggingFaceTokenizer</a> . Here is the full code of the service:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Service</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SpamDetectionService</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">AutoCloseable</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> HuggingFaceTokenizer tokenizer;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> OrtEnvironment env;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> OrtSession session;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">SpamDetectionService</span><span class="hljs-params">(
            <span class="hljs-meta">@Value("${model.path:-/models/model.onnx}")</span> String modelPath,
            <span class="hljs-meta">@Value("${tokenizer.path:-/models/tokenizer.json}")</span> String tokenizerPath)</span> <span class="hljs-keyword">throws</span> IOException, OrtException </span>{

        <span class="hljs-keyword">this</span>.env = OrtEnvironment.getEnvironment();
        <span class="hljs-comment">// Load session options -- no particular settings for GPU or CUDA environments</span>
        OrtSession.SessionOptions options = <span class="hljs-keyword">new</span> OrtSession.SessionOptions();
        options.setInterOpNumThreads(<span class="hljs-number">2</span>);

        <span class="hljs-keyword">this</span>.session = env.createSession(modelPath, options);
        <span class="hljs-keyword">this</span>.tokenizer = HuggingFaceTokenizer.builder()
                .optPadding(<span class="hljs-keyword">true</span>) <span class="hljs-comment">// Add 0s if text is too short</span>
                .optTruncation(<span class="hljs-keyword">true</span>) <span class="hljs-comment">// Cut off if text is too long</span>
                .optTokenizerPath(Paths.get(tokenizerPath))
                .build();
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> SpamCheckResponse <span class="hljs-title">detectSpam</span><span class="hljs-params">(SpamCheckRequest request)</span> <span class="hljs-keyword">throws</span> OrtException </span>{
        <span class="hljs-keyword">long</span> startTime = System.currentTimeMillis();
        <span class="hljs-keyword">var</span> response = <span class="hljs-keyword">this</span>.detectSpam(request.content());
        <span class="hljs-keyword">long</span> endTime = System.currentTimeMillis();
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> SpamCheckResponse(
                response.label,
                response.confidence,
                request.requestId(),
                endTime - startTime
        );
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> RawResult <span class="hljs-title">detectSpam</span><span class="hljs-params">(String text)</span> <span class="hljs-keyword">throws</span> OrtException </span>{
        Encoding encoding = tokenizer.encode(text);
        <span class="hljs-keyword">long</span>[] inputIds = encoding.getIds();
        <span class="hljs-keyword">long</span>[] attentionMask = encoding.getAttentionMask();
        <span class="hljs-keyword">long</span>[] shape = {<span class="hljs-number">1</span>, inputIds.length};

        <span class="hljs-keyword">try</span> (OnnxTensor inputTensor = OnnxTensor.createTensor(env, LongBuffer.wrap(inputIds), shape);
             OnnxTensor maskTensor = OnnxTensor.createTensor(env, LongBuffer.wrap(attentionMask), shape)) {

            Map&lt;String, OnnxTensor&gt; inputs = <span class="hljs-keyword">new</span> HashMap&lt;&gt;();
            inputs.put(<span class="hljs-string">"input_ids"</span>, inputTensor);
            inputs.put(<span class="hljs-string">"attention_mask"</span>, maskTensor);
            String tokenTypeIdsName = <span class="hljs-string">"token_type_ids"</span>;
            String outputName = session.getOutputNames().iterator().next();

            <span class="hljs-keyword">if</span> (session.getInputNames().contains(tokenTypeIdsName)) {
                <span class="hljs-keyword">long</span>[] tokenTypeIds = <span class="hljs-keyword">new</span> <span class="hljs-keyword">long</span>[inputIds.length];
                inputs.put(tokenTypeIdsName, OnnxTensor.createTensor(env, LongBuffer.wrap(tokenTypeIds), shape));
            }

            <span class="hljs-keyword">try</span> (OrtSession.Result results = session.run(inputs)) {
                <span class="hljs-keyword">return</span> formatResults(results, outputName);
            } <span class="hljs-keyword">finally</span> {
                inputs.values().forEach(OnnxTensor::close);
            }
        }
    }

    <span class="hljs-function">record <span class="hljs-title">RawResult</span><span class="hljs-params">(String label, <span class="hljs-keyword">float</span>[] probs, <span class="hljs-keyword">float</span> cleanProb, <span class="hljs-keyword">float</span> scamProb, <span class="hljs-keyword">float</span> confidence)</span> </span>{}

    <span class="hljs-function"><span class="hljs-keyword">private</span> RawResult <span class="hljs-title">formatResults</span><span class="hljs-params">(OrtSession.Result results, String outputName)</span> <span class="hljs-keyword">throws</span> OrtException </span>{
            <span class="hljs-keyword">float</span>[][] logitsArray = (<span class="hljs-keyword">float</span>[][]) results.get(outputName).get().getValue();
            <span class="hljs-keyword">float</span>[] rawLogits = logitsArray[<span class="hljs-number">0</span>];
            <span class="hljs-keyword">float</span>[] probs = softmax(rawLogits);

            <span class="hljs-keyword">float</span> cleanProb = probs[<span class="hljs-number">0</span>] * <span class="hljs-number">100</span>;
            <span class="hljs-keyword">float</span> scamProb = probs[<span class="hljs-number">1</span>] * <span class="hljs-number">100</span>;

            <span class="hljs-keyword">int</span> prediction = (probs[<span class="hljs-number">1</span>] &gt; probs[<span class="hljs-number">0</span>]) ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span>;

            String label = (prediction == <span class="hljs-number">1</span>) ? <span class="hljs-string">"SCAM"</span> : <span class="hljs-string">"CLEAN"</span>;

            <span class="hljs-keyword">float</span> confidence = (prediction == <span class="hljs-number">1</span>) ? scamProb : cleanProb;

            <span class="hljs-comment">//return ("Result: " + label + " (" + String.format("%.2f", confidence) + "% confidence)");</span>
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> RawResult(label, probs, cleanProb, scamProb, confidence);
    }


    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">float</span>[] softmax(<span class="hljs-keyword">float</span>[] logits) {
        <span class="hljs-keyword">float</span>[] probabilities = <span class="hljs-keyword">new</span> <span class="hljs-keyword">float</span>[logits.length];
        <span class="hljs-keyword">float</span> maxLogit = Float.NEGATIVE_INFINITY;
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">float</span> v : logits) {
            <span class="hljs-keyword">if</span> (v &gt; maxLogit) maxLogit = v;
        }
        <span class="hljs-keyword">float</span> sum = <span class="hljs-number">0.0f</span>;
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; logits.length; i++) {
            probabilities[i] = (<span class="hljs-keyword">float</span>) Math.exp(logits[i] - maxLogit);
            sum += probabilities[i];
        }
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; logits.length; i++) {
            probabilities[i] /= sum;
        }

        <span class="hljs-keyword">return</span> probabilities;
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">close</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{
        session.close();
        env.close();
        tokenizer.close();
    }
}
</code></pre>
<p>You may note that the paths have default values which point to a directory starting with <code>/models</code> that’s because we intend to run this by default from a Docker container.</p>
<p>However, you can customize the paths to these models using the following configuration in a Spring Boot configuration file, e.g. in application.yaml</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># application.yaml</span>
<span class="hljs-attr">model:</span>
  <span class="hljs-attr">path:</span> <span class="hljs-string">"/path/to/models/model.onnx"</span>
<span class="hljs-attr">tokenizer:</span>
  <span class="hljs-attr">path:</span> <span class="hljs-string">"/path/to/models/tokenizer.json"</span>
</code></pre>
<h2 id="heading-running-the-service-via-docker">Running the service via Docker</h2>
<p>The project in the repository uses Jib to build docker image from the Java source code. Run the following command to build the container, by default the created image will be named <strong>zikani03/spam-detection-with-onnx</strong></p>
<pre><code class="lang-bash">$ ./mvnw clean jib:dockerBuild
</code></pre>
<p>Once the build completes successfully you can run a docker container using the following, binding on port 8080 which the API runs at inside the container.</p>
<pre><code class="lang-bash">$ docker run -p <span class="hljs-string">"8080:8080"</span>  zikani03/spam-detection-with-onnx
</code></pre>
<p>Once that’s running, you can then test the SPAM Detection service using your favourite HTTP Client e.g. Postman, Insomnia or even just cURL</p>
<pre><code class="lang-bash">$ curl -X POST -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"requestId":"test","content":"Cһeck out our amazinɡ bооѕting serviсe ѡhere you can get to Leveӏ 3 for 3 montһs for just 20 USD.","token":"abc"}'</span> <span class="hljs-string">"http://localhost:8080/api/spam/check"</span>
</code></pre>
<p>You should get a result similar to this :</p>
<pre><code class="lang-json">{<span class="hljs-attr">"result"</span>:<span class="hljs-string">"SCAM"</span>,<span class="hljs-attr">"confidence"</span>:<span class="hljs-number">99.99815368652344</span>,<span class="hljs-attr">"id"</span>:<span class="hljs-string">"test"</span>,<span class="hljs-attr">"checkDurationMillis"</span>:<span class="hljs-number">149</span>}
</code></pre>
<p>I like to load test things with <a target="_blank" href="https://github.com/rakyll/hey">hey</a>, not bad.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770586347231/b7530ac8-b6e3-4aab-9eae-4f25365ea85b.png" alt class="image--center mx-auto" /></p>
<p>The performance is okay, considering this is all running on CPU and not GPU (which I’m sure you can use with the onnxruntime libraries).</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I have been curious about performing Machine Learning with Java for a while and ran into ONNX as I was trying out some Python stuff and got curious if I could leverage ONNX models in Java, and ofcourse you can! Microsoft’s onnxruntime for Java is a great place to start.</p>
<p>Sure, there is a lot more to add to this project to make it a real production-grade service, but I hope I have illustrated how it is possible to do some inference with Java and ONNX models. There are <a target="_blank" href="https://huggingface.co/onnx-community/models">many models out there</a> which you can leverage for different use cases.</p>
<p>I hope you are as excited about doing ML in Java too.</p>
]]></content:encoded></item><item><title><![CDATA[Using the Zig built LightPanda browser for web automation via  playwright-go]]></title><description><![CDATA[I am working on a browser automation thingy and want to try supporting LightPanda as I am interested in seeing if it may be a good lightweight alternative to Chromium for CI (Continuous Integration) tests. Komaso (that’s Chichewa, you can read it as ...]]></description><link>https://code.zikani.me/using-the-zig-built-lightpanda-browser-for-web-automation-via-playwright-go</link><guid isPermaLink="true">https://code.zikani.me/using-the-zig-built-lightpanda-browser-for-web-automation-via-playwright-go</guid><category><![CDATA[lightpanda]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[zig]]></category><category><![CDATA[Browsers]]></category><category><![CDATA[#chrome_devtools]]></category><category><![CDATA[E2E]]></category><category><![CDATA[Automated Testing]]></category><category><![CDATA[playwright]]></category><category><![CDATA[devtools]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Fri, 30 Jan 2026 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>I am working on a <a target="_blank" href="https://labs.zikani.me/basi">browser automation thingy</a> and want to try supporting <a target="_blank" href="https://lightpanda.io/">LightPanda</a> as I am interested in seeing if it may be a good lightweight alternative to Chromium for CI (Continuous Integration) tests. <strong>Komaso</strong> (that’s <a target="_blank" href="https://en.wikipedia.org/wiki/Chewa_language">Chichewa</a>, you can read it as as <em>but also</em>), I am just curious about <a target="_blank" href="https://lightpanda.io/blog/posts/migrating-our-dom-to-zig">their use of Zig</a> and find the project interesting.</p>
<h2 id="heading-connecting-the-gopher-to-the-panda">Connecting the gopher to the panda</h2>
<p>So in order to get this to work in basi, I first needed to see if we can swap out Chrome/Chromium for LightPanda using the Chrome Developer Protocol in playwright-go, the Go library we use in basi, to talk to browsers to tell them what to do. I saw a demo/examples the showing LightPanda use via Playwright NodeJS , so this should be possible, right?.</p>
<h3 id="heading-download-and-run-light-panda">Download and run Light Panda</h3>
<p>Download <a target="_blank" href="https://github.com/lightpanda-io/browser/releases">LightPanda Browser</a> from GitHub releases and then run it with the CDP services enabled on a port of our choice.</p>
<blockquote>
<p>NOTE: the name of the binary you run may be different, depending on your OS and which binary you downloaded. The example below shows for Linux on x86</p>
</blockquote>
<pre><code class="lang-bash">$ lightpanda-x86_64-linux serve --port 9226 --log_level debug
</code></pre>
<h3 id="heading-implementing-the-go-program-to-talk-to-lightpanda">Implementing the Go program to talk to LightPanda</h3>
<p>Create a new Go module project</p>
<pre><code class="lang-java">$ mkdir lightpanda-go-example

$ cd lightpanda-go-example

$ go mod init lightpandagoexample
</code></pre>
<p>Next, Paste the following code in a file named <code>main.go</code></p>
<p>This program starts a connection to LightPanda via Playwright by connecting to the browser using Chrome Devtools Protocol. After it connects, we try to automate a simple flow of going to the Playwright website and checking that the Link to the Community page contains the right text.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>

    playwrightgo <span class="hljs-string">"github.com/playwright-community/playwright-go"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    err := run()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatal(err)
    }
}
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {

    err := playwrightgo.Install(&amp;playwrightgo.RunOptions{
        Browsers: []<span class="hljs-keyword">string</span>{<span class="hljs-string">"chromium"</span>},
        DryRun:   <span class="hljs-literal">true</span>,
    })
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"could not launch playwright: %w"</span>, err)
    }

    pw, err := playwrightgo.Run()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"could not launch playwright: %w"</span>, err)
    }

    browser, err := pw.Chromium.ConnectOverCDP(<span class="hljs-string">"ws://localhost:9226"</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"could not launch LightPanda via CDP: %w"</span>, err)
    }
    context, err := browser.NewContext()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"could not create context: %w"</span>, err)
    }
    page, err := context.NewPage()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"could not create page: %w"</span>, err)
    }

    _, err = page.Goto(<span class="hljs-string">"https://playwright.dev/"</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"could not goto: %w"</span>, err)
    }

    loc := page.Locator(<span class="hljs-string">`a[href="/community/welcome"]`</span>)
    assertions := playwrightgo.NewPlaywrightAssertions()
    assert := assertions.Locator(loc)

    err = assert.ToHaveText(<span class="hljs-string">"Community"</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"could assert community link: %w"</span>, err)
    }

    err = browser.Close()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"could not close browser: %w"</span>, err)
    }
    err = pw.Stop()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"could not stop Playwright: %w"</span>, err)
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>

}
</code></pre>
<h3 id="heading-side-note-lets-compare-the-same-thing-with-playwright-go-directly-vs-using-basi">Side note: Let’s compare the same thing with playwright-go (directly) vs using basi</h3>
<p>For comparison here is how the same test would be written in a file named <code>example.basi</code> and executed with <code>$ basi run example.basi</code></p>
<pre><code class="lang-java">Goto <span class="hljs-string">"https://playwright.dev/"</span>
Find <span class="hljs-string">"a[href='/community/welcome']"</span>
ExpectText <span class="hljs-string">"Community"</span>
</code></pre>
<h3 id="heading-mandatory-for-now-patch-playwright-go-library">Mandatory for now: Patch playwright-go library</h3>
<p>At the time of writing, using playwright-go with lightpanda will only work if you clone playwright-go and checkout to the changes in the <a target="_blank" href="https://github.com/playwright-community/playwright-go/pull/581">pull request I opened</a> which fix an issue with playwright-go’s handling of the response from LightPanda’s implementation of the CDP.</p>
<blockquote>
<p>NOTE: this step is mandatory to make this example work until the PR is merged or similar changes applied. Since playwright-go is <a target="_blank" href="https://github.com/playwright-community/playwright-go/issues/122">looking for new maintainers</a>, I can’t say when that will be</p>
</blockquote>
<p>Here is how to patch a local copy of playwright-go :-</p>
<pre><code class="lang-bash">$ git <span class="hljs-built_in">clone</span> https://github.com/playwright-community/playwright-go

$ <span class="hljs-built_in">cd</span> ./playwright-go

<span class="hljs-comment"># Checkout to the PR I opened</span>
$ git fetch origin pull/581/head:main-with-cdp-fix

$ git checkout main-with-cdp-fix
</code></pre>
<p>Edit your <code>go.mod</code> to add a replace directive and make sure to point the path to the repository we just cloned, above :</p>
<pre><code class="lang-bash">replace github.com/playwright-community/playwright-go =&gt; /path/to/cloned/playwright-go
</code></pre>
<h3 id="heading-run-and-build-the-go-program">Run and build the Go program</h3>
<p>You can now run the Go program</p>
<pre><code class="lang-bash">$ go mod tidy

$ go run main.go
</code></pre>
<p>That should work if you followed the steps. Nice, we just used LightPanda browse for automated e2en browser tests!</p>
<h2 id="heading-the-way-forward">The way forward</h2>
<p>The Zig community is putting out great projects, from TigerBeetle, Ghostty, more. I was intrigued by LightPanda, a headless web browser and was curious if I could use it from a browser automation tool I an working on that’s intended to make authoring and running e2e browser tests more approachable. It’s definitely possible! Being able to connect to LightPanda from Go via playwright means there’s a path to add support for it to basi and hopefully that means making the testing more lightweight especially for CI.</p>
<p>Currently, LightPanda doesn’t support a few things including Form file uploads and Screenshots. So we cannot replace Chrome/Chromium yet but I’m sure LightPanda will grow into maturity as it gets more use and development. Will definitely be watching the space, maybe I may just learn enough Zig to help out too. Even if that doesn’t happen, it’s always fun to tinker with new technology and see the potential of how one can use it today or in the future.</p>
<p>Hope you found this interesting. Thanks.</p>
]]></content:encoded></item><item><title><![CDATA[How Starting Today Wins Tomorrow]]></title><description><![CDATA[Whenever you work on anything, it's typically to address a present or future need or activity. As much as that is the case, most tasks that people work on were defined or specified in the past. Thinking about that, you can reasonably assert that most...]]></description><link>https://code.zikani.me/how-starting-today-wins-tomorrow</link><guid isPermaLink="true">https://code.zikani.me/how-starting-today-wins-tomorrow</guid><category><![CDATA[Productivity]]></category><category><![CDATA[llm]]></category><category><![CDATA[tasks]]></category><category><![CDATA[project management]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Sat, 17 Jan 2026 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Whenever you work on anything, it's typically to address a present or future need or activity. As much as that is the case, most tasks that people work on were defined or specified in the past. Thinking about that, you can reasonably assert that most of our tasks and milestones are targeted toward the future, or I should say “a future”. Thinking about it that way, you can infer that <strong>this very moment</strong> is some <em>weird present-version of the past</em>.</p>
<blockquote>
<p>This very moment is some <em>weird</em> present-version of the past.</p>
</blockquote>
<p>So what if we changed our approach to work and life with the confidence that whatever we do right now may be used or needed in a future however near or far. The consequence of this style of working is that it forces you to become good at somewhat predicting the future. Although the long term future is generally unpredictable, for those of us that are not prophets, near term tasks and activities can be predicted with higher level of confidence</p>
<p>Let me use a couple of examples :-</p>
<ul>
<li><p>Assume in 20 minutes you need to go out of the house to a meeting.Working in this style requires you to set things up to make that upcoming journey easier. You could set your shoes at the door, iron or press your clothes and put them somewhere accessible or within an arms reach from where you will start off. Doing those activities ahead of time will yield value at the time you actually start working on that task. You won't have to think about where to place or find things.</p>
</li>
<li><p>Another example of that is creating a very rough draft of a contract you need to send next week. The sooner you get to a draft, even one that just contains headers, the easier it will be to <strong>re-board</strong> (not onboard) that task later on - the friction will be less.</p>
</li>
</ul>
<p><strong>But what you are describing is just preparation, right?</strong></p>
<p>Yes and no. It goes further than what I will call “basic” preparation. What I am proposing here can be called <strong>preempting</strong>, or an advanced or more rigorous approach to preparation.</p>
<blockquote>
<p>Author’s Note: The definition of the word pre-empt and how it’s being used in this article may seem to be at odds, but let’s appropriate this word for our purposes. Bear with me.</p>
<p><a target="_blank" href="https://www.collinsdictionary.com/dictionary/english/preempt">Collins Dictionary</a> say’s If you <strong>preempt</strong> an action, you prevent it from happening by doing something that makes it unnecessary or impossible</p>
</blockquote>
<p>Pre-empting in this view, means setting things up so that your future self doesn't have to do extra work to get things done. This concept is powerful once you realize that there are a lot of things you can preempt, <em>and</em> that preempting doesn't always mean getting things into a final state. What I mean by that is that you can preemptively start on a task, an leave it in a state where it is started/running but not exactly complete.</p>
<blockquote>
<p>Preempting is like getting a ticket to an event, preparing is what you do when you are about to go to the event.</p>
</blockquote>
<h2 id="heading-leveraging-ai-to-preempt-work-at-the-computer">Leveraging AI to preempt work at the computer</h2>
<p>We are fortunate to live in an age where LLMs (Large Language Models) and Agentic AI solutions exist. Even though they may be in their early forms, these tools can help us pre-empt work, especially work that needs to be done at the computer. One of the best ways to use these tools, that I have found, is to use them to get over the initial hurdle of starting something. For example, you can</p>
<ul>
<li><p>Ask ChatGPT or Gemini to give you a draft of a contract or proposal or just the headings you need</p>
</li>
<li><p>Ask Claude code to scaffold a codebase for you for an idea or to work on the initial implementation of a couple of features</p>
</li>
<li><p>Ask an image generation model to generate variations on ideas for a Logo or brand.</p>
</li>
</ul>
<p>… and many more</p>
<p>The possibilities, with AI in the mix are endless and the amount of productivity this can unlock is huge. In addition to productivity, I believe being able to preempt work can help reduce anxiety related to working on or completing a task.</p>
<p>Go forth and preempt the world.</p>
<p>Thanks for reading.</p>
]]></content:encoded></item><item><title><![CDATA[Pipelined PostgreSQL Queries in Go]]></title><description><![CDATA[In this article we will see how to leverage Postgres support for pipelining queries which “allows applications to send a query without having to read the result of the previously sent query”. This allows you for example to perform batch updates which...]]></description><link>https://code.zikani.me/pipelined-postgresql-queries-in-go</link><guid isPermaLink="true">https://code.zikani.me/pipelined-postgresql-queries-in-go</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[SQL]]></category><category><![CDATA[Batch Processing]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[pgx]]></category><category><![CDATA[Pipeline]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Fri, 05 Dec 2025 12:34:19 GMT</pubDate><content:encoded><![CDATA[<p>In this article we will see how to leverage Postgres support for <a target="_blank" href="https://www.postgresql.org/docs/current/libpq-pipeline-mode.html">pipelining queries</a> which “<em>allows applications to send a query without having to read the result of the previously sent query”</em>. This allows you for example to perform batch updates which you can send serially and comes with a significant performance boost in processing.</p>
<blockquote>
<p><em>Taking advantage of the pipeline mode, a client will wait less for the server, since multiple queries/results can be sent/received in a single network transaction.</em></p>
</blockquote>
<p>In this article we will see how to issue pipelined queries using <a target="_blank" href="https://github.com/jackc/pgx"><strong>pgx</strong></a> library in <strong>Go</strong></p>
<p>You will need the following</p>
<ul>
<li><p>Postgresql version 14 and above</p>
</li>
<li><p>Go 1.24+</p>
</li>
</ul>
<h2 id="heading-example-refund-each-customer-by-5-of-their-balance">Example: Refund each customer by 5% of their balance</h2>
<p>In this article I present a self contained code example that will 1) created a new table, 2) populate it with customers that have random balances and then, 3) uses pipelined queries to process refund for each client.</p>
<p>Because the queries are pipelined, it means our refund queries don’t have to wait for one customers refund query to complete to send the next one.</p>
<p>Now let’s code! Create a new Go project and copy the following code:</p>
<pre><code class="lang-go">$ mkdir pipelinedqueries

$ cd pipelinedqueries

$ <span class="hljs-keyword">go</span> mod init pipelinedqueries
</code></pre>
<p>Copy the following into a new file named <code>main.go</code></p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"math"</span>
    <span class="hljs-string">"math/rand"</span>
    <span class="hljs-string">"os"</span>
    <span class="hljs-string">"time"</span>

    <span class="hljs-string">"github.com/go-faker/faker/v4"</span>
    <span class="hljs-string">"github.com/jackc/pgx/v5"</span>
    <span class="hljs-string">"github.com/jackc/pgx/v5/pgconn"</span>
    <span class="hljs-string">"github.com/shopspring/decimal"</span>
)

<span class="hljs-keyword">const</span> (
    sqlCreateUsersTable = <span class="hljs-string">`
create table pipeline_customers ( 
    id serial not null primary key, 
    name text not null, 
    balance numeric(18,6) not null default 0, 
    last_active_at timestamptz
);
`</span>
    sqlRefundEachCustomer = <span class="hljs-string">`UPDATE pipeline_customers SET balance = balance + $1 where id = $2`</span>
)

<span class="hljs-keyword">type</span> Customer <span class="hljs-keyword">struct</span> {
    ID           <span class="hljs-keyword">int64</span>           <span class="hljs-string">`db:"id"`</span>
    Name         <span class="hljs-keyword">string</span>          <span class="hljs-string">`db:"name"`</span>
    Balance      decimal.Decimal <span class="hljs-string">`db:"balance"`</span>
    LastActiveAt time.Time       <span class="hljs-string">`db:"last_active_at"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">createCustomers</span><span class="hljs-params">(conn *pgx.Conn, N <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">error</span></span> {
    _, err := conn.Exec(context.Background(), <span class="hljs-string">`DROP TABLE IF EXISTS pipeline_customers;`</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    _, err = conn.Exec(context.Background(), sqlCreateUsersTable)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; N; i++ {
        _, err := conn.Exec(context.Background(), <span class="hljs-string">`INSERT INTO pipeline_customers(name, balance, last_active_at) VALUES ($1, $2, $3)`</span>,
            faker.Name(),
            decimal.NewFromFloat32(<span class="hljs-number">1000.0</span>*<span class="hljs-keyword">float32</span>(rand.Intn(<span class="hljs-number">100</span>))*<span class="hljs-number">1.31425326</span>),
            time.Now().Add(time.Duration(rand.Intn(<span class="hljs-number">72</span>)*<span class="hljs-number">-1</span>)*time.Hour),
        )
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">fetchAllCustomers</span><span class="hljs-params">(conn *pgx.Conn)</span> <span class="hljs-params">([]Customer, error)</span></span> {
    customers := <span class="hljs-built_in">make</span>([]Customer, <span class="hljs-number">0</span>)
    rows, err := conn.Query(context.Background(), <span class="hljs-string">`SELECT id,name,balance,last_active_at FROM pipeline_customers order by id asc`</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }
    <span class="hljs-keyword">for</span> rows.Next() {
        customer := Customer{}
        err := rows.Scan(&amp;customer.ID, &amp;customer.Name, &amp;customer.Balance, &amp;customer.LastActiveAt)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
        }
        customers = <span class="hljs-built_in">append</span>(customers, customer)
    }
    <span class="hljs-keyword">return</span> customers, <span class="hljs-literal">nil</span>
}

<span class="hljs-comment">// refundAllCustomers uses pgx's support for pipelined queiries to update each customer balance in a pipeline</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">refundAllCustomers</span><span class="hljs-params">(conn *pgx.Conn, customers []Customer, injectErrors <span class="hljs-keyword">bool</span>)</span> <span class="hljs-title">error</span></span> {
    ctx := context.Background()
    pipeline := conn.PgConn().StartPipeline(ctx)

    eqb := pgx.ExtendedQueryBuilder{}

    <span class="hljs-comment">// introduce error for 1% random customers</span>
    nErrors := <span class="hljs-keyword">int</span>(math.Round(<span class="hljs-keyword">float64</span>(<span class="hljs-built_in">len</span>(customers)) * <span class="hljs-number">0.01</span>))
    errorsInjected := <span class="hljs-number">0</span>

    <span class="hljs-keyword">for</span> _, customer := <span class="hljs-keyword">range</span> customers {
        <span class="hljs-comment">// calculate 5% of the customers balance</span>
        customerRefundAmount, _ := customer.Balance.Mul(decimal.NewFromFloat32(<span class="hljs-number">0.05</span>)).Float64()
        fmt.Println(<span class="hljs-string">"processing customer"</span>, customer.Name, customerRefundAmount)
        queryArgs := []any{
            customerRefundAmount,
            customer.ID,
        }
        <span class="hljs-comment">// introduce error random customers</span>
        <span class="hljs-keyword">if</span> injectErrors &amp;&amp; errorsInjected &lt; nErrors {
            <span class="hljs-keyword">if</span> rand.Intn(<span class="hljs-number">2</span>) &gt;= <span class="hljs-number">1</span> {
                queryArgs = []any{
                    fmt.Sprintf(<span class="hljs-string">"%f%s"</span>, customerRefundAmount, <span class="hljs-string">"RANDOM_STRING"</span>),
                    customer.ID,
                }
                errorsInjected += <span class="hljs-number">1</span>
            }
        }

        err := eqb.Build(conn.TypeMap(), <span class="hljs-literal">nil</span>, queryArgs)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to buld query: %v"</span>, err)
        }

        pipeline.SendQueryParams(sqlRefundEachCustomer, eqb.ParamValues, <span class="hljs-literal">nil</span>, eqb.ParamFormats, eqb.ResultFormats)

    }
    err := pipeline.Sync()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to sync pipeline query: %v"</span>, err)
    }
    results, err := pipeline.GetResults()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to get pipeline results: %v"</span>, err)
    }
    <span class="hljs-keyword">if</span> results == <span class="hljs-literal">nil</span> &amp;&amp; err == <span class="hljs-literal">nil</span> {
        <span class="hljs-comment">// no results received from server</span>
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    }
    <span class="hljs-comment">// spew.Dump(results)</span>
    _, ok := results.(*pgconn.ResultReader)
    <span class="hljs-keyword">if</span> !ok {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to read results from pipelined query: %v"</span>, err)
    }
    err = pipeline.Close()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to close pipeline: %v"</span>, err)
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">printCustomersTable</span><span class="hljs-params">(customers []Customer, n <span class="hljs-keyword">int</span>)</span></span> {
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(customers) &lt; n {
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; n; i++ {
        customer := customers[i]
        fmt.Printf(<span class="hljs-string">"Customer: %s \t\t Last Active: %s\n"</span>, customer.Name, customer.LastActiveAt.Format(time.DateOnly))
        fmt.Printf(<span class="hljs-string">"Balance: %s\n---\n"</span>, customer.Balance)
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    dbURL := <span class="hljs-string">"postgres://user:password@localhost:5432/pipeline_tutorial"</span>
    conn, err := pgx.Connect(context.Background(), dbURL)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        fmt.Fprintf(os.Stderr, <span class="hljs-string">"Unable to connect to database: %v\n"</span>, err)
        os.Exit(<span class="hljs-number">1</span>)
    }
    <span class="hljs-comment">// comment out the next few lines to prevent creating new customers on each run</span>
    err = createCustomers(conn, <span class="hljs-number">100</span>) <span class="hljs-comment">// N = number of customers</span>
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(fmt.Errorf(<span class="hljs-string">"createCustomers: %v"</span>, err))
    }

    customers, err := fetchAllCustomers(conn)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(fmt.Errorf(<span class="hljs-string">"fetchAllCustomers: %v"</span>, err))
    }
    <span class="hljs-comment">// printCustomersTable(customers, 10)</span>
    err = refundAllCustomers(conn, customers, <span class="hljs-literal">true</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(fmt.Errorf(<span class="hljs-string">"refundAllCustomers: %v"</span>, err))
    }

    fmt.Println(<span class="hljs-string">"------- AFTER PIPELINE QUERY -------"</span>)
    otherCustomers, err := fetchAllCustomers(conn)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-built_in">panic</span>(fmt.Errorf(<span class="hljs-string">"fetchAllCustomers: %v"</span>, err))
    }
    printCustomersTable(otherCustomers, <span class="hljs-number">10</span>)
}
</code></pre>
<p><strong>Side Note on SQL errors in pipeline queries:</strong> I have intentionally added error injection to this example code to show you how errors are treated when working with pipelined queries. We inject random error that results in INVALID SQL for 1% of our customers. What you will note when you run the code and the error conditions are satisfied is that the whole pipeline fails - this should give you confidence that SQL or database level errors won’t silently fail during processing pipeline. Logic errors are still something you have to make sure to handle.</p>
<p>The code with the following command:</p>
<pre><code class="lang-go"><span class="hljs-keyword">go</span> run main.<span class="hljs-keyword">go</span>
</code></pre>
<p>Let’s take a closer look at the core parts of the function that handles the Postgresql pipeline queries, I’ve stripped out the error injection and annotated the relevant lines :</p>
<pre><code class="lang-go"><span class="hljs-comment">// refundAllCustomers uses pgx's support for pipelined queries to update each customer balance in a pipeline</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">refundAllCustomers</span><span class="hljs-params">(conn *pgx.Conn, customers []Customer, injectErrors <span class="hljs-keyword">bool</span>)</span> <span class="hljs-title">error</span></span> {
    ctx := context.Background()
    pipeline := conn.PgConn().StartPipeline(ctx) <span class="hljs-comment">// 1 </span>

    eqb := pgx.ExtendedQueryBuilder{} <span class="hljs-comment">// 2</span>

    <span class="hljs-keyword">for</span> _, customer := <span class="hljs-keyword">range</span> customers {
        <span class="hljs-comment">// calculate 5% of the customers balance</span>
        customerRefundAmount, _ := customer.Balance.Mul(decimal.NewFromFloat32(<span class="hljs-number">0.05</span>)).Float64()
        fmt.Println(<span class="hljs-string">"processing customer"</span>, customer.Name, customerRefundAmount)
        queryArgs := []any{
            customerRefundAmount,
            customer.ID,
        }

        err := eqb.Build(conn.TypeMap(), <span class="hljs-literal">nil</span>, queryArgs) <span class="hljs-comment">// 3</span>
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to buld query: %v"</span>, err)
        }

        <span class="hljs-comment">// 4 below</span>
        pipeline.SendQueryParams(sqlRefundEachCustomer, eqb.ParamValues, <span class="hljs-literal">nil</span>, eqb.ParamFormats, eqb.ResultFormats)

    }
    err := pipeline.Sync() <span class="hljs-comment">// 5</span>
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to sync pipeline query: %v"</span>, err)
    }
    results, err := pipeline.GetResults() <span class="hljs-comment">// 6</span>
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to get pipeline results: %v"</span>, err)
    }
    <span class="hljs-keyword">if</span> results == <span class="hljs-literal">nil</span> &amp;&amp; err == <span class="hljs-literal">nil</span> {
        <span class="hljs-comment">// no results received from server</span>
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    }
    <span class="hljs-comment">// spew.Dump(results)</span>
    _, ok := results.(*pgconn.ResultReader) <span class="hljs-comment">// 7</span>
    <span class="hljs-keyword">if</span> !ok {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to read results from pipelined query: %v"</span>, err)
    }
    err = pipeline.Close() <span class="hljs-comment">// 8</span>
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to close pipeline: %v"</span>, err)
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
</code></pre>
<p>Several things are happening in the code above, let’s focus on the interesting bits:</p>
<ol>
<li><p>Obtain a Pipeline <code>pipeline</code> from the connection by starting the pipeline.</p>
</li>
<li><p>Create an extended query builder that’s used to pass the query parameters</p>
</li>
<li><p>Build the query with a type mapping and the arguments for our SQL query</p>
</li>
<li><p>We send the query into the pipeline using the parameters from the extended query builder from the previous step. Since this is running in pipeline, we can continue sending multiple queries to Postgres without for it to process it (note that we are doing this inside the loop)</p>
</li>
<li><p>We then call sync on the pipeline to flush/execute the submitted queries and make the results available</p>
</li>
<li><p>We call <code>GetResults()</code> to obtain the results of the pipeline query</p>
</li>
<li><p>We check if the pipeline gave us a result we can read from, in this example we ignore the result reader</p>
</li>
<li><p>We close the pipeline and release related resources</p>
</li>
</ol>
<h2 id="heading-on-performance-and-resource-utilization">On performance and resource utilization</h2>
<p>Pipeline mode generally consumes more memory on both the client and server as noted in the <a target="_blank" href="https://www.postgresql.org/docs/current/libpq-pipeline-mode.html">Postgres documentation</a> - this makes intuitive sense since we are buffering the queries and their parameters as we run the pipeline, especially before flushing/syncing. It is recommended to do appropriate management of the send/receive queue and to, of course, monitor the behaviour of your systems to identify bottlenecks or spikes in resource utilization.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article I shared how to use pipelined queries in Postgres from a Go program. Pipelined queries can open up a lot of use cases where you need to do batch operations but the data is coming in stream or whatever other use cases you can think of. As a reminder, pipeline mode generally consumes more memory on both the client and server and it is recommended to do appropriate management of the send/receive queue and monitor the behaviour of your systems to avoid bottlenecks.</p>
<p>Hope you found this interesting.</p>
]]></content:encoded></item><item><title><![CDATA[Using git-monorepo to merge existing Git repositories into a monorepo]]></title><description><![CDATA[In this article, we will look at how to use a tool I built named git-monorepo to merge existing Git repositories into a monorepo while maintaining the complete history from each repository. This can be useful if you want to reduce repository-sprawl a...]]></description><link>https://code.zikani.me/using-git-monorepo-to-merge-existing-git-repositories-into-a-monorepo</link><guid isPermaLink="true">https://code.zikani.me/using-git-monorepo-to-merge-existing-git-repositories-into-a-monorepo</guid><category><![CDATA[GitHub]]></category><category><![CDATA[Git]]></category><category><![CDATA[monorepo]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Backup]]></category><category><![CDATA[ci-cd]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Fri, 14 Nov 2025 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>In this article, we will look at how to use a tool I built named <a target="_blank" href="https://github.com/zikani03/git-monorepo"><code>git-monorepo</code></a> to merge existing Git repositories into a monorepo while maintaining the complete history from each repository. This can be useful if you want to reduce repository-sprawl and manage all your projects in one repository, or if you need to perform offsite backup of your code bases for example.</p>
<h2 id="heading-what-is-a-monorepo">What is a monorepo?</h2>
<p>A monorepo is a workspace that contains multiple projects in one Git repository. According <a target="_blank" href="https://semaphoreci.com/blog/what-is-monorepo">to Semaphore CI</a> <em>"A monorepo is a version-controlled code repository that holds many projects. While these projects may be related, they are often logically independent and run by different teams."</em></p>
<h2 id="heading-why-would-i-want-to-merge-my-repos">Why would I want to merge my repos?</h2>
<p>Adopting monorepos can provide several advantages. Firstly, code sharing and reuse become effortless. Developers on your team can easily find and incorporate existing libraries or functionalities into new projects, reducing redundancy and development time.</p>
<p>Monorepos can also enhance collaboration by fostering a shared codebase as teams gain better visibility into interdependent projects built by other teams, enabling faster identification of potential issues and more streamlined enforcement of coding practices and standards across the entire codebase. This can result in better code quality, easier to apply organization wide standards and practices which may help reducing maintenance overhead in the long run.</p>
<p>Another use case is for performing backups of organization wide codebases. This may be ideal for situations where disaster recovery, compliance or regulatory requirement insist that you keep an offsite backup. Being able to merge the code periodically, and download or upload to some Object Storage is a good use case for merging multiple repositories into a monorepo.</p>
<h2 id="heading-installing-and-using-git-monorepo">Installing and using git-monorepo</h2>
<p>Firstly, you will need to install git-monorepo. More documentation on the tool can be found <a target="_blank" href="https://labs.zikani.me/git-monorepo">here</a></p>
<p>Download the latest release from GitHub: <a target="_blank" href="https://github.com/zikani03/git-monorepo/releases">https://github.com/zikani03/git-monorepo/releases</a></p>
<p>After installation, run the following example command to create a monorepo from two GitHub repositories. This may take a bit of time as the repositories have to be cloned from GitHub to your machine, and then the commit histories replayed one-by-one.</p>
<pre><code class="lang-bash">
$ git-monorepo init \
    --sources gh:zikani03/git-monorepo,gh:zikani03/articulated \
    --target my-new-monorepo
</code></pre>
<p>After the command completes, we can inspect the new repository using Git as usual. You will see that the history is now intertwined as if the two repos had been one from the start!</p>
<pre><code class="lang-bash">$ <span class="hljs-built_in">cd</span> my-new-monorepo
$ git <span class="hljs-built_in">log</span> --oneline
</code></pre>
<p>In the example above you can see we use the <code>gh</code> shorthand for GitHub to indicate that the repositories we are using are going to be pulled from GitHub. You can also specify the full URL to the repository and the tool will continue to work :</p>
<pre><code class="lang-bash">
$ git-monorepo init \
    --sources https://github.com/zikani03/git-monorepo,https://github.com/zikani03/articulated \
    --target my-new-monorepo
</code></pre>
<h2 id="heading-it-also-works-with-other-git-hosting-services">It also works with other Git hosting services</h2>
<p>git-monorepo works well with GitHub but you can also use it clone repositories from any Git server. There are shorthands for GitHub (<code>gh</code>), BitBucket (<code>bb</code>) and GitLab (<code>gl</code>). Small caveat is that we have yet to test thoroughly with private repository servers.</p>
<p>Here is an example of cloning projects from GitLab and Codeberg hosting services:</p>
<pre><code class="lang-bash">$ git-monorepo init \
    --sources https://gitlab.com/writeonlyhugo/up-business-theme,https://codeberg.org/201984/dut \
    --target my-new-monorepo
</code></pre>
<h1 id="heading-conclusion">Conclusion</h1>
<p>In this article I shared a tool I built named git-monorepo which can be used to merge repositories into a monorepo. Comments and contributions are always welcome!</p>
]]></content:encoded></item><item><title><![CDATA[How to use EmbeddingGemma to generate embeddings in Go]]></title><description><![CDATA[Embeddings or Vector Embeddings are the numeric representation of text or other data that enables a lot of Machine learning tasks like classification and clustering. If you are familiar with the concept of embeddings or have some interest in machine ...]]></description><link>https://code.zikani.me/how-to-use-embeddinggemma-to-generate-embeddings-in-go</link><guid isPermaLink="true">https://code.zikani.me/how-to-use-embeddinggemma-to-generate-embeddings-in-go</guid><category><![CDATA[embeddinggemma]]></category><category><![CDATA[AI]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Go Language]]></category><category><![CDATA[#Embeddings]]></category><category><![CDATA[gemma]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Fri, 24 Oct 2025 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Embeddings or <a target="_blank" href="https://www.pinecone.io/learn/vector-embeddings/">Vector Embeddings</a> are the numeric representation of text or other data that enables a lot of Machine learning tasks like classification and clustering. If you are familiar with the concept of embeddings or have some interest in machine learning and topics such as RAG (retrieval augmented generation) you may find it exciting that Google <a target="_blank" href="https://developers.googleblog.com/en/introducing-embeddinggemma/">recently released EmbeddingGemma</a>.</p>
<p>EmbeddingGemma sounds compelling for it’s relatively small size and potential for local on-device applications.</p>
<p>From their article :</p>
<blockquote>
<p>EmbeddingGemma generates embeddings, which are numerical representations - in this case, of text (such as sentences and documents) - by transforming it into a vector of numbers to represent meaning in a high-dimensional space. […] EmbeddingGemma empowers developers to build on-device, flexible, and privacy-centric applications. It generates embeddings of documents directly on the device's hardware, helping ensure sensitive user data is secure.</p>
</blockquote>
<p>In this article, we will do something a bit unconventional, we will use EmbeddingGemma to generate embeddings from a Go program. Typically, it is advisable to work with embeddings from language like Python which has better ecosystem and libraries for such things.</p>
<p><strong>Prerequisites</strong></p>
<ul>
<li><p>Go 1.24+</p>
</li>
<li><p>Ollama v0.11.10+</p>
</li>
</ul>
<p>We will use a similar example to the one in the <a target="_blank" href="https://huggingface.co/blog/embeddinggemma">HuggingFace blog post</a>. In order to follow along with this tutorial you will need to have <a target="_blank" href="https://ollama.com/download">Ollama</a> installed, you will need version atleast <a target="_blank" href="https://github.com/ollama/ollama/releases/tag/v0.11.10"><strong>v0.11.10</strong></a> and pull the <code>embeddinggemma:300m</code> model</p>
<pre><code class="lang-bash">$ ollama --version

$ ollama pull embeddinggemma:300m
</code></pre>
<p>Once this completes successfully, you will have the EmbeddingGemma model on your machine, ready to interact with via Ollama to generate embeddings.</p>
<p>Let’s get to coding, start by creating a new go project</p>
<pre><code class="lang-bash">mkdir embeddings-tutorial
<span class="hljs-built_in">cd</span> embeddings-tutorial
go mod init embeddings-tutorial
touch main.go
</code></pre>
<p>Before we can look at the Go code, let’s discuss what it does.</p>
<p>The code below is a small program that interacts with Ollama via the LangChain library. In the program we have a user query or question and a list of facts (think of those as a database) that we can query against. Both the query and facts are hardcoded in the code as strings but they could come from anywhere, a text file, stdin etc.. Once we have the database and the user query we convert them to Vector Embeddings (remember, embeddings are a numeric representation) using Ollama via the <code>llm.CreateEmbedding</code> function.</p>
<p>Once the embeddings are created, we need a way to use them to perform a search which is basically an operation to compare the vector embedding of the query with the embeddings of the information in our “database” to see which fact/statement most closely matches the query. In order to do this, we use the HNSW data structure from the <code>github.com/habedi/hann</code> package. The package implements a few algorithms for working with high-dimensionality data, but we pick HNSW as it is one of the most commonly used approach for working with embeddings:</p>
<p>“HNSW - addresses the challenge of quickly finding items “close” to a query point in large datasets, which is computationally expensive with traditional methods like brute-force comparisons.” - <a target="_blank" href="https://milvus.io/ai-quick-reference/what-is-hnsw">from Milvus.io</a></p>
<p>Copy the following content into <code>main.go</code></p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
   <span class="hljs-string">"context"</span>
   <span class="hljs-string">"fmt"</span>
   <span class="hljs-string">"log"</span>

   <span class="hljs-string">"github.com/habedi/hann/core"</span>
   <span class="hljs-string">"github.com/habedi/hann/hnsw"</span>
   <span class="hljs-string">"github.com/tmc/langchaingo/llms/ollama"</span>
)

<span class="hljs-keyword">const</span> modelID = <span class="hljs-string">"embeddinggemma:300m"</span>

<span class="hljs-keyword">const</span> query = <span class="hljs-string">"Which planet is known as the Red Planet?"</span>

<span class="hljs-keyword">var</span> documents = []<span class="hljs-keyword">string</span>{
   <span class="hljs-string">"Venus is often called Earth's twin because of its similar size and proximity."</span>,
   <span class="hljs-string">"Mars, known for its reddish appearance, is often referred to as the Red Planet."</span>,
   <span class="hljs-string">"Jupiter, the largest planet in our solar system, has a prominent red spot."</span>,
   <span class="hljs-string">"Saturn, famous for its rings, is sometimes mistaken for the Red Planet."</span>,
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">preprocess</span><span class="hljs-params">(content []<span class="hljs-keyword">string</span>)</span> []<span class="hljs-title">string</span></span> {
   res := <span class="hljs-built_in">make</span>([]<span class="hljs-keyword">string</span>, <span class="hljs-built_in">len</span>(content))
   <span class="hljs-keyword">for</span> idx, item := <span class="hljs-keyword">range</span> content {
       res[idx] = fmt.Sprintf(<span class="hljs-string">"title: none | text: %s"</span>, item)
   }
   <span class="hljs-keyword">return</span> res
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
   ctx := context.Background()
   llm, err := ollama.New(
       ollama.WithModel(modelID),
   )

   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
       log.Fatalf(<span class="hljs-string">"failed to connect to ollama: %v"</span>, err)
   }
   <span class="hljs-comment">// our embeddings are</span>
   embeddings, err := llm.CreateEmbedding(ctx, preprocess(documents))
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
       log.Fatalf(<span class="hljs-string">"failed to generate embedding from ollama: %v"</span>, err)
   }

   fmt.Println(<span class="hljs-string">"generated embeddings successfully, now you gotta use 'em"</span>, <span class="hljs-string">"dimension"</span>, <span class="hljs-built_in">len</span>(embeddings[<span class="hljs-number">0</span>]))

   dimension := <span class="hljs-number">768</span>
   m := <span class="hljs-number">4</span>   <span class="hljs-comment">// maximum number of neighbor connections per node</span>
   ef := <span class="hljs-number">16</span> <span class="hljs-comment">// search breadth factor</span>
   distanceFuncName := <span class="hljs-string">"cosine"</span>

   index := hnsw.NewHNSW(dimension, m, ef, core.Distances[distanceFuncName], distanceFuncName)
   <span class="hljs-keyword">for</span> idx, embedding := <span class="hljs-keyword">range</span> embeddings {
       err = index.Add(idx, embedding)
       <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
           log.Fatalf(<span class="hljs-string">"failed to index embedding in HNSW data structure: %v"</span>, err)
       }
   }

   <span class="hljs-comment">// now that we have built an index, we can query it</span>
   queryTask := fmt.Sprintf(<span class="hljs-string">"task: search result | query: %s"</span>, query)
   queryEmbedding, err := llm.CreateEmbedding(ctx, []<span class="hljs-keyword">string</span>{queryTask})
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
       log.Fatalf(<span class="hljs-string">"failed to generate query embedding from ollama: %v"</span>, err)
   }

   numNeighbors := <span class="hljs-number">3</span>
   neighbors, err := index.Search(queryEmbedding[<span class="hljs-number">0</span>], numNeighbors)
   <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
       log.Fatalf(<span class="hljs-string">"failed to search for query in index: %v"</span>, err)
   }

   fmt.Println(<span class="hljs-string">"Results: "</span>)
   <span class="hljs-keyword">for</span> _, n := <span class="hljs-keyword">range</span> neighbors {
       fmt.Printf(<span class="hljs-string">"Relevance: %f \t Content: %s\n"</span>, <span class="hljs-number">1</span>-n.Distance, documents[n.ID])
   }
}
</code></pre>
<p>Now that we have looked at and understand the code, you can then run the following commands:</p>
<pre><code class="lang-bash">$ go mod tidy

$ go run main.go
</code></pre>
<p>You will get output that looks something like this</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757907227541/97ad944d-1fcf-48d5-8420-536cfee48e3b.png" alt class="image--center mx-auto" /></p>
<p>As you can see, <em>the answer we want about Mars is ranked the highest!</em> (has the highest relevance score)</p>
<p>This result is good as it gives us confidence that the query we searched for matched the most relevant items in our “database” - this technique can be used to implement semantic search in applications as well as recommendations based in unstructured input/text.</p>
<blockquote>
<p><strong>Warning and/or Disclaimer:</strong> Now I should mention that while this works, I wouldn’t advise using this approach for production. You are better off using solutions in the Python ecosystem as they have more robust libraries for this sort of thing.</p>
</blockquote>
<p>I hope you found this somewhat interesting.</p>
]]></content:encoded></item><item><title><![CDATA[Make educated guesses, even if they turn out to be wrong]]></title><description><![CDATA[One observation I have made working with fellow developers, especially those with less experience or just getting started is the lack of confidence to make educated/informed guesses about things. I encourage junior developers I work with to learn how...]]></description><link>https://code.zikani.me/make-educated-guesses-even-if-they-turn-out-to-be-wrong</link><guid isPermaLink="true">https://code.zikani.me/make-educated-guesses-even-if-they-turn-out-to-be-wrong</guid><category><![CDATA[Developer]]></category><category><![CDATA[Thinking]]></category><category><![CDATA[learning]]></category><category><![CDATA[debugging]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Thu, 04 Sep 2025 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>One observation I have made working with fellow developers, especially those with less experience or just getting started is the lack of confidence to make educated/informed guesses about things. I encourage junior developers I work with to learn how to make informed guesses, even if they turn out to be wrong, it is an ability that allows one to break down problems by helping you feel a bit more in control of how you acquire knowledge or understand the world. Trying to understand why something isn’t working can often feel daunting or stressful.</p>
<blockquote>
<p>Trying to understand why something isn’t working can often feel daunting or stressful.</p>
</blockquote>
<p>Code is meant for humans.</p>
<blockquote>
<p>Author’s note: Drafted this article before AI agents were a thing. I’m confident that code is <em>still</em> meant for humans but I cannot say to what degree.</p>
</blockquote>
<p>When people make libraries or build tools, they generally build for other humans to consume (that is, read or maintain) and for machines to <strong>execute</strong>. Understanding this, we can be sure of a few things</p>
<ol>
<li><p>People are going to try to use familiar conventions, idioms, words or domain language</p>
</li>
<li><p>People are going to try to make things easier to use for either themselves or others</p>
</li>
</ol>
<p>What this leads to is that, for a lot of things, if you can make reasonable guesses about what’s expected to happen you can be right or close to it about 50% of the time (I don’t have hard numbers nor have i done a study on this, just <a target="_blank" href="https://www.dictionary.com/browse/anecdata">anecdata</a>). And the other time you aren’t on point - you can refer to docs to help align how you understand the library, tool or the world. You will have to correct your assumptions or in another case it could lead to you contributing to a feature to the tool or project (I am thinking particularly of Open Source here) in other cases it could lead to you creating your own spin on the problem and innovating a new approach.</p>
<h2 id="heading-you-cannot-live-on-assumptions-alone">You cannot live on assumptions alone</h2>
<p>A most important point to make and emphasize here is that you have to verify your assumptions, usually this will come from using the tool or library and observing it’s output, writing tests, reading the documentation or diving into the source code, if that is available. Assumptions are a first step to help you try to meld your mental model of the thing and how you expect it to work for you.</p>
<h2 id="heading-what-happens-when-ai-is-writing-the-code-should-i-still-make-assumptions-well-kinda"><strong>What happens when AI is writing the code? Should I still make assumptions?</strong> Well, kinda.</h2>
<p>I’d be remiss if I didn’t talk about how AI fits in or affects this line of thinking. If most of the code you are consuming was generated by AI, it is possible that it may be a bit more difficult to make assumptions since LLMs are …. LLMs. It means you may need to not only make assumptions but also verify more thoroughly as sometimes AI can make mistakes, as we’ve been told countless times by LLM service providers.</p>
]]></content:encoded></item><item><title><![CDATA[Masking and anonymizing people's names in  text using Go for increased privacy]]></title><description><![CDATA[I am working on a personal CRM/Note taking app and wanted to show my friends how it works to get some feedback and ideas from them, just in case I consider making it more widely accessible tool or maybe even Open Source. As I was thinking about how t...]]></description><link>https://code.zikani.me/masking-and-anonymizing-peoples-names-in-text-using-go</link><guid isPermaLink="true">https://code.zikani.me/masking-and-anonymizing-peoples-names-in-text-using-go</guid><category><![CDATA[Go Language]]></category><category><![CDATA[nlp]]></category><category><![CDATA[faker]]></category><category><![CDATA[privacy]]></category><category><![CDATA[note-taking]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Thu, 04 Sep 2025 00:01:25 GMT</pubDate><content:encoded><![CDATA[<p>I am working on a personal CRM/Note taking app and wanted to show my friends how it works to get some feedback and ideas from them, just in case I consider making it more widely accessible tool or maybe even Open Source. As I was thinking about how to show them, I realized that I didn’t want them to see some of the notes I was taking as they contained information on other people (and possibly about them too ;).</p>
<p>I needed a way to hide or mask the names of people, companies and places in the notes to safely show what I was working on. Since we are dealing with a privacy issue I didn’t want to use a remote AI service provider API and I was a bit too lazy to setup and fiddle with a Local LLM for the task <em>(I have comments about working with Local LLMs as I’ve been experimenting with them for a bit at this point, but that’s for another day).</em></p>
<p>Here is the quick and dirty solution I cobbled up using Go and the prose library which <a target="_blank" href="https://zikani.hashnode.dev/neria-an-aws-lambda-function-for-named-entity-recognition-from-any-website">I have used before</a> for Named Entity Recognition. In the example below we will use an excerpt from a BBC News article which contains several named entities.</p>
<p>Follow these steps to get it running in your environment.</p>
<blockquote>
<p>NB: This is a Go program and needs Go to be installed</p>
</blockquote>
<ol>
<li>Create a new directory and initiate a Go module</li>
</ol>
<pre><code class="lang-bash">mkdir anonymize 
<span class="hljs-built_in">cd</span> anonymize
go mod init anonymize
</code></pre>
<ol start="2">
<li>Create a file named <code>main.go</code> and place the following contents</li>
</ol>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"flag"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"strings"</span>

    <span class="hljs-string">"github.com/go-faker/faker/v4"</span>
    <span class="hljs-string">"github.com/jdkato/prose/v2"</span>
    <span class="hljs-string">"github.com/sergi/go-diff/diffmatchpatch"</span>
)

<span class="hljs-comment">// Source: https://www.bbc.com/news/articles/crr24eqnnq9o</span>
<span class="hljs-keyword">const</span> rawText = <span class="hljs-string">`
Using AI to help write code has increased in popularity as the tech becomes more capable and accessible.

Anthropic says it detected a case of so-called "vibe hacking", where its AI was used to write code which could hack into at least 17 different organisations, including government bodies.

It said the hackers "used AI to what we believe is an unprecedented degree".

They used Claude to "make both tactical and strategic decisions, such as deciding which data to exfiltrate, and how to craft psychologically targeted extortion demands".

It even suggested ransom amounts for the victims. Agentic AI - where the tech operates autonomously - has been touted as the next big step in the space. But these examples show some of the risks powerful tools pose to potential victims of cyber-crime. The use of AI means "the time required to exploit cybersecurity vulnerabilities is shrinking rapidly", said Alina Timofeeva, an adviser on cyber-crime and AI. 
`</span>

<span class="hljs-keyword">var</span> showDiff = <span class="hljs-literal">false</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> {
    flag.BoolVar(&amp;showDiff, <span class="hljs-string">"diff"</span>, <span class="hljs-literal">false</span>, <span class="hljs-string">"Show Diff of the anonymized text"</span>)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    flag.Parse()

    <span class="hljs-keyword">if</span> showDiff {
        dmp := diffmatchpatch.New()
        diffs := dmp.DiffMain(rawText, anonymize(rawText), <span class="hljs-literal">true</span>)
        fmt.Println(dmp.DiffPrettyText(diffs))
    } <span class="hljs-keyword">else</span> {
        fmt.Println(anonymize(rawText))
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">anonymize</span><span class="hljs-params">(value <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">if</span> value == <span class="hljs-string">""</span> {
        <span class="hljs-keyword">return</span> value
    }
    doc, err := prose.NewDocument(value)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>
    }

    entitiesAsPatterns := <span class="hljs-built_in">make</span>([]<span class="hljs-keyword">string</span>, <span class="hljs-number">0</span>)
    <span class="hljs-keyword">for</span> _, ent := <span class="hljs-keyword">range</span> doc.Entities() {
        entitiesAsPatterns = <span class="hljs-built_in">append</span>(entitiesAsPatterns, ent.Text)
        <span class="hljs-keyword">switch</span> ent.Label {
        <span class="hljs-keyword">case</span> <span class="hljs-string">"PERSON"</span>:
            entitiesAsPatterns = <span class="hljs-built_in">append</span>(entitiesAsPatterns, faker.Name())
            <span class="hljs-keyword">break</span>
        <span class="hljs-keyword">case</span> <span class="hljs-string">"LOCATION"</span>:
            entitiesAsPatterns = <span class="hljs-built_in">append</span>(entitiesAsPatterns, faker.MacAddress())
            <span class="hljs-keyword">break</span>
        <span class="hljs-keyword">case</span> <span class="hljs-string">"FACILITY"</span>:
            entitiesAsPatterns = <span class="hljs-built_in">append</span>(entitiesAsPatterns, strings.ToUpper(faker.Name()))
            <span class="hljs-keyword">break</span>
        <span class="hljs-keyword">case</span> <span class="hljs-string">"ORGANIZATION"</span>:
            entitiesAsPatterns = <span class="hljs-built_in">append</span>(entitiesAsPatterns, faker.DomainName())
            <span class="hljs-keyword">break</span>
        <span class="hljs-keyword">default</span>:
            entitiesAsPatterns = <span class="hljs-built_in">append</span>(entitiesAsPatterns, <span class="hljs-string">"[MASKED]"</span>)
        }
    }

    r := strings.NewReplacer(entitiesAsPatterns...)
    <span class="hljs-keyword">return</span> r.Replace(value)
}
</code></pre>
<ol start="3">
<li><p>Run the program, it has two output modes - by default it will just print the text with the names the Named Entity Recognition algorithm finds replaced with fake names, and in the second mode it prints a diff which shows which text was changed.</p>
<p> Run without diff:</p>
<pre><code class="lang-bash"> go run main.go
</code></pre>
<p> Run with DIFF output</p>
<pre><code class="lang-bash"> go run main.go -diff
</code></pre>
</li>
</ol>
<p>The output comes out differently each time so you will most likely get a different result. Here is an example of output from the program.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756943677136/8157c4bd-dfe9-4784-a531-13d87c122263.png" alt class="image--center mx-auto" /></p>
<p>Here is how it looks with DIFF mode:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756943946394/505743e5-bd69-4360-b08f-cc3eefb911f4.png" alt class="image--center mx-auto" /></p>
<p>Feel free to try with your own text and modify the program to maybe anonymize text from a document or a database. Go crazy!</p>
<p><strong>Caveat</strong> The prose library works very well with english but may sometimes miss some entities especially titles or positions, and obviously since the rest of the text is maintained the context of your note could give away information to someone who has more information or context, it’s not a perfect solution!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this short article we have seen how we can write a simple Go program to help maintain privacy in our notes by masking or anonymizing the original names of people or places in unstructured text. We leverage the prose library to detect entities, the faker library to generate fake data and Go’s standard library strings.Replacer for the heavy lifting and add some diffing to ensure that we can tell what was actually replaced.</p>
<p>NB: I never actually got around to showing my friends the app I am working on 😅 I guess I just needed motivation to write and publish this article.</p>
<p>I hope you found this interesting. Thanks for reading.</p>
]]></content:encoded></item><item><title><![CDATA[Storing encrypted data in Postgres with Go]]></title><description><![CDATA[In this article, we will see how to store a Go object (struct) as as encrypted data in a database. We will assume Postgres for this article, but the technique should work with other databases as well. To show the versatility of the technique, we will...]]></description><link>https://code.zikani.me/storing-encrypted-data-in-postgres-with-go</link><guid isPermaLink="true">https://code.zikani.me/storing-encrypted-data-in-postgres-with-go</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[golang]]></category><category><![CDATA[Security]]></category><category><![CDATA[encryption]]></category><category><![CDATA[orm]]></category><category><![CDATA[gorm]]></category><category><![CDATA[pgx]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Sat, 31 May 2025 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>In this article, we will see how to store a Go object (struct) as as encrypted data in a database. We will assume Postgres for this article, but the technique should work with other databases as well. To show the versatility of the technique, we will look at using both <strong>pgx</strong> (for raw SQL) and the <strong>gorm</strong> ORM toolkit. Presently, we will look at how to perform symmetric encryption with the symmecrypt library using the AES-GCM algorithms. In a future article we may look at how we can achieve a similar result using GPG via the go-opengpg library.</p>
<p>If you want to go straight to the code, you can find it in this repository: <a target="_blank" href="https://github.com/zikani03/store-encrypted-json-go-struct-tutorial">https://github.com/zikani03/store-encrypted-json-go-struct-tutorial</a></p>
<h2 id="heading-why-and-when-to-encrypt-data-stored-in-the-database">Why and when to encrypt data stored in the database</h2>
<p>Firstly, why would we want to store data encrypted in a database?</p>
<p>With the increase of cyber threats and threat actors on the internet, any application you develop must have Security as a top priority. Encryption provides a good level of security for sensitive data such as Personally Identifying Information (PII), Finance information (like Credit Card information) and other sensitive pieces of data. If a threat actor gains access to the encrypted data, you want to make it harder for them to decrypt and get the raw data.</p>
<p>So, when should we encrypt data in our applications? Ideally, every time. Practically, no one encrypts everything in the database at the application level. You can use disk level encryption to secure your whole storage layer but that is out of scope of this article.</p>
<h2 id="heading-what-will-we-be-encrypting">What will we be encrypting?</h2>
<p>In this article, we will demonstrate the technique using a fictional service that needs to encrypt Customer settings which may contain API Keys to third-party services, something that is common nowadays with SaaS platforms and AI service wrappers which ask you keys for APIs like OpenAI’s or another platform.</p>
<p>We want to keep data securely, especially in a multi-tenant system where each Customer has their own settings, and we want to ensure that data is encrypted so that even a database administrator or anyone with access to the database cannot read that information without a decryption key.</p>
<p>The key structs we will work with are shown below:</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Setting <span class="hljs-keyword">struct</span> {
    CustomerID   <span class="hljs-keyword">int64</span>        <span class="hljs-string">`json:"customer_id" gorm:"primaryKey"`</span>
    Key          <span class="hljs-keyword">string</span>       <span class="hljs-string">`json:"key" gorm:"key"`</span>
    Data         SettingsData <span class="hljs-string">`json:"data" gorm:"column:data_hash;type:text"`</span>
}

<span class="hljs-keyword">type</span> SettingsData <span class="hljs-keyword">struct</span> {
    Version         <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"version"`</span>
    AllowNewMembers <span class="hljs-keyword">bool</span>   <span class="hljs-string">`json:"allowNewMembers"`</span>
    OpenAIAPIKey    <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"apiKey"`</span>
}
</code></pre>
<p>Our database table for this would look like this. Note that the data is stored as a text column</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> tenant_settings (
  customer_id <span class="hljs-built_in">integer</span> primary <span class="hljs-keyword">key</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  <span class="hljs-keyword">key</span> <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  data_hash <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>
);
</code></pre>
<h2 id="heading-encryption-using-aes-gcm-using-symmecrypt">Encryption using AES-GCM using symmecrypt</h2>
<p>Let’s see how to perform basic encryption of data. We will use <a target="_blank" href="https://github.com/ovh/symmecrypt">ovh/symmecrypt</a> to enable us to encrypt data. Firstly, we need to generate an encryption key, which we have to keep somewhere safe as it will be used for both encryption and decryption.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">generateSecureKey</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
    b := <span class="hljs-built_in">make</span>([]<span class="hljs-keyword">byte</span>, <span class="hljs-number">32</span>)
    <span class="hljs-comment">// Read 32 random bytes from the cryptographically secure random number generator</span>
    _, err := rand.Read(b)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        fmt.Println(<span class="hljs-string">"Error generating random bytes:"</span>, err)
        <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>
    }
    <span class="hljs-comment">// Print the random bytes in hexadecimal format</span>
    <span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">"%x"</span>, b)
}
</code></pre>
<p>After generating and storing our encryption key, we need to pick an algorithm to use for encryption/decryption. For the purposes of this article we chose AES-GCM, although the symmecrypt supports other algorithms as well.</p>
<p>We have to store the key in a <code>keyloader</code>, as shown below:</p>
<pre><code class="lang-go">    secretKey := generateSecureKey()
    kk := keyloader.KeyConfig{
        Identifier: EncryptionKeyCtx,
        Cipher:     <span class="hljs-string">"aes-gcm"</span>,
        Timestamp:  time.Now().UnixMilli(),
        Sealed:     <span class="hljs-literal">false</span>,
        Key:        <span class="hljs-keyword">string</span>(secretKey),
    }

    keyList := []configstore.Item{}
    keyList = <span class="hljs-built_in">append</span>(keyList, configstore.NewItem(EncryptionKeyCtx, kk.String(), <span class="hljs-number">1</span>))
    configstore.RegisterProvider(<span class="hljs-string">"env"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span> <span class="hljs-params">(configstore.ItemList, error)</span></span> {
        <span class="hljs-keyword">return</span> configstore.ItemList{
            Items: keyList,
        }, <span class="hljs-literal">nil</span>
    })
</code></pre>
<p>The following snippet shows how to perform the encryption once the key is loaded into the configuration store. As you can see, the <strong>Encrypt</strong> function expects a slice of bytes and also returns a slice of <em>encrypted</em> bytes. As a result we have to decide on how to encode those bytes in order to print them out to the terminal, in this article we are encoding to Base64.</p>
<pre><code class="lang-go">
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">HowToEncrypt</span><span class="hljs-params">()</span></span> {
    k, err := keyloader.LoadSingleKey()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatalf(<span class="hljs-string">"failed to load encryption key"</span>)
        <span class="hljs-keyword">return</span>
    }

    encrypted, err := k.Encrypt([]<span class="hljs-keyword">byte</span>(<span class="hljs-string">"Hello World, Please encrypt me!"</span>))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatalf(<span class="hljs-string">"failed to encrypt data %v"</span>, err)
        <span class="hljs-keyword">return</span>
    }

    fmt.Println(<span class="hljs-string">"Encrypted, encoded as Base64"</span>, base64.StdEncoding.EncodeToString(encrypted))
}
</code></pre>
<h2 id="heading-storing-and-reading-the-encrypted-data-via-pgx-or-databasesql">Storing and reading the encrypted data via pgx or <code>database/sql</code></h2>
<p>In order to store our encrypted data to the database via pgx we need to implement the <a target="_blank" href="https://pkg.go.dev/database/sql/driver@go1.24.1#Valuer"><strong>Valuer</strong></a> interface. This interface must be implemented for the structs that we wish to store in encrypted manner. The extra benefit is that this enables this technique to work with other databases using the standard <code>database/sql</code> module.</p>
<p><strong>Implementing the Value() function</strong></p>
<p>This implementation of the <code>Value()</code> function does several key things to make things work:</p>
<ol>
<li><p>Marshals the value of the struct itself to JSON data</p>
</li>
<li><p>Loads the encryption key from the key loader</p>
</li>
<li><p>Encrypts the JSON data using the loaded key</p>
</li>
<li><p>Encodes and returns the encrypted data as a Base64 string</p>
</li>
</ol>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(settings *SettingsData)</span> <span class="hljs-title">Value</span><span class="hljs-params">()</span> <span class="hljs-params">(driver.Value, error)</span></span> {
    data, err := json.Marshal(settings)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"failed to marshal data %v"</span>, err)
    }

    k, err := keyloader.LoadSingleKey()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"failed to marshal data %v"</span>, err)
    }

    encrypted, err := k.Encrypt(data)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"failed to marshal data %v"</span>, err)
    }

    <span class="hljs-keyword">return</span> base64.StdEncoding.EncodeToString(encrypted), <span class="hljs-literal">nil</span>
}
</code></pre>
<p>As a result of these steps, the database driver will store the value as a Base64 string in the database. That string may be decoded from base64 but because it is encrypted the output will look like garbage. In order to decrypt the data, one must use the same key and algorithm that was used to encrypt it.</p>
<p><strong>Implementing the Scan() function</strong></p>
<p>In order to read the data back and decrypt it we simply have to implement the database Scanner interface. This function does a few things:</p>
<ol>
<li><p>It tries to read/cast the value to a string</p>
</li>
<li><p>It tries to decode the value from Base64 into a slice of bytes</p>
</li>
<li><p>Loads a key from the key loader</p>
</li>
<li><p>Uses the key to decrypt the data, which is expected to be stored as JSON</p>
</li>
<li><p>Unmarshals the JSON into the struct</p>
</li>
</ol>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(settings *SettingsData)</span> <span class="hljs-title">Scan</span><span class="hljs-params">(value <span class="hljs-keyword">interface</span>{})</span> <span class="hljs-title">error</span></span> {
    stored, ok := value.(<span class="hljs-keyword">string</span>)
    <span class="hljs-keyword">if</span> !ok {
        <span class="hljs-keyword">return</span> errors.New(fmt.Sprint(<span class="hljs-string">"Failed to unmarshal JSONB value:"</span>, value))
    }

    data, err := base64.StdEncoding.DecodeString(stored)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to decode base64 %v"</span>, err)
    }

    k, err := keyloader.LoadSingleKey()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to load key %v"</span>, err)
    }
    decryptedData, err := k.Decrypt(data)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"failed to decrypt %v"</span>, err)
    }
    result := SettingsData{}
    err = json.Unmarshal(decryptedData, &amp;result)
    *settings = result
    <span class="hljs-keyword">return</span> err
}
</code></pre>
<h2 id="heading-storing-the-encrypted-data-via-gorm">Storing the encrypted data via Gorm</h2>
<p>Similar to how we store the data when using pgx or regular <code>database/sql</code> - using Gorm also requires implementation of a specific interface, GormValuer, that tells the Gorm library how to handle this data when storing in the database.</p>
<p><strong>Implementing the GormValue() function</strong></p>
<p>Below is an implementation of the <code>GormValue</code> function for our SettingsData type:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s SettingsData)</span> <span class="hljs-title">GormValue</span><span class="hljs-params">(ctx context.Context, db *gorm.DB)</span> <span class="hljs-params">(expr clause.Expr)</span></span> {
    data, err := json.Marshal(s)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        db.AddError(fmt.Errorf(<span class="hljs-string">"failed to marshal data %v"</span>, err))
    }

    k, err := keyloader.LoadSingleKey()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        db.AddError(err)
        <span class="hljs-keyword">return</span>
    }

    encrypted, err := k.Encrypt(data)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        db.AddError(fmt.Errorf(<span class="hljs-string">"failed to encrypt data %v"</span>, err))
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">return</span> clause.Expr{SQL: <span class="hljs-string">"?"</span>, Vars: []<span class="hljs-keyword">interface</span>{}{base64.StdEncoding.EncodeToString(encrypted)}}
}
</code></pre>
<h2 id="heading-where-to-get-and-store-the-encryption-key">Where to get (and store) the Encryption Key</h2>
<p>This part is left as an exercise for the reader. It’s up to you to decide</p>
<ul>
<li>Whether you will store and retrieve the encryption key from Configuration file, environment variables or from a third-party Key Management service like HashiCorp Vault, AWS KMS or Keycloak</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, we explored how to securely store Go structs as encrypted JSON in a PostgreSQL database using the AES-GCM algorithm via the <code>symmecrypt</code> library. We demonstrated how to implement encryption and decryption logic compatible with both <code>pgx</code> and the Gorm ORM by leveraging Go's <code>Valuer</code> and <code>Scanner</code> interfaces. This approach ensures sensitive fields like API keys remain protected—even from database administrators—by encrypting data at the application level.</p>
<p>By integrating encryption into your data models, you add a critical layer of security that complements other infrastructure-level protections. While this method requires some setup, it offers strong guarantees for data confidentiality—especially in multi-tenant systems or applications handling sensitive customer information.</p>
<p>For a complete implementation, check out the sample code in the linked repository. And remember: secure key storage is just as important as encryption itself—be deliberate about how and where your keys are managed.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Go microservice for calculating cash-out or admin fees]]></title><description><![CDATA[In this article we will see how to implement a micro-service to calculate administrative fees. We will use the concept of cash out fees for a fictional mobile money service operator, using Go armed with only the standard library for the HTTP server a...]]></description><link>https://code.zikani.me/building-a-go-microservice-for-calculating-cash-out-or-admin-fees</link><guid isPermaLink="true">https://code.zikani.me/building-a-go-microservice-for-calculating-cash-out-or-admin-fees</guid><category><![CDATA[hurl]]></category><category><![CDATA[golang]]></category><category><![CDATA[Microservices]]></category><category><![CDATA[Docker]]></category><category><![CDATA[#venom]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Fri, 30 May 2025 13:46:33 GMT</pubDate><content:encoded><![CDATA[<p>In this article we will see how to implement a micro-service to calculate administrative fees. We will use the concept of cash out fees for a fictional mobile money service operator, using Go armed with only the standard library for the HTTP server and a utility library for calculating the fees. We will leverage <strong>hurl</strong> and <strong><em>venom</em></strong> for API testing.</p>
<h2 id="heading-pre-requisites">Pre-requisites</h2>
<p>You will need to have the following installed to follow along:</p>
<ul>
<li><p><a target="_blank" href="https://go.dev/dl">Go 1.23+</a></p>
</li>
<li><p><a target="_blank" href="https://hurl.dev">Hurl</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/ovh/venom">Venom</a></p>
</li>
<li><p>Text Editor or IDE (recommend either VSCode or GoLand)</p>
</li>
</ul>
<h2 id="heading-what-are-cash-out-admin-fees"><strong>What are cash out / admin fees?</strong></h2>
<p>Imagine you are offering a service online and want to get commission off of every transaction, such commissions are usually pushed to customers as administrative fees. Typically these fees are calculated relative to the amount the customer/user pays.</p>
<blockquote>
<p>To make this more concrete we will use the concept of mobile money cash-out fee wherein a Customer desires to withdraw an amount from their mobile money wallet and receive cash. Mobile networks typically push the fees onto the customer while splitting that with the agent who fulfils that cash out.</p>
</blockquote>
<p>We will use real cash out values from this article: <a target="_blank" href="https://temovision.com/tnm-mpamba-withdraw-send-and-bank-charges-malawi/">https://temovision.com/tnm-mpamba-withdraw-send-and-bank-charges-malawi/</a></p>
<p>After reading this article you should be able to</p>
<ul>
<li><p>Build a fairly simple micro-service with Go using standard net/http Server</p>
</li>
<li><p>Use the <a target="_blank" href="https://github.com/golang-malawi/chigoli">golang-malawi/chigoli</a> particularly the servicefee module for calculating service fees</p>
</li>
<li><p>Test API end-points using Hurl and Venom</p>
</li>
</ul>
<h2 id="heading-step-1-project-setup-and-calculating-fees-from-the-command-line">Step 1: Project setup and calculating fees from the command-line</h2>
<p>Create a new Go module</p>
<pre><code class="lang-plaintext">go mod init example.com/fees-api
</code></pre>
<p>Add the servicefee dependency from Golang Malawi</p>
<pre><code class="lang-plaintext">go get github.com/golang-malawi/chigoli/servicefee
</code></pre>
<p>First let’s create the logic for calculating fees based on the cash out / withdrawal fee bands.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>

    <span class="hljs-string">"github.com/golang-malawi/chigoli/servicefee"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    serviceFees := servicefee.NewFixedFee(servicefee.FeeExpressions{
        <span class="hljs-string">"x &gt;= 100 &amp;&amp;  x &lt;= 500"</span>:        <span class="hljs-number">20.0</span>,
        <span class="hljs-string">"x &gt;= 501 &amp;&amp;  x &lt;= 1000"</span>:       <span class="hljs-number">40.0</span>,
        <span class="hljs-string">"x &gt;= 1001 &amp;&amp;  x &lt;= 2000"</span>:      <span class="hljs-number">85.0</span>,
        <span class="hljs-string">"x &gt;= 2001 &amp;&amp;  x &lt;= 3000"</span>:      <span class="hljs-number">150.0</span>,
        <span class="hljs-string">"x &gt;= 3001 &amp;&amp;  x &lt;= 5000"</span>:      <span class="hljs-number">200.0</span>,
        <span class="hljs-string">"x &gt;= 5001 &amp;&amp;  x &lt;= 10000"</span>:     <span class="hljs-number">380.0</span>,
        <span class="hljs-string">"x &gt;= 10001 &amp;&amp;  x &lt;= 20000"</span>:    <span class="hljs-number">750.0</span>,
        <span class="hljs-string">"x &gt;= 20001 &amp;&amp;  x &lt;= 40000"</span>:    <span class="hljs-number">1700.0</span>,
        <span class="hljs-string">"x &gt;= 40001 &amp;&amp;  x &lt;= 60000"</span>:    <span class="hljs-number">2500.0</span>,
        <span class="hljs-string">"x &gt;= 60001 &amp;&amp;  x &lt;= 100000"</span>:   <span class="hljs-number">3750.0</span>,
        <span class="hljs-string">"x &gt;= 100001 &amp;&amp;  x &lt;= 200000"</span>:  <span class="hljs-number">6500.0</span>,
        <span class="hljs-string">"x &gt;= 200001 &amp;&amp;  x &lt;= 300000"</span>:  <span class="hljs-number">9000.0</span>,
        <span class="hljs-string">"x &gt;= 300001 &amp;&amp;  x &lt;= 400000"</span>:  <span class="hljs-number">11000.0</span>,
        <span class="hljs-string">"x &gt;= 400001 &amp;&amp;  x &lt;= 500000"</span>:  <span class="hljs-number">12500.0</span>,
        <span class="hljs-string">"x &gt;= 500001 &amp;&amp;  x &lt;= 600000"</span>:  <span class="hljs-number">13500.0</span>,
        <span class="hljs-string">"x &gt;= 600001 &amp;&amp;  x &lt;= 750000"</span>:  <span class="hljs-number">14500.0</span>,
        <span class="hljs-string">"x &gt;= 750001 &amp;&amp;  x &lt;= 1500000"</span>: <span class="hljs-number">14500.0</span>,
    })

    total, fee, err := serviceFees.CalculateTotalAndFee(<span class="hljs-number">6000</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatal(err)
    }
    fmt.Println(<span class="hljs-string">"Total: "</span>, total, <span class="hljs-string">"Fee: "</span>, fee)
}
</code></pre>
<p>Let us test this simple program with <code>go run main.go</code>.You should get output similar to the the following:</p>
<pre><code class="lang-plaintext">Total: 6380 Fee: 380
</code></pre>
<p>If that worked, we can now go to the next step of implementing our API endpoint.</p>
<h2 id="heading-step-2-implementing-the-api-server">Step 2: Implementing the API Server</h2>
<p>Create a main.go file with the following content</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"encoding/json"</span>
    <span class="hljs-string">"errors"</span>
    <span class="hljs-string">"io"</span>
    <span class="hljs-string">"log"</span>
    <span class="hljs-string">"log/slog"</span>
    <span class="hljs-string">"net/http"</span>

    <span class="hljs-string">"github.com/golang-malawi/chigoli/servicefee"</span>
)

<span class="hljs-keyword">var</span> ServiceFees = servicefee.NewFixedFee(servicefee.FeeExpressions{
    <span class="hljs-string">"x &gt;= 100 &amp;&amp;  x &lt;= 500"</span>:        <span class="hljs-number">20.0</span>,
    <span class="hljs-string">"x &gt;= 501 &amp;&amp;  x &lt;= 1000"</span>:       <span class="hljs-number">40.0</span>,
    <span class="hljs-string">"x &gt;= 1001 &amp;&amp;  x &lt;= 2000"</span>:      <span class="hljs-number">85.0</span>,
    <span class="hljs-string">"x &gt;= 2001 &amp;&amp;  x &lt;= 3000"</span>:      <span class="hljs-number">150.0</span>,
    <span class="hljs-string">"x &gt;= 3001 &amp;&amp;  x &lt;= 5000"</span>:      <span class="hljs-number">200.0</span>,
    <span class="hljs-string">"x &gt;= 5001 &amp;&amp;  x &lt;= 10000"</span>:     <span class="hljs-number">380.0</span>,
    <span class="hljs-string">"x &gt;= 10001 &amp;&amp;  x &lt;= 20000"</span>:    <span class="hljs-number">750.0</span>,
    <span class="hljs-string">"x &gt;= 20001 &amp;&amp;  x &lt;= 40000"</span>:    <span class="hljs-number">1700.0</span>,
    <span class="hljs-string">"x &gt;= 40001 &amp;&amp;  x &lt;= 60000"</span>:    <span class="hljs-number">2500.0</span>,
    <span class="hljs-string">"x &gt;= 60001 &amp;&amp;  x &lt;= 100000"</span>:   <span class="hljs-number">3750.0</span>,
    <span class="hljs-string">"x &gt;= 100001 &amp;&amp;  x &lt;= 200000"</span>:  <span class="hljs-number">6500.0</span>,
    <span class="hljs-string">"x &gt;= 200001 &amp;&amp;  x &lt;= 300000"</span>:  <span class="hljs-number">9000.0</span>,
    <span class="hljs-string">"x &gt;= 300001 &amp;&amp;  x &lt;= 400000"</span>:  <span class="hljs-number">11000.0</span>,
    <span class="hljs-string">"x &gt;= 400001 &amp;&amp;  x &lt;= 500000"</span>:  <span class="hljs-number">12500.0</span>,
    <span class="hljs-string">"x &gt;= 500001 &amp;&amp;  x &lt;= 600000"</span>:  <span class="hljs-number">13500.0</span>,
    <span class="hljs-string">"x &gt;= 600001 &amp;&amp;  x &lt;= 750000"</span>:  <span class="hljs-number">14500.0</span>,
    <span class="hljs-string">"x &gt;= 750001 &amp;&amp;  x &lt;= 1500000"</span>: <span class="hljs-number">14500.0</span>,
})

<span class="hljs-keyword">type</span> CalculateFeeRequest <span class="hljs-keyword">struct</span> {
    Amount <span class="hljs-keyword">float64</span> <span class="hljs-string">`json:"Amount"`</span>
}

<span class="hljs-keyword">type</span> FeeResponse <span class="hljs-keyword">struct</span> {
    Amount <span class="hljs-keyword">float64</span> <span class="hljs-string">`json:"Amount"`</span>
    Fee    <span class="hljs-keyword">float64</span> <span class="hljs-string">`json:"Fee"`</span>
    Total  <span class="hljs-keyword">float64</span> <span class="hljs-string">`json:"Total"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">CalculateFeesHandler</span><span class="hljs-params">(serviceFees servicefee.Fees)</span> <span class="hljs-title">func</span><span class="hljs-params">(w http.ResponseWriter, r *http.Request)</span></span> {
    <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(w http.ResponseWriter, r *http.Request)</span></span> {
        request := CalculateFeeRequest{}
        data, err := io.ReadAll(r.Body)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            slog.Error(<span class="hljs-string">"failed to parse request body"</span>, <span class="hljs-string">"error"</span>, err)
            w.WriteHeader(http.StatusBadRequest)
            w.Write([]<span class="hljs-keyword">byte</span>(<span class="hljs-string">"failed to parse request"</span>))
            <span class="hljs-keyword">return</span>
        }

        err = json.Unmarshal(data, &amp;request)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            slog.Error(<span class="hljs-string">"json.Unmarshal: failed to parse request body"</span>, <span class="hljs-string">"error"</span>, err)
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]<span class="hljs-keyword">byte</span>(<span class="hljs-string">"failed to parse request"</span>))
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-keyword">defer</span> r.Body.Close()

        slog.Info(<span class="hljs-string">"processing request for amount"</span>, <span class="hljs-string">"amount"</span>, request.Amount)
        total, fee, err := serviceFees.CalculateTotalAndFee(request.Amount)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            slog.Error(<span class="hljs-string">"failed to process request"</span>, <span class="hljs-string">"error"</span>, err)
            <span class="hljs-keyword">if</span> errors.Is(err, servicefee.ErrNoApplicableFee) {
                data, err := json.Marshal(FeeResponse{
                    Amount: request.Amount,
                    Total:  request.Amount,
                    Fee:    <span class="hljs-number">0</span>,
                })
                <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                    w.WriteHeader(http.StatusInternalServerError)
                    <span class="hljs-keyword">return</span>
                }
                w.WriteHeader(http.StatusOK)
                w.Write(data)
                <span class="hljs-keyword">return</span>
            }
            w.WriteHeader(http.StatusInternalServerError)
            <span class="hljs-keyword">return</span>
        }

        responseData, err := json.Marshal(FeeResponse{
            Amount: request.Amount,
            Total:  total,
            Fee:    fee,
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            slog.Error(<span class="hljs-string">"failed to process request got error"</span>, <span class="hljs-string">"error"</span>, err)
            w.WriteHeader(http.StatusInternalServerError)
            <span class="hljs-keyword">return</span>
        }
        w.Header().Add(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"application/json"</span>)
        w.WriteHeader(http.StatusOK)
        w.Write(responseData)
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    addr := <span class="hljs-string">":4001"</span>

    http.HandleFunc(<span class="hljs-string">"/calculate-fees"</span>, CalculateFeesHandler(ServiceFees))

    slog.Info(<span class="hljs-string">"Starting server on port"</span>, <span class="hljs-string">"address"</span>, addr)
    err := http.ListenAndServe(addr, <span class="hljs-literal">nil</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatalf(<span class="hljs-string">"failed to run server got %v"</span>, err)
    }
}
</code></pre>
<p>We can run this with</p>
<pre><code class="lang-bash">$ go run main.go
</code></pre>
<p>Then in another terminal we can use cURL to verify it works</p>
<pre><code class="lang-bash">$ curl -X POST http://localhost:4001/calculate-fees -d <span class="hljs-string">'{"Amount":4000}'</span>
<span class="hljs-comment"># Output should be something like</span>
{<span class="hljs-string">"Amount"</span>:4000,<span class="hljs-string">"Fee"</span>:200,<span class="hljs-string">"Total"</span>:4200}
</code></pre>
<h2 id="heading-step-3-adding-tests-for-our-endpoint">Step 3: Adding Tests for our Endpoint</h2>
<p>In this section we will implement some tests to ensure that our code works as we expect/anticipate and to catch any bugs or regressions. Tests help developers to validate their code and catch bugs, I highly recommend getting used to writing tests even if you aren’t already using a methodology like <a target="_blank" href="https://www.browserstack.com/guide/what-is-test-driven-development">TDD (Test Driven Development)</a></p>
<p>Create a file named <code>main_test.go</code> and add the following content</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"bytes"</span>
    <span class="hljs-string">"encoding/json"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"io"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"net/http/httptest"</span>
    <span class="hljs-string">"testing"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestServer</span><span class="hljs-params">(t *testing.T)</span></span> {
    mu := http.NewServeMux()
    mu.HandleFunc(<span class="hljs-string">"/calculate-fees"</span>, CalculateFeesHandler(ServiceFees))
    testServer := httptest.NewServer(mu)

    testCases := []<span class="hljs-keyword">struct</span> {
        Amount        <span class="hljs-keyword">float64</span>
        CalculatedFee <span class="hljs-keyword">float64</span>
    }{
        {Amount: <span class="hljs-number">3000</span>, CalculatedFee: <span class="hljs-number">150</span>},
        {Amount: <span class="hljs-number">4000</span>, CalculatedFee: <span class="hljs-number">200</span>},
        {Amount: <span class="hljs-number">6000</span>, CalculatedFee: <span class="hljs-number">380</span>},
        {Amount: <span class="hljs-number">20</span>_000, CalculatedFee: <span class="hljs-number">750</span>},
    }

    <span class="hljs-keyword">for</span> _, testCase := <span class="hljs-keyword">range</span> testCases {
        <span class="hljs-comment">// Create the request data</span>
        body := []<span class="hljs-keyword">byte</span>(fmt.Sprintf(<span class="hljs-string">`{"Amount": %f}`</span>, testCase.Amount))

        res, err := http.Post(testServer.URL+<span class="hljs-string">"/calculate-fees"</span>, <span class="hljs-string">"application/json"</span>, bytes.NewBuffer(body))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            t.Errorf(<span class="hljs-string">"failed to test server: %v"</span>, err)
        }
        <span class="hljs-keyword">defer</span> res.Body.Close()

        data, err := io.ReadAll(res.Body)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            t.Errorf(<span class="hljs-string">"failed to test server: %v"</span>, err)
        }
        responseStruct := FeeResponse{}
        err = json.Unmarshal(data, &amp;responseStruct)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            t.Errorf(<span class="hljs-string">"failed to test server: %v"</span>, err)
        }

        <span class="hljs-keyword">if</span> responseStruct.Fee != testCase.CalculatedFee {
            t.Errorf(<span class="hljs-string">"incorrect fee calculated. \n\twant: %f\n\thave: %f"</span>, testCase.CalculatedFee, responseStruct.Fee)
        }
    }

    t.Cleanup(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        testServer.Close()
    })
}
</code></pre>
<p>Run the tests using the following command</p>
<pre><code class="lang-bash">$ go <span class="hljs-built_in">test</span> ./...
</code></pre>
<h2 id="heading-step-4-functional-testing-the-api-using-hurl-and-venom">Step 4: Functional Testing the API using Hurl and venom</h2>
<p>Create a new file named <code>test_basic.hurl</code> in the directory with the following content:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># File: test_basic.hurl</span>
POST http://localhost:4001/calculate-fees 
Content-Type: application/json
{
  <span class="hljs-string">"Amount"</span>: 6000
}

HTTP 200
[Asserts]
jsonpath <span class="hljs-string">"$.Fee"</span> == 380
</code></pre>
<p>This file defines a Hurl file that instructs hurl to send a POST request to the <code>/calculate-fee</code> endpoint and checks two things 1) checks that the HTTP response status code is 200 OK and 2) Uses JSON Assertions to verify that the fee was calculated correctly.</p>
<p>Use the following command to run hurl to process the file and perform the tests.</p>
<pre><code class="lang-bash">$ hurl test_basic.hurl
</code></pre>
<p>As you can see the above file only allows us to test a hardcoded amount and fee, a better approach would be to use variables so that we can inject different amounts and fees.</p>
<p>See the example below</p>
<pre><code class="lang-bash"><span class="hljs-comment"># File: test_variables.hurl</span>
POST http://localhost:4001/calculate-fees 
Content-Type: application/json
{
  <span class="hljs-string">"Amount"</span>: {{amount}}
}

HTTP 200
[Asserts]
jsonpath <span class="hljs-string">"$.Fee"</span> == {{fee}}
</code></pre>
<p>We can now test the service with variables passed via the command-line. This allows greater flexibility as we can test more cases without cluttering the hurl file with the same configuration for each test case.</p>
<pre><code class="lang-bash">$ hurl --variable amount=4000 --variable fee=200 test_variables.hurl
$ hurl --variable amount=7000 --variable fee=380 test_variables.hurl
</code></pre>
<p><strong>Testing using ovh/venom</strong></p>
<p>Venom is an integration testing tool. It has support for various protocols and uses YAML for defining and structuring tests. Although for our basic needs, hurl is sufficient for the tests - we will look at how we can leverage venom with its support for HTTP as well support for running binaries like <code>hurl</code> for performing tests.</p>
<p>Create a new file named <code>test_venom.yaml</code> and paste into it the configuration below:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Service</span> <span class="hljs-string">fee</span> <span class="hljs-string">microservice</span> <span class="hljs-string">tests</span> 
<span class="hljs-attr">description:</span> <span class="hljs-string">Tests</span> <span class="hljs-string">servicefee</span> <span class="hljs-string">microservice</span>
<span class="hljs-attr">testcases:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Test</span> <span class="hljs-string">that</span> <span class="hljs-string">we</span> <span class="hljs-string">can</span> <span class="hljs-string">shell</span> <span class="hljs-string">out</span> <span class="hljs-string">to</span> <span class="hljs-string">hurl</span>
    <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">exec</span>
      <span class="hljs-attr">command:</span> [ <span class="hljs-string">hurl</span>,  <span class="hljs-string">test_basic.hurl</span> ]
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Test</span> <span class="hljs-string">that</span> <span class="hljs-string">fee</span> <span class="hljs-string">is</span> <span class="hljs-string">calculated</span>
    <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">http</span>
      <span class="hljs-attr">method:</span> <span class="hljs-string">POST</span>
      <span class="hljs-attr">url:</span> <span class="hljs-string">http://localhost:4001/calculate-fees</span>
      <span class="hljs-attr">body:</span> <span class="hljs-string">'{"Amount": 6000}'</span>
      <span class="hljs-attr">assertions:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">result.body</span> <span class="hljs-string">ShouldContainSubstring</span> <span class="hljs-string">Fee</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">result.bodyjson</span> <span class="hljs-string">ShouldContainKey</span> <span class="hljs-string">Fee</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">result.bodyjson.Fee</span> <span class="hljs-string">ShouldEqual</span> <span class="hljs-number">380</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">result.statuscode</span> <span class="hljs-string">ShouldEqual</span> <span class="hljs-number">200</span>
</code></pre>
<p>With venom installed, use the following command to run tests:</p>
<pre><code class="lang-bash">$ venom run test_venom.yaml
</code></pre>
<p>The output will be something like</p>
<pre><code class="lang-bash">          [trac] writing venom.19.log
 • Service fee microservice tests (test_venom.yaml)
        • Test-that-we-can-shell-out-to-hurl PASS
        • Test-that-fee-is-calculated PASS
final status: PASS
</code></pre>
<h2 id="heading-step-5-building-a-docker-image-for-our-service">Step 5: Building a Docker Image for our service</h2>
<p>Now that we have build and tested the API, we will build a Docker image that can be used to run containers either through Docker locally, or a container orchestration platform like Kubernetes.</p>
<p>To create a Docker image, add a <code>Dockerfile</code> in the directory. Place the following into the <code>Dockerfile</code></p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> golang:<span class="hljs-number">1.23</span>-alpine as go-builder
<span class="hljs-keyword">RUN</span><span class="bash"> apk add --no-cache git</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /go/</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> go generate -x -v</span>
<span class="hljs-keyword">RUN</span><span class="bash"> go build -o /bin/servicefee-microservice </span>
<span class="hljs-keyword">RUN</span><span class="bash"> chmod +x /bin/servicefee-microservice </span>

<span class="hljs-keyword">FROM</span> alpine
<span class="hljs-keyword">ENV</span> PORT <span class="hljs-number">4001</span>
<span class="hljs-keyword">ENV</span> ADDRESS <span class="hljs-string">"0.0.0.0"</span>
<span class="hljs-keyword">ENV</span> CACHE_DURATION <span class="hljs-string">"3m"</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=go-builder /bin/servicefee-microservice  /servicefee-microservice </span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"/servicefee-microservice"</span>]</span>
</code></pre>
<p>The above Dockerfile defines a multi-stage Docker build that uses the Alpine linux Operating System as a base image for building the service. The base image has Go installed so we can build the code in that environment the same way as on our local machine/laptop. After building the image, we move onto the next stage (starting at <code>FROM alpine</code>) in which we define the contents of the actual Docker image</p>
<p>With the above Dockerfile, run the following command to build a container image locally</p>
<pre><code class="lang-plaintext">$ docker build -t tutorial/servicefee .
</code></pre>
<p>Once this completes successfully, you should have a Docker image you can run as a container</p>
<pre><code class="lang-plaintext">$ docker run -p "4001:4001" -e "PORT=8080" tutorial/servicefee
</code></pre>
<h2 id="heading-next-steps">Next Steps</h2>
<ul>
<li><p><strong>Loading Service fee configurations from a File or Remote Service</strong></p>
<p>  The service fees are currently hardcoded in the code, ideally the service would load the definition of the fees from a separate file or a <a target="_blank" href="https://aws.amazon.com/kms/">remote configuration service or KMS</a> that can be updated without needing to recompile the code. This will make the service flexible to changing requirements should the fees bands ever need to change.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article we have gone through the process of implementing a basic microservice for calculating service fees. Although we used the use-case of cashout fees, the concepts in this article can be applied to different scenarios where you want to calculate a numerical value based on some bands or ranges of values. We have also seen how to use hurl and venom, two useful tools that a modern Software Engineer must have in their toolkit.</p>
<p>I hope you found this somewhat useful.</p>
<p>Please reach out for comments or if you spot some errors or places that need correction. zikani03 &lt;at&gt; gmail [.] com</p>
]]></content:encoded></item><item><title><![CDATA[Should African DevOps engineers treat servers as Cattle or Pets?]]></title><description><![CDATA[When I first heard the expression, “Don’t treat servers as Pets, treat them more like Cattle” it immediately made sense to me, based on what I knew about the value of Cattle and Pets, then I heard the real explanation that made me pause … and chuckle...]]></description><link>https://code.zikani.me/should-african-devops-engineers-treat-servers-as-cattle-or-pets</link><guid isPermaLink="true">https://code.zikani.me/should-african-devops-engineers-treat-servers-as-cattle-or-pets</guid><category><![CDATA[Devops]]></category><category><![CDATA[Africa]]></category><category><![CDATA[cattle]]></category><category><![CDATA[pets]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Sat, 08 Mar 2025 09:10:17 GMT</pubDate><content:encoded><![CDATA[<p>When I first heard the expression, <em>“Don’t treat servers as Pets, treat them more like Cattle”</em> it immediately made sense to me, based on what I knew about the value of Cattle and Pets, then I heard the real explanation that made me pause … and chuckle.</p>
<p>The simple (maybe naive) interpretation of that phrase is that cattle are more expendable than pets.</p>
<p>See, where I come from cattle are more dear to people than pets. Actually, caring for pets too much is modern luxury. Most people keep dogs for security, not for company. Cats are for dealing with rodents. You own cattle, now that makes you wealthy! The more you own, the richer you are. That doesn’t mean they aren’t expendable, no, but someone who owns cattle will go out of their way to make sure they are bred and fed to grow. They send their sons to herd them at the expense of the child’s education or play time. They’d go on a bike or other form of transport to find feed or pay someone to find feed if the nearby pastures don’t have enough supply for the cattle to feed on.</p>
<p>Thinking of servers as cattle is basically saying that this is a very valuable resource that you must keep alive at all costs.</p>
<p>Contrast that with the western interpretation; that servers can be slaughtered and respawned without too much fan fare or worry. I found it amusing, I still do.</p>
<p>It makes me wonder what an African inspired server orchestration platform would look like.</p>
<p>Some more interesting reads on the topic:</p>
<ul>
<li><p><a target="_blank" href="https://www.engineyard.com/blog/pets-vs-cattle/">https://www.engineyard.com/blog/pets-vs-cattle/</a></p>
</li>
<li><p><a target="_blank" href="https://www.kubermatic.com/blog/cloud-native-best-practices-2-why-cattle-not-pets/#:~:text=Conclusion,be%20treated%20like%20Cattle%20too">https://www.kubermatic.com/blog/cloud-native-best-practices-2-why-cattle-not-pets/</a></p>
</li>
<li><p><a target="_blank" href="https://dev.to/aws-heroes/time-to-rethink-cattle-vs-pets-serverless-5c0j">https://dev.to/aws-heroes/time-to-rethink-cattle-vs-pets-serverless-5c0j</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Tingoyenera kukhaladi mavenda]]></title><description><![CDATA[This article is written in Chichewa. I wanted to try something different. Here goes nothing
Mawu oti venda nthawi zambiri amatikumbutsa anyamata ndi amayi a mtawuni kapena kumsika omwe amagulitsa zinthu zosiyanasiyana monga nsapato, katundu wa magets...]]></description><link>https://code.zikani.me/ti</link><guid isPermaLink="true">https://code.zikani.me/ti</guid><category><![CDATA[vendor]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Thu, 13 Jun 2024 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>This article is written in Chichewa. I wanted to try something different. Here goes nothing</p>
<p>Mawu oti venda nthawi zambiri amatikumbutsa anyamata ndi amayi a mtawuni kapena kumsika omwe amagulitsa zinthu zosiyanasiyana monga nsapato, katundu wa magetsi ndi zina zambiri. Mavenda timawaona mosiyanabe ndi anthu a bizinesi zokhazikika. Koma tikabwela ku nkhani zokhuza <em>Software</em>, mawu okuti <em>Software Vendor</em> amatipatsa malingaliro a makampani aakulu monga <em>Microsoft</em>, ndi <em>Oracle</em>. Koteleko <em>Software Vendor</em> amakhala wolemekezeka ndithu.</p>
<p>Mkulingalira kwanga, ndikuona ngati mavenda "wambawa" akhonzabe  kutiphunzitsa mmene tingapangire kuti nafe tidzafike pa  mlingo  wa   makampani monga <em>Microsoft</em> ndi kumatchedwanso <em>Software Vendor</em>. Tiyeni tiunikile makhalidwe amavenda:</p>
<ol>
<li><p><strong>Venda samaopa</strong> - nthawi zambiri venda amakhala wolimba mtima ndipo samaopa kutsatsa malonda ake. Venda wamanyazi sangagulitse kanthu. Venda amatha kutsatira munthu kamtunda akuyesera kumutsatsa malonda ake.</p>
</li>
<li><p><strong>Venda samachita manyazi kuonetsa malonda ake</strong> - munthu poti ufike pogula zinthu kwa venda, umakhala kuti malondawo waaona ndi maso, penanso nkuyesa komwe ngati chili chovala kapena nsapato. Venda saopa kuonetsa malonda ake ndipo amati "Kuyesa ndi ulere".</p>
</li>
<li><p><strong>Venda amatha kuyendamtunda kuti agulitse</strong> - mavenda ena amatha kuyenda mtunda wautali, kutiakapeze makasitomala. Izi si zachilendo ngati munaonapo anyamata ogulitsa madzi kapena mazira. Amatha kuyenda kutali ndi komwe achokela wapansi cholinga agulitse.</p>
</li>
<li><p><strong>Venda amayesa zida</strong> - Mavenda ambiri ndi ochenjera, ndipo amakonda kuyesa zida makamaka pa nkhani ya mitengo. Mavenda ambiri amangotchula mtengo wawo nadikira kuti munthu unenerere. Akatelo amakhala kuti awonjezerapo pa mtengo omwe anaodera katunduyo, ndipo akufuna kuwinapo. Ngati umatha kunenerera, udzaona kuti venda uja amatha kusitsa mtengo kwambiri kuchoka pomwe anayambira.</p>
<blockquote>
<p>Ineyo nnagulapo lamba mu dera la Lilongwe pa mtengo wa 2 sauzande, koma venda anandigulitsayo adayambira pa 12 sauzande. Sikuti ndimatha kunenelera, koma zinango chitika kuti amafunisitsa kuti apeze ndalama. Tikatelo tiwone mdemanga ina</p>
</blockquote>
</li>
<li><p><strong>Venda ntchito yake ndi kugulitsa</strong> - venda amafuna kugulitsa, akapanda kugulitsa zimakhala kuti zake sizinayende. Ndipo nthawi zambiri tsiku likamatha mumaona ma venda ena amatha kugulitsa zinthu pa mtengo wozizira, cholinga chawo chimakhala choti agulitse basi. Za mawa aziona mawa lo. Khalidwe limeneli ili ndi ubwino ndi kuyipa kwake, koma chachikulu ndichoti mtima ofuna kugulitsa tsiku limenelo ndi omwe umapangitsa venda kuti alimbikire ndipo apeze njira ndi ma kasitomala.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Generating random project names using Chichewa words]]></title><description><![CDATA[Modern day developers are no strangers to automatically generated project names, these have become common and you may run into them if you use for example:

Vercel for deploying projects

Fly.io

Docker (Desktop)

... and many other modern tools that...]]></description><link>https://code.zikani.me/generating-random-project-names-using-chichewa-words</link><guid isPermaLink="true">https://code.zikani.me/generating-random-project-names-using-chichewa-words</guid><category><![CDATA[Java]]></category><category><![CDATA[projects]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Fri, 16 Feb 2024 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Modern day developers are no strangers to automatically generated project names, these have become common and you may run into them if you use for example:</p>
<ul>
<li><p>Vercel for deploying projects</p>
</li>
<li><p>Fly.io</p>
</li>
<li><p>Docker (Desktop)</p>
</li>
<li><p>... and many other modern tools that generate names for things</p>
</li>
</ul>
<p>I was thinking about this and figured it would be an interesting undertaking to try to generate some names from Chichewa words.</p>
<p>First of all, where to get the words. The internet is full of Chichewa speaking folks nowadays, and I guess I could just <a target="_blank" href="https://github.com/zikani03/articulated">scrape a bunch of news sites</a> and social media platforms for Chichewa content, but for this tiny experiment that's overkill. I thought we could mostly make use of <a target="_blank" href="https://github.com/ceekays/chichewa">this old-but-gold resource</a> by my friend, Edmond Kachale, which he worked on years ago with his colleague Prof. Kevin Scannel.</p>
<h2 id="heading-the-name-gen-algorithm">The name gen algorithm</h2>
<blockquote>
<p>The algorithm is embarrassingly simple, we just shuffle the list of words and pick the first N-words from the shuffled list and join the words with a hyphen ( - )</p>
</blockquote>
<p>Initially, I had tried to come up with something that picked random nouns from the Chichewa project I mentioned above but the output wasn't appealing at all. I tried to change the code to try to include the adjectives, adverbs, pronouns, and nouns in different combinations but it ended up being an exercise in linguistics which I am not qualified to perform.</p>
<p>In the end, I settled on a simple "algorithm" - the algorithm is embarrassingly simple, we just shuffle the list of words and pick the first N-words from the shuffled list and join the words with a hyphen ( - ). I went further to modify the code to add a suffix extracted from a Base64 encoded string to ensure that names remain somewhat unique in the face of collisions due to repetitions of the "random" order we pick words from.</p>
<p>I decided this would work better if I came up with my own list of words for the corpus.</p>
<pre><code class="lang-java"><span class="hljs-keyword">package</span> main;

<span class="hljs-keyword">import</span> java.nio.file.*;
<span class="hljs-keyword">import</span> java.util.*;
<span class="hljs-keyword">import</span> java.security.*;


<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChichewaNameGen</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> SecureRandom rng = <span class="hljs-keyword">new</span> SecureRandom();

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String... args)</span> <span class="hljs-keyword">throws</span> Exception </span>{

        rng.setSeed(System.currentTimeMillis());

        <span class="hljs-keyword">var</span> lines = Files.readAllLines(Paths.get(<span class="hljs-string">"./words.txt"</span>));
        <span class="hljs-keyword">var</span> words = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;(lines);
        <span class="hljs-keyword">var</span> N = Integer.parseInt(args.length &gt;= <span class="hljs-number">1</span> ? args[<span class="hljs-number">0</span>] : <span class="hljs-string">"3"</span>);
        <span class="hljs-keyword">var</span> k = Integer.parseInt(args.length &gt;= <span class="hljs-number">2</span> ? args[<span class="hljs-number">1</span>] : <span class="hljs-string">"10"</span>);

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; k; i++) {
            System.out.println(generate(words, N));
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">generate</span><span class="hljs-params">(List&lt;String&gt; words, <span class="hljs-keyword">int</span> maxWords)</span> </span>{
        Collections.shuffle(words);    
        <span class="hljs-keyword">var</span> w = String.join(<span class="hljs-string">"-"</span>, words.subList(<span class="hljs-number">0</span>, maxWords))
                .replace(<span class="hljs-string">"'"</span>, <span class="hljs-string">""</span>).toLowerCase();

        <span class="hljs-keyword">return</span> w + <span class="hljs-string">"-"</span> + randomHash(w);
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">randomHash</span><span class="hljs-params">(String w)</span> </span>{
        <span class="hljs-keyword">var</span> alpha = Base64.getEncoder()
                .encodeToString((System.currentTimeMillis()+ w)
                        .getBytes()).replace(<span class="hljs-string">"="</span>, <span class="hljs-string">""</span>);

        <span class="hljs-keyword">return</span> (alpha).substring(rng.nextInt(<span class="hljs-number">0</span>, alpha.length()-<span class="hljs-number">9</span>))
                .substring(<span class="hljs-number">0</span>, <span class="hljs-number">9</span>);
    }
}
</code></pre>
<h2 id="heading-creating-a-chichewa-word-list-for-quirkier-phrases">Creating a Chichewa word list for quirkier phrases</h2>
<p>I wanted to have a list of words that generates names that roll off the tongue somewhat nicely or are at least quirky enough to be mildly interesting. I came up with a 100 random but relatable <a target="_blank" href="https://gist.github.com/zikani03/be019504cc1827e951986d83488d90da">Chichewa words</a>. I then placed these in the file where the code above looks. I tried several runs of tests and found the results were more pleasing.</p>
<p>Below is an example with a 5 random names, with length of 3 words from the list:</p>
<pre><code class="lang-bash">
$ java ChichewaNameGen.java 3 5  
<span class="hljs-comment"># output of the random words with a substring from a base64 string</span>
fotokoza-ndemanga-tilipo-XRpbGlwbw
zikwanje-malonje-malemba-WFsZW1iYQ
matumba-chiyambi-mwamva-tbXdhbXZh
basiketi-kufika-lolemba-sb2xlbWJh
kufika-chitedze-makope-1tYWtvcGU
</code></pre>
<p>I am confident that those are somewhat quirky phrases. I am not going to try to translate. Okay, I can't resist with the first one ;)</p>
<p>Roughly <code>(fotokoza)-(ndemanga)-(tilipo)</code> -&gt; <code>(explain)-(your-thoughts/opinions)-(we-are-here/around)</code></p>
<h2 id="heading-how-many-names-can-we-generate">How many names can we generate</h2>
<p>The number of random names/phrases we can generate greatly depends on the number of words in the dataset. With 100 random words this approach should give more than 700 thousand random project names if we set N = 3, roughly (<code>100 * 99 * 98 )</code>.</p>
<p>The last part of the string is a 9 character long sub-string from a Base64 string and it's this bit that will help greatly with reducing collisions if we ever need to generate more names for example in some kind of service with millions of users. That should make us safe-ish from running out of random names in most applications of this kind of thing. Collisions would still occur, but we have more of a leeway for generating new names/phrases without running out of options quickly.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This was an interesting little experiment, I was pleased with the results and plan on using this somewhere in a future project. I might come back to this idea later and try to use the Chichewa dataset properly to create words that are built up grammatically but for now a random list of words works. Here is a link to the Gist with the <a target="_blank" href="https://gist.github.com/zikani03/be019504cc1827e951986d83488d90da">100 random chichewa words</a>  </p>
<p>Thanks to <a target="_blank" href="https://sethbonnie.com/blog/">Seth</a>, <a target="_blank" href="https://codeslayermw.hashnode.dev/">Chimwemwe</a> and <a target="_blank" href="https://ntumbuka.me/">Walter</a> for reviewing drafts of this post.</p>
]]></content:encoded></item><item><title><![CDATA[Is the Kitchen Sink the best place to think?]]></title><description><![CDATA[This article was authored at the kitchen sink.
As a young Malawian bachelor (some rude people would say senior bachelor), I often find myself at the kitchen sink doing dishes. Not only because it's the one skill I am confident at in the kitchen when ...]]></description><link>https://code.zikani.me/is-the-kitchen-sink-the-best-place-to-think</link><guid isPermaLink="true">https://code.zikani.me/is-the-kitchen-sink-the-best-place-to-think</guid><category><![CDATA[Thinking]]></category><category><![CDATA[lifestyle]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Tue, 30 Jan 2024 22:16:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706647204597/80c32b54-6b23-4d02-b5d2-5699778708c1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This article was authored at the kitchen sink.</p>
<p>As a young Malawian bachelor (some rude people would say senior bachelor), I often find myself at the kitchen sink doing dishes. Not only because it's the one skill I am confident at in the kitchen when I compare myself to my house mate, particularly his cooking skills, but also because it's the one activity I have found that somehow helps me think a lot more than other activities.</p>
<p>There are few places in a house where one can retreat to have some time to think. Many of us retreat to the revered W.C. 🚽, others will walk about in a garden if they have one, others laze around the couch or if they are fancy on a Hammock, still more others lay in bed looking up at the ceiling as they gather their thoughts. (Side note: I intend to try to be fancy one day because of <a target="_blank" href="https://www.youtube.com/watch?v=f84n5oFoZBc">this talk by Rich Hickey</a>).</p>
<p>As for me, I long for dirty dishes and some time at the kitchen sink.</p>
<h2 id="heading-the-kitchen-sink-theory">The Kitchen Sink Theory</h2>
<p>Why is the Kitchen Sink so amenable to thinking? I have thought about this a bit (no doubt at the Kitchen Sink) but can only offer a theory: Washing dishes is an activity that has a predictable, well-defined process and outcome that it can be done almost mindlessly, but not completely. You cannot mindlessly handle a knife, soapy ceramic plate or glass tumbler. Even though it's a somewhat mindless activity, it leaves one room to spend time thinking about other things throughout the activity. Washing dishes is one of the most delightful activities because it leaves you with a clean kitchen sink, countertop, clean dishes, and a sense of completion - if done right.</p>
<h2 id="heading-what-to-think-about-at-the-kitchen-sink">What to think about at the Kitchen Sink ?</h2>
<p>I tend to think about about life, relationships, programming, work related issues or new ideas. One limitation of working the mind at the kitchen sink is that my hands are arrested by the very dishes, cutlery, water and dish washing liquid and I cannot write down what I am thinking of immediately. Taking a break to write would disrupt the flow of cleaning and thinking so I usually delegate that to later, if I don't get distracted by another thought by the time I am leaving the kitchen. Most of the time the thoughts do stick or lead to some action; whether finding something to read, research on, write down or code up.</p>
<h2 id="heading-there-arent-enough-dirty-dishes-i-need-an-alternative">There aren't enough dirty dishes, I need an alternative.</h2>
<p>Despite the fact that this works for me, the most glaring limitation is that once you are done washing dishes, that's the end of the session. It makes no sense to wash dishes for the sake of washing or just as an excuse to think (though I get tempted to do so every now and then).</p>
<p>Therefore I am looking for an alternative activity, something similarly mindless, that will not wait on me to have dirty dishes at my disposal.</p>
<p>What would you suggest ?</p>
<hr />
<p>❤️ Thank you to my lovely fiancée for helping me edit this post. Thanks for being patient when I'm doing dishes. ❤️</p>
]]></content:encoded></item><item><title><![CDATA[Generating PDF from Markdown with Go]]></title><description><![CDATA[In this quick-n-dirty article, we will see how to generate a PDF from a user-submitted markdown with Go. We will build a simple service using the standard library's net/http for the backend and EasyMDE, a browser editor, for handling markdown on the ...]]></description><link>https://code.zikani.me/generating-pdf-from-markdown-with-go</link><guid isPermaLink="true">https://code.zikani.me/generating-pdf-from-markdown-with-go</guid><category><![CDATA[Go Language]]></category><category><![CDATA[markdown]]></category><category><![CDATA[pdf]]></category><category><![CDATA[goldmark]]></category><category><![CDATA[EasyMDE]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Sat, 21 Oct 2023 22:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698334233098/6833b004-fcdd-4bc8-9e99-de7cf272b6b5.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this quick-n-dirty article, we will see how to generate a PDF from a user-submitted markdown with Go. We will build a simple service using the standard library's <code>net/http</code> for the backend and EasyMDE, a browser editor, for handling markdown on the front-end.</p>
<p>Markdown is a popular authoring format and has gained wide adoption in many different places. Markdown is generally converted to HTML, but there do exist tools like <a target="_blank" href="https://pandoc.org/installing.html">Pandoc</a> which can additionally convert markdown to different formats like .docx documents, PDF etc...</p>
<p>In the Go ecosystem, a popular library for working with Markdown is <a target="_blank" href="https://github.com/yuin/goldmark">goldmark</a> - we which we will use to process user-submitted markdown text and render it to a PDF file. You can imagine this being used for basic authoring of content including letters, articles, simple newsletters, basic reports etc...</p>
<h2 id="heading-markdown-pdf-generator-backend">Markdown PDF generator backend</h2>
<p>For us to process Markdown, we will rely on the popular library <a target="_blank" href="https://github.com/yuin/goldmark">goldmark</a> which has some community-built extensions, the one we are particularly interested in is the <a target="_blank" href="http://github.com/stephenafamo/goldmark-pdf">goldmark-pdf</a> library which will allow us to render Goldmark's Markdown AST into PDF. Firstly, we will create a bare-bones (i.e. not production-grade) web server using the standard library's <code>net/http</code>.</p>
<p>The complete server code is shown below:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"bytes"</span>
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"encoding/base64"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"io/ioutil"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"os"</span>
    <span class="hljs-string">"text/template"</span>

    pdf <span class="hljs-string">"github.com/stephenafamo/goldmark-pdf"</span>
    <span class="hljs-string">"github.com/yuin/goldmark"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    markdown := goldmark.New(
        goldmark.WithRenderer(
            pdf.New(
                pdf.WithTraceWriter(os.Stdout),
                pdf.WithContext(context.Background()),
                pdf.WithImageFS(os.DirFS(<span class="hljs-string">"."</span>)),
                pdf.WithHeadingFont(pdf.GetTextFont(<span class="hljs-string">"Arial"</span>, pdf.FontCourier)),
                pdf.WithBodyFont(pdf.GetTextFont(<span class="hljs-string">"Arial"</span>, pdf.FontCourier)),
                pdf.WithCodeFont(pdf.GetCodeFont(<span class="hljs-string">"Arial"</span>, pdf.FontCourier)),
            ),
        ),
    )

    mux := http.NewServeMux()

    mux.HandleFunc(<span class="hljs-string">"/"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(w http.ResponseWriter, r *http.Request)</span></span> {
        w.Header().Add(<span class="hljs-string">"Cache-Control"</span>, <span class="hljs-string">"cache"</span>)
        indexHtmlTemplate := template.Must(template.ParseFiles(<span class="hljs-string">"index.html"</span>))
        indexHtmlTemplate.ExecuteTemplate(w, <span class="hljs-string">"index.html"</span>, <span class="hljs-literal">nil</span>)
    })

    mux.HandleFunc(<span class="hljs-string">"/generate"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(w http.ResponseWriter, r *http.Request)</span></span> {
        source, err := ioutil.ReadAll(r.Body)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-keyword">var</span> buf bytes.Buffer
        <span class="hljs-keyword">if</span> err := markdown.Convert([]<span class="hljs-keyword">byte</span>(source), &amp;buf); err != <span class="hljs-literal">nil</span> {
            http.Error(w, <span class="hljs-string">"failed to generate pdf"</span>, http.StatusInternalServerError)
            <span class="hljs-keyword">return</span>
        }
        base64Encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
        dataURL := fmt.Sprintf(<span class="hljs-string">"data:application/octet-stream;base64,%s"</span>, base64Encoded)
        w.Write([]<span class="hljs-keyword">byte</span>(dataURL))
    })

    http.ListenAndServe(<span class="hljs-string">":8001"</span>, mux)
}
</code></pre>
<h2 id="heading-the-frontend-adding-an-in-browser-editor">The frontend - adding an in-browser editor</h2>
<p>We are going to complete our simple service by adding a frontend to it which is going to be a simple Markdown Editor with a button for Generating and Downloading the PDF. We will use EasyMDE for this - I went through several different options for an editor and at the moment I like EasyMDE, especially for how easy it is to add extra functionality to the Editor. Will will be extending EasyMDE to enable downloading the generated PDF by adding a button with a PDF icon for the user to generate the PDF.</p>
<p>The complete code for the <code>index.html</code> page is shown below</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"X-UA-Compatible"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"IE=edge"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Markdown to PDF<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">base</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>
        <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/default.min.css"</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"</span>
        <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.bundle.min.js"</span>
        <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha384-pjaaA8dDz/5BgdFUPX6M/9SUZv4d12SUPF0axWc+VRZkx5xU3daN+lYb49+Ax+Tl"</span>
        <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/highlight.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span>hljs.initHighlightingOnLoad();<span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">defer</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://use.fontawesome.com/releases/v5.4.1/js/all.js"</span>
        <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha384-L469/ELG4Bg9sDQbl0hvjMq8pOcqFgkSpwhwnslzvVVGpDjYJ6wJJyYjvG3u8XW7"</span>
        <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/axios@0.26.0/dist/axios.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://unpkg.com/easymde@2.18.0/dist/easymde.min.css"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/easymde@2.18.0/dist/easymde.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"content"</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">textarea</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"editor"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">textarea</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"downloadEl"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"display: none;"</span> <span class="hljs-attr">download</span>=<span class="hljs-string">"Document.pdf"</span>&gt;</span>Download PDF<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span>&gt;</span><span class="javascript">
        <span class="hljs-keyword">let</span> downloadEl = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'downloadEl'</span>)
        <span class="hljs-keyword">new</span> EasyMDE({
            <span class="hljs-attr">autoDownloadFontAwesome</span>: <span class="hljs-literal">false</span>,
            <span class="hljs-attr">toolbar</span>: [
            <span class="hljs-string">"bold"</span>, <span class="hljs-string">"italic"</span>, <span class="hljs-string">"heading"</span>, <span class="hljs-string">"|"</span>, <span class="hljs-string">"quote"</span>, <span class="hljs-string">'strikethrough'</span>, <span class="hljs-string">'code'</span>, <span class="hljs-string">'heading'</span>,<span class="hljs-string">'|'</span>, <span class="hljs-string">'undo'</span>,<span class="hljs-string">'redo'</span>
            ,<span class="hljs-string">'|'</span>, { <span class="hljs-comment">// Separator</span>
                <span class="hljs-attr">name</span>: <span class="hljs-string">"generate-pdf"</span>,
                <span class="hljs-attr">action</span>: <span class="hljs-function">(<span class="hljs-params">editor</span>) =&gt;</span> {
                    axios.post(<span class="hljs-string">"/generate"</span>, editor.value())
                    .then(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> {
                        <span class="hljs-keyword">let</span> pdfDataURL = response.data
                        <span class="hljs-keyword">if</span> (pdfDataURL.indexOf(<span class="hljs-string">"base64"</span>) &gt; <span class="hljs-number">0</span>) {
                            downloadEl.setAttribute(<span class="hljs-string">'href'</span>, pdfDataURL)
                            downloadEl.click()
                        }
                    })
                },
                <span class="hljs-attr">className</span>: <span class="hljs-string">"fa fa-file-pdf"</span>,
                <span class="hljs-attr">title</span>: <span class="hljs-string">"Download PDF"</span>,
            }, <span class="hljs-string">'|'</span>, {
                <span class="hljs-attr">name</span>: <span class="hljs-string">"link"</span>,
                <span class="hljs-attr">action</span>: <span class="hljs-string">'https://blog.nndi.cloud'</span>,
                <span class="hljs-attr">className</span>: <span class="hljs-string">"fa fa-globe"</span>,
                <span class="hljs-attr">title</span>: <span class="hljs-string">"Go to the NNDI Blog"</span>
            }],
            <span class="hljs-attr">element</span>: <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'editor'</span>),
            <span class="hljs-attr">initialValue</span>: <span class="hljs-string">'## Welcome to Markdown to PDF using Go.\nType some markdown in here and then click the PDF button to render the markdown to PDF'</span>
        });
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>With those two bits of code done, we can now build and run our service. Make sure you copy the Go code into a file named <code>main.go</code> and the HTML code in a file named <code>index.html</code> in the same directory - you can then run the following commands:</p>
<pre><code class="lang-bash">$ go mod init markdown-pdf
$ go mod tidy
$ go run main.go
</code></pre>
<p>The last command should start a server on http://localhost:8001 visit that in your browser and try it out.</p>
<p>Thanks for reading.</p>
]]></content:encoded></item><item><title><![CDATA[Implementing a Custom ID generator for Hibernate entities in Java]]></title><description><![CDATA[If you have used Hibernate before you know that you can specify that an Entity class should have it's @Id fields set via @GeneratedValue - typically you use either a sequence or identity strategy to automatically generate, say, auto-incrementing inte...]]></description><link>https://code.zikani.me/implementing-a-custom-id-generator-for-hibernate-entities-in-java</link><guid isPermaLink="true">https://code.zikani.me/implementing-a-custom-id-generator-for-hibernate-entities-in-java</guid><category><![CDATA[hibernate]]></category><category><![CDATA[unique identifier]]></category><category><![CDATA[Springboot]]></category><category><![CDATA[reference]]></category><category><![CDATA[Java]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Sun, 08 Oct 2023 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>If you have used Hibernate before you know that you can specify that an Entity class should have it's <code>@Id</code> fields set via <code>@GeneratedValue</code> - typically you use either a sequence or identity strategy to automatically generate, say, auto-incrementing integer IDs (also take note that Hibernate also provides a <code>@UuidGenerator</code>).</p>
<p>However, Hibernate also allows you to create custom implementations of a generator which you can use to create a custom ID generation scheme - which can be useful for generating Transaction IDs for example.</p>
<p>In this article, I will show you how to implement such a custom generator that will be used to generate user-friendly, somewhat predictable, IDs. The IDs will have the following form, <code>FAN-00YY&lt;Month-Letter&gt;&lt;Day-Letter&gt;#&lt;counter&gt;</code> which would generate a value that looks something like this<code>FAN-0023JL#3642</code>. Of course, you can amend the solution presented here to match your own needs for the format of the IDs.</p>
<p>Let's get coding. To enable us to test the solution easily, we will use hibernate withing a Spring Boot project - so create a new Spring Boot project with Web, JPA and Postgres as dependencies. Done? Okay, great.</p>
<p>Let's assume we have an SQL table to keep track of Fans (whether the electric kind or the eccentric kind is up to you). As you can see, we have defined its id column as text and as a primary key which means we must provide unique values for that field every time we want to insert/create a record.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> fans (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span> primary <span class="hljs-keyword">key</span>,
  <span class="hljs-keyword">name</span> <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  created_at timestamptz
);
</code></pre>
<p>The associated entity class may look like this:</p>
<pre><code class="lang-java"><span class="hljs-keyword">import</span> jakarta.persistence.Column;
<span class="hljs-keyword">import</span> jakarta.persistence.Entity;
<span class="hljs-keyword">import</span> jakarta.persistence.Id;
<span class="hljs-keyword">import</span> jakarta.persistence.Table;

<span class="hljs-keyword">import</span> java.time.OffsetDateTime;

<span class="hljs-meta">@Entity</span>
<span class="hljs-meta">@Table(name = "fans")</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Fan</span> </span>{
    <span class="hljs-meta">@Id</span>
    <span class="hljs-meta">@FanId</span>
    <span class="hljs-keyword">private</span> String id;

    <span class="hljs-meta">@Column(name = "name")</span>
    <span class="hljs-keyword">private</span> String name;

    <span class="hljs-meta">@Column(name="created_at")</span>
    <span class="hljs-keyword">private</span> OffsetDateTime createdAt;

    <span class="hljs-comment">// getters and setters omitted...</span>
}
</code></pre>
<p>The attentive reader will observe that we have added a non-standard annotation <code>@FanId</code> to the id field. This is our custom annotation that will be used to drive the custom ID generation. Lets create the custom annotation as shown below:-</p>
<pre><code class="lang-java"><span class="hljs-keyword">import</span> org.hibernate.annotations.IdGeneratorType;

<span class="hljs-keyword">import</span> java.lang.annotation.Retention;
<span class="hljs-keyword">import</span> java.lang.annotation.RetentionPolicy;
<span class="hljs-keyword">import</span> java.lang.annotation.Target;

<span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> java.lang.annotation.ElementType.FIELD;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> java.lang.annotation.ElementType.METHOD;

<span class="hljs-meta">@IdGeneratorType( FanIdGenerator.class )</span>
<span class="hljs-meta">@Retention(RetentionPolicy.RUNTIME)</span>
<span class="hljs-meta">@Target({ FIELD, METHOD })</span>
<span class="hljs-keyword">public</span> <span class="hljs-meta">@interface</span> FanId {
    <span class="hljs-function"><span class="hljs-keyword">boolean</span> <span class="hljs-title">usePersistentCounter</span><span class="hljs-params">()</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">false</span></span>;
}
</code></pre>
<h2 id="heading-the-id-generator-class">The ID Generator class</h2>
<p>This is the main class that handles the heavy lifting of the ID generation process. It is implemented as a class that implements the <code>BeforeExecutionGenerator</code> which means we can perform some actions before lifecycle events like Entity#save.</p>
<p>The generator defines an interface <code>CounterService</code> to which it delegates the generation of an automatically incrementing counter which forms the last part of our custom ID. In the constructor, we check if the annotation we are processing is set to use persistent counters, if so - we use a specific implementation of the counter service that stores the counters in the database - more on that below.</p>
<p>As you can see in the code below, we specify that we only want this class to be executed on INSERT operations/events.</p>
<pre><code class="lang-java"><span class="hljs-keyword">import</span> org.hibernate.HibernateException;
<span class="hljs-keyword">import</span> org.hibernate.engine.spi.SharedSessionContractImplementor;
<span class="hljs-keyword">import</span> org.hibernate.generator.BeforeExecutionGenerator;
<span class="hljs-keyword">import</span> org.hibernate.generator.EventType;
<span class="hljs-keyword">import</span> org.hibernate.generator.EventTypeSets;
<span class="hljs-keyword">import</span> org.hibernate.id.factory.spi.CustomIdGeneratorCreationContext;

<span class="hljs-keyword">import</span> java.lang.reflect.Member;
<span class="hljs-keyword">import</span> java.time.OffsetDateTime;
<span class="hljs-keyword">import</span> java.util.EnumSet;

<span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> org.hibernate.internal.util.ReflectHelper.getPropertyType;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FanIdGenerator</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">BeforeExecutionGenerator</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> CounterService DEFAULT_IN_PROCESS_COUNTER = <span class="hljs-keyword">new</span> InMemoryCounterService();
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">char</span>[] CHARS = <span class="hljs-keyword">new</span> <span class="hljs-keyword">char</span>[]{
            <span class="hljs-string">'A'</span>, <span class="hljs-string">'B'</span>, <span class="hljs-string">'C'</span>, <span class="hljs-string">'D'</span>, <span class="hljs-string">'E'</span>, <span class="hljs-string">'F'</span>, <span class="hljs-string">'G'</span>,
            <span class="hljs-string">'H'</span>, <span class="hljs-string">'I'</span>, <span class="hljs-string">'J'</span>, <span class="hljs-string">'K'</span>, <span class="hljs-string">'L'</span>, <span class="hljs-string">'M'</span>, <span class="hljs-string">'N'</span>,
            <span class="hljs-string">'O'</span>, <span class="hljs-string">'P'</span>, <span class="hljs-string">'Q'</span>, <span class="hljs-string">'R'</span>, <span class="hljs-string">'S'</span>, <span class="hljs-string">'T'</span>, <span class="hljs-string">'U'</span>,
            <span class="hljs-string">'V'</span>, <span class="hljs-string">'W'</span>, <span class="hljs-string">'X'</span>, <span class="hljs-string">'Y'</span>, <span class="hljs-string">'Z'</span>, <span class="hljs-string">'1'</span>, <span class="hljs-string">'2'</span>,
            <span class="hljs-string">'3'</span>, <span class="hljs-string">'4'</span>, <span class="hljs-string">'5'</span>, <span class="hljs-string">'6'</span>, <span class="hljs-string">'7'</span>, <span class="hljs-string">'8'</span>, <span class="hljs-string">'9'</span>
    };

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> CounterService counterService;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">FanIdGenerator</span><span class="hljs-params">(
            FanId config,
            Member idMember,
            CustomIdGeneratorCreationContext creationContext)</span> </span>{

        <span class="hljs-keyword">final</span> Class&lt;?&gt; propertyType = getPropertyType(idMember);

        <span class="hljs-keyword">if</span> (config.usePersistentCounter()) {
            counterService = <span class="hljs-keyword">new</span> PersistentCounterService();
        } <span class="hljs-keyword">else</span> {
            counterService = DEFAULT_IN_PROCESS_COUNTER;
        }

        <span class="hljs-keyword">if</span> (!String.class.isAssignableFrom(propertyType)) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> HibernateException(<span class="hljs-string">"Unanticipated return type ["</span> + propertyType.getName() + <span class="hljs-string">"] for FanId conversion"</span>);
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">formatYear</span><span class="hljs-params">(<span class="hljs-keyword">int</span> year)</span> </span>{
        <span class="hljs-keyword">return</span> String.format(<span class="hljs-string">"00%d"</span>, year % <span class="hljs-number">100</span>);
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">char</span> <span class="hljs-title">formatMonth</span><span class="hljs-params">(<span class="hljs-keyword">int</span> dayValue)</span> </span>{
        <span class="hljs-keyword">return</span> CHARS[dayValue - <span class="hljs-number">1</span>];
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">char</span> <span class="hljs-title">formatDay</span><span class="hljs-params">(<span class="hljs-keyword">int</span> monthValue)</span> </span>{
        <span class="hljs-keyword">return</span> CHARS[monthValue - <span class="hljs-number">1</span>];
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> Object <span class="hljs-title">generate</span><span class="hljs-params">(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType)</span> </span>{

        OffsetDateTime now = OffsetDateTime.now();
        <span class="hljs-keyword">return</span> String.format(<span class="hljs-string">"FAN-%s%s%s#%s"</span>,
                formatYear(now.getYear()),
                formatMonth(now.getMonthValue()),
                formatDay(now.getDayOfMonth()),
                counterService.nextInteger(session, now)
        );
    }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> EnumSet&lt;EventType&gt; <span class="hljs-title">getEventTypes</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> EventTypeSets.INSERT_ONLY;
    }

    <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">CounterService</span> </span>{
        <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">nextInteger</span><span class="hljs-params">(SharedSessionContractImplementor session, OffsetDateTime onDate)</span></span>;
    }
}
</code></pre>
<blockquote>
<p>The ID generation scheme was inspired by some code shared and implemented by <a target="_blank" href="https://ntumbuka.me/">a friend</a> for a health information system used widely in Malawi, the code for that <a target="_blank" href="https://github.com/EGPAFMalawiHIS/his_emr_api_lab/blob/2bf27234045499d67f84739790a5ef8f42222130/app/services/lab/accession_number_service.rb#L11">is here</a>.</p>
</blockquote>
<h2 id="heading-integer-counter-generator-using-in-memory-counter">Integer Counter generator using in-memory counter</h2>
<p>With this simplest implementation, the counter service uses a variable to keep track of the values generated. We use an AtomicInteger for this to enable some kind of thread safety, in case that's required. The implementation is fairly straightforward:-</p>
<pre><code class="lang-java"><span class="hljs-keyword">import</span> org.hibernate.engine.spi.SharedSessionContractImplementor;

<span class="hljs-keyword">import</span> java.time.OffsetDateTime;
<span class="hljs-keyword">import</span> java.util.concurrent.atomic.AtomicInteger;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InMemoryCounterService</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">FanIdGenerator</span>.<span class="hljs-title">CounterService</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> AtomicInteger id = <span class="hljs-keyword">new</span> AtomicInteger(<span class="hljs-number">1000</span>);

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">nextInteger</span><span class="hljs-params">(SharedSessionContractImplementor session, OffsetDateTime onDate)</span> </span>{
        <span class="hljs-keyword">return</span> id.incrementAndGet();
    }
}
</code></pre>
<p>The above approach works but has the limitation that the counter of the IDs will be reset every time the service is restarted which may lead to errors since that introduces the potential of generating duplicate keys (remember we want the IDs to be unique to use them as primary keys). We can do better by storing the counter's latest value in the database, indexed by date - this way we can be sure that we can always continue from where we left off despite restarts.</p>
<h2 id="heading-integer-counter-generator-using-a-value-stored-in-a-database-table">Integer Counter Generator using a value stored in a database table</h2>
<p>Below is the implementation of the CounterService interface, named <code>PersistentCounterService</code>, which stores counter values in a database table to allow us to fetch easily the latest counter value to try to avoid collisions. Each date has its counter that gets incremented every time we receive a request to generate an ID for that day.</p>
<p>As you can see in the code, the table that keeps track of the counters is named <code>fan_id_counters</code> and we try to create it every time, this allows our ID Generator to be re-usable without any upfront configuration - the only implication is that the user connecting to the database should have appropriate permissions to create tables.</p>
<p>You could always change the implementation to remove the create call and manually create your counter-tracking table ahead of time.</p>
<pre><code class="lang-java"><span class="hljs-keyword">import</span> org.hibernate.engine.spi.SharedSessionContractImplementor;

<span class="hljs-keyword">import</span> java.time.LocalDate;
<span class="hljs-keyword">import</span> java.time.OffsetDateTime;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PersistentCounterService</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">FanIdGenerator</span>.<span class="hljs-title">CounterService</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">int</span> DEFAULT_COUNTER_START = <span class="hljs-number">1000</span>;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> String SQL_CREATE_COUNTER_TABLE = <span class="hljs-string">""</span><span class="hljs-string">"
            CREATE TABLE IF NOT EXISTS fan_id_counters (
                entry_date date primary key not null,
                counter_value integer not null default 1000
            );
            "</span><span class="hljs-string">""</span>;
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">nextInteger</span><span class="hljs-params">(SharedSessionContractImplementor session, OffsetDateTime onDate)</span> </span>{
        <span class="hljs-keyword">final</span> <span class="hljs-keyword">var</span> today = LocalDate.now();
        <span class="hljs-keyword">var</span> statelessSession = session.isStatelessSession() ? session.asStatelessSession() : session.getSession();
        statelessSession.createNativeMutationQuery(SQL_CREATE_COUNTER_TABLE)
                .executeUpdate();

        <span class="hljs-keyword">var</span> list = statelessSession.createNativeQuery(<span class="hljs-string">"SELECT counter_value FROM fan_id_counters WHERE entry_date = :date FOR UPDATE LIMIT 1;"</span>, Integer.class)
                .setParameter("date", today)
                .setFetchSize(<span class="hljs-number">1</span>)
                .getResultList();
        <span class="hljs-keyword">if</span> (list.isEmpty()) {
            statelessSession.createNativeMutationQuery(<span class="hljs-string">"INSERT INTO fan_id_counters(entry_date, counter_value) VALUES (:date , :counter)"</span>)
                    .setParameter(<span class="hljs-string">"date"</span>, today)
                    .setParameter(<span class="hljs-string">"counter"</span>, DEFAULT_COUNTER_START + <span class="hljs-number">1</span>)
                    .executeUpdate();
            <span class="hljs-keyword">return</span> DEFAULT_COUNTER_START;
        }

        statelessSession.createNativeMutationQuery(<span class="hljs-string">"UPDATE fan_id_counters SET counter_value = counter_value + 1 WHERE entry_date = :date ;"</span>)
                .setParameter(<span class="hljs-string">"date"</span>, today)
                .executeUpdate();

        <span class="hljs-keyword">return</span> list.get(<span class="hljs-number">0</span>);
    }
}
</code></pre>
<h2 id="heading-lets-test-this-thing">Let's test this thing</h2>
<p>Now that we have the ID generator setup, we can implement a simple controller to allow us to create Fan entities to ensure that the generator does its job and also do a little bit of load testing to ensure that the generator doesn't trip up when there is a large number of requests, as one use-case would be to use it to generate Transaction IDs or Transaction Reference numbers for example.</p>
<p>First, let's create a simple controller that allows us to list and create Fans</p>
<pre><code class="lang-java"><span class="hljs-meta">@RestController</span>
<span class="hljs-meta">@RequestMapping("/api/v1/fans")</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FanController</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> FanRepository fanRepository;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">FanController</span><span class="hljs-params">(FanRepository fanRepository)</span> </span>{
        <span class="hljs-keyword">this</span>.fanRepository = fanRepository;
    }

    <span class="hljs-meta">@GetMapping</span>
    <span class="hljs-keyword">public</span> ResponseEntity&lt;List&lt;Fan&gt;&gt; createFan(Pageable pageable) {
        <span class="hljs-keyword">return</span> ResponseEntity.ok(fanRepository.findAll(pageable).toList());
    }

    <span class="hljs-meta">@PostMapping</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> ResponseEntity&lt;Fan&gt; <span class="hljs-title">createFan</span><span class="hljs-params">(<span class="hljs-meta">@RequestParam("name")</span> String name)</span> </span>{
        Fan fan = <span class="hljs-keyword">new</span> Fan();
        fan.setName(name);
        fan.setCreatedAt(OffsetDateTime.now());
        fanRepository.saveAndFlush(fan);
        <span class="hljs-keyword">return</span> ResponseEntity.ok(fan);
    }
}
</code></pre>
<p>We can use the following cURL request to create a Fan</p>
<pre><code class="lang-bash">curl -X POST <span class="hljs-string">"http://localhost:8080/api/v1/fans?name=James"</span>
</code></pre>
<p>This could return something like this</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"FAN-0023JL#1000"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"James"</span>,
    <span class="hljs-attr">"createdAt"</span>: <span class="hljs-string">"2023-08-12T21:04:44.135036Z"</span>
},
</code></pre>
<h3 id="heading-load-testing-the-id-generator">Load testing the ID Generator</h3>
<p>We can now use a load testing tool like <a target="_blank" href="https://github.com/rakyll/hey">Hey</a> to load test the server - which will send about 1000 requests with the command below.</p>
<pre><code class="lang-bash">hey -n 1000 -m POST <span class="hljs-string">"http://localhost:8080/api/v1/fans?name=James"</span>
</code></pre>
<p>Here is a screenshot that shows a sample run on my environment, not so bad :) -</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697172409868/173bc51c-0ddc-44e9-8ccf-1b58ee0e83f2.png" alt class="image--center mx-auto" /></p>
<p>We can check in the database and verify that each fan has a uniquely generated ID.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, we have seen how to implement a custom ID generator with Hibernate that uses a custom format that includes an auto-incrementing counter that has two implementations, one in-memory and another that stores the most recent counter in a database table indexed by calendar day. You can adapt the code here to implement your ID generator which can be useful for things like Transaction IDs or Transaction References that are somewhat more user friendly.</p>
<p>Thank you for reading.</p>
]]></content:encoded></item><item><title><![CDATA[A few PostgreSQL tricks]]></title><description><![CDATA[In this article, we will look at some cool, practical query tricks you can use in projects that use PostgreSQL as the DBMS:
Order results but select a specific first-row
This trick allows you to write a query with an order-by clause which allows you ...]]></description><link>https://code.zikani.me/postgresql-tricks-01</link><guid isPermaLink="true">https://code.zikani.me/postgresql-tricks-01</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[SQL]]></category><category><![CDATA[Query]]></category><category><![CDATA[string-methods]]></category><category><![CDATA[json]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Sun, 08 Oct 2023 10:00:00 GMT</pubDate><content:encoded><![CDATA[<p>In this article, we will look at some cool, practical query tricks you can use in projects that use PostgreSQL as the DBMS:</p>
<h2 id="heading-order-results-but-select-a-specific-first-row">Order results but select a specific first-row</h2>
<p>This trick allows you to write a query with an order-by clause which allows you to choose which row should come first based on some condition, despite the presence of another order-by clause.</p>
<p>See the example below which will always return the company with ID = 2 as the first row despite how the other order by clause is written.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> companies_test (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">serial</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  company_name <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  created_at <span class="hljs-built_in">timestamp</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">now</span>()
);

<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> companies_test (company_name)
<span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'Company A'</span>), (<span class="hljs-string">'Company B'</span>),(<span class="hljs-string">'Company C'</span>);

<span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span>, company_name, created_at 
<span class="hljs-keyword">from</span> companies_test
<span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> (<span class="hljs-keyword">id</span> = <span class="hljs-number">2</span>) <span class="hljs-keyword">desc</span>, created_at <span class="hljs-keyword">asc</span>
</code></pre>
<h2 id="heading-using-string-aggregates-to-return-arrays-for-related-records">Using string aggregates to return arrays for related records</h2>
<p>The <code>string_agg</code> function is a good function in Postgres that allows you to aggregate columns after a group by operations.</p>
<p>Let's say you have a database of individuals who share the same first and last names and you want to capture all the dates of birth linked to that name in the database, (sorry if this is a quirky example, could only think of it as it's something I faced) you could do something like this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> individuals_test (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">serial</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  first_name <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  last_name <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  date_of_birth <span class="hljs-built_in">date</span>,
  created_at <span class="hljs-built_in">timestamp</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">now</span>()
);

<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> individuals_test (first_name, last_name, date_of_birth)
<span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'John'</span>, <span class="hljs-string">'Doe'</span>, <span class="hljs-string">'1970-01-01'</span>), (<span class="hljs-string">'John'</span>, <span class="hljs-string">'Doe'</span>, <span class="hljs-string">'2003-10-01'</span>),(<span class="hljs-string">'John'</span>, <span class="hljs-string">'Doe'</span>, <span class="hljs-string">'1993-04-01'</span>);

 <span class="hljs-keyword">SELECT</span> 
   <span class="hljs-keyword">concat</span>(first_name, <span class="hljs-string">' '</span>, last_name) <span class="hljs-keyword">as</span> full_name, 
   string_agg(date_of_birth::<span class="hljs-built_in">text</span>, <span class="hljs-string">';'</span>) <span class="hljs-keyword">as</span> dates_of_birth 
 <span class="hljs-keyword">FROM</span> individuals_test 
 <span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> <span class="hljs-keyword">concat</span>(first_name, <span class="hljs-string">' '</span>, last_name) <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> full_name
</code></pre>
<p>The result will look something like</p>
<pre><code class="lang-markdown">| full<span class="hljs-emphasis">_name | dates_</span>of<span class="hljs-emphasis">_birth                   |
| --------- | -------------------------------- |
| John Doe  | 1970-01-01;2003-10-01;1993-04-01 |</span>
</code></pre>
<h2 id="heading-checking-if-a-record-exists-with-a-true-or-false-result">Checking if a record exists with a true or false result</h2>
<p>Most times we have some parts in our code where we would just like to know if a record exists. Here is a pattern for a query that always guarantees a result that will be either <code>true</code> or <code>false</code> depending on the result of the subquery - this way you don't have to check if the resultset from your query is empty or not:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">exists</span> (<span class="hljs-keyword">select</span> <span class="hljs-keyword">from</span> <span class="hljs-keyword">users</span> <span class="hljs-keyword">where</span> username = :username);
</code></pre>
<h2 id="heading-check-constraint-to-validate-a-json-column">CHECK constraint to validate a JSON column</h2>
<p>Some days you may find yourself storing JSON in a database column - now the thing about JSON is that you will have to deal with a Schema somewhere, usually in your application code. Here is a simple approach to check that the JSON has at least the required top-level keys as a simple validation to ensure incorrectly formatted JSON isn't entered in your columns. You can go crazy with this and validate against a whole JSON Schema - take a look at <a target="_blank" href="https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation">pg_jsonschema</a> by the Supabase team</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> user_app_settings (
  username <span class="hljs-built_in">text</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>,
  <span class="hljs-keyword">settings</span> jsonb
);

<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> user_app_settings 
<span class="hljs-keyword">ADD</span> <span class="hljs-keyword">CONSTRAINT</span> check_settings_fields 
     <span class="hljs-keyword">CHECK</span> (<span class="hljs-keyword">settings</span>::jsonb ?&amp; <span class="hljs-built_in">array</span>[<span class="hljs-string">'cover_photo'</span>, <span class="hljs-string">'current_theme'</span>, <span class="hljs-string">'toolbar_x'</span>, <span class="hljs-string">'toolbar_y'</span>, <span class="hljs-string">'version'</span>]);

 <span class="hljs-comment">-- this insert will work because the JSON is well-formed according to our rules</span>
 <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> user_app_settings
 <span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'zikani03'</span>, <span class="hljs-string">'{"version": "v1.0-beta", "cover_photo": "https://some.site/pic/zikani03", "current_theme":"nord", "toolbar_x": 354.5, "toolbar_y": 100}'</span>::jsonb);

 <span class="hljs-comment">-- this insert below should fail since we don't have the "version" field </span>
 <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> user_app_settings
 <span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'john'</span>, <span class="hljs-string">'{"no_version": "v1.0-beta", "cover_photo": "https://some.site/pic/john", "current_theme":"nord", "toolbar_x": 354.5, "toolbar_y": 100}'</span>::jsonb);
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, I shared a few tricks you can use in your projects that use Postgresql, though some of them would also likely work in other DBMSs if you found the equivalent functions. I hope to share more as I keep learning...</p>
<p>Thanks for reading.</p>
]]></content:encoded></item><item><title><![CDATA[Keeping @JsonProperty and Bean Validation field error messages in sync in a Spring Boot project]]></title><description><![CDATA[Damn, this one bit me very recently. So imagine you have a Java API and because of whatever reasons you agreed that the JSON properties for the requests should be in snake_case_form. Cool, cool, that just wants a simple Jackson @JsonProperty annotati...]]></description><link>https://code.zikani.me/jsonproperty-bean-validation-field-error-messages-in-spring-boot</link><guid isPermaLink="true">https://code.zikani.me/jsonproperty-bean-validation-field-error-messages-in-spring-boot</guid><category><![CDATA[Springboot]]></category><category><![CDATA[Validation]]></category><category><![CDATA[jackson]]></category><category><![CDATA[json]]></category><category><![CDATA[exceptionhandling]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Sat, 23 Sep 2023 22:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Damn, this one bit me very recently. So imagine you have a Java API and because of whatever reasons you agreed that the JSON properties for the requests should be in <code>snake_case_form</code>. Cool, cool, that just wants a simple Jackson <code>@JsonProperty</code> annotation on a POJO (Plain Old Java Object) field. Now imagine you are sending validation error messages to the client, and they discover that the validation messages refer to the fields using <code>camelCaseForm</code> - now you have to reassure them that their request with snake_case_form fields is supposed to be that way, except for there being validation errors, of course.</p>
<p>This very thing happened to me recently and I wanted to document solutions to the problem, I will tell you which I went with at the end.</p>
<p>Let's create a new Spring Boot project to get a better picture of the issue at hand.</p>
<p>Let's add a Person class that looks like this. Notice that we have thrown in Hibernate Validator annotations to validate the data, like the good backend developers we are.</p>
<pre><code class="lang-java"><span class="hljs-keyword">import</span> com.fasterxml.jackson.annotation.JsonProperty;
<span class="hljs-keyword">import</span> jakarta.validation.constraints.Email;
<span class="hljs-keyword">import</span> jakarta.validation.constraints.NotEmpty;
<span class="hljs-keyword">import</span> org.hibernate.validator.constraints.Length;

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> </span>{

    <span class="hljs-meta">@NotEmpty</span>
    <span class="hljs-meta">@Length(min = 2, max = 120)</span>
    <span class="hljs-meta">@JsonProperty("full_name")</span>
    <span class="hljs-keyword">private</span> String fullName;

    <span class="hljs-meta">@Email</span>
    <span class="hljs-meta">@JsonProperty("email")</span>
    <span class="hljs-keyword">private</span> String email;

    <span class="hljs-meta">@NotEmpty</span>
    <span class="hljs-meta">@Length(min = 8, max = 50)</span>
    <span class="hljs-meta">@JsonProperty("phone_number")</span>
    <span class="hljs-keyword">private</span> String phoneNumber;


    <span class="hljs-meta">@JsonProperty("country_iso")</span>
    <span class="hljs-keyword">private</span> String countryISO;

    <span class="hljs-comment">// getters and setters omitted ...</span>
}
</code></pre>
<p>And a Spring Boot controller that just validates the data and returns it</p>
<pre><code class="lang-java"><span class="hljs-comment">// imports omitted for brevity</span>

<span class="hljs-meta">@RestController</span>
<span class="hljs-meta">@RequestMapping("/people")</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PeopleController</span> </span>{

    <span class="hljs-meta">@PostMapping</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> ResponseEntity&lt;Person&gt; <span class="hljs-title">handlePost</span><span class="hljs-params">(<span class="hljs-meta">@Validated</span> <span class="hljs-meta">@RequestBody</span> Person request)</span> </span>{
        <span class="hljs-keyword">return</span> ResponseEntity.ok(request);
    }
}
</code></pre>
<p>We want our clients to send us data that looks like this</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"full_name"</span>: <span class="hljs-string">"John Banda"</span>,
  <span class="hljs-attr">"phone_number"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"email"</span>: <span class="hljs-string">"john.banda@example.com"</span>,
  <span class="hljs-attr">"country_iso"</span>: <span class="hljs-string">"MWI"</span>
}
</code></pre>
<p>Now the client sends a request with invalid data (let's assume it's missing the <code>full_name</code> and <code>phone_number</code> fields) and the API, like any good API, returns a good ol' <code>400 Bad Request</code> to let them know the error of their ways. To figure out how they may have screwed that request up, the client checks the response body and discovers the following:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2023-09-27T20:22:29.627+00:00"</span>,
    <span class="hljs-attr">"status"</span>: <span class="hljs-number">400</span>,
    <span class="hljs-attr">"error"</span>: <span class="hljs-string">"Bad Request"</span>,
    <span class="hljs-attr">"path"</span>: <span class="hljs-string">"/people"</span>
}
</code></pre>
<p>From the status code, they can guess it's an error on their end, but the response doesn't show if it's a validation error or something else wrong with the request data. Now the client isn't sure if they sent the request with the rightly named fields in the first place or if it's our API that's gone off the Rails (if there is a pun here, it is intended ;).</p>
<h2 id="heading-sending-validation-error-response">Sending validation error response</h2>
<p>We would rather give the client a bit more information about why their request is bad. A good way to do that is to catch the validation errors and return a <code>400 BadRequest</code> with actual details about the validation errors. Let's create a ValidationErrorsResponse class with the following code:</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ValidationErrorsResponse</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> code;
    <span class="hljs-keyword">private</span> String message;
    <span class="hljs-keyword">private</span> Map&lt;String, List&lt;String&gt;&gt; validationErrors;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ValidationErrorsResponse</span><span class="hljs-params">(<span class="hljs-keyword">int</span> code, String message)</span> </span>{
        <span class="hljs-keyword">this</span>.code = code;
        <span class="hljs-keyword">this</span>.message = message;
        <span class="hljs-keyword">this</span>.validationErrors = <span class="hljs-keyword">new</span> HashMap&lt;&gt;();
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">put</span><span class="hljs-params">(String field, String error)</span> </span>{
        validationErrors.computeIfAbsent(field, s -&gt; <span class="hljs-keyword">new</span> ArrayList&lt;&gt;()).add(error);
    }

    <span class="hljs-comment">// getters and setters omitted ...   </span>
}
</code></pre>
<p>Now we will need to register an Exception handler for the validation error which we can register against a <code>MethodArgumentNotValidException</code></p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExceptionHandlers</span> </span>{

    <span class="hljs-meta">@ResponseStatus(HttpStatus.BAD_REQUEST)</span>
    <span class="hljs-meta">@ExceptionHandler(MethodArgumentNotValidException.class)</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> ResponseEntity&lt;ValidationErrorsResponse&gt; <span class="hljs-title">handleExceptions</span><span class="hljs-params">(MethodArgumentNotValidException ex)</span> </span>{
        <span class="hljs-keyword">final</span> List&lt;FieldError&gt; fieldErrors = ex.getBindingResult().getFieldErrors();

        ValidationErrorsResponse errorResponse = <span class="hljs-keyword">new</span> ValidationErrorsResponse(
                HttpStatus.BAD_REQUEST.value(),
                HttpStatus.BAD_REQUEST.getReasonPhrase()
        );

        <span class="hljs-keyword">for</span> (FieldError fieldError : fieldErrors) {
            errorResponse.put(fieldError.getField(), fieldError.getDefaultMessage());
        }

        <span class="hljs-keyword">return</span> ResponseEntity.badRequest().body(errorResponse);
    }
}
</code></pre>
<p>It's at this point that if we try to send a POST with an empty or invalid object <code>{}</code> to the <code>/people</code> endpoint, we will get a response similar to the one below, which as you can see shows the fields in <code>camelCase</code> form.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"code"</span>: <span class="hljs-number">400</span>,
    <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Bad Request"</span>,
    <span class="hljs-attr">"validationErrors"</span>: {
        <span class="hljs-attr">"fullName"</span>: [
            <span class="hljs-string">"must not be empty"</span>
        ],
        <span class="hljs-attr">"phoneNumber"</span>: [
            <span class="hljs-string">"must not be empty"</span>
        ]
    }
}
</code></pre>
<p>While this works and in other situations would be sufficient, it is not great for us because the client will now get confused about the naming of the fields; the validation error response shows the fields named in <code>camelCaseForm</code> whereas the request data is expected to be sent in <code>snake_case_form</code>. The very problem we want to avoid!</p>
<p>We have some possible solutions to fix this issue, explained below.</p>
<h2 id="heading-solution-1-rename-the-class-fields-to-the-snake-case-form">Solution 1: Rename the class fields to the snake case form</h2>
<p>So one quick solution to this problem is the naming of the fields, you can just change it to match the <code>@JsonProperty</code> value. This has the downside that your linter or co-worker may shout at you for naming fields "out-of-standard" but fixes the issue without any over-engineering required. The con is that you have to name all the fields in all your other API objects in this way, so don't forget!</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> </span>{

    <span class="hljs-meta">@NotEmpty</span>
    <span class="hljs-meta">@Length(min = 2, max = 120)</span>
    <span class="hljs-meta">@JsonProperty("full_name")</span>
    <span class="hljs-keyword">private</span> String full_name;

    <span class="hljs-meta">@Email</span>
    <span class="hljs-meta">@JsonProperty("email")</span>
    <span class="hljs-keyword">private</span> String email;

    <span class="hljs-meta">@NotEmpty</span>
    <span class="hljs-meta">@Length(min = 8, max = 50)</span>
    <span class="hljs-meta">@JsonProperty("phone_number")</span>
    <span class="hljs-keyword">private</span> String phone_number;


    <span class="hljs-meta">@JsonProperty("country_iso")</span>
    <span class="hljs-keyword">private</span> String country_iso;

    <span class="hljs-comment">// getters and setters omitted ...</span>
}
</code></pre>
<h2 id="heading-solution-2-improve-the-exception-handler-to-maintain-standards">Solution 2: Improve the exception handler to maintain standards</h2>
<p>Another approach to this is to do a little "over-engineering" to solve the problem in a way that maintains your strongly held opinions and standards while ensuring that the client will never be confused about what's what. This approach involves putting a bit more code in the exception handler to check if the field we are dealing with has a <code>@JsonProperty</code> annotation and then uses the <code>value()</code> from that annotation as the field name when building up validation errors, if we can't find the field, we fall back to sending the regular Bean name of the field/property.</p>
<p>We don't have to make any changes to the names of the fields on the Person or any other class. Here is how we could solve the problem with this approach:-</p>
<pre><code class="lang-java">    <span class="hljs-meta">@ResponseStatus(HttpStatus.BAD_REQUEST)</span>
    <span class="hljs-meta">@ExceptionHandler(MethodArgumentNotValidException.class)</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> ResponseEntity&lt;ValidationErrorsResponse&gt; <span class="hljs-title">handleExceptions</span><span class="hljs-params">(MethodArgumentNotValidException ex)</span> </span>{
        <span class="hljs-keyword">final</span> List&lt;FieldError&gt; fieldErrors = ex.getBindingResult().getFieldErrors();

        ValidationErrorsResponse errorResponse = <span class="hljs-keyword">new</span> ValidationErrorsResponse(
                HttpStatus.BAD_REQUEST.value(),
                HttpStatus.BAD_REQUEST.getReasonPhrase()
        );

        <span class="hljs-keyword">for</span> (FieldError fieldError : fieldErrors) {
            <span class="hljs-keyword">try</span> {
                <span class="hljs-keyword">var</span> beanClazz = ex.getTarget().getClass();
                <span class="hljs-keyword">var</span> field = beanClazz.getDeclaredField(fieldError.getField());
                <span class="hljs-keyword">var</span> jsonPropertyAnnotation = field.getAnnotation(JsonProperty.class);
                <span class="hljs-keyword">if</span> (jsonPropertyAnnotation != <span class="hljs-keyword">null</span>) {
                    errorResponse.put(jsonPropertyAnnotation.value(), fieldError.getDefaultMessage());
                } <span class="hljs-keyword">else</span> {
                    LoggerFactory.getLogger(getClass()).debug(<span class="hljs-string">"Failed to get annotated field at {}"</span>, fieldError.getField());
                    errorResponse.put(fieldError.getField(), fieldError.getDefaultMessage());
                }
            } <span class="hljs-keyword">catch</span> (NoSuchFieldException e) {
                LoggerFactory.getLogger(getClass()).error(<span class="hljs-string">"Failed to get annotated field"</span>, e);
                errorResponse.put(fieldError.getField(), fieldError.getDefaultMessage());
            }
        }

        <span class="hljs-keyword">return</span> ResponseEntity.badRequest().body(errorResponse);
    }
</code></pre>
<h2 id="heading-solution-3-use-a-consistent-naming-convention-ie-camelcase">Solution 3: Use a consistent naming convention (i.e. camelCase)</h2>
<p>Another option is to update your API data fields + docs to use <code>camelCasedFields</code> as a standard and avoid compromising on Java bean standards and introducing inconsistencies. This is probably not the most practical solution if your API is already used by clients in production unless you manage the change/upgrade to a new version very well. You may also face resistance from API clients who insist on dealing with fields named in <code>snake_case_form</code>.</p>
<p>There is no code for this section because the solution requires communication and coordination with your teams and clients, which you cannot code around. YMMV.</p>
<hr />
<p>Thanks for reading.</p>
<p>Ohh, I promised to tell you which solution I went with. I went for Solution 1 as a quick fix <s>but after reviewing the other API objects + endpoints in our code, I applied Solution 2 which helped avoid renaming a whole bunch of API object classes</s>. Solution 2 works well with simple flat objects and isn't robust for nested structures. Will write a follow-up soon.</p>
]]></content:encoded></item><item><title><![CDATA[Hiding sensitive information when the screen loses focus with React]]></title><description><![CDATA[I don't remember what prompted me to think of this, but basically, I was uncomfortable that sometimes people leave their screens unlocked with potentially sensitive information displayed on their screens. As a developer, I set out to think of a solut...]]></description><link>https://code.zikani.me/hiding-sensitive-information-with-react</link><guid isPermaLink="true">https://code.zikani.me/hiding-sensitive-information-with-react</guid><category><![CDATA[React]]></category><category><![CDATA[library]]></category><category><![CDATA[sensitive data]]></category><dc:creator><![CDATA[Zikani Nyirenda Mwase]]></dc:creator><pubDate>Sat, 19 Aug 2023 22:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693150302680/74694ca3-35fc-4934-ae81-bb2e2180949d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I don't remember what prompted me to think of this, but basically, I was uncomfortable that sometimes people leave their screens unlocked with potentially sensitive information displayed on their screens. As a developer, I set out to think of a solution that could address this.</p>
<p>Although I appreciate that it will most likely not be implemented in a lot of places it did give me an excuse to try something sort of new to me; I had never built a standalone React library, so it was a good excuse to finally do that.</p>
<h2 id="heading-enter-react-frostedglass">Enter <code>react-frostedglass</code></h2>
<p>You may forgive the corny name. <a target="_blank" href="https://github.com/zikani03/react-frostedglass">react-frostedglass</a> is a React library that enables developers to hide potentially sensitive information when the window is not in focus.</p>
<p>It uses a Layout Effect to apply a blur to components when the window/tab is not in focus. Of course, this does not apply to all the components automatically, rather you as the user of the library have to wrap your components in a <code>FrostedContext</code> provider and use the <code>"frosted"</code> variants of common HTML components.</p>
<h3 id="heading-example-usage">Example Usage</h3>
<p>Let's create a simple react project with Vite to see how the library can be used, first create a Vite project and then install the library using the following commands:</p>
<pre><code class="lang-bash">$ npm create vite@latest app -- --template react

$ <span class="hljs-built_in">cd</span> app

$ npm install https://github.com/zikani03/react-frostedglass.git
</code></pre>
<p>Once that is done, replace the <code>App.jsx</code> with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> { FrostedContext, useFrostedEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'@zikani03/react-frostedglass'</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'./App.css'</span>
<span class="hljs-keyword">import</span> Account <span class="hljs-keyword">from</span> <span class="hljs-string">'./Account'</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> focusCheckInterval = <span class="hljs-number">500</span>;
  <span class="hljs-keyword">const</span> [blurSize, isFrosted] = useFrostedEffect(focusCheckInterval, <span class="hljs-string">'0.3em'</span>)

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">FrostedContext.Provider</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">blurSize:</span> <span class="hljs-attr">blurSize</span> }}&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Account</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">FrostedContext.Provider</span>&gt;</span></span>
  )
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App
</code></pre>
<p>As you can see above, we use the <code>useFrostedEffect</code> hook and we pass it two parameters, the interval (in milliseconds) to be used to check if the window is in focus and the size of the blur - a valid value for this is anything you can use in the <code>filter: blur()</code> CSS property.</p>
<p>Now create a new component in a file named <code>Account.jsx</code>. Use the code listed below:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {FrostedDiv, FrostedSpan, FrostedLabel, withFrost} <span class="hljs-keyword">from</span> <span class="hljs-string">'@zikani03/react-frostedglass'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Account</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> user = {
        <span class="hljs-attr">email</span>: <span class="hljs-string">'user@example.com'</span>,
        <span class="hljs-attr">phone</span>: <span class="hljs-string">'+265-xxx-xxx-xxx'</span>,
        <span class="hljs-attr">address</span>: <span class="hljs-string">'Address 1, City, Country'</span>
    };

    <span class="hljs-keyword">return</span> (
        <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"field"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>Email<span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">FrostedSpan</span>&gt;</span>{user.email}<span class="hljs-tag">&lt;/<span class="hljs-name">FrostedSpan</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>Phone<span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">FrostedDiv</span>&gt;</span>{user.phone}<span class="hljs-tag">&lt;/<span class="hljs-name">FrostedDiv</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">strong</span>&gt;</span>Address<span class="hljs-tag">&lt;/<span class="hljs-name">strong</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">FrostedDiv</span>&gt;</span>{user.address}<span class="hljs-tag">&lt;/<span class="hljs-name">FrostedDiv</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">section</span> {<span class="hljs-attr">...withFrost</span>({ <span class="hljs-attr">blurSize:</span> '<span class="hljs-attr">0.3em</span>' })}&gt;</span>
                Some section that's frosted by default...
            <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
    )
}
</code></pre>
<p>As you can see above, the Account component uses FrostedDiv, FrostedSpan for the content that we want to blur when the window is out of focus</p>
<h3 id="heading-lets-see-how-frosted-components-look">Let's see how Frosted components look</h3>
<p>We can now run the development server to see how all this looks in action</p>
<pre><code class="lang-bash">$ npm run dev
</code></pre>
<p>When we focus on the page we will see everything displaying as normal, except the <code>section</code> element we set to be blurred by default:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693150744691/15fa103c-8b7c-4312-b81b-24a2af539eec.png" alt class="image--center mx-auto" /></p>
<p>Now try opening or focusing on another window and you will observe that the elements become "frosted". How cool is that? 🥶</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693150800304/5798971a-edce-4a54-81b4-199f0aef8760.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-using-the-library">Using the library</h2>
<p>As of the time of writing, I have not published a package to NPM. So if you want to use the library you can install the module directly from GitHub using npm</p>
<pre><code class="lang-bash">npm install --save https://github.com/zikani03/react-frostedglass.git
</code></pre>
<h2 id="heading-observations-and-lessons-learned">Observations and lessons learned</h2>
<p>As mentioned previously, one of my objectives was to learn or at least figure out how to go about creating a standalone React library from scratch. I am glad I was able to accomplish that and here are a few observations:</p>
<ul>
<li><p>Coming up with a nice API/library surface is not always easy, I enjoy creating developer tools and although I have written a <a target="_blank" href="https://github.com/creditdatamw/zerocell">few</a> <a target="_blank" href="https://github.com/zikani03/pgadvisorylock">libraries</a> in various languages I was reminded that picking the right abstractions, and naming things (functions, class etc...) requires lots of thought.</p>
</li>
<li><p>I came to appreciate the use of Context Providers and Layout Effect hooks better</p>
</li>
<li><p>I was able to configure Vite for building the library "from scratch", that is without depending on a template repository or some kind of boilerplate.</p>
</li>
<li><p>Related to the point above, I was able to customize the TypeScript compilation options - of course, I had done this before for another <a target="_blank" href="https://github.com/zikani03/ika/">JavaScript library</a> I built (you should check that out too, by the way!)</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, we have seen how you can use <code>react-frostedglass</code>, a React library I created, to blur components when the screen loses focus. Although this might not be the most practical approach to the problem, it did allow me to figure out how to publish a standalone React library and appreciate the process of building one.</p>
<p>Thank you for reading.</p>
]]></content:encoded></item></channel></rss>