Swagg::Blogg

or: How I Learned to Stop Worrying and Love the WWW
Home RSS

Previously I wrote that I need to plumb my web app to a DB and was told that DBIish was the tool for the job. I was glad to see it has a migrations module to go along with it. I'll go ahead and add my first migration which will create the table for a pastebin. These simply consist of SQL statements in a file I creatively named 'migrations':

-- 1 up
CREATE TABLE IF NOT EXISTS pastes (
         paste_id SERIAL PRIMARY KEY,
       paste_body TEXT
       );

-- 1 down
DROP TABLE pastes;

Now I'll need to connect to my DB. I honestly didn't have time to figure out how I'll pass this object to my model module(s) so for now I'm using a dynamic variable and rather than reading the credentials from a file I'll just use prompt() for now:

my $*dbh = DBIish.connect:
    'Pg',
    :host<devbussy.swagg.net>,
    :database<pastes_bin>,
    :user<pastes_bin>,
    password => prompt 'enter DB password: ';

my $m = DB::Migration::Simple.new:
    :$*dbh,
    :migration-file<migrations>;

$m.migrate: :version<1>;

Now if we run the web app we get prompted for the DB password and then we can see the migration be applied:

$ ./bin/pastes-bin
enter DB password: hogrider69
line: -- 1 up
version: 1, direction: up
line: CREATE TABLE IF NOT EXISTS pastes (
line:          paste_id SERIAL PRIMARY KEY,
line:        paste_body TEXT
line:        );
line: -- 1 down
version: 1, direction: down
line: DROP TABLE pastes;
initializing db-migrations-simple-meta
set initial version to 0
{1 => {down => DROP TABLE pastes;
, up => CREATE TABLE IF NOT EXISTS pastes (
         paste_id SERIAL PRIMARY KEY,
       paste_body TEXT
       );
}}
migrating from version '0' to version '1'
True migrating 'up' from version '0' to version '1'
doing 'up' migrations for 1
executing CREATE TABLE IF NOT EXISTS pastes (
         paste_id SERIAL PRIMARY KEY,
       paste_body TEXT
       )
Humming-Bird listening on port http://localhost:3000

We can check the DB now and see our new table applied along with the metadata for the migrations module itself:

