Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,26 @@ Create a public repo on Github and push your code on it. then share the link bac
* oAuth2 integration to protect your APIs by registering Auth0 free account.
* A simple front end React application offering a visualization of all or part of the data utilizing the API you have built as a back end.
* Anything else you think is cool, relevant, and consistent with the other requirements.

## Installation

This backend code binary mainly divide into 2 parts, which are located in the folder cmd/apis/main.go and cmd/cron/main.go
apis is used to serve API and cron is used to sync data automatically. and this can be installed in each docker or pod for kubernetes cluster

* Please create an empty database using 'indweathdb' as the name. tables and others will be created automatically by the application
* Run APIs using command
```bash
go run cmd/apis/main.go --config config.yaml
```
* Run Cron using command
```bash
go run cmd/cron/main.go --config config.yaml
```
* there is a file named config.yaml as the application config, you can change the connection to postgresql, service port for APIs and include the API key for OpenWeather there.
* point your web browser to http://localhost:8080/swagger to access APIs documentation based on openAPI specification
* I made some unit test endpoints. not complete, but i think enough to prove that i can do it. service/service_test.go and controller/controller_test.go . you can run it by:
```bash
go test /service
go test /controller
```

96 changes: 96 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package client

import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
)

type ResponseBody []byte
type Headers map[string]string
type QueryParams map[string]string

type Client struct {
client *http.Client
headers Headers
method string
url string
payload io.Reader
StatusCode int
log *slog.Logger
queryParams QueryParams
}

func New(baseURL string) *Client {
return &Client{
// this is baseUrl only, need to updated for specific path later using other func
url: baseURL,
client: &http.Client{},
headers: make(map[string]string),
}
}

func (c *Client) Token(token string) *Client {
c.headers["Authorization"] = "Bearer " + token
return c
}

func (c *Client) GetJSON(path string, response interface{}, query map[string]string) (int, error) {
c.headers["Content-Type"] = "application/json"
c.method = "GET"
c.url = c.url + path + "?" + c.convertMapToQuery(query)
return c.sendRequest(response)
}

func (c *Client) sendRequest(responseStruct interface{}) (int, error) {
req, err := http.NewRequest(c.method, c.url, c.payload)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("error creating request: %v", err)
}

for key, value := range c.headers {
req.Header.Set(key, value)
}

resp, err := c.client.Do(req)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("error sending request: %v", err)
}
defer resp.Body.Close()

c.StatusCode = resp.StatusCode

body, err := io.ReadAll(resp.Body)
if err != nil {
return resp.StatusCode, fmt.Errorf("error reading response body: %v", err)
}

// no need to unmarshall the body, since there is no content
if c.StatusCode != http.StatusNoContent {
if err := c.unmarshalJSON(body, responseStruct); err != nil {
return resp.StatusCode, fmt.Errorf("error unmarshaling response body: %v", err)
}
}

return resp.StatusCode, nil
}

func (c *Client) unmarshalJSON(responseBody ResponseBody, target interface{}) error {
err := json.Unmarshal(responseBody, target)
if err != nil {
return fmt.Errorf("failed to unmarshal JSON: %w", err)
}
return nil
}

func (c *Client) convertMapToQuery(params map[string]string) string {
query := url.Values{}
for key, value := range params {
query.Add(key, value)
}

return query.Encode()
}
45 changes: 45 additions & 0 deletions cmd/apis/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"fmt"
"os"
"strings"

"github.com/herux/indegooweather/config"
"github.com/herux/indegooweather/constant"
"github.com/herux/indegooweather/db"
"github.com/herux/indegooweather/route"
"github.com/herux/indegooweather/server"
flag "github.com/spf13/pflag"
)

func main() {
path := getConfigPath()
_ = config.Load(path)
db.Init(false)

srv := server.SetupService(config.Service(), route.RegisterAPI)
srv.Run()
}

func getConfigPath() string {
f := flag.NewFlagSet("indegooweather-apis", flag.ExitOnError)
f.Usage = func() {
fmt.Println(getFlagUsage(f))
os.Exit(0)
}
config := f.String("config", constant.DefaultConfigFile, "configuration file path")
f.Parse(os.Args[1:])

return *config
}

func getFlagUsage(f *flag.FlagSet) string {
usage := "indegooweather-apis\n\n"
usage += "Options:\n"

options := strings.ReplaceAll(f.FlagUsages(), " ", " ")
usage += fmt.Sprintf("%s\n", options)

return usage
}
59 changes: 59 additions & 0 deletions cmd/cron/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import (
"fmt"
"log"
"os"
"strings"

"github.com/go-co-op/gocron/v2"
"github.com/herux/indegooweather/config"
"github.com/herux/indegooweather/constant"
"github.com/herux/indegooweather/cron"
"github.com/herux/indegooweather/db"
"github.com/herux/indegooweather/jobs"
flag "github.com/spf13/pflag"
)

func main() {
path := getConfigPath()
_ = config.Load(path)

db.Init(false)

cron := cron.NewCron()
scheduler, err := gocron.NewScheduler()
if err != nil {
log.Fatalf("error creating scheduler: %v", err)
}

jobs.FetchWeatherJob(scheduler, config.OpenWeatherAPIKey())
jobs.FetchBikestationJob(scheduler)

cron.NewScheduler(scheduler)
cron.Start()

select {}
}

func getConfigPath() string {
f := flag.NewFlagSet("indegooweather-cron", flag.ExitOnError)
f.Usage = func() {
fmt.Println(getFlagUsage(f))
os.Exit(0)
}
config := f.String("config", constant.DefaultConfigFile, "configuration file path")
f.Parse(os.Args[1:])

return *config
}

func getFlagUsage(f *flag.FlagSet) string {
usage := "indegooweather-cron\n\n"
usage += "Options:\n"

options := strings.ReplaceAll(f.FlagUsages(), " ", " ")
usage += fmt.Sprintf("%s\n", options)

return usage
}
12 changes: 12 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
database:
host: localhost
port: 5432
protocol: postgres
user: postgres
password: postgres
dbname: indweathdb

oweather_apikey: e4d25c020947523c7c18b8e4af1ce00e
service:
port: 8080
readTimeout:
69 changes: 69 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package config

import (
"os"

"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
)

type appConfig struct {
ApiKeyOpenWeather string `yaml:"oweather_apikey"`
DB DbConfig `yaml:"database"`
Service ServerConfig `yaml:"service"`
}

type DbConfig struct {
Host string `yaml:"host"`
Port uint16 `yaml:"port"`
Protocol string `yaml:"protocol"`
User string `yaml:"user"`
Password string `yaml:"password"`
Dbname string `yaml:"dbname"`
}

type ServerConfig struct {
Port uint16 `yaml:"port"`
ReadTimeout uint `yaml:"readTimeout"`
}

var (
k = koanf.New(".")
conf = appConfig{}
)

func Load(config string) error {
err := k.Load(file.Provider(config), yaml.Parser())
if err != nil {
goto HandleError
}

if err != nil {
goto HandleError
}

err = k.UnmarshalWithConf("", &conf, koanf.UnmarshalConf{Tag: "yaml"})
if err != nil {
goto HandleError
}

HandleError:
if err != nil {
os.Exit(1)
}

return err
}

func DatabaseConfig() *DbConfig {
return &conf.DB
}

func OpenWeatherAPIKey() string {
return conf.ApiKeyOpenWeather
}

func Service() *ServerConfig {
return &conf.Service
}
5 changes: 5 additions & 0 deletions constant/constant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package constant

const (
DefaultConfigFile = "/etc/indego-weather/config.yaml"
)
Loading