DEV Community

Cover image for Loafang! An alternative to GrapQl?
Adwaith Rajesh
Adwaith Rajesh

Posted on

Loafang! An alternative to GrapQl?

Full disclaimer, I made loafang as a way to kill my time and is still in development.
The current implementation that we will be discussing is written in python. And the example shown will be in python 3.9.
So, let's begin.

What does it aim to solve.

Well, technically it's more about readability and being more verbose in terms of how the query is written and also removing the need to use an external library to send a request from the client's perspective. The way loafang achieves this is by making use of JSON syntax as it's one of those file types that is supported by a lot of languages out of the box.

And it's way too much fun to do something like this. So.....

A sample query

{
    "GET:get-user": {
        "user --name ad": ["username", "email"],
        "posts --user-id ad34": ["latest"]
    }
}

Enter fullscreen mode Exit fullscreen mode

Let's talk about the parts of the query.


"GET:get-user": {
        "user --name ad": ["username", "email"],
        "posts --user-id ad34": ["latest"]
    }
Enter fullscreen mode Exit fullscreen mode

This is an execution block in terms of loafang terminology. You can have multiple execution blocks in the same loafang query (obviously)

GET:get-user
Enter fullscreen mode Exit fullscreen mode

This is the block header, It specifies three things,

  • A Request method, in this case, it's GET (The schema is case sensitive),
  • A unique id that represents the block. (get-user)
  • A block property key. (It's optional)

These three things are arranged in the following order.
METHOD:id:property-key


"user --name ad": ["username", "email"]
Enter fullscreen mode Exit fullscreen mode

This is a query that needs to be executed from the server. And it has three parts.

  • The head, here its user (this can be a database name or something like that).
  • The args (--name)
  • contents, here ["username", "email"]

Now, the contents type must be a list for GET and DELETE queries.
And, must be a dict for POST, PUT, and PATCH, as these three methods require you to update or add things.

Let's write some code.

Let's make a simple example use case for loafang with fastapi and tinydb

We will make a simple example where you can ADD, UPDATE, and GET data from the DB.
I won't let you remove your data from MY database.

Stuff we need

pip3 install loafang fastapi uvicorn tinydb
Enter fullscreen mode Exit fullscreen mode
# app.py

from __future__ import annotations
from typing import Any, Union
from argparse import Namespace  # type hinting purposes only

from fastapi import FastAPI, HTTPException
from loafang import parse, Methods, MethodsError, QueryBuilder
from tinydb import TinyDB, Query

# what our query looks like
query = QueryBuilder("database")
query.add_argument("uname", type=str)

class RequestMethods(Methods):

    # class to handle/create a response for queries in a block
    def __init__(self) -> None:
        # you can have different query parser for different request methods
        self.get_query_parser = query
        self.post_query_parser = query
        self.put_query_parser = query

    def get(self, args: Namespace, contents: list[str]) -> Any:

        # our database only has fields for name, username and email.
        if not all(i in ["name", "email"] for i in contents):
            raise MethodsError(400, "Bad contents arguments")

        User = Query()
        db = TinyDB(args.database)

        db_ret = db.search(User.username == args.uname)
        if not db_ret:
            raise MethodsError(404, "User does not exists")

        else:
            user = db_ret[0]
            return {i: user[i] for i in contents}

    def post(self, args: Namespace, contents: dict[str, Any]) -> Any:

        if not all(i in contents for i in ["name", "email"]):
            raise MethodsError(400, "Bad contents arguments")

        User = Query()
        db = TinyDB(args.database)

        if db.search(User.username == args.uname):
            raise MethodsError(403, "User already exists")

        else:
            db.insert({"username": args.uname, **contents})
            return {"msg": f"user {args.uname} added."}

    def put(self, args: Namespace, contents: dict[str, Any]) -> Any:
        if not all(i in contents for i in ["name", "email"]):
            raise MethodsError(400, "Bad contents arguments")

        User = Query()
        db = TinyDB(args.database)

        user = db.search(User.username == args.uname)

        if not user:
            raise MethodsError(404, "User already exists")

        db.update(contents, User.username == args.uname)
        return {"msg": f"User updated successfully. {args.uname}"}


request_methods = RequestMethods()

# a vague representation of the loafang query
QueryType = Union[dict[str, Any], list[str]]


# initialize the server
app = FastAPI()

@app.get("/")
def read_root():
    return {"ping": "pong"}

@app.post("/loaf")
def loaf(data: QueryType):
    # the parsers gives you three things
    # The parsed data with all the output (None, if something goes wrong)
    # err code (only if something goes wrong, else None)
    # err message (only if something goes wrong else None)
    parsed_data, err, msg = parse(request_methods, data)

    return parsed_data if parsed_data else HTTPException(status_code=err, detail=msg)

Enter fullscreen mode Exit fullscreen mode

Launch the server

uvicorn app:app
Enter fullscreen mode Exit fullscreen mode

Now let's send a sample query to the loaf endpoint and see what happens.

The query
{
    "GET:get-user:pe": {
        "db.json --uname adwaith": ["name", "email"]
    },

    "POST:add-user": {
        "db.json --uname adwaith": {
            "name": "ad",
            "email": "adwaithrajesh3180@gmail.com"
        }
    },

    "PUT:update-user": {
        "db.json --uname adwaith | update-ad": {
            "name": "ad2",
            "email": "new-email@example.com"
        },

        "after": "get-user"
    }
}

Enter fullscreen mode Exit fullscreen mode
The response
{
  "add-user": {
    "db.json --uname adwaith": {
      "msg": "user adwaith added."
    }
  },
  "update-user": {
    "update-ad": {
      "msg": "User updated successfully. adwaith"
    },
    "after": {
      "db.json --uname adwaith": {
        "name": "ad2",
        "email": "new-email@example.com"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's analyze what the hell just happened here,

In the first execution block, you can see that there is a pe property key (currently that is the only one). It tells the parser to skip the block or "prevents the execution" of the block, the block can now be called from other blocks.

The second block is a POST block and has one query in it that adds a user with the name adwaith to the DB 'db.json'.

The third is a PUT block that has one query that updates a user with the name adwaith. But there is something special with that block. It has an after key. It tells the parser that after it executes the current block it needs to execute the block specified in the after.

The after key can only point to a block that has pe as its property key.
A block with pe as its property cannot have an after key

You might also notice that there is a pipe | in between the query. It specifies that anything after the pipe is an alias to the query. As you can see in the result, the key to a query's values is the query itself. So it might be hard at times to retrieve data using the query as the key.

So, that a brief intro to loafang, feel free to ask questions, use the package and if you have any errors, pls report it or ask me on discord.

GitHub: https://github.com/Adwaith-Rajesh/loafang

Full Docs: https://adwaith-rajesh.github.io/loafang/

Have a nice day.

Happy Coding.

Discussion (0)