-
Notifications
You must be signed in to change notification settings - Fork 0
/
controllers.go
168 lines (158 loc) · 5.83 KB
/
controllers.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package rest
import (
"encoding/json"
"fmt"
"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
"io"
"net/http"
"strconv"
)
type ControllerParams struct {
Logger Log
CommandDispatcher CommandDispatcher
ResourceRepository *ResourceRepository
DefaultProjection *Projection
Projections map[string]Projection
Schema *openapi3.T
PathMap map[string]*openapi3.PathItem
Operation map[string]*openapi3.Operation
Echo *echo.Echo
APIConfig *APIConfig
}
// DefaultWriteController handles the write operations (create, update, delete)
func DefaultWriteController(p *ControllerParams) echo.HandlerFunc {
var commandName string
var resourceType string
for method, toperation := range p.Operation {
if toperation.RequestBody == nil || toperation.RequestBody.Value == nil {
continue
}
//get the schema for the operation
for _, requestContent := range toperation.RequestBody.Value.Content {
if requestContent.Schema != nil {
//use the first schema ref to determine the entity type
if requestContent.Schema.Ref != "" {
//get the entity type from the ref
resourceType = requestContent.Schema.Ref
}
}
}
//If there is a x-command extension then dispatch that command by default
var ok bool
if commandName, ok = toperation.Extensions["x-command"].(string); ok {
p.Logger.Debugf("command configured: %s", commandName)
}
//If there is a x-command-name extension then dispatch that command by default otherwise use the default command based on the operation type
if commandName == "" {
switch method {
case http.MethodPost:
commandName = UPDATE_COMMAND
case http.MethodPut:
commandName = UPDATE_COMMAND
case http.MethodPatch:
commandName = UPDATE_COMMAND
case http.MethodDelete:
commandName = DELETE_COMMAND
}
}
}
return func(ctxt echo.Context) error {
var sequenceNo string
var seq int64
//getting etag from context
etag := ctxt.Request().Header.Get("If-Match")
if etag != "" {
_, sequenceNo = SplitEtag(etag)
tseq, err := strconv.Atoi(sequenceNo)
if err != nil {
return NewControllerError("unexpected error updating content type. invalid sequence number", err, http.StatusBadRequest)
}
seq = int64(tseq)
}
body, err := io.ReadAll(ctxt.Request().Body)
if err != nil {
ctxt.Logger().Debugf("unexpected error reading request body: %s", err)
return NewControllerError("unexpected error reading request body", err, http.StatusBadRequest)
}
contentType := ctxt.Request().Header.Get(echo.HeaderContentType)
//for certain content types treat with it differently
switch contentType {
case "application/ld+json":
resource, err := p.ResourceRepository.Initialize(ctxt.Request().Context(), p.Logger, body)
if err != nil {
ctxt.Logger().Errorf("unexpected error creating entity: %s", err)
return NewControllerError("unexpected error creating entity", err, http.StatusBadRequest)
}
//if the sequence number is not one more than the current sequence number then return an error
if seq != 0 && resource.GetSequenceNo() != seq+1 {
return NewControllerError("unexpected error updating content type. invalid sequence number", err, http.StatusPreconditionFailed)
}
errs := p.ResourceRepository.Persist(ctxt.Request().Context(), p.Logger, []Resource{resource})
if len(errs) > 0 {
ctxt.Logger().Errorf("unexpected error persisting entity: %s", errs)
return NewControllerError("unexpected error persisting entity", errs[0], http.StatusBadRequest)
}
//set etag in response header
ctxt.Response().Header().Set("ETag", fmt.Sprintf("%s.%d", resource.GetID(), resource.GetSequenceNo()))
if resource.GetSequenceNo() == 1 {
return ctxt.JSON(http.StatusCreated, resource)
} else {
return ctxt.JSON(http.StatusOK, resource)
}
default:
//At the time of this writing only application/ld+json resources can be written. Everything else is
var defaultProjection Projection
if projection, ok := p.Projections[resourceType]; ok {
defaultProjection = projection
}
response, err := p.CommandDispatcher.Dispatch(ctxt.Request().Context(), &Command{
Type: commandName,
}, ctxt.Logger(), &CommandOptions{
ResourceRepository: p.ResourceRepository,
DefaultProjection: defaultProjection,
})
if response.Code != 0 {
return ctxt.JSON(response.Code, response.Body)
} else {
if err != nil {
return ctxt.NoContent(http.StatusInternalServerError)
} else {
return ctxt.NoContent(http.StatusOK)
}
}
}
}
}
// DefaultReadController handles the read operations viewing a specific item
func DefaultReadController(p *ControllerParams) echo.HandlerFunc {
return func(ctxt echo.Context) error {
contentType := ctxt.Request().Header.Get(echo.HeaderContentType)
//for certain content types treat with it differently
switch contentType {
case "application/ld+json":
resource, err := p.ResourceRepository.Initialize(ctxt.Request().Context(), p.Logger, []byte("{}"))
if err != nil {
return NewControllerError("unexpected error creating entity", err, http.StatusBadRequest)
}
var payload []byte
//if the sequence no is one that means it's a new resource and the resource doesn't exist
if resource.GetSequenceNo() == 1 {
return ctxt.NoContent(http.StatusNotFound)
}
payload, err = json.Marshal(resource)
return ctxt.Blob(http.StatusOK, "application/ld+json", payload)
default:
//if there a path map then use that to get the resource
for path, _ := range p.PathMap {
if path != p.APIConfig.BasePath+"/*" {
//TODO check to see if there is a type in the path and try to get the projection
//TODO check to see the params and try to get the item by that
} else {
return ctxt.NoContent(http.StatusNotFound)
}
}
}
return ctxt.NoContent(http.StatusNotFound)
}
}