Skip to content
This repository has been archived by the owner on Jan 26, 2021. It is now read-only.

Commit

Permalink
Parameterize uri resolution and parsing of options, use package:path.
Browse files Browse the repository at this point in the history
This helps make the compiler more configurable
to embed it in other systems (like pub transformers)

R=cbracken@google.com

Review URL: https://chromiumcodereview.appspot.com//269823003
  • Loading branch information
sigmundch committed May 16, 2014
1 parent c7539dd commit e94fb50
Show file tree
Hide file tree
Showing 16 changed files with 246 additions and 169 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ packages
.project
.buildlog
out
*.sw?
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ PLUGIN_SRC = \
lib/file_generator.dart \
lib/indenting_writer.dart \
lib/message_generator.dart \
lib/options.dart \
lib/output_config.dart \
lib/protobuf_field.dart \
lib/protoc.dart \
lib/src/descriptor.pb.dart \
Expand Down Expand Up @@ -43,7 +45,7 @@ PREGENERATED_SRCS=lib/descriptor.proto lib/plugin.proto
$(PLUGIN_PATH): $(PLUGIN_SRC)
[ -d $(OUTPUT_DIR) ] || mkdir $(OUTPUT_DIR)
# --categories=all is a hack, it should be --categories=Server once dart2dart bug is fixed.
dart2js --checked --output-type=dart --package-root=packages --categories=all -o$(PLUGIN_PATH) bin/protoc_plugin.dart
dart2js --checked --output-type=dart --show-package-warnings --categories=all -o$(PLUGIN_PATH) bin/protoc_plugin.dart
dart prepend.dart $(PLUGIN_PATH)
chmod +x $(PLUGIN_PATH)

Expand All @@ -65,7 +67,7 @@ update-pregenerated: $(PLUGIN_PATH) $(PREGENERATED_SRCS)
build-test-protos: $(TEST_PROTO_LIBS)

run-tests: build-test-protos
dart --checked --package-root=packages/ test/all_tests.dart
dart --checked test/all_tests.dart

clean:
rm -rf $(OUTPUT_DIR)
2 changes: 1 addition & 1 deletion bin/protoc_plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';
import 'package:protoc-plugin/protoc.dart';
import 'package:protoc_plugin/protoc.dart';

