package apigen import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "testing" ) func TestFieldDocumentationAndFormat(t *testing.T) { source := "// 项目说明\n type Req{\n// 状态\n State int `json:\"state,options=0|1\" example:\"1\" enumDescriptions:\"[\\\"禁用\\\",\\\"启用\\\"]\"` // 状态值\n /* 第一行\n 第二行 */\nName string `json:\"name\" example:\"企业名称\"`\n}\nservice demo{\n @handler echo\npost /echo (Req) returns (Req)\n}\n" formatted, err := Format([]byte(source)) if err != nil { t.Fatal(err) } again, err := Format(formatted) if err != nil || !bytes.Equal(formatted, again) { t.Fatal("format is not idempotent", err, string(again)) } if !bytes.Contains(formatted, []byte(" // 状态\n State")) { t.Fatal(string(formatted)) } spec, err := parseText(t, string(formatted)) if err != nil { t.Fatal(err) } c, err := Compile(spec) if err != nil { t.Fatal(err) } if spec.Types[0].Fields[0].Description != "状态\n状态值" || spec.Types[0].Fields[1].Description != "第一行\n第二行" { t.Fatalf("%+v", spec.Types[0].Fields) } schema, err := c.Bindings["Req"].ResponseSchema() if err != nil { t.Fatal(err) } state := schema.Properties["state"] if state.Description != "状态\n状态值" || len(state.EnumDescriptions) != 2 || state.Example != json.Number("1") { t.Fatalf("%+v", state) } dir := t.TempDir() entry := filepath.Join(dir, "demo.api") writeFile(t, entry, source) if _, err = FormatProject(entry, true); err == nil { t.Fatal("check accepted drift") } if readFile(t, entry) != source { t.Fatal("check wrote source") } if _, err = FormatProject(entry, false); err != nil { t.Fatal(err) } if _, err = FormatProject(entry, true); err != nil { t.Fatal(err) } project := filepath.Join(t.TempDir(), "demo") if _, err = Generate(Options{API: entry, Dir: project, Module: "example.com/docs", FrameworkDir: frameworkRoot(t)}); err != nil { t.Fatal(err) } dto := readFile(t, filepath.Join(project, "internal/types/"+dtoFile("demo.api:1:1"))) if !strings.Contains(dto, "// 状态值") || !strings.Contains(dto, `description:"状态\n状态值"`) { t.Fatal(dto) } // Invalid examples fail before project output is created. bad := strings.Replace(source, "example:\"1\"", "example:\"9\"", 1) writeFile(t, entry, bad) if _, err = FormatProject(entry, false); err == nil { t.Fatal("invalid enum example accepted") } if data, _ := os.ReadFile(entry); string(data) != bad { t.Fatal("invalid source changed") } } func TestFormatAlignsFieldsAndPreservesTokens(t *testing.T) { source := []byte("type Req {\n Name string `json:\"name\"` // 名称\n Sequence int64 `json:\"sequence\"` // 编号\n}\n") out, err := Format(source) if err != nil { t.Fatal(err) } again, err := Format(out) if err != nil || !bytes.Equal(out, again) { t.Fatal("alignment not idempotent", err) } lines := strings.Split(string(out), "\n") if strings.Index(lines[1], "`") != strings.Index(lines[2], "`") || strings.Index(lines[1], "//") != strings.Index(lines[2], "//") { t.Fatal("fields not aligned", string(out)) } for _, literal := range []string{"`json:\"name\"`", "`json:\"sequence\"`", "// 名称", "// 编号"} { if !bytes.Contains(out, []byte(literal)) { t.Fatal("literal changed", literal) } } } func TestRequestPolicyCannotCreateGETBody(t *testing.T) { for _, source := range []string{ "service demo { @handler get\n @request(body: required)\n get /x }", "@app(request: {\"body\":\"optional\"})\n service demo { @handler get\n get /x }", } { spec, err := parseText(t, source) if err == nil { _, err = Compile(spec) } if err == nil { t.Fatal("GET body policy accepted") } } } func TestFormatLiteralPreservation(t *testing.T) { source := []byte("type Demo {\n Body string `json:\"body\"\ndescription:\"literal space\"`\n}\n") out, err := Format(source) if err != nil { t.Fatal(err) } if !bytes.Contains(out, []byte("`json:\"body\"\ndescription:\"literal space\"`")) { t.Fatal(string(out)) } for _, src := range []string{"type X {", "}", "info (title:\"unterminated)"} { if _, err := Format([]byte(src)); err == nil { t.Fatal("invalid input accepted") } } }