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
+
+
+
\ 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
+
+
+
+
+
+
+
+ Name |
+ Description |
+ Date |
+ State holiday |
+ Religious holiday |
+ |
+
+
+
+ {{range $entry := .holidays.Holidays}}
+
+ {{$entry.Name}} |
+ {{$entry.Description}} |
+ {{$entry.Date.Format "2006-01-02"}} |
+ {{$entry.IsStateHoliday}} |
+ {{$entry.IsReligiousHoliday}} |
+
+ Edit
+
+ |
+
+ {{end}}
+
+
+
+
+
\ No newline at end of file