README
¶
Introducing deepcopier, a Go library to make copying of structs a bit easier
============================================================================
Context
-------
We are currently refactoring our API_ at Ulule from our monolithic Python
stack with `django-tastypie`_ to a separate Go microservice_.
When working with models in Go, you don't want to expose all columns and
also implement more methods without writing a lot of code, because everyone
knows programmers are lazy ;)
deepcopier_ will help you in your daily job when you want to copy a struct into
another one (think resource) or from another one (think payload).
Installation
------------
Assuming you are already a Go developer, you have your environment up and ready,
so run this command in your shell:
::
$ go get github.com/ulule/deepcopier
You are now ready to use this library.
Usage
-----
To demonstrate why you should use this library, we will build a dead simple REST
API in READ only.
We will use postgresql_ as database so I'm also assuming you
already have postgresql_ installed on your laptop :)
Let's create the databass!
::
$ psql postgres
psql (9.4.1)
Type "help" for help.
postgres=# create user dummy with password '';
CREATE ROLE
postgres=# create database dummy with owner dummy;
CREATE DATABASE
postgres=# \d
No relations found.
We now have a perfectly capable database with no tables, let's jump to the
SQL schema.
.. code-block:: sql
CREATE TABLE account (
id serial PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
username VARCHAR (50) UNIQUE NOT NULL,
password VARCHAR (50) NOT NULL,
email VARCHAR (355) UNIQUE NOT NULL,
date_joined TIMESTAMP NOT NULL
);
Transfer this schema to postgresql_.
::
$ psql -U dummy
psql (9.4.1)
Type "help" for help.
dummy=# CREATE TABLE account(
dummy(# id serial PRIMARY KEY,
dummy(# first_name VARCHAR (50),
dummy(# last_name VARCHAR (50),
dummy(# username VARCHAR (50) UNIQUE NOT NULL,
dummy(# password VARCHAR (50) NOT NULL,
dummy(# email VARCHAR (355) UNIQUE NOT NULL,
dummy(# date_joined TIMESTAMP NOT NULL
dummy(# );
CREATE TABLE
dummy=# \d
List of relations
Schema | Name | Type | Owner
--------+----------------+----------+-------
public | account | table | thoas
public | account_id_seq | sequence | thoas
(2 rows)
dummy=#
First insertions incoming!
::
dummy=# INSERT INTO account (username, first_name, last_name, password, email, date_joined) VALUES ('thoas', 'Florent', 'Messa', '8d56e93bcc8d63a171b5630282264341', 'foo@bar.com', '2015-07-31 15:10:10');
At this point, we have a schema in a great database, we need to setup our
REST API.
We will use:
* `go-json-rest`_ to handle requests
* gorm_ to manipulate the database as an ORM
In your shell, run this to install them
::
$ go get -u github.com/jinzhu/gorm
$ go get github.com/ant0ine/go-json-rest/rest
We will define a first attempt of our API to retrieve user information based
on its username.
We will rewrite our API three times so you need to focus.
.. code-block:: go
// main.go
package main
import (
"fmt"
"github.com/ant0ine/go-json-rest/rest"
"github.com/jinzhu/gorm"
_ "github.com/lib/pq"
"log"
"net/http"
"os"
"time"
)
type Account struct {
ID uint `gorm:"primary_key"`
FirstName string
LastName string
Username string
Password string
Email string
DateJoined time.Time
}
type Accounts struct {
Db gorm.DB
}
func (a *Accounts) Detail(w rest.ResponseWriter, r *rest.Request) {
account := &Account{}
result := a.Db.First(&account, "username = ?", r.PathParam("username"))
if result.RecordNotFound() {
rest.NotFound(w, r)
return
}
w.WriteJson(&account)
}
func main() {
dsn := fmt.Sprintf("user=%s dbname=%s sslmode=disable",
os.Getenv("DATABASE_USER"),
os.Getenv("DATABASE_NAME"))
db, err := gorm.Open("postgres", dsn)
fmt.Println(dsn)
if err != nil {
panic(err)
}
db.DB()
db.DB().Ping()
db.DB().SetMaxIdleConns(10)
db.DB().SetMaxOpenConns(100)
db.SingularTable(true)
db.LogMode(true)
api := rest.NewApi()
api.Use(rest.DefaultDevStack...)
accounts := &Accounts{Db: db}
router, err := rest.MakeRouter(
rest.Get("/users/:username", accounts.Detail),
)
if err != nil {
log.Fatal(err)
}
api.SetApp(router)
log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}
Let's start the server then
::
$ DATABASE_USER=dummy DATABASE_NAME=dummy go run main.go
and retrieve the response.
::
$ curl http://localhost:8080/users/thoas
{
"ID": 1,
"Username": "thoas",
"FirstName": "Florent",
"LastName": "Messa",
"Password": "8d56e93bcc8d63a171b5630282264341",
"Email": "foo@bar.com",
"DateJoined": "2015-07-31T15:10:10Z"
}
Wait a minute? You are exposing the user's password... this not
what we are excepting... We want this specific format
.. code-block:: json
{
"id": 1,
"username": "thoas",
"first_name": "Florent",
"last_name": "Messa",
"name": "Florent Messa",
"email": "foo@bar.com",
"date_joined": "2015-07-31T15:10:10Z",
"api_url": "http://localhost:8080/users/thoas"
}
Implement a separate struct named ``AccountResource``
.. code-block:: go
type AccountResource struct {
ID uint `json:"id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Name string `json:"name"`
Email string `json:"email"`
DateJoined time.Time `json:"date_joined"`
}
func (a Account) Name() string {
return fmt.Sprintf("%s %s", a.FirstName, a.LastName)
}
and rewrite ``Accounts.Detail`` to use deepcopier_
.. code-block:: go
func (a *Accounts) Detail(w rest.ResponseWriter, r *rest.Request) {
account := &Account{}
result := a.Db.First(&account, "username = ?", r.PathParam("username"))
if result.RecordNotFound() {
rest.NotFound(w, r)
return
}
resource := &AccountResource{}
deepcopier.Copy(account).To(resource)
w.WriteJson(&resource)
}
We are good now, we can inspect our result
::
$ curl http://localhost:8080/users/thoas
{
"id": 1,
"username": "thoas",
"first_name": "Florent",
"last_name": "Messa",
"name": "Florent Messa",
"email": "foo@bar.com",
"date_joined": "2015-07-31T15:10:10Z"
}
Easy, right?
We will now rewrite for the last time ``Accounts.Detail`` to provide
some context to retrieve the base url in ``api_url`` attribute.
.. code-block:: go
func (a *Accounts) Detail(w rest.ResponseWriter, r *rest.Request) {
account := &Account{}
result := a.Db.First(&account, "username = ?", r.PathParam("username"))
if result.RecordNotFound() {
rest.NotFound(w, r)
return
}
resource := &AccountResource{}
context := map[string]interface{}{"base_url": r.BaseUrl()}
deepcopier.Copy(account).WithContext(context).To(resource)
w.WriteJson(&resource)
}
We need to update ``AccountResource`` to implement the ``ApiUrl`` new method
.. code-block:: go
type AccountResource struct {
ID uint `json:"id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Name string `json:"name"`
Email string `json:"email"`
DateJoined time.Time `json:"date_joined"`
ApiUrl string `deepcopier:"context" json:"api_url"`
}
func (a Account) Name() string {
return fmt.Sprintf("%s %s", a.FirstName, a.LastName)
}
func (a Account) ApiUrl(context map[string]interface{}) string {
return fmt.Sprintf("%s/users/%s", context["base_url"], a.Username)
}
We have now the final result of what we excepted for the first time :)
::
$ curl http://localhost:8080/users/thoas
{
"id": 1,
"username": "thoas",
"first_name": "Florent",
"last_name": "Messa",
"name": "Florent Messa",
"email": "foo@bar.com",
"date_joined": "2015-07-31T15:10:10Z",
"api_url": "http://localhost:8080/users/thoas"
}
If you have reached to the bottom you belong to the brave!
It has been a long introduction, hope your enjoy it!
Contributing to deepcopier
--------------------------
* Ping us on twitter `@oibafsellig <https://twitter.com/oibafsellig>`_, `@thoas <https://twitter.com/thoas>`_
* Fork the `project <https://github.com/ulule/deepcopier>`_
* Fix `bugs <https://github.com/ulule/deepcopier/issues>`_
Don't hesitate ;)
.. _API: http://developers.ulule.com/
.. _django-tastypie: https://github.com/django-tastypie/django-tastypie
.. _microservice: http://martinfowler.com/articles/microservices.html
.. _React.js: http://facebook.github.io/react/
.. _postgresql: http://www.postgresql.org/
.. _go-json-rest: https://github.com/ant0ine/go-json-rest
.. _gorm: https://github.com/jinzhu/gorm
.. _deepcopier: https://github.com/ulule/deepcopier
Click to show internal directories.
Click to hide internal directories.