Ncog.link devlog 2: Building out CRUD

For those not aware of what Create Read Update Delete is, it’s the ability to do the basic tasks necessary to manage data in a database. There are other actions than CRUD, but at the most basic level, there is usually some form of data that is just simple CRUD for almost every aspect of designing applications and databases.

It does feel a little strange talking about CRUD in the context of game development, so I feel like today’s post will is best served by giving an overview of my philosophy of software development.

In my past lives, I’ve grown to understand the importance of being able to quickly build out new database structures and be able to create a simple administrative user interface for that information. At one job, my boss was a Sharepoint wizard and would pre-configure Sharepoint forms that back-office administrators could use to set up the data structures that my business logic would consume. I was always fascinated by this concept. As I built more and more web applications over the years, I realized how much boilerplate there is around CRUD, and scaffolding is only effective at standing up interfaces quickly, not at making them easy to maintain long term. The Sharepoint setup provided an interesting approach of allowing my boss to quickly standup the CRUD and allow me to focus on the business logic. I want developing my application to feel like that – the CRUD should be easy to implement and maintain, so that I’m focused on writing the actual logic that matters.

Over the years, I’ve been through multiple website redesigns, and updating classes and sometimes DOM structures across large projects is painful. HTML today is much more closely structured to match the data itself rather than the styling, but the two are still very much intertwined. To me, the best design is to force yourself into using a component-driven framework and make sure that you reuse components as much as possible, and to abstract the nuances of any CSS framework to as few locations as possible.

So, while it’s elementary to quickly throw together something that works for a single admin, what I was missing was the components to make creating 100 of those interfaces easy. Let’s do a little overview of what I have created so far:

Basic stats of this pull request currently are: 3,589 additions, 253 deletions. In hindsight, I should have split chunks of work into individual tickets so that I could merge more incrementally.

Basic CSS Structure + Forms

From my previous work on another project named ncog as well (I liked that name), I had already written some Yew components. I’ve done many updates to them and added a few new essential components like Title, Modal, Alert, Flash (Momentary notifications like “Save Succeeded”), and a Radio Button form control.

The CSS framework I’m using is Bulma. The Flash component I’m particularly happy with, here’s an example:

// in update()
self.flash_message = Some(flash::Message::new(
    flash::Kind::Danger,
    message,
    Duration::from_secs(3),
));
// in view()
<Flash message=self.flash_message.clone() />

These easy-to-use components make it easier to build forms with more functionality. Still, more importantly, by using rich base-level components, each form will support a wide range of good user-experience out of the box.

Validations

As I noted in my last update, I wanted to investigate updating a crate known as yew_form. Upon trying to update it, I realized I was just not ready to dive into procedural macros in Rust. I also felt like that, while a proc macro should be able to reduce boilerplate, I didn’t think they were strictly needed. I decided to go my own way.

I wanted my validation system to be tightly tied into my form system, and I’m moderately happy with the current way this functions:

let errors = ModelValidator::default()
            .with_field(RoleFields::Name, self.name.is_present())
            .validate();
... 
<Field<RoleFields> field=RoleFields::Name errors=errors.clone()>
  ...
</Field<RoleFields>>
...

This snippet shows how a simple validation works. Each form control takes an ErrorSet, and the individual field errors display next to each control. I have the system built, but I’m missing many validations. Right now, all I have is checking if a value is present and whether it’s a valid numeric input if the field is a numerical type. Additionally, I need to add support for multi-field validations – for example, a field might only be able to be specified if another field is a particular value.

Localization

While I have no immediate plans to offer translations of ncog.link, I knew that one day I would. And more importantly, I knew I wanted creators to be able to identify their content as being specific to certain languages and locales. I started noticing me hard coding text such as “Edit Role”, and I knew I needed to get my localization support built-in so that localization was easy in the future, not hard.

I’m taking advantage of the excellent Project Fluent, which allows for some great approaches at localization that help maintain localizations over time. I still have more work to do – right now, the localization support I have is purely for the ncog.link webapp and game client itself. To support localized content, I plan on using Project Fluent, but I will need to write some sort of interface to store the resources in a database. I have some great ideas, all stemming from my previous attempt at writing a localization editor at my last job and never having enough time ever to go back and apply what I had learned in the past ten years.

An example is this snippet where I map an error into a localized message to display to the user:

// in en-US/admin.ftl
-name = Name
form-field-required = {$field} is required
role-fields-name = {-name}

// in edit_form.rs
localize!("form-field-required", "field" => e.primary_field().localized_name())

For those who aren’t well-versed in localization challenges, this example shows a straightforward one. Not all languages keep Subject and Verb order. For example, “Name is required” may become “Se requiere el nombre” in Spanish. If I offered a Spanish translation, it might look like this:

// in es-MX/admin.ftl
-name = Nombre
form-field-required = Se requiere {$field}
role-fields-name = {-name}

And how does the field get translated? Well, I made a Trait:

pub trait Namable {
    fn name(&self) -> &'static str;
    fn localized_name(&self) -> String {
        localize!(&self.name())
    }
}

and I implemented it for my RoleFields enum:

impl Namable for RoleFields {
    fn name(&self) -> &'static str {
        match self {
            Self::Name => "role-fields-name",
            ...

Now, if you pay close attention, that isn’t entirely correct for the translation, but rest assured that Project Fluent has all of the tools necessary to make the adjustments needed.

PubSub

For Cosmic Cantina, I asked the question, “How much latency does PostgreSQL’s built-in PubSub have?” If I could avoid yet another hosted service cost, that seemed better. After doing some searching, it appeared many people were using the NOTIFY functionality with great success and under high-performance requirements. Will it be as fast as ? It might not, but based on my research and limited testing, I’m encouraged.

The architecture of ncog.link is currently set up so that a load balancer routes traffic through one or more running instances of ncog.link (presently configured to 2 instances). This means that if you have two windows logged into ncog.link in two browser windows, you might be connected to two separate server processes.

When you update information on one screen, I wanted my application architecture to support full-interactivity and automatically update any other viewers of the data that was changed. I was already using this to allow multiple browser tabs to log in at the same time upon one OAuth redirect by notifying all connections for that specific browser “installation id” that they now are logged in.

Today, I tackled a slightly different flow, but due to the component-based architecture of Yew, after I hooked everything together, it all just worked smoothly:

This clip shows me executing a short SQL statement to flip the permission allowing me to edit the role being displayed in the window in the background. As soon as the notification is sent, the UI updates and enables and disables the components automatically.

There’s just something so rewarding of seeing all of these pieces coming together and working seamlessly.

Where to next?

As the picture of the tracking issue showed, I still have some more work left. I also want to spend some time cleaning up the code I’ve built over the last couple of weeks. There are a few more areas of which I’m still not quite loving my design. After that, I’m going to be working on starting to build out the Asset Libraries feature, probably starting with Text assets.

I didn’t end up streaming much this past week. I decided I wanted to try out Linux, and in the process, I accidentally ended up trashing my Windows partition. Never fear, I practice what I preach: backup everything and use version control. I lost about 20 minutes of code in the end, no big deal – the real time sink is going to be reinstalling Windows if I do.

So far I’ve been happy with my experiment, but I just haven’t been quite in the right mood to stream the last few days. I’m hoping to get a few days of streaming in this coming week, and maybe start looking at figuring out a schedule to try to stick to.

Until next week, feel free to reply here with any questions or comments or jokes, or join me on Discord. Have a great week!