|
<?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; |
|
} |
|
} |