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  }