Przeglądaj źródła

Web UI: package registers routes. Interruptible command 'serve' for both API and UI.

Frederic G. MARAND 5 lat temu
rodzic
commit
fc25c47d9b

+ 2 - 2
.idea/runConfigurations/Kurzd.xml → .idea/runConfigurations/Kurzd_serve.xml

@@ -1,9 +1,9 @@
 <component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Kurzd" type="GoApplicationRunConfiguration" factoryName="Go Application" singleton="true" show_console_on_std_err="true" show_console_on_std_out="true">
+  <configuration default="false" name="Kurzd serve" type="GoApplicationRunConfiguration" factoryName="Go Application" singleton="true" show_console_on_std_err="true" show_console_on_std_out="true">
     <module name="kurz" />
     <working_directory value="$PROJECT_DIR$/cmd/kurzd" />
     <go_parameters value="-o kurzd" />
-    <parameters value="serve api" />
+    <parameters value="serve" />
     <kind value="PACKAGE" />
     <filePath value="$PROJECT_DIR$/" />
     <package value="code.osinet.fr/fgm/kurz/cmd/kurzd" />

+ 28 - 15
api/api.go

@@ -1,5 +1,5 @@
 /*
-The Kurz Web API exposes these routes:
+The Kurz Web API exposes HTTP routes for JSON clients, route names to access them, and types for the requests and responses.
 
   - GET "/<short>" : resolve a short URL
     - Handler: handleGetShort()
@@ -21,15 +21,13 @@ Code 451 MAY be replaced by 403, for example when legal censorship includes a
 gag order, super-injunction (UK), National security letter (US) or similar
 mechanisms.
 
-These routes are exposed by running ListenAndServe(address), which is enough to
+These routes are exposed by running SetupRoutes(address), which is enough to
 configure the Kurz domain API. Be sure to also configure the domain SPI to have
 a complete application.
 */
 package api
 
 import (
-	"net/http"
-
 	"github.com/gorilla/mux"
 )
 
@@ -43,19 +41,34 @@ type Target struct {
 	Target string `json:"target"`
 }
 
