[tools] Suppress empty fidl_api_summarize output

* Generates empty output if the library only has a library
  declaration at the target API level.
* Adds a flag.Setter for the summary format in pkg summarize.
* Combines Write and WriteJSON into GenerateSummary since
  they're only used by this binary which selects the summary
  format based on a flag value. Also moves this logic to
  pkg summarize.

Bug: 102484

Change-Id: Iecc463fdaed7f34f25a2838b438389a314881f2a
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/692721
Reviewed-by: Hunter Freyer <hjfreyer@google.com>
Commit-Queue: Kendal Harland <kjharland@google.com>
Reviewed-by: Alex Zaslavsky <azaslavsky@google.com>
Reviewed-by: Filip Filmar <fmil@google.com>
diff --git a/build/fidl/fidl_summary.gni b/build/fidl/fidl_summary.gni
index 73b3d57..d33a2aa 100644
--- a/build/fidl/fidl_summary.gni
+++ b/build/fidl/fidl_summary.gni
@@ -117,6 +117,7 @@
       rebase_path(_json_representation, root_build_dir),
       "--output-file",
       rebase_path(_summary_file_json, root_build_dir),
+      "--suppress-empty-library",
     ]
     metadata = {
       # Refer to //sdk/cts/plasa/plasa_artifacts.gni for the explanation of
diff --git a/sdk/history/7/fuchsia.bluetooth.gatt2.api_summary.json b/sdk/history/7/fuchsia.bluetooth.gatt2.api_summary.json
index 9c7f40b..e69de29 100644
--- a/sdk/history/7/fuchsia.bluetooth.gatt2.api_summary.json
+++ b/sdk/history/7/fuchsia.bluetooth.gatt2.api_summary.json
@@ -1,6 +0,0 @@
-[
-    {
-        "kind": "library",
-        "name": "fuchsia.bluetooth.gatt2"
-    }
-]
diff --git a/sdk/history/7/fuchsia.debugdata.api_summary.json b/sdk/history/7/fuchsia.debugdata.api_summary.json
index ce21c9d..e69de29 100644
--- a/sdk/history/7/fuchsia.debugdata.api_summary.json
+++ b/sdk/history/7/fuchsia.debugdata.api_summary.json
@@ -1,6 +0,0 @@
-[
-    {
-        "kind": "library",
-        "name": "fuchsia.debugdata"
-    }
-]
diff --git a/sdk/history/7/fuchsia.net.reachability.api_summary.json b/sdk/history/7/fuchsia.net.reachability.api_summary.json
index 7d4fbe19..e69de29 100644
--- a/sdk/history/7/fuchsia.net.reachability.api_summary.json
+++ b/sdk/history/7/fuchsia.net.reachability.api_summary.json
@@ -1,6 +0,0 @@
-[
-    {
-        "kind": "library",
-        "name": "fuchsia.net.reachability"
-    }
-]
diff --git a/sdk/history/7/fuchsia.tracing.perfetto.api_summary.json b/sdk/history/7/fuchsia.tracing.perfetto.api_summary.json
index f679d14..e69de29 100644
--- a/sdk/history/7/fuchsia.tracing.perfetto.api_summary.json
+++ b/sdk/history/7/fuchsia.tracing.perfetto.api_summary.json
@@ -1,6 +0,0 @@
-[
-    {
-        "kind": "library",
-        "name": "fuchsia.tracing.perfetto"
-    }
-]
diff --git a/tools/fidl/fidl_api_summarize/main.go b/tools/fidl/fidl_api_summarize/main.go
index f5b79f4..433eb96 100644
--- a/tools/fidl/fidl_api_summarize/main.go
+++ b/tools/fidl/fidl_api_summarize/main.go
@@ -8,9 +8,10 @@
 package main
 
 import (
+	"bytes"
 	"flag"
 	"fmt"
-	"io"
+	"io/ioutil"
 	"os"
 
 	"go.fuchsia.dev/fuchsia/tools/fidl/lib/fidlgen"
@@ -18,11 +19,17 @@
 )
 
 var (
-	fir    = flag.String("fidl-ir-file", "", "The FIDL IR input file to produce an API summary for.")
-	out    = flag.String("output-file", "", "The output file to write the summary into.")
-	format = flag.String("format", "text", "Specify the output format (text|json)")
+	fir                  = flag.String("fidl-ir-file", "", "The FIDL IR input file to produce an API summary for.")
+	out                  = flag.String("output-file", "", "The output file to write the summary into.")
+	suppressEmptyLibrary = flag.Bool("suppress-empty-library", false, "Generate empty output for libraries with no declarations")
+	format               = summarize.TextSummaryFormat
 )
 
+// TODO(kjharland): Move flags into main().
+func init() {
+	flag.Var(&format, "format", "Specify the output format (text|json)")
+}
+
 // usage prints a user-friendly usage message when the flag --help is provided.
 func usage() {
 	fmt.Fprintf(flag.CommandLine.Output(),
@@ -33,19 +40,6 @@
 	flag.PrintDefaults()
 }
 
-// getWriter returns the appropriate function for writing output, based on the
-// format chosen through the --format flag.
-func getWriter() (func(fidlgen.Root, io.Writer) error, error) {
-	switch *format {
-	case "text":
-		return summarize.Write, nil
-	case "json":
-		return summarize.WriteJSON, nil
-	default:
-		return nil, fmt.Errorf("not a recognized flag value: %v", *format)
-	}
-}
-
 func main() {
 	flag.Usage = usage
 	flag.Parse()
@@ -57,10 +51,6 @@
 }
 
 func mainImpl() error {
-	writerFn, err := getWriter()
-	if err != nil {
-		return fmt.Errorf("While parsing --format: %v", err)
-	}
 	if *fir == "" {
 		return fmt.Errorf("The flag --fidl-ir-file=... is required")
 	}
@@ -72,20 +62,26 @@
 		return fmt.Errorf("The flag --output-file=... is required")
 	}
 
-	f, err := os.Create(*out)
-	if err != nil {
-		return fmt.Errorf("Could not create file: %v: %w", *out, err)
-	}
-
 	root, err := fidlgen.DecodeJSONIr(in)
 	if err != nil {
-		f.Close() // decode error takes precedence.
 		return fmt.Errorf("Could not parse FIDL IR from: %v: %w", *in, err)
 	}
-	if err := writerFn(root, f); err != nil {
-		f.Close() // writerFn error takes precedence.
+
+	b, err := summarize.GenerateSummary(root, format)
+	if err != nil {
 		return fmt.Errorf("While summarizing %v into %v: %w", *in, *out, err)
 	}
 
-	return f.Close()
+	if *suppressEmptyLibrary {
+		emptyLibRoot := fidlgen.Root{Name: root.Name}
+		emptyLibBytes, err := summarize.GenerateSummary(emptyLibRoot, format)
+		if err != nil {
+			return err
+		}
+		if bytes.Equal(b, emptyLibBytes) {
+			return ioutil.WriteFile(*out, []byte{}, 0644)
+		}
+	}
+
+	return ioutil.WriteFile(*out, b, 0644)
 }
diff --git a/tools/fidl/lib/apidiff/apidiff_test.go b/tools/fidl/lib/apidiff/apidiff_test.go
index 5c52f151..58a0cd8 100644
--- a/tools/fidl/lib/apidiff/apidiff_test.go
+++ b/tools/fidl/lib/apidiff/apidiff_test.go
@@ -1248,7 +1248,7 @@
 func summarizeOne(t *testing.T, r fidlgen.Root) string {
 	t.Helper()
 	var buf strings.Builder
-	if err := summarize.WriteJSON(r, &buf); err != nil {
+	if err := summarize.WriteSummary(&buf, r, summarize.JSONSummaryFormat); err != nil {
 		t.Fatalf("error while summarizing: %v", err)
 	}
 	return buf.String()
diff --git a/tools/fidl/lib/summarize/summary.go b/tools/fidl/lib/summarize/summary.go
index d9775ff..0d47762 100644
--- a/tools/fidl/lib/summarize/summary.go
+++ b/tools/fidl/lib/summarize/summary.go
@@ -8,6 +8,7 @@
 package summarize
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -16,6 +17,78 @@
 	"go.fuchsia.dev/fuchsia/tools/fidl/lib/fidlgen"
 )
 
+type summary []Element
+
+// SummaryFormat is a representation of a FIDL API summary.
+//
+// SummaryFormat implements flag.Setter for convenient use in command-line tools.
+type SummaryFormat string
+
+// SummaryFormat constants.
+const (
+	TextSummaryFormat SummaryFormat = "text"
+	JSONSummaryFormat SummaryFormat = "json"
+)
+
+// String implements flag.Setter.
+func (f *SummaryFormat) String() string {
+	return string(*f)
+}
+
+// Set implements flag.Setter.
+func (f *SummaryFormat) Set(value string) error {
+	format := SummaryFormat(value)
+	if _, err := format.formatter(); err != nil {
+		return err
+	}
+	*f = format
+	return nil
+}
+
+func (f SummaryFormat) formatter() (func(io.Writer, summary) error, error) {
+	switch f {
+	case TextSummaryFormat:
+		return formatTextSummary, nil
+	case JSONSummaryFormat:
+		return formatJSONSummary, nil
+	}
+	return nil, fmt.Errorf("unimplemented summary format %q", string(f))
+}
+
+// GenerateSummary summarizes root and returns the serialized result in the given format.
+func GenerateSummary(root fidlgen.Root, format SummaryFormat) ([]byte, error) {
+	var b bytes.Buffer
+	if err := WriteSummary(&b, root, format); err != nil {
+		return nil, err
+	}
+	return b.Bytes(), nil
+}
+
+// WriteSummary summarizes root and writes the serialized result to the given Writer.
+func WriteSummary(w io.Writer, root fidlgen.Root, format SummaryFormat) error {
+	s := summarize(root)
+	f, err := format.formatter()
+	if err != nil {
+		return err
+	}
+	return f(w, s)
+}
+
+func formatTextSummary(w io.Writer, s summary) error {
+	for _, e := range s {
+		fmt.Fprintf(w, "%v\n", e)
+	}
+	return nil
+}
+
+func formatJSONSummary(w io.Writer, s summary) error {
+	e := json.NewEncoder(w)
+	// 4-level indent is chosen to match `fx format-code`.
+	e.SetIndent("", "    ")
+	e.SetEscapeHTML(false)
+	return e.Encode(serialize([]Element(s)))
+}
+
 // Element describes a single platform surface element.  Use Summarize to
 // convert a FIDL AST into Elements.
 type Element interface {
@@ -116,25 +189,6 @@
 	s.symbols.addPayloads(payloads)
 }
 
-// Write produces an API summary for the FIDL AST from the root into the supplied
-// writer.
-func Write(root fidlgen.Root, out io.Writer) error {
-	for _, e := range Elements(root) {
-		fmt.Fprintf(out, "%v\n", e)
-	}
-	return nil
-}
-
-// WriteJSON produces an API summary for the FIDL AST from the root into the
-// supplied writer, and formats the data as JSON.
-func WriteJSON(root fidlgen.Root, out io.Writer) error {
-	e := json.NewEncoder(out)
-	// 4-level indent is chosen to match `fx format-code`.
-	e.SetIndent("", "    ")
-	e.SetEscapeHTML(false)
-	return e.Encode(serialize(Elements(root)))
-}
-
 func serialize(e []Element) []ElementStr {
 	var ret []ElementStr
 	for _, l := range e {
@@ -192,7 +246,7 @@
 
 // Elements returns the API elements found in the supplied AST root in a
 // canonical ordering.
-func Elements(root fidlgen.Root) []Element {
+func summarize(root fidlgen.Root) summary {
 	var s summarizer
 
 	// Do a first pass of the protocols, creating a map of all names of types that
@@ -220,5 +274,5 @@
 	s.addProtocols(root.Protocols)
 	s.addElement(library{r: root})
 
-	return s.Elements()
+	return summary(s.Elements())
 }
diff --git a/tools/fidl/lib/summarize/summary_test.go b/tools/fidl/lib/summarize/summary_test.go
index bdbfff2..904cbaa 100644
--- a/tools/fidl/lib/summarize/summary_test.go
+++ b/tools/fidl/lib/summarize/summary_test.go
@@ -5,12 +5,10 @@
 package summarize
 
 import (
-	"io"
 	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"go.fuchsia.dev/fuchsia/tools/fidl/lib/fidlgen"
 	"go.fuchsia.dev/fuchsia/tools/fidl/lib/fidlgentest"
 )
 
@@ -48,7 +46,7 @@
 	expected string
 }
 
-func TestWrite(t *testing.T) {
+func TestTextSummaryFormat(t *testing.T) {
 	tests := []summaryTestCase{
 		{
 			name: "library only",
@@ -670,10 +668,10 @@
 `,
 		},
 	}
-	runWriteTests(t, tests, Write)
+	runGenerateSummaryTests(t, tests, TextSummaryFormat)
 }
 
-func TestWriteJSON(t *testing.T) {
+func TestJSONSummaryFormat(t *testing.T) {
 	tests := []summaryTestCase{
 		{
 			name: "library only",
@@ -1884,10 +1882,10 @@
 `,
 		},
 	}
-	runWriteTests(t, tests, WriteJSON)
+	runGenerateSummaryTests(t, tests, JSONSummaryFormat)
 }
 
-func runWriteTests(t *testing.T, tests []summaryTestCase, writeFn func(fidlgen.Root, io.Writer) error) {
+func runGenerateSummaryTests(t *testing.T, tests []summaryTestCase, format SummaryFormat) {
 	t.Helper()
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
@@ -1896,17 +1894,18 @@
 				c = c.WithDependency(test.dep)
 			}
 			r := c.Single(test.fidl)
-			var sb strings.Builder
-			if err := writeFn(r, &sb); err != nil {
+			b, err := GenerateSummary(r, format)
+			if err != nil {
 				t.Fatalf("while summarizing file: %v", err)
 			}
-			actual := strings.Split(sb.String(), "\n")
+			summary := string(b)
+			actual := strings.Split(summary, "\n")
 			expected := strings.Split(test.expected, "\n")
 
 			if !cmp.Equal(expected, actual) {
 				t.Errorf("expected:\n---BEGIN---\n%+v\n---END---\n\n"+
 					"actual:\n---BEGIN---\n%+v\n---END---\n\ndiff:\n%v\n\nroot: %+v",
-					test.expected, sb.String(),
+					test.expected, summary,
 					cmp.Diff(expected, actual), r)
 			}
 		})