Add support for creating a PATCH body from a before and after object.

Reiewed in http://codereview.appspot.com/4532086/
diff --git a/apiclient/model.py b/apiclient/model.py
index 61a81d4..7f51d29 100644
--- a/apiclient/model.py
+++ b/apiclient/model.py
@@ -299,3 +299,47 @@
   @property
   def no_content_response(self):
     return self._protocol_buffer()
+
+
+def makepatch(original, modified):
+  """Create a patch object.
+
+  Some methods support PATCH, an efficient way to send updates to a resource.
+  This method allows the easy construction of patch bodies by looking at the
+  differences between a resource before and after it was modified.
+
+  Args:
+    original: object, the original deserialized resource
+    modified: object, the modified deserialized resource
+  Returns:
+    An object that contains only the changes from original to modified, in a
+    form suitable to pass to a PATCH method.
+
+  Example usage:
+    item = service.activities().get(postid=postid, userid=userid).execute()
+    original = copy.deepcopy(item)
+    item['object']['content'] = 'This is updated.'
+    service.activities.patch(postid=postid, userid=userid,
+      body=makepatch(original, item)).execute()
+  """
+  patch = {}
+  for key, original_value in original.iteritems():
+    modified_value = modified.get(key, None)
+    if modified_value is None:
+      # Use None to signal that the element is deleted
+      patch[key] = None
+    elif original_value != modified_value:
+      if type(original_value) == type({}):
+        # Recursively descend objects
+        patch[key] = makepatch(original_value, modified_value)
+      else:
+        # In the case of simple types or arrays we just replace
+        patch[key] = modified_value
+    else:
+      # Don't add anything to patch if there's no change
+      pass
+  for key in modified:
+    if key not in original:
+      patch[key] = modified[key]
+
+  return patch
diff --git a/tests/test_model.py b/tests/test_model.py
new file mode 100644
index 0000000..5b9b2be
--- /dev/null
+++ b/tests/test_model.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Model tests
+
+Unit tests for model utility methods.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import httplib2
+import unittest
+
+from apiclient.model import makepatch
+
+
+TEST_CASES = [
+    # (message, original, modified, expected)
+    ("Remove an item from an object",
+     {'a': 1, 'b': 2},  {'a': 1},         {'b': None}),
+    ("Add an item to an object",
+     {'a': 1},          {'a': 1, 'b': 2}, {'b': 2}),
+    ("No changes",
+     {'a': 1, 'b': 2},  {'a': 1, 'b': 2}, {}),
+    ("Empty objects",
+     {},  {}, {}),
+    ("Modify an item in an object",
+     {'a': 1, 'b': 2},  {'a': 1, 'b': 3}, {'b': 3}),
+    ("Change an array",
+     {'a': 1, 'b': [2, 3]},  {'a': 1, 'b': [2]}, {'b': [2]}),
+    ("Modify a nested item",
+     {'a': 1, 'b': {'foo':'bar', 'baz': 'qux'}},
+     {'a': 1, 'b': {'foo':'bar', 'baz': 'qaax'}},
+     {'b': {'baz': 'qaax'}}),
+    ("Modify a nested array",
+     {'a': 1, 'b': [{'foo':'bar', 'baz': 'qux'}]},
+     {'a': 1, 'b': [{'foo':'bar', 'baz': 'qaax'}]},
+     {'b': [{'foo':'bar', 'baz': 'qaax'}]}),
+    ("Remove item from a nested array",
+     {'a': 1, 'b': [{'foo':'bar', 'baz': 'qux'}]},
+     {'a': 1, 'b': [{'foo':'bar'}]},
+     {'b': [{'foo':'bar'}]}),
+    ("Remove a nested item",
+     {'a': 1, 'b': {'foo':'bar', 'baz': 'qux'}},
+     {'a': 1, 'b': {'foo':'bar'}},
+     {'b': {'baz': None}})
+]
+
+
+class TestPatch(unittest.TestCase):
+
+  def test_patch(self):
+    for (msg, orig, mod, expected_patch) in TEST_CASES:
+      self.assertEqual(expected_patch, makepatch(orig, mod), msg=msg)
+
+
+if __name__ == '__main__':
+  unittest.main()