From 2172b8232efa1e0fd1eef0f56d5dbe7722bde114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Borna=20Rajkovi=C4=87?= Date: Tue, 20 Jun 2023 16:10:46 +0200 Subject: [PATCH] Initial implementation --- .gitignore | 2 +- README.md | 106 +++++++++++++++++--- db/dev/v1_0.sql | 30 ++++++ docker-compose-deploy.yml | 14 +-- docker-compose.yml | 6 +- dockerfile | 6 +- go.mod | 2 +- handlers.go | 159 +++++++++++++++++++++++++++++ holiday/model.go | 31 ++++++ holiday/service.go | 111 ++++++++++++++++++++ main.go | 206 ++++++++++++++++++++++++++++++++++++-- makefile | 18 ++-- render.go | 53 ++++++++-- templates/error.gohtml | 5 + templates/holiday.gohtml | 44 ++++++++ templates/index.gohtml | 102 +++++++++++++++++++ 16 files changed, 838 insertions(+), 57 deletions(-) create mode 100644 handlers.go create mode 100644 holiday/model.go create mode 100644 holiday/service.go create mode 100644 templates/error.gohtml create mode 100644 templates/holiday.gohtml create mode 100644 templates/index.gohtml diff --git a/.gitignore b/.gitignore index 1110d50..0c6d955 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .idea/** -template +holiday-api .env \ No newline at end of file diff --git a/README.md b/README.md index eacea97..b7c38f8 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,96 @@ -# go-web-app-template +# Holiday api -Basic template for creating go applications +Simple api used for tracking holidays -Includes: - * dotenv - * pq - * sqlx - * chi - * google.uuid +## Endpoints -To start using make sure to setup either environment variables listed -below, or create .env file and copy variables below +Fetching is done via `GET /api/v1/holidays` endpoint + +That endpoint accepts a list of required and optional parameters + +### Required parameters +`country` + - determines for which country holidays are fetched, uses ISO 3166-1 Alpha-2 codes [more info here](https://en.wikipedia.org/wiki/ISO_3166-1) + - eg. `country=US` + +### Optional parameters +`year` + - returns only holidays for given year + - eg. `year=2023` + - only used if no more important parameters are defined + +`date` + - returns only holidays for given date + - date must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html) + - eg. `date=2021-12-25` + - if defined year and rangeStart|rangeEnd parameters are ignored + + +`rangeStart|rangeEnd` + - returns holidays in given range with both ends being inclusive + - if either limit isn't defined it is assumed to be up to or all from given limit (if rangeStart isn't defined all holidays before rangeEnd are returned and vice-verse) + - dates must be formatted in ISO 8601 format [more info here](https://www.iso.org/iso-8601-date-and-time-format.html) + - eg. `rangeStart=2021-12-25&rangeEnd=2023-01-23`, `rangeStart=2023-01-20` + - if defined year parameter is ignored + +`stateHoliday` + - if set true only holidays that are tagged as state holidays are returned, similar for if set false, if not set all holidays are returned + - eg. `stateHoliday=true`, `stateHoliday=false` + +`religiousHoliday` + - if set true only holidays that are tagged as religious holidays are returned, similar for if set false, if not set all holidays are returned + - eg. `religiousHoliday=true`, `religiousHoliday=false` + +#### Paging +`pageSize` + - returns at most pageSize number of holidays + - eg. `pageSize=20` + - only applied if page is defined as well, by default set to 20 + +`page` + - returns nth page of holidays, paging starts at 0 + - eg. `page=0` + +## Response + +By default, responses are returned as a json array ``` -PSQL_HOST=localhost -PSQL_PORT=5432 -PSQL_USER=template -PSQL_PASSWORD=templatePassword -PSQL_DB=template +[{ + id: string; + date: string(ISO 8601); + name: string; + description: string; + isStateHoliday: boolean; + isReligiousHoliday: boolean; +},...] +``` +eg. +``` +[{ + "id": "74a2a769-abf2-45d4-bdc4-442bbcc89138", + "date": "2023-12-25", + "name": "Christmas", + "description": "TBD", + "isStateHoliday": true, + "isReligiousHoliday": true +}] ``` -Also, database is required for template to start, so you can start it with `docker compose up -d` \ No newline at end of file +But can be returned as XML or CSV by setting appropriate `Accept` header (application/xml, text/xml or text/csv) + +XML Response +``` + + + + Christmas + TBD + + +``` + +CSV Response +``` +id,date,name,description,is_state_holiday,is_religious_holiday +74a2a769-abf2-45d4-bdc4-442bbcc89138,2023-12-25,Christmas,TBD,true,true +``` \ No newline at end of file diff --git a/db/dev/v1_0.sql b/db/dev/v1_0.sql index e69de29..1a4f060 100644 --- a/db/dev/v1_0.sql +++ b/db/dev/v1_0.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS "holiday" +( + id uuid, + country char(2), + date date, + name varchar(64), + description varchar(512), + is_state boolean, + is_religious boolean +); + +INSERT INTO "holiday" (id, country, date, name, description, is_state, is_religious) +VALUES + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b671', 'HR', '2023-01-01', 'Nova godina', '', true, false), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b672', 'HR', '2023-01-06', 'Sveta tri kralja', '', true, true), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b673', 'HR', '2023-04-09', 'Uskrs', '', true, true), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b674', 'HR', '2023-04-10', 'Uskrsni ponedjeljak', '', true, true), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b675', 'HR', '2023-05-01', 'Praznik rada', '', true, false), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b676', 'HR', '2023-05-30', 'Dan državnosti', '', true, false), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b677', 'HR', '2023-06-08', 'Tijelovo', '', true, true), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b678', 'HR', '2023-06-22', 'Dan antifašističke borbe', '', true, false), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b679', 'HR', '2023-08-05', 'Dan pobjede i domovinske zahvalnost', '', true, false), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b680', 'HR', '2023-08-15', 'Velika gospa', '', true, true), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b681', 'HR', '2023-11-01', 'Dan svih svetih', '', true, true), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b682', 'HR', '2023-11-18', 'Dan sjećanja na žrtve Domovinskog rata', '', true, false), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b683', 'HR', '2023-12-25', 'Božić', '', true, true), + ('096ca6c4-5c04-47a4-b363-4b4cc6f6b684', 'HR', '2023-12-26', 'Štefanje', '', true, true); + + + diff --git a/docker-compose-deploy.yml b/docker-compose-deploy.yml index bdaa228..77401de 100644 --- a/docker-compose-deploy.yml +++ b/docker-compose-deploy.yml @@ -2,14 +2,14 @@ version: '3.1' services: backend: - image: registry.bbr-dev.info/template/backend:latest + image: registry.bbr-dev.info/holiday-api/backend:latest restart: on-failure depends_on: - database ports: - "5281:5281" networks: - - template + - holiday env_file: - .env.docker @@ -18,11 +18,11 @@ services: ports: - "5432:5432" environment: - - POSTGRES_USER=template - - POSTGRES_PASSWORD=templatePassword - - POSTGRES_DB=template + - POSTGRES_USER=holiday + - POSTGRES_PASSWORD=holidayPassword + - POSTGRES_DB=holiday networks: - - template + - holiday networks: - template: \ No newline at end of file + holiday: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index cfa85ef..f19f4ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,6 @@ services: ports: - "5432:5432" environment: - - POSTGRES_USER=template - - POSTGRES_PASSWORD=templatePassword - - POSTGRES_DB=template \ No newline at end of file + - POSTGRES_USER=holiday + - POSTGRES_PASSWORD=holidayPassword + - POSTGRES_DB=holiday \ No newline at end of file diff --git a/dockerfile b/dockerfile index b3cc11b..c974fd2 100644 --- a/dockerfile +++ b/dockerfile @@ -10,10 +10,10 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . ENV CGO_ENABLED=0 -RUN go build -tags timetzdata template +RUN go build -tags timetzdata holiday-api ### Stage 2: Run ### FROM scratch COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt -COPY --from=go-build /root/template /usr/bin/template -ENTRYPOINT ["template"] +COPY --from=go-build /root/holiday-api /usr/bin/holiday-api +ENTRYPOINT ["holiday-api"] diff --git a/go.mod b/go.mod index 3e8b7db..ac84cd9 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module template +module holiday-api go 1.19 diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..065b9c4 --- /dev/null +++ b/handlers.go @@ -0,0 +1,159 @@ +package main + +import ( + "encoding/xml" + "fmt" + "github.com/google/uuid" + "holiday-api/holiday" + "log" + "net/http" + "strconv" + "time" +) + +func getHolidays(service holiday.Service) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + log.Printf("Failed parsing params: %v", err) + } + paging := holiday.Paging{ + PageSize: readInt(r, "pageSize", 1000), + Page: readInt(r, "page", 0), + } + search := holiday.Search{ + Country: r.Form.Get("country"), + Year: readIntNullable(r, "year"), + Date: readDateNullable(r, "date"), + RangeStart: readDateNullable(r, "rangeStart"), + RangeEnd: readDateNullable(r, "rangeEnd"), + StateHoliday: readBoolNullable(r, "stateHoliday"), + ReligiousHoliday: readBoolNullable(r, "religiousHoliday"), + } + holidays, err := service.Find(search, paging) + if err != nil { + render(w, r, 404, ErrorResponse{Created: time.Now(), Message: "failed fetching holidays"}) + } else { + render(w, r, 200, mapHolidays(holidays)) + } + } +} + +type DateResponse struct{ time.Time } + +func (d DateResponse) MarshalJSON() ([]byte, error) { + return []byte("\"" + d.String() + "\""), nil +} +func (d DateResponse) String() string { + return fmt.Sprintf(`%04d-%02d-%02d`, d.Year(), d.Month(), d.Day()) +} + +type ErrorResponse struct { + XMLName xml.Name `json:"-" xml:"Error"` + Created time.Time `json:"created" xml:"created"` + Message string `json:"message" xml:"message"` + Code int `json:"-" xml:"-"` +} + +type HolidayResponse struct { + XMLName xml.Name `json:"-" xml:"Holidays"` + Holidays []HolidayItemResponse `json:"holidays"` +} + +type HolidayItemResponse struct { + XMLName xml.Name `json:"-" xml:"Holiday"` + Id uuid.UUID `json:"id" xml:"id,attr"` + Date DateResponse `json:"date" xml:"date,attr"` + Name string `json:"name" xml:"name"` + Description string `json:"description" xml:"description,omitempty"` + IsStateHoliday bool `json:"isStateHoliday" xml:"isState,attr"` + IsReligiousHoliday bool `json:"isReligiousHoliday" xml:"isReligious,attr"` +} + +type HolidaySingleResponse struct { + Id *uuid.UUID + Country string + Date DateResponse + Name string + Description string + IsStateHoliday bool + IsReligiousHoliday bool +} + +type HolidaySingleRequest struct { + Id *uuid.UUID + Country string + Date DateResponse + Name string + Description string + IsStateHoliday bool + IsReligiousHoliday bool +} + +func mapHolidays(holidays []holiday.Holiday) HolidayResponse { + var response = make([]HolidayItemResponse, 0, len(holidays)) + for _, h := range holidays { + response = append(response, HolidayItemResponse{ + Id: h.Id, + Date: DateResponse{h.Date}, + Name: h.Name, + Description: h.Description, + IsStateHoliday: h.IsStateHoliday, + IsReligiousHoliday: h.IsReligiousHoliday, + }) + } + return HolidayResponse{ + Holidays: response, + } +} + +func readDateNullable(r *http.Request, param string) *time.Time { + if r.Form.Has(param) { + var year, month, date int + count, err := fmt.Sscanf(r.Form.Get(param), "%d-%d-%d", &year, &month, &date) + if count < 3 { + log.Printf("failed reading %s: %v", param, err) + return nil + } else { + readDate := time.Date(year, time.Month(month), date, 0, 0, 0, 0, time.UTC) + return &readDate + } + } else { + return nil + } +} + +func readIntNullable(r *http.Request, param string) *int { + if r.Form.Has(param) { + if value, err := strconv.Atoi(r.Form.Get(param)); err == nil { + return &value + } else { + return nil + } + } else { + return nil + } +} + +func readBoolNullable(r *http.Request, param string) *bool { + if r.Form.Has(param) { + if value, err := strconv.ParseBool(r.Form.Get(param)); err == nil { + return &value + } else { + return nil + } + } else { + return nil + } +} + +func readInt(r *http.Request, param string, defaultValue int) int { + if r.Form.Has(param) { + if value, err := strconv.Atoi(r.Form.Get(param)); err == nil { + return value + } else { + return defaultValue + } + } else { + return defaultValue + } +} diff --git a/holiday/model.go b/holiday/model.go new file mode 100644 index 0000000..49af5a0 --- /dev/null +++ b/holiday/model.go @@ -0,0 +1,31 @@ +package holiday + +import ( + "github.com/google/uuid" + "time" +) + +type Holiday struct { + Id uuid.UUID `db:"id"` + Country string `db:"country"` + Date time.Time `db:"date"` + Name string `db:"name"` + Description string `db:"description"` + IsStateHoliday bool `db:"is_state"` + IsReligiousHoliday bool `db:"is_religious"` +} + +type Paging struct { + PageSize int + Page int +} + +type Search struct { + Country string + Year *int + Date *time.Time + RangeStart *time.Time + RangeEnd *time.Time + StateHoliday *bool + ReligiousHoliday *bool +} diff --git a/holiday/service.go b/holiday/service.go new file mode 100644 index 0000000..e102a6d --- /dev/null +++ b/holiday/service.go @@ -0,0 +1,111 @@ +package holiday + +import ( + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "strconv" + "strings" + "time" +) + +type Service struct { + DB *sqlx.DB +} + +func (s *Service) Find(search Search, paging Paging) ([]Holiday, error) { + var holidays []Holiday + var err error + if search.Date != nil { + holidays, err = s.findByDate(*search.Date, search.StateHoliday, search.ReligiousHoliday, search.Country) + } else if search.RangeStart != nil || search.RangeEnd != nil { + holidays, err = s.findForRange(getDateOrDefault(search.RangeStart, 1000), getDateOrDefault(search.RangeEnd, 3000), search.StateHoliday, search.ReligiousHoliday, search.Country) + } else if search.Year != nil { + holidays, err = s.findByYear(*search.Year, search.StateHoliday, search.ReligiousHoliday, search.Country) + } else { + holidays, err = s.find(search.StateHoliday, search.ReligiousHoliday, search.Country) + } + if err != nil { + return nil, err + } + return s.paginate(holidays, paging), err +} + +func (s *Service) FindById(id uuid.UUID) (Holiday, error) { + var holiday Holiday + return holiday, s.DB.Get(&holiday, `SELECT * FROM "holiday" WHERE "id" = $1;`, id) +} + +func (s *Service) findByDate(date time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) { + var holidays []Holiday + return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE "date" = $1 AND country = $2 `+s.filter(isState, isReligious)+";", date, country) +} + +func (s *Service) findForRange(rangeStart time.Time, rangeEnd time.Time, isState *bool, isReligious *bool, country string) ([]Holiday, error) { + var holidays []Holiday + return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE "date" BETWEEN $1 AND $2 AND country = $3`+s.filter(isState, isReligious)+";", rangeStart, rangeEnd, country) +} + +func (s *Service) findByYear(year int, isState *bool, isReligious *bool, country string) ([]Holiday, error) { + var holidays []Holiday + return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE extract(year from "date") = $1 AND country = $2 `+s.filter(isState, isReligious)+";", year, country) +} + +func (s *Service) find(isState *bool, isReligious *bool, country string) ([]Holiday, error) { + var holidays []Holiday + return holidays, s.DB.Select(&holidays, `SELECT * FROM "holiday" WHERE country = $1 `+s.filter(isState, isReligious)+";", country) +} + +func (s *Service) paginate(holidays []Holiday, paging Paging) []Holiday { + start := paging.Page * paging.PageSize + end := (paging.Page + 1) * paging.PageSize + if end < len(holidays) { + return holidays[start:end] + } else if start < len(holidays) { + return holidays[start:] + } else { + return []Holiday{} + } +} + +func (s *Service) filter(isState *bool, isReligious *bool) string { + var filters []string + if isState != nil { + filters = append(filters, "is_state = "+strconv.FormatBool(*isState)) + } + if isReligious != nil { + filters = append(filters, "is_religious = "+strconv.FormatBool(*isReligious)) + } + if len(filters) > 0 { + return " AND " + strings.Join(filters, " AND ") + } else { + return "" + } +} + +func (s *Service) Update(holiday Holiday) (Holiday, error) { + _, err := s.DB.Exec(`UPDATE holiday SET "country" = $1, "name" = $2, "description" = $3, "date" = $4, "is_state"=$5, "is_religious"=$6 WHERE "id" = $7`, + &holiday.Country, &holiday.Name, &holiday.Description, &holiday.Date, &holiday.IsStateHoliday, &holiday.IsReligiousHoliday, &holiday.Id, + ) + return holiday, err +} + +func (s *Service) Create(holiday Holiday) (Holiday, error) { + holiday.Id = uuid.Must(uuid.NewRandom()) + _, err := s.DB.Exec(`INSERT INTO holiday (id, country, name, description, date, is_state, is_religious) values ($1, $2, $3, $4, $5, $6, $7)`, + &holiday.Id, &holiday.Country, &holiday.Name, &holiday.Description, &holiday.Date, &holiday.IsStateHoliday, &holiday.IsReligiousHoliday, + ) + return holiday, err +} + +func (s *Service) Delete(id uuid.UUID) error { + _, err := s.DB.Exec(`DELETE FROM holiday WHERE "id" = $1`, &id) + return err +} + +func getDateOrDefault(date *time.Time, year int) time.Time { + if date == nil { + return time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) + } else { + return *date + } +} diff --git a/main.go b/main.go index 09f4568..c9ce341 100644 --- a/main.go +++ b/main.go @@ -2,19 +2,36 @@ package main import ( "embed" + "fmt" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" + "github.com/google/uuid" "github.com/joho/godotenv" + _ "github.com/lib/pq" + "holiday-api/holiday" + "holiday-api/migration" + "html/template" "log" "net/http" - "template/migration" + "os" + "strconv" + "strings" + "time" ) //go:embed db/dev/*.sql var devMigrations embed.FS +//go:embed db/prod/*.sql +var prodMigrations embed.FS + +var isDev = false + func init() { godotenv.Load() + if strings.Contains(os.Getenv("PROFILE"), "dev") { + isDev = true + } log.SetPrefix("") log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) } @@ -24,7 +41,11 @@ func main() { if err != nil { log.Fatalf("couldn't connect to db: %v", err) } - if err := migration.InitializeMigrations(client, devMigrations); err != nil { + migrationFolder := prodMigrations + if isDev { + migrationFolder = devMigrations + } + if err := migration.InitializeMigrations(client, migrationFolder); err != nil { log.Fatalf("couldn't execute migrations: %v", err) } @@ -34,16 +55,183 @@ func main() { r.Use(middleware.Logger) r.Use(middleware.Recoverer) - r.Get("/", helloWorld) + holidayService := holiday.Service{client} + + r.Get("/api/v1/holidays", getHolidays(holidayService)) + + r.Mount("/admin", adminDashboard(holidayService)) log.Fatal(http.ListenAndServe(":5281", r)) } -func helloWorld(w http.ResponseWriter, r *http.Request) { - response := struct { - Title string `json:"title"` - Message string `json:"message"` - }{Title: "hello world", Message: "this is an example of response"} +func adminDashboard(service holiday.Service) http.Handler { + r := chi.NewRouter() - render(w, r, http.StatusOK, response) + credentials := loadCredentials() + templates, err := template.New("").Funcs(template.FuncMap{ + "boolcmp": func(value *bool, expected string) bool { + if value == nil { + return expected == "nil" + } else { + return (*value && expected == "true") || (!(*value) && expected == "false") + } + }, + "deferint": func(value *int) int { return *value }, + }).ParseFiles("templates/index.gohtml", "templates/holiday.gohtml", "templates/error.gohtml") + if err != nil { + log.Fatalf("couldn't load templates: %v", err) + } + + r.Use(middleware.BasicAuth("/admin", credentials)) + + r.Get("/holiday", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + id := r.FormValue("id") + + response := HolidaySingleResponse{ + Id: nil, + Country: "", + Date: DateResponse{time.Now()}, + Name: "", + Description: "", + IsStateHoliday: true, + IsReligiousHoliday: false, + } + + if id != "" { + if h, err := service.FindById(uuid.Must(uuid.Parse(id))); err == nil { + response = HolidaySingleResponse{ + Id: &h.Id, + Country: h.Country, + Date: DateResponse{h.Date}, + Name: h.Name, + Description: h.Description, + IsStateHoliday: h.IsStateHoliday, + IsReligiousHoliday: h.IsReligiousHoliday, + } + } + } + renderTemplate(w, r, 200, templates, "holiday.gohtml", response) + }) + + r.Post("/holiday/delete", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + var id uuid.UUID + if i := r.FormValue("id"); i != "" { + id = uuid.MustParse(i) + } else { + log.Printf("Failed deleting holiday: missing id", err) + w.WriteHeader(500) + w.Write([]byte("There was an error")) + } + err := service.Delete(id) + if err != nil { + log.Printf("Failed deleting holiday: %v", err) + w.WriteHeader(500) + w.Write([]byte("There was an error")) + } else { + w.Header().Add("Location", "/admin") + w.WriteHeader(http.StatusSeeOther) + w.Write([]byte{}) + } + }) + + r.Post("/holiday", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + var id *uuid.UUID = nil + if i := r.FormValue("id"); i != "" { + temp := uuid.MustParse(i) + id = &temp + } + name := strings.TrimSpace(r.FormValue("name")) + description := strings.TrimSpace(r.FormValue("description")) + isStateHoliday := parseCheckbox(r.FormValue("stateHoliday")) + isReligiousHoliday := parseCheckbox(r.FormValue("religiousHoliday")) + date := parseDate(r.FormValue("date")) + country := r.FormValue("country") + + hol := holiday.Holiday{ + Country: country, + Date: date, + Name: name, + Description: description, + IsStateHoliday: isStateHoliday, + IsReligiousHoliday: isReligiousHoliday, + } + + var err error + if id != nil { + hol.Id = *id + hol, err = service.Update(hol) + } else { + hol, err = service.Create(hol) + } + if err != nil { + log.Printf("Failed updating or creating holiday: %v", err) + w.WriteHeader(500) + w.Write([]byte("There was an error")) + } else { + w.Header().Add("Location", "/admin/holiday?id="+hol.Id.String()) + w.WriteHeader(http.StatusSeeOther) + w.Write([]byte{}) + } + }) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + country := r.FormValue("country") + if country == "" { + country = "HR" + } + var stateHoliday *bool = nil + if h := r.FormValue("stateHoliday"); h != "" { + if hol, err := strconv.ParseBool(h); err == nil { + stateHoliday = &hol + } + } + var religiousHoliday *bool = nil + if h := r.FormValue("religiousHoliday"); h != "" { + if hol, err := strconv.ParseBool(h); err == nil { + religiousHoliday = &hol + } + } + year := time.Now().Year() + if y := r.FormValue("year"); y != "" { + if yr, err := strconv.ParseInt(y, 10, 64); err == nil { + year = int(yr) + } + } + + search := holiday.Search{Year: &year, Country: country, StateHoliday: stateHoliday, ReligiousHoliday: religiousHoliday} + + holidays, _ := service.Find(search, holiday.Paging{PageSize: 100}) + holidayResponse := mapHolidays(holidays) + + response := map[string]any{} + response["holidays"] = holidayResponse + response["search"] = search + + renderTemplate(w, r, 200, templates, "index.gohtml", response) + }) + + return r +} + +func parseDate(date string) time.Time { + var year, month, day int + fmt.Sscanf(date, "%d-%d-%d", &year, &month, &day) + return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) +} + +func parseCheckbox(value string) bool { + if value != "" { + return true + } + return false +} + +func loadCredentials() map[string]string { + credentials := envMustExist("AUTH_KEY") + values := strings.Split(credentials, ":") + return map[string]string{values[0]: values[1]} } diff --git a/makefile b/makefile index 02434d2..4df36f2 100644 --- a/makefile +++ b/makefile @@ -10,17 +10,17 @@ setup: go get docker-dev: - docker image build -t registry.bbr-dev.info/template/backend:$(VERSION)-dev . - docker tag registry.bbr-dev.info/template/backend:$(VERSION)-dev registry.bbr-dev.info/template/backend:latest-dev - docker image push registry.bbr-dev.info/template/backend:$(VERSION)-dev - docker image push registry.bbr-dev.info/template/backend:latest-dev + docker image build -t registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev . + docker tag registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev registry.bbr-dev.info/holiday-api/backend:latest-dev + docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION)-dev + docker image push registry.bbr-dev.info/holiday-api/backend:latest-dev docker-prod: - docker image build -t registry.bbr-dev.info/template/backend:$(VERSION) . - docker tag registry.bbr-dev.info/template/backend:$(VERSION) registry.bbr-dev.info/template/backend:latest - docker image push registry.bbr-dev.info/template/backend:$(VERSION) - docker image push registry.bbr-dev.info/template/backend:latest + docker image build -t registry.bbr-dev.info/holiday-api/backend:$(VERSION) . + docker tag registry.bbr-dev.info/holiday-api/backend:$(VERSION) registry.bbr-dev.info/holiday-api/backend:latest + docker image push registry.bbr-dev.info/holiday-api/backend:$(VERSION) + docker image push registry.bbr-dev.info/holiday-api/backend:latest release: git tag $(VERSION) @@ -30,4 +30,4 @@ test: go test ./... clean: - rm -rf template + rm -rf holiday-api diff --git a/render.go b/render.go index 2f8a72e..1f5e27d 100644 --- a/render.go +++ b/render.go @@ -1,21 +1,58 @@ package main import ( + "bytes" "encoding/json" + "encoding/xml" + "html/template" "log" "net/http" ) -func render[T any](w http.ResponseWriter, r *http.Request, status int, response T) error { - if body, err := json.MarshalIndent(response, "", " "); err == nil { - w.Header().Add("content-type", "application/json") +func renderTemplate[T any](w http.ResponseWriter, r *http.Request, status int, templates *template.Template, template string, response T) error { + buffer := bytes.Buffer{} + if err := templates.Lookup(template).Execute(&buffer, response); err == nil { + w.Header().Add("content-type", "text/html; charset=utf-8") w.WriteHeader(status) - _, err = w.Write(body) + _, err = w.Write(buffer.Bytes()) return err } else { - w.WriteHeader(http.StatusInternalServerError) - log.Printf("couldn't parse response") - _, err = w.Write([]byte{}) - return err + return onError(w, err) + } +} + +func onError(w http.ResponseWriter, err error) error { + w.WriteHeader(http.StatusInternalServerError) + log.Printf("failed writing response: %v", err) + _, _ = w.Write([]byte{}) + return err +} + +func render[T any](w http.ResponseWriter, r *http.Request, status int, response T) error { + switch r.Header.Get("accept") { + case "application/xml": + fallthrough + case "text/xml": + if body, err := xml.MarshalIndent(response, "", " "); err == nil { + // we return the same type as requested + w.Header().Add("content-type", r.Header.Get("accept")+"; charset=utf-8") + w.WriteHeader(status) + _, err = w.Write([]byte(xml.Header)) + _, err = w.Write(body) + return err + } else { + return onError(w, err) + } + case "application/json": + fallthrough + default: + if body, err := json.MarshalIndent(response, "", " "); err == nil { + w.Header().Add("content-type", "application/json; charset=utf-8") + w.WriteHeader(status) + _, err = w.Write(body) + return err + } else { + return onError(w, err) + } } } diff --git a/templates/error.gohtml b/templates/error.gohtml new file mode 100644 index 0000000..1d41308 --- /dev/null +++ b/templates/error.gohtml @@ -0,0 +1,5 @@ + + +

Test for error

+ + \ No newline at end of file diff --git a/templates/holiday.gohtml b/templates/holiday.gohtml new file mode 100644 index 0000000..31540a0 --- /dev/null +++ b/templates/holiday.gohtml @@ -0,0 +1,44 @@ + + + + + + + Holiday-api holiday + + +

Holiday-api - {{if .Id}}Updating{{else}}Creating{{end}}

+

Page for creating or editing holidays

+Back +
+ {{if .Id}} + + {{end}} + + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/templates/index.gohtml b/templates/index.gohtml new file mode 100644 index 0000000..2018e96 --- /dev/null +++ b/templates/index.gohtml @@ -0,0 +1,102 @@ + + + + + + + Holiday-api + + +

Holiday-api

+

Welcome to admin interface for holiday api

+ +
+ Add a holiday +
+
+
+ + + + + + + +
+ + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ + +
+
+
+ + + + + + + + + + + + + {{range $entry := .holidays.Holidays}} + + + + + + + + + {{end}} + +
NameDescriptionDateState holidayReligious holiday
{{$entry.Name}}{{$entry.Description}}{{$entry.Date.Format "2006-01-02"}}{{$entry.IsStateHoliday}}{{$entry.IsReligiousHoliday}} + Edit +
+ + +
+
+
+ + \ No newline at end of file