blob: c1a66f0a3473b7a28fe7c40d91a78967415ee3c1 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// TODO(https://fxbug.dev/84961): Fix null safety and remove this language version.
// @dart=2.9
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:protoc_plugin/protoc.dart';
/// This is the alternate plugin driver for dart protobuf bindings. This
/// plugin driver is suppled into the `protoc` (or protoc_wrapper.py). This
/// driver is used alongside GN, takes in a mapping of dart package name -> dart
/// package paths, and uses these mappings to transform strictly file-based
/// imports to package-based imports.
///
/// The plugin option passed to `protoc` end up here. The option format is:
/// A semicolon-separated list of package name -> package directory mappings of
/// the form `package_name|input_root`. `input_root` designates the directory
/// relative to which input protos are located -- typically the root of the GN
/// package, where the `BUILD.gn` file is located.
///
/// The options above are used in the following way:
/// * Given a file-based import path in the .proto file, the generated dart
/// import path is rebased to a package path if possible.
/// * See [DartImportMappingOutputConfiguration.resolveImport] below.
class DartImportMappingOptionParser extends SingleOptionParser {
/// This is the option key we pass to protoc;
static const String kImportOptionKey = 'PackagePaths';
/// Output map of: package input root => package name
final Map<String, String> packageMapping;
DartImportMappingOptionParser(this.packageMapping);
@override
void parse(String name, String value, onError(String message)) {
if (value == null) {
onError('Invalid $kImportOptionKey option. Expected a non-empty value.');
return;
}
for (var entry in value.split(';')) {
var fields = entry.split('|');
if (fields.length != 2) {
onError('ERROR: expected package_name|input_root. Got: $entry');
continue;
}
if (packageMapping.containsKey(fields[1])) {
onError('ERROR: ${fields[1]} already exists.');
continue;
}
packageMapping[fields[1]] = fields[0];
}
}
}
class DartImportMappingOutputConfiguration extends DefaultOutputConfiguration {
/// Output map (package root) => package name
final Map<String, String> packageMapping;
DartImportMappingOutputConfiguration(this.packageMapping);
/// Given a file-based import path ("a/b/c/d.proto"), this routine looks
/// through the [packageMapping] to find a package that this import may fall
/// under. If it can find one, it rebases the import path to the package
/// input root in the generated import string.
@override
Uri resolveImport(Uri target, Uri source, String extension) {
for (int i = target.pathSegments.length; i > 0; i--) {
var candidatePath = target.pathSegments.sublist(0, i).join('/');
if (packageMapping.containsKey(candidatePath)) {
var packageName = packageMapping[candidatePath];
var importPath = target.pathSegments
.sublist(i, target.pathSegments.length)
.join('/');
var genImportPath = '${path.withoutExtension(importPath)}.pb.dart';
return Uri.parse('package:$packageName/$genImportPath');
}
}
return super.resolveImport(target, source, extension);
}
}
void main() {
var packageMapping = <String, String>{};
CodeGenerator(stdin, stdout).generate(
optionParsers: {
DartImportMappingOptionParser.kImportOptionKey:
DartImportMappingOptionParser(packageMapping),
},
config: DartImportMappingOutputConfiguration(packageMapping),
);
}