github.com/scottcagno/storage@v1.8.0/pkg/web/api/api.go (about) 1 package api 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "regexp" 9 "strconv" 10 "strings" 11 ) 12 13 type InMemoryResource struct { 14 id int `json:"id"` 15 data interface{} `json:"data"` 16 } 17 18 func (imr *InMemoryResource) ID() int { 19 return imr.id 20 } 21 22 type InMemoryResourceManager struct { 23 data map[int]*InMemoryResource 24 } 25 26 func NewInMemoryResourceManager() *InMemoryResourceManager { 27 return &InMemoryResourceManager{ 28 data: make(map[int]*InMemoryResource), 29 } 30 } 31 32 func (mrm *InMemoryResourceManager) NewResource() Resource { 33 return new(InMemoryResource) 34 } 35 36 func (mrm *InMemoryResourceManager) Insert(data ...Resource) error { 37 // loop over one or more provided resources 38 for i := range data { 39 // get pointer to each one at a time 40 resource := data[i] 41 // check to see if it already exists 42 if _, ok := mrm.data[resource.ID()]; !ok { 43 // insert int into the table if it does not exist 44 mrm.data[resource.ID()] = resource.(*InMemoryResource) 45 } 46 } 47 return nil 48 } 49 50 func (mrm *InMemoryResourceManager) Update(data ...Resource) error { 51 // loop over one or more provided resources 52 for i := range data { 53 // get pointer to each one at a time 54 resource := data[i] 55 // just write the resource to the table, overwriting any old ones 56 mrm.data[resource.ID()] = resource.(*InMemoryResource) 57 } 58 return nil 59 } 60 61 func (mrm *InMemoryResourceManager) Delete(ids ...int) error { 62 // check nil list 63 if ids == nil { 64 // remove all 65 for id, _ := range mrm.data { 66 delete(mrm.data, id) 67 } 68 return nil 69 } 70 // loop over one or more provided resources 71 for _, id := range ids { 72 // check to see if it exists 73 if _, ok := mrm.data[id]; ok { 74 // if it does, delete that data resource entry 75 delete(mrm.data, id) 76 } 77 } 78 return nil 79 } 80 81 func (mrm *InMemoryResourceManager) Search(ids ...int) ([]Resource, error) { 82 // make resource list to return 83 var resources []Resource 84 // check nil list 85 if ids == nil { 86 // add all to list 87 for _, resource := range mrm.data { 88 resources = append(resources, resource) 89 } 90 return resources, nil 91 } 92 // loop over one or more provided resources 93 for _, id := range ids { 94 // check to see if it exists 95 if resource, ok := mrm.data[id]; ok { 96 // if it does, add it to the list 97 resources = append(resources, resource) 98 } 99 } 100 return resources, nil 101 } 102 103 type Resource interface { 104 ID() int 105 // Name() string 106 //MarshalBinary() (data []byte, err error) 107 //UnmarshalBinary(data []byte) error 108 } 109 110 // ResourceManager is an interface to provide a generic 111 // manager of data resources. It is meant to be implemented 112 // by the user using this package. 113 type ResourceManager interface { 114 115 // NewResource method should return a new empty data resource. 116 // It should not add or modify the underlying storage engine 117 // in any way. 118 NewResource() Resource 119 120 // Insert method should take the data provided, add it to 121 // the storage engine and return a nil error on success. 122 Insert(data ...Resource) error 123 124 // Update method should take the data provided, find the 125 // matching data resource entries, and update them in the 126 // underlying storage engine. It is expected to return a 127 // nil error on success. 128 Update(data ...Resource) error 129 130 // Delete method should remove one or all of the data resource 131 // entries from the underlying storage engine and return a nil 132 // error on success. 133 Delete(ids ...int) error 134 135 // Search method should return one or all of the data resource 136 // entries from the underlying storage engine. It is also expected 137 // to return a nil error on success. 138 Search(ids ...int) ([]Resource, error) 139 } 140 141 const byIdRegx = `\/*([A-z\-\_]*)\/*%s\/*[0-9]+` 142 const byReRegx = `^\/*([A-z\-\_]*)$` 143 144 type API struct { 145 resourceName string 146 manager ResourceManager 147 byID *regexp.Regexp 148 byRe *regexp.Regexp 149 } 150 151 func NewAPI(resourceName string, manager ResourceManager) *API { 152 return &API{ 153 resourceName: resourceName, 154 manager: manager, 155 byID: regexp.MustCompile(fmt.Sprintf(byIdRegx, resourceName)), 156 byRe: regexp.MustCompile(byReRegx), 157 } 158 } 159 160 func (api *API) ServeHTTP(w http.ResponseWriter, r *http.Request) { 161 switch r.Method { 162 case http.MethodGet: 163 if api.byID.MatchString(r.URL.Path) { 164 api.returnByID(w, r) 165 } 166 if api.byRe.MatchString(r.URL.Path) { 167 api.returnAll(w, r) 168 } 169 http.NotFoundHandler() 170 case http.MethodPost: 171 api.insert(w, r) 172 case http.MethodPut: 173 api.updateByID(w, r) 174 case http.MethodDelete: 175 if api.byID.MatchString(r.URL.Path) { 176 api.deleteByID(w, r) 177 } 178 if api.byRe.MatchString(r.URL.Path) { 179 api.deleteAll(w, r) 180 } 181 case http.MethodOptions: 182 api.info(w, r) 183 default: 184 api.notFound(w, r) 185 } 186 } 187 188 func (api *API) info(w http.ResponseWriter, r *http.Request) { 189 fmt.Fprintf(w, "%s", api.manager) 190 return 191 } 192 193 func (api *API) notFound(w http.ResponseWriter, r *http.Request) { 194 Response(w, http.StatusNotFound) 195 return 196 } 197 198 // insert example: POST -> example.com/{resource} 199 func (api *API) insert(w http.ResponseWriter, r *http.Request) { 200 // read the data that has been posted to this endpoint 201 body, err := io.ReadAll(r.Body) 202 if err != nil { 203 Response(w, http.StatusBadRequest) 204 return 205 } 206 // create a new data resource instance 207 res := api.manager.NewResource() 208 // fill the empty data resource out 209 err = json.Unmarshal(body, res) 210 if err != nil { 211 Response(w, http.StatusBadRequest) 212 return 213 } 214 // add the newly filled out resource to the storage engine 215 err = api.manager.Insert(res) 216 if err != nil { 217 Response(w, http.StatusBadRequest) 218 return 219 } 220 // otherwise, we are good! 221 Response(w, http.StatusOK) 222 return 223 } 224 225 func getIDFromPath(path string) int { 226 ss := strings.Split(path, "/") 227 for _, s := range ss { 228 id, err := strconv.Atoi(s) 229 if err == nil { 230 return id 231 } 232 } 233 return -1 234 } 235 236 // returnByID example: GET -> example.com/{resource}/{id} 237 func (api *API) returnByID(w http.ResponseWriter, r *http.Request) { 238 // get id from the url 239 id := getIDFromPath(r.URL.Path) 240 if id < 0 { 241 Response(w, http.StatusBadRequest) 242 return 243 } 244 // use the manager to find the data resource by id 245 res, err := api.manager.Search(id) 246 if err != nil || len(res) != 1 { 247 Response(w, http.StatusNotFound) 248 return 249 } 250 // respond with the data 251 ResponseWithData(w, http.StatusOK, res) 252 return 253 } 254 255 // returnAll example: GET -> example.com/{resource} 256 func (api *API) returnAll(w http.ResponseWriter, r *http.Request) { 257 // use the manager to find and return all the data resources 258 res, err := api.manager.Search(nil...) 259 if err != nil { 260 Response(w, http.StatusNotFound) 261 return 262 } 263 // respond with data 264 ResponseWithData(w, http.StatusOK, res) 265 return 266 } 267 268 // deleteOne example: DELETE -> example.com/{resource}/{id} 269 func (api *API) deleteByID(w http.ResponseWriter, r *http.Request) { 270 // get id from the url 271 id := getIDFromPath(r.URL.Path) 272 if id < 0 { 273 Response(w, http.StatusBadRequest) 274 return 275 } 276 // use the manager to delete the specified data resouce 277 err := api.manager.Delete(id) 278 if err != nil { 279 Response(w, http.StatusNotFound) 280 return 281 } 282 // respond with data 283 Response(w, http.StatusOK) 284 return 285 } 286 287 // deleteAll example: DELETE -> example.com/{resource} 288 func (api *API) deleteAll(w http.ResponseWriter, r *http.Request) { 289 // attempt to delete all the data resources 290 err := api.manager.Delete(nil...) 291 if err != nil { 292 Response(w, http.StatusInternalServerError) 293 return 294 } 295 // otherwise, we are good! 296 Response(w, http.StatusOK) 297 } 298 299 // updateOne example: PUT -> example.com/{resource}/{id} 300 func (api *API) updateByID(w http.ResponseWriter, r *http.Request) { 301 // get id from the url 302 id := getIDFromPath(r.URL.Path) 303 if id < 0 { 304 Response(w, http.StatusBadRequest) 305 return 306 } 307 // read the data that has been posted to this endpoint 308 body, err := io.ReadAll(r.Body) 309 if err != nil { 310 Response(w, http.StatusBadRequest) 311 return 312 } 313 // create a new data resource instance 314 res := api.manager.NewResource() 315 // fill the empty data resource out 316 err = json.Unmarshal(body, res) 317 if err != nil { 318 Response(w, http.StatusBadRequest) 319 return 320 } 321 // update the specified resource 322 err = api.manager.Update(res) 323 if err != nil { 324 Response(w, http.StatusInternalServerError) 325 return 326 } 327 // otherwise, we are good 328 ResponseWithData(w, http.StatusOK, res) 329 return 330 } 331 332 func Response(w http.ResponseWriter, code int) { 333 w.Header().Set("Content-Type", "application/json; charset=utf-8") 334 w.Header().Set("X-Content-Type-Options", "nosniff") 335 w.WriteHeader(code) 336 data, err := json.Marshal(struct { 337 Code int `json:"code"` 338 Message string `json:"message"` 339 }{ 340 Code: code, 341 Message: http.StatusText(code), 342 }) 343 if err != nil { 344 code := http.StatusInternalServerError 345 http.Error(w, http.StatusText(code), code) 346 return 347 } 348 fmt.Fprintln(w, data) 349 } 350 351 func ResponseWithData(w http.ResponseWriter, code int, data interface{}) { 352 w.Header().Set("Content-Type", "application/json; charset=utf-8") 353 w.Header().Set("X-Content-Type-Options", "nosniff") 354 w.WriteHeader(code) 355 data, err := json.Marshal(struct { 356 Code int `json:"code"` 357 Message string `json:"message"` 358 Data interface{} `json:"data"` 359 }{ 360 Code: code, 361 Message: http.StatusText(code), 362 Data: data, 363 }) 364 if err != nil { 365 code := http.StatusInternalServerError 366 http.Error(w, http.StatusText(code), code) 367 return 368 } 369 fmt.Fprintln(w, data) 370 }