Prologue

Rubik aims to be a fast, scalable and productive Web framework for Go, by composing pre-built blocks of code to fasten the development process. It offers:

  1. 🟒 Writing server-side code for faster backend development using gen command.
  2. 🟑 Generating client SDKs for your backend on the fly for better/fast integration of frontends.
  3. πŸ”΅ Debugging your APIs quicker with Rubik REPL.
  4. 🟑 Communication between services with it's own Messaging Passing layer and includes Event-Driven Development.

Rubik has a robust/thin layer of Go's reflection (which is optional) supporting the whole framework and provide many simple high-level APIs for handling complex tasks. The directory structure of Rubik is minimal and effective.

Why Rubik?

Rubik Framework for Go is a part of "Rubik Project" which was started in light of use-cases where you want stability and speed in creating your ideas and making it to life as quick as possible. It provides with a layer of abstraction so you don't steer far away from Go's standard library and use great tools to get you started with web development in Go.

If you are new to Go and want to create a web app easily and quickly with familiar architecture as most frameworks from different languages Rubik is the best way to create your app.

Is Rubik stable?

No, currently it is not!. The implementation of this project is being done in such a way that you will not have to think about anything while implementing your ideas. It should feel natural just to use Blocks and inbuilt tools while working on your ideas and "Rubik is not there yet." But it is stable enough to quickly create prototypes of your ideas.

How can you help Rubik Project?

  1. You can help Rubik Project by trying it out and helping us fix bugs as quickly as possible to make this project stable.
  2. Contributing with Pull Requests with potential fixes and features.
  3. You can make this project sustainable by donating us to make Rubik more of a main and serious attempt to create a framework that is "fast, flexible and solid", instead of this being developed in our spare time.

Goals

  • Keep a small core
  • Support Extensions
  • Simple and Descriptive with structural composition instead of functional
  • Extensive tooling and Automation with projects that blends well with the Rubik ecosystem
  • Avoid Repitition of common logic, "if one of us develops a foreign implementation it should be for all of us"

Indicators

🟒 Solid 🟑 Flaky πŸ”΅ Implementation Undecided

πŸ“Ί Resources

This topic holds the culmunation of guides, docs, and videos about Rubik. Since Rubik is an young project yet to set foot on production you will always have a lack of resources and places to look for Projects.

Below are some list of resources to browse through or watch:

TypePurposeURL
VideoFirst Rubik Demo (gives you a general idea on the ethos of the framework)Link
DocGoDoc of RubikLink

You can also follow my YouTube channel for upcoming content and implementation of Rubik Framework to get a grasp.

πŸ““ Getting Started

Before you can use Rubik you need to make sure that development environment is up-to-date and ensure that everything is setup correctly. Let's go throught the requirements of Rubik first.

Install Go

Rubik Framework is written using Go programming language but you'll need Go runtime to execute your Rubik server. The official Go installation guide provides you with a step by step instruction to install Go.

Testing your Go installation:

# this command outputs the version of Go runtime that you have installed
go version

Installing okrubik

Rubik CLI helps you in being productive and accomplish your tasks with ease. It has some really nice advantages over normal execution that is discussed here.

Shell:

curl https://rubik.ashishshekar.com/install | sh

Thie CLI will be downloaded and installed under $HOME/.rubik/bin folder. You need to add this path to your bash_profile or it's equivalents:

# example
nano ~/.bash_profile
# add the below line to the end of the file
export PATH="$HOME/.rubik/bin:$PATH"

Now let's check if the installation was successful. Run:

okrubik

# Welcome to Rubik Command-Line Manager use --help for help text

You have just set-up your development environment like a boss. You are ready to Go!!.

Generating a New Rubik Project

In this tutorial you'll learn how to setup your Rubik project using the command-line generator and how easy it is to setup a productive environment with Rubik.

In the previous tutorial we installed okrubik a CLI for this framework, to ensure that it is installed run:

okrubik help

which outputs the following help text for all the commands available in Rubik.

Rubik is an efficient web framework for Go that encapsulates
common tasks and functions and provides ease of REST API development.

Complete documentation is available at https://rubikorg.github.io

Usage:
  okrubik [flags]
  okrubik [command]

Available Commands:
  bundle      Create/Manage release bundle of your Rubik service
  exec        Execute a rubik command defined inside rubik.toml under [x] object
  gen         Generates project code for your Rubik server
  help        Help about any command
  new         Create a new Rubik project
  run         Runs the app created under this workspace
  upgrade     Upgrade the project dependencies or upgrade self

Flags:
  -h, --help   help for okrubik

Use "okrubik [command] --help" for more information about a command.
okrubik new helloworld

You'll have a new directory called helloworld now and will consists of Rubik project files. Let's run the server by running:

okrubik run -a server

The -a flag accepts a name of the application in our case, the default server as the argument. If you have multiple server projects under the same workspace you can do okrubik run which shows a dynamic CLI selection of the list of your Rubik servers which we will take a look at in this chapter.

Viewing server

Now let's visit localhost:7000. Wew! Our Rubik project is all set-up in just under a minute. This is a great start!

πŸ’Ÿ Compatibility with Go stdlib

If you have developed your applications using the net/http package and want to use Handlers as Rubik's route controller, it can be easily migrated to Controller type in Rubik using simple conversion methods. The middlewares which are also generally http.Handler or http.HandlerFunc types can be migrated as well.

Converting http.Handler

To convert a http.Handler into rubik.Controller you can use UseHandler() which accepts a Handler as an input and returns a Controller. It can be inlined during Route declaration like so.

import (
    "net/http"
    r "github.com/rubikorg/rubik"
)

type MyHandler {}

func (MyHandler) ServeHTTP(req *http.Request, writer *http.ResponseWriter) {
    // your code
}

var myRoute = r.Route{
    Path: "/mypath",
    Controller: r.UseHandler(MyHandler{}),
}

Converting http.HandlerFunc

To migrate/use your old http.HandlerFunc you can use UseHandlerFunc() which accepts a HandlerFunc and returns Controller. It can also be inlined.

import (
    "net/http"
    r "github.com/rubikorg/rubik"
)

func myHandlerFunc(req *http.Request, writer *http.ResponseWriter) {
    // your code
}

var myRoute = r.Route{
    Path: "/mypath",
    Controller: r.UseHandlerFunc(myHandlerFunc),
}

Using stdlib Handlers

Middlewares in Rubik are just Controller type. Using the above two methods you can generally use most of the Handler and HandlerFunc's designed for stdlib in your Rubik project but some middlewares has made a design choice of using intermediate handler functions such as CORS handler. To use such functions there is a helper method called UseIntermHandler() which takes in func(http.Handler) http.Handler type and returns a Rubik Controller.

Example:

import (
    "github.com/rs/cors"
    r "github.com/rubikorg/rubik"
)

var c = cors.New(cors.Options{
    AllowedOrigins: []string{"http://foo.com", "http://foo.com:8080"},
    AllowCredentials: true,
    Debug: true,
})

var myRoute = r.Route{
    Path: "/mypath",
    Middlewares: r.Middleware{
        r.UseIntermHandler(c.Handler),
    },
    Controller: myCtl,
}

⌨️ Commanding your Workspace

Rubik comes with a lot of useful commands that makes web development easy in Go, in this chapter we are going to take a look at okrubik CLI's commands and how you can configure your workspace.

Workspace Configuration

The configuration of your workspace in Rubik is done using the rubik.toml file. Let's take a look at the default Rubik configuration file.

name = "okrubik"
module = "github.com/rubikorg/okrubik"
flat = false
maxprocs = 0
log = false

[[app]]
  name = "server"
  path = "./cmd/server"
  watch = true
  communicate = false

[x]
  [x.test]
    command = "go test -cover ./cmd/server/..."

> create

The create command is used to create a new Rubik project. It downloads all the boilerplate code required to get a barebones version of Rubik server running. It creates a new folder with the project name provided. There are 2 ways to run this command.

Interactive Mode

okrubik create

Command Mode

okrubik create -n helloworld -m helloworld -p 4000

> gen

gen command generates components for your Rubik projects. Currently it can generate 2 things:

  1. Binary
okrubik gen bin $binaryName
  1. Router
okrubik gen router $routerName

> run

Interactive Mode

okrubik run

Command Mode

okrubik run -a $appName

> exec

The exec command runs your custom commands mentioned inside rubik.toml. As per our 1st topic, we can see this x object declaration.

[x]
  [x.test]
    command = "go test -cover ./cmd/server/..."

This block of configuration has a [x.test] which can be run inside your workspace. Let's try it out, and add a command called foo which just writes foo to stdout.

    [x.foo]
      command = "echo foo"

Now lets run this command using okrubik.

okrubik x foo

# foo

The x is an alias for exec and you can see it outputs foo as it runs the command that is declared inside [x.foo].

🎹 Rubik Components

In this chapter we will take a look at the components the Rubik Server is composed of. The components allow us to write code with less coupling and highly domain-driven code.

