utron

package module
v0.0.0-...-bb28a2d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2015 License: MIT Imports: 25 Imported by: 0

README

utron GoDoc Coverage Status Build Status

utron is a lightweight MVC framework in Go (Golang) for building fast, scalable and robust database-driven web applications.

Features

  • Postgres, MySQL and Foundation database support
  • Modular (you can choose which component to use)
  • Middleware support. All alice compatible Middleware works out of the box
  • Gopher spirit (write golang, use all the golang libraries you like)
  • Lightweight. Only MVC
  • Multiple configuration files support (currently json, yaml and toml)

Overview

utron is a lightweight MVC framework. It is based on the principles of simplicity, relevance and elegance.

  • Simplicity. The design is simple, easy to understand and doesn't introduce many layers between you and the standard library. It is a goal of the project that users should understand the whole framework in a single day.

  • Relevance. utron doesn't assume anything. We focus on things that matter, this way we are able to ensure easy maintenance and keeping the system well-organized, well-planned and sweet.

  • Elegance. utron uses golang best practises. We are not afraid of heights, it's just that we need a parachute in our backpack. The source code is heavily documented, any functionality should be well explained and well tested.

Motivation

After two years of playing with golang, I have looked on some of my projects and asked myself: "How golang is that?"

So, utron is my reimagining of lightweight MVC, that maintains the golang spirit, and works seamlessly with the current libraries.

Installation

$ go get github.com/gernest/utron

The MVC

There is nothing revolutionary about MVC that utron brings on the table.

  • M is for models, it is the data structures that helps in data persistence, utron uses gorm an already established Object Relational Mapper for golang. So if you are familiar with gorm then you are good on the M part.

  • V is for Views. Views are templates that render the final output. utron uses golang standard templates. You don't have to learn anything new, just the text/template package to master views.

  • C is for controllers. This is where the application logic stands. In order to achieve modularity, there are some things that utron requires of controllers. This subject is explained in more detail below.

With the power of composition and inheritance, utron achieves a beautiful MVC workflow. I recommend you read the source code, it is well documented so as to demystify any magical unicorns.

We will create a TODO List application in utron to explore all components that makes utron MVC tick. The source code of the final application is included in this repository and can be found here utron todoMVC

TODO list application with utron

Project structure

This is the structure of the todo list application that will showcase how you can build web apps with utron:

todo
├── config
│   ├── app.json
│   ├── app.toml
│   └── app.yml
├── controllers
│   └── todo.go
├── models
│   └── todo.go
├── static
│   └── todo.css
├── views
│   ├── error.html
│   └── index.html
└── main.go

5 directories, 9 files

I have included three configuration files to show how, but you are better off with just one.

Configurations

utron support yaml, json and toml configurations files. In our todo app, we put the configuration files in the config directory. I have included all three formats for clarity, you can be just fine with either one of them.

utron searches for a file named app.json, or app.yml or app.toml in the config directory. The first to be found is the one to be used.

This is the content of config/app.json file:

{
	"app_name": "utron web app",
	"base_url": "http://localhost:8090",
	"port": 8090,
	"verbose": false,
	"static_dir": "static",
	"view_dir": "views",
	"database": "postgres",
	"database_conn": "postgres://postgres:postgres@localhost/todo"
}

You can override the values from the config file by setting environment variables. The names of the environment variables are shown below (with their details)

setting environment name details
app_name APP_NAME application name
base_url BASE_URL the base url to use in your views
port PORT port number the server will listen to
verbose VERBOSE if set to true, will make all state information log to stdout
static_dir STATIC_DIR directory to serve static files e.g. images, js or css
view_dir VIEWS_DIR directory to look for views
database DATABASE the name of the database you use, e.g. postgres, mysql, foundation
database_conn DATABASE_CONN connection string to your database

If you haven't specified explicitly the location of the configuration directory, it defaults to the directory named config in the current working directory.

Models

utron uses the gorm library as its Object Relational Mapper, so you won't need to learn anything fancy. In our todo app, we need to define a Todo model that will be used to store our todo details.

In the file models/todo.go we define our todo model like this

package models

import (
	"time"

	"github.com/gernest/utron"
)

type Todo struct {
	ID        int       `schema: "-"`
	Body      string    `schema:"body"`
	CreatedAt time.Time `schema:"-"`
	UpdatedAt time.Time `schema:"-"`
}

func init() {
	utron.RegisterModels(&Todo{})
}

Notice that we need to register our model by calling utron.RegisterModels(&Todo{}) in the init function otherwise utron won't be aware of the model.

