feat: complete contract-driven business development and MySQL integration
This commit is contained in:
@@ -36,6 +36,8 @@ type node struct {
|
||||
}
|
||||
type field struct {
|
||||
index int
|
||||
maxBytes int64
|
||||
maxFiles int
|
||||
name, source string
|
||||
value *node
|
||||
optional bool
|
||||
@@ -113,6 +115,13 @@ func (c compiler) compile(t reflect.Type, root bool, stack map[reflect.Type]bool
|
||||
if t.Kind() != reflect.Pointer && (t.Implements(marshaler) || reflect.PointerTo(t).Implements(marshaler) || t.Implements(unmarshaler) || reflect.PointerTo(t).Implements(unmarshaler)) {
|
||||
return nil, errors.New("contract: custom JSON marshalers require explicit Scalar registration")
|
||||
}
|
||||
if t == uploadType {
|
||||
if root {
|
||||
return nil, errors.New("upload File cannot be a root DTO")
|
||||
}
|
||||
n.schema = &openapi.Schema{Type: "string", Format: "binary"}
|
||||
return n, nil
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Interface:
|
||||
if t.NumMethod() != 0 {
|
||||
@@ -223,7 +232,7 @@ func (c compiler) compileField(sf reflect.StructField, root bool, stack map[refl
|
||||
var sourceTag string
|
||||
for key, value := range tags {
|
||||
switch key {
|
||||
case "json", "path", "form", "header":
|
||||
case "json", "path", "form", "header", "file", "multipart", "multipartJSON":
|
||||
if value == "-" {
|
||||
return f, errors.New("ignored fields are not part of an API contract")
|
||||
}
|
||||
@@ -232,6 +241,8 @@ func (c compiler) compileField(sf reflect.StructField, root bool, stack map[refl
|
||||
}
|
||||
f.source = key
|
||||
sourceTag = value
|
||||
case "description", "example", "enumDescriptions":
|
||||
// Documentation is validated after the field constraints.
|
||||
case "binding":
|
||||
f.binding = value
|
||||
default:
|
||||
@@ -254,6 +265,45 @@ func (c compiler) compileField(sf reflect.StructField, root bool, stack map[refl
|
||||
return f, err
|
||||
}
|
||||
f.value = n
|
||||
if f.source == "file" {
|
||||
if sf.Type != uploadType && (sf.Type.Kind() != reflect.Slice || sf.Type.Elem() != uploadType) {
|
||||
return f, errors.New("file field requires File or []File")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, option := range parts[1:] {
|
||||
key, v, _ := strings.Cut(option, "=")
|
||||
if seen[key] {
|
||||
return f, errors.New("duplicate file option")
|
||||
}
|
||||
seen[key] = true
|
||||
switch key {
|
||||
case "optional":
|
||||
if option != key {
|
||||
return f, errors.New("optional takes no value")
|
||||
}
|
||||
f.optional = true
|
||||
case "maxBytes":
|
||||
f.maxBytes, err = fieldLimit(v)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
case "maxFiles":
|
||||
f.maxFiles, err = strconv.Atoi(v)
|
||||
if err != nil || f.maxFiles < 1 || f.maxFiles > 10000 || sf.Type.Kind() != reflect.Slice {
|
||||
return f, errors.New("maxFiles requires []File and a limit 1..10000")
|
||||
}
|
||||
default:
|
||||
return f, fmt.Errorf("unsupported file option %q", key)
|
||||
}
|
||||
}
|
||||
if f.binding != "" {
|
||||
return f, errors.New("file constraints use optional/maxBytes/maxFiles")
|
||||
}
|
||||
return f, applyDocumentation(&f, tags)
|
||||
}
|
||||
if containsUpload(n) {
|
||||
return f, errors.New("File requires file source")
|
||||
}
|
||||
opaque := base(n).scalar
|
||||
isList := base(n).typ.Kind() == reflect.Slice && opaque == nil
|
||||
if isList {
|
||||
@@ -263,25 +313,35 @@ func (c compiler) compileField(sf reflect.StructField, root bool, stack map[refl
|
||||
if isList && (f.source == "path" || f.source == "header") {
|
||||
return f, errors.New("path/header values must be scalars")
|
||||
}
|
||||
if f.source != "json" && !opaque.Text {
|
||||
if !jsonSource(f.source) && !opaque.Text {
|
||||
return f, errors.New("custom scalar needs text: true and UnmarshalText for path/query/header")
|
||||
}
|
||||
if f.binding != "" {
|
||||
return f, errors.New("custom scalar constraints belong in its decoder; use optional/pointer for presence and null")
|
||||
}
|
||||
for _, option := range parts[1:] {
|
||||
if strings.HasPrefix(option, "maxBytes=") && multipartSource(f.source) {
|
||||
f.maxBytes, err = fieldLimit(strings.TrimPrefix(option, "maxBytes="))
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if option != "optional" && option != "omitempty" {
|
||||
return f, errors.New("custom scalar supports only optional/omitempty field options")
|
||||
return f, errors.New("custom scalar supports optional/omitempty and multipart maxBytes")
|
||||
}
|
||||
f.optional = true
|
||||
}
|
||||
if f.source == "path" && (f.optional || sf.Type.Kind() == reflect.Pointer) {
|
||||
return f, errors.New("path fields must be required non-pointer values")
|
||||
}
|
||||
if err := applyDocumentation(&f, tags); err != nil {
|
||||
return f, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
scalar := base(n).typ.Kind()
|
||||
if f.source != "json" && !isScalar(scalar) && !(scalar == reflect.Slice && isScalar(base(base(n).elem).typ.Kind())) {
|
||||
if !jsonSource(f.source) && !isScalar(scalar) && !(scalar == reflect.Slice && isScalar(base(base(n).elem).typ.Kind())) {
|
||||
return f, errors.New("path/query/header values must be scalars or scalar slices")
|
||||
}
|
||||
if f.source == "path" && !isScalar(scalar) {
|
||||
@@ -303,6 +363,14 @@ func (c compiler) compileField(sf reflect.StructField, root bool, stack map[refl
|
||||
return f, errors.New("optional takes no value")
|
||||
}
|
||||
f.optional = true
|
||||
case "maxBytes":
|
||||
if !multipartSource(f.source) {
|
||||
return f, errors.New("maxBytes field option is only for multipart")
|
||||
}
|
||||
f.maxBytes, err = fieldLimit(value)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
case "default":
|
||||
if !isScalar(scalar) {
|
||||
return f, errors.New("default requires a scalar")
|
||||
@@ -354,6 +422,9 @@ func (c compiler) compileField(sf reflect.StructField, root bool, stack map[refl
|
||||
return f, fmt.Errorf("default violates validation: %w", err)
|
||||
}
|
||||
}
|
||||
if err := applyDocumentation(&f, tags); err != nil {
|
||||
return f, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
@@ -432,7 +503,7 @@ func (b *Binder) ResponseSchema() (*openapi.Schema, error) {
|
||||
func (b *Binder) Parameters() []openapi.Parameter {
|
||||
var out []openapi.Parameter
|
||||
for _, f := range b.root.fields {
|
||||
if f.source == "json" {
|
||||
if f.source == "json" || multipartSource(f.source) {
|
||||
continue
|
||||
}
|
||||
in := f.source
|
||||
@@ -445,7 +516,7 @@ func (b *Binder) Parameters() []openapi.Parameter {
|
||||
} else if base(f.value).typ.Kind() == reflect.Slice && base(base(f.value).elem).scalar != nil {
|
||||
schema.Items = &openapi.Schema{Type: "string"}
|
||||
}
|
||||
out = append(out, openapi.Parameter{Name: f.name, In: in, Required: !f.optional && !f.hasDefault, Schema: schema})
|
||||
out = append(out, openapi.Parameter{Name: f.name, In: in, Required: !f.optional && !f.hasDefault, Schema: schema, Description: f.value.schema.Description})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -455,3 +526,10 @@ func copySchema(s *openapi.Schema) *openapi.Schema {
|
||||
_ = json.Unmarshal(data, &out)
|
||||
return &out
|
||||
}
|
||||
|
||||
func containsUpload(n *node) bool {
|
||||
if n.typ == uploadType {
|
||||
return true
|
||||
}
|
||||
return n.elem != nil && containsUpload(n.elem)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user