Mininim aims to be a general purpose and highly modular framework that intends to provide for rapid CLI and web-based application development. Having spent the last 15+ years as a professional PHP developer, one thing which has held me back from other languages, specifically statically typed languages, is the general difficulty in performing certain things I very much take for granted in the PHP world to enable extremely modular and fast development. Critical among these features are:
In addition to these, the framework should generally be as approachable as possible for people coming from more traditional object-oriented languages. That means additional "keywords" that appear semi-magical, e.c. this, self, super, should be available and not require any additional boilerplate.
In addition to learning the language, I've been researching and expanding my knowledge of the general ecosystem. My first approach aimed to rely on the classes package found at https://github.com/jjv360/nim-classes. This ran into some very early issues with respect to co-dependency since ultimately the macros rely on distinct type declarations for each class. While this would still be stylistically preferable, this limitations on forward type declarations prevent it from being so Accordingly, I wrote my own macros to support defining and transforming the body of a given class's methods/procs/iterators.
The second major issue I ran into deals more with modularity and discoverability. Ideally, installing a package and importing it into the main application should be the maximal amount of effort it takes to enable its features. In PHP land, extensive runtime libraries, reflection, and the general dynamic nature of the langauge means this can be easily driven with no code modifications. In my PHP framework (https://hiraeth.dev), much of the integration is even driven simply by basic configuration files, which will literally map classes across different components with simple configuration paradigms, auto-wired and/or delegated dependency creation, "preparers" (to inject dependencies post instantiation when the class implements a given interface, etc).
The third and final issue I ran into, a consequence of replacing the classes package was returning to the "terseness" which it provided. How do I create methods which always work an ref object for its type without constantly re-typing the first parameter, how do I enable the magical symbols that point to the current "class," the parent "class," and for that matter, call the parent's method from inside the child for reliable constructor and method overloading without throwing procCall and casting all over the place?
In the end, the "core" Mininim package provide 3 major groups of functionality:
To test this core functionality, I have stamped out some very basic modules for web support (middleware and routing), and CLI sub command offerings. None of these are fully developed or remotely production ready, but it's enough that I can start to show some code. So let's get into that using my current example app:
Here's what the most basic application may look like:
{. warning[UnusedImport]:off .}
import
dotenv,
mininim/loader,
mininim/dic,
mininim/cli
dotenv.load()
loader.scan("./local")
var
app = App.init(config)
console = app.get(Console)
quit(console.run())
Let's break it down real quick (leaving aside the dotenv which is just standard support for .env, not required by mininim if you set up your environment otherwise).
loader.scan("./local")
The scan macro effectively imports all .nim files in the local directory of the application. This services the modularity/discoverability aspect of Mininim, which will become clearer later. Put most simply for now, individual files/modules register their functionality with the system through what I am calling a Shape which is comprised of one or more Facet. Facets can also have their own shape which provides even lower level functionality like transformation of callback hooks for any other module that employs that facet. Again, more on this later.
This is also why the file begins with: {. warning[UnusedImport]:off .}
var app = App.init(config)
The application is then initialized using the global config (which is effectively just a sequence of all the facets found). Obviously prior to this you could scan/remove/modify the config as needed, but part of the goal of this is effectively that if it's in your code base, it should be providing some meaningful functionality. If it's not, just remove it -- if you're afraid of losing it, that's what version control is for!
With the instantiation of the App, we also see the first use of the init() macro. This macro is designed to basically give consistent and overloadable constructors across all Minimim code. Here's what it looks like:
macro init*(self: typedesc, args: varargs[untyped]): untyped =
if args.len > 0:
result = quote do:
block:
var this = system.new(`self`)
this.init(`args`)
this
else:
result = quote do:
block:
var this = system.new(`self`)
this.init()
this
Corresponding to that is the init() procedure, which by default does nothing:
proc init*(this: auto) =
discard
As you can see from the code, use of init() doesn't really depend on anything too specific to Mininim. You could, hypothetically provide constructors for all sorts of objects.
Up until this point, we have only relied on the loader, which implicitly exports all the core Mininm functionality. However, our next line effectively demonstrates how the dependency injection works via the service locator:
var console = app.get(Console)
This tells our application to build our console. How does the application know how to do this? Because the mininim/cli module told it how:
shape Console: @[
Shared(),
Delegate(
hook: proc(app: App): Console =
result = Console.init(app)
)
]
In this particular case, the only dependency the Console has is the App for which it's operating (which it can then use when building Command dependencies). We can also, however, see that the Console has another Facet in addition to its Delegate. By adding the Shared facet to its shape, the application is instructed (when resolving the dependency) to share the previously built instance (singleton).
Let's dive a little deeper into what the Console itself looks like. Firstly, we have it's type definition:
type Console* = ref object of Class
app*: App
args*: seq[string]
opts*: Table[string, string]
We can see here that a Mininim "class" will be a referenced object of type Class. There is no special functionality regarding Class, it is purely in place for familiarity and clarity that these types are meant to employ object-oriented paradigms. Ignoring the other properties of the object for a moment, we can also see here where the App dependency is defined. Accordingly, we'll take a look now at the "body" of the Console class:
begin Console:
method init*(app: App): void {. base, mutator .} =
this.app = app
method run*(): int {. base .} =
result = 0
for kind, key, val in getopt():
case kind
of cmdArgument:
this.args.add(key)
of cmdLongOption, cmdShortOption:
this.opts[key] = val
of cmdEnd:
discard
if this.args.len > 0:
let
command = this.app.config.findOne(Command, (name: this.args[0]))
if command != nil:
result = cast[CommandHook](command.hook)(this)
Most important a this time is probably just to look at the init() method which we saw called by the Delegate facet added to Console's "shape" earlier. Note, the method is marked with the custom pragma of mutator which signifies that this (the first argument added by the begin macro, should be var to ensure mutability as we'll be assigning the value of a property directly on the instance.
Secondarily, we can take a look at the run method, which as shown gives a basic idea of how commands are resolved. Specifically:
let command = this.app.config.findOne(Command, (name: this.args[0]))
Here, we use the previously provided App for the instance, to find any Facet in the registered config of the type Command and where the name is equal to the first argument passed to our application. We then call its hook and provide the console to it so it can use it to look up additional arguments and options.
Returning to our main module / application, we can see this being invoked in the final line:
quit(console.run())
At this point, this all might seem very complicated. And it is. Part of my 15+ year career in PHP has taught me that despite all of the facilities provided by the language to make dynamic programming more accessible, if you want to create frameworks/libraries with good developer experiences, you will need to absorb some of that complexity so that they can write very simple code. So... assuming that this is the "boilerplate" app, and all of this supporting code and logic has been defined for us... how do we actually create a command?
Let's go ahead and add a new file to local/commands/Home.nim:
import
mininim,
mininim/cli
type
Home = ref object of Class
begin Home:
method execute*(console: Console): int {. base .} =
echo "Hello Mininim!"
result = 0
shape Home: @[
Command(
name: "welcome",
description: "Show the welcome message"
)
]
That's it. We can now do:
[01:09] matt@niamey:~/Projects/mininim$ app/bin/mininim welcome
Hello Mininim!
While I hope you enjoyed the journey of how I got here, the important point for me is the last piece. What will it take to add a new sub-command to my application? What will it take to add a new route and handle the request? What will it take to add routing in the first place?
The answer to that would be (hopefully shockingly) simple (in the end):
nimble add https://github.com/mattsah/mininim-web
Update the main.nim file we started with to:
import mininim/web
Add the following to .env:
WEB_SERVER_MIDDLEWARE=router
You can now add the Route facet to the shape of a class, along with properties/methods for handling:
import
mininim,
mininim/cli,
mininim/web,
mininim/web/router
type
Home = ref object of Class
request*: Request
begin Home:
method execute*(console: Console): int {. base .} =
echo "Hello Mininim!"
result = 0
method invoke*(): Response {. base .} =
result = (status: 0, headers: @[], stream: newStringStream("Hello Mininim!"))
shape Home: @[
Command(
name: "welcome",
description: "Show the welcome message"
),
Route(
path: "/hello/mininim",
methods: @[HttpGet]
)
]
Please note, while everything demonstrated above is currently functional, interfaces, types, and all of that are highly subject to change. These are very early concepts and will develop with time. For example, the Response is likely to become a proper class rather than a tuple.
Pretty much just try to build stuff with this, and implement new features as I go. Obvious failures at the moment:
Hope this was interesting.
Hope this was interesting
Oh it definitely was, thanks for the nice read :-)
I was very skeptical at first when I saw your main.nim code because of all the implicit declarations but it did make more sense as the article evolved. I personally like it when everything is explicit and I can understand what a module does without having to know the assumptions about the project structure imposed but a particular framework. But I can see how this approach could be useful. RoR proves this approach can be vary productive.
Thanks for sharing and good luck with Mininim!
I was hesitant to give the repos, as I don't want to convey that everything is even properly packaged or ready to try yet. Although the functions mentioned work, at present, the nimble packages aren't published. I tore down some old and too lengthy README placeholders, did a bit of cleanup, and you can at least see the code here:
Example application: https://github.com/mattsah/mininim -- this will eventually be stripped of anything but the CLI, but right now has web stuff in it.
Various packages: