Skip to content

Commit 5b4f4d8

Browse files
committed
Add endpoint to export retrospective as JSON or Markdown
1 parent 289233e commit 5b4f4d8

5 files changed

Lines changed: 137 additions & 1 deletion

File tree

internal/schedule/schedule.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ type ScheduleParams struct {
2121
fx.In
2222
Service *service.Service
2323
Config *config.Config
24+
Logger *zap.Logger
2425
Lifecycle fx.Lifecycle
2526
}
2627

2728
func New(p ScheduleParams) *Schedule {
2829
s := &Schedule{
2930
service: p.Service,
3031
config: p.Config,
32+
logger: p.Logger,
3133
stopCh: make(chan struct{}),
3234
}
3335

internal/server/server.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"database/sql"
1010
"fmt"
1111
"net/http"
12+
"strings"
1213

1314
"github.com/gin-contrib/sessions"
1415
"github.com/gin-contrib/sessions/cookie"
@@ -707,6 +708,61 @@ func (ct *Controller) voteAnswer(c *gin.Context) {
707708

708709
}
709710

711+
// exportRetrospective godoc
712+
//
713+
// @Summary Export Retrospective
714+
// @Tags Retrospective
715+
// @Accept json
716+
// @Produce json
717+
// @Produce text/markdown
718+
// @Param export body types.RetrospectiveExportRequest true "Export Retrospective"
719+
// @Success 200 {object} types.Retrospective "Retrospective Object (JSON) or Markdown file"
720+
// @Failure 400 {string} string "Invalid input"
721+
// @Failure 404 {string} string "Not Found"
722+
// @Failure 500 {string} string "Internal error"
723+
// @Router /retrospective/export [post]
724+
func (ct *Controller) exportRetrospective(c *gin.Context) {
725+
var input types.RetrospectiveExportRequest
726+
if err := c.BindJSON(&input); err != nil {
727+
ct.logger.Error("error parsing body content", zap.Error(err))
728+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body content"})
729+
return
730+
}
731+
732+
if err := input.Validate(); err != nil {
733+
ct.logger.Error("invalid input", zap.Error(err))
734+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
735+
return
736+
}
737+
738+
retro, err := ct.service.GetRetrospective(c, input.RetrospectiveID)
739+
if err == sql.ErrNoRows {
740+
ct.logger.Error("retrospective not found", zap.String("id", input.RetrospectiveID.String()))
741+
c.JSON(http.StatusNotFound, gin.H{"error": "restrospective not found"})
742+
return
743+
}
744+
745+
if err != nil {
746+
ct.logger.Error("error getting retrospective", zap.Error(err))
747+
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
748+
return
749+
}
750+
751+
filename := fmt.Sprintf("retrospective-%s", strings.ReplaceAll(retro.Name, " ", "_"))
752+
switch input.ExportType {
753+
case types.ExportTypeMarkdown:
754+
755+
markdown := ct.service.ConvertRetrospectiveToMarkdown(c, retro)
756+
c.Header("Content-Type", "text/markdown")
757+
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.md\"", filename))
758+
c.String(http.StatusOK, markdown)
759+
default:
760+
c.Header("Content-Type", "application/json")
761+
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.json\"", filename))
762+
c.JSON(http.StatusOK, retro)
763+
}
764+
}
765+
710766
// @license.name MIT
711767
// @license.url https://github.com/simple-retro/api/blob/master/LICENSE
712768
func (ct *Controller) Start() {
@@ -739,6 +795,7 @@ func (ct *Controller) Start() {
739795
api.GET("/retrospective/:id", ct.getRetrospective)
740796
api.PATCH("/retrospective/:id", ct.updateRetrospective)
741797
api.DELETE("/retrospective/:id", ct.deleteRetrospective)
798+
api.POST("/retrospective/export", ct.exportRetrospective)
742799
api.GET("/hello/:id", ct.subscribeChanges)
743800
api.GET("/limits", ct.getLimits)
744801

internal/service/convert.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package service
2+
3+
import (
4+
"api/types"
5+
"context"
6+
"fmt"
7+
"sort"
8+
"strings"
9+
)
10+
11+
func (s *Service) ConvertRetrospectiveToMarkdown(ctx context.Context, retro *types.Retrospective) string {
12+
var sb strings.Builder
13+
14+
sb.WriteString("# Simple Retro\n\n")
15+
16+
sb.WriteString("## " + retro.Name + "\n\n")
17+
18+
if retro.Description != "" {
19+
sb.WriteString(retro.Description + "\n\n")
20+
}
21+
22+
sb.WriteString("*Created on " + retro.CreatedAt.Format("January 2, 2006 at 3:04 PM") + "*\n\n")
23+
24+
sb.WriteString("---\n\n")
25+
26+
for _, question := range retro.Questions {
27+
sb.WriteString("### " + question.Text + "\n\n")
28+
29+
answers := make([]types.Answer, len(question.Answers))
30+
copy(answers, question.Answers)
31+
sort.Slice(answers, func(i, j int) bool {
32+
return answers[i].Position < answers[j].Position
33+
})
34+
35+
for _, answer := range answers {
36+
if answer.Votes > 0 {
37+
sb.WriteString(fmt.Sprintf("- %s (%d Up votes)\n", answer.Text, answer.Votes))
38+
} else {
39+
sb.WriteString("- " + answer.Text + "\n")
40+
}
41+
}
42+
sb.WriteString("\n")
43+
}
44+
45+
return sb.String()
46+
}

types/retrospective.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import (
77
)
88

99
type VoteAction string
10+
type ExportType string
1011

1112
const (
1213
VoteAdd VoteAction = "ADD_VOTE"
1314
VoteRemove VoteAction = "REMOVE_VOTE"
15+
16+
ExportTypeJSON ExportType = "JSON"
17+
ExportTypeMarkdown ExportType = "MARKDOWN"
1418
)
1519

1620
type Retrospective struct {
@@ -55,6 +59,11 @@ type AnswerVoteRequest struct {
5559
Action VoteAction `json:"action"`
5660
}
5761

62+
type RetrospectiveExportRequest struct {
63+
RetrospectiveID uuid.UUID `json:"retrospective_id"`
64+
ExportType ExportType `json:"export_type"`
65+
}
66+
5867
func (v VoteAction) String() string {
5968
return string(v)
6069
}

types/validations.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package types
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
6+
"github.com/google/uuid"
7+
)
48

59
const (
610
NAME_LIMIT = 100
@@ -106,3 +110,21 @@ func (a *AnswerVoteRequest) Validate() error {
106110
return fmt.Errorf("invalid vote action")
107111
}
108112
}
113+
114+
func (r RetrospectiveExportRequest) Validate() error {
115+
if r.RetrospectiveID.String() == "" {
116+
return fmt.Errorf("retrospective id cannot be empty")
117+
}
118+
119+
if r.RetrospectiveID == uuid.Nil {
120+
return fmt.Errorf("retrospective id cannot be nil")
121+
}
122+
123+
switch r.ExportType {
124+
case ExportTypeJSON, ExportTypeMarkdown:
125+
return nil
126+
default:
127+
return fmt.Errorf("invalid export type")
128+
}
129+
130+
}

0 commit comments

Comments
 (0)