Repository: ogen-go/ogen
Pull request #356 (2022-05-23 14:42:18)

feat(openapi): parse encoding field of MediaType


Patch:
diff --git a/gen/gen_contents.go b/gen/gen_contents.go
index 2e621a03..fe24de9a 100644
--- a/gen/gen_contents.go
+++ b/gen/gen_contents.go
@@ -99,13 +99,24 @@ func (g *Generator) generateContents(
 				return nil
 
 			case "application/x-www-form-urlencoded":
+				if s := media.Schema; s != nil && (s.AdditionalProperties != nil || len(s.PatternProperties) > 0) {
+					return &ErrNotImplemented{"complex urlencoded schema"}
+				}
+				if len(media.Encoding) > 0 {
+					return &ErrNotImplemented{"urlencoded encoding"}
+				}
+
 				t, err := g.generateSchema(ctx, typeName, media.Schema)
 				if err != nil {
 					return errors.Wrap(err, "generate schema")
 				}
+
 				if !t.IsStruct() {
 					return errors.Wrapf(&ErrNotImplemented{"urlencoded schema type"}, "%s", t.Kind)
 				}
+				if err := isParamAllowed(t, true, map[*ir.Type]struct{}{}); err != nil {
+					return err
+				}
 
 				t.AddFeature("urlencoded")
 				t, err = boxType(ctx, ir.GenericVariant{
diff --git a/openapi/mediatype.go b/openapi/mediatype.go
index a1a92b40..420bb460 100644
--- a/openapi/mediatype.go
+++ b/openapi/mediatype.go
@@ -11,5 +11,12 @@ type MediaType struct {
 	Schema   *jsonschema.Schema
 	Example  json.RawMessage
 	Examples map[string]*Example
-	// Encoding map[string]*Encoding
+	Encoding map[string]*Encoding
+}
+
+// Encoding is Encoding Type Object.
+type Encoding struct {
+	Style         ParameterStyle
+	Explode       bool
+	AllowReserved bool
 }
diff --git a/openapi/operation.go b/openapi/operation.go
index 0a72cab7..4e7e6dcf 100644
--- a/openapi/operation.go
+++ b/openapi/operation.go
@@ -47,8 +47,8 @@ type PathPart struct {
 type RequestBody struct {
 	Ref         string
 	Description string
-	Content     map[string]*MediaType
 	Required    bool
+	Content     map[string]*MediaType
 }
 
 // Header is an OpenAPI Header definition.
diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go
index d001a3e8..23765feb 100644
--- a/openapi/parser/parse_mediatype.go
+++ b/openapi/parser/parse_mediatype.go
@@ -4,6 +4,7 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
 )
 
@@ -29,6 +30,40 @@ func (p *parser) parseMediaType(m ogen.Media) (*openapi.MediaType, error) {
 		return nil, errors.Wrap(err, "schema")
 	}
 
+	encodings := make(map[string]*openapi.Encoding, len(m.Encoding))
+	if s != nil && len(m.Encoding) > 0 {
+		names := make(map[string]jsonschema.Property, len(s.Properties))
+		for _, prop := range s.Properties {
+			names[prop.Name] = prop
+		}
+
+		for name, e := range m.Encoding {
+			prop, ok := names[name]
+			if !ok {
+				return nil, errors.Errorf("unknown encoding prop %q", name)
+			}
+
+			encoding := &openapi.Encoding{
+				Style:         inferParamStyle(openapi.LocationQuery, e.Style),
+				Explode:       inferParamExplode(openapi.LocationQuery, e.Explode),
+				AllowReserved: e.AllowReserved,
+			}
+			encodings[name] = encoding
+
+			if err := validateParamStyle(&openapi.Parameter{
+				Name:     name,
+				Schema:   prop.Schema,
+				Content:  nil,
+				In:       openapi.LocationQuery,
+				Style:    encoding.Style,
+				Explode:  encoding.Explode,
+				Required: prop.Required,
+			}); err != nil {
+				return nil, err
+			}
+		}
+	}
+
 	examples := make(map[string]*openapi.Example, len(m.Examples))
 	for name, ex := range m.Examples {
 		examples[name], err = p.parseExample(ex, resolveCtx{})
@@ -53,5 +88,6 @@ func (p *parser) parseMediaType(m ogen.Media) (*openapi.MediaType, error) {
 		Schema:   s,
 		Example:  m.Example,
 		Examples: examples,
+		Encoding: encodings,
 	}, nil
 }
diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go
index 7feb9d1c..11d7f8f6 100644
--- a/openapi/parser/parse_parameter.go
+++ b/openapi/parser/parse_parameter.go
@@ -194,12 +194,12 @@ func validateParamStyle(p *openapi.Parameter) error {
 
 	styles, ok := table[p.In]
 	if !ok {
-		return errors.Errorf("invalid style: %q", p.Style)
+		return errors.Errorf("invalid in: %q", p.In)
 	}
 
 	types, ok := styles[stexp{p.Style, p.Explode}]
 	if !ok {
-		return errors.Errorf("invalid style explode combination")
+		return errors.Errorf("invalid style explode combination %q, explode:%v", p.Style, p.Explode)
 	}
 
 	allowed := func(t byte) bool { return types&t != 0 }
@@ -224,7 +224,7 @@ func validateParamStyle(p *openapi.Parameter) error {
 				case jsonschema.String, jsonschema.Integer, jsonschema.Number, jsonschema.Boolean:
 					// ok
 				default:
-					return errors.Errorf("all oneOf schemas must be simple types")
+					return errors.New("all oneOf schemas must be simple types")
 				}
 			}
 
@@ -234,5 +234,5 @@ func validateParamStyle(p *openapi.Parameter) error {
 		}
 	}
 
-	return errors.Errorf("invalid schema:style:explode combination")
+	return errors.New("invalid schema:style:explode combination")
 }
diff --git a/spec.go b/spec.go
index 9f06487b..24e57321 100644
--- a/spec.go
+++ b/spec.go
@@ -279,6 +279,30 @@ type Media struct {
 	Schema   *Schema             `json:"schema,omitempty"`
 	Example  json.RawMessage     `json:"example,omitempty"`
 	Examples map[string]*Example `json:"examples,omitempty"`
+
+	// A map between a property name and its encoding information. The key, being the property name, MUST exist in
+	// the schema as a property. The encoding object SHALL only apply to requestBody objects when the media
+	// type is multipart or application/x-www-form-urlencoded.
+	Encoding map[string]Encoding `json:"encoding,omitempty"`
+}
+
+// Encoding describes single encoding definition applied to a single schema property.
+type Encoding struct {
+	// Describes how the parameter value will be serialized
+	// depending on the type of the parameter value.
+	Style string `json:"style,omitempty"`
+
+	// When this is true, parameter values of type array or object
+	// generate separate parameters for each value of the array
+	// or key-value pair of the map.
+	// For other types of parameters this property has no effect.
+	Explode *bool `json:"explode,omitempty"`
+
+	// Determines whether the parameter value SHOULD allow reserved characters, as defined by
+	// RFC3986 :/?#[]@!$&'()*+,;= to be included without percent-encoding.
+	// The default value is false. This property SHALL be ignored if the request body media type
+	// is not application/x-www-form-urlencoded.
+	AllowReserved bool `json:"allowReserved,omitempty"`
 }
 
 // Discriminator discriminates types for OneOf, AllOf, AnyOf.


Test patch:
diff --git a/_testdata/negative/duplicate_parameter.json b/_testdata/negative/duplicate_parameter.json
index c03e0911..38637dcd 100644
--- a/_testdata/negative/duplicate_parameter.json
+++ b/_testdata/negative/duplicate_parameter.json
@@ -35,7 +35,10 @@
       "Parameter": {
         "name": "foo",
         "in": "query",
-        "style": "form"
+        "style": "form",
+        "schema": {
+          "type": "string"
+        }
       }
     }
   }
diff --git a/_testdata/negative/optional_path_parameter.json b/_testdata/negative/optional_path_parameter.json
index f2a605e0..0dc2cda5 100644
--- a/_testdata/negative/optional_path_parameter.json
+++ b/_testdata/negative/optional_path_parameter.json
@@ -32,7 +32,10 @@
       "Parameter": {
         "name": "parm",
         "in": "path",
-        "required": false
+        "required": false,
+        "schema": {
+          "type": "string"
+        }
       }
     }
   }
diff --git a/_testdata/negative/unknown_encoding_in.json b/_testdata/negative/unknown_encoding_in.json
new file mode 100644
index 00000000..d95a72f5
--- /dev/null
+++ b/_testdata/negative/unknown_encoding_in.json
@@ -0,0 +1,45 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/healthz": {
+      "post": {
+        "requestBody": {
+          "content": {
+            "application/x-www-form-urlencoded": {
+              "encoding": {
+                "foobar": {
+                  "in": "query1",
+                  "style": "form"
+                }
+              },
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "foobar": {
+                    "type": "string"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "User info",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/unknown_encoding_property.json b/_testdata/negative/unknown_encoding_property.json
new file mode 100644
index 00000000..8a926c3c
--- /dev/null
+++ b/_testdata/negative/unknown_encoding_property.json
@@ -0,0 +1,43 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/healthz": {
+      "post": {
+        "requestBody": {
+          "content": {
+            "application/x-www-form-urlencoded": {
+              "encoding": {
+                "bar": {
+                }
+              },
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "foo": {
+                    "type": "string"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "User info",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/unknown_encoding_style.json b/_testdata/negative/unknown_encoding_style.json
new file mode 100644
index 00000000..67c4e111
--- /dev/null
+++ b/_testdata/negative/unknown_encoding_style.json
@@ -0,0 +1,44 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/healthz": {
+      "post": {
+        "requestBody": {
+          "content": {
+            "application/x-www-form-urlencoded": {
+              "encoding": {
+                "foobar": {
+                  "style": "matrix"
+                }
+              },
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "foobar": {
+                    "type": "string"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "User info",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/unknown_parameter_in.json b/_testdata/negative/unknown_parameter_in.json
index 1213ebe6..98074f49 100644
--- a/_testdata/negative/unknown_parameter_in.json
+++ b/_testdata/negative/unknown_parameter_in.json
@@ -9,7 +9,12 @@
       "get": {
         "parameters": [
           {
-            "$ref": "#/components/parameters/Parameter"
+            "name": "foo",
+            "in": "query1",
+            "style": "form",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "responses": {
@@ -26,12 +31,5 @@
         }
       }
     }
-  },
-  "components": {
-    "parameters": {
-      "Parameter": {
-        "in": "foo"
-      }
-    }
   }
 }
diff --git a/_testdata/negative/unknown_parameter_style.json b/_testdata/negative/unknown_parameter_style.json
index 943f3828..413802c7 100644
--- a/_testdata/negative/unknown_parameter_style.json
+++ b/_testdata/negative/unknown_parameter_style.json
@@ -9,7 +9,12 @@
       "get": {
         "parameters": [
           {
-            "$ref": "#/components/parameters/Parameter"
+            "name": "foo",
+            "in": "header",
+            "style": "matrix",
+            "schema": {
+              "type": "string"
+            }
           }
         ],
         "responses": {
@@ -26,14 +31,5 @@
         }
       }
     }
-  },
-  "components": {
-    "parameters": {
-      "Parameter": {
-        "name": "foo",
-        "in": "query",
-        "style": "foo"
-      }
-    }
   }
 }