package main import ( "encoding/json" "errors" "fmt" "html/template" "io" "log" "net/http" "net/url" "strconv" "strings" "github.com/Masterminds/sprig/v3" ) const ( App = "Contacts.app" SecretKey = "hypermedia rocks" ) func MakeTemplate(first string, others ...string) *template.Template { all := append([]string{first}, others...) paths := make([]string, len(all)) for i, name := range all { paths[i] = fmt.Sprintf("./templates/%s.gohtml", name) } // All templates are named like "whatever.html" to match the Python version. tpl := template.Must(template.New(first + ".html"). Funcs(sprig.FuncMap()). Funcs(template.FuncMap{ "get_flashed_messages": func() []string { return []string{"some message"} }, }). ParseFiles(paths...), ) return tpl } var ( /* @app.route("/") def index(): */ RouteFront = http.RedirectHandler("/contacts", http.StatusFound) ) /* @app.route("/contacts") def contacts(): */ func MakeRouteContacts() http.HandlerFunc { tpl := MakeTemplate( "layout", "index", "archive_ui", "rows", ) return func(w http.ResponseWriter, r *http.Request) { search := r.URL.Query().Get("q") page, err := strconv.Atoi(r.URL.Query().Get("page")) if err != nil || page == 0 { page = 1 } var contactsSet []Contact if search != "" { contactsSet = (&Contact{}).Search(search) if r.Header.Get("HX-Trigger") == "search" { tpl.Execute(w, anyMap{"contacts": contactsSet}) return } } else { contactsSet = (&Contact{}).All(page) } if err := tpl.Execute(w, anyMap{ "contacts": contactsSet, "search": search, "page": page, "archiver": GetArchiver(), }); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } /* @app.route("/contacts/archive", methods=["POST"]) def start_archive(): */ func MakeStartArchive() http.HandlerFunc { tpl := MakeTemplate( "archive_ui", ) return func(w http.ResponseWriter, r *http.Request) { archiver := GetArchiver() archiver.Run() tpl.Execute(w, anyMap{"archiver": archiver}) } } /* @app.route("/contacts/archive", methods=["GET"]) def archive_status(): */ func MakeArchiveStatus() http.HandlerFunc { tpl := MakeTemplate( "archive_ui", ) return func(w http.ResponseWriter, r *http.Request) { archiver := GetArchiver() tpl.Execute(w, anyMap{"archiver": archiver}) } } /* @app.route("/contacts/archive/file", methods=["GET"]) def archive_content(): */ func ArchiveContent(w http.ResponseWriter, r *http.Request) { archiver := GetArchiver() w.Header().Set("Content-Disposition", `attachement; filename="archive.json"`) http.ServeFile(w, r, archiver.ArchiveFile()) } /* @app.route("/contacts/archive", methods=["DELETE"]) def reset_archive(): */ func MakeResetArchive() http.HandlerFunc { tpl := MakeTemplate( "archive_ui", ) return func(w http.ResponseWriter, r *http.Request) { archiver := GetArchiver() archiver.Reset() tpl.Execute(w, anyMap{"archiver": archiver}) } } /* @app.route("/contacts/count") def contacts_count(): */ func ContactsCount(w http.ResponseWriter, r *http.Request) { count := (&Contact{}).Count() _, _ = fmt.Fprintf(w, "(%d total Contacts)", count) } /* @app.route("/contacts/new", methods=['GET']) def contacts_new_get(): */ func MakeContactsNewGet() http.HandlerFunc { tpl := MakeTemplate( "layout", "new", ) return func(w http.ResponseWriter, r *http.Request) { if err := tpl.Execute(w, anyMap{"contact": NewContact(nil)}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } /* @app.route("/contacts/new", methods=['POST']) def contacts_new(): */ func MakeContactsNew() http.HandlerFunc { tpl := MakeTemplate( "layout", "new", ) return func(w http.ResponseWriter, r *http.Request) { c := NewContact(anyMap{ "first": r.FormValue("first_name"), "last": r.FormValue("last_name"), "email": r.FormValue("phone"), "phone": r.FormValue("email"), }) if c.Save() { flash("Created New Contact!") http.Redirect(w, r, "/contacts", http.StatusFound) return } else { tpl.Execute(w, anyMap{"contact": c}) } } } /* @app.route("/contacts/") def contacts_view(contact_id=0): */ func MakeContactsView() http.HandlerFunc { tpl := MakeTemplate( "layout", "show", ) return func(w http.ResponseWriter, r *http.Request) { contact_id, _ := strconv.Atoi(r.PathValue("contact_id")) contact := (&Contact{}).Find(uint64(contact_id)) if err := tpl.Execute(w, anyMap{"contact": contact}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } /* @app.route("/contacts//edit", methods=["GET"]) def contacts_edit_get(contact_id=0): */ func MakeContactsEditGet() http.HandlerFunc { tpl := MakeTemplate( "layout", "edit", ) return func(w http.ResponseWriter, r *http.Request) { contact_id, _ := strconv.Atoi(r.PathValue("contact_id")) contact := (&Contact{}).Find(uint64(contact_id)) tpl.Execute(w, anyMap{"contact": contact}) } } /* @app.route("/contacts//edit", methods=["POST"]) def contacts_edit_post(contact_id=0): */ func MakeContactsEditPost() http.HandlerFunc { tpl := MakeTemplate( "layout", "edit", ) return func(w http.ResponseWriter, request *http.Request) { contact_id, _ := strconv.Atoi(request.PathValue("contact_id")) c := (&Contact{}).Find(uint64(contact_id)) c.Update( request.FormValue("first_name"), request.FormValue("last_name"), request.FormValue("phone"), request.FormValue("email"), ) if c.Save() { flash("Updated Contact!") http.Redirect(w, request, fmt.Sprintf("/contacts/%d", contact_id), http.StatusFound) return } else { tpl.Execute(w, anyMap{"contact": c}) } } } /* @app.route("/contacts//email", methods=["GET"]) def contacts_email_get(contact_id=0): */ func ContactsEmailGet(w http.ResponseWriter, r *http.Request) { contact_id, _ := strconv.Atoi(r.PathValue("contact_id")) c := (&Contact{}).Find(uint64(contact_id)) c.Email = r.FormValue("email") c.validate() // FIXME find what this is really expected to do in the original Python. for _, err := range c.Errors { upErr := strings.ToUpper(err.Error()) if strings.Contains(upErr, "EMAIL") { fmt.Fprintln(w, err) } } } /* @app.route("/contacts/", methods=["DELETE"]) def contacts_delete(contact_id=0): */ func ContactsDelete(w http.ResponseWriter, r *http.Request) { contact_id, _ := strconv.Atoi(r.PathValue("contact_id")) contact := (&Contact{}).Find(uint64(contact_id)) contact.Delete() if r.Header.Get("HX-Trigger") == "delete-btn" { flash("Deleted Contact!") http.Redirect(w, r, "/contacts", http.StatusSeeOther) return } else { return } } /* @app.route("/contacts/", methods=["DELETE"]) def contacts_delete_all(): */ func MakeContactsDeleteAll() http.HandlerFunc { tpl := MakeTemplate( "layout", "index", "archive_ui", "rows", ) return func(w http.ResponseWriter, r *http.Request) { // Per rfc7231#section-4.3.5 "A payload within a DELETE request message has no defined semantics" // // So Go does not parse forms on methods other than POST, PUT and PATCH, // but this handler is called with DELETE, so we need a manual parse. if r == nil || r.Body == nil { http.Error(w, "invalid void request", http.StatusBadRequest) return } body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, fmt.Sprintf("reading request body: %v", err), http.StatusBadRequest) return } values, err := url.ParseQuery(string(body)) if err != nil { http.Error(w, fmt.Sprintf("parsing form data: %v", err), http.StatusBadRequest) return } contactIDs := values["selected_contact_ids"] for _, sContactID := range contactIDs { if contactID, err := strconv.Atoi(sContactID); contactID > 0 && err == nil { contact := (&Contact{}).Find(uint64(contactID)) contact.Delete() } } flash("Deleted contacts!") contacts_set := (&Contact{}).All(1) if err := tpl.Execute(w, anyMap{"contacts": contacts_set}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } } /* # =========================================================== # JSON Data API # =========================================================== */ /* @app.route("/api/v1/contacts", methods=["GET"]) def json_contacts(): */ func JSONContacts(w http.ResponseWriter, r *http.Request) { contacts_set := (&Contact{}).All(1) enc := json.NewEncoder(w) if err := enc.Encode(anyMap{"contacts": contacts_set}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } /* @app.route("/api/v1/contacts", methods=["POST"]) def json_contacts_new(): */ func JSONContactsNew(w http.ResponseWriter, r *http.Request) { if r == nil || r.Body == nil { http.Error(w, "invalid void request", http.StatusBadRequest) return } body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, fmt.Sprintf("error reading body: %v", err), http.StatusBadRequest) return } m := make(anyMap, 4) if err := json.Unmarshal(body, &m); err != nil { http.Error(w, fmt.Sprintf("parsing JSON request: %v", err), http.StatusBadRequest) return } c := NewContact(anyMap{ "first": m["first_name"], "last": m["last_name"], "phone": m["phone"], "email": m["email"], }) enc := json.NewEncoder(w) if c.Save() { if err := enc.Encode(c); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } else { w.WriteHeader(http.StatusBadRequest) if err := enc.Encode(anyMap{"errors": c.Errors}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } /* @app.route("/api/v1/contacts/", methods=["GET"]) def json_contacts_view(contact_id=0): */ func JSONContactsView(w http.ResponseWriter, r *http.Request) { contact_id, _ := strconv.Atoi(r.PathValue("contact_id")) c := (&Contact{}).Find(uint64(contact_id)) enc := json.NewEncoder(w) if err := enc.Encode(c); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } /* @app.route("/api/v1/contacts/", methods=["PUT"]) def json_contacts_edit(contact_id): */ func JSONContactsEdit(w http.ResponseWriter, r *http.Request) { contact_id, _ := strconv.Atoi(r.PathValue("contact_id")) if r == nil || r.Body == nil { http.Error(w, "invalid void request", http.StatusBadRequest) return } body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, fmt.Sprintf("error reading body: %v", err), http.StatusBadRequest) return } m := make(map[string]string, 4) if err := json.Unmarshal(body, &m); err != nil { http.Error(w, fmt.Sprintf("parsing JSON request: %v", err), http.StatusBadRequest) return } c := (&Contact{}).Find(uint64(contact_id)) c.Update( m["first_name"], m["last_name"], m["phone"], m["email"], ) enc := json.NewEncoder(w) if c.Save() { if err := enc.Encode(c); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } else { w.WriteHeader(http.StatusBadRequest) if err := enc.Encode(anyMap{"errors": c.Errors}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } /* @app.route("/api/v1/contacts/", methods=["DELETE"]) def json_contacts_delete(contact_id=0): */ func JSONContactsDelete(w http.ResponseWriter, r *http.Request) { contact_id, _ := strconv.Atoi(r.PathValue("contact_id")) contact := (&Contact{}).Find(uint64(contact_id)) contact.Delete() enc := json.NewEncoder(w) _ = enc.Encode(anyMap{"success": true}) } /* * TODO implement me */ func flash(message string) { } func setupRoutes(mux *http.ServeMux) { // OK mux.Handle("/", RouteFront) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) mux.HandleFunc("/contacts", MakeRouteContacts()) mux.Handle("POST /contacts/archive", MakeStartArchive()) mux.Handle("GET /contacts/archive", MakeArchiveStatus()) mux.HandleFunc("GET /contacts/archive/file", ArchiveContent) mux.Handle("DELETE /contacts/archive", MakeResetArchive()) mux.HandleFunc("GET /contacts/count", ContactsCount) mux.Handle("GET /contacts/new", MakeContactsNewGet()) mux.Handle("POST /contacts/new", MakeContactsNew()) mux.Handle("GET /contacts/{contact_id}", MakeContactsView()) mux.Handle("GET /contacts/{contact_id}/edit", MakeContactsEditGet()) mux.Handle("POST /contacts/{contact_id}/edit", MakeContactsEditPost()) mux.HandleFunc("GET /contacts/{contact_id}/email", ContactsEmailGet) mux.HandleFunc("DELETE /contacts/{contact_id}", ContactsDelete) mux.Handle("DELETE /contacts/", MakeContactsDeleteAll()) mux.HandleFunc("GET /api/v1/contacts", JSONContacts) mux.HandleFunc("POST /api/v1/contacts", JSONContactsNew) mux.HandleFunc("GET /api/v1/contacts/{contact_id}", JSONContactsView) mux.HandleFunc("PUT /api/v1/contacts/{contact_id}", JSONContactsEdit) mux.HandleFunc("DELETE /api/v1/contacts/{contact_id}", JSONContactsDelete) } func main() { mux := http.NewServeMux() setupRoutes(mux) (&Contact{}).LoadDB() if err := http.ListenAndServe(":8080", mux); err != nil { if !errors.Is(err, http.ErrServerClosed) { log.Printf("Server error: %v\n", err) return } } log.Println("Server shutdown") }