support create and update operations

Change-Id: I8e6d3da278e5bb923c83579788132dd7b8d205c9
diff --git a/xos/grpc/README.md b/xos/grpc/README.md
index 4d7b8e4..24da428 100644
--- a/xos/grpc/README.md
+++ b/xos/grpc/README.md
@@ -1 +1,21 @@
-if you don't run env.sh before running grpc_server.sh, things break.
+Quick Start Notes:
+
+1) cd protos
+
+2) make install-protoc
+
+3) apt-get -y install cython
+
+4) python -m pip install grpcio grpcio-tools
+
+5) make rebuild-protos
+
+6) make
+
+7) cd ..
+
+8) source env.sh
+
+9) python ./grpc_server.py
+
+Note: If you don't run env.sh before running grpc_server.sh, things break.
diff --git a/xos/tools/apigen/grpc_api.template.py b/xos/tools/apigen/grpc_api.template.py
index 47578f1..b7217cc 100644
--- a/xos/tools/apigen/grpc_api.template.py
+++ b/xos/tools/apigen/grpc_api.template.py
@@ -1,5 +1,6 @@
 import time
 from protos import xos_pb2
+from google.protobuf.empty_pb2 import Empty
 
 from core.models import *
 
@@ -18,18 +19,6 @@
         pClass = getattr(xos_pb2, djangoClass.__name__ + "s")
         return pClass
 
-    def convertString(self, x):
-        if not x:
-            return ""
-        else:
-            return str(x)
-
-    def convertInt(self, x):
-        if not x:
-            return 0
-        else:
-            return int(x)
-
     def convertFloat(self, x):
         if not x:
             return 0
@@ -51,24 +40,65 @@
     def objToProto(self, obj):
         p_obj = self.getProtoClass(obj.__class__)()
         for field in obj._meta.fields:
-            if (field.get_internal_type() == "CharField") or (field.get_internal_type() == "TextField") or (field.get_internal_type() == "SlugField"):
-                setattr(p_obj, field.name, self.convertString(getattr(obj, field.name)))
-            elif (field.get_internal_type() == "BooleanField"):
+            if getattr(obj, field.name) == None:
+                continue
+
+            ftype = field.get_internal_type()
+            if (ftype == "CharField") or (ftype == "TextField") or (ftype == "SlugField"):
+                setattr(p_obj, field.name, str(getattr(obj, field.name)))
+            elif (ftype == "BooleanField"):
                 setattr(p_obj, field.name, getattr(obj, field.name))
-            elif (field.get_internal_type() == "AutoField"):
-                setattr(p_obj, field.name, self.convertInt(getattr(obj, field.name)))
-            elif (field.get_internal_type() == "IntegerField") or (field.get_internal_type() == "PositiveIntegerField"):
-                setattr(p_obj, field.name, self.convertInt(getattr(obj, field.name)))
-            elif (field.get_internal_type() == "ForeignKey"):
+            elif (ftype == "AutoField"):
+                setattr(p_obj, field.name, int(getattr(obj, field.name)))
+            elif (ftype == "IntegerField") or (ftype == "PositiveIntegerField"):
+                setattr(p_obj, field.name, int(getattr(obj, field.name)))
+            elif (ftype == "ForeignKey"):
                 setattr(p_obj, field.name+"_id", self.convertForeignKey(getattr(obj, field.name)))
-            elif (field.get_internal_type() == "DateTimeField"):
+            elif (ftype == "DateTimeField"):
                 setattr(p_obj, field.name, self.convertDateTime(getattr(obj, field.name)))
-            elif (field.get_internal_type() == "FloatField"):
-                setattr(p_obj, field.name, self.convertFloat(getattr(obj, field.name)))
-            elif (field.get_internal_type() == "GenericIPAddressField"):
-                setattr(p_obj, field.name, self.convertString(getattr(obj, field.name)))
+            elif (ftype == "FloatField"):
+                setattr(p_obj, field.name, float(getattr(obj, field.name)))
+            elif (ftype == "GenericIPAddressField"):
+                setattr(p_obj, field.name, str(getattr(obj, field.name)))
         return p_obj
 