utron will automatically create the table todos if it doesn't exist yet.

Don't be confused by the schema tag, I just added them since we will use the schema package to decode form values(this has nothing to do with utron, you can use whatever form library you fancy.)

Controllers

utron controllers are structs that implement the Controller interface. To help make utron usable, utron provides a BaseController which implements the Controller interface and offers additional conveniences to help in composing reusable code.

You get all the benefits of BaseController by embedding it in your struct. Our TODO Controller is in the controller/todo.go

package controllers

import (
	"net/http"
	"strconv"

	"github.com/gernest/utron"
	"github.com/gernest/utron/fixtures/todo/models"
	"github.com/gorilla/schema"
)

var decoder = schema.NewDecoder()

type TODO struct {
	*utron.BaseController
	Routes []string
}

func (t *TODO) Home() {
	todos := []*models.Todo{}
	t.Ctx.DB.Order("created_at desc").Find(&todos)
	t.Ctx.Data["List"] = todos
	t.Ctx.Template = "index"
	t.HTML(http.StatusOK)
}
func (t *TODO) Create() {
	todo := &models.Todo{}
	req := t.Ctx.Request()
	req.ParseForm()
	if err := decoder.Decode(todo, req.PostForm); err != nil {
		t.Ctx.Data["Message"] = err.Error()
		t.Ctx.Template = "error"
		t.HTML(http.StatusInternalServerError)
		return
	}

	t.Ctx.DB.Create(todo)
	t.Ctx.Redirect("/", http.StatusFound)
}

func (t *TODO) Delete() {
	todoID := t.Ctx.Params["id"]
	ID, err := strconv.Atoi(todoID)
	if err != nil {
		t.Ctx.Data["Message"] = err.Error()
		t.Ctx.Template = "error"
		t.HTML(http.StatusInternalServerError)
		return
	}
	t.Ctx.DB.Delete(&models.Todo{ID: ID})
	t.Ctx.Redirect("/", http.StatusFound)
}

func NewTODO() *TODO {
	return &TODO{
		Routes: []string{
			"get;/;Home",
			"post;/create;Create",
			"get;/delete/{id};Delete",
		},
	}
}

func init() {
	utron.RegisterController(NewTODO())
}

Note we registered our controller by calling utron.RegisterController(NewTODO()) in the init function so as to make utron aware of our controller. See Routing section below for more explanation of what the controller is doing.

Routing

By registering a controller, there are two ways of assigning routes.

case 1- vanilla routing

By registering a Controller, routes are auto-generated for the controller methods. The format is /:controler/:method where :controller is the lowercase name of the Controller, and :method is its method in lowercase.

so (*TODO) Hello() will map to /todo/hello

case 2- Specifying Routes field

The user controller can define a field named Routes it should be of type []string, then you can assign routes by appending route string to the Routes field.

This is a better explanation from comments on the router.go file.

		// if there is any field named Routes, and it is of signature []string
		// then the field's value is used to overide the patterns defined earlier.
		//
		// It is not necessary for every user implementation to define method named Routes
		// If we cant find it then we just ignore its use( fallback to defaults).
		//
		// Route strings, are of the form "httpMethods;path;method"
		// where httMethod: is a comma separated list of http method strings
		//                  e.g GET,POST,PUT.
		//                  The case does not matter, you can use lower case or upper case characters
		//                  or even mixed case, that is get,GET,gET and GeT will all be treated as GET
		//
		//        path:     Is a url path or pattern, utron uses the gorilla mux package. So, everything you can do
		//                  with gorilla mux url path then you can do here.
		//                  e.g /hello/{world}
		//                  Don't worry about the params, they will be accessible via .Ctx.Params field in your
		//                  controller.
		//
		//        method:   The name of the user Controller method to execute for this route.

So, that explains much the following lines in our todo app in controllers/todo.go

func NewTODO() *TODO {
	return &TODO{
		Routes: []string{
			"get;/;Home",
			"post;/create;Create",
			"get;/delete/{id};Delete",
		},
	}
}
case 3: using routes file

You can define routes in a file, the supported formats are json, toml and yaml. The routes file should be in the config directory.

utron will look for file named routes.json, routes.toml or routes.yml in that order, the first to be found is the one to be used.

I have included a sample routes file in fixtures/config/routes.json.

The difference with case 2 above is you will need to specify the name of the controller explicitly. That is for TODO controller, we can define the home route string in routes file like get;/;TODO.Home.

We won't use this in our TODO list app, but you can find it useful in your use case.'

Views

utron views are golang templates. This is the content of views/index.html:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>Utron Todo MVC</title>
	<link href="/static/todo.css" rel="stylesheet">
