Repository: goadesign/goa
Pull request #3480 (2024-02-20 16:12:49)

Add openapi:json:indent Meta
This feature makes it possible to reduce the differences in regenerated JSON.

raphael: This is great, thank you!

Patch:
diff --git a/dsl/meta.go b/dsl/meta.go
index 93af6626..6e881a0e 100644
--- a/dsl/meta.go
+++ b/dsl/meta.go
@@ -137,6 +137,22 @@ import (
 //	    Meta("openapi:generate", "false")
 //	})
 //
+// - "openapi:json:prefix" specifies the prefix used to format the OpenAPI
+// specification encoded in JSON. It can be used with "openapi:json:indent".
+// Applicable to API only.
+//
+//	var _ = API("MyAPI", func() {
+//	    Meta("openapi:json:prefix", "  ")
+//	})
+//
+// - "openapi:json:indent" specifies the indent used to format the OpenAPI
+// specification encoded in JSON. It can be used with "openapi:json:prefix".
+// Applicable to API only.
+//
+//	var _ = API("MyAPI", func() {
+//	    Meta("openapi:json:indent", "  ")
+//	})
+//
 // - "swagger:summary" DEPRECATED, use "openapi:summary" instead
 //
 // - "openapi:summary" sets the OpenAPI operation summary field. The special
diff --git a/http/codegen/openapi/v2/files.go b/http/codegen/openapi/v2/files.go
index c782554d..9b088be2 100644
--- a/http/codegen/openapi/v2/files.go
+++ b/http/codegen/openapi/v2/files.go
@@ -19,7 +19,7 @@ func Files(root *expr.RootExpr) ([]*codegen.File, error) {
 	}
 	jsonSection := &codegen.SectionTemplate{
 		Name:    "openapi",
-		FuncMap: template.FuncMap{"toJSON": toJSON},
+		FuncMap: template.FuncMap{"toJSON": toJSON(root.API.Meta)},
 		Source:  "{{ toJSON .}}",
 		Data:    spec,
 	}
@@ -41,12 +41,22 @@ func Files(root *expr.RootExpr) ([]*codegen.File, error) {
 	}, nil
 }
 
