chore: release v0.4.1
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package rudder
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type schema struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Nullable bool `json:"nullable,omitempty"`
|
||||
Properties map[string]*schema `json:"properties,omitempty"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
Items *schema `json:"items,omitempty"`
|
||||
AdditionalProperties *schema `json:"additionalProperties,omitempty"`
|
||||
Enum []any `json:"enum,omitempty"`
|
||||
Minimum *float64 `json:"minimum,omitempty"`
|
||||
Maximum *float64 `json:"maximum,omitempty"`
|
||||
MinLength *uint64 `json:"minLength,omitempty"`
|
||||
MaxLength *uint64 `json:"maxLength,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeFor[time.Time]()
|
||||
jsonMarshalerType = reflect.TypeFor[json.Marshaler]()
|
||||
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
||||
)
|
||||
|
||||
func mustSchema(label string, t reflect.Type) json.RawMessage {
|
||||
body, err := json.Marshal(schemaOf(t, map[reflect.Type]bool{}))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("rudder: %s schema: %v", label, err))
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func schemaOf(t reflect.Type, visiting map[reflect.Type]bool) *schema {
|
||||
if t.Kind() == reflect.Pointer {
|
||||
element := schemaOf(t.Elem(), visiting)
|
||||
element.Nullable = true
|
||||
return element
|
||||
}
|
||||
if t == timeType {
|
||||
return &schema{Type: "string", Format: "date-time"}
|
||||
}
|
||||
if t.Implements(jsonMarshalerType) || reflect.PointerTo(t).Implements(jsonMarshalerType) {
|
||||
return &schema{}
|
||||
}
|
||||
if t.Implements(textMarshalerType) || reflect.PointerTo(t).Implements(textMarshalerType) {
|
||||
return &schema{Type: "string"}
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
return &schema{Type: "boolean"}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return &schema{Type: "integer"}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
zero := 0.0
|
||||
return &schema{Type: "integer", Minimum: &zero}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return &schema{Type: "number"}
|
||||
case reflect.String:
|
||||
return &schema{Type: "string"}
|
||||
case reflect.Interface:
|
||||
return &schema{}
|
||||
case reflect.Slice:
|
||||
if t.Elem().Kind() == reflect.Uint8 {
|
||||
return &schema{Type: "string", Format: "byte"}
|
||||
}
|
||||
return &schema{Type: "array", Items: schemaOf(t.Elem(), visiting)}
|
||||
case reflect.Array:
|
||||
return &schema{Type: "array", Items: schemaOf(t.Elem(), visiting)}
|
||||
case reflect.Map:
|
||||
if t.Elem().Kind() == reflect.Interface {
|
||||
return &schema{}
|
||||
}
|
||||
return &schema{Type: "object", AdditionalProperties: schemaOf(t.Elem(), visiting)}
|
||||
case reflect.Struct:
|
||||
object := &schema{Type: "object", Properties: map[string]*schema{}}
|
||||
addFields(object, t, visiting)
|
||||
slices.Sort(object.Required)
|
||||
return object
|
||||
}
|
||||
panic(fmt.Sprintf("rudder: type %s cannot be encoded as JSON", t))
|
||||
}
|
||||
|
||||
func addFields(object *schema, t reflect.Type, visiting map[reflect.Type]bool) {
|
||||
if visiting[t] {
|
||||
panic(fmt.Sprintf("rudder: recursive type %s is not supported in function arguments and results", t))
|
||||
}
|
||||
visiting[t] = true
|
||||
defer delete(visiting, t)
|
||||
var embedded []reflect.Type
|
||||
for field := range t.Fields() {
|
||||
tag := field.Tag.Get("json")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
name, options, _ := strings.Cut(tag, ",")
|
||||
fieldType := field.Type
|
||||
if field.Anonymous && name == "" {
|
||||
if fieldType.Kind() == reflect.Pointer {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
if fieldType.Kind() == reflect.Struct {
|
||||
embedded = append(embedded, fieldType)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
if name == "" {
|
||||
name = field.Name
|
||||
}
|
||||
property := schemaOf(fieldType, visiting)
|
||||
applyConstraints(property, t.String()+"."+field.Name, field.Tag.Get("rudder"))
|
||||
object.Properties[name] = property
|
||||
if !strings.Contains(","+options+",", ",omitempty,") && !strings.Contains(","+options+",", ",omitzero,") {
|
||||
object.Required = append(object.Required, name)
|
||||
}
|
||||
}
|
||||
for _, embeddedType := range embedded {
|
||||
inner := &schema{Properties: map[string]*schema{}}
|
||||
addFields(inner, embeddedType, visiting)
|
||||
required := map[string]bool{}
|
||||
for _, name := range inner.Required {
|
||||
required[name] = true
|
||||
}
|
||||
for name, property := range inner.Properties {
|
||||
if _, ok := object.Properties[name]; ok {
|
||||
continue
|
||||
}
|
||||
object.Properties[name] = property
|
||||
if required[name] {
|
||||
object.Required = append(object.Required, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyConstraints(property *schema, field string, tag string) {
|
||||
for tag != "" {
|
||||
var entry string
|
||||
if strings.HasPrefix(tag, "pattern=") {
|
||||
entry, tag = tag, ""
|
||||
} else {
|
||||
entry, tag, _ = strings.Cut(tag, ",")
|
||||
}
|
||||
key, value, _ := strings.Cut(entry, "=")
|
||||
if err := applyConstraint(property, key, value); err != nil {
|
||||
panic(fmt.Sprintf("rudder: invalid rudder tag on %s: %s: %v", field, entry, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyConstraint(property *schema, key string, value string) error {
|
||||
numeric := property.Type == "integer" || property.Type == "number"
|
||||
switch key {
|
||||
case "min", "max":
|
||||
if !numeric {
|
||||
return fmt.Errorf("%s needs an integer or number field, got %q", key, property.Type)
|
||||
}
|
||||
bound, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key == "min" {
|
||||
property.Minimum = &bound
|
||||
} else {
|
||||
property.Maximum = &bound
|
||||
}
|
||||
case "minLength", "maxLength":
|
||||
if property.Type != "string" {
|
||||
return fmt.Errorf("%s needs a string field, got %q", key, property.Type)
|
||||
}
|
||||
length, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key == "minLength" {
|
||||
property.MinLength = &length
|
||||
} else {
|
||||
property.MaxLength = &length
|
||||
}
|
||||
case "pattern":
|
||||
if property.Type != "string" {
|
||||
return fmt.Errorf("pattern needs a string field, got %q", property.Type)
|
||||
}
|
||||
if _, err := regexp.Compile(value); err != nil {
|
||||
return err
|
||||
}
|
||||
property.Pattern = value
|
||||
case "format":
|
||||
if property.Type == "" || value == "" {
|
||||
return fmt.Errorf("format needs a typed field and a value")
|
||||
}
|
||||
property.Format = value
|
||||
case "enum":
|
||||
for option := range strings.SplitSeq(value, "|") {
|
||||
switch property.Type {
|
||||
case "string":
|
||||
property.Enum = append(property.Enum, option)
|
||||
case "integer":
|
||||
parsed, err := strconv.ParseInt(option, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
property.Enum = append(property.Enum, parsed)
|
||||
case "number":
|
||||
parsed, err := strconv.ParseFloat(option, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
property.Enum = append(property.Enum, parsed)
|
||||
default:
|
||||
return fmt.Errorf("enum needs a string, integer or number field, got %q", property.Type)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown constraint %q; use min, max, minLength, maxLength, pattern, enum or format", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user