Repository: rustless/rustless (language: rust)

Issue:
JSON API example?
Looking at the [postgres example](https://github.com/rustless/rustless/blob/master/examples/postgres/src/main.rs),
and I am wondering if you would be able to extend this to conform to [JSON-API](http://jsonapi.org/),
with Create, Update, and Delete statements as well.

I have previously [attempted something like this using nickel.rs](https://github.com/bguiz/rust-scratch/blob/master/nickel-postgres/src/main.rs#L217),
as this is a use case that I have to satisfy before I can start writing back end code using rust.
I found it rather convoluted at the time with nickel,
and want to know if it would be any easier using rustless -
I'm certainly hoping so!



Discussion / hints:
@bguiz thanks for the suggestion. I'll find some time today to clarify this.

@s-panferov That would be really great fi you did. I'm looking through the rest of this project, thanks for putting it together!


Fix patch:
diff --git a/README.md b/README.md
index 7e6c1391..cb34a8a3 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
 
 - [What is Rustless?](#what-is-rustless)
 - [Basic Usage](#basic-usage)
+- [Complex example](#complex-example)
 - [Mounting](#mounting)
 - [Parameters validation and coercion](#parameters-validation-and-coercion)
 - [Use JSON Schema](#use-json-schema)
@@ -125,6 +126,22 @@ fn main() {
 }
 ~~~
 
+## Complex example
+
+If you want to see how you can write come complex application using Rustless please see the [example](https://github.com/rustless/rustless/tree/master/examples/postgres). 
+
+In that example please note these aspects:
+
+* Complex nested API with versioning.
+* CRUD operations with rust-postgres.
+* Swagger 2.0 intergration.
+* JSON Schema validations.
+* Error reporting.
+* Serializers.
+* File structure.
+* Integration with [docopt](https://github.com/docopt/docopt.rs).
+* Integration with [deuterium-orm](https://github.com/deuterium-orm/deuterium-orm). Database migrations.
+
 ## Mounting
 
 In Rustless you can use three core entities to build your RESTful app: `Api`, `Namespace` and `Endpoint`. 
diff --git a/examples/postgres/Cargo.toml b/examples/postgres/Cargo.toml
index e90e557d..5425afec 100644
--- a/examples/postgres/Cargo.toml
+++ b/examples/postgres/Cargo.toml
@@ -4,6 +4,10 @@ name = "postgres_example"
 version = "0.0.1"
 authors = ["Stanislav Panferov <fnight.m@gmail.com>"]
 
+[[bin]]
+name = "backend"
+path = "src/main.rs"
+
 [dependencies]
 postgres = "*"
 r2d2 = "*"
@@ -12,6 +16,13 @@ typemap = "*"
 time = "*"
 rustc-serialize = "*"
 iron = "*"
+deuterium_orm = "*"
+docopt = "*"
+docopt_macros = "*"
+uuid = "*"
+jsonway = "*"
+valico = "*"
+url = "*"
 
 [dependencies.rustless]
 path = "../.."
\ No newline at end of file
diff --git a/examples/postgres/src/api/mod.rs b/examples/postgres/src/api/mod.rs
new file mode 100644
index 00000000..55ea2ef2
--- /dev/null
+++ b/examples/postgres/src/api/mod.rs
@@ -0,0 +1,29 @@
+use rustless;
+use rustless::errors;
+use rustless::prelude::*;
+use rustless::server::status;
+use jsonway;
+use rustc_serialize::json::ToJson;
+
+mod tweets;
+
+pub fn root() -> rustless::Api {
+    rustless::Api::build(|api| {
+        api.prefix("api");
+        api.version("v1", rustless::Versioning::Path);
+
+        // Add error formatter to send validation errors back to the client
+        api.error_formatter(|error, _media| {
+            if error.is::<errors::Validation>() {
+                let val_err = error.downcast::<errors::Validation>().unwrap();
+                return Some(rustless::Response::from_json(status::StatusCode::Ok, &jsonway::object(|json| {
+                    json.set_json("errors", val_err.reason.to_json())
+                }).unwrap()))
+            }
+
+            None
+        });
+        
+        api.mount(tweets::tweets("tweets"));
+    })
+}
\ No newline at end of file
diff --git a/examples/postgres/src/api/tweets.rs b/examples/postgres/src/api/tweets.rs
new file mode 100644
index 00000000..e40750a7
--- /dev/null
+++ b/examples/postgres/src/api/tweets.rs
@@ -0,0 +1,144 @@
+use rustless;
+use rustless::prelude::*;
+use jsonway::{ObjectSerializer, ArraySerializer};
+use uuid;
+use url;
+
+use super::super::db::DatabaseExt;
+use super::super::models::tweet;
+use super::super::serializers::tweet_serializer;
+
+pub fn tweets(path: &str) -> rustless::Namespace {
+    rustless::Namespace::build(path, |tweets| {
+        
+        tweets.get("latest", |endpoint| {
+            endpoint.desc("Get latest tweets");
+            endpoint.handle(|client, _params| {
+                // Note that .db() is an extension methods that we created with DatabaseExt
+                let cn = client.app.db();
+                let tweets = tweet::Tweet::latest(&*cn);
+                client.json(&tweet_serializer::TweetListSerializer::new(&tweets).serialize(true))
+            })
+        });
+
+        tweets.post("", |endpoint| {
+            endpoint.desc("Create new tweet");
+            endpoint.params(|params| {
+                params.req("tweet", |tweet| {
+                    tweet.desc("Tweet model in JSON format");
+                    tweet.schema(|tweet| {
+                        tweet.id("http://tweet.example.com/tweet-short");
+                        tweet.object();
+                        tweet.properties(|props| {
+                            props.insert("author_name", |author_name| {
+                                author_name.string();
+                                author_name.max_length(30);
+                            });
+                            props.insert("content", |content| {
+                                content.string();
+                                content.max_length(140);
+                            })
+                        });
+                        tweet.required(vec![
+                            "author_name".to_string(), 
+                            "content".to_string()
+                        ]);
+                        tweet.additional_properties(false);
+                    })
+                })
+            });
+            endpoint.handle(|client, params| {
+                // Note that .db() is an extension methods that we created with DatabaseExt
+                let cn = client.app.db();
+                let tweet = params.find("tweet").unwrap();
+
+                let tweet = tweet::Tweet::new(
+                    tweet.find("author_name").unwrap().as_string().unwrap().to_string(),
+                    tweet.find("content").unwrap().as_string().unwrap().to_string()
+                );
+
+                tweet.create(&*cn);
+                client.json(&tweet_serializer::TweetSerializer.serialize(&tweet, true))
+            })
+        });
+
+        tweets.namespace(":tweet_id", |single| {
+            single.params(|params| {
+                params.req("tweet_id", |tweet_id| {
+                    tweet_id.desc("Tweet ID in UUID format");
+                })
+            });
+
+            single.get("", |endpoint| {
+                endpoint.desc("Get tweet by ID");
+                endpoint.handle(|mut client, params| {
+                    // Note that .db() is an extension methods that we created with DatabaseExt
+                    let cn = client.app.db();
+                    let id: uuid::Uuid = params.find("tweet_id").unwrap().as_string().unwrap().parse().unwrap();
+                    let tweet = tweet::Tweet::find(id, &*cn);
+
+                    if tweet.is_some() {
+                        client.json(&tweet_serializer::TweetSerializer.serialize(&tweet.unwrap(), true))
+                    } else {
+                        client.not_found();
+                        client.empty()
+                    }
+                })
+            });
+
+            single.put("", |endpoint| {
+                endpoint.desc("Update tweet");
+                endpoint.params(|params| {
+                    params.req("tweet", |tweet| {
+                        tweet.desc("Tweet model in JSON format");
+                        tweet.schema_id(url::Url::parse("http://tweet.example.com/tweet-short").unwrap())
+                    })
+                });
+                endpoint.handle(|mut client, params| {
+                    // Note that .db() is an extension methods that we created with DatabaseExt
+                    let cn = client.app.db();
+                    let tweet_params = params.find("tweet").unwrap();
+                    let id: uuid::Uuid = params.find("tweet_id").unwrap().as_string().unwrap().parse().unwrap();
+
+                    let tweet = tweet::Tweet::find(id, &*cn);
+
+                    if tweet.is_some() {
+                        let mut tweet = tweet.unwrap();
+                        let new_author_name = tweet_params.find("author_name").unwrap().as_string().unwrap().to_string();
+                        let new_content = tweet_params.find("content").unwrap().as_string().unwrap().to_string();
+                        tweet.set_author_name(new_author_name);
+                        tweet.set_content(new_content);
+
+                        tweet.update(&*cn);
+                        client.json(&tweet_serializer::TweetSerializer.serialize(&tweet, true))
+                    } else {
+                        client.not_found();
+                        client.empty()
+                    }
+                })
+            });
+
+            single.delete("", |endpoint| {
+                endpoint.desc("Delete tweet");
+                endpoint.handle(|mut client, params| {
+                    // Note that .db() is an extension methods that we created with DatabaseExt
+                    let cn = client.app.db();
+                    let id: uuid::Uuid = params.find("tweet_id").unwrap().as_string().unwrap().parse().unwrap();
+
+                    let tweet = tweet::Tweet::find(id, &*cn);
+
+                    if tweet.is_some() {
+                        let tweet = tweet.unwrap();
+                        tweet.delete(&*cn);
+                        client.json(&tweet_serializer::TweetSerializer.serialize(&tweet, true))
+                    } else {
+                        client.not_found();
+                        client.empty()
+                    }
+                })
+            });
+
+        })
+        
+    })
+}
\ No newline at end of file
diff --git a/examples/postgres/src/db/migrations/_150210064045_create_tweets.rs b/examples/postgres/src/db/migrations/_150210064045_create_tweets.rs
new file mode 100644
index 00000000..28ff2b8d
--- /dev/null
+++ b/examples/postgres/src/db/migrations/_150210064045_create_tweets.rs
@@ -0,0 +1,24 @@
+pub use postgres::Connection;
+pub use deuterium_orm::migration::RawMigration;
+
+#[derive(Debug)]
+pub struct CreateTweets;
+impl RawMigration<Connection> for CreateTweets {
+    fn up(&self, cn: &Connection) {
+        let t = cn.transaction().unwrap();
+        t.execute("CREATE TABLE tweets (
+            id               UUID PRIMARY KEY,
+            author_name       VARCHAR(30),
+            content          VARCHAR(140),
+            created_at       timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
+            updated_at       timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL 
+        )", &[]).unwrap();
+
+        t.set_commit();
+        t.finish().unwrap();
+    }
+
+    fn down(&self, cn: &Connection) {
+        cn.execute("DROP TABLE tweets;", &[]).unwrap(); 
+    }
+}
\ No newline at end of file
diff --git a/examples/postgres/src/db/migrations/mod.rs b/examples/postgres/src/db/migrations/mod.rs
new file mode 100644
index 00000000..1ffe5f99
--- /dev/null
+++ b/examples/postgres/src/db/migrations/mod.rs
@@ -0,0 +1,29 @@
+macro_rules! migrations {
+    ($(($m:ident, $ver: expr, $name:ident)),*) => (
+
+        // FIMXE Move this to `deuterium-orm` after it will work there.
+        pub fn migrations<'a>() -> ::deuterium_orm::migration::Migrations {
+            let mut migrations = Vec::new();
+
+            $(
+                mod $m;
+
+                let name = stringify!($name);
+                let migration = ::deuterium_orm::migration::Migration::new(
+                    $ver,
+                    name,
+                    box $m::$name as Box<::deuterium_orm::migration::RawMigration<::postgres::Connection>>
+                );
+
+                migrations.push(box migration);   
+            )*
+
+            migrations
+        }
+        
+    )
+}
+
+load_migrations! { 
+    "src/db/migrations"
+}
\ No newline at end of file
diff --git a/examples/postgres/src/db/mod.rs b/examples/postgres/src/db/mod.rs
new file mode 100644
index 00000000..414c5d18
--- /dev/null
+++ b/examples/postgres/src/db/mod.rs
@@ -0,0 +1,28 @@
+use rustless::{self, Extensible};
+
+// A set of helpful types we need to shorten things
+pub use deuterium_orm::adapter::postgres::PostgresPool;
+pub use deuterium_orm::adapter::postgres::PostgresPooledConnection;
+pub use deuterium_orm::adapter::postgres::setup;
+
+pub use postgres::GenericConnection as Connection;
+
+pub mod migrations;
+
+// A unit struct we need to store our pool into TypeMap
+// See `typemap` docs for clarification.
+pub struct AppDb;
+impl ::typemap::Key for AppDb {
+    type Value = PostgresPool;
+}
+
+// Let's create a shorthand function to simplify a DB access within our
+// endpoints.
+pub trait DatabaseExt: rustless::Extensible {
+    fn db(&self) -> PostgresPooledConnection;
+}
+impl DatabaseExt for rustless::Application {
+    fn db(&self) -> PostgresPooledConnection {
+        self.ext().get::<AppDb>().unwrap().get().unwrap()
+    }
+}
\ No newline at end of file
diff --git a/examples/postgres/src/main.rs b/examples/postgres/src/main.rs
index e0854245..fc3bb60f 100644
--- a/examples/postgres/src/main.rs
+++ b/examples/postgres/src/main.rs
@@ -1,130 +1,132 @@
 #![feature(env)]
+#![feature(plugin)]
+#![feature(path)]
+#![feature(core)]
 
 extern crate postgres;
-extern crate r2d2;
-extern crate r2d2_postgres;
+#[plugin] #[macro_use]
+extern crate deuterium_orm;
 extern crate rustless;
 extern crate typemap;
 extern crate time;
 extern crate "rustc-serialize" as rustc_serialize;
 extern crate iron;
+extern crate docopt;
+#[plugin] #[no_link] 
+extern crate docopt_macros;
+extern crate uuid;
+extern crate jsonway;
+extern crate valico;
+extern crate url;
 
 use std::env;
-use rustc_serialize::json;
-
 use rustless::prelude::*;
+use rustless::batteries::schemes;
+use rustless::batteries::swagger;
+use valico::json_schema;
+use std::old_io::net::ip;
+
+use self::db::DatabaseExt;
+
+mod db;
+mod serializers;
+mod models;
+mod api;
+
+docopt!(Args derive Debug, "
+Example backend.
+
+Usage:
+  backend run
+  backend g migration <migration-name>
+  backend db migrate [<version>]
+  backend db rollback [<steps>]
+  backend --version
+
+Options:
+  -h --help        Show this screen.
+  --version        Show version.
+  --ip=<ip>        Specify server ip [default: 127.0.0.1]
+  --port=<port>    Specify server port [default: 3001]
+");
+
+fn run_db(app: &mut rustless::Application) {
+    let connection_str = env::var_string("POSTGRES_CONNECTION")
+        .ok().expect("Provide POSTGRES_CONNECTION environment variable");
+    let pool = self::db::setup(&connection_str[], 5);
 
-// A set of helpful types we need to shorten things
-pub type PostgresPool = r2d2::Pool<r2d2_postgres::PostgresConnectionManager>;
-pub type PostgresPooledConnection<'a> = r2d2::PooledConnection<'a, r2d2_postgres::PostgresConnectionManager>;
-
-// A unit struct we need to store our pool into TypeMap
-// See `typemap` docs for clarification.
-pub struct AppDb;
-impl ::typemap::Key for AppDb {
-    type Value = PostgresPool;
+    // Here we use TypeMap to store out database pool
+    app.ext.insert::<self::db::AppDb>(pool);
 }
 
-// Let's create a shorthand function to simplify a DB access within our
-// endpoints.
-pub trait DatabaseExt: rustless::Extensible {
-    fn db(&self) -> PostgresPooledConnection;
-}
-impl DatabaseExt for rustless::Application {
-    fn db(&self) -> PostgresPooledConnection {
-        self.ext().get::<AppDb>().unwrap().get().unwrap()
-    }
-}
+#[allow(dead_code)]
+fn main() {
+    let mut app = rustless::Application::new(self::api::root());
+
+    swagger::enable(&mut app, swagger::Spec {
+        info: swagger::Info {
+            title: "Example tweet API".to_string(),
+            description: Some("Simple API to demonstration".to_string()),
+            contact: Some(swagger::Contact {
+                name: "Stanislav Panferov".to_string(),
+                url: Some("http://panferov.me".to_string()),
+                ..std::default::Default::default()
+            }),
+            license: Some(swagger::License {
+                name: "MIT".to_string(),
+                url: "http://opensource.org/licenses/MIT".to_string()
+            }),
+            ..std::default::Default::default()
+        },
+        ..std::default::Default::default()
+    });
 
-/// Our main model
-#[derive(Debug, RustcEncodable)]
-pub struct Jedi {
-    id: i32,
-    name: String,
-    force_level: i32
-}
+    let args: Args = Args::docopt().decode().unwrap_or_else(|e| e.exit());
+    println!("{:?}", args);
 
-/// This function creates a pool of connections to PostgreSQL database
-/// using `r2d2_postgres` functionality. See `r2d2_postgres` docs for clarification.
-#[allow(dead_code)]
-pub fn setup_db(connection_str: &str, pool_size: u32) -> PostgresPool {
-    let manager = r2d2_postgres::PostgresConnectionManager::new(connection_str, postgres::SslMode::None);
-    let config = r2d2::Config {
-        pool_size: pool_size,
-        test_on_check_out: true,
-        ..::std::default::Default::default()
-    };
-
-    let handler = Box::new(r2d2::NoopErrorHandler);
-    r2d2::Pool::new(config, manager, handler).unwrap()
-}
+    if args.flag_version {
 
-/// The function creates and fills the main table of our data.
-fn setup_tables(cn: &postgres::GenericConnection) {
-   cn.batch_execute(r#"
-        DROP TABLE IF EXISTS jedi CASCADE;
-        CREATE TABLE jedi (
-            id          serial PRIMARY KEY,
-            name        varchar(40) NOT NULL,
-            force_level integer NOT NULL
-        );
+        println!("0.0.0-dev")
 
-        INSERT INTO jedi (name, force_level) VALUES
-            ('Luke Skywalker', 100),
-            ('Mace Windu', 90),
-            ('Obi-Wan Kenoby', 99),
-            ('Kit Fisto', 70),
-            ('Count Dooku', 99),
-            ('Darth Maul', 70),
-            ('Anakin Skywalker', 100);
+    } else if args.cmd_run {
 
-    "#).unwrap();
-}
+        app.root_api.mount(swagger::create_api("api-docs"));
+        schemes::enable_schemes(&mut app, json_schema::Scope::new()).unwrap();
 
-#[allow(dead_code)]
-fn main() {
-    let api = rustless::Api::build(|api| {
-        api.prefix("api");
-        api.version("v1", rustless::Versioning::Path);
-        
-        api.get("jedi", |endpoint| {
-            endpoint.handle(|mut client, _params| {
-                // Note that .db() is an extension methods that we created with DatabaseExt
-                let cn = client.app.db();
-
-                // See `rust-postgres` docs for clarification.
-                let statement = cn.prepare("SELECT id, name, force_level FROM jedi;").ok().expect("Connection is broken");
-                let jedi_list: Vec<Jedi> = statement.query(&[]).unwrap().map(|row| {
-                    Jedi {
-                        id: row.get(0),
-                        name: row.get(1),
-                        force_level: row.get(2)
-                    }
-                }).collect();
-
-                // Here we set right content type and send our answer back to the client
-                client.set_json_content_type();
-                client.text(json::encode(&jedi_list).ok().unwrap())
-            })
-        });
-    });
+        run_db(&mut app);
 
-    let mut app = rustless::Application::new(api);
-    let connection_str = env::var_string("POSTGRES_CONNECTION").ok().expect("Provide POSTGRES_CONNECTION environment variable");
-    let pool = setup_db(&connection_str[], 5);
+        // See `iron` docs for clarification.
+        let chain = iron::Chain::new(app);
 
-    {
-        // We create a new scope because we need the connection only for `setup_tables(..)` call
-        let connection = pool.get().unwrap();
-        setup_tables(&*connection);
-    }
+        let host: ip::IpAddr = args.flag_ip.parse().unwrap();
+        let port: u16 = args.flag_port.parse().unwrap();
 
-    // Here we use TypeMap to store out database pool
-    app.ext.insert::<AppDb>(pool);
+        iron::Iron::new(chain).listen((host, port)).unwrap();
+
+        println!("On {}", port);
+
+    } else if args.cmd_db && args.cmd_migrate {
+
+        run_db(&mut app);
+        deuterium_orm::migration::run(&db::migrations::migrations(), &*app.db());
 
-    // See `iron` docs for clarification.
-    let chain = iron::Chain::new(app);
-    iron::Iron::new(chain).listen("localhost:4000").unwrap();
+    } else if args.cmd_db && args.cmd_rollback {
 
-    println!("On 4000");
+        run_db(&mut app);
+        deuterium_orm::migration::rollback(
+          args.arg_steps.parse().unwrap_or(1), 
+          &db::migrations::migrations(),
+          &*app.db()
+        );
+
+    } else if args.cmd_g && args.cmd_migration {
+
+        let name = deuterium_orm::migration::create_migration_file(
+          &args.arg_migration_name[], 
+          Path::new("src/db/migrations")
+        );
+
+        println!("Migration with name {} was generated.", name);
+
+    }
 }
\ No newline at end of file
diff --git a/examples/postgres/src/models/mod.rs b/examples/postgres/src/models/mod.rs
new file mode 100644
index 00000000..b6c7d457
--- /dev/null
+++ b/examples/postgres/src/models/mod.rs
@@ -0,0 +1,1 @@
+pub mod tweet;
\ No newline at end of file
diff --git a/examples/postgres/src/models/tweet.rs b/examples/postgres/src/models/tweet.rs
new file mode 100644
index 00000000..e0861ecd
--- /dev/null
+++ b/examples/postgres/src/models/tweet.rs
@@ -0,0 +1,93 @@
+use uuid;
+use time;
+
+use super::super::db;
+
+pub struct Tweet {
+    id: uuid::Uuid,
+    author_name: String,
+    content: String,
+    created_at: time::Timespec,
+    updated_at: time::Timespec
+}
+
+impl Tweet {
+
+    pub fn get_id(&self) -> &uuid::Uuid { &self.id }
+    pub fn get_author_name(&self) -> &str { self.author_name.as_slice() }
+    pub fn get_content(&self) -> &str { self.content.as_slice() }
+    pub fn get_created_at(&self) -> &time::Timespec { &self.created_at }
+    pub fn get_updated_at(&self) -> &time::Timespec { &self.updated_at }
+
+    pub fn set_author_name(&mut self, author_name: String) { self.author_name = author_name; }
+    pub fn set_content(&mut self, content: String) { self.content = content; }
+
+    pub fn new(author_name: String, content: String) -> Tweet {
+        Tweet {
+            id: uuid::Uuid::new_v4(),
+            author_name: author_name,
+            content: content,
+            created_at: time::get_time(),
+            updated_at: time::get_time()
+        }
+    }
+
+    pub fn latest(cn: &db::Connection) -> Vec<Tweet> {
+        let st = cn.prepare("SELECT * FROM tweets ORDER BY updated_at DESC LIMIT 10").unwrap();
+        let mut tweets = vec![];
+        for tweet_row in st.query(&[]).unwrap() {
+            let tweet = Tweet {
+                id: tweet_row.get("id"),
+                author_name: tweet_row.get("author_name"),
+                content: tweet_row.get("content"),
+                created_at: tweet_row.get("created_at"),
+                updated_at: tweet_row.get("updated_at"),
+            };
+
+            tweets.push(tweet);
+        }
+
+        tweets
+    }
+
+    pub fn find(id: uuid::Uuid, cn: &db::Connection) -> Option<Tweet> {
+        let st = cn.prepare("SELECT * FROM tweets WHERE id = $1 LIMIT 1").unwrap();
+        for tweet_row in st.query(&[&id]).unwrap() {
+            return Some(Tweet {
+                id: tweet_row.get("id"),
+                author_name: tweet_row.get("author_name"),
+                content: tweet_row.get("content"),
+                created_at: tweet_row.get("created_at"),
+                updated_at: tweet_row.get("updated_at"),
+            })
+        }
+
+        None
+    }
+
+    pub fn create(&self, cn: &db::Connection) {
+        cn.execute("INSERT INTO tweets VALUES ($1, $2, $3, $4, $5);", &[
+            &self.id,
+            &self.author_name,
+            &self.content,
+            &self.created_at,
+            &self.updated_at
+        ]).unwrap();
+    }
+
+    pub fn update(&mut self, cn: &db::Connection) {
+        cn.execute("UPDATE tweets SET (author_name, content, created_at, updated_at) = ($2, $3, $4, $5) WHERE tweets.id = $1;", &[
+            &self.id,
+            &self.author_name,
+            &self.content,
+            &self.created_at,
+            &self.updated_at
+        ]).unwrap();
+    }
+
+    pub fn delete(&self, cn: &db::Connection) {
+        cn.execute("DELETE FROM tweets WHERE tweets.id = $1;", &[
+            &self.id,
+        ]).unwrap();
+    }
+}
\ No newline at end of file
diff --git a/examples/postgres/src/serializers/mod.rs b/examples/postgres/src/serializers/mod.rs
new file mode 100644
index 00000000..a30544e3
--- /dev/null
+++ b/examples/postgres/src/serializers/mod.rs
@@ -0,0 +1,1 @@
+pub mod tweet_serializer;
\ No newline at end of file
diff --git a/examples/postgres/src/serializers/tweet_serializer.rs b/examples/postgres/src/serializers/tweet_serializer.rs
new file mode 100644
index 00000000..0775db8f
--- /dev/null
+++ b/examples/postgres/src/serializers/tweet_serializer.rs
@@ -0,0 +1,38 @@
+use jsonway::{self, ObjectSerializer};
+use time;
+
+use super::super::models::tweet;
+
+pub struct TweetSerializer;
+impl jsonway::ObjectSerializer<tweet::Tweet> for TweetSerializer {
+    fn root(&self) -> Option<&str> { Some("tweet") }
+    fn build(&self, tweet: &tweet::Tweet, json: &mut jsonway::ObjectBuilder) {
+        json.set("id", tweet.get_id().to_string());
+        json.set("author_name", tweet.get_author_name().to_string());
+        json.set("content", tweet.get_content().to_string());
+        json.set("created_at", time::at_utc(tweet.get_created_at().clone()).rfc3339().to_string());
+        json.set("updated_at", time::at_utc(tweet.get_updated_at().clone()).rfc3339().to_string());
+    }
+}
+
+pub struct TweetListSerializer<'a> {
+    tweets: &'a Vec<tweet::Tweet>
+}
+
+impl<'a> TweetListSerializer<'a> {
+    pub fn new(tweets: &'a Vec<tweet::Tweet>) -> TweetListSerializer<'a> {
+        TweetListSerializer {
+            tweets: tweets
+        }
+    }
+}
+
+impl<'a> jsonway::ArraySerializer for TweetListSerializer<'a> {
+    fn root(&self) -> Option<&str> { Some("tweets") }
+    fn build(&self, array: &mut jsonway::ArrayBuilder) {
+        for tweet in self.tweets.iter() {
+            array.push(TweetSerializer.serialize(tweet, false));
+        }
+    }
+}
+


Test patch: