Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions protoc_plugin/lib/protoc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import 'mixins.dart';
import 'names.dart';
import 'src/code_generator.dart';
import 'src/gen/dart_options.pb.dart';
import 'src/gen/google/api/annotations.pb.dart';
import 'src/gen/google/api/client.pb.dart';
import 'src/gen/google/api/http.pb.dart';
import 'src/gen/google/api/routing.pb.dart';
import 'src/gen/google/protobuf/compiler/plugin.pb.dart';
import 'src/gen/google/protobuf/descriptor.pb.dart';
import 'src/grpc_annotations.dart';
import 'src/linker.dart';
import 'src/options.dart';
import 'src/output_config.dart';
Expand Down
7 changes: 7 additions & 0 deletions protoc_plugin/lib/src/code_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import 'package:protobuf/protobuf.dart';
import '../names.dart' show lowerCaseFirstLetter;
import '../protoc.dart' show FileGenerator;
import 'gen/dart_options.pb.dart';
import 'gen/google/api/annotations.pb.dart';
import 'gen/google/api/client.pb.dart';
import 'gen/google/api/routing.pb.dart';
import 'gen/google/protobuf/compiler/plugin.pb.dart';
import 'linker.dart';
import 'options.dart';
Expand Down Expand Up @@ -90,6 +92,11 @@ class CodeGenerator {

Dart_options.registerAllExtensions(extensions);
Client.registerAllExtensions(extensions);
// The 'http' annotation.
Annotations.registerAllExtensions(extensions);
// todo: firestore might not use the 'routing' annotation
// The 'routing' annotation.
Routing.registerAllExtensions(extensions);

final builder = await _streamIn.fold(
BytesBuilder(),
Expand Down
37 changes: 37 additions & 0 deletions protoc_plugin/lib/src/gen/google/api/annotations.pb.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// This is a generated file - do not edit.
//
// Generated from google/api/annotations.proto.

// @dart = 3.3

// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names

import 'dart:core' as $core;

import 'package:protobuf/protobuf.dart' as $pb;

import 'http.pb.dart' as $0;

export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;

class Annotations {
static final http = $pb.Extension<$0.HttpRule>(
_omitMessageNames ? '' : 'google.protobuf.MethodOptions',
_omitFieldNames ? '' : 'http',
72295728,
$pb.PbFieldType.OM,
defaultOrMaker: $0.HttpRule.getDefault,
subBuilder: $0.HttpRule.create);
static void registerAllExtensions($pb.ExtensionRegistry registry) {
registry.add(http);
}
}

const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');
11 changes: 11 additions & 0 deletions protoc_plugin/lib/src/gen/google/api/annotations.pbenum.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This is a generated file - do not edit.
//
// Generated from google/api/annotations.proto.

// @dart = 3.3

// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names
7 changes: 6 additions & 1 deletion protoc_plugin/lib/src/gen/google/protobuf/descriptor.pb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2406,7 +2406,7 @@ class FieldOptions extends $pb.GeneratedMessage {
$core.bool? deprecated,
$core.bool? lazy,
FieldOptions_JSType? jstype,
$core.bool? weak,
@$core.Deprecated('This field is deprecated.') $core.bool? weak,
$core.bool? unverifiedLazy,
$core.bool? debugRedact,
FieldOptions_OptionRetention? retention,
Expand Down Expand Up @@ -2605,13 +2605,18 @@ class FieldOptions extends $pb.GeneratedMessage {
@$pb.TagNumber(6)
void clearJstype() => $_clearField(6);

/// DEPRECATED. DO NOT USE!
/// For Google-internal migration only. Do not use.
@$core.Deprecated('This field is deprecated.')
@$pb.TagNumber(10)
$core.bool get weak => $_getBF(5);
@$core.Deprecated('This field is deprecated.')
@$pb.TagNumber(10)
set weak($core.bool value) => $_setBool(5, value);
@$core.Deprecated('This field is deprecated.')
@$pb.TagNumber(10)
$core.bool hasWeak() => $_has(5);
@$core.Deprecated('This field is deprecated.')
@$pb.TagNumber(10)
void clearWeak() => $_clearField(10);

Expand Down
247 changes: 247 additions & 0 deletions protoc_plugin/lib/src/grpc_annotations.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// Copyright (c) 2025, 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.

import 'package:collection/collection.dart';

import 'gen/google/api/http.pb.dart';

class PathTemplate {
final List<PathSegment> segments;
final String? verb;

PathTemplate({required this.segments, this.verb});

factory PathTemplate.parse(String path) {
// Path template syntax:
//
// Template = "/" Segments [ Verb ] ;
// Segments = Segment { "/" Segment } ;
// Segment = "*" | "**" | LITERAL | Variable ;
// Variable = "{" FieldPath [ "=" Segments ] "}" ;
// FieldPath = IDENT { "." IDENT } ;
// Verb = ":" LITERAL ;

String? verb;

// Look for a trailing ":<verb>".
if (path.contains(':')) {
final index = path.indexOf(':');
verb = path.substring(index + 1);
path = path.substring(0, index);
}

// parse into variable and literal segments
final segments =
_findSegments(path).map((str) {
return str.startsWith('{')
? PathVariablePathSegment(
PathVariable.parse(str.substring(1, str.length - 1)),
)
: LiteralPathSegment(str);
}).toList();

return PathTemplate(segments: segments, verb: verb);
}

static List<PathTemplate> parseRules(List<HttpRule> rules) {
// get put post delete patch

final result = <PathTemplate>[];

for (final rule in rules) {
String? template;

if (rule.hasGet()) {
template = rule.get;
} else if (rule.hasPut()) {
template = rule.put;
} else if (rule.hasPost()) {
template = rule.post;
} else if (rule.hasDelete()) {
template = rule.delete;
} else if (rule.hasPatch()) {
template = rule.patch;
}

if (template != null) {
result.add(PathTemplate.parse(template));
}
}

return result;
}

@override
String toString() => '${segments.join('/')}${verb == null ? '' : ':$verb'}';
}

/// Either a [LiteralPathSegment] or a [PathVariablePathSegment].
abstract class PathSegment {}

final class LiteralPathSegment extends PathSegment {
final String literal;

LiteralPathSegment(this.literal);

@override
bool operator ==(Object other) {
return other is LiteralPathSegment && other.literal == literal;
}

@override
int get hashCode => literal.hashCode;

@override
String toString() => literal;
}

final class PathVariablePathSegment extends PathSegment {
final PathVariable variable;

PathVariablePathSegment(this.variable);

@override
bool operator ==(Object other) {
return other is PathVariablePathSegment && other.variable == variable;
}

@override
int get hashCode => variable.hashCode;

@override
String toString() => '{$variable}';
}

class PathVariable {
final List<String> fieldPath;
final List<String> segments;

PathVariable({required this.fieldPath, required this.segments});

factory PathVariable.parse(String str) {
// name=projects/*/databases/*/documents/*/**
final index = str.indexOf('=');

if (index == -1) {
// parse path variable shorthand
return PathVariable(fieldPath: [str], segments: ['*']);
} else {
final field = str.substring(0, index);
final segments = str.substring(index + 1);

return PathVariable(
fieldPath: field.split('.'),
segments: segments.split('/'),
);
}
}

/// A dot-concanenated version of the field path.
String get name => fieldPath.join('.');

@override
bool operator ==(Object other) {
return other is PathVariable &&
ListEquality().equals(other.fieldPath, fieldPath) &&
ListEquality().equals(other.segments, segments);
}

@override
int get hashCode => Object.hashAll(fieldPath) ^ Object.hashAll(segments);

@override
String toString() => '${fieldPath.join('.')}=${segments.join('/')}';
}

/// Return a sequence of either path variables or segments.
List<String> _findSegments(String str) {
// /v1/{parent=projects/*/databases/*/documents/*/**}/{collection_id}
// =>
// v1, {parent=projects/*/databases/*/documents/*/**}, {collection_id}

final result = <String>[];

while (str.isNotEmpty) {
var index = str.indexOf('{');

if (index == 0) {
// extract the path variable
final end = str.indexOf('}');
result.add(str.substring(0, end + 1));
str = str.substring(end + 1);
} else if (index == -1) {
// extract the sequence of segments
result.addAll(str.split('/').where((segment) => segment.isNotEmpty));
str = '';
} else {
// extract the sequence of segments
final segments = str.substring(0, index);
result.addAll(segments.split('/').where((segment) => segment.isNotEmpty));
str = str.substring(index);
}
}

return result;
}

extension PathVariableExt on PathVariable {
/// Create a regex definition to match this path variable.
String createRegexMatcher() {
return segments
.map((string) {
switch (string) {
case '*':
return '[^/]*';
case '**':
return '.*';
default:
return string;
}
})
.join('/');
}

/// Return the field path in camel-case format.
String get fieldPathCamelCase {
return fieldPath.map((str) => snakeToCamelCase(str)).join('.');
}

/// Generate a condition which we can use to know whether a field in a proto
/// is populated.
String protoRequestPath(String prefix) {
// Generate something like:
//
// request.hasDocument() && request.document.hasName()

final result = <String>[];

for (int i = 0; i < fieldPath.length; i++) {
final path = fieldPath.sublist(0, i + 1);

final accessor = path
.sublist(0, path.length - 1)
.map((str) => snakeToCamelCase(str))
.join('.');
final hazzor = '.has${snakeToCamelCase(path.last.titleCase)}()';

result.add('$prefix${accessor.isEmpty ? '' : '.$accessor'}$hazzor');
}

return result.join(' && ');
}
}

extension StringExt on String {
String get titleCase => substring(0, 1).toUpperCase() + substring(1);
}

/// Convert snake case to camel case (`foo_bar` => `fooBar`).
String snakeToCamelCase(String str) {
final items = str.split('_');
return items.first +
items
.skip(1)
.map((str) => str[0].toUpperCase() + str.substring(1))
.join('');
}
Loading