all: only update clients if revision pulled is newer

The discovery doc api can return different revisions of an api spec
with back to back calls. We should not allow our clients to flip
back forth between revisions when we re-generate them. With this
CL we now check to make sure the spec pulled has a newer revison
than the local cache before updating. If the revision is older,
we now skip generating that client.

Fixes: #472
Change-Id: I269ac1a37f74f07bf6d07db6d6213a05227c8f81
Reviewed-on: https://code-review.googlesource.com/c/google-api-go-client/+/55650
Reviewed-by: Tyler Bui-Palsulich <tbp@google.com>
Reviewed-by: kokoro <noreply+kokoro@google.com>
diff --git a/google-api-go-generator/gen.go b/google-api-go-generator/gen.go
index e4d1401..8278efa 100644
--- a/google-api-go-generator/gen.go
+++ b/google-api-go-generator/gen.go
@@ -61,6 +61,11 @@
 	serviceTypes = []string{"Service", "APIService"}
 )
 
+var (
+	errOldRevision = errors.New("revision pulled older than local cached revision")
+	errNoDoc       = errors.New("could not read discovery doc")
+)
+
 // API represents an API to generate, as well as its state while it's
 // generating.
 type API struct {
@@ -142,7 +147,10 @@
 		matches = append(matches, api)
 		log.Printf("Generating API %s", api.ID)
 		err := api.WriteGeneratedCode()
-		if err != nil && err != errNoDoc {
+		if err == errOldRevision {
+			log.Printf("Old revision found for %s, skipping generation", api.ID)
+			continue
+		} else if err != nil && err != errNoDoc {
 			errors = append(errors, &generateError{api, err})
 			continue
 		}
@@ -285,6 +293,40 @@
 	return a, nil
 }
 
+func checkAndUpdateSpecFile(file string, contents []byte) error {
+	// check if file exists
+	if _, err := os.Stat(file); os.IsNotExist(err) {
+		return writeFile(file, contents)
+	}
+	existing, err := ioutil.ReadFile(file)
+	if err != nil {
+		return err
+	}
+	if err := isNewerRevision(existing, contents); err != nil {
+		return err
+	}
+	return writeFile(file, contents)
+}
+
+// isNewerRevision returns nil if the contents of new has a newer revision than
+// the contents of old.
+func isNewerRevision(old []byte, new []byte) error {
+	type docRevision struct {
+		Revision string `json:"revision"`
+	}
+	var oldDoc, newDoc docRevision
+	if err := json.Unmarshal(old, &oldDoc); err != nil {
+		return err
+	}
+	if err := json.Unmarshal(new, &newDoc); err != nil {
+		return err
+	}
+	if newDoc.Revision < oldDoc.Revision {
+		return errOldRevision
+	}
+	return nil
+}
+
 func writeFile(file string, contents []byte) error {
 	// Don't write it if the contents are identical.
 	existing, err := ioutil.ReadFile(file)
@@ -496,10 +538,9 @@
 	return filepath.Join(a.SourceDir(), a.Package()+"-api.json")
 }
 
-var errNoDoc = errors.New("could not read discovery doc")
-
 // WriteGeneratedCode generates code for a.
-// It returns errNoDoc if we couldn't read the discovery doc.
+// It returns errNoDoc if we couldn't read the discovery doc or errOldRevision
+// if the API spec file being pulled in is older than the local cache.
 func (a *API) WriteGeneratedCode() error {
 	genfilename := *output
 	jsonBytes := a.jsonBytes()
@@ -509,7 +550,7 @@
 		return errNoDoc
 	}
 	if genfilename == "" {
-		if err := writeFile(a.JSONFile(), jsonBytes); err != nil {
+		if err := checkAndUpdateSpecFile(a.JSONFile(), jsonBytes); err != nil {
 			return err
 		}
 		outdir := a.SourceDir()
diff --git a/google-api-go-generator/gen_test.go b/google-api-go-generator/gen_test.go
index 092483d..1eda4b9 100644
--- a/google-api-go-generator/gen_test.go
+++ b/google-api-go-generator/gen_test.go
@@ -213,3 +213,32 @@
 		}
 	}
 }
+
+func TestIsNewerRevision(t *testing.T) {
+	olderBytesPath, newerBytesPath := filepath.Join("testdata", "rev20200415.json"), filepath.Join("testdata", "rev20200416.json")
+	olderBytes, err := ioutil.ReadFile(olderBytesPath)
+	if err != nil {
+		t.Fatalf("ioutil.ReadFile(%q) = %v; want nil", olderBytesPath, err)
+	}
+	newerBytes, err := ioutil.ReadFile(newerBytesPath)
+	if err != nil {
+		t.Fatalf("ioutil.ReadFile(%q) = %v; want nil", newerBytesPath, err)
+	}
+
+	// newBytes > oldBytes
+	if err := isNewerRevision(olderBytes, newerBytes); err != nil {
+		t.Fatalf("isNewerRevision(%q, %q) = %v; want nil", string(olderBytes), string(newerBytes), err)
+	}
+	// newBytes == newBytes
+	if err := isNewerRevision(newerBytes, newerBytes); err != nil {
+		t.Fatalf("isNewerRevision(%q, %q) = %v; want nil", string(newerBytes), string(newerBytes), err)
+	}
+	// newBytes < newBytes
+	err = isNewerRevision(newerBytes, olderBytes)
+	if err == nil {
+		t.Fatalf("isNewerRevision(%q, %q) = nil; want %v", string(newerBytes), string(olderBytes), errOldRevision)
+	}
+	if err != errOldRevision {
+		t.Fatalf("got %v, want %v", err, errOldRevision)
+	}
+}
diff --git a/google-api-go-generator/testdata/rev20200415.json b/google-api-go-generator/testdata/rev20200415.json
new file mode 100644
index 0000000..6b1c206
--- /dev/null
+++ b/google-api-go-generator/testdata/rev20200415.json
@@ -0,0 +1,3 @@
+{
+    "revision": "20200415"
+}
\ No newline at end of file
diff --git a/google-api-go-generator/testdata/rev20200416.json b/google-api-go-generator/testdata/rev20200416.json
new file mode 100644
index 0000000..c04f361
--- /dev/null
+++ b/google-api-go-generator/testdata/rev20200416.json
@@ -0,0 +1,3 @@
+{
+    "revision": "20200416"
+}
\ No newline at end of file