Skip to content

Commit e91e2f1

Browse files
authored
feat: add API documentation for all endpoints (#124)
1 parent dcd973a commit e91e2f1

27 files changed

+6866
-1056
lines changed

docs/docs.go

Lines changed: 2118 additions & 131 deletions
Large diffs are not rendered by default.

docs/swagger.json

Lines changed: 2118 additions & 131 deletions
Large diffs are not rendered by default.

docs/swagger.yaml

Lines changed: 1451 additions & 109 deletions
Large diffs are not rendered by default.

internal/controllers/account.go

Lines changed: 161 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,25 @@ package controllers
33
import (
44
"fmt"
55
"net/http"
6-
"strconv"
76

87
"github.com/envelope-zero/backend/internal/httputil"
98
"github.com/envelope-zero/backend/internal/models"
109
"github.com/gin-gonic/gin"
1110
)
1211

12+
type AccountListResponse struct {
13+
Data []models.Account `json:"data"`
14+
}
15+
16+
type AccountResponse struct {
17+
Data models.Account `json:"data"`
18+
Links AccountLinks `json:"links"`
19+
}
20+
21+
type AccountLinks struct {
22+
Transactions string `json:"transactions" example:"https://example.com/api/v1/budgets/3/accounts/17/transactions"`
23+
}
24+
1325
// RegisterAccountRoutes registers the routes for accounts with
1426
// the RouterGroup that is passed.
1527
func RegisterAccountRoutes(r *gin.RouterGroup) {
@@ -39,8 +51,10 @@ func RegisterAccountRoutes(r *gin.RouterGroup) {
3951
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
4052
// @Tags Accounts
4153
// @Success 204
42-
// @Param budgetId path uint64 true "ID of the budget"
43-
// @Param accountId path uint64 true "ID of the account"
54+
// @Failure 400 {object} httputil.HTTPError
55+
// @Failure 404
56+
// @Param budgetId path uint64 true "ID of the budget"
57+
// @Param accountId path uint64 true "ID of the account"
4458
// @Router /v1/budgets/{budgetId}/accounts/{accountId}/transactions [options]
4559
func OptionsAccountTransactions(c *gin.Context) {
4660
httputil.OptionsGet(c)
@@ -50,6 +64,8 @@ func OptionsAccountTransactions(c *gin.Context) {
5064
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
5165
// @Tags Accounts
5266
// @Success 204
67+
// @Failure 400 {object} httputil.HTTPError
68+
// @Failure 404
5369
// @Param budgetId path uint64 true "ID of the budget"
5470
// @Router /v1/budgets/{budgetId}/accounts [options]
5571
func OptionsAccountList(c *gin.Context) {
@@ -60,118 +76,211 @@ func OptionsAccountList(c *gin.Context) {
6076
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
6177
// @Tags Accounts
6278
// @Success 204
63-
// @Param budgetId path uint64 true "ID of the budget"
64-
// @Param accountId path uint64 true "ID of the account"
79+
// @Failure 400 {object} httputil.HTTPError
80+
// @Failure 404
81+
// @Param budgetId path uint64 true "ID of the budget"
82+
// @Param accountId path uint64 true "ID of the account"
6583
// @Router /v1/budgets/{budgetId}/accounts/{accountId} [options]
6684
func OptionsAccountDetail(c *gin.Context) {
6785
httputil.OptionsGetPatchDelete(c)
6886
}
6987

70-
// GetAccountTransactions returns all transactions for the account.
88+
// @Summary List all transactions for an account
89+
// @Description Returns a list of all transactions for the account requested
90+
// @Tags Accounts
91+
// @Produce json
92+
// @Success 200 {object} TransactionListResponse
93+
// @Failure 400 {object} httputil.HTTPError
94+
// @Failure 404
95+
// @Failure 500 {object} httputil.HTTPError
96+
// @Param budgetId path uint64 true "ID of the budget"
97+
// @Param accountId path uint64 true "ID of the account"
98+
// @Router /v1/budgets/{budgetId}/accounts/{accountId}/transactions [get]
7199
func GetAccountTransactions(c *gin.Context) {
72-
var account models.Account
73-
err := models.DB.First(&account, c.Param("accountId")).Error
100+
account, err := getAccountResource(c)
74101
if err != nil {
75-
httputil.FetchErrorHandler(c, err)
76102
return
77103
}
78104

79-
c.JSON(http.StatusOK, gin.H{"data": account.Transactions()})
105+
c.JSON(http.StatusOK, TransactionListResponse{
106+
Data: account.Transactions(),
107+
})
80108
}
81109

82-
// CreateAccount creates a new account.
110+
// @Summary Create account
111+
// @Description Create a new account for a specific budget
112+
// @Tags Accounts
113+
// @Produce json
114+
// @Success 201 {object} AccountResponse
115+
// @Failure 400 {object} httputil.HTTPError
116+
// @Failure 404
117+
// @Failure 500 {object} httputil.HTTPError
118+
// @Param budgetId path uint64 true "ID of the budget"
119+
// @Param account body models.AccountCreate true "Account"
120+
// @Router /v1/budgets/{budgetId}/accounts [post]
83121
func CreateAccount(c *gin.Context) {
84122
var data models.Account
85123

86-
if status, err := bindData(c, &data); err != nil {
87-
c.JSON(status, gin.H{"error": err.Error()})
124+
if status, err := httputil.BindData(c, &data); err != nil {
125+
httputil.NewError(c, status, err)
126+
return
127+
}
128+
129+
budget, err := getBudgetResource(c)
130+
if err != nil {
88131
return
89132
}
90133

91-
data.BudgetID, _ = strconv.ParseUint(c.Param("budgetId"), 10, 0)
134+
data.BudgetID = budget.ID
92135
models.DB.Create(&data)
93136

94-
c.JSON(http.StatusCreated, gin.H{"data": data})
137+
c.JSON(http.StatusCreated, AccountResponse{Data: data})
95138
}
96139

97-
// GetAccounts retrieves all accounts.
140+
// @Summary List accounts
141+
// @Description Returns a list of all accounts for the budget
142+
// @Tags Accounts
143+
// @Produce json
144+
// @Success 200 {object} AccountListResponse
145+
// @Failure 400 {object} httputil.HTTPError
146+
// @Failure 404
147+
// @Failure 500 {object} httputil.HTTPError
148+
// @Param budgetId path uint64 true "ID of the budget"
149+
// @Router /v1/budgets/{budgetId}/accounts [get]
98150
func GetAccounts(c *gin.Context) {
99151
var accounts []models.Account
100152

101153
// Check if the budget exists at all
102-
budget, err := getBudget(c)
154+
budget, err := getBudgetResource(c)
103155
if err != nil {
104156
return
105157
}
106158

107159
models.DB.Where(&models.Account{
108-
BudgetID: budget.ID,
160+
AccountCreate: models.AccountCreate{
161+
BudgetID: budget.ID,
162+
},
109163
}).Find(&accounts)
110164

111165
for i, account := range accounts {
112-
response, err := account.WithCalculations()
113-
if err != nil {
114-
httputil.FetchErrorHandler(c, fmt.Errorf("could not get values for account %v: %v", account.Name, err))
115-
return
116-
}
117-
118-
accounts[i] = *response
166+
accounts[i] = account.WithCalculations()
119167
}
120168

121-
c.JSON(http.StatusOK, gin.H{"data": accounts})
169+
c.JSON(http.StatusOK, AccountListResponse{Data: accounts})
122170
}
123171

124-
// GetAccount retrieves an account by its ID.
172+
// @Summary Get account
173+
// @Description Returns a specific account
174+
// @Tags Accounts
175+
// @Produce json
176+
// @Success 200 {object} AccountResponse
177+
// @Failure 400 {object} httputil.HTTPError
178+
// @Failure 404
179+
// @Failure 500 {object} httputil.HTTPError
180+
// @Param budgetId path uint64 true "ID of the budget"
181+
// @Param accountId path uint64 true "ID of the account"
182+
// @Router /v1/budgets/{budgetId}/accounts/{accountId} [get]
125183
func GetAccount(c *gin.Context) {
126-
account, err := getAccount(c)
127-
if err != nil {
128-
return
129-
}
130-
131-
apiResponse, err := account.WithCalculations()
184+
_, err := getAccountResource(c)
132185
if err != nil {
133-
httputil.FetchErrorHandler(c, fmt.Errorf("could not get values for account %v: %v", account.Name, err))
134186
return
135187
}
136188

137-
c.JSON(http.StatusOK, gin.H{
138-
"data": apiResponse,
139-
"links": map[string]string{
140-
"transactions": requestURL(c) + "/transactions",
141-
},
142-
})
189+
c.JSON(http.StatusOK, newAccountResponse(c))
143190
}
144191

145-
// UpdateAccount updates an account, selected by the ID parameter.
192+
// @Summary Update account
193+
// @Description Updates an account. Only values to be updated need to be specified.
194+
// @Tags Accounts
195+
// @Produce json
196+
// @Success 200 {object} AccountResponse
197+
// @Failure 400 {object} httputil.HTTPError
198+
// @Failure 404
199+
// @Failure 500 {object} httputil.HTTPError
200+
// @Param budgetId path uint64 true "ID of the budget"
201+
// @Param accountId path uint64 true "ID of the account"
202+
// @Param account body models.AccountCreate true "Account"
203+
// @Router /v1/budgets/{budgetId}/accounts/{accountId} [patch]
146204
func UpdateAccount(c *gin.Context) {
147-
var account models.Account
148-
149-
err := models.DB.First(&account, c.Param("accountId")).Error
205+
account, err := getAccountResource(c)
150206
if err != nil {
151-
httputil.FetchErrorHandler(c, err)
152207
return
153208
}
154209

155210
var data models.Account
156-
if status, err := bindData(c, &data); err != nil {
157-
c.JSON(status, gin.H{"error": err.Error()})
211+
if status, err := httputil.BindData(c, &data); err != nil {
212+
httputil.NewError(c, status, err)
158213
return
159214
}
160215

161216
models.DB.Model(&account).Updates(data)
162-
c.JSON(http.StatusOK, gin.H{"data": account})
217+
c.JSON(http.StatusOK, newAccountResponse(c))
163218
}
164219

165-
// DeleteAccount removes a account, identified by its ID.
220+
// @Summary Delete account
221+
// @Description Deletes the specified account.
222+
// @Tags Accounts
223+
// @Produce json
224+
// @Success 204
225+
// @Failure 400 {object} httputil.HTTPError
226+
// @Failure 404
227+
// @Failure 500 {object} httputil.HTTPError
228+
// @Param budgetId path uint64 true "ID of the budget"
229+
// @Param accountId path uint64 true "ID of the account"
230+
// @Router /v1/budgets/{budgetId}/accounts/{accountId} [delete]
166231
func DeleteAccount(c *gin.Context) {
167-
var account models.Account
168-
err := models.DB.First(&account, c.Param("accountId")).Error
232+
account, err := getAccountResource(c)
169233
if err != nil {
170-
httputil.FetchErrorHandler(c, err)
171234
return
172235
}
173236

174237
models.DB.Delete(&account)
175238

176239
c.JSON(http.StatusNoContent, gin.H{})
177240
}
241+
242+
// getAccountResource verifies that the request URI is valid for the account and returns it.
243+
func getAccountResource(c *gin.Context) (models.Account, error) {
244+
var account models.Account
245+
246+
budget, err := getBudgetResource(c)
247+
if err != nil {
248+
return models.Account{}, err
249+
}
250+
251+
accountID, err := httputil.ParseID(c, "accountId")
252+
if err != nil {
253+
return models.Account{}, err
254+
}
255+
256+
err = models.DB.First(&account, &models.Account{
257+
AccountCreate: models.AccountCreate{
258+
BudgetID: budget.ID,
259+
},
260+
Model: models.Model{
261+
ID: accountID,
262+
},
263+
}).Error
264+
if err != nil {
265+
httputil.FetchErrorHandler(c, err)
266+
return models.Account{}, err
267+
}
268+
269+
return account, nil
270+
}
271+
272+
// newAccountResponse creates a response object for an account.
273+
func newAccountResponse(c *gin.Context) AccountResponse {
274+
// When this function is called, all parent resources have already been validated
275+
budget, _ := getBudgetResource(c)
276+
account, _ := getAccountResource(c)
277+
278+
url := httputil.RequestPathV1(c) + fmt.Sprintf("/budgets/%d/accounts/%d", budget.ID, account.ID)
279+
280+
return AccountResponse{
281+
Data: account.WithCalculations(),
282+
Links: AccountLinks{
283+
Transactions: url + "/transactions",
284+
},
285+
}
286+
}

0 commit comments

Comments
 (0)