-func toJSON(d any) string {
-	b, err := json.Marshal(d)
-	if err != nil {
-		panic("openapi: " + err.Error()) // bug
+func toJSON(meta expr.MetaExpr) func(any) string {
+	prefix, p := meta.Last("openapi:json:prefix")
+	indent, i := meta.Last("openapi:json:indent")
+	marshal := json.Marshal
+	if p || i {
+		marshal = func(v any) ([]byte, error) {
+			return json.MarshalIndent(v, prefix, indent)
+		}
+	}
+	return func(d any) string {
+		b, err := marshal(d)
+		if err != nil {
+			panic("openapi: " + err.Error()) // bug
+		}
+		return string(b)
 	}
-	return string(b)
 }
 
 func toYAML(d any) string {
diff --git a/http/codegen/openapi/v3/files.go b/http/codegen/openapi/v3/files.go
index c6cb2c57..5863b053 100644
--- a/http/codegen/openapi/v3/files.go
+++ b/http/codegen/openapi/v3/files.go
@@ -16,7 +16,7 @@ func Files(root *expr.RootExpr) ([]*codegen.File, error) {
 	spec := New(root)
 	jsonSection := &codegen.SectionTemplate{
 		Name:    "openapi_v3",
-		FuncMap: template.FuncMap{"toJSON": toJSON},
+		FuncMap: template.FuncMap{"toJSON": toJSON(root.API.Meta)},
 		Source:  "{{ toJSON .}}",
 		Data:    spec,
 	}
@@ -39,12 +39,22 @@ func Files(root *expr.RootExpr) ([]*codegen.File, error) {
 	}, nil
 }
 
-func toJSON(d any) string {
-	b, err := json.Marshal(d)
-	if err != nil {
-		panic("openapi: " + err.Error()) // bug
+func toJSON(meta expr.MetaExpr) func(any) string {
+	prefix, p := meta.Last("openapi:json:prefix")
+	indent, i := meta.Last("openapi:json:indent")
+	marshal := json.Marshal
+	if p || i {
+		marshal = func(v any) ([]byte, error) {
+			return json.MarshalIndent(v, prefix, indent)
+		}
+	}
+	return func(d any) string {
+		b, err := marshal(d)
+		if err != nil {
+			panic("openapi: " + err.Error()) // bug
+		}
+		return string(b)
 	}
-	return string(b)
 }
 
 func toYAML(d any) string {


Test patch:
diff --git a/http/codegen/openapi/v2/files_test.go b/http/codegen/openapi/v2/files_test.go
index 2e828155..3c4f5988 100644
--- a/http/codegen/openapi/v2/files_test.go
+++ b/http/codegen/openapi/v2/files_test.go
@@ -44,6 +44,9 @@ func TestSections(t *testing.T) {
 		{"not-generate-server", testdata.NotGenerateServerDSL},
 		{"not-generate-host", testdata.NotGenerateHostDSL},
 		{"not-generate-attribute", testdata.NotGenerateAttributeDSL},
+		{"json-prefix", testdata.JSONPrefixDSL},
+		{"json-indent", testdata.JSONIndentDSL},
+		{"json-prefix-indent", testdata.JSONPrefixIndentDSL},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
diff --git a/http/codegen/openapi/v2/testdata/TestSections/json-indent_file0.golden b/http/codegen/openapi/v2/testdata/TestSections/json-indent_file0.golden
new file mode 100644
index 00000000..2b2e4797
--- /dev/null
+++ b/http/codegen/openapi/v2/testdata/TestSections/json-indent_file0.golden
@@ -0,0 +1,60 @@
+{
+  "swagger": "2.0",
+  "info": {
+    "title": "",
+    "version": "0.0.1"
+  },
+  "host": "goa.design",
+  "consumes": [
+    "application/json",
+    "application/xml",
+    "application/gob"
+  ],
+  "produces": [
+    "application/json",
+    "application/xml",
+    "application/gob"
+  ],
+  "paths": {
+    "/path1": {
+      "get": {
+        "tags": [
+          "service-name"
+        ],
+        "summary": "Download filename",
+        "operationId": "service-name#/path1",
+        "responses": {
+          "200": {
+            "description": "File downloaded",
+            "schema": {
+              "type": "file"
+            }
+          }
+        },
+        "schemes": [
+          "https"
+        ]
+      }
+    },
+    "/path2": {
+      "get": {
+        "tags": [
+          "user-tag"
+        ],
+        "summary": "Download filename",
+        "operationId": "service-name#/path2",
+        "responses": {
+          "200": {
+            "description": "File downloaded",
+            "schema": {
+              "type": "file"
+            }
+          }
+        },
+        "schemes": [
+          "https"
+        ]
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/http/codegen/openapi/v2/testdata/TestSections/json-indent_file1.golden b/http/codegen/openapi/v2/testdata/TestSections/json-indent_file1.golden
new file mode 100644
index 00000000..666cad02
--- /dev/null
+++ b/http/codegen/openapi/v2/testdata/TestSections/json-indent_file1.golden
@@ -0,0 +1,40 @@
+swagger: "2.0"
+info:
+    title: ""
+    version: 0.0.1
+host: goa.design
+consumes:
+    - application/json
+    - application/xml
+    - application/gob
+produces:
+    - application/json
+    - application/xml
+    - application/gob
+paths:
+    /path1:
+        get:
+            tags:
+                - service-name
+            summary: Download filename
+            operationId: service-name#/path1
+            responses:
+                "200":
+                    description: File downloaded
+                    schema:
+                        type: file
+            schemes:
+                - https
+    /path2:
+        get:
+            tags:
+                - user-tag
+            summary: Download filename
+            operationId: service-name#/path2
+            responses:
+                "200":
+                    description: File downloaded
+                    schema:
+                        type: file
+            schemes:
+                - https
diff --git a/http/codegen/openapi/v2/testdata/TestSections/json-prefix-indent_file0.golden b/http/codegen/openapi/v2/testdata/TestSections/json-prefix-indent_file0.golden
new file mode 100644
index 00000000..0bdfe1dd
--- /dev/null
+++ b/http/codegen/openapi/v2/testdata/TestSections/json-prefix-indent_file0.golden
@@ -0,0 +1,60 @@
+{
+   "swagger": "2.0",
+   "info": {
+     "title": "",
+     "version": "0.0.1"
+   },
+   "host": "goa.design",
+   "consumes": [
+     "application/json",
+     "application/xml",
+     "application/gob"
+   ],
+   "produces": [
+     "application/json",
+     "application/xml",
+     "application/gob"
+   ],
+   "paths": {
+     "/path1": {
+       "get": {
+         "tags": [
+           "service-name"
+         ],
+         "summary": "Download filename",
+         "operationId": "service-name#/path1",
+         "responses": {
+           "200": {
+             "description": "File downloaded",
+             "schema": {
+               "type": "file"
+             }
+           }
+         },
+         "schemes": [
+           "https"
+         ]
+       }
+     },
+     "/path2": {
+       "get": {
+         "tags": [
+           "user-tag"
+         ],
+         "summary": "Download filename",
+         "operationId": "service-name#/path2",
+         "responses": {
+           "200": {
+             "description": "File downloaded",
+             "schema": {
+               "type": "file"
+             }
+           }
+         },
+         "schemes": [
+           "https"
+         ]
+       }
+     }
+   }
+ }
\ No newline at end of file
diff --git a/http/codegen/openapi/v2/testdata/TestSections/json-prefix-indent_file1.golden b/http/codegen/openapi/v2/testdata/TestSections/json-prefix-indent_file1.golden
new file mode 100644
index 00000000..666cad02
--- /dev/null
+++ b/http/codegen/openapi/v2/testdata/TestSections/json-prefix-indent_file1.golden
@@ -0,0 +1,40 @@
+swagger: "2.0"
+info:
+    title: ""
+    version: 0.0.1
+host: goa.design
+consumes:
+    - application/json
+    - application/xml
+    - application/gob
+produces:
+    - application/json
+    - application/xml
+    - application/gob
+paths:
+    /path1:
+        get:
+            tags:
+                - service-name
+            summary: Download filename
+            operationId: service-name#/path1
+            responses:
+                "200":
+                    description: File downloaded
+                    schema:
+                        type: file
+            schemes:
+                - https
+    /path2:
+        get:
+            tags:
+                - user-tag
+            summary: Download filename
+            operationId: service-name#/path2
+            responses:
+                "200":
+                    description: File downloaded
+                    schema:
+                        type: file
+            schemes:
+                - https
diff --git a/http/codegen/openapi/v2/testdata/TestSections/json-prefix_file0.golden b/http/codegen/openapi/v2/testdata/TestSections/json-prefix_file0.golden
new file mode 100644
index 00000000..b4d0bbba
--- /dev/null
+++ b/http/codegen/openapi/v2/testdata/TestSections/json-prefix_file0.golden
@@ -0,0 +1,60 @@
+{
+  "swagger": "2.0",
+  "info": {
+  "title": "",
+  "version": "0.0.1"
+  },
+  "host": "goa.design",
+  "consumes": [
+  "application/json",
+  "application/xml",
+  "application/gob"
+  ],
+  "produces": [
+  "application/json",
+  "application/xml",
+  "application/gob"
+  ],
+  "paths": {
+  "/path1": {
+  "get": {
+  "tags": [
+  "service-name"
+  ],
+  "summary": "Download filename",
+  "operationId": "service-name#/path1",
+  "responses": {
+  "200": {
+  "description": "File downloaded",
+  "schema": {
+  "type": "file"
+  }
+  }
+  },
+  "schemes": [
+  "https"
+  ]
+  }
+  },
+  "/path2": {
+  "get": {
+  "tags": [
+  "user-tag"
+  ],
+  "summary": "Download filename",
+  "operationId": "service-name#/path2",
+  "responses": {
+  "200": {
+  "description": "File downloaded",
+  "schema": {
+  "type": "file"
+  }
+  }
+  },
+  "schemes": [
+  "https"
+  ]
+  }
+  }
+  }
+  }
\ No newline at end of file
diff --git a/http/codegen/openapi/v2/testdata/TestSections/json-prefix_file1.golden b/http/codegen/openapi/v2/testdata/TestSections/json-prefix_file1.golden
new file mode 100644
index 00000000..666cad02
--- /dev/null
+++ b/http/codegen/openapi/v2/testdata/TestSections/json-prefix_file1.golden
@@ -0,0 +1,40 @@
+swagger: "2.0"
+info:
+    title: ""
+    version: 0.0.1
+host: goa.design
+consumes:
+    - application/json
+    - application/xml
+    - application/gob
+produces:
+    - application/json
+    - application/xml
+    - application/gob
+paths:
+    /path1:
+        get:
+            tags:
+                - service-name
+            summary: Download filename
+            operationId: service-name#/path1
+            responses:
+                "200":
+                    description: File downloaded
+                    schema:
+                        type: file
+            schemes:
+                - https
+    /path2:
+        get:
+            tags:
+                - user-tag
+            summary: Download filename
+            operationId: service-name#/path2
+            responses:
+                "200":
+                    description: File downloaded
+                    schema:
+                        type: file
+            schemes:
+                - https
diff --git a/http/codegen/openapi/v3/files_test.go b/http/codegen/openapi/v3/files_test.go
index 103682bb..b15ddc4b 100644
--- a/http/codegen/openapi/v3/files_test.go
+++ b/http/codegen/openapi/v3/files_test.go
@@ -48,6 +48,9 @@ func TestFiles(t *testing.T) {
 		{"not-generate-server", testdata.NotGenerateServerDSL},
 		{"not-generate-host", testdata.NotGenerateHostDSL},
 		{"not-generate-attribute", testdata.NotGenerateAttributeDSL},
+		{"json-prefix", testdata.JSONPrefixDSL},
+		{"json-indent", testdata.JSONIndentDSL},
+		{"json-prefix-indent", testdata.JSONPrefixIndentDSL},
 		// TestEndpoints
 		{"endpoint", testdata.ExtensionDSL},
 		{"endpoint-swagger", testdata.ExtensionSwaggerDSL},
diff --git a/http/codegen/openapi/v3/testdata/golden/json-indent_file0.golden b/http/codegen/openapi/v3/testdata/golden/json-indent_file0.golden
new file mode 100644
index 00000000..0332d154
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/json-indent_file0.golden
@@ -0,0 +1,48 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "Goa API",
+    "version": "0.0.1"
+  },
+  "servers": [
+    {
+      "url": "https://goa.design"
+    }
+  ],
+  "paths": {
+    "/path1": {
+      "get": {
+        "tags": [
+          "service-name"
+        ],
+        "summary": "Download filename",
+        "operationId": "service-name#/path1",
+        "responses": {
+          "200": {
+            "description": "File downloaded"
+          }
+        }
+      }
+    },
+    "/path2": {
+      "get": {
+        "tags": [
+          "user-tag"
+        ],
+        "summary": "Download filename",
+        "operationId": "service-name#/path2",
+        "responses": {
+          "200": {
+            "description": "File downloaded"
+          }
+        }
+      }
+    }
+  },
+  "components": {},
+  "tags": [
+    {
+      "name": "service-name"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/http/codegen/openapi/v3/testdata/golden/json-indent_file1.golden b/http/codegen/openapi/v3/testdata/golden/json-indent_file1.golden
new file mode 100644
index 00000000..3d0cf89b
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/json-indent_file1.golden
@@ -0,0 +1,28 @@
+openapi: 3.0.3
+info:
+    title: Goa API
+    version: 0.0.1
+servers:
+    - url: https://goa.design
+paths:
+    /path1:
+        get:
+            tags:
+                - service-name
+            summary: Download filename
+            operationId: service-name#/path1
+            responses:
+                "200":
+                    description: File downloaded
+    /path2:
+        get:
+            tags:
+                - user-tag
+            summary: Download filename
+            operationId: service-name#/path2
+            responses:
+                "200":
+                    description: File downloaded
+components: {}
+tags:
+    - name: service-name
diff --git a/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file0.golden b/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file0.golden
new file mode 100644
index 00000000..6fb22d4b
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file0.golden
@@ -0,0 +1,48 @@
+{
+   "openapi": "3.0.3",
+   "info": {
+     "title": "Goa API",
+     "version": "0.0.1"
+   },
+   "servers": [
+     {
+       "url": "https://goa.design"
+     }
+   ],
+   "paths": {
+     "/path1": {
+       "get": {
+         "tags": [
+           "service-name"
+         ],
+         "summary": "Download filename",
+         "operationId": "service-name#/path1",
+         "responses": {
+           "200": {
+             "description": "File downloaded"
+           }
+         }
+       }
+     },
+     "/path2": {
+       "get": {
+         "tags": [
+           "user-tag"
+         ],
+         "summary": "Download filename",
+         "operationId": "service-name#/path2",
+         "responses": {
+           "200": {
+             "description": "File downloaded"
+           }
+         }
+       }
+     }
+   },
+   "components": {},
+   "tags": [
+     {
+       "name": "service-name"
+     }
+   ]
+ }
\ No newline at end of file
diff --git a/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file1.golden b/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file1.golden
new file mode 100644
index 00000000..3d0cf89b
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/json-prefix-indent_file1.golden
@@ -0,0 +1,28 @@
+openapi: 3.0.3
+info:
+    title: Goa API
+    version: 0.0.1
+servers:
+    - url: https://goa.design
+paths:
+    /path1:
+        get:
+            tags:
+                - service-name
+            summary: Download filename
+            operationId: service-name#/path1
+            responses:
+                "200":
+                    description: File downloaded
+    /path2:
+        get:
+            tags:
+                - user-tag
+            summary: Download filename
+            operationId: service-name#/path2
+            responses:
+                "200":
+                    description: File downloaded
+components: {}
+tags:
+    - name: service-name
diff --git a/http/codegen/openapi/v3/testdata/golden/json-prefix_file0.golden b/http/codegen/openapi/v3/testdata/golden/json-prefix_file0.golden
new file mode 100644
index 00000000..554c149d
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/json-prefix_file0.golden
@@ -0,0 +1,48 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+  "title": "Goa API",
+  "version": "0.0.1"
+  },
+  "servers": [
+  {
+  "url": "https://goa.design"
+  }
+  ],
+  "paths": {
+  "/path1": {
+  "get": {
+  "tags": [
+  "service-name"
+  ],
+  "summary": "Download filename",
+  "operationId": "service-name#/path1",
+  "responses": {
+  "200": {
+  "description": "File downloaded"
+  }
+  }
+  }
+  },
+  "/path2": {
+  "get": {
+  "tags": [
+  "user-tag"
+  ],
+  "summary": "Download filename",
+  "operationId": "service-name#/path2",
+  "responses": {
+  "200": {
+  "description": "File downloaded"
+  }
+  }
+  }
+  }
+  },
+  "components": {},
+  "tags": [
+  {
+  "name": "service-name"
+  }
+  ]
+  }
\ No newline at end of file
diff --git a/http/codegen/openapi/v3/testdata/golden/json-prefix_file1.golden b/http/codegen/openapi/v3/testdata/golden/json-prefix_file1.golden
new file mode 100644
index 00000000..3d0cf89b
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/json-prefix_file1.golden
@@ -0,0 +1,28 @@
+openapi: 3.0.3
+info:
+    title: Goa API
+    version: 0.0.1
+servers:
+    - url: https://goa.design
+paths:
+    /path1:
+        get:
+            tags:
+                - service-name
+            summary: Download filename
+            operationId: service-name#/path1
+            responses:
+                "200":
+                    description: File downloaded
+    /path2:
+        get:
+            tags:
+                - user-tag
+            summary: Download filename
+            operationId: service-name#/path2
+            responses:
+                "200":
+                    description: File downloaded
+components: {}
+tags:
+    - name: service-name
diff --git a/http/codegen/testdata/openapi_dsls.go b/http/codegen/testdata/openapi_dsls.go
index a76aac5b..e831ba3d 100644
--- a/http/codegen/testdata/openapi_dsls.go
+++ b/http/codegen/testdata/openapi_dsls.go
@@ -805,3 +805,55 @@ var NotGenerateAttributeDSL = func() {
 		})
 	})
 }
+
+var JSONPrefixDSL = func() {
+	var _ = API("test", func() {
+		Server("test", func() {
+			Host("localhost", func() {
+				URI("https://goa.design")
+			})
+		})
+		Meta("openapi:json:prefix", "  ")
+	})
+	var _ = Service("service-name", func() {
+		Files("path1", "filename")
+		Files("path2", "filename", func() {
+			Meta("openapi:tag:user-tag")
+		})
+	})
+}
+
+var JSONIndentDSL = func() {
+	var _ = API("test", func() {
+		Server("test", func() {
+			Host("localhost", func() {
+				URI("https://goa.design")
+			})
+		})
+		Meta("openapi:json:indent", "  ")
+	})
+	var _ = Service("service-name", func() {
+		Files("path1", "filename")
+		Files("path2", "filename", func() {
+			Meta("openapi:tag:user-tag")
+		})
+	})
+}
+
+var JSONPrefixIndentDSL = func() {
+	var _ = API("test", func() {
+		Server("test", func() {
+			Host("localhost", func() {
+				URI("https://goa.design")
+			})
+		})
+		Meta("openapi:json:prefix", " ")
+		Meta("openapi:json:indent", "  ")
+	})
+	var _ = Service("service-name", func() {
+		Files("path1", "filename")
+		Files("path2", "filename", func() {
+			Meta("openapi:tag:user-tag")
+		})
+	})
+}