void main() {
new CodeGenerator(stdin, stdout, stderr).generate();
Expand Down
71 changes: 12 additions & 59 deletions lib/code_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ class CodeGenerator extends ProtobufContainer {

CodeGenerator(this._streamIn, this._streamOut, this._streamErr);

void generate() {
/// Runs the code generator. The optional [optionParsers] can be used to
/// change how command line options are parsed (see [parseGenerationOptions]
/// for details), and [outputConfiguration] can be used to override where
/// generated files are created and how imports between generated files are
/// constructed (see [OutputConfiguration] for details).
void generate({
Map<String, SingleOptionParser> optionParsers,
OutputConfiguration outputConfiguration}) {
_streamIn
.fold(new BytesBuilder(), (builder, data) => builder..add(data))
.then((builder) => builder.takeBytes())
Expand All @@ -27,13 +34,15 @@ class CodeGenerator extends ProtobufContainer {
var response = new CodeGeneratorResponse();

// Parse the options in the request. Return the errors is any.
var options = new GenerationOptions(request, response);
var options = parseGenerationOptions(
request, response, optionParsers);
if (options == null) {
_streamOut.add(response.writeToBuffer());
return;
}

var ctx = new GenerationContext(options);
var ctx = new GenerationContext(options, outputConfiguration == null
? new DefaultOutputConfiguration() : outputConfiguration);
List<FileGenerator> generators = <FileGenerator>[];
for (FileDescriptorProto file in request.protoFile) {
var generator = new FileGenerator(file, this, ctx);
Expand All @@ -52,59 +61,3 @@ class CodeGenerator extends ProtobufContainer {
String get classname => null;
String get fqname => '';
}


class GenerationOptions {
final Map<String, String> fieldNameOptions;

GenerationOptions._(this.fieldNameOptions);

// Parse the options in the request. If there was an error in the
// options null is returned and the error is set on the response.
factory GenerationOptions(request, response) {
var fieldNameOptions = <String, String>{};
var parameter = request.parameter != null ? request.parameter : '';
List<String> options = parameter.trim().split(',');
List<String> errors = [];
for (var option in options) {
option = option.trim();
if (option.isEmpty) continue;
List<String> nameValue = option.split('=');
if (nameValue.length != 1 && nameValue.length != 2) {
errors.add('Illegal option: $option');
continue;
}
String name = nameValue[0].trim();
String value;
if (nameValue.length > 1) value = nameValue[1].trim();
if (name == 'field_name') {
if (value == null) {
errors.add('Illegal option: $option');
continue;
}
List<String> fromTo = value.split('|');
if (fromTo.length != 2) {
errors.add('Illegal option: $option');
continue;
}
var fromName = fromTo[0].trim();
var toName = fromTo[1].trim();
if (fromName.length == 0 || toName.length == 0) {
errors.add('Illegal option: $option');
continue;
}
fieldNameOptions['.$fromName'] = toName;
} else {
errors.add('Illegal option: $option');
}
}
if (errors.length > 0) {
response.error = errors.join('\n');
return null;
} else {
return new GenerationOptions._(fieldNameOptions);
}
}

String fieldNameOption(String fqname) => fieldNameOptions[fqname];
}
55 changes: 8 additions & 47 deletions lib/file_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,6 @@ class FileGenerator extends ProtobufContainer {
return index == -1 ? fileName : fileName.substring(0, index);
}

// Create the URI for the generated Dart file from the URI of the
// .proto file.
Uri _generatedFilePath(Uri protoFilePath) {
var dartFileName = _fileNameWithoutExtension(protoFilePath) + ".pb.dart";
return protoFilePath.resolve(dartFileName);
}

String _generateClassName(Uri protoFilePath) {
String s = _fileNameWithoutExtension(protoFilePath).replaceAll('-', '_');
return '${s[0].toUpperCase()}${s.substring(1)}';
Expand All @@ -56,49 +49,15 @@ class FileGenerator extends ProtobufContainer {
return _fileNameWithoutExtension(protoFilePath).replaceAll('-', '_');
}

Uri _relative(Uri target, Uri base) {
// Ignore the last segment of the base.
List<String> baseSegments =
base.pathSegments.sublist(0, base.pathSegments.length - 1);
List<String> targetSegments = target.pathSegments;
if (baseSegments.length == 1 && baseSegments[0] == '.') {
baseSegments = [];
}
if (targetSegments.length == 1 && targetSegments[0] == '.') {
targetSegments = [];
}
int common = 0;
int length = min(targetSegments.length, baseSegments.length);
while (common < length && targetSegments[common] == baseSegments[common]) {
common++;
}

final segments = <String>[];
if (common < baseSegments.length && baseSegments[common] == '..') {
throw new ArgumentError(
"Cannot create a relative path from $base to $target");
}
for (int i = common; i < baseSegments.length; i++) {
segments.add('..');
}
for (int i = common; i < targetSegments.length; i++) {
segments.add('${targetSegments[i]}');
}
if (segments.isEmpty) {
segments.add('.');
}
return new Uri(pathSegments: segments);
}

CodeGeneratorResponse_File generateResponse() {
MemoryWriter writer = new MemoryWriter();
IndentingWriter out = new IndentingWriter(' ', writer);

generate(out);

Uri filePath = new Uri(scheme: 'file', path: _fileDescriptor.name);
Uri filePath = new Uri.file(_fileDescriptor.name);
return new CodeGeneratorResponse_File()
..name = _generatedFilePath(filePath).path
..name = _context.outputConfiguration.outputPathFor(filePath).path
..content = writer.toString();
}

Expand Down Expand Up @@ -127,12 +86,13 @@ class FileGenerator extends ProtobufContainer {
// protoc should never generate an import with an absolute path.
throw("FAILURE: Import with absolute path is not supported");
}
// Create a relative path from the current file to the import.
Uri relativeProtoPath = _relative(importPath, filePath);
// Create a path from the current file to the imported proto.
Uri resolvedImport = _context.outputConfiguration.resolveImport(
importPath, filePath);
// Find the file generator for this import as it contains the
// package name.
FileGenerator fileGenerator = _context.lookupFile(import);
out.print("import '${_generatedFilePath(relativeProtoPath)}'");
out.print("import '$resolvedImport'");
if (package != fileGenerator.package && !fileGenerator.package.isEmpty) {
out.print(' as ${fileGenerator.packageImportPrefix}');
}
Expand Down Expand Up @@ -175,12 +135,13 @@ class FileGenerator extends ProtobufContainer {

class GenerationContext {
final GenerationOptions options;
final OutputConfiguration outputConfiguration;
final Map<String, ProtobufContainer> _registry =
<String, ProtobufContainer>{};
final Map<String, FileGenerator> _files =
<String, FileGenerator>{};

GenerationContext(this.options);
GenerationContext(this.options, this.outputConfiguration);

void register(ProtobufContainer container) {
_registry[container.fqname] = container;
Expand Down
117 changes: 117 additions & 0 deletions lib/options.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of protoc;

/// Helper function implementing a generic option parser that reads
/// `request.parameters` and treats each token as either a flag ("name") or a
/// key-value pair ("name=value"). For each option "name", it looks up whether a
/// [SingleOptionParser] exists in [parsers] and delegates the actual parsing of
/// the option to it. Returns `true` if no errors were reported.
bool genericOptionsParser(
CodeGeneratorRequest request, CodeGeneratorResponse response,
Map<String, SingleOptionParser> parsers) {
var parameter = request.parameter != null ? request.parameter : '';
var options = parameter.trim().split(',');
var map = <String, String>{};
var errors = [];

for (var option in options) {
option = option.trim();
if (option.isEmpty) continue;
var reportError = (details) {
errors.add('Error found trying to parse the option: $option.\n$details');
};

var nameValue = option.split('=');
if (nameValue.length != 1 && nameValue.length != 2) {
reportError('Options should be a single token, or a name=value pair');
continue;
}
var name = nameValue[0].trim();
var parser = parsers[name];
if (parser == null) {
reportError('Unknown option ($name).');
continue;
}

var value = nameValue.length > 1 ? nameValue[1].trim() : null;
parser.parse(name, value, reportError);
}

if (errors.length == 0) return true;

response.error = errors.join('\n');
return false;
}

/// Options expected by the protoc code generation compiler.
class GenerationOptions {
/// Maps a fully qualified field name, to the desired name we wish to
/// generate. For example `MyMessage.has_field` to `HasFld`.
final Map<String, String> fieldNameOverrides;

GenerationOptions(this.fieldNameOverrides);
}

/// A parser for a name-value pair option. Options parsed in
/// [genericOptionsParser] delegate to instances of this class to
/// parse the value of a specific option.
abstract class SingleOptionParser {

/// Parse the [name]=[value] value pair and report any errors to [onError]. If
/// the option is a flag, [value] will be null. Note, [name] is commonly
/// unused. It is provided because [SingleOptionParser] can be registered for
/// multiple option names in [genericOptionsParser].
void parse(String name, String value, onError(String details));
}

/// Parser used by the compiler, which supports the `field_name` option (see
/// [FieldNameOptionParser]) and any additional option added in [parsers]. If
/// [parsers] has a key for `field_name`, it will be ignored.
GenerationOptions parseGenerationOptions(
CodeGeneratorRequest request, CodeGeneratorResponse response,
[Map<String, SingleOptionParser> parsers]) {
var fieldNameOptionParser = new FieldNameOptionParser();
var map = {};
if (parsers != null) parsers.forEach((k, v) { map[k] = v; });
map['field_name'] = fieldNameOptionParser;
if (genericOptionsParser(request, response, map)) {
return new GenerationOptions(fieldNameOptionParser.mappings);
}
return null;
}

/// A [SingleOptionParser] to parse the `field_name` option. This option
/// overrides the default name given to some fields that would otherwise collide
/// with existing field names in Dart core objects or in [GeneratedMessage].
/// (see `README.md` for details).
class FieldNameOptionParser implements SingleOptionParser {
/// Maps a fully qualified field name, to the desired name we wish to
/// generate. For example `MyMessage.has_field` to `HasFld`.
final Map<String, String> mappings = {};

void parse(String name, String value, onError(String message)) {
if (value == null) {
onError('Invalid field_name option, expected a non-emtpy value.');
return;
}

List<String> fromTo = value.split('|');
if (fromTo.length != 2) {
onError('Invalid field_name option, expected a single "|" separator.');
return;
}

var fromName = fromTo[0].trim();
var toName = fromTo[1].trim();
if (fromName.isEmpty || toName.isEmpty) {
onError('Invalid field_name option, '
'"from" and "to" names should not be empty.');
return;
}

mappings['.$fromName'] = toName;
}
}
44 changes: 44 additions & 0 deletions lib/output_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of protoc;

/// Configures where output of the protoc compiler should be placed and how to
/// import one generated file from another.
abstract class OutputConfiguration {

/// Returns [filePath] with it's extension replaced with '.pb.dart'.
String replacePathExtension(String filePath) =>
'${path.withoutExtension(filePath)}.pb.dart';

/// Returns [file] with it's extension replaced with '.pb.dart'.
Uri replaceUriExtension(Uri file) =>
path.url.toUri(replacePathExtension(path.url.fromUri(file)));

/// Resolves an import URI. Both [source] and [target] are .proto files,
/// where [target] is imported from [source]. The result URI can be used to
/// import [target]'s .pb.dart output from [source]'s .pb.dart output.
Uri resolveImport(Uri target, Uri source);

/// Returns the path, under the output folder, where the code will be
/// generated for [inputPath]. The input is expected to be a .proto file,
/// while the output is expected to be a .pb.dart file.
Uri outputPathFor(Uri inputPath);
}

/// Default [OutputConfiguration] that uses the same path as the input
/// file for the output file (just replaces the extension), and that uses
/// relative paths to resolve imports.
class DefaultOutputConfiguration extends OutputConfiguration {

Uri outputPathFor(Uri input) => replaceUriExtension(input);

Uri resolveImport(Uri target, Uri source) {
var builder = path.url;
var targetPath = builder.fromUri(target);
var sourceDir = builder.dirname(builder.fromUri(source));
return builder.toUri(replacePathExtension(
builder.relative(targetPath, from: sourceDir)));
}
}
Loading

0 comments on commit e94fb50

Please sign in to comment.