Add php protobuffer support for transition to GTFS-realtime
[busui.git] / lib / Protobuf-PHP / library / DrSlump / Protobuf / Compiler / JsonGenerator.php
blob:a/lib/Protobuf-PHP/library/DrSlump/Protobuf/Compiler/JsonGenerator.php -> blob:b/lib/Protobuf-PHP/library/DrSlump/Protobuf/Compiler/JsonGenerator.php
--- a/lib/Protobuf-PHP/library/DrSlump/Protobuf/Compiler/JsonGenerator.php
+++ b/lib/Protobuf-PHP/library/DrSlump/Protobuf/Compiler/JsonGenerator.php
@@ -1,1 +1,353 @@
-
+<?php
+
+namespace DrSlump\Protobuf\Compiler;
+
+use DrSlump\Protobuf;
+use google\protobuf as proto;
+
+class JsonGenerator extends AbstractGenerator
+{
+    public function getNamespace(proto\FileDescriptorProto $proto)
+    {
+        $namespace = $proto->getPackage();
+        $opts = $proto->getOptions();
+        if (isset($opts['json.package'])) {
+            $namespace = $opts['jsonpackage'];
+        }
+        if (isset($opts['json.namespace'])) {
+            $namespace = $opts['json.namespace'];
+        }
+
+        $namespace = trim($namespace, '.');
+        return $namespace;
+    }
+
+    public function compileEnum(proto\EnumDescriptorProto $enum, $namespace)
+    {
+        $s[]= "$namespace.$enum->name = {";
+        $lines = array();
+        foreach ($enum->getValueList() as $value) {
+            $lines[] = "  /** @const */ $value->name: $value->number";
+        }
+        $s[]= implode(",\n", $lines);
+        $s[]= '};';
+        $s[]= '';
+        return implode("\n", $s);
+    }
+
+    public function compileExtension(proto\FieldDescriptorProto $field, $ns, $indent)
+    {
+        $extendee = $this->normalizeReference($field->getExtendee());
+        $name = $field->getName();
+        if ($ns) {
+            $name = $ns . '.' . $name;
+        }
+        $field->setName($name);
+
+        $s[]= "ProtoJson.extend($extendee, {";
+        $s[]= "  $field->number: " . $this->generateField($field);
+        $s[]= "});";
+        $s[]= '';
+
+        return $indent . implode("\n$indent", $s);
+    }
+
+    public function compileMessage(proto\DescriptorProto $msg, $namespace)
+    {
+        $s[]= "/**";
+        $s[]= " * @constructor";
+        $s[]= " * @augments {ProtoJson.Message}";
+        $s[]= " * @extends ProtoJson.Message";
+        $s[]= " * @memberOf $namespace";
+        $s[]= " * @param {object} data - Optional, provide initial data to parse";
+        $s[]= " */";
+        $s[]= "$namespace.$msg->name = ProtoJson.create({";
+        $s[]= "  fields: {";
+
+        $lines = array();
+        foreach ($msg->getFieldList() as $field) {
+            $lines[] = "    $field->number: " . $this->generateField($field);
+        }
+        $s[] = implode(",\n", $lines);
+
+        $s[]= "  },";
+        $s[]= "  ranges: [";
+        // @todo dump extension ranges
+        $s[]= "  ]";
+        $s[]= "});";
+        $s[]= "";
+
+        // Compute a new namespace with the message name as suffix
+        $namespace .= "." . $msg->getName();
+
+        // Generate getters/setters
+        foreach ($msg->getFieldList() as $field) {
+            $s[]= $this->generateAccessors($field, $namespace);
+        }
+
+        // Generate Enums
+        foreach ($msg->getEnumTypeList() as $enum):
+        $s[]= $this->compileEnum($enum, $namespace);
+        endforeach;
+
+        // Generate nested messages
+        foreach ($msg->getNestedTypeList() as $msg):
+        $s[]= $this->compileMessage($msg, $namespace);
+        endforeach;
+
+        // Collect extensions
+        foreach ($msg->getExtensionList() as $field) {
+            $this->extensions[$field->getExtendee()][] = array($namespace, $field);
+        }
+
+        return implode("\n", $s);
+    }
+
+    public function compileProtoFile(proto\FileDescriptorProto $proto)
+    {
+        $file = new proto\compiler\CodeGeneratorResponse\File();
+
+        $opts = $proto->getOptions();
+        $name = pathinfo($proto->getName(), PATHINFO_FILENAME);
+        $name .= isset($opts['json.suffix'])
+                 ? $opts['json.suffix']
+                 : '.js';
+        $file->setName($name);
+
+        $namespace = $this->getNamespace($proto);
+
+        $s[]= "// DO NOT EDIT! Generated by Protobuf for PHP protoc plugin " . Protobuf::VERSION;
+        $s[]= "// Source: " . $proto->getName();
+        $s[]= "//   Date: " . date('Y-m-d H:i:s');
+        $s[]= "";
+
+        $s[]= "(function(){";
+        $s[]= "/** @namespace */";
+        $s[]= "var $namespace = $namespace || {};";
+        $s[]= "";
+        $s[]= "// Make it CommonJS compatible";
+        $s[]= "if (typeof exports !== 'undefined') {";
+        $s[]= "  var ProtoJson = this.ProtoJson;";
+        $s[]= "  if (!ProtoJson && typeof require !== 'undefined')";
+        $s[]= "    ProtoJson = require('ProtoJson');";
+        $s[]= "  $namespace = exports;";
+        $s[]= "} else {";
+        $s[]= "  this.$namespace = $namespace;";
+        $s[]= "}";
+        $s[]= "";
+
+
+        // Generate Enums
+        foreach ($proto->getEnumTypeList() as $enum) {
+            $s[]= $this->compileEnum($enum, $namespace);
+        }
+
+        // Generate Messages
+        foreach ($proto->getMessageTypeList() as $msg) {
+            $s[] = $this->compileMessage($msg, $namespace);
+        }
+
+        // Collect extensions
+        if ($proto->hasExtension()) {
+            foreach ($proto->getExtensionList() as $field) {
+                $this->extensions[$field->getExtendee()][] = array($namespace, $field);
+            }
+        }
+
+        // Dump all extensions found in this proto file
+        if (count($this->extensions)) {
+            foreach ($this->extensions as $extendee => $fields) {
+                foreach ($fields as $pair) {
+                    list($ns, $field) = $pair;
+                    $s[]= $this->compileExtension($field, $ns, '');
+                }
+            }
+        }
+
+        $s[]= "})();";
+
+        $src = implode("\n", $s);
+        $file->setContent($src);
+        return array($file);
+    }
+
+    public function generateField(proto\FieldDescriptorProto $field)
+    {
+        $reference = 'null';
+        if ($field->hasTypeName()) {
+            $reference = $field->getTypeName();
+            if (substr($reference, 0, 1) !== '.') {
+                throw new \RuntimeException('Only fully qualified names are supported: ' . $reference);
+            }
+            $reference = "'" . $this->normalizeReference($reference) . "'";
+        }
+
+        $default = 'null';
+        if ($field->hasDefaultValue()):
+            switch ($field->getType()) {
+            case Protobuf::TYPE_BOOL:
+                $default = $field->getDefaultValue() ? 'true' : 'false';
+                break;
+            case Protobuf::TYPE_STRING:
+                $default = '"' . addcslashes($field->getDefaultValue(), '"\\') . '"';
+                break;
+            case Protobuf::TYPE_ENUM:
+                $default = $this->normalizeReference($field->getTypeName()) . '.' . $field->getDefaultValue();
+                break;
+            default: // Numbers
+                $default = $field->getDefaultValue();
+            }
+        endif;
+
+        $data = array(
+            "'" . $field->getName() . "'",
+            $field->getLabel(),
+            $field->getType(),
+            $reference,
+            $default,
+            '{}'
+        );
+
+        return '[' . implode(', ', $data) . ']';
+    }
+
+    public function generateAccessors($field, $namespace)
+    {
+        $camel = $this->comp->camelize(ucfirst($field->getName()));
+
+        $s[]= "/**";
+        $s[]= " * Check <$field->name> value";
+        $s[]= " * @return {Boolean}";
+        $s[]= " */";
+        $s[]= "$namespace.prototype.has$camel = function(){";
+        $s[]= "  return this._has($field->number);";
+        $s[]= "};";
+        $s[]= "";
+
+        $s[]= "/**";
+        $s[]= " * Set a value for <$field->name>";
+        $s[]= " * @param {" . $this->getJsDoc($field) . "} value";
+        $s[]= " * @return {".  $namespace . "}";
+        $s[]= " */";
+        $s[]= "$namespace.prototype.set$camel = function(value){";
+        $s[]= "  return this._set($field->number, value);";
+        $s[]= "};";
+        $s[]= "";
+
+
+        $s[]= "/**";
+        $s[]= " * Clear the value of <$field->name>";
+        $s[]= " * @return {".  $namespace . "}";
+        $s[]= " */";
+        $s[]= "$namespace.prototype.clear$camel = function(){";
+        $s[]= "  return this._clear($field->number);";
+        $s[]= "};";
+        $s[]= "";
+
+
+        if ($field->getLabel() !== Protobuf::RULE_REPEATED):
+
+        $s[]= "/**";
+        $s[]= " * Get <$field->name> value";
+        $s[]= " * @return {" . $this->getJsDoc($field) . "}";
+        $s[]= " */";
+        $s[]= "$namespace.prototype.get$camel = function(){";
+        $s[]= "  return this._get($field->number);";
+        $s[]= "};";
+        $s[]= "";
+
+        else:
+
+        $s[]= "/**";
+        $s[]= " * Get an item from <$field->name>";
+        $s[]= " * @param {int} idx";
+        $s[]= " * @return {" . $this->getJsDoc($field) . "}";
+        $s[]= " */";
+        $s[]= "$namespace.prototype.get$camel = function(idx){";
+        $s[]= "  return this._get($field->number, idx);";
+        $s[]= "};";
+        $s[]= "";
+
+
+        $s[]= "/**";
+        $s[]= " * Get <$field->name> value";
+        $s[]= " * @return {" . $this->getJsDoc($field) . "[]}";
+        $s[]= " */";
+        $s[]= "$namespace.prototype.get{$camel}List = function(){";
+        $s[]= "  return this._get($field->number);";
+        $s[]= "};";
+        $s[]= "";
+
+        $s[]= "/**";
+        $s[]= " * Add a value to <$field->name>";
+        $s[]= " * @param {" . $this->getJsDoc($field) . "} value";
+        $s[]= " * @return {" . $namespace . "}";
+        $s[]= " */";
+        $s[]= "$namespace.prototype.add$camel = function(value){";
+        $s[]= "  return this._add($field->number, value);";
+        $s[]= "};";
+        $s[]= "";
+
+        endif;
+
+
+        return implode("\n", $s);
+    }
+
+    public function getJsDoc(proto\FieldDescriptorProto $field)
+    {
+        switch ($field->getType()) {
+            case Protobuf::TYPE_DOUBLE:
+            case Protobuf::TYPE_FLOAT:
+                return 'Float';
+            case Protobuf::TYPE_INT64:
+            case Protobuf::TYPE_UINT64:
+            case Protobuf::TYPE_INT32:
+            case Protobuf::TYPE_FIXED64:
+            case Protobuf::TYPE_FIXED32:
+            case Protobuf::TYPE_UINT32:
+            case Protobuf::TYPE_SFIXED32:
+            case Protobuf::TYPE_SFIXED64:
+            case Protobuf::TYPE_SINT32:
+            case Protobuf::TYPE_SINT64:
+                return 'Int';
+            case Protobuf::TYPE_BOOL:
+                return 'Boolean';
+            case Protobuf::TYPE_STRING:
+                return 'String';
+            case Protobuf::TYPE_MESSAGE:
+                return $this->normalizeReference($field->getTypeName());
+            case Protobuf::TYPE_BYTES:
+                return 'String';
+            case Protobuf::TYPE_ENUM:
+                return 'Int (' . $this->normalizeReference($field->getTypeName()) . ')';
+
+            case Protobuf::TYPE_GROUP:
+            default:
+                return 'unknown';
+        }
+    }
+
+    public function normalizeReference($reference)
+    {
+          // Remove leading dot
+          $reference = ltrim($reference, '.');
+
+          if (!$this->comp->hasPackage($reference)) {
+              $found = false;
+              foreach ($this->comp->getPackages() as $package=>$namespace) {
+                  if (0 === strpos($reference, $package.'.')) {
+                      $reference = $namespace . substr($reference, strlen($package));
+                      $found = true;
+                  }
+              }
+              if (!$found) {
+                  $this->comp->warning('Non tracked package name found "' . $reference . '"');
+              }
+          } else {
+              $reference = $this->comp->getPackage($reference);
+          }
+
+          return $reference;
+    }
+}