Component List

  1. Guards
  2. Assertions
  3. Middlewares
  4. Entity
  5. Controllers

πŸš” Guards

Guards are rubik.Controller that "guards" your route from potentially bad/error-prone requests. As you know that in Rubik every request is controlled/manipulated/processed by the rubik.Controller type which you can read in Section 4.5.

There is no difference between a Middleware, Core request handler and Guards are nothing different in case of function signature but the only difference is that, Guards run before Assertions.

Let's say you want to reject any request that does not have the x-rubik-client header. Let's write our guard.

Example Guard

func rejectNonRubikClients(req *rubik.Request) {
	if (req.Raw.Header.Get("x-rubik-client") == "") {
		req.Throw(401, "You are not authorized to access this API.", rubik.Type.JSON)
		return
	}
}

Adding this guard to any of your route.

var myRoute = rubik.Route{
	Guards: []Controller{rejectNonRubikClients},
	Path: "/getMeSomething",
	Controller: myCtl,
}

In this case myRoute will reject any client with:

{
	"code": 401,
	"message": "You are not authorized to access this API."
}

for any request with x-rubik-client header being empty.

πŸ” Assertions

Assertions are used to validate and confirm that a value from the client/user is of the right type and meets the requirements of your business needs. E.g: email value inside body must be of the form of an email id and not any other type or structure.

Note:

When assertions fail it responds with 400 (Bad Request) with the assertion message provided by you.

Assertion type:

type Assertion func (interface{}) error

Writing your own assertions:

Let's write a simple assertion to check if the integer value equates to 0 or not:

func isZero(val interface{}) error {
	msg := "$ does not equates to 0."
	switch val.(type) {
		case string:
			intval, err := strconv.Atoi(val)
			if err != nil {
				return errors.New("$ is not an integer value")
			}

			if intval != 0 {
				return errors.New(msg)
			}
			return nil
		case int:
			val != 0 {
				return errors.New(msg)
			}
			return nil
		default:
			return nil
	}
}

Notice the $ symbol, this is used to populate the name of the variable it is checking. As per the above example if the Threshold cannot be converted to an integer the error message will be Threshold is not an integer value.

Using your assertion:

type AddEn {
	Food 	string
	Threshold 	int
}

var addRoute = rubik.Route{
	Path: "/add",
	Entity: AddEn{},
	Validations: rubik.Validation{
		"Threshold": []rubik.Assertion{isZero},
	},
	Controller: addCtl,
}

Middlewares

In Rubik Middlewares are Controllers that run after your Assertions, meaning that they are just Guards that postpone their executions until your Validations are done. Let's combine Guards: Section 4.1 example and add a middleware to get to know them better.

From Guards: Section 4.1 code we have this rubik.Controller:

func rejectNonRubikClients(req *rubik.Request) {
	if (req.Raw.Header.Get("x-rubik-client") == "") {
		req.Throw(401, "You are not authorized to access this API.", rubik.Type.JSON)
		return
	}
}

Example Middleware

func checkAuthHeader(req *rubik.Request) {
	if (req.Raw.Header.Get("authorization") == "") {
		req.Throw(401, "Contact support!", rubik.Type.JSON)
		return
	}
}

Add it to the myRoute with validations:

type MyEntity struct{
	Name string
}

var myRoute = rubik.Route{
	Path: "/getMeSomething"
	Entity: MyEntity{},
	Guards: []rubik.Controller{rejectNonRubikClients},
	Validation: rubik.Validation{
		"name": []rubik.Assertion{checker.StrIsOneOf("tom", "jerry")},
	},
	Middlewares: []rubik.Controller{checkAuthHeader},
}
// replace myRouter with Router of your choice
[myRouter].Add(myRoute)

Lets cURL it:

curl -H "authotization: Basic hi" "http://localhost:$PORT/getMeSomething?name=tom"

Response

{
	"code": 401,
	"message": "You are not authorized to access this API."
}
curl -H "x-rubik-client: clientToken" "http://localhost:$PORT/getMeSomething?name=tom"

Response

{
	"code": 401,
	"message": "Contact support!"
}

Entity

Entity in Rubik specifies the requirements for your target route to function. Specifying your requirements explicitely comes with a lot of advantages. Let's try to specify an entity.

type MyEntity struct {
	Name string
}

MyEntity is a spcification of requirement of your API, which states that "my API requires name inside query to function properly."

Example Route with Entity

var myRoute = rubik.Route {
	Path: "/myPath",
	Entity: MyEntity,
	Controller: myController,
}