</head>
<body>
<form method="post" action="/create">
    <table>
        <tr>
           <th>
               Create A TODO
           </th>
        </tr>
        <tr>
            <td>
                <input name="body">
            </td>
            <td>
                <button type="submit">create</button>
            </td>
        </tr>
    </table>
</form>
<table>
    <tr>
        <th>
            My TODO LIST
        </th>
    </tr>
    {{range $k,$v:=.List}}
    <tr>
        <td>
            {{$v.ID}}
        </td>
        <td>
            {{$v.Body}}
        </td>
        <td>
            <a href="/delete/{{$v.ID}}">
                <button>Delete</button>
            </a>
        </td>
    </tr>
    {{end}}
</table>
</body>
</html>

Note that we have access to .List in our view. This is set in the controller, additionally you can access the application configuration via .Config context.

Above is a simple golang template to render our todo list application.

The main.go file

package main

import (
	"github.com/gernest/utron"
	_ "github.com/gernest/utron/fixtures/todo/controllers"
	_ "github.com/gernest/utron/fixtures/todo/models"
)

func main() {
	utron.Run()
}

Running the TODO app

In case you want to run the app we just created, it is included in this repository in fixtures/todo

  • Prerequisite
  • a working database connection (postres, mysql or foundation)
  • golang toolchain installed and the go command in your system $PATH.

step 1 Install utron which will also include the todo app

$ go get github.com/gernest/utron

step 2 cd into the todo app directory

$ cd $GOPATH/src/github.com/gernest/utron/fixtures/todo

step 3 install dependency

$ go get github.com/gorilla/schema

step 4 edit config/app.json by setting database and database_conn to your values

step 5 run the app

go run main.go

If you see something like this

$ 2015/09/15 18:27:24 >>INFO>> starting server at http://localhost:8090

Then everything is okay, open http://localhost:8090 in your browser to start writing your todos. If you experience anything different, redo the steps and make sure you did them in order and with no errors. If so, and it still doesn't work, better open an issue.

Screenshot

todo app with utron

Contributing

Start with clicking the star button to make the author and his neighbors happy. Then fork it and submit a pull request for whatever change you want to be added to this project.

Or, open an issue for any questions.

Author

Geofrey Ernest geofreyernest@live.com

Twitter : @gernesti

Facebook : Geofrey Ernest

Are you hiring?

