Skip to content

Commit be821fa

Browse files
committed
feat: kitex tool generate http tags
1 parent c42c193 commit be821fa

File tree

7 files changed

+417
-2
lines changed

7 files changed

+417
-2
lines changed

tool/cmd/kitex/args/args.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func (a *Arguments) buildFlags(version string) *flag.FlagSet {
127127
f.Var(&a.FrugalStruct, "frugal-struct", "Replace fastCodec code to frugal. Use `-frugal-struct @all` for all, `-frugal-struct @auto` for annotated structs (go.codec=\"frugal\"), or specify multiple structs (e.g., `-frugal-struct A -frugal-struct B`).")
128128

129129
f.BoolVar(&a.NoRecurse, "no-recurse", false, `Don't generate thrift files recursively, just generate the given file.'`)
130+
f.BoolVar(&a.HTTPTags, "http-tags", false, `Recognize api.xx tags in IDL and generate HTTP go tags and router info in kitex_gen.'`)
130131

131132
if env.UseProtoc() {
132133
f.Var(&a.ProtobufOptions, "protobuf", "Specify arguments for the protobuf compiler.")
@@ -151,6 +152,10 @@ func (a *Arguments) buildFlags(version string) *flag.FlagSet {
151152
"no_processor",
152153
)
153154

155+
if a.HTTPTags {
156+
a.ThriftOptions = append(a.ThriftOptions, "gen_json_tag=false")
157+
}
158+
154159
f.Usage = func() {
155160
fmt.Fprintf(os.Stderr, `Version %s
156161
Usage: %s [flags] IDL

tool/internal_pkg/generator/generator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ type Config struct {
149149

150150
FrugalStruct util.StringSlice
151151
NoRecurse bool
152+
HTTPTags bool
152153

153154
BuiltinTpl util.StringSlice // specify the built-in template to use
154155

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2025 CloudWeGo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package thriftgo
16+
17+
import (
18+
"sort"
19+
"strings"
20+
21+
"github.com/cloudwego/kitex/tool/internal_pkg/util"
22+
23+
"github.com/cloudwego/thriftgo/parser"
24+
)
25+
26+
const (
27+
AnnotationQuery = "api.query"
28+
AnnotationForm = "api.form"
29+
AnnotationPath = "api.path"
30+
AnnotationHeader = "api.header"
31+
AnnotationCookie = "api.cookie"
32+
AnnotationBody = "api.body"
33+
AnnotationRawBody = "api.raw_body"
34+
)
35+
36+
var bindingTags = map[string]string{
37+
AnnotationPath: "path",
38+
AnnotationQuery: "query",
39+
AnnotationHeader: "header",
40+
AnnotationCookie: "cookie",
41+
AnnotationBody: "form",
42+
AnnotationForm: "form",
43+
AnnotationRawBody: "raw_body",
44+
}
45+
46+
type genHTTPTagOption struct {
47+
// thriftgo: -thrift snake_type_json_tag
48+
snakeTyleJSONTag bool
49+
// thriftgo: -thrift low_camel_case_json
50+
lowerCamelCaseJSONTag bool
51+
// hertz: --unset_omitempty
52+
unsetOmitempty bool
53+
// hertz: --snake_tag
54+
snakeStyleHTTPTag bool
55+
}
56+
57+
// genHTTPTags append api.xx and json tag into struct field for http binding usage.
58+
func genHTTPTags(f *parser.Field, opt genHTTPTagOption) string {
59+
w := initTagWriter(f, opt)
60+
var found bool
61+
for _, a := range f.Annotations {
62+
if tag, ok := bindingTags[a.Key]; ok {
63+
tagVal := a.Values[len(a.Values)-1]
64+
if a.Key == AnnotationBody {
65+
w.resetJsonVal(tagVal)
66+
}
67+
w.addTag(tag, tagVal)
68+
found = true
69+
}
70+
}
71+
if !found {
72+
w.addTag("form", f.Name)
73+
w.addTag("query", f.Name)
74+
}
75+
return w.dump()
76+
}
77+
78+
type tagWriter struct {
79+
jsonVal string
80+
tags map[string]string
81+
opt genHTTPTagOption
82+
f *parser.Field
83+
}
84+
85+
func (tw *tagWriter) resetJsonVal(jsonVal string) {
86+
tw.jsonVal = jsonVal
87+
}
88+
89+
func (tw *tagWriter) addTag(k, v string) {
90+
tw.tags[k] = v
91+
}
92+
93+
func initTagWriter(f *parser.Field, opt genHTTPTagOption) *tagWriter {
94+
return &tagWriter{f.Name, make(map[string]string), opt, f}
95+
}
96+
97+
func (tw tagWriter) dump() string {
98+
var tagSuffix, jsonSuffix string
99+
if tw.f.GetRequiredness().IsRequired() {
100+
tagSuffix = ",required"
101+
jsonSuffix = ",required"
102+
} else if tw.f.GetRequiredness().IsOptional() && !tw.opt.unsetOmitempty {
103+
jsonSuffix = ",omitempty"
104+
}
105+
// same as github.com/cloudwego/thriftgo/generator/golang/utils.go genFieldTags
106+
if tw.opt.snakeTyleJSONTag {
107+
tw.jsonVal = util.Snakify(tw.jsonVal)
108+
}
109+
if tw.opt.lowerCamelCaseJSONTag {
110+
tw.jsonVal = util.LowerCamelCase(tw.jsonVal)
111+
}
112+
var sb strings.Builder
113+
tw.tags["json"] = tw.jsonVal
114+
keys := make([]string, 0, len(tw.tags))
115+
for tag := range tw.tags {
116+
keys = append(keys, tag)
117+
}
118+
// tags are arranged in alphabet order.
119+
sort.Strings(keys)
120+
for _, tag := range keys {
121+
tagVal := tw.tags[tag]
122+
if tag == "json" {
123+
sb.WriteString(` json:"` + tw.jsonVal + jsonSuffix + `"`)
124+
continue
125+
}
126+
if tw.opt.snakeStyleHTTPTag {
127+
tagVal = util.Snakify(tagVal)
128+
}
129+
sb.WriteString(` ` + tag + `:"` + tagVal + tagSuffix + `"`)
130+
}
131+
return sb.String()
132+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright 2025 CloudWeGo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package thriftgo
16+
17+
import (
18+
"testing"
19+
20+
"github.com/cloudwego/kitex/internal/test"
21+
"github.com/cloudwego/thriftgo/parser"
22+
)
23+
24+
func TestGenHTTPTags(t *testing.T) {
25+
emptyOpt := genHTTPTagOption{}
26+
// required, no annotation, generate form and query by default
27+
httpTag := genHTTPTags(&parser.Field{
28+
Name: "f1",
29+
Requiredness: parser.FieldType_Required,
30+
}, emptyOpt)
31+
test.Assert(t, httpTag == ` form:"f1,required" json:"f1,required" query:"f1,required"`, httpTag)
32+
33+
// default, no annotation
34+
httpTag = genHTTPTags(&parser.Field{
35+
Name: "f11",
36+
Requiredness: parser.FieldType_Default,
37+
}, emptyOpt)
38+
test.Assert(t, httpTag == ` form:"f11" json:"f11" query:"f11"`, httpTag)
39+
40+
// optional + go.tag (ignored by genHTTPTags)
41+
httpTag = genHTTPTags(&parser.Field{
42+
Name: "f12",
43+
Requiredness: parser.FieldType_Optional,
44+
Annotations: []*parser.Annotation{
45+
{Key: "go.tag", Values: []string{`some_tag:"some_tag_value"`}},
46+
},
47+
}, emptyOpt)
48+
test.Assert(t, httpTag == ` form:"f12" json:"f12,omitempty" query:"f12"`, httpTag)
49+
50+
// required, api.query
51+
httpTag = genHTTPTags(&parser.Field{
52+
Name: "f22",
53+
Requiredness: parser.FieldType_Required,
54+
Annotations: []*parser.Annotation{
55+
{Key: "api.query", Values: []string{"abc"}},
56+
},
57+
}, emptyOpt)
58+
test.Assert(t, httpTag == ` json:"f22,required" query:"abc,required"`, httpTag)
59+
60+
// required, api.form
61+
httpTag = genHTTPTags(&parser.Field{
62+
Name: "f23",
63+
Requiredness: parser.FieldType_Required,
64+
Annotations: []*parser.Annotation{
65+
{Key: "api.form", Values: []string{"abc"}},
66+
},
67+
}, emptyOpt)
68+
test.Assert(t, httpTag == ` form:"abc,required" json:"f23,required"`, httpTag)
69+
70+
// required, api.path
71+
httpTag = genHTTPTags(&parser.Field{
72+
Name: "f24",
73+
Requiredness: parser.FieldType_Required,
74+
Annotations: []*parser.Annotation{
75+
{Key: "api.path", Values: []string{"abc"}},
76+
},
77+
}, emptyOpt)
78+
test.Assert(t, httpTag == ` json:"f24,required" path:"abc,required"`, httpTag)
79+
80+
// required, api.header
81+
httpTag = genHTTPTags(&parser.Field{
82+
Name: "f25",
83+
Requiredness: parser.FieldType_Required,
84+
Annotations: []*parser.Annotation{
85+
{Key: "api.header", Values: []string{"abc"}},
86+
},
87+
}, emptyOpt)
88+
test.Assert(t, httpTag == ` header:"abc,required" json:"f25,required"`, httpTag)
89+
90+
// required, api.cookie
91+
httpTag = genHTTPTags(&parser.Field{
92+
Name: "f26",
93+
Requiredness: parser.FieldType_Required,
94+
Annotations: []*parser.Annotation{
95+
{Key: "api.cookie", Values: []string{"abc"}},
96+
},
97+
}, emptyOpt)
98+
test.Assert(t, httpTag == ` cookie:"abc,required" json:"f26,required"`, httpTag)
99+
100+
// required, api.raw_body
101+
httpTag = genHTTPTags(&parser.Field{
102+
Name: "f27",
103+
Requiredness: parser.FieldType_Required,
104+
Annotations: []*parser.Annotation{
105+
{Key: "api.raw_body", Values: []string{"abc"}},
106+
},
107+
}, emptyOpt)
108+
test.Assert(t, httpTag == ` json:"f27,required" raw_body:"abc,required"`, httpTag)
109+
110+
// required, multiple annotations, generate all
111+
httpTag = genHTTPTags(&parser.Field{
112+
Name: "f31",
113+
Requiredness: parser.FieldType_Required,
114+
Annotations: []*parser.Annotation{
115+
{Key: "api.query", Values: []string{"abc"}},
116+
{Key: "api.form", Values: []string{"def"}},
117+
{Key: "api.header", Values: []string{"abc"}},
118+
},
119+
}, emptyOpt)
120+
// tags are arranged in alphabet order.
121+
test.Assert(t, httpTag == ` form:"def,required" header:"abc,required" json:"f31,required" query:"abc,required"`, httpTag)
122+
123+
// required, duplicate api.query values, use the last one.
124+
httpTag = genHTTPTags(&parser.Field{
125+
Name: "f32",
126+
Requiredness: parser.FieldType_Required,
127+
Annotations: []*parser.Annotation{
128+
{Key: "api.query", Values: []string{"abc", "def"}},
129+
},
130+
}, emptyOpt)
131+
test.Assert(t, httpTag == ` json:"f32,required" query:"def,required"`, httpTag)
132+
133+
// required, api.body, api body generate json and form
134+
httpTag = genHTTPTags(&parser.Field{
135+
Name: "f41",
136+
Requiredness: parser.FieldType_Required,
137+
Annotations: []*parser.Annotation{
138+
{Key: "api.body", Values: []string{"abc"}},
139+
},
140+
}, emptyOpt)
141+
test.Assert(t, httpTag == ` form:"abc,required" json:"abc,required"`, httpTag)
142+
143+
// default, api.body
144+
httpTag = genHTTPTags(&parser.Field{
145+
Name: "f42",
146+
Requiredness: parser.FieldType_Default,
147+
Annotations: []*parser.Annotation{
148+
{Key: "api.body", Values: []string{"abc"}},
149+
},
150+
}, emptyOpt)
151+
test.Assert(t, httpTag == ` form:"abc" json:"abc"`, httpTag)
152+
153+
// default, api.body
154+
httpTag = genHTTPTags(&parser.Field{
155+
Name: "f43",
156+
Requiredness: parser.FieldType_Optional,
157+
Annotations: []*parser.Annotation{
158+
{Key: "api.body", Values: []string{"abc"}},
159+
},
160+
}, emptyOpt)
161+
test.Assert(t, httpTag == ` form:"abc" json:"abc,omitempty"`, httpTag)
162+
163+
// default, api.body, api.cookie
164+
httpTag = genHTTPTags(&parser.Field{
165+
Name: "f43",
166+
Requiredness: parser.FieldType_Optional,
167+
Annotations: []*parser.Annotation{
168+
{Key: "api.body", Values: []string{"abc"}},
169+
{Key: "api.cookie", Values: []string{"efg"}},
170+
},
171+
}, emptyOpt)
172+
test.Assert(t, httpTag == ` cookie:"efg" form:"abc" json:"abc,omitempty"`, httpTag)
173+
}
174+
175+
func TestGenHTTPTagsWithOption(t *testing.T) {
176+
// snake field name to camel, only for json
177+
httpTag := genHTTPTags(&parser.Field{
178+
Name: "field_a",
179+
Requiredness: parser.FieldType_Required,
180+
}, genHTTPTagOption{lowerCamelCaseJSONTag: true})
181+
test.Assert(t, httpTag == ` form:"field_a,required" json:"fieldA,required" query:"field_a,required"`, httpTag)
182+
183+
// camel field name to snake, only for json
184+
httpTag = genHTTPTags(&parser.Field{
185+
Name: "FieldB",
186+
Requiredness: parser.FieldType_Required,
187+
}, genHTTPTagOption{snakeTyleJSONTag: true})
188+
test.Assert(t, httpTag == ` form:"FieldB,required" json:"field_b,required" query:"FieldB,required"`, httpTag)
189+
190+
// snake opt + camel opt => snake fmt and then camel fmt
191+
httpTag = genHTTPTags(&parser.Field{
192+
Name: "FieldB",
193+
Requiredness: parser.FieldType_Required,
194+
}, genHTTPTagOption{snakeTyleJSONTag: true, lowerCamelCaseJSONTag: true})
195+
test.Assert(t, httpTag == ` form:"FieldB,required" json:"fieldB,required" query:"FieldB,required"`, httpTag)
196+
197+
// it also works for api.body json tag
198+
httpTag = genHTTPTags(&parser.Field{
199+
Name: "field_c",
200+
Requiredness: parser.FieldType_Optional,
201+
Annotations: []*parser.Annotation{
202+
{Key: "api.body", Values: []string{"hey_hello"}},
203+
},
204+
}, genHTTPTagOption{lowerCamelCaseJSONTag: true})
205+
test.Assert(t, httpTag == ` form:"hey_hello" json:"heyHello,omitempty"`, httpTag)
206+
207+
// snakeStyleHTTPTag option only works for form、query..... not for json and “json generated by api.body”
208+
httpTag = genHTTPTags(&parser.Field{
209+
Name: "field_c",
210+
Requiredness: parser.FieldType_Optional,
211+
Annotations: []*parser.Annotation{
212+
{Key: "api.body", Values: []string{"heyHello"}},
213+
{Key: "api.query", Values: []string{"howAreYou"}},
214+
{Key: "api.cookie", Values: []string{"fineThanks"}},
215+
},
216+
}, genHTTPTagOption{snakeStyleHTTPTag: true})
217+
test.Assert(t, httpTag == ` cookie:"fine_thanks" form:"hey_hello" json:"heyHello,omitempty" query:"how_are_you"`, httpTag)
218+
219+
// unsetOmitempty remove 'omitempty' in json
220+
httpTag = genHTTPTags(&parser.Field{
221+
Name: "field_c",
222+
Requiredness: parser.FieldType_Optional,
223+
Annotations: []*parser.Annotation{
224+
{Key: "api.body", Values: []string{"heyHello"}},
225+
{Key: "api.query", Values: []string{"howAreYou"}},
226+
{Key: "api.cookie", Values: []string{"fineThanks"}},
227+
},
228+
}, genHTTPTagOption{unsetOmitempty: true})
229+
test.Assert(t, httpTag == ` cookie:"fineThanks" form:"heyHello" json:"heyHello" query:"howAreYou"`, httpTag)
230+
}

0 commit comments

Comments
 (0)