-/*
-ListenAndServe() is the
- */
-func ListenAndServe(addr string) error {
-	router := mux.NewRouter()
+// Route names.
+const (
+	RouteGetShort   = "kurz.api.get_short"
+	RoutePostTarget = "kurz.api.post_target"
+)
+
+// Content types.
+const (
+	// JsonType is the MIME JSON type.
+	JsonType = "application/json"
+
+	JsonTypeHeader = JsonType + "; charset=utf-8"
+
+	// JsonTypeRegex is a regex matching the MIME JSON type anywhere
+	JsonTypeRegex = JsonType
+)
+
+// SetupRoutes() configures Web API routes on the passed mux.Router.
+//
+func SetupRoutes(router *mux.Router) {
+	// BUG(fgm): improve Accept header matchers once https://github.com/golang/go/issues/19307 is completed.
 	router.HandleFunc("/{short}", handleGetShort).
+		HeadersRegexp("Accept", JsonTypeRegex).
 		Methods("GET", "HEAD").
-		Name("kurz.get_short")
+		Name(RouteGetShort)
 	router.HandleFunc("/", handlePostTarget).
-		HeadersRegexp("Content-Type", "^application/json$").
+		HeadersRegexp("Accept", JsonTypeRegex).
+		Headers("Content-Type", JsonType).
 		Methods("POST").
-		Name("kurd.post_target")
-	http.Handle("/", router)
-	err := http.ListenAndServe(addr, router)
-	return err
+		Name(RoutePostTarget)
 }

+ 5 - 0
api/post.go

@@ -23,6 +23,7 @@ func handlePostTarget(w http.ResponseWriter, r *http.Request) {
 	payload, err := ioutil.ReadAll(r.Body)
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
+		w.Header().Set("Content-Type", JsonTypeHeader)
 		w.Write(jsonFromString(`{ error: "Incomplete request body"}`))
 		return
 	}
@@ -30,6 +31,7 @@ func handlePostTarget(w http.ResponseWriter, r *http.Request) {
 	err = json.Unmarshal(payload, &target)
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
+		w.Header().Set("Content-Type", JsonTypeHeader)
 		w.Write(jsonFromString(`{ error: "Invalid JSON request"}`))
 		return
 	}
@@ -44,6 +46,7 @@ func handlePostTarget(w http.ResponseWriter, r *http.Request) {
 			switch domainErr.Kind {
 			case domain.TargetInvalidError:
 				w.WriteHeader(http.StatusBadRequest)
+				w.Header().Set("Content-Type", JsonTypeHeader)
 				w.Write(jsonFromString(`{ error: "Invalid target requested"}`))
 
 			// Covers all the domain.Storage* error cases too.
@@ -61,6 +64,7 @@ func handlePostTarget(w http.ResponseWriter, r *http.Request) {
 	payload, err = json.Marshal(short)
 	if err != nil {
 		w.WriteHeader(http.StatusInternalServerError)
+		w.Header().Set("Content-Type", JsonTypeHeader)
 		w.Write(jsonFromString(`{ error: "Short URL serialization error"}`))
 		return
 	}
@@ -71,6 +75,7 @@ func handlePostTarget(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusConflict)
 	}
 
+	w.Header().Set("Content-Type", JsonTypeHeader)
 	w.Write(payload)
 	return
 }

+ 36 - 8
cmd/kurzd/serve.go

@@ -1,19 +1,27 @@
 package main
 
 import (
+	"code.osinet.fr/fgm/kurz/web"
+	"context"
+	"database/sql"
+	"github.com/spf13/viper"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"time"
+
 	"code.osinet.fr/fgm/kurz/api"
 	"code.osinet.fr/fgm/kurz/domain"
 	"code.osinet.fr/fgm/kurz/infrastructure"
-	"database/sql"
+	"github.com/gorilla/mux"
 	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-	"log"
 )
 
 var cmdServe = &cobra.Command{
 	Args:  cobra.NoArgs,
 	Long:  "Start HTTP Server",
-	Run:   serveMarkupHandler,
+	Run:   serveHandler,
 	Short: "Top-level command for HTTP Serving.",
 	Use:   "serve",
 }
@@ -51,12 +59,32 @@ func ensureInfrastructure(db *sql.DB) *sql.DB {
 	return db
 }
 
-// serveMarkupHandler handles paths below /web/public.
-func serveMarkupHandler(_ *cobra.Command, args []string) {
+// serveHandler handles Web paths.
+func serveHandler(_ *cobra.Command, args []string) {
 	db = ensureInfrastructure(db)
 	defer db.Close()
 
+	router := mux.NewRouter()
+	api.SetupRoutes(router)
+	web.SetupRoutes(router)
+	http.Handle("/", router)
+
 	address := viper.Get("web.address").(string)
-	err := api.ListenAndServe(address)
-	log.Fatal(err)
+
+	// Start a server that can handle a SIGINT to shutdown.
+	stop := make(chan os.Signal, 1)
+	signal.Notify(stop, os.Interrupt)
+
+	server := &http.Server{Addr: address, Handler: router}
+	go func() {
+		log.Printf("Listening on %s", address)
+		err := server.ListenAndServe()
+		log.Fatal(err)
+	}()
+	<-stop
+
+	log.Println("Shutting down server")
+	ctx, _ := context.WithTimeout(context.Background(), 1*time.Second)
+	server.Shutdown(ctx)
+	log.Println("Server gracefully stopped")
 }

+ 0 - 32
cmd/kurzd/serve_api.go

@@ -1,32 +0,0 @@
-package main
-
-import (
-	"log"
-
-	"code.osinet.fr/fgm/kurz/api"
-	_ "code.osinet.fr/fgm/kurz/migrations"
-	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-)
-
-var cmdServeAPI = &cobra.Command{
-	Args:  cobra.NoArgs,
-	Long:  "Serve Kurz as a JSON web API.",
-	Run:   serveAPIHandler,
-	Short: "Serve Kurz API",
-	Use:   "api",
-}
-
-func init() {
-	cmdServe.AddCommand(cmdServeAPI)
-}
-
-// Set up infrastructure and listen on specified address.
-func serveAPIHandler(_ *cobra.Command, args []string) {
-	db = ensureInfrastructure(db)
-	defer db.Close()
-
-	address := viper.Get("api.address").(string)
-	err := api.ListenAndServe(address)
-	log.Fatal(err)
-}

+ 8 - 0
web/get_root.go

@@ -0,0 +1,8 @@
+package web
+
+import "net/http"
+
+// handleGetRoot handles path /
+func handleGetRoot(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Hello Kurz"))
+}

+ 8 - 0
web/get_short.go

@@ -0,0 +1,8 @@
+package web
+
+import "net/http"
+
+// handleGetShort handles path /<short>
+func handleGetShort(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Hello short"))
+}

+ 8 - 0
web/post_target.go

@@ -0,0 +1,8 @@
+package web
+
+import "net/http"
+
+// handlePostTarget handles form POST requests to /
+func handlePostTarget(w http.ResponseWriter, r *http.Request) {
+
+}

+ 45 - 0
web/web.go

@@ -0,0 +1,45 @@
+/*
+The Kurz Web UI exposes HTTP routes for browsers, route names to access them, and types for the requests.
+
+These routes are exposed by running SetupRoutes(address), which is enough to
+configure the Kurz domain API. Be sure to also configure the domain SPI to have
+a complete application.
+*/
+package web
+
+import "github.com/gorilla/mux"
+
+// Route names.
+const (
+	RouteGetRoot    = "kurz.web.get_root"
+	RouteGetShort   = "kurz.web.get_short"
+	RoutePostTarget = "kurz.web.post_target"
+)
+
+// Content types.
+const (
+	// HtmlType is the MIME HTML type.
+	HtmlType = "text/html"
+
+	// HtmlTypeRegex is a regex matching the MIME HTML type anywhere
+	HtmlTypeRegex = HtmlType
+)
+
+// SetupRoutes treats s as UTF-8-encoded bytes and returns a copy with all Unicode letters that begin
+// words mapped to their title case.
+func SetupRoutes(router *mux.Router) {
+	// BUG(fgm): improve Accept header matchers once https://github.com/golang/go/issues/19307 is completed.
+	router.HandleFunc("/{short}", handleGetShort).
+		HeadersRegexp("Accept", HtmlTypeRegex).
+		Methods("GET", "HEAD").
+		Name(RouteGetShort)
+	router.HandleFunc("/", handlePostTarget).
+		HeadersRegexp("Accept", HtmlTypeRegex).
+		Headers("Content-Type", HtmlType).
+		Methods("POST").
+		Name(RoutePostTarget)
+	router.HandleFunc("/", handleGetRoot).
+		HeadersRegexp("Accept", HtmlTypeRegex).
+		Methods("GET", "HEAD").
+		Name(RouteGetRoot)
+}