Skip to content

Commit

Permalink
feat: Add WebhookService (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
allisson committed Mar 5, 2021
1 parent dbdcbf7 commit 7bbec35
Show file tree
Hide file tree
Showing 19 changed files with 971 additions and 23 deletions.
27 changes: 27 additions & 0 deletions cmd/postmand/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import (
"time"

"github.com/allisson/go-env"
"github.com/allisson/postmand/http"
"github.com/allisson/postmand/http/handler"
"github.com/allisson/postmand/repository"
"github.com/allisson/postmand/service"
"github.com/go-chi/chi/v5"
"github.com/jmoiron/sqlx"
_ "github.com/joho/godotenv/autoload"
"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -90,6 +93,30 @@ func main() {

<-idleConnsClosed

return nil
},
},
{
Name: "server",
Aliases: []string{"s"},
Usage: "executes http server",
Action: func(c *cli.Context) error {
webhookRepository := repository.NewWebhook(db)
webhookService := service.NewWebhook(webhookRepository)
webhookHandler := handler.NewWebhook(webhookService, logger)

mux := http.NewRouter(logger)
mux.Route("/v1/webhooks", func(r chi.Router) {
r.Get("/", webhookHandler.List)
r.Post("/", webhookHandler.Create)
r.Get("/{webhook_id}", webhookHandler.Get)
r.Put("/{webhook_id}", webhookHandler.Update)
r.Delete("/{webhook_id}", webhookHandler.Delete)
})

server := http.NewServer(mux, env.GetInt("POSTMAND_HTTP_PORT", 8000), logger)
server.Run()

return nil
},
},
Expand Down
4 changes: 0 additions & 4 deletions entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ type Webhook struct {
// Validate implements ozzo validation Validatable interface
func (w Webhook) Validate() error {
return validation.ValidateStruct(&w,
validation.Field(&w.ID, validation.Required, is.UUIDv4),
validation.Field(&w.Name, validation.Required, validation.Length(3, 255)),
validation.Field(&w.URL, validation.Required, is.URL),
validation.Field(&w.ContentType, validation.Required, validation.In("application/x-www-form-urlencoded", "application/json")),
Expand All @@ -68,10 +67,7 @@ type Delivery struct {
// Validate implements ozzo validation Validatable interface
func (d Delivery) Validate() error {
return validation.ValidateStruct(&d,
validation.Field(&d.ID, validation.Required, is.UUIDv4),
validation.Field(&d.WebhookID, validation.Required, is.UUIDv4),
validation.Field(&d.ScheduledAt, validation.Required),
validation.Field(&d.Status, validation.Required, validation.In(DeliveryStatusPending, DeliveryStatusSucceeded, DeliveryStatusFailed)),
)
}

Expand Down
9 changes: 2 additions & 7 deletions entity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestWebhook(t *testing.T) {
{
"required fields",
Webhook{},
`{"content_type":"cannot be blank","delivery_attempt_timeout":"cannot be blank","id":"must be a valid UUID v4","max_delivery_attempts":"cannot be blank","name":"cannot be blank","retry_max_backoff":"cannot be blank","retry_min_backoff":"cannot be blank","url":"cannot be blank","valid_status_codes":"cannot be blank"}`,
`{"content_type":"cannot be blank","delivery_attempt_timeout":"cannot be blank","max_delivery_attempts":"cannot be blank","name":"cannot be blank","retry_max_backoff":"cannot be blank","retry_min_backoff":"cannot be blank","url":"cannot be blank","valid_status_codes":"cannot be blank"}`,
},
{
"Short name",
Expand Down Expand Up @@ -73,12 +73,7 @@ func TestDelivery(t *testing.T) {
{
"required fields",
Delivery{},
`{"id":"must be a valid UUID v4","scheduled_at":"cannot be blank","status":"cannot be blank","webhook_id":"must be a valid UUID v4"}`,
},
{
"invalid status option",
Delivery{ID: uuid.New(), WebhookID: uuid.New(), ScheduledAt: time.Now().UTC(), Status: "error"},
`{"status":"must be a valid value"}`,
`{"webhook_id":"must be a valid UUID v4"}`,
},
}
for _, tt := range tests {
Expand Down
8 changes: 8 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package postmand

import "errors"

var (
// ErrWebhookNotFound is returned by any operation that can't load a webhook.
ErrWebhookNotFound = errors.New("webhook_not_found")
)
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.16
require (
github.com/DATA-DOG/go-txdb v0.1.3
github.com/allisson/go-env v0.3.0
github.com/go-chi/chi/v5 v5.0.0
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/golang-migrate/migrate/v4 v4.14.1
github.com/google/uuid v1.2.0
Expand All @@ -13,6 +14,7 @@ require (
github.com/joho/godotenv v1.3.0
github.com/jpillora/backoff v1.0.0
github.com/lib/pq v1.9.0
github.com/steinfletcher/apitest v1.5.2
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.3.0
go.uber.org/zap v1.16.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
github.com/go-chi/chi/v5 v5.0.0 h1:DBPx88FjZJH3FsICfDAfIfnb7XxKIYVGG6lOPlhENAg=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Expand Down Expand Up @@ -291,6 +293,8 @@ github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/snowflakedb/glog v0.0.0-20180824191149-f5055e6f21ce/go.mod h1:EB/w24pR5VKI60ecFnKqXzxX3dOorz1rnVicQTQrGM0=
github.com/snowflakedb/gosnowflake v1.3.5/go.mod h1:13Ky+lxzIm3VqNDZJdyvu9MCGy+WgRdYFdXp96UcLZU=
github.com/steinfletcher/apitest v1.5.2 h1:o5R0km8ZI6xooSDwsHdDCD9OpEXda7CJeQwyoSrJmPM=
github.com/steinfletcher/apitest v1.5.2/go.mod h1:TrZemFOZ1yNgKoAeAsth3Z3vEavTloE1hP/U2PSd3w0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
Expand Down
38 changes: 38 additions & 0 deletions http/handler/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package handler

import "net/http"

var errorResponses = map[string]errorResponse{
"internal_server_error": {
Code: 1,
Message: "internal server error",
StatusCode: http.StatusInternalServerError,
},
"invalid_id": {
Code: 2,
Message: "invalid id",
StatusCode: http.StatusNotFound,
},
"malformed_request_body": {
Code: 3,
Message: "malformed request body",
StatusCode: http.StatusBadRequest,
},
"request_validation_failed": {
Code: 4,
Message: "request validation failed",
StatusCode: http.StatusBadRequest,
},
"webhook_not_found": {
Code: 5,
Message: "webhook not found",
StatusCode: http.StatusNotFound,
},
}

type errorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
StatusCode int `json:"-"`
}
108 changes: 108 additions & 0 deletions http/handler/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package handler

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"

"github.com/allisson/postmand"
validation "github.com/go-ozzo/ozzo-validation/v4"
"go.uber.org/zap"
)

func makeResponse(w http.ResponseWriter, body []byte, statusCode int, contentType string, logger *zap.Logger) {
w.Header().Set("Content-Type", fmt.Sprintf("%s; charset=utf-8", contentType))
w.WriteHeader(statusCode)
_, err := w.Write(body)
if err != nil {
logger.Error("http-failed-to-write-response-body", zap.Error(err))
}
}

func makeJSONResponse(w http.ResponseWriter, statusCode int, body interface{}, logger *zap.Logger) {
d, err := json.Marshal(body)
if err != nil {
logger.Error("http-failed-to-marshal-body", zap.Error(err))
}
c := new(bytes.Buffer)
err = json.Compact(c, d)
if err != nil {
logger.Error("http-failed-to-compact-json", zap.Error(err))
}
makeResponse(w, c.Bytes(), statusCode, "application/json", logger)
}

func makeErrorResponse(w http.ResponseWriter, er *errorResponse, logger *zap.Logger) {
makeJSONResponse(w, er.StatusCode, er, logger)
}

func readBodyJSON(r *http.Request, into interface{}, logger *zap.Logger) *errorResponse {
requestBody, err := io.ReadAll(r.Body)
if err != nil {
logger.Error("read-request-body-error", zap.Error(err))
er := errorResponses["internal_server_error"]
return &er

}

if err := json.Unmarshal(requestBody, into); err != nil {
logger.Error("request-json-unmarshal-error", zap.Error(err))
er := errorResponses["malformed_request_body"]
return &er
}

if val, ok := into.(validation.Validatable); ok {
if err := val.Validate(); err != nil {
if e, ok := err.(validation.InternalError); ok {
logger.Error("read-request-validate-error", zap.Error(e))
er := errorResponses["internal_server_error"]
return &er
}
er := errorResponses["request_validation_failed"]
er.Details = err.Error()
return &er
}
}

return nil
}

func makeListOptions(r *http.Request, filters []string) (postmand.RepositoryListOptions, error) {
listOptions := postmand.RepositoryListOptions{}

if err := r.ParseForm(); err != nil {
return listOptions, err
}

// Parse limit and offset
limit := 50
offset := 0
if r.Form.Get("limit") != "" {
v, err := strconv.Atoi(r.Form.Get("limit"))
if err == nil && v <= limit {
limit = v
}
}
if r.Form.Get("offset") != "" {
v, err := strconv.Atoi(r.Form.Get("offset"))
if err == nil {
offset = v
}
}
listOptions.Limit = limit
listOptions.Offset = offset

// Parse filters
f := make(map[string]interface{})
for _, filter := range filters {
if r.Form.Get(filter) != "" {
f[filter] = r.Form.Get(filter)
}
}
listOptions.Filters = f

return listOptions, nil
}
Loading

0 comments on commit 7bbec35

Please sign in to comment.