Add a couple posts I missed during the migration

This commit is contained in:
Kaan Barmore-Genç 2024-05-09 22:48:33 -05:00
parent ea5052f04a
commit 04f124cb39
Signed by: kaan
GPG key ID: B2E280771CD62FCF
2 changed files with 388 additions and 0 deletions

View file

@ -0,0 +1,180 @@
---
title: 'Building a Well Organized Rust CLI Tool'
date: 2023-12-09T00:05:58-06:00
toc: false
images:
tags:
- dev
- rust
---
Here's a pattern came up with for building CLI tools in Rust using
[Clap](https://crates.io/crates/clap) with derive and using a custom trait.
First, define a "run command" trait that all commands will use.
```rs
#[async_trait::async_trait]
pub trait RunCommand {
async fn run(&self) -> anyhow::Result<()>;
}
```
I'm using [async-trait](https://crates.io/crates/async-trait) so my commands can
be async, and [anyhow](https://crates.io/crates/anyhow) for error handling
because I don't particularly care about the types of the errors my commands
might return. Neither of these are required though, skip them if you prefer.
The "trick" is to then implement your `RunCommand` trait for all the commands
and subcommands for your Clap parser. This is just how traits work, but what's
useful is that it makes it really easy to organize your commands and subcommands
into different folders while keeping the definition of the command next to the
implementation of that command.
For example, imagine a program with commands like:
```
myprogram query --some-params
myprogram account create --other-params
myprogram account delete --even-more-params
```
I'd recommend organizing the code into a folder structure like this:
```
.
├── account
│ ├── create.rs
│ ├── delete.rs
│ └── mod.rs
├── mod.rs
├── query.rs
└── run_command.rs <-- the RunCommand trait is defined here
```
These files will then look like this:
##### `account/create.rs`:
```rs
#[derive(Debug, Args)]
pub struct AccountCreate { /* params */ }
#[async_trait::async_trait]
impl RunCommand for AccountCreate {
async fn run(&self) -> anyhow::Result<()> {
// Your command implementation here!
}
}
```
##### `account/delete.rs`
Omitted for brevity, same as create
##### `account/mod.rs`:
```rs
#[derive(Debug, Subcommand)]
pub enum AccountCommands {
Create(AccountCreate),
Delete(AccountDelete),
}
#[async_trait]
impl RunCommand for AccountCommands {
async fn run(&self) -> anyhow::Result<()> {
match self {
Self::Create(cmd) => cmd.run().await,
Self::Delete(cmd) => cmd.run().await,
}
}
}
#[derive(Debug, Args)]
pub struct Account {
#[command(subcommand)]
command: AccountCommands,
}
#[async_trait]
impl RunCommand for Account {
async fn run(&self) -> anyhow::Result<()> {
self.command.run().await
}
}
```
##### `query.rs`:
```rs
#[derive(Debug, Args)]
pub struct QueryCommand { /* params */ }
#[async_trait::async_trait]
impl RunCommand for QueryCommand {
async fn run(&self) -> anyhow::Result<()> {
// Your command implementation here!
}
}
```
##### `mod.rs`
Finally tying it all together:
```rs
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
command: Option<Commands>,
// Any global options that apply to all commands go below!
#[arg(short, long)]
pub verbosity: Option<String>,
}
#[async_trait::async_trait]
impl RunCommand for Cli {
async fn run(&self) -> anyhow::Result<()> {
if let Some(command) = &self.command {
command.run().await?;
exit(0);
} else {
Ok(())
}
}
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Account(Account),
Query(QueryCommand),
}
#[async_trait::async_trait]
impl RunCommand for Commands {
async fn run(&self) -> anyhow::Result<()> {
match self {
Commands::Account(account) => account.run().await,
Commands::Query(query) => query.run().await,
}
}
}
```
Now, in `main` all you have to do is call your trait:
```rs
let cli = Cli::parse();
cli.run().await.unwrap();
```
I love the filesystem-routing like feel of this. Whenever you see a command in
the CLI, you immediately know what file to go to if you need to change or fix
something. Please don't discount the value of this! As your application grows,
it can be a massive headache to come across a bug and then having to dig around
the entire codebase to find where that code is coming from. If all commands have
a particular file, you can always start your search from that file and use "go
to definition" to dig into the implementation.
This also helps if you are adding a new command, it's immediately obvious where
the code needs to go. Simple but elegant, and a significant productivity boost.

View file

@ -0,0 +1,208 @@
---
title: "What I'm enjoying using for mobile app development"
date: 2024-01-02T04:33:55Z
draft: false
toc: false
images:
tags:
- dev
- react
- mobile
- typescript
---
I like baking bread. Well, most of all I love bread, and baking it myself is
more convenient than tracking down a good bakery nearby. I used to bake a lot
--and as a hipster I'll tell you that it was before everyone else started doing
it at the start of the pandemic-- but I got bored of it some time ago and quit.
But I've been eating more bread recently, and decided I could bake something
better, and cheaper, than the bread from the grocery store. I'm pretty happy
with what I've been baking too, they are all turning out with a good crust and
soft insides. What's the inside of the bread called? Anyway, my bread is good
but a little bland, so I decided to also get back into sourdough bread to get
some nice flavor.
Sourdough bread is made with sourdough starter, a live culture of yeast and
bacteria that needs to be maintained with regular feedings of flour. So before
you can bake sourdough bread, you must first prepare your starter and grow it
for a few weeks to get its strength up, then keep it alive for as long as you
want to keep baking. And before you can prepare a sourdough starter, you must
first develop a mobile app.
Well, of course I could have just set up reminder in my calendar and skip having
to develop an actual app. Or just use on of the proprietary ones. But why do the
easy thing when you can waste tens --if not hundreds-- of hours developing an
app to maybe save a few seconds of your time or to make a point about using open
source software? At least it will teach me some stuff along the way.
## What tech to use?
But before I could start developing an app, I had to decide what to build it
with. My first two requirements: it had to be cross platform, and it had to be
able to show scheduled notifications on my phone. My first thought was to use a
PWA. PWAs are web apps that can be "installed" on your phone like a regular app,
but run entirely on your browser. It's sort of an Electron-lite. That would be
great since I already know a lot about web development, alas PWAs can't send
scheduled notifications while the app is in the background. That's a pretty
vital part of this project so that rules out PWAs. Also I think Safari has some
problems with PWAs? So that also would have ruled it out if I somehow worked
around the notifications.
Similar to PWAs, I could have used something like Electron to develop an app
using web technologies. In this case it would be an actual app, so I could
actually run code in the background and send scheduled notifications. Hurray!
Except that I wasn't sure I wanted to do this. My experience with apps developed
this way is terrible, and it's often very obvious someone basically put a web
browser in kiosk mode. These apps are often slow, the UIs are completely devoid
of the native look and feel, and they are often full of weird browserisms like
being able to hold and select the text on the page. Maybe this perception is a form of [Survivorship bias](https://en.wikipedia.org/wiki/Survivorship_bias#In_the_military)
and there are lots of apps I used developed with these technologies that work great,
but I decided to see what else was available first.
After more digging, I finally narrowed my choices down to 2 options: React
Native and Flutter. React Native takes the usual React, and instead of rendering the virtual DOM into a real DOM it renders it
to native UI components. React Native is something I used before, with my [Bulgur Cloud](https://bulgur-cloud.github.io) project.
While I liked React Native, I actually ended up rewriting Bulgur Cloud --in Next.js-- so I was a little hesitant.
Flutter on the other hand uses Dart, and is developed by Google with a focus on cross platform apps.
Flutter works by rendering everything to a canvas... which sort of surprised me.
My biggest concern with this was performance and accessibility, using native components
gives you both out of the box. If you render everything yourself, then it's
your responsibility to handle accessibility and optimize rendering.
Articles and discussions I came across online reassure me that Flutter is "doing better" at all of these,
so hopefully it's not a problem.
At this point I was still paralyzed by this decision, so I ended up creating a project with both to check out
the out of the box example. Which really did not help, they both work and look fine.
The code is pretty easy to understand with both. I have concerns with the future of both projects,
because React Native feels too under control of Expo and Flutter feels too under control of Google.
I finally broke my paralysis by just going with React Native. I already know
React, I know a ton of stuff about the React ecosystem, I've already used React
Native before. Flutter and the Dart language look fine and I'm sure I could
learn them, but it's just easier for me to get this project off the ground if I
use what I already know. I also remembered that the reason why I rewrote Bulgur
Cloud was because React Native for Web is a bad experience, but in this case I
don't care about the web so it's not a problem.
## To Expo, or not to Expo?
When you start using React Native, the first thing you see is Expo. Look at the
official getting started guide for React Native and it immediately recommends
you use Expo. Expo then keeps coming up again and again in the docs. It's easy
to see why:
- Expo solves a major getting started hurdle: installing Android Studio or XCode
and connecting it to your phone so you can run it. Instead, you install the
Expo Go app from your phones app store and scan a QR code to get started
immediately.
- Expo comes with a lot of built-in packages. If you go with Expo, you can use
all these packages _and_ you can still use all the regular React Native
packages.
- Expo doesn't technically lock you into anything, you can "eject" from Expo at
any time.
I decided to use Expo. While I have some concerns that Expo is an
investor-backed company and not a community effort, the Expo packages are all
open source. Expo Go is open source. And you can still install Android Studio
and XCode and build everything yourself.
## What else?
Expo comes with a lot out of the box, but there were some packages I wanted to
play with and some packages I knew I wanted on top of that. Some of these include:
### Tamagui
[Tamagui](https://tamagui.dev) is a UI kit for React Native. I first saw it
advertised as a "headless" library meaning that it implements all the
functionality but comes with no styles, so you style everything yourself. I
later found out that this was not entirely correct though because Tamagui does
come with default styles, but lets you opt out of them or customize them.
I really like Tamagui because it comes as a complete package. It's not just UI
components but also animations, CSS shorthands and a token based design system.
It also comes with a cool "sub-theme" system that allows you to have variants of
your themes, so you don't just have a "dark" theme but you can also have
"dark-forest" and "dark-blue" and "dark-whatever" to create different themes for
different sections of you app without having to hand-code everything.
### Expo-Sqlite
[Expo-sqlite](https://docs.expo.dev/versions/latest/sdk/sqlite/) is an expo package, but it's an optional one.
It gives you Sqlite. That's about all. Sqlite is awesome, so why not.
There are some other options for storage, some even support additional features like encryption
if that's something you need. But I don't think it's crucial if I encrypt
my sourdough starter data, so I'd rather take the Sqlite features.
### nearform/sql
[@nearform/sql](https://github.com/nearform/sql) is a library for SQL injection
prevention. What's cool about it is that you get to write SQL queries with the
javascript string interpolation without risking SQL injection. So you can do:
```js
DB.query(SQL`INSERT INTO students(name) VALUES (${name})`);
```
without little [Bobby Tables](https://xkcd.com/327/) ruining your day.
nearform/sql is not compatible with Expo-sqlite out of the box, but a basic
wrapper function can easily get you there. Here it is, I'd
publish it on npm but it's so short you might as well just copy and paste it.
```ts
export function sql(strings: any, ...values: any[]): Query {
const statement = SQL(strings, ...values);
return {
sql: statement.text,
args: statement.values,
};
}
```
### SWR
[SWR](https://swr.vercel.app) describes itself as "React Hooks for Data
Fetching". My little app is local only and doesn't need to fetch data from
anywhere, so why SWR? Well, SWR is great whenever you need to get data from
anywhere outside of your apps own state. In my case, I need to get data out of
SQLite and into my app, so I use SWR to fetch the data from the database.
The main use case for me is the caching and deduplication. SWR caches results
and won't fetch again if the same hook is called multiple times, so you can use
your hooks everywhere without having to worry about the same data being fetched
multiple times for no reason. You can also invalidate the cache whenever you
want, which will invalidate all users of the hook and fetch the data just once
to update everything.
You need some workarounds to get SWR working in React Native, but [official docs have you covered](https://swr.vercel.app/docs/advanced/react-native.en-US).
### Formik
I was hesitant and I dragged my feet the very first time I encountered
[Formik](https://formik.org), but after trying it once I actually fell in love
with it. It really does everything you need a form to do. The best part to me is
that you get to avoid having a million `useState`s in your form, and thanks to
the Formik context you can organize your code without drilling values and
setters down into components.
You have to do a bit of work for Formik to work in React Native, but the [official guide tells you what to do](https://formik.org/docs/guides/react-native)
so it's an easy change. Wrap that in a custom form input component and you can forget
the incompatibility exists at all.
## Some More Packages
This is getting really long (for my usual posts), so a few more quick mentions:
- `date-fns` is the definitive date & time library. It's incredible.
- `radash` is like lodash. I'm honestly not fully sure if it's better or worse, but I do keep coming back to it.
- `rrule` makes calculating recurring dates easy. You can serialize and deserialize your rrule's which I also appreciate.
- `ulidx` is a random ID generator. It's like nanoid, but at the cost of just a few more characters they are ordered by creation date.
- `zod` is my favorite schema tool. Never leave home without it.
## Fin
I'm about done with the basic functionality I wanted for this app, so I'm hoping
to have a version of this up on the Google Play store by the end of the week.
Oh and I still haven't made that sourdough starter,