I have 2 years experience working with golang and 5 years doing web development. I don't have any juicy CV because I'm in Tanzania, and almost everyone I graduated with has no job, never had one and doubts if he/she will ever land one (unless, of course, you are lucky which I'm not.)

And if you are a recruiter, any tips on where I'm doing it wrong will be highly appreciated, because I have my full trust that code will get me out of this abysmal poverty.

Acknowledgement

These are amazing projects that made utron possible.

Roadmap

  • Fix a lot of typos (English is my third language).

Licence

This project is released under MIT licence see LICENCE for more details.

Documentation

Overview

Package utron is a lighweight MVC framework for building fast, scalable and robust web applications

example hello world in utron

type Hello struct {
	*BaseController
}

func (h *Hello) World() {
	h.Ctx.Write([]byte("hello world"))
	h.String(http.StatusOK)
}

Index

Constants

This section is empty.

Variables

View Source
var Content = struct {
	Type        string
	TextPlain   string
	TextHTML    string
	Application struct {
		Form, JSON, MultipartForm string
	}
}{
	"Content-Type", "text/plain", "text/html",
	struct {
		Form, JSON, MultipartForm string
	}{
		"application/x-www-form-urlencoded",
		"application/json",
		"multipart/form-data",
	},
}

Content holds http response content type strings

View Source
var (

	// ErrRouteStringFormat is returned when the route string is of the wrong format
	ErrRouteStringFormat = errors.New("wrong route string, example is\" get,post;/hello/world;Hello\"")
)

Functions

func Migrate

func Migrate()

Migrate runs migrations on the global utron app.

func RegisterController

func RegisterController(ctrl Controller, middlewares ...interface{})

RegisterController register ctrl in the global utron App.

func RegisterModels

func RegisterModels(models ...interface{})

RegisterModels registers models in the global utron App.

func Run

func Run()

Run runs a http server, serving the global utron App.

By using this, you should make sure you followed MVC pattern,

func ServeHTTP

func ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP serves request using global utron App

func SetConfigPath

func SetConfigPath(path string)

SetConfigPath sets the path to look for configurations files in the global utron App.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is the main utron application

func NewApp

func NewApp() *App

NewApp creates a new bare-bone utron application. To use MVC components, you should call Init method before serving requests.

func NewMVC

func NewMVC(cfg ...string) (*App, error)

NewMVC creates a new MVC utron app, if cfg is passed, it should be a directory to look for configuration file. The App returned is initialized.

func (*App) AddController

func (a *App) AddController(ctrl Controller, middlewares ...interface{})

AddController registers ctrl, and middlwares if provided

func (*App) Init

func (a *App) Init() error

Init initializes MVC App

func (*App) ServeHTTP

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP serves http, it can be used with other http.Handler implementations

func (*App) Set

func (a *App) Set(value interface{})

Set assigns value to *App components. The following can be set

Logger by passing Logger
View by passing View
Router by passing *Router
Config by passing *Config
Model by passing *Model

func (*App) SetConfigPath

func (a *App) SetConfigPath(dir string)

SetConfigPath sets dir as a path to search for config files

type BaseController

type BaseController struct {
	Ctx *Context
}

BaseController implements the Controlller interface, It is recommended all user defined Contollers should embed *BaseController.

func (*BaseController) HTML

func (b *BaseController) HTML(code int)

HTML renders text/html with the given code as status code

func (*BaseController) JSON

func (b *BaseController) JSON(code int)

JSON renders application/json with the given code

func (*BaseController) New

func (b *BaseController) New(ctx *Context)

New sets ctx as the active context

func (*BaseController) Render

func (b *BaseController) Render() error

Render commits the changes made in the active context.

func (*BaseController) String

func (b *BaseController) String(code int)

String renders text/plain with given code as status code

type Config

type Config struct {
	AppName      string `json:"app_name" yaml:"app_name" toml:"app_name"`
	BaseURL      string `json:"base_url" yaml:"base_url" toml:"base_url"`
	Port         int    `json:"port" yaml:"port" toml:"port"`
	Verbose      bool   `json:"verbose" yaml:"verbose" toml:"verbose"`
	StaticDir    string `json:"static_dir" yaml:"static_dir" toml:"static_dir"`
	ViewsDir     string `json:"view_dir" yaml:"view_dir" toml:"view_dir"`
	Database     string `json:"database" yaml:"database" toml:"database"`
	DatabaseConn string `json:"database_conn" yaml:"database_conn" toml:"database_conn"`
}

Config stores configurations values

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns the default configuration settings.

func NewConfig

func NewConfig(path string) (*Config, error)

NewConfig reads configuration from path. The format is deduced from the file extension

  • .json - is decoded as json
  • .yml - is decoded as yaml
  • .toml - is decoded as toml

func (*Config) SyncEnv

func (c *Config) SyncEnv() error

SyncEnv overrides c field's values that are set in the environment.

The environment variable names are derived from config fields by underscoring, and uppercasing the name. E.g. AppName will have a corresponding environment variable APP_NAME

NOTE only int, string and bool fields are supported and the corresponding values are set. when the field value is not supported it is ignored.

type Context

type Context struct {

	// Params are the parameters specified in the url patterns
	// utron uses gorilla mux for routing. So basically Params stores results
	// after calling mux.Vars function .
	//
	// e.g. if you have route /hello/{world}
	// when you make request to /hello/gernest , then
	// in the Params, key named world will have value gernest. meaning Params["world"]=="gernest"
	Params map[string]string

	// Data keeps values that are going to be passed to the view as context
	Data map[string]interface{}

	// Template is the name of the template to be rendered by the view
	Template string

	// Cfg is the application configuration
	Cfg *Config

	//DB is the database stuff, with all models registered
	DB *Model
	// contains filtered or unexported fields
}

Context wraps request and response. It provides methods for handling responses

func NewContext

func NewContext(w http.ResponseWriter, r *http.Request) *Context

NewContext creates new context for the given w and r

func (*Context) Commit

func (c *Context) Commit() error

Commit writes the results on the underlying http.Responsewriter and commits the changes. This should be called only once, subsequent calls to this will result in an error.

If there is a view, and the template is specified the the view is rendered and its output is writen to the respose, otherwise any data written to the context is written to the ResponseWriter.

func (*Context) GetData

func (c *Context) GetData(key interface{}) interface{}

GetData retrievess any data stored in the request using gorilla.Context package

func (*Context) HTML

func (c *Context) HTML()

HTML renders text/html response

func (*Context) Init

func (c *Context) Init()

Init initializes the context

func (*Context) JSON

func (c *Context) JSON()

JSON renders JSON response

func (*Context) Redirect

func (c *Context) Redirect(url string, code int)

Redirect redirects request to url using code as status code.

func (*Context) Request

func (c *Context) Request() *http.Request

Request returns the *http.Request object used by the context

func (*Context) Response

func (c *Context) Response() http.ResponseWriter

Response returns the http.ResponseWriter object used by the context

func (*Context) Set

func (c *Context) Set(value interface{})

Set sets value in the cotext object. You can use this to change the following

  • Request by passing *http.Request
  • ResponseWriter by passing http.ResponseVritter
  • view by passing View
  • response status code by passing an int

func (*Context) SetHeader

func (c *Context) SetHeader(key, value string)

SetHeader sets response header

func (*Context) TextPlain

func (c *Context) TextPlain()

TextPlain renders text/plain response

func (*Context) Write

func (c *Context) Write(data []byte) (int, error)

Write writes the data to the context, data is written to the http.ResponseWriter upon calling Commit().

data will only be used when Template is not specified and there is no View set. You can use this for creating API's (which does not depend on views like JSON API's)

