From 2699bf618778808dcb25fea2ceab6889540bbe26 Mon Sep 17 00:00:00 2001
From: Jack Christensen <jack@jackchristensen.com>
Date: Mon, 5 Aug 2013 13:08:45 -0500
Subject: [PATCH] Added example url shortener

---
 examples/url_shortener/README.md     |  25 ++++++
 examples/url_shortener/main.go       | 120 +++++++++++++++++++++++++++
 examples/url_shortener/structure.sql |   4 +
 3 files changed, 149 insertions(+)
 create mode 100644 examples/url_shortener/README.md
 create mode 100644 examples/url_shortener/main.go
 create mode 100644 examples/url_shortener/structure.sql

diff --git a/examples/url_shortener/README.md b/examples/url_shortener/README.md
new file mode 100644
index 00000000..cc04d600
--- /dev/null
+++ b/examples/url_shortener/README.md
@@ -0,0 +1,25 @@
+# Description
+
+This is a sample REST URL shortener service implemented using pgx as the connector to a PostgreSQL data store.
+
+# Usage
+
+Create a PostgreSQL database and run structure.sql into it to create the necessary data schema.
+
+Edit connectionOptions in main.go with the location and credentials for your database.
+
+Run main.go:
+
+    go run main.go
+
+## Create or Update a Shortened URL
+
+    curl -X PUT -d 'http://www.google.com' http://localhost:8080/google
+
+## Get a Shortened URL
+
+    curl http://localhost:8080/google
+
+## Delete a Shortened URL
+
+    curl -X DELETE http://localhost:8080/google
diff --git a/examples/url_shortener/main.go b/examples/url_shortener/main.go
new file mode 100644
index 00000000..54df21ba
--- /dev/null
+++ b/examples/url_shortener/main.go
@@ -0,0 +1,120 @@
+package main
+
+import (
+	"fmt"
+	"github.com/jackc/pgx"
+	"io/ioutil"
+	"net/http"
+	"os"
+)
+
+var pool *pgx.ConnectionPool
+
+// afterConnect creates the prepared statements that this application uses
+func afterConnect(conn *pgx.Connection) (err error) {
+	err = conn.Prepare("getUrl", `
+    select url from shortened_urls where id=$1
+  `)
+	if err != nil {
+		return
+	}
+
+	err = conn.Prepare("deleteUrl", `
+    delete from shortened_urls where id=$1
+  `)
+	if err != nil {
+		return
+	}
+
+	// There technically is a small race condition in doing an upsert with a CTE
+	// where one of two simultaneous requests to the shortened URL would fail
+	// with a unique index violation. As the point of this demo is pgx usage and
+	// not how to perfectly upsert in PostgreSQL it is deemed acceptable.
+	err = conn.Prepare("putUrl", `
+    with upsert as (
+      update shortened_urls
+      set url=$2
+      where id=$1
+      returning *
+    )
+    insert into shortened_urls(id, url)
+    select $1, $2 where not exists(select 1 from upsert)
+  `)
+	return
+}
+
+func getUrlHandler(w http.ResponseWriter, req *http.Request) {
+	if url, err := pool.SelectValue("getUrl", req.URL.Path); err == nil {
+		http.Redirect(w, req, url.(string), http.StatusSeeOther)
+	} else if _, ok := err.(pgx.NotSingleRowError); ok {
+		http.NotFound(w, req)
+	} else {
+		http.Error(w, "Internal server error", http.StatusInternalServerError)
+	}
+}
+
+func putUrlHandler(w http.ResponseWriter, req *http.Request) {
+	id := req.URL.Path
+	var url string
+	if body, err := ioutil.ReadAll(req.Body); err == nil {
+		url = string(body)
+	} else {
+		http.Error(w, "Internal server error", http.StatusInternalServerError)
+		return
+	}
+
+	if _, err := pool.Execute("putUrl", id, url); err == nil {
+		w.WriteHeader(http.StatusOK)
+	} else {
+		http.Error(w, "Internal server error", http.StatusInternalServerError)
+	}
+}
+
+func deleteUrlHandler(w http.ResponseWriter, req *http.Request) {
+	if _, err := pool.Execute("deleteUrl", req.URL.Path); err == nil {
+		w.WriteHeader(http.StatusOK)
+	} else {
+		http.Error(w, "Internal server error", http.StatusInternalServerError)
+	}
+}
+
+func urlHandler(w http.ResponseWriter, req *http.Request) {
+	switch req.Method {
+	case "GET":
+		getUrlHandler(w, req)
+
+	case "PUT":
+		putUrlHandler(w, req)
+
+	case "DELETE":
+		deleteUrlHandler(w, req)
+
+	default:
+		w.Header().Add("Allow", "GET, PUT, DELETE")
+		w.WriteHeader(http.StatusMethodNotAllowed)
+	}
+}
+
+func main() {
+	var err error
+	connectionOptions := pgx.ConnectionParameters{
+		Host:     "127.0.0.1",
+		User:     "jack",
+		Password: "jack",
+		Database: "url_shortener"}
+	poolOptions := pgx.ConnectionPoolOptions{MaxConnections: 5, AfterConnect: afterConnect}
+	pool, err = pgx.NewConnectionPool(connectionOptions, poolOptions)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Unable to create connection pool: %v\n", err)
+		os.Exit(1)
+	}
+
+	http.HandleFunc("/", urlHandler)
+
+	fmt.Println("Starting URL shortener on localhost:8080...")
+	err = http.ListenAndServe("localhost:8080", nil)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Unable to start web server: %v\n", err)
+		os.Exit(1)
+	}
+}
diff --git a/examples/url_shortener/structure.sql b/examples/url_shortener/structure.sql
new file mode 100644
index 00000000..0b9de259
--- /dev/null
+++ b/examples/url_shortener/structure.sql
@@ -0,0 +1,4 @@
+create table shortened_urls (
+  id text primary key,
+  url text not null
+);
\ No newline at end of file