From 7f36b758baedbc8879d157e112bef74070986043 Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Sat, 27 Apr 2024 20:05:58 +0000 Subject: [PATCH 1/7] feat(entoas): support field descriptions --- entoas/extension.go | 5 +- entoas/generator.go | 129 ++++++++++++++++++++++----------------- entoas/generator_test.go | 19 +++--- 3 files changed, 86 insertions(+), 67 deletions(-) diff --git a/entoas/extension.go b/entoas/extension.go index 5c155ec3b..040fda092 100644 --- a/entoas/extension.go +++ b/entoas/extension.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "io" + "math" "os" "path/filepath" @@ -71,8 +72,8 @@ type ( func NewExtension(opts ...ExtensionOption) (*Extension, error) { ex := &Extension{config: &Config{ DefaultPolicy: PolicyExpose, - MinItemsPerPage: one, - MaxItemsPerPage: maxu8, + MinItemsPerPage: 1, + MaxItemsPerPage: math.MaxUint8, }} for _, opt := range opts { if err := opt(ex); err != nil { diff --git a/entoas/generator.go b/entoas/generator.go index 0a2d6f8ee..e543b7047 100644 --- a/entoas/generator.go +++ b/entoas/generator.go @@ -444,7 +444,7 @@ func listOp(spec *ogen.Spec, n *gen.Type) (*ogen.Operation, error) { InQuery(). SetName("page"). SetDescription("what page to render"). - SetSchema(ogen.Int().SetMinimum(&one)), + SetSchema(ogen.Int().SetMinimum(ptr(int64(1)))), ogen.NewParameter(). InQuery(). SetName("itemsPerPage"). @@ -524,40 +524,57 @@ func property(f *gen.Field) (*ogen.Property, error) { return ogen.NewProperty().SetName(f.Name).SetSchema(s), nil } -var ( - zero int64 - one int64 = 1 - min8 int64 = math.MinInt8 - max8 int64 = math.MaxInt8 - maxu8 int64 = math.MaxUint8 - min16 int64 = math.MinInt16 - max16 int64 = math.MaxInt16 - maxu16 int64 = math.MaxUint16 - maxu32 int64 = math.MaxUint32 - types = map[string]*ogen.Schema{ - "bool": ogen.Bool(), - "time.Time": ogen.DateTime(), - "string": ogen.String(), - "[]byte": ogen.Bytes(), - "uuid.UUID": ogen.UUID(), - "int": ogen.Int(), - "int8": ogen.Int32().SetMinimum(&min8).SetMaximum(&max8), - "int16": ogen.Int32().SetMinimum(&min16).SetMaximum(&max16), - "int32": ogen.Int32(), - "uint": ogen.Int64().SetMinimum(&zero).SetMaximum(&maxu32), - "uint8": ogen.Int32().SetMinimum(&zero).SetMaximum(&maxu8), - "uint16": ogen.Int32().SetMinimum(&zero).SetMaximum(&maxu16), - "uint32": ogen.Int64().SetMinimum(&zero).SetMaximum(&maxu32), - "int64": ogen.Int64(), - "uint64": ogen.Int64().SetMinimum(&zero), - "float32": ogen.Float(), - "float64": ogen.Double(), +// ptr returns a pointer to v, for the purposes of base types (int, string, etc). +func ptr[T any](v T) *T { + return &v +} + +// mapTypeToSchema returns an ogen.Schema for the given gen.Field, if it exists. +// returns nil if the type is not supported. +func mapTypeToSchema(baseType string) *ogen.Schema { + switch baseType { + case "bool": + return ogen.Bool() + case "time.Time": + return ogen.DateTime() + case "string": + return ogen.String() + case "[]byte": + return ogen.Bytes() + case "uuid.UUID": + return ogen.UUID() + case "int": + return ogen.Int() + case "int8": + return ogen.Int32().SetMinimum(ptr(int64(math.MinInt8))).SetMaximum(ptr(int64(math.MaxInt8))) + case "int16": + return ogen.Int32().SetMinimum(ptr(int64(math.MinInt16))).SetMaximum(ptr(int64(math.MaxInt16))) + case "int32": + return ogen.Int32().SetMinimum(ptr(int64(math.MinInt32))).SetMaximum(ptr(int64(math.MaxInt32))) + case "int64": + return ogen.Int64().SetMinimum(ptr(int64(math.MinInt64))).SetMaximum(ptr(int64(math.MaxInt64))) + case "uint": + return ogen.Int64().SetMinimum(ptr(int64(0))).SetMaximum(ptr(int64(math.MaxUint32))) + case "uint8": + return ogen.Int32().SetMinimum(ptr(int64(0))).SetMaximum(ptr(int64(math.MaxUint8))) + case "uint16": + return ogen.Int32().SetMinimum(ptr(int64(0))).SetMaximum(ptr(int64(math.MaxUint16))) + case "uint32": + return ogen.Int64().SetMinimum(ptr(int64(0))).SetMaximum(ptr(int64(math.MaxUint32))) + case "uint64": + return ogen.Int64().SetMinimum(ptr(int64(0))) + case "float32": + return ogen.Float() + case "float64": + return ogen.Double() + default: + return nil } -) +} // OgenSchema returns the ogen.Schema to use for the given gen.Field. func OgenSchema(f *gen.Field) (*ogen.Schema, error) { - // If there is a custom property given on the field use it. + // If there is a custom property/schema given on the field, use it. ant, err := FieldAnnotation(f) if err != nil { return nil, err @@ -565,6 +582,10 @@ func OgenSchema(f *gen.Field) (*ogen.Schema, error) { if ant.Schema != nil { return ant.Schema, nil } + + var schema *ogen.Schema + baseType := f.Type.String() + // Enum values need special case. if f.IsEnum() { var d json.RawMessage @@ -581,20 +602,31 @@ func OgenSchema(f *gen.Field) (*ogen.Schema, error) { return nil, err } } - return ogen.String().AsEnum(d, vs...), nil + schema = ogen.String().AsEnum(d, vs...) } - s := f.Type.String() - // Handle slice types. - if strings.HasPrefix(s, "[]") { - if t, ok := types[s[2:]]; ok { - return t.AsArray(), nil + + if schema == nil { + if strings.HasPrefix(baseType, "[]") { // Handle slice types. + schema = mapTypeToSchema(baseType[2:]) + if schema != nil { + schema = schema.AsArray() + } + } + + if schema == nil { + schema = mapTypeToSchema(baseType) } } - t, ok := types[s] - if !ok { - return nil, fmt.Errorf("no OAS-type exists for type %q of field %s", s, f.StructField()) + + if schema == nil { + return nil, fmt.Errorf("no OAS-type exists for type %q of field %s", baseType, f.StructField()) + } + + if schema.Description == "" { + schema.Description = f.Comment() } - return t, nil + + return schema, nil } // NodeOperations returns the list of operations to expose for this node. @@ -733,21 +765,6 @@ func reqBody(n *gen.Type, op Operation, allowClientUUIDs bool) (*ogen.RequestBod return req, nil } -// // exampleValue returns the user defined example value for the ent schema field. -// func exampleValue(f *gen.Field) (interface{}, error) { -// a, err := FieldAnnotation(f) -// if err != nil { -// return nil, err -// } -// if a != nil && a.Example != nil { -// return a.Example, err -// } -// if f.IsEnum() { -// return f.EnumValues()[0], nil -// } -// return nil, nil -// } - // contains checks if a string slice contains the given value. func contains(xs []Operation, s Operation) bool { for _, x := range xs { diff --git a/entoas/generator_test.go b/entoas/generator_test.go index 9d9425a20..01049375f 100644 --- a/entoas/generator_test.go +++ b/entoas/generator_test.go @@ -18,6 +18,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "math" "net/http" "net/url" "testing" @@ -36,15 +37,15 @@ func TestOgenSchema(t *testing.T) { for d, ex := range map[*entfield.Descriptor]*ogen.Schema{ // Numeric entfield.Int("int").Descriptor(): ogen.Int(), - entfield.Int8("int8").Descriptor(): ogen.Int32().SetMinimum(&min8).SetMaximum(&max8), - entfield.Int16("int16").Descriptor(): ogen.Int32().SetMinimum(&min16).SetMaximum(&max16), - entfield.Int32("int32").Descriptor(): ogen.Int32(), - entfield.Int64("int64").Descriptor(): ogen.Int64(), - entfield.Uint("uint").Descriptor(): ogen.Int64().SetMinimum(&zero).SetMaximum(&maxu32), - entfield.Uint8("uint8").Descriptor(): ogen.Int32().SetMinimum(&zero).SetMaximum(&maxu8), - entfield.Uint16("uint16").Descriptor(): ogen.Int32().SetMinimum(&zero).SetMaximum(&maxu16), - entfield.Uint32("uint32").Descriptor(): ogen.Int64().SetMinimum(&zero).SetMaximum(&maxu32), - entfield.Uint64("uint64").Descriptor(): ogen.Int64().SetMinimum(&zero), + entfield.Int8("int8").Descriptor(): ogen.Int32().SetMinimum(ptr(int64(math.MinInt8))).SetMaximum(ptr(int64(math.MaxInt8))), + entfield.Int16("int16").Descriptor(): ogen.Int32().SetMinimum(ptr(int64(math.MinInt16))).SetMaximum(ptr(int64(math.MaxInt16))), + entfield.Int32("int32").Descriptor(): ogen.Int32().SetMinimum(ptr(int64(math.MinInt32))).SetMaximum(ptr(int64(math.MaxInt32))), + entfield.Int64("int64").Descriptor(): ogen.Int64().SetMinimum(ptr(int64(math.MinInt64))).SetMaximum(ptr(int64(math.MaxInt64))), + entfield.Uint("uint").Descriptor(): ogen.Int64().SetMinimum(ptr(int64(0))).SetMaximum(ptr(int64(math.MaxUint32))), + entfield.Uint8("uint8").Descriptor(): ogen.Int32().SetMinimum(ptr(int64(0))).SetMaximum(ptr(int64(math.MaxUint8))), + entfield.Uint16("uint16").Descriptor(): ogen.Int32().SetMinimum(ptr(int64(0))).SetMaximum(ptr(int64(math.MaxUint16))), + entfield.Uint32("uint32").Descriptor(): ogen.Int64().SetMinimum(ptr(int64(0))).SetMaximum(ptr(int64(math.MaxUint32))), + entfield.Uint64("uint64").Descriptor(): ogen.Int64().SetMinimum(ptr(int64(0))), entfield.Float32("float32").Descriptor(): ogen.Float(), entfield.Float("float64").Descriptor(): ogen.Double(), // Basic From 43071d4c26afb1bc28b4f4394165cd9946503617 Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Sat, 27 Apr 2024 20:13:52 +0000 Subject: [PATCH 2/7] fix(entoas): Example annotation now provides an example for fields --- entoas/annotation.go | 9 +++++---- entoas/extension.go | 2 +- entoas/generator.go | 7 +++++++ entoas/generator_test.go | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/entoas/annotation.go b/entoas/annotation.go index 2759bc72e..e0f71b8f6 100644 --- a/entoas/annotation.go +++ b/entoas/annotation.go @@ -29,7 +29,7 @@ type ( // Groups holds the serialization groups to use on this field / edge. Groups serialization.Groups // OpenAPI Specification example value for a schema field. - Example interface{} + Example any // OpenAPI Specification schema to use for a schema field. Schema *ogen.Schema // Create has meta information about a creation operation. @@ -71,8 +71,9 @@ func OperationPolicy(p Policy) OperationConfigOption { return func(c *OperationConfig) { c.Policy = p } } -// Example returns an example annotation. -func Example(v interface{}) Annotation { return Annotation{Example: v} } +// Example returns an example annotation on a field. This is meant to show an example value of +// what the field would look like. +func Example(v any) Annotation { return Annotation{Example: v} } // Schema returns a Schema annotation. func Schema(s *ogen.Schema) Annotation { return Annotation{Schema: s} } @@ -166,7 +167,7 @@ func (op *OperationConfig) merge(other OperationConfig) { } // Decode from ent. -func (a *Annotation) Decode(o interface{}) error { +func (a *Annotation) Decode(o any) error { buf, err := json.Marshal(o) if err != nil { return err diff --git a/entoas/extension.go b/entoas/extension.go index 040fda092..4637c6808 100644 --- a/entoas/extension.go +++ b/entoas/extension.go @@ -224,7 +224,7 @@ func (c Config) Name() string { } // Decode from ent. -func (c *Config) Decode(o interface{}) error { +func (c *Config) Decode(o any) error { buf, err := json.Marshal(o) if err != nil { return err diff --git a/entoas/generator.go b/entoas/generator.go index e543b7047..9e012aaae 100644 --- a/entoas/generator.go +++ b/entoas/generator.go @@ -626,6 +626,13 @@ func OgenSchema(f *gen.Field) (*ogen.Schema, error) { schema.Description = f.Comment() } + if ant.Example != nil { + schema.Example, err = json.Marshal(ant.Example) + if err != nil { + return nil, fmt.Errorf("failed to marshal example for field %s: %w", f.StructField(), err) + } + } + return schema, nil } diff --git a/entoas/generator_test.go b/entoas/generator_test.go index 01049375f..d48b5434a 100644 --- a/entoas/generator_test.go +++ b/entoas/generator_test.go @@ -122,7 +122,7 @@ func DefaultLink() *Link { } // Scan implements the Scanner interface. -func (l *Link) Scan(value interface{}) (err error) { +func (l *Link) Scan(value any) (err error) { switch v := value.(type) { case nil: case []byte: From 31b54c6568d3d79b0218acf2bf67527da3d2a909 Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Sat, 27 Apr 2024 20:25:58 +0000 Subject: [PATCH 3/7] chore(entoas): regenerate examples --- entoas/internal/oastypes/openapi.json | 24 ++++++--- entoas/internal/pets/openapi.json | 60 +++++++++++++++-------- entoas/internal/simple/category.go | 6 +-- entoas/internal/simple/openapi.json | 37 +++++++++++--- entoas/internal/simple/pet.go | 6 +-- entoas/internal/simple/schema/category.go | 13 ++--- entoas/internal/simple/schema/pet.go | 9 ++-- entoas/internal/simple/schema/user.go | 4 +- entoas/internal/simple/user.go | 4 +- 9 files changed, 112 insertions(+), 51 deletions(-) diff --git a/entoas/internal/oastypes/openapi.json b/entoas/internal/oastypes/openapi.json index 8c7173e12..ca3f69dd5 100644 --- a/entoas/internal/oastypes/openapi.json +++ b/entoas/internal/oastypes/openapi.json @@ -94,11 +94,15 @@ }, "int32": { "type": "integer", - "format": "int32" + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648 }, "int64": { "type": "integer", - "format": "int64" + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808 }, "uint": { "type": "integer", @@ -391,11 +395,15 @@ }, "int32": { "type": "integer", - "format": "int32" + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648 }, "int64": { "type": "integer", - "format": "int64" + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808 }, "uint": { "type": "integer", @@ -566,11 +574,15 @@ }, "int32": { "type": "integer", - "format": "int32" + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648 }, "int64": { "type": "integer", - "format": "int64" + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808 }, "uint": { "type": "integer", diff --git a/entoas/internal/pets/openapi.json b/entoas/internal/pets/openapi.json index 953547a2f..6f982f02a 100644 --- a/entoas/internal/pets/openapi.json +++ b/entoas/internal/pets/openapi.json @@ -400,7 +400,8 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -409,7 +410,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 }, "categories": { "type": "array", @@ -564,7 +566,8 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -573,7 +576,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 }, "categories": { "type": "array", @@ -1215,7 +1219,8 @@ "type": "integer" }, "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -1224,7 +1229,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 } }, "required": [ @@ -1239,7 +1245,8 @@ "type": "integer" }, "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -1248,7 +1255,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 }, "categories": { "type": "array", @@ -1278,7 +1286,8 @@ "type": "integer" }, "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -1287,7 +1296,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 } }, "required": [ @@ -1302,7 +1312,8 @@ "type": "integer" }, "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -1311,7 +1322,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 } }, "required": [ @@ -1323,7 +1335,8 @@ "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -1332,7 +1345,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 }, "owner": { "$ref": "#/components/schemas/PetRead_Owner" @@ -1368,7 +1382,8 @@ "type": "integer" }, "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -1377,7 +1392,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 } }, "required": [ @@ -1407,7 +1423,8 @@ "type": "integer" }, "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -1416,7 +1433,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 } }, "required": [ @@ -1551,7 +1569,8 @@ "type": "integer" }, "name": { - "type": "string" + "type": "string", + "example": "Kuro" }, "nicknames": { "type": "array", @@ -1560,7 +1579,8 @@ } }, "age": { - "type": "integer" + "type": "integer", + "example": 1 } }, "required": [ diff --git a/entoas/internal/simple/category.go b/entoas/internal/simple/category.go index f4f974e2e..d1390720d 100644 --- a/entoas/internal/simple/category.go +++ b/entoas/internal/simple/category.go @@ -16,11 +16,11 @@ type Category struct { config `json:"-"` // ID of the ent. ID int `json:"id,omitempty"` - // Name holds the value of the "name" field. + // Name of the category. Name string `json:"name,omitempty"` - // Readonly holds the value of the "readonly" field. + // If the category is read-only and cannot be modified. Readonly string `json:"readonly,omitempty"` - // SkipInSpec holds the value of the "skip_in_spec" field. + // This field should be skipped in the spec. SkipInSpec string `json:"skip_in_spec,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the CategoryQuery when eager-loading is set. diff --git a/entoas/internal/simple/openapi.json b/entoas/internal/simple/openapi.json index 639e2f068..5e1d6a10c 100644 --- a/entoas/internal/simple/openapi.json +++ b/entoas/internal/simple/openapi.json @@ -78,6 +78,7 @@ "type": "object", "properties": { "name": { + "description": "Name of the category.", "type": "string" }, "pets": { @@ -224,6 +225,7 @@ "type": "object", "properties": { "name": { + "description": "Name of the category.", "type": "string" }, "pets": { @@ -400,16 +402,21 @@ "type": "object", "properties": { "name": { - "type": "string" + "description": "Name of the pet.", + "type": "string", + "example": "Kuro" }, "nicknames": { + "description": "Various nicknames of the pet.", "type": "array", "items": { "type": "string" } }, "age": { - "type": "integer" + "description": "Age of the pet.", + "type": "integer", + "example": 1 }, "categories": { "type": "array", @@ -564,16 +571,21 @@ "type": "object", "properties": { "name": { - "type": "string" + "description": "Name of the pet.", + "type": "string", + "example": "Kuro" }, "nicknames": { + "description": "Various nicknames of the pet.", "type": "array", "items": { "type": "string" } }, "age": { - "type": "integer" + "description": "Age of the pet.", + "type": "integer", + "example": 1 }, "categories": { "type": "array", @@ -867,9 +879,11 @@ "type": "object", "properties": { "name": { + "description": "The users full name.", "type": "string" }, "age": { + "description": "The age of the user.", "type": "integer" }, "pets": { @@ -1017,9 +1031,11 @@ "type": "object", "properties": { "name": { + "description": "The users full name.", "type": "string" }, "age": { + "description": "The age of the user.", "type": "integer" }, "pets": { @@ -1134,9 +1150,11 @@ "type": "integer" }, "name": { + "description": "Name of the category.", "type": "string" }, "readonly": { + "description": "If the category is read-only and cannot be modified.", "type": "string" }, "pets": { @@ -1159,16 +1177,21 @@ "type": "integer" }, "name": { - "type": "string" + "description": "Name of the pet.", + "type": "string", + "example": "Kuro" }, "nicknames": { + "description": "Various nicknames of the pet.", "type": "array", "items": { "type": "string" } }, "age": { - "type": "integer" + "description": "Age of the pet.", + "type": "integer", + "example": 1 }, "categories": { "type": "array", @@ -1198,9 +1221,11 @@ "type": "integer" }, "name": { + "description": "The users full name.", "type": "string" }, "age": { + "description": "The age of the user.", "type": "integer" }, "pets": { diff --git a/entoas/internal/simple/pet.go b/entoas/internal/simple/pet.go index 0a598695d..d2852061a 100644 --- a/entoas/internal/simple/pet.go +++ b/entoas/internal/simple/pet.go @@ -18,11 +18,11 @@ type Pet struct { config `json:"-"` // ID of the ent. ID int `json:"id,omitempty"` - // Name holds the value of the "name" field. + // Name of the pet. Name string `json:"name,omitempty"` - // Nicknames holds the value of the "nicknames" field. + // Various nicknames of the pet. Nicknames []string `json:"nicknames,omitempty"` - // Age holds the value of the "age" field. + // Age of the pet. Age int `json:"age,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the PetQuery when eager-loading is set. diff --git a/entoas/internal/simple/schema/category.go b/entoas/internal/simple/schema/category.go index 3f6981abc..d251b4173 100644 --- a/entoas/internal/simple/schema/category.go +++ b/entoas/internal/simple/schema/category.go @@ -15,11 +15,10 @@ package schema import ( + "entgo.io/contrib/entoas" "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" - - "entgo.io/contrib/entoas" ) // Category holds the schema definition for the Category entity. @@ -30,11 +29,13 @@ type Category struct { // Fields of the Category. func (Category) Fields() []ent.Field { return []ent.Field{ - field.String("name"), + field.String("name").Comment("Name of the category."), field.String("readonly"). - Annotations( - entoas.Annotation{ReadOnly: true}), - field.String("skip_in_spec").Annotations(entoas.Annotation{Skip: true}), + Annotations(entoas.Annotation{ReadOnly: true}). + Comment("If the category is read-only and cannot be modified."), + field.String("skip_in_spec"). + Annotations(entoas.Annotation{Skip: true}). + Comment("This field should be skipped in the spec."), } } diff --git a/entoas/internal/simple/schema/pet.go b/entoas/internal/simple/schema/pet.go index 889dfeaea..1db97546f 100644 --- a/entoas/internal/simple/schema/pet.go +++ b/entoas/internal/simple/schema/pet.go @@ -36,16 +36,19 @@ func (Pet) Fields() []ent.Field { Annotations( entoas.Groups("pet"), entoas.Example("Kuro"), - ), + ). + Comment("Name of the pet."), field.JSON("nicknames", []string{}). Optional(). - Annotations(entoas.Groups("pet")), + Annotations(entoas.Groups("pet")). + Comment("Various nicknames of the pet."), field.Int("age"). Optional(). Annotations( entoas.Groups("pet"), entoas.Example(1), - ), + ). + Comment("Age of the pet."), } } diff --git a/entoas/internal/simple/schema/user.go b/entoas/internal/simple/schema/user.go index b36b75f38..ae89e33cf 100644 --- a/entoas/internal/simple/schema/user.go +++ b/entoas/internal/simple/schema/user.go @@ -28,8 +28,8 @@ type User struct { // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ - field.String("name"), - field.Int("age"), + field.String("name").Comment("The users full name."), + field.Int("age").Comment("The age of the user."), } } diff --git a/entoas/internal/simple/user.go b/entoas/internal/simple/user.go index a98233cb5..57ca1d07b 100644 --- a/entoas/internal/simple/user.go +++ b/entoas/internal/simple/user.go @@ -16,9 +16,9 @@ type User struct { config `json:"-"` // ID of the ent. ID int `json:"id,omitempty"` - // Name holds the value of the "name" field. + // The users full name. Name string `json:"name,omitempty"` - // Age holds the value of the "age" field. + // The age of the user. Age int `json:"age,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the UserQuery when eager-loading is set. From 7999c9485fc313fb92d7506a8205da68ae210bb2 Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Sun, 28 Apr 2024 07:13:44 +0000 Subject: [PATCH 4/7] fix(entoas): don't set required on field when it has a default value - also skip including both an edge and it's associated field, if the field isn't skipped and a field is actually associated to the edge --- entoas/generator.go | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/entoas/generator.go b/entoas/generator.go index 9e012aaae..3694466a1 100644 --- a/entoas/generator.go +++ b/entoas/generator.go @@ -755,10 +755,14 @@ func reqBody(n *gen.Type, op Operation, allowClientUUIDs bool) (*ogen.RequestBod if err != nil { return nil, err } - addProperty(c, p, op == OpCreate && !f.Optional) + addProperty(c, p, op == OpCreate && !f.Optional && !f.Default) } } for _, e := range n.Edges { + if op == OpUpdate && (e.Immutable || (e.Field() != nil && e.Field().Immutable)) { + continue + } + s, err := OgenSchema(e.Type.ID) if err != nil { return nil, err @@ -766,7 +770,29 @@ func reqBody(n *gen.Type, op Operation, allowClientUUIDs bool) (*ogen.RequestBod if !e.Unique { s = s.AsArray() } - addProperty(c, s.ToProperty(e.Name), op == OpCreate && !e.Optional) + + if e.Field() != nil { + f := e.Field() + a, err := FieldAnnotation(f) + if err != nil { + return nil, err + } + + if a.ReadOnly { + continue + } + + if !a.Skip { + // If the edge has a field, and the field isn't skipped, then there is no + // point in having two fields that can be used during create (especially + // if both are required). + continue + } + + addProperty(c, s.ToProperty(e.Name), (op == OpCreate && !e.Optional && !f.Default) || (op == OpUpdate && !f.UpdateDefault)) + } else { + addProperty(c, s.ToProperty(e.Name), (op == OpCreate && !e.Optional)) + } } req.SetJSONContent(c) return req, nil From 50b29f89dd680398dd73ca203c184d8a066b07ab Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Sun, 28 Apr 2024 07:21:32 +0000 Subject: [PATCH 5/7] chore(entoas): regenerate examples (fix default values) --- entoas/internal/oastypes/openapi.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/entoas/internal/oastypes/openapi.json b/entoas/internal/oastypes/openapi.json index ca3f69dd5..993436e65 100644 --- a/entoas/internal/oastypes/openapi.json +++ b/entoas/internal/oastypes/openapi.json @@ -231,7 +231,6 @@ "float64", "string_field", "bool", - "uuid", "time", "text", "state", @@ -242,7 +241,6 @@ "nicknames", "json_slice", "json_obj", - "other", "nillable" ] } From 8ba4ad5e7c622f2a10db914864461bc7768f741f Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Sun, 28 Apr 2024 07:52:45 +0000 Subject: [PATCH 6/7] fix(entoas): inclusive edge tags and use edge comments as descriptions --- entoas/generator.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/entoas/generator.go b/entoas/generator.go index 3694466a1..cff02f69d 100644 --- a/entoas/generator.go +++ b/entoas/generator.go @@ -359,6 +359,17 @@ func readEdgeOp(spec *ogen.Spec, n *gen.Type, e *gen.Edge) (*ogen.Operation, err spec.RefResponse(strconv.Itoa(http.StatusNotFound)), spec.RefResponse(strconv.Itoa(http.StatusInternalServerError)), ) + + // If edge has a comment, override summary/description. + if e.Comment() != "" { + op = op.SetSummary(e.Comment()).SetDescription(e.Comment()) + } + + // If the edge is a different type than the node, add the edge type as a tag. + if n.Name != e.Type.Name { + op = op.AddTags(e.Type.Name) + } + return op, nil } @@ -512,6 +523,17 @@ func listEdgeOp(spec *ogen.Spec, n *gen.Type, e *gen.Edge) (*ogen.Operation, err spec.RefResponse(strconv.Itoa(http.StatusNotFound)), spec.RefResponse(strconv.Itoa(http.StatusInternalServerError)), ) + + // If edge has a comment, override summary/description. + if e.Comment() != "" { + op = op.SetSummary(e.Comment()).SetDescription(e.Comment()) + } + + // If the edge is a different type than the node, add the edge type as a tag. + if n.Name != e.Type.Name { + op = op.AddTags(e.Type.Name) + } + return op, nil } From 6e09be6c2ad17f04726a1b1b5320f14e9edced1a Mon Sep 17 00:00:00 2001 From: Liam Stanley Date: Sun, 28 Apr 2024 08:00:13 +0000 Subject: [PATCH 7/7] chore(entoas): regenerate examples (include edge tags and edge descriptions) --- entoas/internal/pets/openapi.json | 12 ++++++--- entoas/internal/simple/category.go | 2 +- entoas/internal/simple/openapi.json | 32 +++++++++++++---------- entoas/internal/simple/pet.go | 6 ++--- entoas/internal/simple/schema/category.go | 2 +- entoas/internal/simple/schema/pet.go | 9 ++++--- entoas/internal/simple/schema/user.go | 2 +- entoas/internal/simple/user.go | 2 +- 8 files changed, 39 insertions(+), 28 deletions(-) diff --git a/entoas/internal/pets/openapi.json b/entoas/internal/pets/openapi.json index 6f982f02a..3db964eb0 100644 --- a/entoas/internal/pets/openapi.json +++ b/entoas/internal/pets/openapi.json @@ -267,7 +267,8 @@ "/categories/{id}/pets": { "get": { "tags": [ - "Category" + "Category", + "Pet" ], "summary": "List attached Pets", "description": "List attached Pets.", @@ -629,7 +630,8 @@ "/pets/{id}/categories": { "get": { "tags": [ - "Pet" + "Pet", + "Category" ], "summary": "List attached Categories", "description": "List attached Categories.", @@ -757,7 +759,8 @@ "/pets/{id}/owner": { "get": { "tags": [ - "Pet" + "Pet", + "User" ], "summary": "Find the attached User", "description": "Find the attached User of the Pet with the given ID", @@ -1067,7 +1070,8 @@ "/users/{id}/pets": { "get": { "tags": [ - "User" + "User", + "Pet" ], "summary": "List attached Pets", "description": "List attached Pets.", diff --git a/entoas/internal/simple/category.go b/entoas/internal/simple/category.go index d1390720d..1dcd18cb4 100644 --- a/entoas/internal/simple/category.go +++ b/entoas/internal/simple/category.go @@ -30,7 +30,7 @@ type Category struct { // CategoryEdges holds the relations/edges for other nodes in the graph. type CategoryEdges struct { - // Pets holds the value of the pets edge. + // Pets that belong in this category. Pets []*Pet `json:"pets,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. diff --git a/entoas/internal/simple/openapi.json b/entoas/internal/simple/openapi.json index 5e1d6a10c..64f2c69f1 100644 --- a/entoas/internal/simple/openapi.json +++ b/entoas/internal/simple/openapi.json @@ -269,10 +269,11 @@ "/categories/{id}/pets": { "get": { "tags": [ - "Category" + "Category", + "Pet" ], - "summary": "List attached Pets", - "description": "List attached Pets.", + "summary": "Pets that belong in this category.", + "description": "Pets that belong in this category.", "operationId": "listCategoryPets", "parameters": [ { @@ -637,10 +638,11 @@ "/pets/{id}/categories": { "get": { "tags": [ - "Pet" + "Pet", + "Category" ], - "summary": "List attached Categories", - "description": "List attached Categories.", + "summary": "Categories for which the pet belongs to.", + "description": "Categories for which the pet belongs to.", "operationId": "listPetCategories", "parameters": [ { @@ -703,8 +705,8 @@ "tags": [ "Pet" ], - "summary": "List attached Friends", - "description": "List attached Friends.", + "summary": "Friends of the pet.", + "description": "Friends of the pet.", "operationId": "listPetFriends", "parameters": [ { @@ -765,10 +767,11 @@ "/pets/{id}/owner": { "get": { "tags": [ - "Pet" + "Pet", + "User" ], - "summary": "Find the attached User", - "description": "Find the attached User of the Pet with the given ID", + "summary": "Owner of the pet.", + "description": "Owner of the pet.", "operationId": "readPetOwner", "parameters": [ { @@ -1079,10 +1082,11 @@ "/users/{id}/pets": { "get": { "tags": [ - "User" + "User", + "Pet" ], - "summary": "List attached Pets", - "description": "List attached Pets.", + "summary": "The pets that the user owns.", + "description": "The pets that the user owns.", "operationId": "listUserPets", "parameters": [ { diff --git a/entoas/internal/simple/pet.go b/entoas/internal/simple/pet.go index d2852061a..88cb1bf90 100644 --- a/entoas/internal/simple/pet.go +++ b/entoas/internal/simple/pet.go @@ -33,11 +33,11 @@ type Pet struct { // PetEdges holds the relations/edges for other nodes in the graph. type PetEdges struct { - // Categories holds the value of the categories edge. + // Categories for which the pet belongs to. Categories []*Category `json:"categories,omitempty"` - // Owner holds the value of the owner edge. + // Owner of the pet. Owner *User `json:"owner,omitempty"` - // Friends holds the value of the friends edge. + // Friends of the pet. Friends []*Pet `json:"friends,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. diff --git a/entoas/internal/simple/schema/category.go b/entoas/internal/simple/schema/category.go index d251b4173..0f40dfe24 100644 --- a/entoas/internal/simple/schema/category.go +++ b/entoas/internal/simple/schema/category.go @@ -42,6 +42,6 @@ func (Category) Fields() []ent.Field { // Edges of the Category. func (Category) Edges() []ent.Edge { return []ent.Edge{ - edge.To("pets", Pet.Type), + edge.To("pets", Pet.Type).Comment("Pets that belong in this category."), } } diff --git a/entoas/internal/simple/schema/pet.go b/entoas/internal/simple/schema/pet.go index 1db97546f..a28285186 100644 --- a/entoas/internal/simple/schema/pet.go +++ b/entoas/internal/simple/schema/pet.go @@ -56,14 +56,17 @@ func (Pet) Fields() []ent.Field { func (Pet) Edges() []ent.Edge { return []ent.Edge{ edge.From("categories", Category.Type). - Ref("pets"), + Ref("pets"). + Comment("Categories for which the pet belongs to."), edge.From("owner", User.Type). Ref("pets"). Unique(). Annotations( entoas.Groups("pet:list", "pet:read", "test:edge", "test:view"), - ), - edge.To("friends", Pet.Type), + ). + Comment("Owner of the pet."), + edge.To("friends", Pet.Type). + Comment("Friends of the pet."), } } diff --git a/entoas/internal/simple/schema/user.go b/entoas/internal/simple/schema/user.go index ae89e33cf..e4203261f 100644 --- a/entoas/internal/simple/schema/user.go +++ b/entoas/internal/simple/schema/user.go @@ -36,6 +36,6 @@ func (User) Fields() []ent.Field { // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ - edge.To("pets", Pet.Type), + edge.To("pets", Pet.Type).Comment("The pets that the user owns."), } } diff --git a/entoas/internal/simple/user.go b/entoas/internal/simple/user.go index 57ca1d07b..473aa336e 100644 --- a/entoas/internal/simple/user.go +++ b/entoas/internal/simple/user.go @@ -28,7 +28,7 @@ type User struct { // UserEdges holds the relations/edges for other nodes in the graph. type UserEdges struct { - // Pets holds the value of the pets edge. + // The pets that the user owns. Pets []*Pet `json:"pets,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not.