$ psql -h devbussy.swagg.net -U pastes_bin
Password for user pastes_bin: 
psql (15.3 (Debian 15.3-0+deb12u1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

pastes_bin=> \d
                     List of relations
 Schema |           Name            |   Type   |   Owner    
--------+---------------------------+----------+------------
 public | db-migrations-simple-meta | table    | pastes_bin
 public | pastes                    | table    | pastes_bin
 public | pastes_paste_id_seq       | sequence | pastes_bin
(3 rows)

pastes_bin=> \d pastes;
                                 Table "public.pastes"
   Column   |  Type   | Collation | Nullable |                 Default                  
------------+---------+-----------+----------+------------------------------------------
 paste_id   | integer |           | not null | nextval('pastes_paste_id_seq'::regclass)
 paste_body | text    |           |          | 
Indexes:
    "pastes_pkey" PRIMARY KEY, btree (paste_id)

Now I wanted to have separate model and controller modules but I'm still working on how to pass $dbh around sanely so for now I'm going to just add this to test:

use Pastes-Bin::Model::Paste;

# No routes yet just prompt to 'fake it'
my $new-paste = prompt 'enter a new paste: ';
Pastes-Bin::Model::Paste.create: $new-paste;

Here's the model code:

unit class Pastes-Bin::Model::Paste;

submethod create(Str $new-paste) {
    $*dbh.execute(q:to/END_SQL/, $new-paste)
        INSERT INTO pastes (paste_body)
        VALUES (?);
       END_SQL
}

And now we run it again:

$ ./bin/pastes-bin 
enter DB password: hogrider69
line: -- 1 up
version: 1, direction: up
line: CREATE TABLE IF NOT EXISTS pastes (
line:          paste_id SERIAL PRIMARY KEY,
line:        paste_body TEXT
line:        );
line: -- 1 down
version: 1, direction: down
line: DROP TABLE pastes;
current-version: allrows: [[1]]
{1 => {down => DROP TABLE pastes;
, up => CREATE TABLE IF NOT EXISTS pastes (
         paste_id SERIAL PRIMARY KEY,
       paste_body TEXT
       );
}}
migrating from version '1' to version '1'
DB already at version 1
enter a new paste: testing 123...
Humming-Bird listening on port http://localhost:3000
^C

And now in the DB:

pastes_bin=> SELECT * FROM pastes;
 paste_id | paste_body 
----------+------------
(0 rows)

pastes_bin=> SELECT * FROM pastes;
 paste_id |   paste_body   
----------+----------------
        1 | testing 123...
(1 row)

It's nice to see I can talk to a DB with ease with DBIish and it handles migrations. I need to next dig into how I can organize my code in Raku where $dbh can be 'passed around'. I tried creating a separate controller module that calls the model class but in that configuration it did not see the $*dbh dynamic variable... If/when I figure that out then that'll be the next blog entry on my journey into Raku.

#webDev #noScript #RakuLang

Now that my little textboard project is in a decent state I've begun thinking about what I wanted to try next. I knew it'll be some sort of webbed site and there'd be a database but other than that it was up in the air. Mojo.js appealed to me for obvious reasons but honestly I found the Node/JavaScript conventions and ecosystem pretty intimidating. I've never been any good at JS and the whole front-end thing is an enigma to me, as is probably obvious if you've seen my prior art.

Thankfully I discovered Humming-Bird which I like because it reminds me of Mojo or Sinatra. It's also a great excuse to use Raku, a language I also find intimidating but at least feels more 'fun' and less 'enterprise' than JS or even Perl/Ruby. If you already have a nice little CPAN/Perl set-up then getting Raku is easy enough:

cpanm App::Rakubrew && rakubrew download

If not then Debian ships it:

apt install rakudo

Finally we can use zef to install Humming-Bird:

zef install Humming-Bird

Humming-Bird is quite minimal like Sinatra is so I want to install something for templates. Template::Mustache was recommended to me after I had trouble getting others to work:

zef install Template::Mustache

To get started let's import Humming-Bird::Core and our template module; I'm going to keep my template in a directory called templates:

#!/usr/bin/env raku

use v6.d;
use Humming-Bird::Core;
use Template::Mustache;

my $template = Template::Mustache.new: :from<./templates>;

I'll go ahead and throw in a basic template named index.mustache:

<!DOCTYPE html>
<html>
<head>
  <title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>
<p>We will we will... RAKU!!</p>
</body>
</html>

To render this template we only need to add the following to our script, I've named mine basic-site.raku if you're playing along at home:

get '/', -> $request, $response {
    my Str %stash = title => 'Hello, web!';

    $response.html($template.render: 'index', %stash);
};

listen 3000;

That is literally all you need to take flight:

$ curl -i http://localhost:3000
HTTP/1.1 200 OK
Content-Length: 144
Date: Fri, 10 Nov 2023 02:29:44 +0000
X-Server: Humming-Bird (Raku)
content-type: text/html; charset=utf8

<!DOCTYPE html>
<html>
<head>
  <title>Hello, web!</title>
</head>
<body>
<h1>Hello, web!</h1>
<p>We will we will... RAKU!!</p>
</body>
</html>

This is pretty decent as-is but if there's anything I create more than silly websites, it's compiler errors and warnings. I'd like them to be more verbose so I can also add this:

# Add the 'middleware' and 'advice' parts
use Humming-Bird::Middleware;
use Humming-Bird::Advice;

# All you need to use 'em out-of-the-box
middleware &middleware-logger;
advice     &advice-logger;

Our entire script now looks like this:

#!/usr/bin/env raku

use v6.d;
use Humming-Bird::Core;
use Humming-Bird::Middleware;
use Humming-Bird::Advice;
use Template::Mustache;

middleware &middleware-logger;
advice     &advice-logger;

my $template = Template::Mustache.new: :from<./templates>;

get '/', -> $request, $response {
    my Str %stash = title => 'Hello, web!';

    $response.html($template.render: 'index', %stash);
};

listen 3000;

You can take a better look at it in my git repo along with my other Raku experiments so far. I'm glad Humming-Bird arrived on the scene as it's left me with no more excuses to not at least try Raku and so far I'm really diggin it. I've found the best sources of Raku halp to be Reddit and their Discord (which is bridged to IRC as well); you can get your hyperlinks on their website.

#webDev #noScript #RakuLang

So there's these <meta> elements by Meta. Well not the <meta> element itself... But Open Graph. I wanted my links to look like this when I spam them in Discord:

A link preview of an article from web3isgoinggreat.com

Remember that scene from American Psycho where Christian Bale is sweating over a business card?

So anyways, I look at the HTML and see these <meta> tags. That's gotta be it, I mean I see these then some tags that say something about Twitter and I don't want Twitter since I'm really actually not a terrible person once you get to know me so I guess I'll implement these tags. The image seems easy enough, throw it in a Mojo template but what's annoying is I have my <head> entirely in the layout rather than the template itself. I first considered a partial template like I've seen in Rails before but Mojo couldn't find the partial. I think this was due to me just using a single layout for all my templates associated with different controllers. My site is pretty simple so honestly I didn't want to copy the same layout for every controller.

So I was left to hit the docs looking for some sort of feature or something that could help me out here. Finally I settled on the content_for() helper. In my layout I put the following:

<head>
  <!-- Some stuff here... -->
  <%= content 'open_graph' =%>
  <meta property="og:url" content="<%= url_for->to_abs %>">
  <meta property="og:site_name" content="Post::Text">
  <meta property="og:image"
        content="<%= url_for('/images/logo.png')->to_abs %>">
  <meta property="og:image:type" content="image/png">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="1200">
  <meta property="og:image:alt"
        content="Post::Text logo; a small nerdy anime girl giving a V sign">
</head>

Now in my templates I simply do:

% layout 'default';
% title 'New Thread';
% content_for open_graph => begin
  <meta property="og:type" content="website">
  <meta property="og:title" content="<%= title %>">
  <meta property="og:description" content="Start a new thread.">
% end
<h2 class="page-title"><%= title %></h2>
<!-- Begin the rest of the page here... -->

Well that oughta do it! I mean I even made my image 1200 pixels wide, this oughta look breathtaking...

A URL preivew of posttext.pl with a smaller image preview

That's... A URL preview but why is my image so smol? It should be 4K ultra-HD like Molly White's site! I mean yeah there's these Twitter tags but why would it use those? There's not even a 'site name' and I clearly see them in both our previews so it must be using Open Graph. Unless... It's using both. But there's no way it'd do that I mean are the Twitter tags even considered a standard? Hell, is Open Graph even a standard? Out of desperation I try...

% content_for twitter_card => begin
  <meta name="twitter:title" content="<%= title %>">
  <meta name="twitter:description" content="Start a new thread.">
% end

And back to the layout:

<%= content 'twitter_card' =%>
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:creator" content="@swaggboi@eattherich.club">
<meta name="twitter:site"
      content="@posttext@fedi.seriousbusiness.international">
<meta name="twitter:image"
      content="<%= url_for('/images/logo.png')->to_abs %>">
<meta name="twitter:image:alt"
      content="Post::Text logo; a small nerdy anime girl giving a V sign">

Well, what's it gunna be Discord? Make my day...

The same link to posttext.pl but now the image is large

😳

Alright then... It's got the site title from Open Graph... And the image from the stupid Twitter tags. I guess. Cool.

Well anyways that's done and Post::Text is finally live so I'm happy. On to the next harebrained project! 🫡

P.S. I know it's been a while since I've blogged, my last prediction is partially becoming true already 💀

#perl #mojolicious #textboard

Anyone got any New Years predictions?

I predict that “human-generated” code will be deprecated as unsafe in favor of AI-generated code. Layoffs and record profits will follow.

I actually normally wouldn't enjoy this but thankfully this Mojo::Pg wrapper makes it easy. I honestly don't have any experience with the original DBD::Pg as I'm still very new to the database world so I hope this doesn't read too much like I'm lost in the sauce. First I took a look at my current tables using the psql tool:

post_text=> \d
                   List of relations
 Schema |         Name          |   Type   |   Owner
--------+-----------------------+----------+-----------
 public | mojo_migrations       | table    | post_text
 public | replies               | table    | post_text
 public | replies_reply_id_seq  | sequence | post_text
 public | threads               | table    | post_text
 public | threads_thread_id_seq | sequence | post_text
(5 rows)

post_text=> \d replies;
                                            Table "public.replies"
     Column     |           Type           | Collation | Nullable |                  Default
----------------+--------------------------+-----------+----------+-------------------------------------------
 reply_id       | integer                  |           | not null | nextval('replies_reply_id_seq'::regclass)
 thread_id      | integer                  |           |          |
 reply_date     | timestamp with time zone |           | not null | now()
 reply_author   | character varying(64)    |           |          |
 reply_body     | character varying(4096)  |           |          |
 hidden_status  | boolean                  |           | not null |
 flagged_status | boolean                  |           | not null |
Indexes:
    "replies_pkey" PRIMARY KEY, btree (reply_id)
Foreign-key constraints:
    "replies_thread_id_fkey" FOREIGN KEY (thread_id) REFERENCES threads(thread_id)

These long lines are ugly so here's a pastebin link. Essentially I need to s/reply_/remark_/g; including that sequence, index and foreign-key constraint:

-- This file is migrations/5/up.sql btw

 ALTER TABLE replies
RENAME TO remarks;

 ALTER TABLE remarks
RENAME reply_id
    TO remark_id;

 ALTER TABLE remarks
RENAME reply_date
    TO remark_date;

 ALTER TABLE remarks
RENAME reply_author
   TO remark_author;

 ALTER TABLE remarks
RENAME reply_body
    TO remark_body;

 ALTER TABLE remarks
RENAME CONSTRAINT replies_thread_id_fkey
    TO remarks_thread_id_fkey;

 ALTER INDEX replies_pkey
RENAME TO remarks_pkey;

 ALTER SEQUENCE replies_reply_id_seq
RENAME TO remarks_remark_id_seq;

I'm also going to do the exact opposite for our rolling-back pleasure:

-- This one is migrations/5/down.sql

 ALTER TABLE remarks
RENAME TO replies;

 ALTER TABLE replies
RENAME remark_id
    TO reply_id;

 ALTER TABLE replies
RENAME remark_date
    TO reply_date;

 ALTER TABLE replies
RENAME remark_author
    TO reply_author;

 ALTER TABLE replies
RENAME remark_body
    TO reply_body;

 ALTER TABLE replies
RENAME CONSTRAINT remarks_thread_id_fkey
    TO replies_thread_id_fkey;

 ALTER INDEX remarks_pkey
RENAME TO replies_pkey;

 ALTER SEQUENCE remarks_remark_id_seq
RENAME TO replies_reply_id_seq;

Idk why but I always find this step feels weird to me because it feels like I'm undoing the undo lol. But we will need it later so let's absolutely include it. Now I'm gunna see if it works...

daniel@netburst:~/git/PostText$ ./PostText.pl eval 'app->pg->migrations->from_dir("migrations")->migrate(5);'
daniel@netburst:~/git/PostText$ echo $?
0

No news is good news I guess. Let's whip out psql again and see how it looks:

post_text=> \d
                   List of relations
 Schema |         Name          |   Type   |   Owner
--------+-----------------------+----------+-----------
 public | mojo_migrations       | table    | post_text
 public | remarks               | table    | post_text
 public | remarks_remark_id_seq | sequence | post_text
 public | threads               | table    | post_text
 public | threads_thread_id_seq | sequence | post_text
(5 rows)

post_text=> \d remarks;
                                            Table "public.remarks"
     Column     |           Type           | Collation | Nullable |                  Default
----------------+--------------------------+-----------+----------+--------------------------------------------
 remark_id      | integer                  |           | not null | nextval('remarks_remark_id_seq'::regclass)
 thread_id      | integer                  |           |          |
 remark_date    | timestamp with time zone |           | not null | now()
 remark_author  | character varying(64)    |           |          |
 remark_body    | character varying(4096)  |           |          |
 hidden_status  | boolean                  |           | not null |
 flagged_status | boolean                  |           | not null |
Indexes:
    "remarks_pkey" PRIMARY KEY, btree (remark_id)
Foreign-key constraints:
    "remarks_thread_id_fkey" FOREIGN KEY (thread_id) REFERENCES threads(thread_id)

Aaaaaand the pastebin. I think we're in good shape. I'm going to migrate back for now as I still need to make some changes in the controller logic to use the new Remark model instead of my old Reply model.

daniel@netburst:~/git/PostText$ ./PostText.pl eval 'app->pg->migrations->from_dir("migrations")->migrate(4);'
daniel@netburst:~/git/PostText$ echo $?
0

I keep the migration hard-coded in the method call in the app itself just to save myself from accidentally migrating to the latest before it's ready:

# From PostText.pl
app->pg->migrations->from_dir('migrations')->migrate(4);

I know there's some reason I started doing that... I accidentally something but now I can't remember. Gunna just stick with the cargo cult and leave it be.

Next I gotta work on the aforementioned controller logic. And then moar tests.

#database #mojolicious #perl #sql #webdev

What should I do...

Is this dangerous?

So I ran into a funny little snafu last night. I finally implemented my Reply model for my little textboard project but once I did this I noticed the app no longer shows in the snazzy built-in error pages. That's bummer because I find them real helpful when running apps in development mode. I kinda gave up on this last night and looked at it again this morning and noticed the following in the console output (requesting a route that does not exist, expect a 404):

[2022-08-20 13:08:43.53717] [27345] [trace] [CgcDACZgwvD4] GET "/swagg"
Mojo::Reactor::Poll: I/O watcher failed: Can't locate object method "exception" via package "PostText::Model::Reply" at /home/daniel/perl5/lib/perl5/Mojolicious.pm line 200.

It must be trying to dereference exception from an object named reply... Let's see...

[daniel@netburst mojo]$ grep -i -r '\->reply' * | grep -i 'lite'
t/mojolicious/exception_lite_app.t:  $c->reply->exception(undef);
t/mojolicious/exception_lite_app.t:  $c->reply->exception;
t/mojolicious/exception_lite_app.t:  $c->reply->exception(Mojo::Exception->new);
t/mojolicious/lite_app.t:  $c->render_maybe('this_does_not_ever_exist') or $c->reply->static($file);
t/mojolicious/lite_app.t:get '/static' => sub { shift->reply->static('hello.txt') };
t/mojolicious/longpolling_lite_app.t:  Mojo::IOLoop->timer(0.25 => sub { $c->reply->static('hello.txt') });
t/mojolicious/static_lite_app.t:get '/hello3.txt' => sub { shift->reply->static('hello2.txt') };
t/mojolicious/static_lite_app.t:  $c->reply->static('hello2.txt');
t/mojolicious/static_lite_app.t:  $c->reply->asset($mem);
t/mojolicious/static_lite_app.t:  $c->reply->file(curfile->sibling('templates2', '42.html.ep'));

Gah! I think I can work around this I suppose by just renaming the reply helper to something other than reply but for consistency I'm going to go ahead and migrate everything to use the name Remark instead of Reply. 'Comment' would remind me too much of social media so I guess these threads are gettin remarked upon.

This, of course, means another night of databases. 😩

#perl #mojolicious #webdev #database

I used to use the flash() helper in Mojolicious for reporting errors back to the user but recently discovered I can just use Mojolicious::Validator::Validation in my templates which seems like the right tool for the job. I was already using it in my Mojolicious::Lite app like so:

$v->required('name' )->size(1, 63  );
$v->required('title')->size(1, 127 );
$v->required('post' )->size(2, 4000);

But other than changing the status to 400 I didn't realize I could use this to also report to the user that their request was invalid. The methods can be used in the templates themselves:

<form method="post">
  <div class="name field">
    <%= label_for name => 'Author' %>
    <%= text_field name =>'Anonymous', maxlength => 63, minlength => 1 %>
    <% if (my $error = validation->error('name')) { =%>
      <p class="field-with-error">Invalid name: 1 to 63 characters please.</p>
    <% } =%>
  </div>
  <div class="title field">
    <%= label_for title => 'Title' %>
    <%= text_field 'title', maxlength => 127, minlength => 1 %>
    <% if (my $error = validation->error('title')) { =%>
      <p class="field-with-error">Invalid title: 1 to 127 characters please.</p>
    <% } =%>
  </div>
  <div class="text field">
    <%= label_for post => 'Text' %>
    <%= text_area 'post', (
        maxlength => 4000,
        minlength => 2,
        required  => 'true',
        rows      => 6
    ) %>
    <% if (my $error = validation->error('post')) { =%>
      <p class="field-with-error">Invalid post: Up to 4,000 characters only.</p>
    <% } =%>
  </div>
  <%= submit_button 'Post', class => 'post button' %>
</form>

So when the user makes their initial GET request, the following HTML is rendered within the form:

<div class="name field">
  <label for="name">Author</label>
  <input maxlength="63" minlength="1" name="name" type="text" value="Anonymous">
</div>
<div class="title field">
  <label for="title">Title</label>
  <input maxlength="127" minlength="1" name="title" type="text">
</div>
<div class="text field">
  <label for="post">Text</label>
  <textarea maxlength="4000" minlength="2" name="post" required="true" rows="6"></textarea>
</div>
<input class="post button" type="submit" value="Post">

Now let's say you screw up and submit a null value for the Title in your subsequent POST request, in your response the form is rendered again like this:

<div class="name field">
  <label for="name">Author</label>
  <input maxlength="63" minlength="1" name="name" type="text" value="anon">
</div>
<div class="title field">
  <label class="field-with-error" for="title">Title</label>
  <input class="field-with-error" maxlength="127" minlength="1" name="title" type="text">
    <p class="field-with-error">Invalid title: 1 to 127 characters please.</p>
</div>
<div class="text field">
  <label for="post">Text</label>
  <textarea maxlength="4000" minlength="2" name="post" required="true" rows="6">hi</textarea>
</div>
<input class="post button" type="submit" value="Post">

The class attribute field-with-error was added to the invalid fields allowing me to decorate this to hint to the user. Then I added another little paragraph (with the same class) to make sure accessibility isn't an issue. These are definitely scenarios where I choose TagHelpers over vanilla HTML in my templates.

Also while testing all of this, I was resorting to using curl to submit invalid input since my browser won't let me thanks to attributes such as maxlength and minlength being present. Finally I realized I could do the same with Mojo's built-in commands which is awesome:

$ ./PostText.pl get -M POST -f 'name=anon' -f 'title=' -f 'post=hi' '/post'
[2022-08-15 19:28:16.84229] [76893] [trace] [jJBy5DsZGrMq] POST "/post"
[2022-08-15 19:28:16.84266] [76893] [trace] [jJBy5DsZGrMq] Routing to a callback
[2022-08-15 19:28:16.84279] [76893] [trace] [jJBy5DsZGrMq] Routing to a callback
[2022-08-15 19:28:16.84359] [76893] [trace] [jJBy5DsZGrMq] Rendering template "post.html.ep"
[2022-08-15 19:28:16.84564] [76893] [trace] [jJBy5DsZGrMq] Rendering template "layouts/main.html.ep"
[2022-08-15 19:28:16.84741] [76893] [trace] [jJBy5DsZGrMq] 400 Bad Request (0.005116s, 195.465/s)
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Post::Text - New Thread</title>
  <link href="/asset/942e7be1d2/PostText.css" rel="stylesheet">
</head>
<body>
<h1>Post::Text</h1>
<nav>
  <a href="/view">View</a>
  <a href="/post">New</a>
</nav>
<hr>
<h2>New Thread</h2>
<form method="post">
  <div class="name field">
    <label for="name">Author</label>
    <input maxlength="63" minlength="1" name="name" type="text" value="anon">
  </div>
  <div class="title field">
    <label class="field-with-error" for="title">Title</label>
    <input class="field-with-error" maxlength="127" minlength="1" name="title" type="text">
      <p class="field-with-error">Invalid title: 1 to 127 characters please.</p>
  </div>
  <div class="text field">
    <label for="post">Text</label>
    <textarea maxlength="4000" minlength="2" name="post" required="true" rows="6">hi</textarea>
  </div>
  <input class="post button" type="submit" value="Post">
</form>
<footer>
  <p>In UTF-8 we trust.</p>
</footer>
</body>
</html>

I see two ways forward on this little project. I can maybe pause now and blow this up into a full-structure Mojolicious app or I can implement my next model (involving new-to-me SQL stuff like FOREIGN KEY and JOIN). Really either of these will be new-to-me and will take some time so I'll probably try to pick the lowest hanging fruit of the two... Once I figure out what the hell that is.

#perl #mojolicious

So I used to read data into an array of arrays like so:

sub get_threads($self) {
    $self->pg->db->query(<<~'END_SQL')->arrays()
        SELECT thread_id,
               TO_CHAR(thread_date, 'Dy Mon DD HH:MI:SS AM TZ YYYY'),
               thread_author,
               thread_title,
               thread_body
          FROM threads
         WHERE NOT hidden_status
         ORDER BY thread_date DESC;
       END_SQL
}

Then I'd plop that data into templates like so:

<% for my $thread (@$threads) { =%>
  <article class="thread">
    <h3 class="title"><%= @$thread[3] %></h3>
    <h4 class="date"><%= @$thread[1] %></h4>
    <h5 class="author"><%= @$thread[2] %></h5>
    <p class="body"><%= @$thread[0] %></p>
  </article>
<% } =%>

This is already pretty cool but I kept losing track of what index number went to which data field. Then I saw that there is a hashes() method in Mojo::Pg::Results and thought... What if we used 100% of the brain? (Edit: Formatting turned out bad for this one)

sub get_threads($self) {
    $self->pg->db->query(<<~'END_SQL')->hashes()
        SELECT thread_id                                             AS id,
               TO_CHAR(thread_date, 'Dy Mon DD HH:MI:SS AM TZ YYYY') AS date,
               thread_author                                         AS author,
               thread_title                                          AS title,
               thread_body                                           AS body
          FROM threads
         WHERE NOT hidden_status
         ORDER BY thread_date DESC;
       END_SQL
}

By using SQL to assign an alias to the column names my templates now look much cleaner:

<% for my $thread (@$threads) { =%>
  <article class="thread">
    <h3 class="title"><%= %$thread{'title'} %></h3>
    <h4 class="date"><%= %$thread{'date'} %></h4>
    <h5 class="author"><%= %$thread{'author'} %></h5>
    <p class="body"><%= %$thread{'body'} %></p>
  </article>
<% } =%>

Readability becomes more and more important as my memory gets worse and worse... I need to learn POD so if I'm a boi of my word there may be a blog post on that in the future. Knowing me... Years in the future.

#perl #mojolicious #sql

I will say if I had to choose either PHP or JS, I'd choose JS lol. But I do think the World Wide Web without Perl is worse off. I hope the O.G. Perl Mojolicious lives on in harmony.

#perl #javascript #mojolicious #insomnia

I recently have come to the come to the conclusion that 'warranty' as we know it in 2022 is a big fat Greek lie because:

  1. Devices aren't even repairable anymore. What are they gunna do for you?
  2. Who says they'll honor the warranty anyways?

I'm swearing them off forever now. My new warranty is the SwaggPlan:

  1. Always decline the warranty. Always.
  2. Use search tools like StackOverflow, YouTube, etc for fucking everything. Just this week I've fixed a fridge and HVAC. I know literally nothing about these things.
  3. You are probably capable of figuring out much more shit than you give yourself credit for!

Forget 'peace of mind', warranty only provides the illusion of that. Real peace of mind is ensuring you're never reliant on people/processes that are demonstrably unreliable.

Right to repair now,

Dan

#righttorepair #diy

Enter your email to subscribe to updates.