+    def protoToArgs(self, djangoClass, message):
+        args={}
+        fmap={}
+        fset={}
+        for field in djangoClass._meta.fields:
+            fmap[field.name] = field
+            if field.get_internal_type() == "ForeignKey":
+               # foreign key can be represented as an id
+               fmap[field.name + "_id"] = field
+
+        for (fieldDesc, val) in message.ListFields():
+            name = fieldDesc.name
+            if name in fmap:
+                if (name=="id"):
+                    # don't let anyone set the id
+                    continue
+                ftype = fmap[name].get_internal_type()
+                if (ftype == "CharField") or (ftype == "TextField") or (ftype == "SlugField"):
+                    args[name] = val
+                elif (ftype == "BooleanField"):
+                    args[name] = val
+                elif (ftype == "AutoField"):
+                    args[name] = val
+                elif (ftype == "IntegerField") or (ftype == "PositiveIntegerField"):
+                    args[name] = val
+                elif (ftype == "ForeignKey"):
+                    args[name] = val # field name already has "_id" at the end
+                elif (ftype == "DateTimeField"):
+                    pass # do something special here
+                elif (ftype == "FloatField"):
+                    args[name] = val
+                elif (ftype == "GenericIPAddressField"):
+                    args[name] = val
+                fset[name] = True
+
+        return args
+
     def querysetToProto(self, djangoClass, queryset):
         objs = queryset
         p_objs = self.getPluralProtoClass(djangoClass)()
@@ -83,12 +113,37 @@
         obj = djangoClass.objects.get(id=id)
         return self.objToProto(obj)
 
+    def create(self, djangoClass, request):
+        args = self.protoToArgs(djangoClass, request)
+        new_obj = djangoClass(**args)
+        new_obj.save()
+        return self.objToProto(new_obj)
+
+    def update(self, djangoClass, id, message):
+        obj = djangoClass.objects.get(id=id)
+        args = self.protoToArgs(djangoClass, message)
+        for (k,v) in args.iteritems():
+            setattr(obj, k, v)
+        obj.save()
+        return self.objToProto(obj)
+
 {% for object in generator.all() %}
     def List{{ object.camel() }}(self, request, context):
       return self.querysetToProto({{ object.camel() }}, {{ object.camel() }}.objects.all())
 
     def Get{{ object.camel() }}(self, request, context):
       return self.get({{ object.camel() }}, request.id)
+
+    def Create{{ object.camel() }}(self, request, context):
+      return self.create({{ object.camel() }}, request)
+
+    def Delete{{ object.camel() }}(self, request, context):
+      {{ object.camel() }}.objects.get(id=request.id).delete()
+      return Empty()
+
+    def Update{{ object.camel() }}(self, request, context):
+      return self.update({{ object.camel() }}, request.id, request)
+
 {% endfor %}
 
 
diff --git a/xos/tools/apigen/protobuf.template.txt b/xos/tools/apigen/protobuf.template.txt
index 6daefdc..e3f5790 100644
--- a/xos/tools/apigen/protobuf.template.txt
+++ b/xos/tools/apigen/protobuf.template.txt
@@ -6,10 +6,15 @@
 import "google/api/annotations.proto";
 import "common.proto";
 
+// Note: all fields are wrapped in a "oneof". This causes proto3 to always send
+// fields that are set by the caller, regardless if they are set to a default
+// value. XOS uses this to know when to apply a default value.
+
 {% for object in generator.all() %}
 
 message {{ object.camel() }} {
-  {%- for field in object.all_fields -%}
+  {%- for field in object.all_fields %}
+    oneof {{ field.name }}_present {
     {%- if (field.get_internal_type() == "CharField") or (field.get_internal_type() == "TextField") or (field.get_internal_type() == "SlugField") %}
       string {{ field.name }} = {{ loop.index }};
     {%- elif field.get_internal_type() == "BooleanField" %}
@@ -31,6 +36,7 @@
     {%- else %}
       UNKNOWN {{ field.get_internal_type() }} {{ field.name }} = {{ loop.index }};
     {%- endif %}
+    }
   {%- endfor %}
 }
 
@@ -52,6 +58,23 @@
             get: "/api/v1/{{ object.plural() }}/{id}"
         };
   }
+  rpc Create{{ object.camel() }}({{ object.camel() }}) returns ({{ object.camel() }}) {
+        option (google.api.http) = {
+            post: "/api/v1/{{ object.plural() }}"
+            body: "*"
+        };
+  }
+  rpc Update{{ object.camel() }}({{ object.camel() }}) returns ({{ object.camel() }}) {
+        option (google.api.http) = {
+            put: "/api/v1/{{ object.plural() }}/{id}"
+            body: "*"
+        };
+  }
+  rpc Delete{{ object.camel() }}(ID) returns (google.protobuf.Empty) {
+        option (google.api.http) = {
+            delete: "/api/v1/{{ object.plural() }}/{id}"
+        };
+  }
 {% endfor %}
 }