type Controller

type Controller interface {
	New(*Context)
	Render() error
}

Controller is an interface for utron controllers

type DefaultLogger

type DefaultLogger struct {
	*log.Logger
}

DefaultLogger is the default logger

func (*DefaultLogger) Errors

func (d *DefaultLogger) Errors(v ...interface{})

Errors log error messages

func (*DefaultLogger) Info

func (d *DefaultLogger) Info(v ...interface{})

Info logs info messages

func (*DefaultLogger) Success

func (d *DefaultLogger) Success(v ...interface{})

Success logs success messages

func (*DefaultLogger) Warn

func (d *DefaultLogger) Warn(v ...interface{})

Warn logs warning messages

type Logger

type Logger interface {
	Info(v ...interface{})
	Errors(fv ...interface{})
	Warn(v ...interface{})
	Success(v ...interface{})
}

Logger is an interface for utron logger

func NewDefaultLogger

func NewDefaultLogger(out io.Writer) Logger

NewDefaultLogger returns a default logger writing to out

type Model

type Model struct {
	*gorm.DB
	// contains filtered or unexported fields
}

Model facilitate database interactions, supports postgres, mysql and foundation

func NewModel

func NewModel() *Model

NewModel returns a new Model without opening database connection

func NewModelWithConfig

func NewModelWithConfig(cfg *Config) (*Model, error)

NewModelWithConfig creates a new model, and opens database connection based on cfg settings

func (*Model) AutoMigrateAll

func (m *Model) AutoMigrateAll()

AutoMigrateAll runs migrations for all the registered models

func (*Model) IsOpen

func (m *Model) IsOpen() bool

IsOpen returns true if the Model has alredy established connecntion to the database

func (*Model) OpenWithConfig

func (m *Model) OpenWithConfig(cfg *Config) error

OpenWithConfig opens database connection with the settings found in cfg

func (*Model) Register

func (m *Model) Register(values ...interface{}) error

Register adds the values to the models registry

type Router

type Router struct {
	*mux.Router
	// contains filtered or unexported fields
}

Router registers routes and handlers. It embeds gorilla mux Router

func NewRouter

func NewRouter(app ...*App) *Router

NewRouter returns a new Router, if app is passed then it is used

func (*Router) Add

func (r *Router) Add(ctrl Controller, middlewares ...interface{}) error

Add registers ctrl. It takes additional comma separated list of middleware. middlewares are of type

func(http.Handler)http.Handler
or
func(*Context)error

utron uses the alice package to chain middlewares, this means all alice compatible middleware works out of the box

func (*Router) LoadRoutesFile

func (r *Router) LoadRoutesFile(file string) error

LoadRoutesFile loads routes from a json file. Example of the routes file.

{
	"routes": [
		"get,post;/hello;Sample.Hello",
		"get,post;/about;Hello.About"
	]
}

supported formats are json,toml and yaml with extension .json, .toml and .yml respectively.

TODO refactor the decoding part to a separate function? This part shares the same logic as the one found in NewConfig()

type SimpleView

type SimpleView struct {
	// contains filtered or unexported fields
}

SimpleView implements View interface, but based on golang templates.

func (*SimpleView) Render

func (s *SimpleView) Render(out io.Writer, name string, data interface{}) error

Render executes template named name, passing data as context, the output is written to out.

type View

type View interface {
	Render(out io.Writer, name string, data interface{}) error
}

View is an interface for rendering templates.

func NewSimpleView

func NewSimpleView(viewDir string) (View, error)

NewSimpleView returns a SimpleView with templates loaded from viewDir

Directories

Path Synopsis
fixtures

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL