From 95384c294d43ebb6d318882fc83f86887a395b14 Mon Sep 17 00:00:00 2001 From: Giovanni Cappellotto Date: Sun, 22 Oct 2017 09:47:31 -0400 Subject: [PATCH 1/7] Add a server minimal implementation The server listens on port 3000 for TCP connections and serves: * `GET /` - returns a static `index.html` asset * `GET /stylesheets/*` - returns static assets found in the `static/stylesheets` directory * `GET /hello` - an example handler function that returns a string Resources: * https://codegangsta.gitbooks.io/building-web-apps-with-go/content/testing/unit_testing/index.html * https://codegangsta.gitbooks.io/building-web-apps-with-go/content/testing/end_to_end/index.html * https://codegangsta.gitbooks.io/building-web-apps-with-go/content/url_routing/index.html * https://github.com/julienschmidt/httprouter * https://golang.org/pkg/net/http/httptest/ --- server.go | 38 +++++++++++++++++++++++++++ server_test.go | 50 ++++++++++++++++++++++++++++++++++++ static/index.html | 15 +++++++++++ static/stylesheets/style.css | 3 +++ 4 files changed, 106 insertions(+) create mode 100644 server.go create mode 100644 server_test.go create mode 100644 static/index.html create mode 100644 static/stylesheets/style.css diff --git a/server.go b/server.go new file mode 100644 index 0000000..fd624cd --- /dev/null +++ b/server.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/julienschmidt/httprouter" +) + +func serveIndex(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { + http.ServeFile(w, req, "static/index.html") +} + +// HelloWorld is a cool function +func HelloWorld(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + // Simply write some test data for now + fmt.Fprintln(w, "Hello World!") +} + +// App is a cool function +func App() *httprouter.Router { + router := httprouter.New() + + // Add a handler on /hello + router.GET("/hello", HelloWorld) + + // Serve static assets + router.GET("/", serveIndex) + router.ServeFiles("/stylesheets/*filepath", http.Dir("static/stylesheets")) + + return router +} + +func main() { + // Fire up the server + fmt.Println("Server listening on port 3000") + http.ListenAndServe("localhost:3000", App()) +} diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..cb5aa41 --- /dev/null +++ b/server_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func Test_HelloWorld(t *testing.T) { + req, err := http.NewRequest("GET", "http://example.com/foo", nil) + if err != nil { + t.Fatal(err) + } + + res := httptest.NewRecorder() + HelloWorld(res, req, nil) + + exp := "Hello World!\n" + act := res.Body.String() + if exp != act { + t.Fatalf("Expected '%s' got '%s'", exp, act) + } +} + +func Test_App(t *testing.T) { + ts := httptest.NewServer(App()) + defer ts.Close() + + res, err := http.Get(ts.URL) + if err != nil { + t.Fatal(err) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + + if err != nil { + t.Fatal(err) + } + + exp, err := ioutil.ReadFile("static/index.html") + if err != nil { + t.Fatal(err) + } + + if string(exp) != string(body) { + t.Fatalf("Expected '%s' got '%s'", exp, body) + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..84b1a8e --- /dev/null +++ b/static/index.html @@ -0,0 +1,15 @@ + + + + + + issuehunter + + + + + +

issuehunter

+

Open Source Crowdfunding on the Ethereum Blockchain

+ + diff --git a/static/stylesheets/style.css b/static/stylesheets/style.css new file mode 100644 index 0000000..2206be4 --- /dev/null +++ b/static/stylesheets/style.css @@ -0,0 +1,3 @@ +body { + font-family: monospace; +} From f96ebc4d7f0cabf20bf9a5a7d4f2da67ea46761f Mon Sep 17 00:00:00 2001 From: Giovanni Cappellotto Date: Sun, 22 Oct 2017 18:23:18 -0400 Subject: [PATCH 2/7] Add logger middleware See https://github.com/julienschmidt/httprouter/issues/177#issuecomment-271161335 --- server.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server.go b/server.go index fd624cd..b8ca8e6 100644 --- a/server.go +++ b/server.go @@ -2,11 +2,21 @@ package main import ( "fmt" + "log" "net/http" "github.com/julienschmidt/httprouter" ) +type Logger struct { + handler http.Handler +} + +func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s", r.Method, r.URL.Path) + l.handler.ServeHTTP(w, r) +} + func serveIndex(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { http.ServeFile(w, req, "static/index.html") } @@ -18,7 +28,7 @@ func HelloWorld(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { } // App is a cool function -func App() *httprouter.Router { +func App() http.Handler { router := httprouter.New() // Add a handler on /hello @@ -28,7 +38,7 @@ func App() *httprouter.Router { router.GET("/", serveIndex) router.ServeFiles("/stylesheets/*filepath", http.Dir("static/stylesheets")) - return router + return Logger{router} } func main() { From f6d9953544b2d8619d3f6cb54c43b50424698d0e Mon Sep 17 00:00:00 2001 From: Giovanni Cappellotto Date: Sun, 22 Oct 2017 18:29:58 -0400 Subject: [PATCH 3/7] no-op - Fix missing doc warning --- server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server.go b/server.go index b8ca8e6..7c3f4e8 100644 --- a/server.go +++ b/server.go @@ -8,6 +8,7 @@ import ( "github.com/julienschmidt/httprouter" ) +// Logger is a cool type type Logger struct { handler http.Handler } From a83782255a49f17a46848792817369f4618920e3 Mon Sep 17 00:00:00 2001 From: Giovanni Cappellotto Date: Sun, 22 Oct 2017 18:31:17 -0400 Subject: [PATCH 4/7] Pass Logger by reference --- server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.go b/server.go index 7c3f4e8..d2e7756 100644 --- a/server.go +++ b/server.go @@ -13,7 +13,7 @@ type Logger struct { handler http.Handler } -func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (l *Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path) l.handler.ServeHTTP(w, r) } @@ -39,7 +39,7 @@ func App() http.Handler { router.GET("/", serveIndex) router.ServeFiles("/stylesheets/*filepath", http.Dir("static/stylesheets")) - return Logger{router} + return &Logger{router} } func main() { From 4fa32782bd9da9788326ac8139541a88592dc30c Mon Sep 17 00:00:00 2001 From: Giovanni Cappellotto Date: Sun, 22 Oct 2017 19:03:39 -0400 Subject: [PATCH 5/7] Log handler execution duration I wanted to log the response status too, but it seems like there isn't a straightforward way to do that, see https://github.com/felixge/httpsnoop#why-this-package-exists and https://github.com/urfave/negroni/blob/master/logger.go. --- server.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/server.go b/server.go index d2e7756..e6c86a6 100644 --- a/server.go +++ b/server.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "time" "github.com/julienschmidt/httprouter" ) @@ -13,9 +14,12 @@ type Logger struct { handler http.Handler } -func (l *Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s", r.Method, r.URL.Path) - l.handler.ServeHTTP(w, r) +func (l *Logger) ServeHTTP(w http.ResponseWriter, req *http.Request) { + start := time.Now() + + l.handler.ServeHTTP(w, req) + + log.Printf("- %s %s (%s)", req.Method, req.URL.Path, time.Since(start).String()) } func serveIndex(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { From bc3bb5a074a62c7c05c1f6029000c837b353a7a1 Mon Sep 17 00:00:00 2001 From: Giovanni Cappellotto Date: Sun, 22 Oct 2017 19:07:30 -0400 Subject: [PATCH 6/7] Read PORT config from an env variable --- README.md | 4 ++++ server.go | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a012e04..6a7ccdf 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ # issuehunter-api + +## Configuration + +* `PORT` HTTP server port, default: 8080 diff --git a/server.go b/server.go index e6c86a6..455555e 100644 --- a/server.go +++ b/server.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "os" "time" "github.com/julienschmidt/httprouter" @@ -47,7 +48,12 @@ func App() http.Handler { } func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + // Fire up the server - fmt.Println("Server listening on port 3000") - http.ListenAndServe("localhost:3000", App()) + fmt.Println("Server listening on port " + port) + http.ListenAndServe(":"+port, App()) } From 5541a5464f82a9d0b25a8db0cedb7a3791d77bf5 Mon Sep 17 00:00:00 2001 From: Giovanni Cappellotto Date: Wed, 29 Nov 2017 15:54:44 -0500 Subject: [PATCH 7/7] WIP... --- TODO.md | 4 ++++ db/schema.sql | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ server.go | 30 +++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 TODO.md create mode 100644 db/schema.sql diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..1c39efe --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +https://stevenwhite.com/building-a-rest-service-with-golang-2/ +https://codegangsta.gitbooks.io/building-web-apps-with-go/content/deployment/index.html + +https://derekchiang.com/posts/reusable-and-type-safe-options-for-go-apis/ diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..7125db9 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,53 @@ +DROP TABLE IF EXISTS campaigns; + +CREATE TABLE IF NOT EXISTS campaigns( + uid VARCHAR(255) NOT NULL PRIMARY KEY, + rewarded BOOLEAN, + total NUMERIC(100), -- uint256 + created_by VARCHAR(255), + pre_reward_period_expires_at TIMESTAMP, + reward_period_expires_at TIMESTAMP, + resolved_by VARCHAR(255), + patch_verifier VARCHAR(255), + tip_per_mille INT, + tips_amount NUMERIC(100), + repo_url VARCHAR(255), + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + created_at_block_id VARCHAR(255) NOT NULL, + updated_at_block_id VARCHAR(255) NOT NULL +); + +CREATE INDEX campaigns_total_idx ON campaigns (total); +CREATE INDEX campaigns_created_at_idx ON campaigns (created_at); +CREATE INDEX campaigns_updated_at_idx ON campaigns (updated_at); + +DROP TABLE IF EXISTS funds; + +CREATE TABLE IF NOT EXISTS funds( + funder_address VARCHAR(255), + amount NUMERIC(100), -- uint256 + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + created_at_block_id VARCHAR(255) NOT NULL, + updated_at_block_id VARCHAR(255) NOT NULL +); + +CREATE INDEX funder_address_idx ON funds (funder_address); +CREATE INDEX funds_created_at_idx ON funds (created_at); +CREATE INDEX funds_updated_at_idx ON funds (updated_at); + +DROP TABLE IF EXISTS patches; + +CREATE TABLE IF NOT EXISTS patches( + author_address VARCHAR(255), + ref VARCHAR(255), + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + created_at_block_id VARCHAR(255) NOT NULL, + updated_at_block_id VARCHAR(255) NOT NULL +); + +CREATE INDEX author_address_idx ON funds (author_address); +CREATE INDEX patches_created_at_idx ON patches (created_at); +CREATE INDEX patches_updated_at_idx ON patches (updated_at); diff --git a/server.go b/server.go index 455555e..eb3d4bf 100644 --- a/server.go +++ b/server.go @@ -1,6 +1,7 @@ package main import ( + "database/sql" "fmt" "log" "net/http" @@ -8,6 +9,7 @@ import ( "time" "github.com/julienschmidt/httprouter" + _ "github.com/lib/pq" ) // Logger is a cool type @@ -36,10 +38,13 @@ func HelloWorld(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { // App is a cool function func App() http.Handler { router := httprouter.New() + db := NewDB() // Add a handler on /hello router.GET("/hello", HelloWorld) + router.GET("/campaigns", CampaignsIndex(db)) + // Serve static assets router.GET("/", serveIndex) router.ServeFiles("/stylesheets/*filepath", http.Dir("static/stylesheets")) @@ -47,6 +52,31 @@ func App() http.Handler { return &Logger{router} } +func CampaignsIndex(db *sql.DB) func(http.ResponseWriter, *http.Request, httprouter.Params) { + return func(rw http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var title, author string + err := db.QueryRow("SELECT title, author FROM campaigns").Scan(&title, &author) + if err != nil && err == sql.ErrNoRows { + fmt.Fprintf(rw, "No campaigns found") + return + } + if err != nil { + panic(err) + } + + fmt.Fprintf(rw, "The first campaign is '%s' by '%s'", title, author) + } +} + +func NewDB() *sql.DB { + db, err := sql.Open("postgres", "postgres://gcappellotto@localhost/issuehunter?sslmode=disable") + if err != nil { + panic(err) + } + + return db +} + func main() { port := os.Getenv("PORT") if port == "" {