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 %}
}