Skip to content

Commit 300cb9b

Browse files
authored
Merge pull request #120 from ogen-go/feat/field-fmt
feat: add field format
2 parents b4e01a4 + fe86b1e commit 300cb9b

File tree

10 files changed

+276
-18
lines changed

10 files changed

+276
-18
lines changed

example/google/api/field_info.proto

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2023 Google LLC
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+
syntax = "proto3";
16+
17+
package google.api;
18+
19+
import "google/protobuf/descriptor.proto";
20+
21+
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
22+
option java_multiple_files = true;
23+
option java_outer_classname = "FieldInfoProto";
24+
option java_package = "com.google.api";
25+
option objc_class_prefix = "GAPI";
26+
27+
extend google.protobuf.FieldOptions {
28+
// Rich semantic descriptor of an API field beyond the basic typing.
29+
//
30+
// Examples:
31+
//
32+
// string request_id = 1 [(google.api.field_info).format = UUID4];
33+
// string old_ip_address = 2 [(google.api.field_info).format = IPV4];
34+
// string new_ip_address = 3 [(google.api.field_info).format = IPV6];
35+
// string actual_ip_address = 4 [
36+
// (google.api.field_info).format = IPV4_OR_IPV6
37+
// ];
38+
google.api.FieldInfo field_info = 291403980;
39+
}
40+
41+
// Rich semantic information of an API field beyond basic typing.
42+
message FieldInfo {
43+
// The standard format of a field value. The supported formats are all backed
44+
// by either an RFC defined by the IETF or a Google-defined AIP.
45+
enum Format {
46+
// Default, unspecified value.
47+
FORMAT_UNSPECIFIED = 0;
48+
49+
// Universally Unique Identifier, version 4, value as defined by
50+
// https://datatracker.ietf.org/doc/html/rfc4122. The value may be
51+
// normalized to entirely lowercase letters. For example, the value
52+
// `F47AC10B-58CC-0372-8567-0E02B2C3D479` would be normalized to
53+
// `f47ac10b-58cc-0372-8567-0e02b2c3d479`.
54+
UUID4 = 1;
55+
56+
// Internet Protocol v4 value as defined by [RFC
57+
// 791](https://datatracker.ietf.org/doc/html/rfc791). The value may be
58+
// condensed, with leading zeros in each octet stripped. For example,
59+
// `001.022.233.040` would be condensed to `1.22.233.40`.
60+
IPV4 = 2;
61+
62+
// Internet Protocol v6 value as defined by [RFC
63+
// 2460](https://datatracker.ietf.org/doc/html/rfc2460). The value may be
64+
// normalized to entirely lowercase letters, and zero-padded partial and
65+
// empty octets. For example, the value `2001:DB8::` would be normalized to
66+
// `2001:0db8:0:0`.
67+
IPV6 = 3;
68+
69+
// An IP address in either v4 or v6 format as described by the individual
70+
// values defined herein. See the comments on the IPV4 and IPV6 types for
71+
// allowed normalizations of each.
72+
IPV4_OR_IPV6 = 4;
73+
}
74+
75+
// The standard format of a field value. This does not explicitly configure
76+
// any API consumer, just documents the API's format for the field it is
77+
// applied to.
78+
Format format = 1;
79+
}

example/message.proto

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ package service.v1;
55
option go_package = "service/v1;service";
66

77
import "google/api/field_behavior.proto";
8+
import "google/api/field_info.proto";
89
import "google/protobuf/timestamp.proto";
910

1011
message CreateItemRequest {
1112
string name = 1 [(google.api.field_behavior) = REQUIRED];
1213
}
1314

