internal/api: add missing fields to openapi.yaml

The schema generator skipped embedded fields, so the Package response
schema omitted the fields promoted from PackageInfo.
Walk embedded structs and flatten their fields into the parent,
matching how encoding/json promotes them.

Fixes golang/go#79989

Change-Id: Id74a313ff4ab5667d647425ba04c5d31a57f73f0
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/790640
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
kokoro-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Ethan Lee <ethanalee@google.com>
Auto-Submit: Ethan Lee <ethanalee@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
diff --git a/internal/api/openapi.yaml b/internal/api/openapi.yaml
index c9dd9bc..3f2f9a7 100644
--- a/internal/api/openapi.yaml
+++ b/internal/api/openapi.yaml
@@ -2,7 +2,7 @@
   "openapi": "3.0.3",
   "info": {
     "title": "Go Pkgsite API",
-    "version": "v0.1.0",
+    "version": "v0.1.1",
     "description": "API for accessing information about Go packages and modules on pkg.go.dev."
   },
   "servers": [
@@ -653,6 +653,10 @@
           "isLatest": {
             "type": "boolean"
           },
+          "isRedistributable": {
+            "description": "Whether the license allows distribution.",
+            "type": "boolean"
+          },
           "isStandardLibrary": {
             "type": "boolean"
           },
@@ -665,6 +669,15 @@
           "modulePath": {
             "type": "string"
           },
+          "name": {
+            "type": "string"
+          },
+          "path": {
+            "type": "string"
+          },
+          "synopsis": {
+            "type": "string"
+          },
           "version": {
             "type": "string"
           }
diff --git a/internal/api/openapi_test.go b/internal/api/openapi_test.go
index e8a58ae..637cfb9 100644
--- a/internal/api/openapi_test.go
+++ b/internal/api/openapi_test.go
@@ -100,6 +100,34 @@
 }`,
 		},
 		{
+			name: "embedded struct",
+			data: `
+package api
+type Package struct {
+	Version string ` + "`" + `json:"version"` + "`" + `
+	PackageInfo
+}
+type PackageInfo struct {
+	Path     string ` + "`" + `json:"path"` + "`" + `
+	Synopsis string ` + "`" + `json:"synopsis"` + "`" + `
+}
+`,
+			want: `"Package": {
+    "properties": {
+      "path": {
+        "type": "string"
+      },
+      "synopsis": {
+        "type": "string"
+      },
+      "version": {
+        "type": "string"
+      }
+    },
+    "type": "object"
+  }`,
+		},
+		{
 			name: "instantiated generic",
 			data: `
 package api
@@ -209,7 +237,7 @@
 func GenerateOpenAPI() (string, error) {
 	const (
 		openAPISpecVersion = "3.0.3"
-		apiVersion         = "v0.1.0"
+		apiVersion         = "v0.1.1"
 		apiPathPrefix      = "/v1beta"
 	)
 
@@ -309,8 +337,7 @@
 		return nil, err
 	}
 
-	schemas := make(map[string]any)
-
+	structs := make(map[string]*ast.StructType)
 	for _, decl := range file.Decls {
 		genDecl, ok := decl.(*ast.GenDecl)
 		if !ok || genDecl.Tok != token.TYPE {
@@ -321,49 +348,57 @@
 			if !ok {
 				continue
 			}
-			structType, ok := typeSpec.Type.(*ast.StructType)
-			if !ok {
-				continue
-			}
-
 			typeName := typeSpec.Name.Name
-			properties := make(map[string]any)
-
-			for _, field := range structType.Fields.List {
-				if field.Names == nil {
-					continue
-				}
-
-				fieldName := field.Names[0].Name
-				tag := ""
-				if field.Tag != nil {
-					tag = field.Tag.Value
-				}
-				jsonName := extractJSONName(tag)
-				if jsonName == "" {
-					jsonName = fieldName
-				}
-
-				typeStr := typeExprToString(field.Type)
-				prop := mapFieldType(typeStr)
-				if field.Doc != nil {
-					prop["description"] = strings.TrimSpace(field.Doc.Text())
-				} else if field.Comment != nil {
-					prop["description"] = strings.TrimSpace(field.Comment.Text())
-				}
-				properties[jsonName] = prop
-			}
-
-			schemas[typeName] = map[string]any{
-				"type":       "object",
-				"properties": properties,
+			if structType, ok := typeSpec.Type.(*ast.StructType); ok {
+				structs[typeName] = structType
 			}
 		}
 	}
 
+	schemas := make(map[string]any)
+	for name, structType := range structs {
+		properties := make(map[string]any)
+		collectProperties(structType, structs, properties)
+		schemas[name] = map[string]any{
+			"type":       "object",
+			"properties": properties,
+		}
+	}
+
 	return schemas, nil
 }
 
+// collectProperties adds the schema property for each field of st to properties,
+// recursing into embedded structs so their fields are promoted to the parent.
+func collectProperties(st *ast.StructType, structs map[string]*ast.StructType, properties map[string]any) {
+	for _, field := range st.Fields.List {
+		if field.Names == nil {
+			if embedded, ok := structs[typeExprToString(field.Type)]; ok {
+				collectProperties(embedded, structs, properties)
+			}
+			continue
+		}
+
+		fieldName := field.Names[0].Name
+		tag := ""
+		if field.Tag != nil {
+			tag = field.Tag.Value
+		}
+		jsonName := extractJSONName(tag)
+		if jsonName == "" {
+			jsonName = fieldName
+		}
+
+		prop := mapFieldType(typeExprToString(field.Type))
+		if field.Doc != nil {
+			prop["description"] = strings.TrimSpace(field.Doc.Text())
+		} else if field.Comment != nil {
+			prop["description"] = strings.TrimSpace(field.Comment.Text())
+		}
+		properties[jsonName] = prop
+	}
+}
+
 func mapFieldType(t string) map[string]any {
 	switch t {
 	case "string":