// definition of myController
func myController(req *rubik.Request) {
	entity, _ := req.Entity.(*MyEntity)
	req.Respond("You have entered name as: " + entity.Name)
}

// replace myRouter with Router of your choice
[myRouter].Add(myRoute)

Lets cURL it:

curl "http://localhost:$PORT/myPath?name=tom"

Response

You have entered name as: tom

Controllers

Lifecycle of Rubik

The component lifecyle of Rubik is arranged in such a way that the more error prone and security dense components align at the top of the lifecycle.

lifecycle

πŸ“¦ Blocks: Runtime Extensions

Components that are independant of the core system is called as a block. Blocks can never depend on a specific nature of the server yet provide extended functionality to your Rubik server.

It can be seen as plugin system of Rubik ecosystem, but -- it has some features that tie it to the boot sequence of the Rubik's core thereby eventually making it part of the system.

Writing a Block

Writing a block is as simple as implementing a singl method, thet serves as the main func for your extension. A struct is considered as a Rubik Block only if you implement the OnAttach method of the rubik.Block interface.

So what does it look like?

> myawesomeblock.go

package awesome

import (
	"fmt"
	r "github.com/rubikorg/rubik"
)

type BlockAwesome struct{}

func (a BlockAwesome) OnAttach(app *r.App) error {
	// block code
	fmt.Println("awesome block is initialized!")
	return nil
}

There are 2 things that catches the eye:

  1. app parameter - The app parameter is supplied to you by rubik which gives you limited access to the implementer's server.

!!! Note This is not the Rubik instance itself, as this would be too big a vulnerability it supplies you with only the functions that you need to build a block.

  1. error return statement - OnAttach is called first in the boot sequence by Rubik so if you return an error the server will panic and not start, which is an intended workflow.

!!! Quote If the block can not even initialize properly, it can never function properly.

Attaching a Block

A block can never be known by the Rubik server until you attach it, it can be done by calling rubik.Attach inside the init method of this block file.

Considering the above example:

> myawesomeblock.go

package awesome

import (
	"fmt"
	r "github.com/rubikorg/rubik"
)

type BlockAwesome struct{}

func (a BlockAwesome) OnAttach(app *r.App) error {
	// block code
	fmt.Println("awesome block is initialized!")
	return nil
}

func init() {
	r.Attach("myawesomeblock", BlockAwesome{})
}

rubik.Attach takes in a (name string, block rubik.Block) as it's parameters.

Best Practices

Notice how we had initialized the block? By passing a raw string -- this can prove dangerous while developers using this block try to Get your Block for usage.

  • It is is always a good practise to declare a BlockName constant inside your block file. This can prove very useful while getting the block.
// BlockName is this block's name -- like so
// and pass it inside the Attach method
const BlockName = "myawesomeblock"
  • Always Attach it inside the init method. Blocks are inherently separate components and is designed in such a way too -- so why let developers Attach it separately inside their main file?

Getting a Block

Developers can get your block while implementing their business logic from anywhere in the project. Since Rubik is a singleton instance it maintaines a map of blocks by the name that you specify.

A very good example of this type of implementation is the CORS block:

import (
    "github.com/rubikorg/blocks/yourBlock"
    r "github.com/rubikorg/rubik"
)

// notice how BlockName came in handy?
var yourBlock = r.GetBlock(yourBlock.BlockName)

r.GetBlock() gets the Block by name, rubik knows that it's a Block (as it satisfies the Block interface) but compiler just doesn't know which one (yet ..generics please rant!!) ..

!!! Note There is no possible way that r.GetBlock returns any other Block because of the map and unique key that it it maintains. So rest assured while coercing to your Block type.

Accessible Functions from Blocks

Decode

type: Function

Decode decodes the internal rubik server config into the struct that you provide. It returns error if the config is not unmarshalable OR there if there is no config initialized by the given name parameter. It is useful when you really want some configs to be present for your block to work.

func (sb *App) Decode(name string, target interface{}) error {}

Config

type: Function

Config gets config by name

func (sb *App) Config(name string) interface{} {}

CurrentURL

type: String

It is the localhost prepended current URL of your Rubik server.

app.CurrentURL

RouteTree

type: Struct All The initialized Routes and their belongings such as Entity, Description etc.

app.RouteTree

Note

A RouteTree is not initialized until boot sequence completes. If you want to make use of the RouteTree you can use rubik.AttachAfter() which attaches your Block after the boot sequence is complete

Block -> JWT Authentication

This extension block for Rubik is used to easily authenticate rubik.Request containing Json Web Token.

