github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/internal/web/api/spend_type.go (about) 1 package api 2 3 import ( 4 "context" 5 "net/http" 6 7 "github.com/ShoshinNikita/budget-manager/internal/db" 8 "github.com/ShoshinNikita/budget-manager/internal/logger" 9 "github.com/ShoshinNikita/budget-manager/internal/pkg/errors" 10 "github.com/ShoshinNikita/budget-manager/internal/pkg/reqid" 11 "github.com/ShoshinNikita/budget-manager/internal/web/api/models" 12 "github.com/ShoshinNikita/budget-manager/internal/web/utils" 13 ) 14 15 type SpendTypesHandlers struct { 16 db SpendTypesDB 17 log logger.Logger 18 } 19 20 type SpendTypesDB interface { 21 GetSpendTypes(ctx context.Context) ([]db.SpendType, error) 22 AddSpendType(ctx context.Context, args db.AddSpendTypeArgs) (id uint, err error) 23 EditSpendType(ctx context.Context, args db.EditSpendTypeArgs) error 24 RemoveSpendType(ctx context.Context, id uint) error 25 } 26 27 // @Summary Get All Spend Types 28 // @Tags Spend Types 29 // @Router /api/spend-types [get] 30 // @Produce json 31 // @Success 200 {object} models.GetSpendTypesResp 32 // @Failure 500 {object} models.Response "Internal error" 33 // 34 func (h SpendTypesHandlers) GetSpendTypes(w http.ResponseWriter, r *http.Request) { 35 ctx := r.Context() 36 log := reqid.FromContextToLogger(ctx, h.log) 37 38 // Process 39 types, err := h.db.GetSpendTypes(ctx) 40 if err != nil { 41 utils.EncodeInternalError(ctx, w, log, "couldn't get Spend Types", err) 42 return 43 } 44 45 resp := &models.GetSpendTypesResp{ 46 SpendTypes: types, 47 } 48 utils.Encode(ctx, w, log, utils.EncodeResponse(resp)) 49 } 50 51 // @Summary Create Spend Type 52 // @Tags Spend Types 53 // @Router /api/spend-types [post] 54 // @Accept json 55 // @Param body body models.AddSpendTypeReq true "New Spend Type" 56 // @Produce json 57 // @Success 201 {object} models.AddSpendTypeResp 58 // @Failure 400 {object} models.Response "Invalid request" 59 // @Failure 500 {object} models.Response "Internal error" 60 // 61 func (h SpendTypesHandlers) AddSpendType(w http.ResponseWriter, r *http.Request) { 62 ctx := r.Context() 63 log := reqid.FromContextToLogger(ctx, h.log) 64 65 // Decode 66 req := &models.AddSpendTypeReq{} 67 if ok := utils.DecodeRequest(w, r, log, req); !ok { 68 return 69 } 70 log = log.WithRequest(req) 71 72 // Process 73 args := db.AddSpendTypeArgs{ 74 Name: req.Name, 75 ParentID: req.ParentID, 76 } 77 id, err := h.db.AddSpendType(ctx, args) 78 if err != nil { 79 utils.EncodeInternalError(ctx, w, log, "couldn't add Spend Type", err) 80 return 81 } 82 log = log.WithField("id", id) 83 log.Debug("Spend Type was successfully added") 84 85 resp := &models.AddSpendTypeResp{ 86 ID: id, 87 } 88 utils.Encode(ctx, w, log, utils.EncodeResponse(resp), utils.EncodeStatusCode(http.StatusCreated)) 89 } 90 91 // @Summary Edit Spend Type 92 // @Tags Spend Types 93 // @Router /api/spend-types [put] 94 // @Accept json 95 // @Param body body models.EditSpendTypeReq true "Updated Spend Type" 96 // @Produce json 97 // @Success 200 {object} models.Response 98 // @Failure 400 {object} models.Response "Invalid request" 99 // @Failure 404 {object} models.Response "Spend Type doesn't exist" 100 // @Failure 500 {object} models.Response "Internal error" 101 // 102 func (h SpendTypesHandlers) EditSpendType(w http.ResponseWriter, r *http.Request) { 103 ctx := r.Context() 104 log := reqid.FromContextToLogger(ctx, h.log) 105 106 // Decode 107 req := &models.EditSpendTypeReq{} 108 if ok := utils.DecodeRequest(w, r, log, req); !ok { 109 return 110 } 111 log = log.WithRequest(req) 112 113 if req.ParentID != nil && *req.ParentID != 0 { 114 allSpendTypes, err := h.db.GetSpendTypes(ctx) 115 if err != nil { 116 utils.EncodeInternalError(ctx, w, log, "couldn't get all Spend Types to check for a cycle", err) 117 return 118 } 119 120 hasCycle, err := checkSpendTypeForCycle(allSpendTypes, req.ID, *req.ParentID) 121 if hasCycle { 122 err = errors.New("Spend Type with new parent type will have a cycle") 123 } else if err != nil { 124 err = errors.Wrap(err, "check for a cycle failed") 125 } 126 if err != nil { 127 utils.EncodeError(ctx, w, log, err, http.StatusBadRequest) 128 return 129 } 130 } 131 132 // Process 133 args := db.EditSpendTypeArgs{ 134 ID: req.ID, 135 Name: req.Name, 136 ParentID: req.ParentID, 137 } 138 err := h.db.EditSpendType(ctx, args) 139 if err != nil { 140 switch { 141 case errors.Is(err, db.ErrSpendTypeNotExist): 142 utils.EncodeError(ctx, w, log, err, http.StatusNotFound) 143 default: 144 utils.EncodeInternalError(ctx, w, log, "couldn't edit Spend Type", err) 145 } 146 return 147 } 148 log.Debug("Spend Type was successfully edited") 149 150 utils.Encode(ctx, w, log) 151 } 152 153 func checkSpendTypeForCycle(spendTypesSlice []db.SpendType, originalID, newParentID uint) (hasCycle bool, _ error) { 154 spendTypes := make(map[uint]db.SpendType, len(spendTypesSlice)) 155 for _, t := range spendTypesSlice { 156 spendTypes[t.ID] = t 157 } 158 159 parentType := spendTypes[newParentID] 160 // Max depth is 15 161 for i := 0; i < 15; i++ { 162 if parentType.ID == 0 { 163 // Unexpected error 164 return false, errors.New("invalid Spend Type") 165 } 166 if parentType.ID == originalID { 167 // Has cycle 168 return true, nil 169 } 170 if parentType.ParentID == 0 { 171 // No more parents 172 return false, nil 173 } 174 175 parentType = spendTypes[parentType.ParentID] 176 } 177 178 return false, errors.New("Spend Type has too many parents or already has a cycle") 179 } 180 181 // @Summary Remove Spend Type 182 // @Tags Spend Types 183 // @Router /api/spend-types [delete] 184 // @Accept json 185 // @Param body body models.RemoveSpendTypeReq true "Spend Type id" 186 // @Produce json 187 // @Success 200 {object} models.Response 188 // @Failure 400 {object} models.Response "Invalid request" 189 // @Failure 404 {object} models.Response "Spend Type doesn't exist" 190 // @Failure 500 {object} models.Response "Internal error" 191 // 192 func (h SpendTypesHandlers) RemoveSpendType(w http.ResponseWriter, r *http.Request) { 193 ctx := r.Context() 194 log := reqid.FromContextToLogger(ctx, h.log) 195 196 // Decode 197 req := &models.RemoveSpendTypeReq{} 198 if ok := utils.DecodeRequest(w, r, log, req); !ok { 199 return 200 } 201 log = log.WithRequest(req) 202 203 // Process 204 err := h.db.RemoveSpendType(ctx, req.ID) 205 if err != nil { 206 switch { 207 case errors.Is(err, db.ErrSpendTypeNotExist): 208 utils.EncodeError(ctx, w, log, err, http.StatusNotFound) 209 case errors.Is(err, db.ErrSpendTypeIsUsed): 210 utils.EncodeError(ctx, w, log, err, http.StatusBadRequest) 211 default: 212 utils.EncodeInternalError(ctx, w, log, "couldn't remove Spend Type", err) 213 } 214 return 215 } 216 log.Debug("Spend Type was successfully removed") 217 218 utils.Encode(ctx, w, log) 219 }