Monorepo for Tangled tangled.org

draft: appview: service layer #800

open opened by boltless.me targeting master from sl/uvpzuszrulvq

Obviously file naming of appview/web/handler/*.go files are directly against to go convention. Though I think flattening all handler files can significantly reduce the effort involved in file naming and structuring. We are already grouping core services by domains, and doing same for web handers is just over-complicating.

Signed-off-by: Seongmin Lee git@boltless.me

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3m5jyyj76xa22
+384 -411
Interdiff #8 #9
appview/oauth/handler.go

This file has not been changed.

appview/oauth/session.go

This file has not been changed.

appview/service/issue/errors.go

This file has not been changed.

appview/service/issue/issue.go

This file has not been changed.

appview/service/issue/state.go

This file has not been changed.

appview/service/repo/errors.go

This file has not been changed.

appview/service/repo/repo.go

This file has not been changed.

appview/service/repo/repoinfo.go

This file has not been changed.

appview/session/context.go

This file has not been changed.

appview/session/session.go

This file has not been changed.

appview/state/legacy_bridge.go

This file has not been changed.

-23
appview/web/handler/oauth_client_metadata.go
··· 1 - package handler 2 - 3 - import ( 4 - "encoding/json" 5 - "net/http" 6 - 7 - "tangled.org/core/appview/oauth" 8 - ) 9 - 10 - func OauthClientMetadata(o *oauth.OAuth) http.HandlerFunc { 11 - return func(w http.ResponseWriter, r *http.Request) { 12 - doc := o.ClientApp.Config.ClientMetadata() 13 - doc.JWKSURI = &o.JwksUri 14 - doc.ClientName = &o.ClientName 15 - doc.ClientURI = &o.ClientUri 16 - 17 - w.Header().Set("Content-Type", "application/json") 18 - if err := json.NewEncoder(w).Encode(doc); err != nil { 19 - http.Error(w, err.Error(), http.StatusInternalServerError) 20 - return 21 - } 22 - } 23 - }
-19
appview/web/handler/oauth_jwks.go
··· 1 - package handler 2 - 3 - import ( 4 - "encoding/json" 5 - "net/http" 6 - 7 - "tangled.org/core/appview/oauth" 8 - ) 9 - 10 - func OauthJwks(o *oauth.OAuth) http.HandlerFunc { 11 - return func(w http.ResponseWriter, r *http.Request) { 12 - w.Header().Set("Content-Type", "application/json") 13 - body := o.ClientApp.Config.PublicJWKS() 14 - if err := json.NewEncoder(w).Encode(body); err != nil { 15 - http.Error(w, err.Error(), http.StatusInternalServerError) 16 - return 17 - } 18 - } 19 - }
+292
appview/web/handler/user_repo_issues.go
··· 1 1 package handler 2 2 3 3 import ( 4 + "errors" 5 + "fmt" 4 6 "net/http" 5 7 6 8 "tangled.org/core/api/tangled" ··· 8 10 "tangled.org/core/appview/models" 9 11 "tangled.org/core/appview/pages" 10 12 "tangled.org/core/appview/pagination" 13 + "tangled.org/core/appview/reporesolver" 11 14 isvc "tangled.org/core/appview/service/issue" 12 15 rsvc "tangled.org/core/appview/service/repo" 13 16 "tangled.org/core/appview/session" ··· 85 88 } 86 89 } 87 90 } 91 + 92 + func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc { 93 + return func(w http.ResponseWriter, r *http.Request) { 94 + ctx := r.Context() 95 + l := log.FromContext(ctx).With("handler", "Issue") 96 + issue, ok := request.IssueFromContext(ctx) 97 + if !ok { 98 + l.Error("malformed request, failed to get issue") 99 + p.Error503(w) 100 + return 101 + } 102 + repoOwnerId, ok := request.OwnerFromContext(ctx) 103 + if !ok { 104 + l.Error("malformed request") 105 + p.Error503(w) 106 + return 107 + } 108 + 109 + // render 110 + err := func() error { 111 + user := session.UserFromContext(ctx) 112 + repoinfo, err := rs.GetRepoInfo(ctx, repoOwnerId, issue.Repo, "", "", user) 113 + if err != nil { 114 + l.Error("failed to load repo", "err", err) 115 + return err 116 + } 117 + 118 + reactionMap, err := db.GetReactionMap(d, 20, issue.AtUri()) 119 + if err != nil { 120 + l.Error("failed to get issue reactions", "err", err) 121 + return err 122 + } 123 + 124 + userReactions := map[models.ReactionKind]bool{} 125 + if user != nil { 126 + userReactions = db.GetReactionStatusMap(d, user.Did, issue.AtUri()) 127 + } 128 + 129 + backlinks, err := db.GetBacklinks(d, issue.AtUri()) 130 + if err != nil { 131 + l.Error("failed to fetch backlinks", "err", err) 132 + return err 133 + } 134 + 135 + labelDefs, err := db.GetLabelDefinitions( 136 + d, 137 + orm.FilterIn("at_uri", issue.Repo.Labels), 138 + orm.FilterContains("scope", tangled.RepoIssueNSID), 139 + ) 140 + if err != nil { 141 + l.Error("failed to fetch label defs", "err", err) 142 + return err 143 + } 144 + 145 + defs := make(map[string]*models.LabelDefinition) 146 + for _, l := range labelDefs { 147 + defs[l.AtUri().String()] = &l 148 + } 149 + 150 + return p.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 151 + LoggedInUser: user, 152 + RepoInfo: *repoinfo, 153 + Issue: issue, 154 + CommentList: issue.CommentList(), 155 + Backlinks: backlinks, 156 + OrderedReactionKinds: models.OrderedReactionKinds, 157 + Reactions: reactionMap, 158 + UserReacted: userReactions, 159 + LabelDefs: defs, 160 + }) 161 + }() 162 + if err != nil { 163 + l.Error("failed to render", "err", err) 164 + p.Error503(w) 165 + return 166 + } 167 + } 168 + } 169 + 170 + func NewIssue(rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 171 + return func(w http.ResponseWriter, r *http.Request) { 172 + ctx := r.Context() 173 + l := log.FromContext(ctx).With("handler", "NewIssue") 174 + 175 + // render 176 + err := func() error { 177 + user := session.UserFromContext(ctx) 178 + repo, ok := request.RepoFromContext(ctx) 179 + if !ok { 180 + return fmt.Errorf("malformed request") 181 + } 182 + repoOwnerId, ok := request.OwnerFromContext(ctx) 183 + if !ok { 184 + return fmt.Errorf("malformed request") 185 + } 186 + repoinfo, err := rs.GetRepoInfo(ctx, repoOwnerId, repo, "", "", user) 187 + if err != nil { 188 + return err 189 + } 190 + return p.RepoNewIssue(w, pages.RepoNewIssueParams{ 191 + LoggedInUser: user, 192 + RepoInfo: *repoinfo, 193 + }) 194 + }() 195 + if err != nil { 196 + l.Error("failed to render", "err", err) 197 + p.Error503(w) 198 + return 199 + } 200 + } 201 + } 202 + 203 + func NewIssuePost(is isvc.Service, p *pages.Pages) http.HandlerFunc { 204 + noticeId := "issues" 205 + return func(w http.ResponseWriter, r *http.Request) { 206 + ctx := r.Context() 207 + l := log.FromContext(ctx).With("handler", "NewIssuePost") 208 + repo, ok := request.RepoFromContext(ctx) 209 + if !ok { 210 + l.Error("malformed request, failed to get repo") 211 + // TODO: 503 error with more detailed messages 212 + p.Error503(w) 213 + return 214 + } 215 + var ( 216 + title = r.FormValue("title") 217 + body = r.FormValue("body") 218 + ) 219 + 220 + _, err := is.NewIssue(ctx, repo, title, body) 221 + if err != nil { 222 + if errors.Is(err, isvc.ErrDatabaseFail) { 223 + p.Notice(w, noticeId, "Failed to create issue.") 224 + } else if errors.Is(err, isvc.ErrPDSFail) { 225 + p.Notice(w, noticeId, "Failed to create issue.") 226 + } else { 227 + p.Notice(w, noticeId, "Failed to create issue.") 228 + } 229 + return 230 + } 231 + p.HxLocation(w, "/") 232 + } 233 + } 234 + 235 + func IssueEdit(is isvc.Service, rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 236 + return func(w http.ResponseWriter, r *http.Request) { 237 + ctx := r.Context() 238 + l := log.FromContext(ctx).With("handler", "IssueEdit") 239 + issue, ok := request.IssueFromContext(ctx) 240 + if !ok { 241 + l.Error("malformed request, failed to get issue") 242 + p.Error503(w) 243 + return 244 + } 245 + repoOwnerId, ok := request.OwnerFromContext(ctx) 246 + if !ok { 247 + l.Error("malformed request") 248 + p.Error503(w) 249 + return 250 + } 251 + 252 + // render 253 + err := func() error { 254 + user := session.UserFromContext(ctx) 255 + repoinfo, err := rs.GetRepoInfo(ctx, repoOwnerId, issue.Repo, "", "", user) 256 + if err != nil { 257 + return err 258 + } 259 + return p.EditIssueFragment(w, pages.EditIssueParams{ 260 + LoggedInUser: user, 261 + RepoInfo: *repoinfo, 262 + 263 + Issue: issue, 264 + }) 265 + }() 266 + if err != nil { 267 + l.Error("failed to render", "err", err) 268 + p.Error503(w) 269 + return 270 + } 271 + } 272 + } 273 + 274 + func IssueEditPost(is isvc.Service, p *pages.Pages) http.HandlerFunc { 275 + noticeId := "issues" 276 + return func(w http.ResponseWriter, r *http.Request) { 277 + ctx := r.Context() 278 + l := log.FromContext(ctx).With("handler", "IssueEdit") 279 + issue, ok := request.IssueFromContext(ctx) 280 + if !ok { 281 + l.Error("malformed request, failed to get issue") 282 + p.Error503(w) 283 + return 284 + } 285 + 286 + newIssue := *issue 287 + newIssue.Title = r.FormValue("title") 288 + newIssue.Body = r.FormValue("body") 289 + 290 + err := is.EditIssue(ctx, &newIssue) 291 + if err != nil { 292 + if errors.Is(err, isvc.ErrDatabaseFail) { 293 + p.Notice(w, noticeId, "Failed to edit issue.") 294 + } else if errors.Is(err, isvc.ErrPDSFail) { 295 + p.Notice(w, noticeId, "Failed to edit issue.") 296 + } else { 297 + p.Notice(w, noticeId, "Failed to edit issue.") 298 + } 299 + return 300 + } 301 + 302 + p.HxRefresh(w) 303 + } 304 + } 305 + 306 + func CloseIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 307 + noticeId := "issue-action" 308 + return func(w http.ResponseWriter, r *http.Request) { 309 + ctx := r.Context() 310 + l := log.FromContext(ctx).With("handler", "CloseIssue") 311 + issue, ok := request.IssueFromContext(ctx) 312 + if !ok { 313 + l.Error("malformed request, failed to get issue") 314 + p.Error503(w) 315 + return 316 + } 317 + 318 + err := is.CloseIssue(ctx, issue) 319 + if err != nil { 320 + if errors.Is(err, isvc.ErrForbidden) { 321 + http.Error(w, "forbidden", http.StatusUnauthorized) 322 + } else { 323 + p.Notice(w, noticeId, "Failed to close issue. Try again later.") 324 + } 325 + return 326 + } 327 + 328 + ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo) 329 + p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) 330 + } 331 + } 332 + 333 + func ReopenIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 334 + noticeId := "issue-action" 335 + return func(w http.ResponseWriter, r *http.Request) { 336 + ctx := r.Context() 337 + l := log.FromContext(ctx).With("handler", "ReopenIssue") 338 + issue, ok := request.IssueFromContext(ctx) 339 + if !ok { 340 + l.Error("malformed request, failed to get issue") 341 + p.Error503(w) 342 + return 343 + } 344 + 345 + err := is.ReopenIssue(ctx, issue) 346 + if err != nil { 347 + if errors.Is(err, isvc.ErrForbidden) { 348 + http.Error(w, "forbidden", http.StatusUnauthorized) 349 + } else { 350 + p.Notice(w, noticeId, "Failed to reopen issue. Try again later.") 351 + } 352 + return 353 + } 354 + 355 + ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo) 356 + p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) 357 + } 358 + } 359 + 360 + func IssueDelete(s isvc.Service, p *pages.Pages) http.HandlerFunc { 361 + noticeId := "issue-actions-error" 362 + return func(w http.ResponseWriter, r *http.Request) { 363 + ctx := r.Context() 364 + l := log.FromContext(ctx).With("handler", "IssueDelete") 365 + issue, ok := request.IssueFromContext(ctx) 366 + if !ok { 367 + l.Error("failed to get issue") 368 + // TODO: 503 error with more detailed messages 369 + p.Error503(w) 370 + return 371 + } 372 + err := s.DeleteIssue(ctx, issue) 373 + if err != nil { 374 + p.Notice(w, noticeId, "failed to delete issue") 375 + return 376 + } 377 + p.HxLocation(w, "/") 378 + } 379 + }
-115
appview/web/handler/user_repo_issues_issue.go
··· 1 - package handler 2 - 3 - import ( 4 - "net/http" 5 - 6 - "tangled.org/core/api/tangled" 7 - "tangled.org/core/appview/db" 8 - "tangled.org/core/appview/models" 9 - "tangled.org/core/appview/pages" 10 - isvc "tangled.org/core/appview/service/issue" 11 - rsvc "tangled.org/core/appview/service/repo" 12 - "tangled.org/core/appview/session" 13 - "tangled.org/core/appview/web/request" 14 - "tangled.org/core/log" 15 - "tangled.org/core/orm" 16 - ) 17 - 18 - func Issue(s isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc { 19 - return func(w http.ResponseWriter, r *http.Request) { 20 - ctx := r.Context() 21 - l := log.FromContext(ctx).With("handler", "Issue") 22 - issue, ok := request.IssueFromContext(ctx) 23 - if !ok { 24 - l.Error("malformed request, failed to get issue") 25 - p.Error503(w) 26 - return 27 - } 28 - repoOwnerId, ok := request.OwnerFromContext(ctx) 29 - if !ok { 30 - l.Error("malformed request") 31 - p.Error503(w) 32 - return 33 - } 34 - 35 - // render 36 - err := func() error { 37 - user := session.UserFromContext(ctx) 38 - repoinfo, err := rs.GetRepoInfo(ctx, repoOwnerId, issue.Repo, "", "", user) 39 - if err != nil { 40 - l.Error("failed to load repo", "err", err) 41 - return err 42 - } 43 - 44 - reactionMap, err := db.GetReactionMap(d, 20, issue.AtUri()) 45 - if err != nil { 46 - l.Error("failed to get issue reactions", "err", err) 47 - return err 48 - } 49 - 50 - userReactions := map[models.ReactionKind]bool{} 51 - if user != nil { 52 - userReactions = db.GetReactionStatusMap(d, user.Did, issue.AtUri()) 53 - } 54 - 55 - backlinks, err := db.GetBacklinks(d, issue.AtUri()) 56 - if err != nil { 57 - l.Error("failed to fetch backlinks", "err", err) 58 - return err 59 - } 60 - 61 - labelDefs, err := db.GetLabelDefinitions( 62 - d, 63 - orm.FilterIn("at_uri", issue.Repo.Labels), 64 - orm.FilterContains("scope", tangled.RepoIssueNSID), 65 - ) 66 - if err != nil { 67 - l.Error("failed to fetch label defs", "err", err) 68 - return err 69 - } 70 - 71 - defs := make(map[string]*models.LabelDefinition) 72 - for _, l := range labelDefs { 73 - defs[l.AtUri().String()] = &l 74 - } 75 - 76 - return p.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 77 - LoggedInUser: user, 78 - RepoInfo: *repoinfo, 79 - Issue: issue, 80 - CommentList: issue.CommentList(), 81 - Backlinks: backlinks, 82 - OrderedReactionKinds: models.OrderedReactionKinds, 83 - Reactions: reactionMap, 84 - UserReacted: userReactions, 85 - LabelDefs: defs, 86 - }) 87 - }() 88 - if err != nil { 89 - l.Error("failed to render", "err", err) 90 - p.Error503(w) 91 - return 92 - } 93 - } 94 - } 95 - 96 - func IssueDelete(s isvc.Service, p *pages.Pages) http.HandlerFunc { 97 - noticeId := "issue-actions-error" 98 - return func(w http.ResponseWriter, r *http.Request) { 99 - ctx := r.Context() 100 - l := log.FromContext(ctx).With("handler", "IssueDelete") 101 - issue, ok := request.IssueFromContext(ctx) 102 - if !ok { 103 - l.Error("failed to get issue") 104 - // TODO: 503 error with more detailed messages 105 - p.Error503(w) 106 - return 107 - } 108 - err := s.DeleteIssue(ctx, issue) 109 - if err != nil { 110 - p.Notice(w, noticeId, "failed to delete issue") 111 - return 112 - } 113 - p.HxLocation(w, "/") 114 - } 115 - }
-40
appview/web/handler/user_repo_issues_issue_close.go
··· 1 - package handler 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - "net/http" 7 - 8 - "tangled.org/core/appview/pages" 9 - "tangled.org/core/appview/reporesolver" 10 - isvc "tangled.org/core/appview/service/issue" 11 - "tangled.org/core/appview/web/request" 12 - "tangled.org/core/log" 13 - ) 14 - 15 - func CloseIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 16 - noticeId := "issue-action" 17 - return func(w http.ResponseWriter, r *http.Request) { 18 - ctx := r.Context() 19 - l := log.FromContext(ctx).With("handler", "CloseIssue") 20 - issue, ok := request.IssueFromContext(ctx) 21 - if !ok { 22 - l.Error("malformed request, failed to get issue") 23 - p.Error503(w) 24 - return 25 - } 26 - 27 - err := is.CloseIssue(ctx, issue) 28 - if err != nil { 29 - if errors.Is(err, isvc.ErrForbidden) { 30 - http.Error(w, "forbidden", http.StatusUnauthorized) 31 - } else { 32 - p.Notice(w, noticeId, "Failed to close issue. Try again later.") 33 - } 34 - return 35 - } 36 - 37 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo) 38 - p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) 39 - } 40 - }
-84
appview/web/handler/user_repo_issues_issue_edit.go
··· 1 - package handler 2 - 3 - import ( 4 - "errors" 5 - "net/http" 6 - 7 - "tangled.org/core/appview/pages" 8 - isvc "tangled.org/core/appview/service/issue" 9 - rsvc "tangled.org/core/appview/service/repo" 10 - "tangled.org/core/appview/session" 11 - "tangled.org/core/appview/web/request" 12 - "tangled.org/core/log" 13 - ) 14 - 15 - func IssueEdit(is isvc.Service, rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 16 - return func(w http.ResponseWriter, r *http.Request) { 17 - ctx := r.Context() 18 - l := log.FromContext(ctx).With("handler", "IssueEdit") 19 - issue, ok := request.IssueFromContext(ctx) 20 - if !ok { 21 - l.Error("malformed request, failed to get issue") 22 - p.Error503(w) 23 - return 24 - } 25 - repoOwnerId, ok := request.OwnerFromContext(ctx) 26 - if !ok { 27 - l.Error("malformed request") 28 - p.Error503(w) 29 - return 30 - } 31 - 32 - // render 33 - err := func() error { 34 - user := session.UserFromContext(ctx) 35 - repoinfo, err := rs.GetRepoInfo(ctx, repoOwnerId, issue.Repo, "", "", user) 36 - if err != nil { 37 - return err 38 - } 39 - return p.EditIssueFragment(w, pages.EditIssueParams{ 40 - LoggedInUser: user, 41 - RepoInfo: *repoinfo, 42 - 43 - Issue: issue, 44 - }) 45 - }() 46 - if err != nil { 47 - l.Error("failed to render", "err", err) 48 - p.Error503(w) 49 - return 50 - } 51 - } 52 - } 53 - 54 - func IssueEditPost(is isvc.Service, p *pages.Pages) http.HandlerFunc { 55 - noticeId := "issues" 56 - return func(w http.ResponseWriter, r *http.Request) { 57 - ctx := r.Context() 58 - l := log.FromContext(ctx).With("handler", "IssueEdit") 59 - issue, ok := request.IssueFromContext(ctx) 60 - if !ok { 61 - l.Error("malformed request, failed to get issue") 62 - p.Error503(w) 63 - return 64 - } 65 - 66 - newIssue := *issue 67 - newIssue.Title = r.FormValue("title") 68 - newIssue.Body = r.FormValue("body") 69 - 70 - err := is.EditIssue(ctx, &newIssue) 71 - if err != nil { 72 - if errors.Is(err, isvc.ErrDatabaseFail) { 73 - p.Notice(w, noticeId, "Failed to edit issue.") 74 - } else if errors.Is(err, isvc.ErrPDSFail) { 75 - p.Notice(w, noticeId, "Failed to edit issue.") 76 - } else { 77 - p.Notice(w, noticeId, "Failed to edit issue.") 78 - } 79 - return 80 - } 81 - 82 - p.HxRefresh(w) 83 - } 84 - }
-40
appview/web/handler/user_repo_issues_issue_reopen.go
··· 1 - package handler 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - "net/http" 7 - 8 - "tangled.org/core/appview/pages" 9 - "tangled.org/core/appview/reporesolver" 10 - isvc "tangled.org/core/appview/service/issue" 11 - "tangled.org/core/appview/web/request" 12 - "tangled.org/core/log" 13 - ) 14 - 15 - func ReopenIssue(is isvc.Service, p *pages.Pages) http.HandlerFunc { 16 - noticeId := "issue-action" 17 - return func(w http.ResponseWriter, r *http.Request) { 18 - ctx := r.Context() 19 - l := log.FromContext(ctx).With("handler", "ReopenIssue") 20 - issue, ok := request.IssueFromContext(ctx) 21 - if !ok { 22 - l.Error("malformed request, failed to get issue") 23 - p.Error503(w) 24 - return 25 - } 26 - 27 - err := is.ReopenIssue(ctx, issue) 28 - if err != nil { 29 - if errors.Is(err, isvc.ErrForbidden) { 30 - http.Error(w, "forbidden", http.StatusUnauthorized) 31 - } else { 32 - p.Notice(w, noticeId, "Failed to reopen issue. Try again later.") 33 - } 34 - return 35 - } 36 - 37 - ownerSlashRepo := reporesolver.GetBaseRepoPath(r, issue.Repo) 38 - p.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) 39 - } 40 - }
-79
appview/web/handler/user_repo_issues_new.go
··· 1 - package handler 2 - 3 - import ( 4 - "errors" 5 - "fmt" 6 - "net/http" 7 - 8 - "tangled.org/core/appview/pages" 9 - isvc "tangled.org/core/appview/service/issue" 10 - rsvc "tangled.org/core/appview/service/repo" 11 - "tangled.org/core/appview/session" 12 - "tangled.org/core/appview/web/request" 13 - "tangled.org/core/log" 14 - ) 15 - 16 - func NewIssue(rs rsvc.Service, p *pages.Pages) http.HandlerFunc { 17 - return func(w http.ResponseWriter, r *http.Request) { 18 - ctx := r.Context() 19 - l := log.FromContext(ctx).With("handler", "NewIssue") 20 - 21 - // render 22 - err := func() error { 23 - user := session.UserFromContext(ctx) 24 - repo, ok := request.RepoFromContext(ctx) 25 - if !ok { 26 - return fmt.Errorf("malformed request") 27 - } 28 - repoOwnerId, ok := request.OwnerFromContext(ctx) 29 - if !ok { 30 - return fmt.Errorf("malformed request") 31 - } 32 - repoinfo, err := rs.GetRepoInfo(ctx, repoOwnerId, repo, "", "", user) 33 - if err != nil { 34 - return err 35 - } 36 - return p.RepoNewIssue(w, pages.RepoNewIssueParams{ 37 - LoggedInUser: user, 38 - RepoInfo: *repoinfo, 39 - }) 40 - }() 41 - if err != nil { 42 - l.Error("failed to render", "err", err) 43 - p.Error503(w) 44 - return 45 - } 46 - } 47 - } 48 - 49 - func NewIssuePost(is isvc.Service, p *pages.Pages) http.HandlerFunc { 50 - noticeId := "issues" 51 - return func(w http.ResponseWriter, r *http.Request) { 52 - ctx := r.Context() 53 - l := log.FromContext(ctx).With("handler", "NewIssuePost") 54 - repo, ok := request.RepoFromContext(ctx) 55 - if !ok { 56 - l.Error("malformed request, failed to get repo") 57 - // TODO: 503 error with more detailed messages 58 - p.Error503(w) 59 - return 60 - } 61 - var ( 62 - title = r.FormValue("title") 63 - body = r.FormValue("body") 64 - ) 65 - 66 - _, err := is.NewIssue(ctx, repo, title, body) 67 - if err != nil { 68 - if errors.Is(err, isvc.ErrDatabaseFail) { 69 - p.Notice(w, noticeId, "Failed to create issue.") 70 - } else if errors.Is(err, isvc.ErrPDSFail) { 71 - p.Notice(w, noticeId, "Failed to create issue.") 72 - } else { 73 - p.Notice(w, noticeId, "Failed to create issue.") 74 - } 75 - return 76 - } 77 - p.HxLocation(w, "/") 78 - } 79 - }
appview/web/middleware/auth.go

This file has not been changed.

appview/web/middleware/ensuredidorhandle.go

This file has not been changed.

appview/web/middleware/log.go

This file has not been changed.

appview/web/middleware/middleware.go

This file has not been changed.

appview/web/middleware/normalize.go

This file has not been changed.

appview/web/middleware/paginate.go

This file has not been changed.

appview/web/middleware/resolve.go

This file has not been changed.

+5 -3
appview/web/request/context.go
··· 7 7 "tangled.org/core/appview/models" 8 8 ) 9 9 10 - type ctxKeyOwner struct{} 11 - type ctxKeyRepo struct{} 12 - type ctxKeyIssue struct{} 10 + type ( 11 + ctxKeyOwner struct{} 12 + ctxKeyRepo struct{} 13 + ctxKeyIssue struct{} 14 + ) 13 15 14 16 func WithOwner(ctx context.Context, owner *identity.Identity) context.Context { 15 17 return context.WithValue(ctx, ctxKeyOwner{}, owner)
-8
appview/web/routes.go
··· 22 22 "tangled.org/core/rbac" 23 23 ) 24 24 25 - // Rules 26 - // - Use single function for each endpoints (unless it doesn't make sense.) 27 - // - Name handler files following the related path (ancestor paths can be 28 - // trimmed.) 29 - // - Pass dependencies to each handlers, don't create structs with shared 30 - // dependencies unless it serves some domain-specific roles like 31 - // service/issue. Same rule goes to middlewares. 32 - 33 25 // RouterFromState creates a web router from `state.State`. This exist to 34 26 // bridge between legacy web routers under `State` and new architecture 35 27 func RouterFromState(s *state.State) http.Handler {
cmd/appview/main.go

This file has not been changed.

+34
appview/web/handler/oauth.go
··· 1 + package handler 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + 7 + "tangled.org/core/appview/oauth" 8 + ) 9 + 10 + func OauthClientMetadata(o *oauth.OAuth) http.HandlerFunc { 11 + return func(w http.ResponseWriter, r *http.Request) { 12 + doc := o.ClientApp.Config.ClientMetadata() 13 + doc.JWKSURI = &o.JwksUri 14 + doc.ClientName = &o.ClientName 15 + doc.ClientURI = &o.ClientUri 16 + 17 + w.Header().Set("Content-Type", "application/json") 18 + if err := json.NewEncoder(w).Encode(doc); err != nil { 19 + http.Error(w, err.Error(), http.StatusInternalServerError) 20 + return 21 + } 22 + } 23 + } 24 + 25 + func OauthJwks(o *oauth.OAuth) http.HandlerFunc { 26 + return func(w http.ResponseWriter, r *http.Request) { 27 + w.Header().Set("Content-Type", "application/json") 28 + body := o.ClientApp.Config.PublicJWKS() 29 + if err := json.NewEncoder(w).Encode(body); err != nil { 30 + http.Error(w, err.Error(), http.StatusInternalServerError) 31 + return 32 + } 33 + } 34 + }
+53
appview/web/readme.md
··· 1 + # appview/web 2 + 3 + ## package structure 4 + 5 + ``` 6 + web/ 7 + |- routes.go 8 + |- handler/ 9 + | |- xrpc/ 10 + |- middleware/ 11 + |- request/ 12 + ``` 13 + 14 + - `web/routes.go` : all possible routes defined in single file 15 + - `web/handler` : general http handlers 16 + - `web/handler/xrpc` : xrpc handlers 17 + - `web/middleware` : all middlwares 18 + - `web/request` : define methods to insert/fetch values from request context. shared between middlewares and handlers. 19 + 20 + ### file name convention on `web/handler` 21 + 22 + - Follow the absolute uri path of the handlers (replace `/` to `_`.) 23 + - Trailing path segments can be omitted. 24 + - Avoid conflicts between prefix and names. 25 + - e.g. using both `user_repo_pulls.go` and `user_repo_pulls_rounds.go` (with `user_repo_pulls_` prefix) 26 + 27 + ### handler-generators instead of raw handler function 28 + 29 + instead of: 30 + ```go 31 + type Handler struct { 32 + is isvc.Service 33 + rs rsvc.Service 34 + } 35 + func (h *Handler) RepoIssues(w http.ResponseWriter, r *http.Request) { 36 + // ... 37 + } 38 + ``` 39 + 40 + prefer: 41 + ```go 42 + func RepoIssues(is isvc.Service, rs rsvc.Service, p *pages.Pages, d *db.DB) http.HandlerFunc { 43 + return func(w http.ResponseWriter, r *http.Request) { 44 + // ... 45 + } 46 + } 47 + ``` 48 + 49 + Pass dependencies to each handler-generators and avoid creating structs with shared dependencies unless it serves somedomain-specific roles like `service/issue.Service`. Same rule applies to middlewares too. 50 + 51 + This pattern is inspired by [the grafana blog post](https://grafana.com/blog/how-i-write-http-services-in-go-after-13-years/#maker-funcs-return-the-handler). 52 + 53 + Function name can be anything as long as it is clear.

History

14 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
appview/{service,web}: service layer
1/3 failed, 2/3 success
expand
merge conflicts detected
expand
  • appview/pages/templates/user/login.html:33
  • appview/state/profile.go:817
  • appview/pages/templates/user/login.html:31
  • appview/pages/templates/user/login.html:93
  • appview/repo/artifact.go:251
  • appview/state/profile.go:528
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
1/3 failed, 2/3 success
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
3/3 failed
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
2/3 failed, 1/3 success
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
appview/{service,web}: service layer
1/3 failed, 2/3 timeout
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
1/3 failed, 1/3 timeout, 1/3 success
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
draft: appview/service: service layer
3/3 success
expand
expand 0 comments
1 commit
expand
draft: appview: service layer
3/3 failed
expand
expand 0 comments
1 commit
expand
draft: appview: service layer
3/3 failed
expand
expand 0 comments