1415
message Item {
15-
string id = 1 [(google.api.field_behavior) = REQUIRED];
16+
string id = 1 [
17+
(google.api.field_behavior) = REQUIRED,
18+
(google.api.field_info).format = UUID4
19+
];
1620
ItemType type = 2 [(google.api.field_behavior) = REQUIRED];
1721
string name = 3 [(google.api.field_behavior) = REQUIRED];
1822
google.protobuf.Timestamp created_at = 4 [(google.api.field_behavior) = REQUIRED]; // Datetime when item was created.

example/openapi.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ components:
175175
properties:
176176
id:
177177
type: string
178+
format: uuid
178179
type:
179180
$ref: '#/components/schemas/ItemType'
180181
name:

go.mod

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ require (
88
github.com/go-faster/yaml v0.4.6
99
github.com/ogen-go/ogen v0.76.0
1010
github.com/stretchr/testify v1.8.4
11-
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
12-
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e
11+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
12+
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b
1313
google.golang.org/protobuf v1.31.0
1414
)
1515

@@ -22,15 +22,15 @@ require (
2222
github.com/google/go-cmp v0.5.9 // indirect
2323
github.com/google/uuid v1.3.1 // indirect
2424
github.com/mattn/go-colorable v0.1.13 // indirect
25-
github.com/mattn/go-isatty v0.0.19 // indirect
25+
github.com/mattn/go-isatty v0.0.20 // indirect
2626
github.com/pmezard/go-difflib v1.0.0 // indirect
2727
github.com/segmentio/asm v1.2.0 // indirect
2828
go.uber.org/multierr v1.11.0 // indirect
2929
go.uber.org/zap v1.26.0 // indirect
3030
golang.org/x/net v0.17.0 // indirect
3131
golang.org/x/sys v0.13.0 // indirect
3232
golang.org/x/text v0.13.0 // indirect
33-
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e // indirect
33+
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
3434
gopkg.in/yaml.v2 v2.4.0 // indirect
3535
gopkg.in/yaml.v3 v3.0.1 // indirect
3636
)

go.sum

+12-12
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
2727
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
2828
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
2929
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
30-
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
31-
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
30+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
31+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
3232
github.com/ogen-go/ogen v0.76.0 h1:tdYMXVi9Khui4PDU2aTMaNcWVrTCwqjhs/xYNbI5Zsw=
3333
github.com/ogen-go/ogen v0.76.0/go.mod h1:ZPh0dhw8iSh8NtB+V0oh082H1rzSkW7S2zhY9HYnlB4=
3434
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -45,10 +45,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
4545
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
4646
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
4747
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
48-
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
49-
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
50-
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
51-
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
48+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
49+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
50+
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
51+
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
5252
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
5353
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
5454
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
@@ -59,13 +59,13 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
5959
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6060
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
6161
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
62-
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
63-
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
62+
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
63+
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
6464
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
65-
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
66-
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
67-
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e h1:z3vDksarJxsAKM5dmEGv0GHwE2hKJ096wZra71Vs4sw=
68-
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
65+
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA=
66+
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI=
67+
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k=
68+
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870=
6969
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
7070
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
7171
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"openapi":"3.1.0","info":{"title":"","version":""},"paths":{"/api/v1/items/{id}":{"get":{"operationId":"getItem","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"crud.v1.Crud.GetItem response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Item"}}}}}}}},"components":{"schemas":{"Item":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ipv4":{"type":"string","format":"ipv4"},"ipv6":{"type":"string","format":"ipv6"},"ip":{"type":"string","format":"ip"}},"required":["id","ipv4","ipv6","ip"]}}}}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
openapi: 3.1.0
2+
info:
3+
title: ""
4+
version: ""
5+
paths:
6+
/api/v1/items/{id}:
7+
get:
8+
operationId: getItem
9+
parameters:
10+
- name: id
11+
in: path
12+
required: true
13+
schema:
14+
type: string
15+
responses:
16+
"200":
17+
description: crud.v1.Crud.GetItem response
18+
content:
19+
application/json:
20+
schema:
21+
$ref: '#/components/schemas/Item'
22+
components:
23+
schemas:
24+
Item:
25+
type: object
26+
properties:
27+
id:
28+
type: string
29+
format: uuid
30+
ipv4:
31+
type: string
32+
format: ipv4
33+
ipv6:
34+
type: string
35+
format: ipv6
36+
ip:
37+
type: string
38+
format: ip
39+
required:
40+
- id
41+
- ipv4
42+
- ipv6
43+
- ip
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
proto_file: {
2+
name: "crud.proto"
3+
package: "crud.v1"
4+
message_type: {
5+
name: "Item"
6+
field: {
7+
name: "id"
8+
number: 1
9+
label: LABEL_OPTIONAL
10+
type: TYPE_STRING
11+
json_name: "id"
12+
options:{
13+
[google.api.field_behavior]:REQUIRED,
14+
[google.api.field_info]:{format:UUID4}
15+
}
16+
}
17+
field: {
18+
name: "ipv4"
19+
number: 2
20+
label: LABEL_OPTIONAL
21+
type: TYPE_STRING
22+
json_name: "ipv4"
23+
options:{
24+
[google.api.field_behavior]:REQUIRED,
25+
[google.api.field_info]:{format:IPV4}
26+
}
27+
}
28+
field: {
29+
name: "ipv6"
30+
number: 3
31+
label: LABEL_OPTIONAL
32+
type: TYPE_STRING
33+
json_name: "ipv6"
34+
options:{
35+
[google.api.field_behavior]:REQUIRED,
36+
[google.api.field_info]:{format:IPV6}
37+
}
38+
}
39+
field: {
40+
name: "ip"
41+
number: 4
42+
label: LABEL_OPTIONAL
43+
type: TYPE_STRING
44+
json_name: "ip"
45+
options:{
46+
[google.api.field_behavior]:REQUIRED,
47+
[google.api.field_info]:{format:IPV4_OR_IPV6}
48+
}
49+
}
50+
}
51+
message_type: {
52+
name: "GetItemRequest"
53+
field: {
54+
name: "id"
55+
number: 1
56+
label: LABEL_OPTIONAL
57+
type: TYPE_STRING
58+
json_name: "id"
59+
options:{
60+
[google.api.field_behavior]:REQUIRED
61+
}
62+
}
63+
}
64+
service: {
65+
name: "Crud"
66+
method: {
67+
name: "GetItem"
68+
input_type: "GetItemRequest"
69+
output_type: "Item"
70+
options: {
71+
[google.api.http]: {
72+
get: "/api/v1/items/{id}"
73+
}
74+
}
75+
}
76+
}
77+
options: {
78+
go_package: "service/v1;service"
79+
}
80+
}

