/naff

Primary LanguageGoGNU Affero General Public License v3.0AGPL-3.0

NAFF

NAFF is a code generation tool. It stands for:

Not
Another
F([aceoru]{2,3})king
Framework

selfishware

NAFF was built by myself, for myself. If NAFF is useful to you too, I consider that simultaneously cool and incidental. If it isn't helpful to you, then I guess I'm sorry? Please don't be mad.

I wasn't even going to make this code public (see the below section), I was just going to provide binaries, but I don't run binaries on my machine whose code I'm not allowed to read, and I don't suggest you do so either.

I'm honestly not interested in feature ideas/pull requests/complaints/suggestions/etc and have consequently turned those features off in Gitlab. As far as I am personally concerned, NAFF has exactly one user, and I am them. Even this README is for my own self (save for this section).

motivation

NAFF is primarily meant to generate CRUD code for a number of custom data types. The idea is you think of what your individual database tables might look like as plain objects, and write your input package accordingly.

I may write a blog post about this one day, but the long and short of it is: I have an irrational distaste towards so-called "batteries included" web frameworks, and ORMS (almost equally).

The goal for NAFF is to generate well-tested, capable service repositories. As such, NAFF doesn't just generate primary application logic, it also generates:

  • the Makefile and all relevant targets
  • A service client
  • Unit tests with 90+% code coverage
  • Integration tests for all databases
  • Setup for browser-driven tests for multiple browsers
  • Mandatory user 2FA
  • PASETO authentication
  • Database migrations/querying code
  • All dependency injection code
  • Dockerfiles
  • Docker-compose files
  • Routing code
  • GitLab CI scripts
  • Telemetry (tracing/logging/metrics collection)
  • Secure configuration parsing/rendering
  • Prometheus and Grafana configuration files
  • Fairly strict linter configuration and code that adheres to it

NAFF has support for multiple database providers. Currently those are:

  • PostgreSQL
  • MySQL

Each of these may be (de)activated to your liking.

usage

Start by defining any arbitrary Go package with some types in it. For instance, say I have a Go package vcsproviderofchoice.here/verygoodsoftwarenotvirus/addressbook, with a file types.go with the following:

package addressbook

type Friend struct {
    FirstName string
    LastName string
    YearOfBirth uint
    MonthOfBirth uint
    DayOfBirth uint
}

Running naff gen vcsproviderofchoice.here/verygoodsoftwarenotvirus/addressbook will trigger a series of prompts from the CLI, and then it will generate code.

NAFF writes code only on clean slates, and as such will nuke the output directory from orbit. So if I tell NAFF to write output to some precious path, I'm going to have a bad time.

The structs you defined can only have a small selection of types, basically anything that can be defined as a constant in Go. No type aliases, no embedded structs, no time.Time*, no slices. Pointers are allowed, though. Here are all the allowed types:

bool
int
int8
int16
int32
int64
uint
uint8
uint16
uint32
uint64
float32
float64
string
uintptr  // only allowed for the _META_ field

After generating a codebase, it's a good idea to run make gamut, which will generate all the initial files you're going to need to start writing the real code.

*by default, the code generated by NAFF keeps time as uint64s representing epoch time

flag prompts

When you run NAFF, it will ask you a series of questions, namely:

  1. What the project is called
  2. Where the input data models are stored
  3. Where the project should go
  4. Which databases you'd like to support

advanced usage (ownership)

NAFF looks for a small, inconsistent smattering of flags in types. Let's say we wanted users to be able to set a birth year for their friend on creation, but not to change that birth year ever. You could accomplish something like that with this:

package addressbook

type Friend struct {
    FirstName string
    LastName string
    YearOfBirth uint `naff:"!editable"`
    MonthOfBirth uint
    DayOfBirth uint
}

A similar flag !creatable is also supported.

NAFF is capable of generating code that accounts for "ownership" between structs. The example I've been using is an old-school web discussion board, where you have the hierarchy Forums > Subforums > Threads > Posts. That hierarchy can be represented to NAFF like so:

package discussion

type Forum struct {
    Name string
}

type Subforum struct {
    Name string
    _META_ uintptr `belongs_to:"Forum"`
}

type Thread struct {
    Name string
    _META_ uintptr `belongs_to:"Subforum,User"`
}

type Post struct {
    Content string
    _META_ uintptr `belongs_to:"Post,User"`
}

Note the _META_ field. NAFF won't generate anything around this field, but looks to the struct tags of that field for information about how to handle types.

By default, all objects belong to a user, so you only need to indicate user ownership in the _META_ field if the struct also belongs to another struct. You can indicate that an object belongs to nobody (effectively an enumeration) with the struct tag belongs_to:"__nobody__":

package postalservice

type ValidZipCode struct {
    Code string
    _META_ uintptr `belongs_to:"__nobody__"`
}

Note that Thread and Post both belong to users and other structs. In this example, ordinary users can query posts and threads despite their ownership, because that matches the real-world analog. You can restrict something to a user with another tag:

package addressbook

type Friend struct {
    FirstName string
    LastName string
    YearOfBirth uint `naff:"!editable"`
    MonthOfBirth uint
    DayOfBirth uint
   _META_ uintptr `belongs_to:"Post,User"`
}

type FriendPhoto struct {
    PhotoURL string
    _META_ uintptr `restricted_to_user:"true"`
}

This will make it so that the generated code will only return a given user's data to that user.

development

To compare the output of this repo with the source repository, Meld is used. You can install it on most good operating systems, and even Windows.

To make comparisons less noisy, you can/should use the following regex query as Meld text filters:

\t+(\w+\s)?"gitlab\.com\/verygoodsoftwarenotvirus\/(naff\/example_output|todo)