diff --git a/README.md b/README.md index 031cbd3..a0c2541 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ As a fan of [TimeAPI.io](https://www.timeapi.io/swagger/index.html), but not thi ## Usage ```shell -neo@matrix:~$ curl -s -H "Accept: application/json" "http://localhost:9100/Europe/Berlin" | jq +neo@matrix:~$ curl -s -H "Accept: application/json" "http://localhost:9100/Time/current/zone?timeZone=Europe/Berlin" | jq { "year": 2023, "month": 6, diff --git a/apiTime/api.go b/apiTime/api.go new file mode 100644 index 0000000..f4fb061 --- /dev/null +++ b/apiTime/api.go @@ -0,0 +1,140 @@ +package apiTime + +import "git.0x0001f346.de/andreas/api" + +var tags []string = []string{"Time"} + +func GetComponents() *api.Components { + components := &api.Components{ + Schemas: map[string]*api.ComponentsSchema{ + "CurrentTime": { + Properties: map[string]*api.ComponentsProperty{ + "year": { + Type: "integer", + Description: "Year", + Format: "int64", + Example: 2020, + }, + "month": { + Type: "integer", + Description: "Month", + Format: "int64", + Example: 12, + }, + "day": { + Type: "integer", + Description: "Day", + Format: "int64", + Example: 13, + }, + "hour": { + Type: "integer", + Description: "Hour of the day in range 0-24", + Format: "int64", + Example: 9, + }, + "minute": { + Type: "integer", + Description: "Minute", + Format: "int64", + Example: 30, + }, + "seconds": { + Type: "integer", + Description: "Second", + Format: "int64", + Example: 30, + }, + "milliSeconds": { + Type: "integer", + Description: "Milliseconds", + Format: "int64", + Example: 123, + }, + "dateTime": { + Type: "string", + Description: "Full date time", + Format: "date-time", + Example: "2020-12-13T09:30:30+01:00", + }, + "date": { + Type: "string", + Description: "Date string", + Example: "13/12/2020", + }, + "time": { + Type: "string", + Description: "Time string", + Example: "09:30", + }, + "timeZone": { + Type: "string", + Description: "TimeZone of the result", + Example: "Europe/Berlin", + }, + "dayOfWeek": { + Type: "string", + Description: "The day of the week", + Example: "Monday", + }, + "dstActive": { + Type: "boolean", + Description: "Boolean describing whether DST is applied and active in that timezone", + Example: false, + }, + }, + AdditionalProperties: false, + }, + }, + } + + return components +} + +func GetPaths() map[string]*api.Path { + paths := map[string]*api.Path{ + "/Time/current/zone": { + GET: &api.Method{ + Function: TimeCurrentZone, + Tags: tags, + Summary: "Gets the current time of a time zone.", + Parameters: []*api.Parameters{ + { + Name: "timeZone", + In: "query", + Description: "Full IANA time zone names.", + Schema: &api.ParametersSchema{ + Type: "string", + }, + Example: "Europe/Berlin", + AllowReserved: true, + }, + }, + Responses: map[int]*api.Response{ + 200: { + Description: "Current time", + Content: map[string]*api.ResponseContent{ + "application/json": { + Schema: &api.ResponseSchema{ + Ref: "#/components/schemas/CurrentTime", + }, + }, + }, + }, + 400: { + Description: "Error message", + Content: map[string]*api.ResponseContent{ + "application/json": { + Schema: &api.ResponseSchema{ + Type: "string", + }, + }, + }, + }, + }, + }, + }, + } + + return paths +} diff --git a/apiTime/time.go b/apiTime/time.go new file mode 100644 index 0000000..f7e160c --- /dev/null +++ b/apiTime/time.go @@ -0,0 +1,81 @@ +package apiTime + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "time" +) + +type dateTimeResponse struct { + Year int `json:"year,omitempty"` + Month int `json:"month,omitempty"` + Day int `json:"day,omitempty"` + Hour int `json:"hour,omitempty"` + Minute int `json:"minute,omitempty"` + Seconds int `json:"seconds,omitempty"` + MilliSeconds int `json:"milliSeconds,omitempty"` + DateTime string `json:"dateTime,omitempty"` + Date string `json:"date,omitempty"` + Time string `json:"time,omitempty"` + TimeZone string `json:"timeZone,omitempty"` + DayOfWeek string `json:"dayOfWeek,omitempty"` + DstActive bool `json:"dstActive,omitempty"` +} + +func TimeCurrentZone(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Language", "en") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + timeZone := "" + + vals := r.URL.Query() + parameters, ok := vals["timeZone"] + if ok && len(parameters) == 1 { + timeZone = parameters[0] + } + + if timeZone == "" { + w.WriteHeader(400) + fmt.Fprint(w, "{\"error\": \"invalid value for timeZone\"}") + return + } + + loc, err := time.LoadLocation(timeZone) + if err != nil { + w.WriteHeader(400) + fmt.Fprint(w, "{\"error\": \"invalid value for timeZone\"}") + return + } + + log.Printf("%q %q\n", loc.String(), r.RemoteAddr) + + w.WriteHeader(200) + fmt.Fprint(w, getDateTimeResponseAsJSONString(loc)) +} + +func getDateTimeResponse(loc *time.Location) dateTimeResponse { + t := time.Now().In(loc) + return dateTimeResponse{ + Year: t.Year(), + Month: int(t.Month()), + Day: t.Day(), + Hour: t.Hour(), + Minute: t.Minute(), + Seconds: t.Second(), + MilliSeconds: func(i int64, err error) int { return int(i) }(strconv.ParseInt(strconv.Itoa(t.Nanosecond())[:3], 10, 0)), + DateTime: t.Format(time.RFC3339), + Date: fmt.Sprintf("%02d/%02d/%04d", t.Month(), t.Day(), t.Year()), + Time: fmt.Sprintf("%02d:%02d", t.Hour(), t.Minute()), + TimeZone: loc.String(), + DayOfWeek: t.Weekday().String(), + DstActive: t.IsDST(), + } +} + +func getDateTimeResponseAsJSONString(loc *time.Location) string { + r, _ := json.Marshal(getDateTimeResponse(loc)) + return string(r) +} diff --git a/main.go b/main.go index 0fe8515..079f961 100644 --- a/main.go +++ b/main.go @@ -1,34 +1,83 @@ package main import ( - "encoding/json" "fmt" "log" "net/http" - "strconv" - "time" + + "git.0x0001f346.de/andreas/Datetime-API/apiTime" + "git.0x0001f346.de/andreas/api" + "github.com/gorilla/mux" ) -var portToListenOn int = 9100 -var defaultTZ string = "Europe/Berlin" - -type DateTimeResponse struct { - Year int `json:"year,omitempty"` - Month int `json:"month,omitempty"` - Day int `json:"day,omitempty"` - Hour int `json:"hour,omitempty"` - Minute int `json:"minute,omitempty"` - Seconds int `json:"seconds,omitempty"` - MilliSeconds int `json:"milliSeconds,omitempty"` - DateTime string `json:"dateTime,omitempty"` - Date string `json:"date,omitempty"` - Time string `json:"time,omitempty"` - TimeZone string `json:"timeZone,omitempty"` - DayOfWeek string `json:"dayOfWeek,omitempty"` - DstActive bool `json:"dstActive,omitempty"` -} +const apiTitle string = "Datetime-API" +const apiDescription string = "A simple API to get the current time for a given time zone." +const apiVersion string = "0.1" +const portToListenOn int = 9100 func main() { + API := buildAPI() + router := mux.NewRouter() + + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Language", "en") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + fmt.Fprint(w, API.ToJSONString()) + }).Methods("GET") + + for route, path := range API.Paths { + if path.DELETE != nil { + if path.DELETE.Function == nil { + continue + } + router.HandleFunc(route, path.DELETE.Function).Methods("DELETE") + } + if path.GET != nil { + if path.GET.Function == nil { + continue + } + router.HandleFunc(route, path.GET.Function).Methods("GET") + } + if path.PATCH != nil { + if path.PATCH.Function == nil { + continue + } + router.HandleFunc(route, path.PATCH.Function).Methods("PATCH") + } + if path.POST != nil { + if path.POST.Function == nil { + continue + } + router.HandleFunc(route, path.POST.Function).Methods("POST") + } + if path.PUT != nil { + if path.PUT.Function == nil { + continue + } + router.HandleFunc(route, path.PUT.Function).Methods("PUT") + } + } + + printStartupBanner() + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", portToListenOn), router)) +} + +func buildAPI() api.API { + fullAPI := api.GetAPIPrototype(apiTitle, apiDescription, apiVersion) + + for route, path := range apiTime.GetPaths() { + fullAPI.Paths[route] = path + } + + for name, schema := range apiTime.GetComponents().Schemas { + fullAPI.Components.Schemas[name] = schema + } + + return fullAPI +} + +func printStartupBanner() { fmt.Println("********************************************") fmt.Println("* *") fmt.Println("* git.0x0001f346.de/andreas/Datetime-API *") @@ -37,51 +86,4 @@ func main() { fmt.Println("") fmt.Printf("Listening: http://localhost:%d\n", portToListenOn) fmt.Println("") - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - loc, _ := time.LoadLocation(defaultTZ) - if r.URL.String() != "/" { - newloc, err := time.LoadLocation(r.URL.String()[1:]) - if err == nil { - loc = newloc - } - } - - log.Printf( - "%q %q\n", - loc.String(), - r.RemoteAddr, - ) - - w.Header().Add("Content-Language", "en") - w.Header().Add("Content-Type", "application/json; charset=utf-8") - w.Header().Add("Server", "git.0x0001f346.de/andreas/Datetime-API") - fmt.Fprint(w, GetDateTimeResponseAsJSONString(loc)) - }) - - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", portToListenOn), nil)) -} - -func GetDateTimeResponse(loc *time.Location) DateTimeResponse { - t := time.Now().In(loc) - return DateTimeResponse{ - Year: t.Year(), - Month: int(t.Month()), - Day: t.Day(), - Hour: t.Hour(), - Minute: t.Minute(), - Seconds: t.Second(), - MilliSeconds: func(i int64, err error) int { return int(i) }(strconv.ParseInt(strconv.Itoa(t.Nanosecond())[:3], 10, 0)), - DateTime: t.Format(time.RFC3339), - Date: fmt.Sprintf("%02d/%02d/%04d", t.Month(), t.Day(), t.Year()), - Time: fmt.Sprintf("%02d:%02d", t.Hour(), t.Minute()), - TimeZone: loc.String(), - DayOfWeek: t.Weekday().String(), - DstActive: t.IsDST(), - } -} - -func GetDateTimeResponseAsJSONString(loc *time.Location) string { - r, _ := json.Marshal(GetDateTimeResponse(loc)) - return string(r) }