internal/gen/fieldinfo.go

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package gen
2+
3+
import (
4+
"google.golang.org/genproto/googleapis/api/annotations"
5+
"google.golang.org/protobuf/proto"
6+
"google.golang.org/protobuf/reflect/protoreflect"
7+
)
8+
9+
func isFieldUUID4Format(opts protoreflect.ProtoMessage) bool {
10+
return isFieldInfoIndicator(opts, annotations.FieldInfo_UUID4)
11+
}
12+
13+
func isFieldIPV4Format(opts protoreflect.ProtoMessage) bool {
14+
return isFieldInfoIndicator(opts, annotations.FieldInfo_IPV4)
15+
}
16+
17+
func isFieldIPV6Format(opts protoreflect.ProtoMessage) bool {
18+
return isFieldInfoIndicator(opts, annotations.FieldInfo_IPV6)
19+
}
20+
21+
func isFieldIPFormat(opts protoreflect.ProtoMessage) bool {
22+
return isFieldInfoIndicator(opts, annotations.FieldInfo_IPV4_OR_IPV6)
23+
}
24+
25+
func isFieldInfoIndicator(opts protoreflect.ProtoMessage, indicator annotations.FieldInfo_Format) bool {
26+
fieldInfo, ok := proto.GetExtension(opts, annotations.E_FieldInfo).(*annotations.FieldInfo)
27+
if !ok || fieldInfo == nil {
28+
return false
29+
}
30+
31+
return fieldInfo.Format == indicator
32+
}

internal/gen/schema.go

+19-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ func (g *Generator) mkFieldSchema(fd protoreflect.FieldDescriptor, description s
135135
return ogen.NewSchema().SetType("number").SetFormat("double").SetDeprecated(isDeprecated(fd.Options())).SetDescription(mkDescription(description)), nil
136136

137137
case protoreflect.StringKind:
138-
return ogen.NewSchema().SetType("string").SetDeprecated(isDeprecated(fd.Options())).SetDescription(mkDescription(description)), nil
138+
schema := ogen.NewSchema().SetType("string").SetDeprecated(isDeprecated(fd.Options())).SetDescription(mkDescription(description))
139+
setFieldFormat(schema, fd.Options())
140+
return schema, nil
139141
case protoreflect.BytesKind:
140142
// Go's protojson encodes binary data as base64 string.
141143
//
@@ -275,3 +277,19 @@ func mkDescription(description string) (d string) {
275277
d = strings.TrimLeft(d, "/ ")
276278
return d
277279
}
280+
281+
func setFieldFormat(s *ogen.Schema, opts protoreflect.ProtoMessage) {
282+
switch {
283+
case isFieldUUID4Format(opts):
284+
s.SetFormat("uuid")
285+
286+
case isFieldIPV4Format(opts):
287+
s.SetFormat("ipv4")
288+
289+
case isFieldIPV6Format(opts):
290+
s.SetFormat("ipv6")
291+
292+
case isFieldIPFormat(opts):
293+
s.SetFormat("ip")
294+
}
295+
}

0 commit comments

Comments
 (0)