| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 611503e commit f2e468c
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -80,6 +80,18 @@ type BindStyledParameterOptions struct { | |||
| 80 | 80 | // When set to "byte" and the destination is []byte, the value is | |
| 81 | 81 | // base64-decoded rather than treated as a generic slice. | |
| 82 | 82 | Format string | |
| 83 | + // Types is the OpenAPI 3.1 multi-type union member list of the parameter | ||
| 84 | + // (e.g. ["string", "integer"]). It is only consulted when the | ||
| 85 | + // destination is an empty interface (`any`): the value binds to the | ||
| 86 | + // first member that parses, trying boolean, integer, number, then | ||
| 87 | + // string, with numeric detection following the JSON number production. | ||
| 88 | + // The bound value's dynamic type is one of exactly bool, int64, float64, | ||
| 89 | + // string, or, with Format "byte", []byte (widths narrow only under the | ||
| 90 | + // NarrowUnionNumericFormats package variable). Concrete destinations ignore | ||
| 91 | + // it and keep the reflection-driven behavior; arrays of unions and | ||
| 92 | + // deepObject-style binding are not covered. See | ||
| 93 | + // BindStringToObjectOptions.Types for the full semantics and scope. | ||
| 94 | + Types []string | ||
| 83 | 95 | // AllowReserved, when true, indicates that the parameter value may | |
| 84 | 96 | // contain RFC 3986 reserved characters without percent-encoding. | |
| 85 | 97 | AllowReserved bool | |
@@ -193,7 +205,11 @@ func BindStyledParameterWithOptions(style string, paramName string, value string | |||
| 193 | 205 | } | |
| 194 | 206 | value = parts[0] | |
| 195 | 207 | } | |
| 196 | - return BindStringToObject(value, dest) | ||
| 208 | + return BindStringToObjectWithOptions(value, dest, BindStringToObjectOptions{ | ||
| 209 | + Type: opts.Type, | ||
| 210 | + Format: opts.Format, | ||
| 211 | + Types: opts.Types, | ||
| 212 | + }) | ||
| 197 | 213 | } | |
| 198 | 214 | ||
| 199 | 215 | // This is a complex set of operations, but each given parameter style can be | |
@@ -386,6 +402,18 @@ type BindQueryParameterOptions struct { | |||
| 386 | 402 | // When set to "byte" and the destination is []byte, the value is | |
| 387 | 403 | // base64-decoded rather than treated as a generic slice. | |
| 388 | 404 | Format string | |
| 405 | + // Types is the OpenAPI 3.1 multi-type union member list of the parameter | ||
| 406 | + // (e.g. ["string", "integer"]). It is only consulted when the | ||
| 407 | + // destination is an empty interface (`any`): the value binds to the | ||
| 408 | + // first member that parses, trying boolean, integer, number, then | ||
| 409 | + // string, with numeric detection following the JSON number production. | ||
| 410 | + // The bound value's dynamic type is one of exactly bool, int64, float64, | ||
| 411 | + // string, or, with Format "byte", []byte (widths narrow only under the | ||
| 412 | + // NarrowUnionNumericFormats package variable). Concrete destinations ignore | ||
| 413 | + // it and keep the reflection-driven behavior; arrays of unions and | ||
| 414 | + // deepObject-style binding are not covered. See | ||
| 415 | + // BindStringToObjectOptions.Types for the full semantics and scope. | ||
| 416 | + Types []string | ||
| 389 | 417 | // AllowReserved, when true, indicates that the parameter value may | |
| 390 | 418 | // contain RFC 3986 reserved characters without percent-encoding. | |
| 391 | 419 | AllowReserved bool | |
@@ -520,7 +548,11 @@ func BindQueryParameterWithOptions(style string, explode bool, required bool, pa | |||
| 520 | 548 | return nil | |
| 521 | 549 | } | |
| 522 | 550 | } | |
| 523 | - err = BindStringToObject(values[0], output) | ||
| 551 | + err = BindStringToObjectWithOptions(values[0], output, BindStringToObjectOptions{ | ||
| 552 | + Type: opts.Type, | ||
| 553 | + Format: opts.Format, | ||
| 554 | + Types: opts.Types, | ||
| 555 | + }) | ||
| 524 | 556 | } | |
| 525 | 557 | if err != nil { | |
| 526 | 558 | return err | |
@@ -552,7 +584,11 @@ func BindQueryParameterWithOptions(style string, explode bool, required bool, pa | |||
| 552 | 584 | // is only meaningful for array and object types. | |
| 553 | 585 | // See: https://swagger.io/docs/specification/serialization/ | |
| 554 | 586 | if k != reflect.Slice && k != reflect.Struct && k != reflect.Map { | |
| 555 | - err := BindStringToObject(values[0], output) | ||
| 587 | + err := BindStringToObjectWithOptions(values[0], output, BindStringToObjectOptions{ | ||
| 588 | + Type: opts.Type, | ||
| 589 | + Format: opts.Format, | ||
| 590 | + Types: opts.Types, | ||
| 591 | + }) | ||
| 556 | 592 | if err != nil { | |
| 557 | 593 | return err | |
| 558 | 594 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -42,6 +42,28 @@ type BindStringToObjectOptions struct { | |||
| 42 | 42 | // When set to "byte" and the destination is []byte, the source string is | |
| 43 | 43 | // base64-decoded rather than treated as a generic slice. | |
| 44 | 44 | Format string | |
| 45 | + // Types is the OpenAPI 3.1 multi-type union member list of the parameter | ||
| 46 | + // (e.g. ["string", "integer"]). A "null" entry — the 3.1 nullability | ||
| 47 | + // marker, not a union member — is ignored, whether or not the generator | ||
| 48 | + // already stripped it. (Type, which the runtime does not currently read, | ||
| 49 | + // carries no meaning when Types is set.) | ||
| 50 | + // | ||
| 51 | + // Types is only consulted when the destination is an empty interface | ||
| 52 | + // (`any`): the source string is bound to the first member that parses, | ||
| 53 | + // trying boolean, integer, number, then string (most restrictive grammar | ||
| 54 | + // first — the always-succeeding string member would otherwise shadow the | ||
| 55 | + // rest). Numeric detection uses the JSON number production (RFC 8259 | ||
| 56 | + // section 6), so tokens like "007" and "+1" bind as strings. The bound | ||
| 57 | + // value's dynamic type is one of exactly bool, int64, float64, string, | ||
| 58 | + // or, with Format "byte", []byte; width formats (int32, float, ...) are | ||
| 59 | + // annotation-only unless the application opts into narrowing via the | ||
| 60 | + // NarrowUnionNumericFormats package variable. | ||
| 61 | + // | ||
| 62 | + // Concrete destinations ignore this field and keep the reflection-driven | ||
| 63 | + // behavior. Array element binding does not yet support unions, and | ||
| 64 | + // deepObject-style binding does not consult this field (its JSON decode | ||
| 65 | + // path produces float64 for all numbers). | ||
| 66 | + Types []string | ||
| 45 | 67 | } | |
| 46 | 68 | ||
| 47 | 69 | // BindStringToObjectWithOptions takes a string, and attempts to assign it to the destination | |
@@ -190,6 +212,22 @@ func BindStringToObjectWithOptions(src string, dst interface{}, opts BindStringT | |||
| 190 | 212 | // We fall through to the error case below if we haven't handled the | |
| 191 | 213 | // destination type above. | |
| 192 | 214 | fallthrough | |
| 215 | + case reflect.Interface: | ||
| 216 | + // An interface destination normally can't be bound: there is no | ||
| 217 | + // type information to parse with, so it falls to the error below. | ||
| 218 | + // The exception is an empty interface (`any`) destination for a | ||
| 219 | + // declared OpenAPI 3.1 multi-type union — opts.Types names the | ||
| 220 | + // member types, and the value binds to the first member that | ||
| 221 | + // parses. See bindStringToUnionMember for the exact semantics. | ||
| 222 | + if t.Kind() == reflect.Interface && t.NumMethod() == 0 && len(opts.Types) > 0 { | ||
| 223 | + bound, bindErr := bindStringToUnionMember(src, opts) | ||
| 224 | + if bindErr != nil { | ||
| 225 | + return fmt.Errorf("error binding string parameter: %w", bindErr) | ||
| 226 | + } | ||
| 227 | + v.Set(reflect.ValueOf(bound)) | ||
| 228 | + return nil | ||
| 229 | + } | ||
| 230 | + fallthrough | ||
| 193 | 231 | case reflect.Map: | |
| 194 | 232 | // A bool-keyed map (such as nullable.Nullable[T], which is | |
| 195 | 233 | // map[bool]T) is treated as a nullable wrapper: bind src into a | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,217 @@ | |||
| 1 | + package runtime | ||
| 2 | + | ||
| 3 | + import ( | ||
| 4 | + "fmt" | ||
| 5 | + "strconv" | ||
| 6 | + "strings" | ||
| 7 | + ) | ||
| 8 | + | ||
| 9 | + // NarrowUnionNumericFormats controls whether numeric width formats narrow | ||
| 10 | + // the dynamic type produced when a multi-type union parameter is bound into | ||
| 11 | + // an `any` destination. | ||
| 12 | + // | ||
| 13 | + // When false (the default), the bound value's dynamic type is always one of | ||
| 14 | + // bool, int64, float64, string, or []byte, regardless of the schema's | ||
| 15 | + // `format`: an edit to a spec's format can never change the types a running | ||
| 16 | + // handler's type switch sees. When true, `format: int32` produces int32 | ||
| 17 | + // (values outside int32 range fall through to the next union member) and | ||
| 18 | + // `format: float` produces float32, widening the possible dynamic types to | ||
| 19 | + // bool, int32, int64, float32, float64, string, and []byte. `int64` and | ||
| 20 | + // `double` name the defaults either way. Formats never affect concrete | ||
| 21 | + // (non-`any`) destinations, whose Go type was fixed at generation time. | ||
| 22 | + // | ||
| 23 | + // Note one asymmetry with concrete destinations: a concrete int32 | ||
| 24 | + // destination rejects an out-of-range value with an overflow error, but an | ||
| 25 | + // `any` destination binds it to the next union member instead — typically | ||
| 26 | + // the string member, verbatim. Enabling narrowing to get int32 typing | ||
| 27 | + // therefore also accepts that silent widening; a handler that needs | ||
| 28 | + // out-of-range values rejected must check for the string case itself. | ||
| 29 | + // | ||
| 30 | + // Like DefaultQueryEncoder, set it once during program initialization; it is | ||
| 31 | + // not safe to mutate concurrently with in-flight requests. The opt-in lives | ||
| 32 | + // here rather than in generated code because the trade-off belongs to | ||
| 33 | + // whoever owns the handler's type switch: enabling it is a promise that the | ||
| 34 | + // application handles the narrowed types. | ||
| 35 | + var NarrowUnionNumericFormats bool | ||
| 36 | + | ||
| 37 | + // unionMemberOrder is the order in which union member types are attempted | ||
| 38 | + // when binding a parameter value into an `any` destination: most restrictive | ||
| 39 | + // grammar first, so that the always-succeeding string member cannot shadow | ||
| 40 | + // the others. This is deliberately NOT the schema's declaration order — JSON | ||
| 41 | + // Schema defines the `type` array as an unordered set, so declaration order | ||
| 42 | + // carries no meaning, and any tool that normalizes a spec could otherwise | ||
| 43 | + // silently change binding behavior. The "null" nullability marker and | ||
| 44 | + // non-scalar names ("array", "object") never appear here, so they are | ||
| 45 | + // structurally skipped during the walk regardless of what the generator | ||
| 46 | + // emitted in Types. | ||
| 47 | + var unionMemberOrder = [4]string{"boolean", "integer", "number", "string"} | ||
| 48 | + | ||
| 49 | + // bindStringToUnionMember binds src against the members of an OpenAPI 3.1 | ||
| 50 | + // multi-type union (opts.Types), returning the value of the first member | ||
| 51 | + // that parses. Members are tried in unionMemberOrder, restricted to the | ||
| 52 | + // members actually present in opts.Types. | ||
| 53 | + // | ||
| 54 | + // Numeric detection uses the JSON number production (RFC 8259 section 6), | ||
| 55 | + // not strconv leniency: "007", "+1" and " 1" are not JSON numbers, so they | ||
| 56 | + // fall through to the string member rather than being silently | ||
| 57 | + // reinterpreted. | ||
| 58 | + // | ||
| 59 | + // The dynamic type of the returned value is one of exactly bool, int64, | ||
| 60 | + // float64, string, or — with Format "byte" — []byte. Width formats (int32, | ||
| 61 | + // int64, float, double) are annotation-only by default and do not narrow | ||
| 62 | + // the produced type: honoring them would mean an edit to a spec's `format` | ||
| 63 | + // silently changes the dynamic type a running handler's type switch sees, | ||
| 64 | + // with no compile error. Applications that want width narrowing opt in via | ||
| 65 | + // the NarrowUnionNumericFormats package variable. "byte" is always | ||
| 66 | + // load-bearing because it changes the wire decoding (base64) rather than a | ||
| 67 | + // width; other annotation-only formats (date-time, uuid, ...) are ignored — | ||
| 68 | + // per OpenAPI 3.1 semantics `format` is an annotation and must not reject a | ||
| 69 | + // value, so parse failure cannot discriminate members. A format whose host | ||
| 70 | + // type is not present in opts.Types is inert. | ||
| 71 | + // | ||
| 72 | + // Non-scalar member names ("array", "object"), the "null" nullability marker | ||
| 73 | + // and unknown names are skipped: styled serialization of those into `any` | ||
| 74 | + // has no defined meaning. If no member parses, an error naming the union's | ||
| 75 | + // bindable members is returned. | ||
| 76 | + func bindStringToUnionMember(src string, opts BindStringToObjectOptions) (any, error) { | ||
| 77 | + for _, name := range unionMemberOrder { | ||
| 78 | + if !unionHasMember(opts.Types, name) { | ||
| 79 | + continue | ||
| 80 | + } | ||
| 81 | + switch name { | ||
| 82 | + case "boolean": | ||
| 83 | + // JSON grammar: exactly the lowercase literals, unlike | ||
| 84 | + // strconv.ParseBool which also accepts "1", "t", "TRUE", etc. | ||
| 85 | + if src == "true" { | ||
| 86 | + return true, nil | ||
| 87 | + } | ||
| 88 | + if src == "false" { | ||
| 89 | + return false, nil | ||
| 90 | + } | ||
| 91 | + case "integer": | ||
| 92 | + if isJSONInteger(src) { | ||
| 93 | + if NarrowUnionNumericFormats && opts.Format == "int32" { | ||
| 94 | + if val, err := strconv.ParseInt(src, 10, 32); err == nil { | ||
| 95 | + return int32(val), nil | ||
| 96 | + } | ||
| 97 | + } else if val, err := strconv.ParseInt(src, 10, 64); err == nil { | ||
| 98 | + return val, nil | ||
| 99 | + } | ||
| 100 | + // Overflow of the (possibly narrowed) width: not | ||
| 101 | + // representable as this member, fall through to the next | ||
| 102 | + // one (number takes it as a float, string takes it | ||
| 103 | + // verbatim). | ||
| 104 | + } | ||
| 105 | + case "number": | ||
| 106 | + if isJSONNumber(src) { | ||
| 107 | + if NarrowUnionNumericFormats && opts.Format == "float" { | ||
| 108 | + if val, err := strconv.ParseFloat(src, 32); err == nil { | ||
| 109 | + return float32(val), nil | ||
| 110 | + } | ||
| 111 | + } else if val, err := strconv.ParseFloat(src, 64); err == nil { | ||
| 112 | + return val, nil | ||
| 113 | + } | ||
| 114 | + // Out of range for the (possibly narrowed) width: fall | ||
| 115 | + // through. | ||
| 116 | + } | ||
| 117 | + case "string": | ||
| 118 | + if opts.Format == "byte" { | ||
| 119 | + // Consistent with the concrete []byte destination: a | ||
| 120 | + // declared base64 wire encoding that doesn't decode is an | ||
| 121 | + // error, not a silent fallback to the raw string. | ||
| 122 | + // base64Decode's error already names the offending value. | ||
| 123 | + return base64Decode(src) | ||
| 124 | + } | ||
| 125 | + return src, nil | ||
| 126 | + } | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + // Name only the bindable members in the error: the generator is expected | ||
| 130 | + // to strip the "null" nullability marker before emitting Types, but the | ||
| 131 | + // runtime and generator version independently, so don't rely on it. | ||
| 132 | + members := make([]string, 0, len(opts.Types)) | ||
| 133 | + for _, name := range opts.Types { | ||
| 134 | + if name != "null" { | ||
| 135 | + members = append(members, name) | ||
| 136 | + } | ||
| 137 | + } | ||
| 138 | + if len(members) == 0 { | ||
| 139 | + // Degenerate input (e.g. Types: ["null"]): say so instead of | ||
| 140 | + // printing "type union []", which would read as a runtime bug. | ||
| 141 | + return nil, fmt.Errorf("value '%s' can not bind: type union has no bindable members (declared %v)", src, opts.Types) | ||
| 142 | + } | ||
| 143 | + return nil, fmt.Errorf("value '%s' does not match any member of type union %v", src, members) | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + // unionHasMember reports whether name appears in types. A linear scan: the | ||
| 147 | + // list has at most a handful of entries and this runs per parameter per | ||
| 148 | + // request, so avoiding a map allocation matters more than big-O. | ||
| 149 | + func unionHasMember(types []string, name string) bool { | ||
| 150 | + for _, t := range types { | ||
| 151 | + if t == name { | ||
| 152 | + return true | ||
| 153 | + } | ||
| 154 | + } | ||
| 155 | + return false | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + // isJSONNumber reports whether s is a number under JSON grammar (RFC 8259): | ||
| 159 | + // an optional leading '-', an integer part with no leading zeros, and | ||
| 160 | + // optional fraction and exponent parts. No '+' sign, no whitespace, no hex. | ||
| 161 | + func isJSONNumber(s string) bool { | ||
| 162 | + i := 0 | ||
| 163 | + if i < len(s) && s[i] == '-' { | ||
| 164 | + i++ | ||
| 165 | + } | ||
| 166 | + // Integer part: "0", or a nonzero digit followed by digits. | ||
| 167 | + if i >= len(s) { | ||
| 168 | + return false | ||
| 169 | + } | ||
| 170 | + switch { | ||
| 171 | + case s[i] == '0': | ||
| 172 | + i++ | ||
| 173 | + case s[i] >= '1' && s[i] <= '9': | ||
| 174 | + i++ | ||
| 175 | + for i < len(s) && isDigit(s[i]) { | ||
| 176 | + i++ | ||
| 177 | + } | ||
| 178 | + default: | ||
| 179 | + return false | ||
| 180 | + } | ||
| 181 | + // Fraction part. | ||
| 182 | + if i < len(s) && s[i] == '.' { | ||
| 183 | + i++ | ||
| 184 | + if i >= len(s) || !isDigit(s[i]) { | ||
| 185 | + return false | ||
| 186 | + } | ||
| 187 | + for i < len(s) && isDigit(s[i]) { | ||
| 188 | + i++ | ||
| 189 | + } | ||
| 190 | + } | ||
| 191 | + // Exponent part. | ||
| 192 | + if i < len(s) && (s[i] == 'e' || s[i] == 'E') { | ||
| 193 | + i++ | ||
| 194 | + if i < len(s) && (s[i] == '+' || s[i] == '-') { | ||
| 195 | + i++ | ||
| 196 | + } | ||
| 197 | + if i >= len(s) || !isDigit(s[i]) { | ||
| 198 | + return false | ||
| 199 | + } | ||
| 200 | + for i < len(s) && isDigit(s[i]) { | ||
| 201 | + i++ | ||
| 202 | + } | ||
| 203 | + } | ||
| 204 | + return i == len(s) | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + // isJSONInteger reports whether s is an integer token under JSON grammar: a | ||
| 208 | + // JSON number with no fraction or exponent part. This deliberately rejects | ||
| 209 | + // strconv leniencies like "007" or "+1", which would silently change the | ||
| 210 | + // value ("007" binds as the string "007", not the integer 7). | ||
| 211 | + func isJSONInteger(s string) bool { | ||
| 212 | + return isJSONNumber(s) && !strings.ContainsAny(s, ".eE") | ||
| 213 | + } | ||
| 214 | + | ||
| 215 | + func isDigit(c byte) bool { | ||
| 216 | + return c >= '0' && c <= '9' | ||
| 217 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments