bahndb-rest/main.go

337 lines
8.8 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"net/http"
"strconv"
"strings"
"time"
"log"
"os"
"github.com/joho/godotenv"
"bahndb_rest/queries"
)
var pool *pgxpool.Pool
func convErrorHandler(str string, err error) string {
if err != nil {
return err.Error()
}
return str
}
type RequestError struct {
Code string `json:"code"`
Message string `json:"messaage"`
}
func run() error {
// load environment file
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
connectstr := "host=" + os.Getenv("DB_HOST") + " port=" + os.Getenv("DB_PORT") + " user=" + os.Getenv("DB_USER") + " password=" + os.Getenv("DB_PASSWORD") + " dbname=" + os.Getenv("DB_DATABASE") + " pool_max_conns=70 pool_max_conn_lifetime=1h30m"
// connect to db
pool, err = pgxpool.New(context.Background(), connectstr)
if err != nil {
log.Fatal("Error creating pool", err.Error())
}
fmt.Println("Connected to database")
//create new request multiplexer
mux := http.NewServeMux()
// register stopinfo handler
mux.Handle("/stop/search", &stopInfoHandler{})
mux.Handle("/stop/{id}/info", &stopIdInfoHandler{})
mux.Handle("/stop/{id}/departures", &stopDeparturesHandler{})
fmt.Println("Starting server on port 8080")
err = http.ListenAndServe(":8080", mux)
if err != nil {
log.Fatal("Error serving server", err.Error())
}
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
// Search For a Stop by Name
type stopInfoHandler struct{}
func (h *stopInfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
enc := json.NewEncoder(w)
ctx := context.Background()
w.Header().Set("Content-Type", "application/json")
conn, err := pool.Acquire(ctx)
if err != nil {
w.WriteHeader(500)
_ = enc.Encode(RequestError{Code: "0001", Message: "DB Error: " + err.Error()})
return
}
defer conn.Release()
// load all generated queries
queries := bahndb_rest.New(conn)
// Parse query text
var searchQuery = r.FormValue("query")
if searchQuery == "" {
w.WriteHeader(400)
_ = enc.Encode(RequestError{Code: "0000", Message: "'query' is required"})
return
}
var searchQueries = strings.Split(searchQuery, " ")
for i, query := range searchQueries {
if i == 0 {
// Parse whether result shall be exact
if r.FormValue("contains") != "true" {
searchQueries[0] = "%" + query + "%"
} else {
searchQueries[0] = query + "%"
}
} else {
searchQueries[i] = "%" + query + "%"
}
}
var longDistance pgtype.Bool
// Parse whether only long distance stations shall be returned
if r.FormValue("longDistance") == "true" {
longDistance = pgtype.Bool{Bool: true, Valid: true}
} else {
longDistance = pgtype.Bool{Bool: false, Valid: false}
}
stations, err := queries.GetStationsByName(ctx, bahndb_rest.GetStationsByNameParams{SearchParams: searchQueries, LongDistance: longDistance})
if err != nil {
w.WriteHeader(500)
_ = enc.Encode(RequestError{Code: "0001", Message: "DB Error: " + err.Error()})
return
}
w.WriteHeader(http.StatusOK)
err = enc.Encode(stations)
if err != nil {
w.WriteHeader(500)
_ = enc.Encode(RequestError{Code: "0002", Message: "Could not write response: " + err.Error()})
return
}
return
}
type stopIdInfoHandler struct{}
func (h *stopIdInfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
enc := json.NewEncoder(w)
ctx := context.Background()
w.Header().Set("Content-Type", "application/json")
conn, err := pool.Acquire(ctx)
if err != nil {
w.WriteHeader(500)
_ = enc.Encode(RequestError{Code: "0001", Message: "DB Error: " + err.Error()})
return
}
defer conn.Release()
// load all generated queries
queries := bahndb_rest.New(conn)
// Parse stop id
var stopId = r.PathValue("id")
if stopId == "" {
w.WriteHeader(400)
_ = enc.Encode(RequestError{Code: "0000", Message: "'id' is required"})
return
}
// Execute DB Query
stations, err := queries.GetStationById(ctx, stopId)
if err != nil {
w.WriteHeader(500)
_ = enc.Encode(RequestError{Code: "0001", Message: "DB Error: " + err.Error()})
return
}
// Return
w.WriteHeader(http.StatusOK)
err = enc.Encode(stations)
if err != nil {
w.WriteHeader(500)
_ = enc.Encode(RequestError{Code: "0002", Message: "Could not write response: " + err.Error()})
return
}
return
}
// Return departures at specific stop
type GetDeparturesByIdRowWithString struct {
bahndb_rest.GetDeparturesByIdRow
}
func (r GetDeparturesByIdRowWithString) MarshalJSON() ([]byte, error) {
var direction map[string]interface{}
err := json.Unmarshal(r.Direction, &direction)
if err != nil {
return nil, err
}
// Create an anonymous struct that mirrors the original but with string Direction
tmp := struct {
ID string `json:"id"`
LineName pgtype.Text `json:"lineName"`
LineProductName pgtype.Text `json:"lineProductName"`
DepartureDelay pgtype.Int4 `json:"departureDelay"`
Direction interface{} `json:"direction"`
Stopovers interface{} `json:"stopovers"`
Departure pgtype.Timestamptz `json:"departure"`
PlannedDeparture pgtype.Timestamptz `json:"plannedDeparture"`
DeparturePlatform pgtype.Text `json:"departurePlattform"`
PlannedDeparturePlatform pgtype.Text `json:"plannedDeparturePlattform"`
Cancelled pgtype.Text `json:"cancelled"`
RealtimeDataUpdatedAt pgtype.Timestamptz `json:"realtimeDataUpdatedAt"`
}{
ID: r.ID,
LineName: r.LineName,
LineProductName: r.LineProductName,
DepartureDelay: r.DepartureDelay,
Direction: direction,
Stopovers: r.Stopovers,
Departure: r.Departure,
PlannedDeparture: r.PlannedDeparture,
DeparturePlatform: r.DeparturePlatform,
PlannedDeparturePlatform: r.PlannedDeparturePlatform,
Cancelled: r.Cancelled,
RealtimeDataUpdatedAt: r.RealtimeDataUpdatedAt,
}
return json.Marshal(tmp)
}
type stopDeparturesHandler struct{}
func (h *stopDeparturesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
enc := json.NewEncoder(w)
ctx := context.Background()
w.Header().Set("Content-Type", "application/json")
// Acquire DB Connection
conn, err := pool.Acquire(ctx)
if err != nil {
w.WriteHeader(500)
_ = enc.Encode(RequestError{Code: "0000", Message: "'id' is required"})
return
}
defer conn.Release()
queries := bahndb_rest.New(conn)
// Parse stop id
var stopId = r.PathValue("id")
if stopId == "" {
w.WriteHeader(400)
_ = enc.Encode(RequestError{Code: "0001", Message: "DB Error: " + err.Error()})
return
}
stopIdPg := pgtype.Text{String: stopId, Valid: true}
// Parse time from string, default to now
timeFromStr := strings.Replace(r.FormValue("from"), " ", "+", -1)
var timeFrom time.Time
if timeFromStr == "" {
timeFrom = time.Now()
} else {
timeFrom, err = time.Parse(time.RFC3339, timeFromStr)
}
timeFromPg := pgtype.Timestamptz{Time: timeFrom, Valid: true}
// Parse Duration to get departures for, default to 60
duration, err := strconv.Atoi(r.FormValue("duration"))
if err != nil {
duration = 60
}
// Parse upper Time limit, default to from duration minutes
timeToStr := strings.Replace(r.FormValue("to"), " ", "+", -1)
var timeTo time.Time
if timeToStr == "" {
timeTo = timeFrom.Add(time.Duration(duration) * time.Minute)
} else {
timeTo, err = time.Parse(time.RFC3339, timeFromStr)
}
timeToPg := pgtype.Timestamptz{Time: timeTo, Valid: true}
// Parse realtime point in time, default to most recent
timeRtUpdateStr := strings.Replace(r.FormValue("dataAt"), " ", "+", -1)
var timeRtUpdate time.Time
if timeRtUpdateStr == "" {
timeRtUpdate = time.Now().Add(time.Duration(48) * time.Hour)
} else {
timeRtUpdate, err = time.Parse(time.RFC3339, timeFromStr)
}
timeRtUpdatePg := pgtype.Timestamptz{Time: timeRtUpdate, Valid: true}
// Execute Query
stations, err := queries.GetDeparturesById(ctx, bahndb_rest.GetDeparturesByIdParams{Stop: stopIdPg,
Departure: timeFromPg,
Departure_2: timeToPg,
Realtimedataupdatedat: timeRtUpdatePg})
if err != nil {
w.WriteHeader(500)
_ = enc.Encode(RequestError{Code: "0001", Message: "DB Error: " + err.Error()})
return
}
var stationsReturn []GetDeparturesByIdRowWithString
for _, station := range stations {
stationsReturn = append(stationsReturn, GetDeparturesByIdRowWithString{station})
}
// Return Data
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = enc.Encode(stationsReturn)
if err != nil {
w.WriteHeader(500)
_ = enc.Encode(RequestError{Code: "0002", Message: "Could not write response: " + err.Error()})
return
}
return
}