Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add WebhookService #11

Merged
merged 1 commit into from Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions cmd/postmand/main.go
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
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
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
@@ -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
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
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
@@ -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
@@ -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
}