Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(entoas): add field descriptions and fix examples #577

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
9 changes: 5 additions & 4 deletions entoas/annotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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} }
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions entoas/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"encoding/json"
"errors"
"io"
"math"
"os"
"path/filepath"

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -223,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
Expand Down
188 changes: 130 additions & 58 deletions entoas/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -444,7 +455,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").
Expand Down Expand Up @@ -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
}

Expand All @@ -524,47 +546,68 @@ 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
}
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
Expand All @@ -581,20 +624,38 @@ 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()
}

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 t, nil

return schema, nil
}

// NodeOperations returns the list of operations to expose for this node.
Expand Down Expand Up @@ -716,38 +777,49 @@ 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
}
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
}

// // 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 {
Expand Down
21 changes: 11 additions & 10 deletions entoas/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"database/sql/driver"
"encoding/json"
"fmt"
"math"
"net/http"
"net/url"
"testing"
Expand All @@ -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
Expand Down Expand Up @@ -121,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:
Expand Down
Loading
Loading