Skip to content

Commit

Permalink
refactor: Problems 移至根目录
Browse files Browse the repository at this point in the history
  • Loading branch information
caixw committed Jan 5, 2024
1 parent 9f34ea3 commit e7cf979
Show file tree
Hide file tree
Showing 18 changed files with 281 additions and 377 deletions.
10 changes: 8 additions & 2 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ func BenchmarkContext_Object_withHeader(b *testing.B) {
func BenchmarkNewRFC7807(b *testing.B) {
for i := 0; i < b.N; i++ {
p := newRFC7807()
p.Init("id", "title", "detail", 400)
p.Type = "id"
p.Title = "title"
p.Detail = "detail"
p.Status = 400
p.WithExtensions(&object{Name: "n1", Age: 11}).WithParam("p1", "v1")
rfc7807Pool.Put(p)
}
Expand All @@ -189,7 +192,10 @@ func BenchmarkRFC7807_unmarshal_json(b *testing.B) {
ctx := s.NewContext(w, r, types.NewContext())

p := newRFC7807()
p.Init("id", "title", "detail", 400)
p.Type = "id"
p.Title = "title"
p.Detail = "detail"
p.Status = 400
p.WithExtensions(&object{Name: "n1", Age: 11}).WithParam("p1", "v1")
for i := 0; i < b.N; i++ {
p.Apply(ctx)
Expand Down
12 changes: 9 additions & 3 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ func (ctx *Context) ID() string { return ctx.id }

// SetCharset 设置输出的字符集
//
// 相当于重新设置了 [Context.Request] 的 Accept-Charset 报头,但是不会实际修改 [Context.Request]。
// 不会修改 [Context.Request] 中的 Accept-Charset 报头,如果需要同时修改此报头
// 可以通过 [Server.NewContext] 构建一个新 [Context] 对象。
func (ctx *Context) SetCharset(charset string) {
if ctx.Wrote() {
panic("已有内容输出,不可再更改!")
Expand All @@ -242,7 +243,8 @@ func (ctx *Context) Charset() string { return ctx.outputCharsetName }

// SetMimetype 设置输出的格式
//
// 相当于重新设置了 [Context.Request] 的 Accept 报头,但是不会实际修改 [Context.Request]。
// 不会修改 [Context.Request] 中的 Accept 报头,如果需要同时修改此报头
// 可以通过 [Server.NewContext] 构建一个新 [Context] 对象。
func (ctx *Context) SetMimetype(mimetype string) {
if ctx.Wrote() {
panic("已有内容输出,不可再更改!")
Expand All @@ -264,6 +266,9 @@ func (ctx *Context) SetMimetype(mimetype string) {
func (ctx *Context) Mimetype(problem bool) string { return ctx.outputMimetype.name(problem) }

// SetEncoding 设置输出的压缩编码
//
// 不会修改 [Context.Request] 中的 Accept-Encoding 报头,如果需要同时修改此报头
// 可以通过 [Server.NewContext] 构建一个新 [Context] 对象。
func (ctx *Context) SetEncoding(enc string) {
if ctx.Wrote() {
panic("已有内容输出,不可再更改!")
Expand Down Expand Up @@ -294,7 +299,8 @@ func (ctx *Context) Encoding() string {

// SetLanguage 修改输出的语言
//
// 相当于重新设置了 [Context.Request] 的 Accept-Language 报头,但是不会实际修改 [Context.Request]。
// 不会修改 [Context.Request] 中的 Accept-Language 报头,如果需要同时修改此报头
// 可以通过 [Server.NewContext] 构建一个新 [Context] 对象。
func (ctx *Context) SetLanguage(tag language.Tag) {
// 不判断是否有内容已经输出,允许中途改变语言。
if ctx.languageTag != tag {
Expand Down
3 changes: 3 additions & 0 deletions locales/und.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ messages:
- key: unique identity generator
message:
msg: unique identity generator
- key: unset callback name
message:
msg: unset callback name
- key: unsupported serialization
message:
msg: unsupported serialization
3 changes: 3 additions & 0 deletions locales/zh-CN.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ messages:
- key: unique identity generator
message:
msg: 唯一 ID 生成器
- key: unset callback name
message:
msg: 未设置回调方法的名称
- key: unsupported serialization
message:
msg: 不支持序列化或是反序列化
16 changes: 16 additions & 0 deletions make_problems.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func main() {

makeID(buf, kvs)
makeIDs(buf, kvs)
makeInitProblems(buf, kvs)

if buf.Err != nil {
panic(buf.Err)
Expand Down Expand Up @@ -62,3 +63,18 @@ func makeIDs(buf *errwrap.Buffer, kvs []status.Pair) {

buf.WString("}\n\n")
}

func makeInitProblems(buf *errwrap.Buffer, kvs []status.Pair) {
buf.WString("func initProblems(p*Problems){")

for _, item := range kvs {
status := "http." + item.Name
title := "problem." + strconv.Itoa(item.Value)
detail := title + ".detail"
title = "StringPhrase(\"" + title + "\")"
detail = "StringPhrase(\"" + detail + "\")"

buf.Printf(`p.Add(%s,&LocaleProblem{ID:%s,Title:%s,Detail:%s})`, status, item.ID(), title, detail).WByte('\n')
}
buf.WString("}\n\n")
}
1 change: 1 addition & 0 deletions output.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func (ctx *Context) Render(status int, body any) {

data, err := ctx.Marshal(body)
if err != nil {
// [RFC7807.Apply] 并未调用 [Context.Render],应该不会死循环。
ctx.Error(err, ProblemNotAcceptable).Apply(ctx)
return
}
Expand Down
129 changes: 106 additions & 23 deletions problem.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ package web

import (
"errors"
"fmt"
"io/fs"
"slices"
"sync"

"github.com/issue9/sliceutil"
"github.com/issue9/localeutil"

"github.com/issue9/web/internal/errs"
"github.com/issue9/web/internal/header"
"github.com/issue9/web/internal/status"
)

const rfc7807PoolMaxParams = 30 // len(RFC7807.Params) 少于此值才会回收。
Expand Down Expand Up @@ -65,27 +68,30 @@ type (
Name string `json:"name" xml:"name" form:"name"` // 出错字段的名称
Reason string `json:"reason" xml:"reason" form:"reason"` // 出错信息
}

Problems struct {
prefix string
problems []*LocaleProblem // 需保证元素的顺序相同
}

LocaleProblem struct {
ID string
Title, Detail LocaleStringer

status int
typ string // 带前缀的 ID 值
}
)

func newRFC7807() *RFC7807 {
p := rfc7807Pool.Get().(*RFC7807)

if p.Params != nil {
p.Params = p.Params[:0]
}
p.Extensions = nil
p.Instance = ""

// 其它字段在 init 会被初始化

return p
}

func (p *RFC7807) Init(id, title, detail string, status int) {
p.Type = id
p.Title = title
p.Detail = detail
p.Status = status
// 其它的基本字段在 [Problems.initProblem] 中初始化
}

func (p *RFC7807) Error() string { return p.Title }
Expand Down Expand Up @@ -117,7 +123,7 @@ func (p *RFC7807) Apply(ctx *Context) Problem {
}

func (p *RFC7807) WithParam(name, reason string) Problem {
if sliceutil.Exists(p.Params, func(pp RFC7807Param, _ int) bool { return pp.Name == name }) {
if slices.IndexFunc(p.Params, func(pp RFC7807Param) bool { return pp.Name == name }) > -1 {
panic("已经存在")
}
p.Params = append(p.Params, RFC7807Param{Name: name, Reason: reason})
Expand All @@ -139,9 +145,9 @@ func (p *RFC7807) private() {}
// Problem 返回指定 id 的 [Problem]
func (ctx *Context) Problem(id string) Problem { return ctx.initProblem(newRFC7807(), id) }

func (ctx *Context) initProblem(p *RFC7807, id string) Problem {
ctx.Server().Problems().Init(p, id, ctx.LocalePrinter())
return p.WithInstance(ctx.ID())
func (ctx *Context) initProblem(pp *RFC7807, id string) Problem {
ctx.Server().Problems().initProblem(pp, id, ctx.LocalePrinter())
return pp.WithInstance(ctx.ID())
}

// Error 将 err 输出到 ERROR 通道并尝试以指定 id 的 [Problem] 返回
Expand All @@ -151,24 +157,24 @@ func (ctx *Context) initProblem(p *RFC7807, id string) Problem {
// - err 是否为 [fs.ErrPermission],如果是采用 [ProblemForbidden] 作为 ID;
// - err 是否为 [fs.ErrNotExist],如果是采用 [ProblemNotFound] 作为 ID;
// - 采用 [ProblemInternalServerError];
func (ctx *Context) Error(err error, id string) Problem {
if id == "" {
func (ctx *Context) Error(err error, problemID string) Problem {
if problemID == "" {
var herr *errs.HTTP
switch {
case errors.As(err, &herr):
id = problemsID[herr.Status]
problemID = problemsID[herr.Status]
err = herr.Message
case errors.Is(err, fs.ErrPermission):
id = ProblemForbidden
problemID = ProblemForbidden
case errors.Is(err, fs.ErrNotExist):
id = ProblemNotFound
problemID = ProblemNotFound
default:
id = ProblemInternalServerError
problemID = ProblemInternalServerError
}
}

ctx.Logs().ERROR().Handler().Handle(ctx.Logs().NewRecord().DepthError(3, err))
return ctx.Problem(id)
return ctx.Problem(problemID)
}

func (ctx *Context) NotFound() Problem { return ctx.Problem(ProblemNotFound) }
Expand All @@ -182,3 +188,80 @@ func (v *FilterContext) Problem(id string) Problem {
}
return v.Context().initProblem(v.problem, id)
}

func InternalNewProblems(prefix string) *Problems {
ps := &Problems{
prefix: prefix,
problems: make([]*LocaleProblem, 0, 100),
}
initProblems(ps)

return ps
}

// Prefix 所有 ID 的统一前缀
func (ps *Problems) Prefix() string { return ps.prefix }

// Add 添加新项
//
// NOTE: 已添加的内容无法修改,如果确实有需求,只能通过修改翻译项的方式间接进行修改。
func (ps *Problems) Add(s int, p ...*LocaleProblem) *Problems {
if !status.IsProblemStatus(s) { // 只需验证大于 400 的状态码。
panic("status 必须是一个有效的状态码")
}

for _, pp := range p {
if ps.exists(pp.ID) {
panic(fmt.Sprintf("存在相同值的 id 参数 %s", pp.ID))
}

if pp.Title == nil {
panic("title 不能为空")
}

if pp.Detail == nil {
panic("detail 不能为空")
}

if ps.Prefix() == ProblemAboutBlank {
pp.typ = ProblemAboutBlank
} else {
pp.typ = ps.Prefix() + pp.ID
}

pp.status = s

ps.problems = append(ps.problems, pp)
}

return ps
}

func (ps *Problems) exists(id string) bool {
return slices.IndexFunc(ps.problems, func(p *LocaleProblem) bool { return p.ID == id }) > -1
}

// Visit 遍历错误代码
//
// visit 签名:
//
// func(status int, p *LocaleProblem)
//
// status 该错误代码反馈给用户的 HTTP 状态码;
func (ps *Problems) Visit(visit func(status int, p *LocaleProblem)) {
for _, s := range ps.problems {
visit(s.status, s)
}
}

func (ps *Problems) initProblem(pp *RFC7807, id string, p *localeutil.Printer) {
if i := slices.IndexFunc(ps.problems, func(p *LocaleProblem) bool { return p.ID == id }); i > -1 {
sp := ps.problems[i]
pp.Type = sp.typ
pp.Title = sp.Title.LocaleString(p)
pp.Detail = sp.Detail.LocaleString(p)
pp.Status = sp.status
return
}
panic(fmt.Sprintf("未找到有关 %s 的定义", id)) // 初始化时没有给定相关的定义,所以直接 panic。
}
Loading

0 comments on commit e7cf979

Please sign in to comment.