fifql is still under active development, and is considered *alpha*
fifql is a query language consisting of fif, and an exposed web server handler for querying a web server. It is meant to be a replacement for graphql, while providing the full benefits of a sandboxed stack machine.
fifql leverages the fif stack-machine as a query language, which offers an easier medium of expressing data formatted in the EDN data format. fifql works awesome with clojure and clojurescript applications on both the server-side, and on the receiving client-side.
This example creates a new fifql server containing one custom word function which returns an integer value plus 2. It also defines a word variable ‘server-details which contains the server name and port.
(ns fifql.example.server
(:require
;; High-performance Web Server
[org.httpkit.server :as httpkit]
;; Routing Library
[compojure.core :refer [GET POST defroutes]]
[compojure.route :as route]
;; Fifql Library
[fifql.server :refer [create-ring-request-handler]]
[fifql.core :as fifql]))
(def server-name "A fifql Example Server")
(def server-port 8080)
;; Create our stack machine, and define some word functions
(def stack-machine
(-> (fifql/create-stack-machine)
(fifql/set-word 'add2 (fifql/wrap-function 1 (fn [x] (+ 2 x)))
:doc "(n -- n) Add 2 to the value"
:group :fifql/example)
(fifql/set-var 'server-details {:server-port server-port :server-name server-name}
:doc "The server details"
:group :fifql/example)))
;; Create our ring request handler to use with Httpkit
(def fifql-handler
(create-ring-request-handler
:prepare-stack-machine stack-machine))
;; Create our routes. The fifql ring handler supports both GET and POST requests
(defroutes app
(GET "/fifql" req fifql-handler)
(POST "/fifql" req fifql-handler)
(route/not-found "<h1>Page not Found</h1>"))
;; Start the web server. Note that any server that supports ring
;; request handlers are supported.
(defn start
[]
(httpkit/run-server #'app {:port server-port}))
(defn -main [& args]
(start))
Querying the server via a curl
command:
# HTTP GET Examples
$ curl http://localhost/fifql?query=2,add2
{:input-string "2,add2", :stack (4), :stdout [], :stderr []}
$ curl http://localhost/fifql?query=server-details
{:input-string "server-details", :stack ({:server-port 8080, :server-name "A fifql Example Server"}), :stdout [], :stderr []}
$ curl http://localhost/fifql?query=server-details,:server-port,get
{:input-string "server-details,:server-port,get", :stack (8080), :stdout [], :stderr []}
# HTTP POST Examples
$ curl -d "2 2 +" -X POST http://localhost:8080/fifql -H "Accept: application/edn"
{:input-string "2 2 +", :stack (4), :stdout [], :stderr []}
$ curl -d "\"Hello World!\" println" -X POST http://localhost:8080/fifql -H "Accept: application/edn"
{:input-string "\"Hello World!\" println", :stack (), :stdout ["Hello World!\r\n"], :stderr []}
We can also query using clojure and clojurescript. An example within clojure:
(require '[fifql.client :refer [query sform]])
(def some-value 10)
(query "http://localhost:8080/fifql"
(sform %= some-value 2 +))
;; {:input-string "10 2 +", :stack (12), :stdout [], :stderr []}
Note that we can inline clojure evaluations within the `sform`
function by escaping with a preceding %=
symbol.
Similarly, there is a clojurescript equivalent query that involves a callback:
(ns fifql.cljs.example
(:require [fifql.client :refer [query sform] :include-macros true]))
(query "http://localhost:8080/fifql"
(sform 2 2 +)
(fn [result] (.log js/console result)))
;; {:input-string "2 2 +", :stack (4), :stdout [], :stderr []}
fif-ql requires clojure 1.9+
For the latest version, please visit clojars.org
fifql is inspired by GraphQL and offers more flexibility when performing queries. Instead of forcing the user to tie into a particular schema, why not let them come up with thier own schemas from fundamental data structures?
Sometimes it can be unclear what a user wants from an API, so this gives them complete freedom on how the data should be retrieved from the system.
Additionally, the fif stack language is already sandboxed and includes additional security to prevent malicious intent.
As an example, assume that I want to retrieve the first 10 users from a user-listing
In GraphQL, this query would look like this:
{userListing(first: 10) {
totalCount
items {
name
id
}
endCursor
hasNextPage
}}
In fifql, this query is constructed from a few word functions,
namely example/user-listing
, example/user-count
, and
example/users-after?
;; What key value pairs do we want from each user in the user-listing?
def user-keys [:name :id]
;; Grab the first 10 values in the user listing, and place in the word variable 'ulisting
{:first 10} example/user-listing *ulisting <> setg
;; Construct our end cursor, place in the word variable 'end-cursor
ulisting last :id get *end-cursor <> setg
;; Construct our data to be returned on the stack
{:total-count (example/user-count)
;; map over the user-listing selecting only the key-value pairs that we want
:items ((user-keys select-keys) ulisting map vec)
:end-cursor (end-cursor str)
:has-next-page? (end-cursor example/users-after?)} ?
;;
;; Notes:
;; '?' is used to 'realize' the data, this is a fundamental fif concept.
The result of the first element of the stack:
{:total-count 43
:items [{:name "Ben" :id 1} {:name "John" :id 2} ...]
:end-cursor "9"
:has-next-page? true}
An important note to make about the fifql version of the query. The user has chosen how to represent the data for themselves, leaving them with full control. This takes unneeded burden off of the API development.
In the event that such queries become commonplace, the API can be extended to include more personalized and concrete functions for the client ie.
{:first 10} example/user-page ;; generates the same query result as the query above.
As a result, this makes fifql more flexible and a much more powerful alternative to GraphQL.
If you’re not familiar with stack programming, a lot of what has been presented here might look scary and unconventional. Stack-programming made a prime appearance when the programming language Forth was developed, and it has remained an often overlooked alternative in modern software development outside of embedded systems.
Stack-programming is great as a query language due to how it presents a lot fewer surprises. Values are simply pushed and popped off of a stack. The resulting stack is then returned to the user who performed the query. It couldn’t get much simpler than that.
That being said there are several more advantages
Clojure functions are easily adapted to work in the fif stack machine. No need to write a schema and write a bunch of resolvers, just write clojure functions and wrap them into word functions.
Since everything is done in the EDN data format, there is no need for complicated keyword to string conversions when working within clojure and clojurescript.
Stack-machines developed can be easily tested, with a ton of examples available in the fif repository source code.
You can take full advantage of fifql without having to learn the ins and outs of the entire fif language. In the event that you would like to learn more, you can check out the fif playground and get more accustomed to what is possible.
GraphQL makes a distinction with Queries and Mutations. This distinction does not exist in fifql, since it’s just another word function.
The fifql ring request handler has been designed to allow you to dictate which stack-machine a request is privileged to use.
As an example, I will write a :prepare-stack-machine
function that
will only allow a user to use a set of word functions that can
mutate the server.
(def guest-stack-machine
(-> (fifql/create-stack-machine)
(import-guest-libs)))
(def admin-stack-machine
(-> guest-stack-machine
(import-admin-libs)))
(def fifql-handler
(create-ring-request-handler
:prepare-stack-machine
(fn [req]
(if (-> req :session :admin?)
admin-stack-machine
guest-stack-machine))))
create-ring-request-handler
can also optionally include the
:post-response
function which can manipulate the response while
having full access to the evaluated stack machine.
As an example, i’ll check the stack-machine for a username and password, and manipulate the user’s session if they provide the correct credentials.
(require '[fifql.core :refer [get-var]])
;; ..building on the last example
(def fifql-handler
(create-ring-request-handler
;;..
:post-response
(fn [sm request response]
(let [username (get-var sm 'username)
password (get-var sm 'password)]
;; Set the session to an admin session if the
;; stack-machine has the correct `username and `password
;; set.
(if (and (= username "admin") (= password "123"))
(assoc-in response [:session :admin?] true)
response)))))
An example client request
(require '[fifql.client :refer [sform query]])
(defn authenticate-script [username password]
(sform
def username %= username
def password %= password))
(query "http://localhost:8080/fifql" (authenticate-script "admin" "123"))
fifql also has support for ExpressJS for use with clojurescript in NodeJS. Here is an example of a ExpressJS server.
(ns fifql.dev.server
(:require
[fif.core :as fif]
[fifql.core :as fifql]
[fifql.server :refer [create-express-request-handler
add-request-middleware!]]))
(def express (js/require "express"))
(def app (express))
(def server-name "A fifql Example Server")
(def server-port 8081)
(def server-details
{:server-port server-port :server-name server-name})
;; Create our stack machine, and define some word functions
(def stack-machine
(-> (fifql/create-stack-machine)
(fifql/set-word 'add2 (fifql/wrap-function 1 (fn [x] (+ 2 x)))
:doc "(n -- n) Add 2 to the value"
:group :fifql/example)
(fifql/set-var 'server-details server-details
:doc "The server details"
:group :fifql/example)))
;; Generate our fifql handler
(def fifql-handler
(create-express-request-handler
:prepare-stack-machine stack-machine))
;; Create some routes. Note: we also add our own middleware
(doto app
(add-request-middleware!)
(.get "/fifql" fifql-handler)
(.post "/fifql" fifql-handler))
(defn -main [& args]
(.listen app server-port
#(.log js/console (str "Server Started on port " server-port))))
(set! *main-cli-fn* -main)
Starting this server and running the the same examples from the introduction reveals the same output:
$ curl -d "2 add2" http://localhost:8081/fifql -H "Content-Type: application/fif"
{:input-string "2 add2", :stack (4), :stdout [], :stderr []}
- The returned
:stderr
key for responses is unused in NodeJS, since clojurescript does not map to it. The key remains in the returned response for consistency. - When overloading the
:post-response
function handler in the ExpressJS request handler does not require you to return the response object due to the nature of ExpressJS’s design.