Usage

import

_ "github.com/rubikorg/blocks/guard/jwt"

Example:


import (
	r "github.com/rubikorg/rubik"
	"github.com/dgrijalva/jwt-go" // used for coercing type
	jwtBlock "github.com/rubikorg/blocks/guard/jwt"
)

var myRoute = r.Route{
	Guards: []Controller{jwtBlock.HeaderJWTMiddleware},
	Path: "/getSomething",
	Controller: myController,
}

func myController(req *r.Request) {
	claims, _ := req.Claims.(jwt.MapClaims)
}

Config

[jwt_auth]
secret = "my jwt secret"
cooke_http_only = false
cooke_http_only_error "your request requires it to come through HTTP"
cookie_key = "tok"
unauth_error = "this is acustom unauthorized error"
expiry_time = 3 # your token expires in 3 seconds
expiry_key = "exp" # which claim field inside JWT has the expiry time

Create Token

func CreateToken(claims jwt.MapClaims, expiry bool) (string, error)

Rubik comes with a pretty neat extension method to create a JWT token from the jwt.MapClaims that you provide, and errors when a signature could not be made from the secret provided in config.

Error Status

401 - The controllers return Unauthorized when token is present but is not a valid token

403 - The controllers return Forbidden when there is no token for a JWT Block protected route

There are 3 types of Controllers available in this block:

Cookie Authentication Controller

func CookieJWTMiddleware(req *rubik.Request)

This middleware controller can be used inside the rubik.Route to authenticate JWT with default key as 'token' and can override with cookie_key config so this block will look for overriden key, instead of default one

Header Authentication Controller

func HeaderJWTMiddleware(req *rubik.Request)

This middlware controller can be used in rubik.Route to authenticate JWT from the header. It follows the Bearer standard where you pass Bearer $TOKEN as the authorization header. It does not have any override from the config.

Error-Free Authentication Controller

func ParseClaimsMiddleware(req *rubik.Request)

This middleware can be used when there is no need to throw any error to the client but want to parse the jwt.MapClaims and be available inside the rubik.Request.Claims.

Block -> Swagger

Swagger block let's you add documentation and to your Rubik REST APIs and serve as a tool for people to know and interact with your server.

Usage

Add this to main.go of your rubik server

_ "github.com/rubikorg/blocks/swagger"

Steps

  • Add Swagger Information about your server into config/default.toml file:
[swagger]
title = "my rubik server"
description = "this is my awesome rubik server"
version = "1.0.1"
termsOfService = "http://www.apache.org/licenses/LICENSE-2.0.html"
  • Go to SERVER_URL/rubik/docs - replace SERVER_URL with your rubik server Base URL.

Block -> HTTP Logger

HTTP Logger block logs the request and response details with result status code.

Usage

  • Add this import to your main.go file
_ "github.com/rubikorg/blocks/logger"

Simple, right?

Changelog

v0.2.6

  • A Route now can have multiple Guards
  • Added Validation Layer after Gurads, with Assertion type
  • Add support for Claims type which can be used as Authorization medium in JWT, Basic etc..
  • Support added for ExtensionBlock which can be used as blocks which does not require server to run but perform operations depending upon server characteristic.
okrubik run --ext

runs the ExtensionBlocks attached to your server.

  • Host and Port both of them are required from the config directory
  • Cleanup unused and unwanted functions
  • Dependency injection is now developer-facing

v0.2.0

  • Changes to Rubik’s Controller signature β€” Everything is a controller - #32
var r := rubik.Route{
	Path: β€œ/”,
	Controller: indexCtl,
}

// === old method signature ===
// fund indexCtlOld(en interface{}) rubik.ByteResponse{
// 	return rubik.Success("Hello World")
// }

// === much cleaner and simple 0.2.0 method signature ===
func indexCtl(req *rubik.Request) {
	// rubik.Failure is now (req.Throw)
	req.Respond(β€œHello World")
}
  • Now supports interop with Go's stdlib -- Any Handler || HandlerFunc can be casted to Controller
var r := rubik.Route{
	Path: β€œ/”,
	Controller: rubik.UseHandlerFunc(indexHandlerFunc), // OR UseHandler() for handlers
}

func indexHandlerFunc(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(&w, β€œHello world")
}
  • okrubik gen router ${name} this command now parses ast properly and adds the new router import and its invocation
  • There is a new method call NewProbe() which helps you with testing rubik.Router(s)
  • CLI is now migrated to cobra instead of vanilla flag.Parse() for scalability reasons - #5
  • Improved API Documentation