فهرست منبع

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

Frederic G. MARAND 6 سال پیش
والد
کامیت
fc25c47d9b
9فایلهای تغییر یافته به همراه140 افزوده شده و 57 حذف شده
  1. 2 2
      .idea/runConfigurations/Kurzd_serve.xml
  2. 28 15
      api/api.go
  3. 5 0
      api/post.go
  4. 36 8
      cmd/kurzd/serve.go
  5. 0 32
      cmd/kurzd/serve_api.go
  6. 8 0
      web/get_root.go
  7. 8 0
      web/get_short.go
  8. 8 0
      web/post_target.go
  9. 45 0
      web/web.go

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

@@ -1,9 +1,9 @@
 <component name="ProjectRunConfigurationManager">
 <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" />
     <module name="kurz" />
     <working_directory value="$PROJECT_DIR$/cmd/kurzd" />
     <working_directory value="$PROJECT_DIR$/cmd/kurzd" />
     <go_parameters value="-o kurzd" />
     <go_parameters value="-o kurzd" />
-    <parameters value="serve api" />
+    <parameters value="serve" />
     <kind value="PACKAGE" />
     <kind value="PACKAGE" />
     <filePath value="$PROJECT_DIR$/" />
     <filePath value="$PROJECT_DIR$/" />
     <package value="code.osinet.fr/fgm/kurz/cmd/kurzd" />
     <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
   - GET "/<short>" : resolve a short URL
     - Handler: handleGetShort()
     - 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
 gag order, super-injunction (UK), National security letter (US) or similar
 mechanisms.
 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
 configure the Kurz domain API. Be sure to also configure the domain SPI to have
 a complete application.
 a complete application.
 */
 */
 package api
 package api
 
 
 import (
 import (
-	"net/http"
-
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 )
 )
 
 
@@ -43,19 +41,34 @@ type Target struct {
 	Target string `json:"target"`
 	Target string `json:"target"`
 }
 }
 
 
-/*
+// Route names.
-ListenAndServe() is the
+const (
- */
+	RouteGetShort   = "kurz.api.get_short"
-func ListenAndServe(addr string) error {
+	RoutePostTarget = "kurz.api.post_target"
-	router := mux.NewRouter()
+)
+
+// 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).
 	router.HandleFunc("/{short}", handleGetShort).
+		HeadersRegexp("Accept", JsonTypeRegex).
 		Methods("GET", "HEAD").
 		Methods("GET", "HEAD").
-		Name("kurz.get_short")
+		Name(RouteGetShort)
 	router.HandleFunc("/", handlePostTarget).
 	router.HandleFunc("/", handlePostTarget).
-		HeadersRegexp("Content-Type", "^application/json$").
+		HeadersRegexp("Accept", JsonTypeRegex).
+		Headers("Content-Type", JsonType).
 		Methods("POST").
 		Methods("POST").
-		Name("kurd.post_target")
+		Name(RoutePostTarget)
-	http.Handle("/", router)
-	err := http.ListenAndServe(addr, router)
-	return err
 }
 }

+ 5 - 0
api/post.go

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

+ 36 - 8
cmd/kurzd/serve.go

@@ -1,19 +1,27 @@
 package main
 package main
 
 
 import (
 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/api"
 	"code.osinet.fr/fgm/kurz/domain"
 	"code.osinet.fr/fgm/kurz/domain"
 	"code.osinet.fr/fgm/kurz/infrastructure"
 	"code.osinet.fr/fgm/kurz/infrastructure"
-	"database/sql"
+	"github.com/gorilla/mux"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
-	"log"
 )
 )
 
 
 var cmdServe = &cobra.Command{
 var cmdServe = &cobra.Command{
 	Args:  cobra.NoArgs,
 	Args:  cobra.NoArgs,
 	Long:  "Start HTTP Server",
 	Long:  "Start HTTP Server",
-	Run:   serveMarkupHandler,
+	Run:   serveHandler,
 	Short: "Top-level command for HTTP Serving.",
 	Short: "Top-level command for HTTP Serving.",
 	Use:   "serve",
 	Use:   "serve",
 }
 }
@@ -51,12 +59,32 @@ func ensureInfrastructure(db *sql.DB) *sql.DB {
 	return db
 	return db
 }
 }
 
 
-// serveMarkupHandler handles paths below /web/public.
+// serveHandler handles Web paths.
-func serveMarkupHandler(_ *cobra.Command, args []string) {
+func serveHandler(_ *cobra.Command, args []string) {
 	db = ensureInfrastructure(db)
 	db = ensureInfrastructure(db)
 	defer db.Close()
 	defer db.Close()
 
 
+	router := mux.NewRouter()
+	api.SetupRoutes(router)
+	web.SetupRoutes(router)
+	http.Handle("/", router)
+
 	address := viper.Get("web.address").(string)
 	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)
+}