Implement client and server
This commit is contained in:
130
client/client.go
Normal file
130
client/client.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const baseURL = "http://localhost:8080"
|
||||||
|
|
||||||
|
var httpClient = &http.Client{}
|
||||||
|
|
||||||
|
func doRequest(method, url string, body any) []byte {
|
||||||
|
var reqBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to marshal request body: %v", err)
|
||||||
|
}
|
||||||
|
reqBody = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to send request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to read response body: %v", err)
|
||||||
|
}
|
||||||
|
return respBody
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListNotes() {
|
||||||
|
body := doRequest(http.MethodGet, baseURL+"/notes", nil)
|
||||||
|
var notes []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
LastUpdate time.Time `json:"last_update"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, ¬es); err != nil {
|
||||||
|
log.Fatalf("Failed to parse notes: %v", err)
|
||||||
|
}
|
||||||
|
if len(notes) == 0 {
|
||||||
|
fmt.Println("No notes found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate column widths from headers and content
|
||||||
|
idW, updatedW, contentW := 2, 7, 7 // minimums: "ID", "Updated", "Content"
|
||||||
|
for _, n := range notes {
|
||||||
|
if w := len(fmt.Sprintf("%d", n.ID)); w > idW {
|
||||||
|
idW = w
|
||||||
|
}
|
||||||
|
if w := len(n.LastUpdate.Format("2006-01-02 15:04:05")); w > updatedW {
|
||||||
|
updatedW = w
|
||||||
|
}
|
||||||
|
if w := len(n.Content); w > contentW {
|
||||||
|
contentW = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row := func(id, updated, content string) {
|
||||||
|
fmt.Printf("| %-*s | %-*s | %-*s |\n", idW, id, updatedW, updated, contentW, content)
|
||||||
|
}
|
||||||
|
sep := fmt.Sprintf("|-%s-|-%s-|-%s-|", strings.Repeat("-", idW), strings.Repeat("-", updatedW), strings.Repeat("-", contentW))
|
||||||
|
|
||||||
|
row("ID", "Updated", "Content")
|
||||||
|
fmt.Println(sep)
|
||||||
|
for _, n := range notes {
|
||||||
|
row(fmt.Sprintf("%d", n.ID), n.LastUpdate.Format("2006-01-02 15:04:05"), n.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateNote(content string) {
|
||||||
|
body := doRequest(http.MethodPost, baseURL+"/notes", map[string]string{"content": content})
|
||||||
|
var note struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, ¬e); err != nil {
|
||||||
|
log.Fatalf("Failed to parse note: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Created note #%d: %s\n", note.ID, note.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNoteByID(id int) {
|
||||||
|
body := doRequest(http.MethodGet, fmt.Sprintf("%s/note/%d", baseURL, id), nil)
|
||||||
|
var note struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
LastUpdate time.Time `json:"last_update"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, ¬e); err != nil {
|
||||||
|
log.Fatalf("Failed to parse note: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Note #%d (updated %s):\n %s\n", note.ID, note.LastUpdate.Format("2006-01-02 15:04:05"), note.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateNoteByID(id int, content string) {
|
||||||
|
body := doRequest(http.MethodPut, fmt.Sprintf("%s/note/%d", baseURL, id), map[string]string{"content": content})
|
||||||
|
var note struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, ¬e); err != nil {
|
||||||
|
log.Fatalf("Failed to parse note: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Updated note #%d: %s\n", note.ID, note.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteNoteByID(id int) {
|
||||||
|
doRequest(http.MethodDelete, fmt.Sprintf("%s/note/%d", baseURL, id), nil)
|
||||||
|
fmt.Printf("Deleted note #%d\n", id)
|
||||||
|
}
|
||||||
102
main.go
102
main.go
@@ -1,9 +1,107 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.hrafn.xyz/aether/notes/client"
|
||||||
|
"git.hrafn.xyz/aether/notes/internal/repository"
|
||||||
|
"git.hrafn.xyz/aether/notes/internal/store/sqlite"
|
||||||
|
"git.hrafn.xyz/aether/notes/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("Hello, World!")
|
serverMode := flag.Bool("server", false, "Start the application in server mode")
|
||||||
|
list := flag.Bool("list", false, "List all notes in the database")
|
||||||
|
create := flag.Bool("create", false, "Create a new note")
|
||||||
|
update := flag.Bool("update", false, "Update an existing note")
|
||||||
|
delete := flag.Bool("delete", false, "Delete a note from the database")
|
||||||
|
get := flag.Bool("get", false, "Get a note by ID")
|
||||||
|
|
||||||
|
databasefile := flag.String("db", filepath.Join(os.Getenv("HOME"), "notes.db"), "Path to the SQLite database file (only used in server mode)")
|
||||||
|
id := flag.Int("id", 0, "ID of the note to get, update, or delete (only used with -get, -update, or -delete)")
|
||||||
|
content := flag.String("content", "", "Content of the note to create or update (only used with -create or -update)")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
modes := map[string]bool{
|
||||||
|
"server": *serverMode,
|
||||||
|
"list": *list,
|
||||||
|
"create": *create,
|
||||||
|
"update": *update,
|
||||||
|
"delete": *delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledCount := 0
|
||||||
|
for _, enabled := range modes {
|
||||||
|
if enabled {
|
||||||
|
enabledCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if enabledCount == 0 {
|
||||||
|
log.Fatal("No mode specified. Use -server, -list, -create, -update, -delete, or -get.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if enabledCount > 1 {
|
||||||
|
log.Fatal("Multiple modes specified. Please specify only one mode at a time.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *serverMode {
|
||||||
|
dbPath := *databasefile
|
||||||
|
sqliteStore, err := sqlite.NewSQLiteStore(context.Background(), dbPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer sqliteStore.Close()
|
||||||
|
repo := repository.NewNoteRepository(sqliteStore)
|
||||||
|
s := server.GetServer(repo, log.Default())
|
||||||
|
err = s.Start(":8080")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *list {
|
||||||
|
client.ListNotes()
|
||||||
|
}
|
||||||
|
|
||||||
|
if *create {
|
||||||
|
if *content == "" {
|
||||||
|
log.Fatal("Content must be provided with -create")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
client.CreateNote(*content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *get {
|
||||||
|
if *id == 0 {
|
||||||
|
log.Fatal("ID must be provided with -get")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
client.GetNoteByID(*id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *update {
|
||||||
|
if *id == 0 {
|
||||||
|
log.Fatal("ID must be provided with -update")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if *content == "" {
|
||||||
|
log.Fatal("Content must be provided with -update")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
client.UpdateNoteByID(*id, *content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *delete {
|
||||||
|
if *id == 0 {
|
||||||
|
log.Fatal("ID must be provided with -delete")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
client.DeleteNoteByID(*id)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,40 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.hrafn.xyz/aether/notes/internal/repository"
|
"git.hrafn.xyz/aether/notes/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
noteRepo repository.NoteRepository
|
noteRepo *repository.NoteRepository
|
||||||
|
logger *log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetServer(repo repository.NoteRepository) Server {
|
func GetServer(repo *repository.NoteRepository, logger *log.Logger) Server {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
server := Server{
|
server := Server{
|
||||||
httpServer: &http.Server{Handler: mux},
|
httpServer: &http.Server{Handler: loggingMiddleware(mux, logger)},
|
||||||
noteRepo: repo,
|
noteRepo: repo,
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
mux.HandleFunc("GET /notes", server.getNotes)
|
mux.HandleFunc("GET /notes", server.getNotes)
|
||||||
mux.HandleFunc("POST /notes", server.createNote)
|
mux.HandleFunc("POST /notes", server.createNote)
|
||||||
mux.HandleFunc("GET /notes/{id}", server.getNoteByID)
|
mux.HandleFunc("GET /note/{id}", server.getNoteByID)
|
||||||
mux.HandleFunc("PUT /notes/{id}", server.updateNoteByID)
|
mux.HandleFunc("PUT /note/{id}", server.updateNoteByID)
|
||||||
mux.HandleFunc("DELETE /notes/{id}", server.deleteNoteByID)
|
mux.HandleFunc("DELETE /note/{id}", server.deleteNoteByID)
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(addr string) error {
|
func (s *Server) Start(addr string) error {
|
||||||
s.httpServer.Addr = addr
|
s.httpServer.Addr = addr
|
||||||
|
s.logger.Printf("Starting server on %s", addr)
|
||||||
return s.httpServer.ListenAndServe()
|
return s.httpServer.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +42,31 @@ func (s *Server) Stop() error {
|
|||||||
return s.httpServer.Close()
|
return s.httpServer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loggingMiddleware(next http.Handler, logger *log.Logger) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
||||||
|
next.ServeHTTP(wrapped, r)
|
||||||
|
duration := time.Since(start)
|
||||||
|
logger.Printf("%s %s - %d (%dms)", r.Method, r.RequestURI, wrapped.statusCode, duration.Milliseconds())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *responseWriter) WriteHeader(code int) {
|
||||||
|
rw.statusCode = code
|
||||||
|
rw.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) getJsonResponse(w http.ResponseWriter, data any) {
|
func (s *Server) getJsonResponse(w http.ResponseWriter, data any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
// Encode data to JSON and write to response
|
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||||
|
s.logger.Printf("failed to encode response: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getErrorResponse(w http.ResponseWriter, err error) {
|
func (s *Server) getErrorResponse(w http.ResponseWriter, err error) {
|
||||||
@@ -58,7 +86,10 @@ func (s *Server) createNote(w http.ResponseWriter, r *http.Request) {
|
|||||||
note := struct {
|
note := struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}{}
|
}{}
|
||||||
// Decode JSON body into note struct
|
if err := json.NewDecoder(r.Body).Decode(¬e); err != nil {
|
||||||
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
createdNote, err := s.noteRepo.CreateNote(r.Context(), note.Content)
|
createdNote, err := s.noteRepo.CreateNote(r.Context(), note.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.getErrorResponse(w, err)
|
s.getErrorResponse(w, err)
|
||||||
@@ -92,7 +123,10 @@ func (s *Server) updateNoteByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
note := struct {
|
note := struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}{}
|
}{}
|
||||||
// Decode JSON body into note struct
|
if err := json.NewDecoder(r.Body).Decode(¬e); err != nil {
|
||||||
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
updatedNote, err := s.noteRepo.UpdateNote(r.Context(), id, note.Content)
|
updatedNote, err := s.noteRepo.UpdateNote(r.Context(), id, note.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.getErrorResponse(w, err)
|
s.getErrorResponse(w, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user