feat: added server configuration using env vars (#7)

* feat: added server configuration using env vars

* updated readme

* refactor: removed error returns where unneeded
This commit is contained in:
Felipe Martin Garcia 2022-08-09 18:27:37 +02:00 committed by GitHub
parent e69ecef6c8
commit 5543a19df1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 150 additions and 50 deletions

View File

@ -2,35 +2,60 @@
A service/library to extract product information from URLs.
## Configuration
| Variable name | Default | Description |
| -------------- | ------- | ------------------------------ |
| `HTTP_ENABLED` | `true` | Enable/Disable the HTTP server |
| `HTTP_PORT` | `8080` | Port to serve the HTTP in |
## Servers
### HTTP
- `POST /item`
Parameters:
- **url**: The URL to extract information from
Responses:
- `200`: Information extracted
- `400`: Shop not supported, missing parameters
- `500`: Internal error, check logs
- `GET /liveness`
Responses:
- `200`: Server running
## Data model
Currently, this information is extracted from the site (if possbile):
``` js
{
"image_url": "<url>", // (string) URL to an image file
"in_stock": false, // (bool) If the item is currently available for purchase
"name": "<name>", // (string) The name of the product as it appears on the site
"price": 14.21, // (optional, float) The price of the product [parsed by the library]
"price_text": "14,21 €", // (optional, string) The price of the product as it appears on the site (with currency)
"release_date": "2021-03-22T00:00:00Z", // (optional, string RFC3339) the release date of the item
"url": "<url>" // (string) The URL of the item
"description": "...",
"image_url": "https://...",
"in_stock": false,
"name": "...",
"price": 0.0,
"price_text": "0,0 €",
"release_date": "2019-03-08T00:00:00Z",
"url": "https://..."
}
```
[pkg/models/product.go](./pkg/models/product.go)
## Supported sites
Support is handled in a _best effort_ basis. Some sites do not provided all exposed fields.
- [Amazon.es](https://amazon.es)
- [Amazon.com](https://amazon.com)
- [Akira Comics](https://www.akiracomics.com)
- [AkiraComics](https://www.akiracomics.com)
- [Casa del libro](https://www.casadellibro.com)
- [Heroes De Papel](https://heroesdepapel.es)
- [Steam](https://store.steampowered.com)
## Running
```
go run cmd/server/main.go
```

View File

@ -1,6 +1,9 @@
package main
import (
"context"
"log"
"github.com/fmartingr/bazaar/internal/server"
"github.com/fmartingr/bazaar/pkg/manager"
"github.com/fmartingr/bazaar/pkg/shop/akiracomics"
@ -20,11 +23,13 @@ func main() {
m.Register(gtmstore.Domains, gtmstore.NewGTMStoreShopFactory())
m.Register(casadellibro.Domains, casadellibro.NewCasaDelLibroShopFactory())
server := server.NewServer(server.ServerConf{
HttpPort: 8080,
}, &m)
ctx := context.Background()
server.Start()
server := server.NewServer(server.ParseServerConfiguration(ctx), &m)
if err := server.Start(ctx); err != nil {
log.Panicf("error starting server: %s", err)
}
server.WaitStop()
}

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.19
require (
github.com/PuerkitoBio/goquery v1.8.0
github.com/goodsign/monday v1.0.0
github.com/sethvargo/go-envconfig v0.8.2
github.com/stretchr/testify v1.8.0
)

3
go.sum
View File

@ -7,8 +7,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goodsign/monday v1.0.0 h1:Yyk/s/WgudMbAJN6UWSU5xAs8jtNewfqtVblAlw0yoc=
github.com/goodsign/monday v1.0.0/go.mod h1:r4T4breXpoFwspQNM+u2sLxJb2zyTaxVGqUfTBjWOu8=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sethvargo/go-envconfig v0.8.2 h1:DDUVuG21RMgeB/bn4leclUI/837y6cQCD4w8hb5797k=
github.com/sethvargo/go-envconfig v0.8.2/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

64
internal/server/config.go Normal file
View File

@ -0,0 +1,64 @@
package server
import (
"bufio"
"context"
"log"
"os"
"strings"
"github.com/sethvargo/go-envconfig"
)
// readDotEnv reads the configuration from variables in a .env file (only for contributing)
func readDotEnv() map[string]string {
file, err := os.Open(".env")
if err != nil {
return nil
}
defer file.Close()
result := make(map[string]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "#") {
continue
}
keyval := strings.SplitN(line, "=", 2)
result[keyval[0]] = keyval[1]
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
return result
}
type ServerConfig struct {
Hostname string `env:"HOSTNAME,required"`
Http struct {
Enabled bool `env:"HTTP_ENABLED,default=True"`
Port int `env:"HTTP_PORT,default=8080"`
}
}
func ParseServerConfiguration(ctx context.Context) *ServerConfig {
var cfg ServerConfig
lookuper := envconfig.MultiLookuper(
envconfig.MapLookuper(map[string]string{"HOSTNAME": os.Getenv("HOSTNAME")}),
envconfig.MapLookuper(readDotEnv()),
envconfig.PrefixLookuper("BAZAAR_", envconfig.OsLookuper()),
envconfig.OsLookuper(),
)
if err := envconfig.ProcessWith(ctx, &cfg, lookuper); err != nil {
log.Fatalf("Error parsing configuration: %s", err)
}
return &cfg
}

View File

@ -50,7 +50,9 @@ func NewHttpServer(port int, m *manager.Manager) *httpServer {
payload, _ := json.Marshal(product)
rw.Header().Add("Content-Type", "application/json")
rw.Write(payload)
if _, err := rw.Write(payload); err != nil {
log.Printf("error writting response: %s", err)
}
})
mux.HandleFunc("/liveness", func(w http.ResponseWriter, r *http.Request) {

View File

@ -14,54 +14,58 @@ import (
"github.com/fmartingr/bazaar/pkg/manager"
)
type ServerConf struct {
HttpPort int
}
type Server struct {
http internalModels.Server
Http internalModels.Server
config *ServerConfig
cancel context.CancelFunc
}
func (s *Server) Start() error {
ctx, cancel := context.WithCancel(context.Background())
func (s *Server) Start(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
s.cancel = cancel
go func() {
if err := s.http.Start(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("error starting server: %s", err)
}
}()
if s.config.Http.Enabled {
go func() {
if err := s.Http.Start(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("error starting server: %s", err)
}
}()
}
return nil
}
func (s *Server) WaitStop() error {
func (s *Server) WaitStop() {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
sig := <-signals
log.Printf("signal %s received, shutting down", sig)
return s.Stop()
s.Stop()
}
func (s *Server) Stop() error {
func (s *Server) Stop() {
s.cancel()
shuwdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := s.http.Stop(shuwdownContext); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("error shutting down http server: %s", err)
}
return nil
}
func NewServer(serverConf ServerConf, m *manager.Manager) *Server {
return &Server{
http: NewHttpServer(serverConf.HttpPort, m),
if s.config.Http.Enabled {
if err := s.Http.Stop(shuwdownContext); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("error shutting down http server: %s", err)
}
}
}
func NewServer(conf *ServerConfig, m *manager.Manager) *Server {
server := &Server{
config: conf,
}
if conf.Http.Enabled {
server.Http = NewHttpServer(conf.Http.Port, m)
}
return server
}

View File

@ -14,19 +14,15 @@ type Manager struct {
domains map[string]models.Shop
}
func (m *Manager) Register(domains []string, shopFactory models.ShopFactory) error {
func (m *Manager) Register(domains []string, shopFactory models.ShopFactory) {
baseShop := models.NewShopOptions(clients.NewBasicHttpClient())
shop := shopFactory(baseShop)
for _, domain := range domains {
if _, exists := m.domains[domain]; exists {
return fmt.Errorf("domain %s is already registered", domain)
} else {
if _, exists := m.domains[domain]; !exists {
m.domains[domain] = shop
}
}
return nil
}
func (m *Manager) GetShop(host string) models.Shop {