diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..c90128e3 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,46 @@ +version: 2.1 + +jobs: + # skipping build step because Gemfile.lock is not included in the source + # this makes the bundler caching step a noop + test: + parameters: + ruby-image: + type: string + docker: + - image: << parameters.ruby-image >> + steps: + - run: + name: Install protobuf compiler + command: | + archive=protoc-3.16.0-linux-x86_64 + curl -O -L https://github.com/protocolbuffers/protobuf/releases/download/v3.16.0/$archive.zip + sudo unzip -d '/usr/local' $archive.zip 'bin/*' 'include/*' + sudo chown -R $(whoami) /usr/local/bin/protoc /usr/local/include/google + rm -rf $archive.zip + - run: + command: protoc --version + - run: + command: sudo apt-get update + - run: + message: Install ZeroMQ + command: sudo apt-get -y install libzmq3-dev + - checkout + - run: + command: gem install bundler + - run: + command: bundle install + - run: + command: bundle exec rake + +workflows: + build_and_test: + jobs: + - test: + matrix: + parameters: + ruby-image: + - circleci/jruby:9.2.6.0-jdk + - circleci/jruby:9.1.17.0-jdk + - circleci/ruby:2.5 + - circleci/ruby:2.7 diff --git a/.gitignore b/.gitignore index 1c987c7e..00a05ea3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,21 @@ *.gem +*.swp pkg/* .bundle .rvmrc +.rspec *.log coverage doc .yardoc +.DS_Store +*.so +Gemfile.lock +tmp/* +ext/defs +ext/out +ext/ruby_generator/protoc-ruby +.ruby-gemset +.ruby-version +ext/ruby_generator/gdb.run +ext/ruby_generator/protoc-ruby.dSYM diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..ea05f74d --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,81 @@ +inherit_from: .rubocop_todo.yml + +AllCops: + DisplayCopNames: true + Exclude: + - 'spec/support/protos/*.pb.rb' + - 'varint_prof.rb' + +Bundler/DuplicatedGem: + Enabled: false + +Layout/EndAlignment: + EnforcedStyleAlignWith: keyword + +Lint/Loop: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Layout/CaseIndentation: + EnforcedStyle: end + +Style/ClassAndModuleChildren: + Exclude: + - '**/*.pb.rb' + - 'spec/**/*.rb' + +Naming/ClassAndModuleCamelCase: + Exclude: + - '**/*.pb.rb' + +Layout/EmptyLineBetweenDefs: + AllowAdjacentOneLineDefs: true + +Layout/EmptyLines: + Exclude: + - '**/*.pb.rb' + +Naming/FileName: + Exclude: + - '**/protoc-gen-ruby*' + +Style/GuardClause: + MinBodyLength: 4 + +Style/HashSyntax: + EnforcedStyle: hash_rockets + +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +Style/Semicolon: + AllowAsExpressionSeparator: true + +Style/SingleLineBlockParams: + Enabled: false + +Layout/TrailingEmptyLines: + Exclude: + - '**/*.pb.rb' + +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: comma + +Style/TrivialAccessors: + AllowDSLWriters: true + AllowPredicates: true + +Style/Encoding: + Exclude: + - '**/*.pb.rb' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..bd670fea --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,541 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2020-04-13 09:00:39 +0200 using RuboCop version 0.81.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: TreatCommentsAsGroupSeparators, Include. +# Include: **/*.gemspec +Gemspec/OrderedDependencies: + Exclude: + - 'protobuf.gemspec' + +# Offense count: 2 +# Configuration parameters: Include. +# Include: **/*.gemspec +Gemspec/RubyVersionGlobalsUsage: + Exclude: + - 'protobuf.gemspec' + +# Offense count: 2 +# Cop supports --auto-correct. +Layout/ClosingParenthesisIndentation: + Exclude: + - 'spec/lib/protobuf/generators/base_spec.rb' + - 'spec/lib/protobuf/rpc/service_spec.rb' + +# Offense count: 42 +# Cop supports --auto-correct. +Layout/EmptyLineAfterGuardClause: + Enabled: false + +# Offense count: 1 +# Cop supports --auto-correct. +Layout/EmptyLineAfterMagicComment: + Exclude: + - 'protobuf.gemspec' + +# Offense count: 83 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty_lines, no_empty_lines +Layout/EmptyLinesAroundBlockBody: + Enabled: false + +# Offense count: 75 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only +Layout/EmptyLinesAroundClassBody: + Enabled: false + +# Offense count: 39 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines +Layout/EmptyLinesAroundModuleBody: + Enabled: false + +# Offense count: 30 +# Cop supports --auto-correct. +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'Rakefile' + - 'lib/protobuf/field.rb' + - 'lib/protobuf/rpc/connectors/base.rb' + - 'spec/lib/protobuf/enum_spec.rb' + - 'spec/lib/protobuf/rpc/middleware/response_encoder_spec.rb' + +# Offense count: 7 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: squiggly, active_support, powerpack, unindent +Layout/HeredocIndentation: + Exclude: + - 'spec/lib/protobuf/generators/enum_generator_spec.rb' + - 'spec/lib/protobuf/generators/file_generator_spec.rb' + - 'spec/lib/protobuf/generators/service_generator_spec.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: symmetrical, new_line, same_line +Layout/MultilineArrayBraceLayout: + Exclude: + - 'spec/lib/protobuf/generators/field_generator_spec.rb' + - 'spec/lib/protobuf/message_spec.rb' + +# Offense count: 10 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: symmetrical, new_line, same_line +Layout/MultilineMethodCallBraceLayout: + Exclude: + - 'spec/functional/code_generator_spec.rb' + - 'spec/lib/protobuf/generators/field_generator_spec.rb' + - 'spec/lib/protobuf/optionable_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: aligned, indented, indented_relative_to_receiver +Layout/MultilineMethodCallIndentation: + Exclude: + - 'spec/lib/protobuf/generators/file_generator_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Layout/RescueEnsureAlignment: + Exclude: + - 'lib/protobuf/rpc/servers/socket/server.rb' + +# Offense count: 5 +# Cop supports --auto-correct. +Layout/SpaceAfterNot: + Exclude: + - 'lib/protobuf/field/base_field.rb' + - 'lib/protobuf/rpc/connectors/zmq.rb' + - 'lib/protobuf/rpc/servers/socket/worker.rb' + - 'lib/protobuf/tasks/compile.rake' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator. +# SupportedStylesForExponentOperator: space, no_space +Layout/SpaceAroundOperators: + Exclude: + - 'lib/protobuf/field/base_field.rb' + +# Offense count: 6 +Lint/DuplicateMethods: + Exclude: + - 'lib/protobuf/generators/field_generator.rb' + - 'lib/protobuf/rpc/buffer.rb' + - 'lib/protobuf/rpc/stat.rb' + +# Offense count: 1 +Lint/EmptyWhen: + Exclude: + - 'lib/protobuf/rpc/servers/socket/server.rb' + +# Offense count: 1 +Lint/InterpolationCheck: + Exclude: + - 'spec/lib/protobuf/message_spec.rb' + +# Offense count: 1 +# Configuration parameters: MaximumRangeSize. +Lint/MissingCopEnableDirective: + Exclude: + - 'lib/protobuf/message/fields.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +Lint/RedundantCopDisableDirective: + Exclude: + - 'lib/protobuf.rb' + - 'lib/protobuf/message/fields.rb' + +# Offense count: 5 +# Cop supports --auto-correct. +Lint/RedundantRequireStatement: + Exclude: + - 'lib/protobuf/rpc/servers/zmq/broker.rb' + - 'lib/protobuf/rpc/servers/zmq/server.rb' + - 'lib/protobuf/rpc/servers/zmq/worker.rb' + - 'lib/protobuf/rpc/servers/zmq_runner.rb' + - 'lib/protobuf/rpc/service_directory.rb' + +# Offense count: 2 +# Configuration parameters: AllowComments. +Lint/SuppressedException: + Exclude: + - 'lib/protobuf.rb' + +# Offense count: 44 +# Configuration parameters: IgnoredMethods. +Metrics/AbcSize: + Max: 55 + +# Offense count: 116 +# Configuration parameters: CountComments, ExcludedMethods. +# ExcludedMethods: refine +Metrics/BlockLength: + Max: 742 + +# Offense count: 1 +# Configuration parameters: CountBlocks. +Metrics/BlockNesting: + Max: 5 + +# Offense count: 19 +# Configuration parameters: IgnoredMethods. +Metrics/CyclomaticComplexity: + Max: 12 + +# Offense count: 65 +# Configuration parameters: CountComments, ExcludedMethods. +Metrics/MethodLength: + Max: 38 + +# Offense count: 2 +# Configuration parameters: CountKeywordArgs. +Metrics/ParameterLists: + Max: 7 + +# Offense count: 18 +# Configuration parameters: IgnoredMethods. +Metrics/PerceivedComplexity: + Max: 13 + +# Offense count: 5 +# Configuration parameters: ForbiddenDelimiters. +# ForbiddenDelimiters: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) +Naming/HeredocDelimiterNaming: + Exclude: + - 'spec/lib/protobuf/generators/file_generator_spec.rb' + - 'spec/lib/protobuf/generators/service_generator_spec.rb' + +# Offense count: 13 +# Configuration parameters: EnforcedStyleForLeadingUnderscores. +# SupportedStylesForLeadingUnderscores: disallowed, required, optional +Naming/MemoizedInstanceVariableName: + Exclude: + - 'lib/protobuf/field/base_field.rb' + - 'lib/protobuf/logging.rb' + - 'lib/protobuf/rpc/client.rb' + - 'lib/protobuf/rpc/connectors/base.rb' + - 'lib/protobuf/rpc/connectors/socket.rb' + - 'lib/protobuf/rpc/connectors/zmq.rb' + - 'lib/protobuf/rpc/server.rb' + - 'lib/protobuf/rpc/servers/socket/server.rb' + - 'lib/protobuf/rpc/servers/socket/worker.rb' + - 'lib/protobuf/rpc/servers/zmq/server.rb' + - 'lib/protobuf/rpc/stat.rb' + - 'spec/support/server.rb' + +# Offense count: 2 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: io, id, to, by, on, in, at, ip, db, os, pp +Naming/MethodParameterName: + Exclude: + - 'lib/protobuf/logging.rb' + - 'spec/lib/protobuf/rpc/service_filters_spec.rb' + +# Offense count: 6 +# Cop supports --auto-correct. +# Configuration parameters: PreferredName. +Naming/RescuedExceptionsVariableName: + Exclude: + - 'lib/protobuf/rpc/connectors/base.rb' + - 'lib/protobuf/rpc/middleware/exception_handler.rb' + - 'lib/protobuf/rpc/middleware/request_decoder.rb' + - 'lib/protobuf/rpc/middleware/response_encoder.rb' + - 'lib/protobuf/rpc/service_filters.rb' + +# Offense count: 6 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, EnforcedStyle. +# SupportedStyles: nested, compact +Style/ClassAndModuleChildren: + Exclude: + - '**/*.pb.rb' + - 'spec/lib/protobuf/generators/field_generator_spec.rb' + - 'spec/lib/protobuf/generators/service_generator_spec.rb' + +# Offense count: 2 +Style/CommentedKeyword: + Exclude: + - 'lib/protobuf/rpc/servers/socket_runner.rb' + - 'lib/protobuf/rpc/servers/zmq_runner.rb' + +# Offense count: 203 +Style/Documentation: + Enabled: false + +# Offense count: 12 +Style/DoubleNegation: + Exclude: + - 'lib/protobuf.rb' + - 'lib/protobuf/cli.rb' + - 'lib/protobuf/rpc/connectors/zmq.rb' + - 'lib/protobuf/rpc/servers/zmq/broker.rb' + - 'lib/protobuf/rpc/servers/zmq/server.rb' + - 'lib/protobuf/rpc/servers/zmq/worker.rb' + - 'lib/protobuf/rpc/service_directory.rb' + +# Offense count: 9 +# Cop supports --auto-correct. +Style/EmptyCaseCondition: + Exclude: + - 'lib/protobuf/enum.rb' + - 'lib/protobuf/field/base_field.rb' + - 'lib/protobuf/rpc/connectors/zmq.rb' + - 'lib/protobuf/rpc/servers/socket/server.rb' + - 'lib/protobuf/rpc/servers/socket_runner.rb' + - 'lib/protobuf/rpc/servers/zmq_runner.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: empty, nil, both +Style/EmptyElse: + Exclude: + - 'lib/protobuf/enum.rb' + - 'lib/protobuf/message/fields.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: compact, expanded +Style/EmptyMethod: + Exclude: + - 'lib/protobuf/code_generator.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +Style/Encoding: + Exclude: + - '**/*.pb.rb' + - 'protobuf.gemspec' + - 'spec/lib/protobuf/field/string_field_spec.rb' + - 'spec/lib/protobuf/message_spec.rb' + +# Offense count: 1 +Style/EvalWithLocation: + Exclude: + - 'lib/protobuf/message/fields.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Style/ExpandPathArguments: + Exclude: + - 'spec/benchmark/tasks.rb' + - 'spec/lib/protobuf/cli_spec.rb' + +# Offense count: 167 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, always_true, never +Style/FrozenStringLiteralComment: + Enabled: false + +# Offense count: 1 +# Configuration parameters: MinBodyLength. +Style/GuardClause: + Exclude: + - 'lib/protobuf/generators/base.rb' + +# Offense count: 22 +# Cop supports --auto-correct. +Style/IfUnlessModifier: + Enabled: false + +# Offense count: 1 +Style/MethodMissingSuper: + Exclude: + - 'lib/protobuf/rpc/client.rb' + +# Offense count: 1 +Style/MissingRespondToMissing: + Exclude: + - 'lib/protobuf/rpc/client.rb' + +# Offense count: 17 +# Cop supports --auto-correct. +Style/MultilineWhenThen: + Exclude: + - 'lib/protobuf/generators/field_generator.rb' + - 'lib/protobuf/generators/printable.rb' + - 'lib/protobuf/rpc/connectors/zmq.rb' + - 'lib/protobuf/rpc/servers/socket_runner.rb' + - 'lib/protobuf/rpc/servers/zmq_runner.rb' + +# Offense count: 1 +Style/MultipleComparison: + Exclude: + - 'lib/protobuf/generators/group_generator.rb' + +# Offense count: 5 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: literals, strict +Style/MutableConstant: + Exclude: + - 'lib/protobuf/generators/field_generator.rb' + - 'lib/protobuf/rpc/buffer.rb' + - 'lib/protobuf/rpc/servers/zmq/util.rb' + +# Offense count: 51 +# Cop supports --auto-correct. +# Configuration parameters: Strict. +Style/NumericLiterals: + MinDigits: 21 + +# Offense count: 10 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. +# SupportedStyles: predicate, comparison +Style/NumericPredicate: + Exclude: + - 'spec/**/*' + - 'lib/protobuf/generators/file_generator.rb' + - 'lib/protobuf/generators/message_generator.rb' + - 'lib/protobuf/rpc/buffer.rb' + - 'lib/protobuf/rpc/servers/socket/server.rb' + - 'lib/protobuf/rpc/servers/zmq/broker.rb' + - 'lib/protobuf/rpc/servers/zmq/server.rb' + - 'lib/protobuf/rpc/servers/zmq/worker.rb' + +# Offense count: 15 +# Cop supports --auto-correct. +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'lib/protobuf/cli.rb' + - 'spec/lib/protobuf/field/field_array_spec.rb' + - 'spec/lib/protobuf/rpc/service_directory_spec.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +Style/RedundantBegin: + Exclude: + - 'lib/protobuf/message/fields.rb' + - 'lib/protobuf/rpc/servers/zmq/server.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/RedundantCondition: + Exclude: + - 'lib/protobuf/rpc/connectors/base.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/RedundantInterpolation: + Exclude: + - 'lib/protobuf.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +# Configuration parameters: AllowMultipleReturnValues. +Style/RedundantReturn: + Exclude: + - 'lib/protobuf/field/int64_field.rb' + - 'lib/protobuf/rpc/connectors/base.rb' + - 'lib/protobuf/rpc/connectors/zmq.rb' + +# Offense count: 23 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: implicit, explicit +Style/RescueStandardError: + Exclude: + - 'lib/protobuf/cli.rb' + - 'lib/protobuf/field/int64_field.rb' + - 'lib/protobuf/field/varint_field.rb' + - 'lib/protobuf/generators/file_generator.rb' + - 'lib/protobuf/message.rb' + - 'lib/protobuf/rpc/connectors/base.rb' + - 'lib/protobuf/rpc/connectors/ping.rb' + - 'lib/protobuf/rpc/connectors/socket.rb' + - 'lib/protobuf/rpc/middleware/exception_handler.rb' + - 'lib/protobuf/rpc/middleware/request_decoder.rb' + - 'lib/protobuf/rpc/middleware/response_encoder.rb' + - 'lib/protobuf/rpc/servers/zmq/broker.rb' + - 'lib/protobuf/rpc/servers/zmq/server.rb' + - 'lib/protobuf/rpc/servers/zmq/worker.rb' + - 'lib/protobuf/rpc/service_directory.rb' + +# Offense count: 9 +# Cop supports --auto-correct. +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'lib/protobuf/generators/field_generator.rb' + - 'lib/protobuf/generators/service_generator.rb' + - 'lib/protobuf/message/serialization.rb' + - 'lib/protobuf/rpc/connectors/base.rb' + - 'lib/protobuf/rpc/connectors/ping.rb' + - 'lib/protobuf/rpc/servers/zmq/server.rb' + +# Offense count: 71 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: only_raise, only_fail, semantic +Style/SignalException: + Enabled: false + +# Offense count: 6 +# Cop supports --auto-correct. +Style/StderrPuts: + Exclude: + - 'lib/protobuf/cli.rb' + - 'lib/protobuf/code_generator.rb' + - 'lib/protobuf/rpc/servers/zmq/server.rb' + +# Offense count: 627 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Enabled: false + +# Offense count: 26 +# Cop supports --auto-correct. +# Configuration parameters: MinSize. +# SupportedStyles: percent, brackets +Style/SymbolArray: + EnforcedStyle: brackets + +# Offense count: 4 +# Cop supports --auto-correct. +Style/UnpackFirst: + Exclude: + - 'lib/protobuf/field/double_field.rb' + - 'lib/protobuf/field/fixed32_field.rb' + - 'lib/protobuf/field/float_field.rb' + - 'lib/protobuf/field/sfixed32_field.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +# Configuration parameters: WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + EnforcedStyle: percent + MinSize: 3 + +# Offense count: 728 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 196 diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..032d0ce5 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,345 @@ +# Stable (3.8.x) + + +3.10.0 +------ +- Add headers to request proto +- Add support for compiling v3 protos with optional fields as v2 optional fields + +3.9.0 +----- +- Performance improvements + +3.8.0 +----- +- Map types now supported (#367) + +3.7.0 (pre3) +----------- +- Evaluate extension fields in code generation (#329) +- Change the ping port to use ms timeout and make the default 200ms (#332) +- Fix build failures caused by Rails 5 requirement of Ruby 2.2.2 (#343) +- Add functional tests (#346) +- Extract client and server (#347) (#348) +- BUG: Fix decoding of packed fields (#349) +- Add support for custom file options (#350) +- BUG: enum_for_tags returns nil if tag nil (#352) +- Update rubocop and fix cops (#353) +- Optimization for varint (#356) +- Add support for custom field options (#357) +- Add support for custom enum options (#359) +- Add support for custom message options (#360) +- Acceptable check is not needed most places as coerce runs the same logic (#361) +- Encode straight to stream without intermediary copies (#362) +- Move dynamic rule checks to the initialize method (#363) +- Add support for custom service options (#364) +- Add support for custom method options (#365) +- Upcase enum default (#366) + +3.7.0 (pre2) +----------- +- BUG: Track if a repeated field has been deliberately set (#325) + +3.7.0 (pre1) +----------- +- BUG: Revert to old behavior for setting repeated fields to nil +- BUG: Set binmode for protoc-gen-ruby STDIN and STDOUT to compile proto files on Windows +- Make all things Optionable and fix requires + +3.7.0 (pre0) +----------- +- Add `PB_USE_RAW_RPC_NAMES` option to preserve raw RPC name (since #underscore can be lossy). +- Add `PB_ENUM_UPCASE` option to generate enum values as upcased. +- Clean up dynamic code generation in prep for extension namespacing. +- Namespace extension fields. +- Field values should be stored via their fully qualified names +- Refresh google/protobuf/descriptor.{proto,pb.rb} +- Properly encode and decode negative enum values. + +# Stable (3.6.x) + +3.6.9 +-------- +- Make protobuf serivce directory pluggable. + +3.6.7 +----- +- An issue was reported with the encode memoization added in #293 with using any array modification +method on repeated fields. Remove memoization on encode (#305) until we can find a better solution. + +3.5.5 +-------- +- Add native Varint for MRI. + +3.5.4 +-------- +- Ensures ActiveSupport::Deprecation does not get a stack trace when deprecations are disabled. + +3.5.3 +-------- +- Optimized get_extension_field and get_field calls. + +3.5.2 +-------- +- Optimized valid_tag?, enums_for_tag and enums_for_tags + +3.5.1 +-------- +- Adds compatibility for Rails 4.2+ as CLI options were broken +- Fixes bug with MRI and "dead" thread in zmq broker +- Fixes Rubocop compatability with new version + +3.0.4 +-------- + +- Raise specific MethodNotFound when service class doesn't respond to (publicly implement) + the rpc method called from the client. Stop rescuing all NoMethodError's thrown + by service implementations. [#193, @liveh2o] + +3.0.3 +--------- + +- Fix recursive memory/cpu growth issue when calling class-level `Message.to_json`. [#190] + +3.0.2 +--------- + +- Queue requests at the broker when concurrent requests hit the ZMQ server, distribute to + worker threads on each turn of the read poll loop. [#189, @abrandoned, @liveh2o] + +3.0.1 +--------- + +- Fix NoMethodError that can occur when serializing a message with a missing required field. [#187, @abrandoned] + +3.0.0 +--------- + +A lot has changed since the last stable v2.8.12. For all the relevant changes, +see the closed [pull requests and issues list in github](https://github.com/localshred/protobuf/issues?milestone=1&state=closed). +Below is a high-level list of fixes, deprecations, breaking changes, and new APIs. + +### EventMachine is dead, Long live EventMachine + +The EventMachine client and server have been removed from this gem. They code was far +too error prone and flawed to feasibly support going forward. It's recommended +to switch to the socket implementation (using `PB_CLIENT_TYPE` and `PB_SERVER_TYPE` of `socket`) +for a painless switchover. The ZMQ implementation is much more performant but +does have a dependency on libzmq. + +### Server Middlewares + +The server/dispatcher stack has been converted to the Middleware pattern! +Exception handling (#162, #164), Request decoding (#160, #166), Response encoding (#161, #167), +Logging and stats (#163), and Method dispatch (#159) have all been extracted into their +own Middlewares to greatly simplify testing and further development of the server +stack, furthering our preparations for removing the socket implementations (zmq, socket) +into their own gems in version 4.0. Major props to @liveh2o for [tackling this beast](https://github.com/localshred/protobuf/tree/master/lib/protobuf/rpc/middleware). + +#### Bug Fixes + +- Resolve DNS names (e.g. localhost) when using ZMQ server. [#46, reported by @reddshack] +- Switched to hash based value storage for messages to fix large field tag memory issues. [#118, #165] +- `Enum.fetch` used to return an enum of any type if that is the value passed in. [#168] + +#### Deprecations + +__!! NOTE: These deprecated methods will be removed in v3.1. !!__ + +- Deprecated `BaseField#type` in favor of `#type_class`. +- Deprecated `Message.get_ext_field_by_name` in favor of `.get_extension_field` or `.get_field(name_or_tag, true)`. +- Deprecated `Message.get_ext_field_by_tag,` in favor of `.get_extension_field` or `.get_field(name_or_tag, true)`. +- Deprecated `Message.get_field_by_name,` in favor of `.get_field`. +- Deprecated `Message.get_field_by_tag,` in favor of `.get_field`. +- Deprecated `Enum.enum_by_value` in favor of `.enum_for_tag`. +- Deprecated `Enum.name_by_value` in favor of `.name_for_tag`. +- Deprecated `Enum.get_name_by_tag` in favor of `.name_for_tag`. +- Deprecated `Enum.value_by_name` in favor of `.enum_for_name`. +- Deprecated `Enum.values` in favor of `.enums`. Beware that `.enums` returns an array where `.values` returns a hash. + Use `.all_tags` if you just need all the valid tag numbers. In other words, don't do this anymore: `MyEnum.values.values.map(&:to_i).uniq`. + +#### Breaking Changes + +- Require Active Support 3.2+. [#177] +- All files/classes relating to the EventMachine client and server are gone. Use `PB_CLIENT_TYPE` and `PB_SERVER_TYPE` of `socket` instead. [#116] +- Cleaned up the `Enum` class, deprecating/renaming most methods. tl;dr, just use `MyEnum.fetch`. + See #134 for more comprehensive documentation about which methods are going and away and which are being renamed. [#134] +- Pulled `EnumValue` into `Enum`. The `EnumValue` class no longer exists. Use `Enum` for type-checking instead. [#168]. +- Removed previously deprecated `bin/rprotoc` executable. Use `protoc --ruby_out=...` instead. [13fbdb9] +- Removed previously deprecated `Service#rpc` method. Use `Service#env#method_name` instead. [f391294] +- Changed the `Service#initialize` to take an `Env` object instead of separate request, method, and client parameters. [6c61bf72] +- Removed attribute readers for `Service#method_name` and `Service#client_host`. Use `Service#env` to get them instead. +- Removed `lib/protobuf/message/message.rb`. Use `lib/protobuf/message.rb` instead. +- Removed field getters from Message instances (e.g. `Message#get_field_by_name`). + Use class-level getters instead (see Deprecations section). +- Moved `lib/protobuf/message/decoder.rb` to `lib/protobuf/decoder.rb`. The module is still named `Protobuf::Decoder`. +- Removed `Protobuf::Field::ExtensionFields` class. +- Removed instance-level `max` and `min` methods from all relevant Field classes (e.g. Int32Field, Uint64Field, etc). + Use class-level methods of the same names instead. [#176, 992eb051] +- `PbError#to_response` no longer receives an argument, instead returning a new `Socketrpc::Response` object. [#147, @liveh2o] +- The Server module has been stripped of almost all methods, now simply invokes the Middleware stack for each request. [#159, @liveh2o] +- Removed `Protobuf::PROTOC_VERSION` constant now that the compiler supports any protoc version. + +#### New APIs + +- Added support for [enum `allow_alias` option](https://developers.google.com/protocol-buffers/docs/proto#enum). [#134] +- `Enum.all_tags` returns an array of unique tags. Use it to replace `Enum.values.values.map(&:to_i)` (`Enum.values` is deprecated). +- `Enum.enums` returns an array of all defined enums for that class (including any aliased Enums). +- Reinstated support for symbol primitive field types in generated Message code. [#170] +- `Message.get_field` accepts a second boolean parameter (default false) to return an extension field if found. [#169] +- Mirror existing `Decoder#decode_from(stream)` with `Encoder#encode_to(stream)`. [#169] +- `Server` now invokes the [middleware stack](https://github.com/localshred/protobuf/tree/master/lib/protobuf/rpc/middleware) for request handling. [#159, @liveh2o] +- Added `protobuf:compile` and `protobuf:clean` rake tasks. Simply `load 'protobuf/tasks/compile.rake'` in your Rakefile (see `compile.rake` for arguments and usage). [#142, #143] +- Add `Protobuf::Deprecator` module to alias deprecated methods. [#165] +- Add support for assigning a symbol to a string or bytes field. [#181, @abrandoned] +- Add `first_alive_load_balance` option to rpc server. Pass `PB_FIRST_ALIVE_LOAD_BALANCE` + as an env variable to the client process to ensure the client asks the server + if it is alive (able to server requests to clients). [#183, @abrandoned] + +2.8.13 +--------- + +- Backport #190 to 2.8 stable series. [#192] + +2.8.12 +--------- + +- Fix thread busy access in zmq server/worker. [#151, @abrandoned] + +2.8.11 +--------- + +- Default ZMQ server to use inproc protocol instead of tcp (zero-copy between server-broker-worker). [#145, @brianstien] +- Add `broadcast_busy` functionality that removes server from cluster if the workers are full. [#149, @abrandoned] +- Add cli option for `--no-zmq_inproc`. [#149, @abrandoned] +- Add cli option for `--broadcast_busy`. [#149, @abrandoned] + +2.8.10 +--------- + +- Allow passing a file extension to compile/clean rake tasks. [#143] + +2.8.9 +--------- + +- Deprecated Protobuf::Lifecycle module in favor of using ActiveSupport::Notifications. [#139, @devin-c] +- Modify `$LOAD_PATH` inside descriptors.rb to make it easier for other libraries to write their own compiler plugins using our pre-compiled descriptors. [#141] +- Add protobuf:clean and protobuf:compile rake tasks for use in external libraries to compile source definitions to a destination. [#142] + +2.8.8 +--------- + +- ServiceDirectory beacons broadcast on same ip as listening clients. [#133, @devin-c] + +2.8.7 +--------- + +- Fire ActiveSupport load hooks when RPC Server and Client classes are loaded. [#126, @liveh2o] +- Prevent infinite loop when doing service lookup from directory. [#125, @brianstien] + +2.8.6 +--------- + +- Fix string/byte encoding issue when unicode characters present. Reported by @foxban. This was also backported to v2.7.12. [#120] + +2.8.5 +---------- + +- Fix issue where ServiceDirectory lookups were failing when given a class name, breaking the directory load balancing. (#119) + +2.8.4 +---------- + +- Fix issue where frozen strings assigned in a repeated field would cause encoding runtime errors. (#117) + +2.8.3 +---------- + +- Add Deprecation warning when requiring `protobuf/evented`. Version 3.x will not support the eventmachine transport layer for client or server. + +2.8.2 +---------- + +- Remove the <4.0 version constraint on ActiveSupport. + +2.8.1 +---------- + +- Improve `ServiceDirectory` lookup speed ~10x, lookups now done in constant time (devin-c). +- Add Timestamp to end of rpc stat log (represents ending time of request processing). +- Set `request_size` in the rpc stat within ZMQ Worker (previously missing). +- Ensure `request_size` and `response_size` are set on rpc stat for client requests. + +2.8.0 +----------- + +- New compiler supports protobuf compilation/runtime with protoc <= v2.5.0 (c++ compiler removed). [#109] +- Deprecated rprotoc in favor of protoc. [0bc9674] +- Added service dynamic discovery to the ZMQ connector and server. [#91, @devin-c] +- No longer creating `-java` platform gem due to removal of c++ compiler. +- Added WTFPL license. + +2.7.12 +----------- + +- Backport string/byte encoding issue when unicode characters present. [code: #122, original issue: #120] + +2.0.0 +----------- + +#### `rprotoc` changes + +* New option `--ruby_out` to specify the output directory to place generated ruby files. If not provided, ruby code will not be generated. +* Extends `libprotoc` to hook in directly to google's provided compiler mechanism. +* Removed all previous compiler code including the racc parser, node visitors, etc. +* See `protoc --help` for default options. + +#### `rprotoc` generated files changes + +* Import `require`s now occur outside of any module or class declaration which solves ruby vm warnings previously seen. +* Empty inherited Message and Enum classes are pre-defined in the file, then reopened and their fields applied. This solves the issue of recursive field dependencies of two or more types in the same file. +* Generated DSL lines for message fields include the fully qualified name of the type (e.g. `optional ::Protobuf::Field::StringField, :name, 1`) +* Support for any combination of `packed`, `deprecated`, and `default` as options to pass to a field definition. +* Services are now generated in the corresponding `.pb.rb` file instead of their own `*_service.rb` files as before. + +#### `rpc_server` changes + +* Removed `--env` option. The running application or program is solely in charge of ensuring it's environment is properly loaded. +* Removed reading of `PB_CLIENT_TYPE`, `PB_SERVER_TYPE` environment variables. Should use mode switches or custom requires (see below) instead. +* Removed `--client_socket` in favor of using mode switches. This also means client calls made by the `rpc_server` will run as the same connector type as the given mode (socket, zmq, or evented). +* Removed `--pre-cache-definitions` switch in favor of always pre-caching for performance. +* Removed `--gc-pause-serialization` since using `--gc-pause-request` in conjunction was redundant. +* Removed `--client-type` in favor of mode switches. +* Removed `--server-type` in favor of mode switches. +* Added mode switch `--evented`. +* Added `--threads` to specify number of ZMQ Worker threads to use. Ignored if mode is not zmq. +* Added `--print-deprecation-warnings` switch to tell the server whether or not to print deprecation warnings on field usage. Enabled by default. +* See `rpc_server help start` for all options and usage. Note: the `start` task is the default and not necessary when running the `rpc_server`. + +#### Message changes + +* `Message#get_field` usage should now specify either `Message#get_field_by_name` or `Message#get_field_by_tag`, depending on your lookup criteria. +* Support for STDERR output when accessing a message field which has been defined as `[deprecated=true]`. Deprecated warnings can be skipped by running your application or program with `PB_IGNORE_DEPRECATIONS=1`. +* Significant internal refactoring which provides huge boosts in speed and efficiency both in accessing/writing Message field values, as well as serialization and deserialization routines. +* Refactor `Message#to_hash` to delegate hash representations to the field values, simply collecting the display values and returning a hash of fields that are set. This also affects `to_json` output. + +#### Enum changes + +* Add `Enum.fetch` class method to polymorphically retrieve an `EnumValue` object. +* Add `Enum.value_by_name` to retrieve the corresponding `EnumValue` to the given symbol name. +* Add `Enum.enum_by_value` to retrieve the corresponding `EnumValue` to the given integer value. + +#### RPC Service changes + +* `async_responder` paradigm is no longer supported. +* `self.response=` paradigm should be converted to using `respond_with(object)`. +* Significant internal changes that should not bleed beyond the API but which make maintaining the code much easier. + +#### RPC Client changes + +* In the absence of `PB_CLIENT_TYPE` environment var, you should be requiring the specific connector type specifically. For instance, if you wish to run in zmq mode for client requests, update your Gemfile: `gem 'protobuf', :require => 'protobuf/zmq'`. +* `:async` option on client calls is no longer recognized. + +#### Other changes + +* Moved files out of `lib/protobuf/common` folder into `lib/protobuf`. Files affected are logger, wire\_type, util. The only update would need to be the require path to these files since the modules were always `Protobuf::{TYPE}`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..beb902ad --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing + +I love accepting issues and pull requests. I only ask for a few guidelines to +be followed that will make it much easier for me to solve your issue or get +your code merged. + +1. Use GitHub Issues or Pull Requests over sending an email. It's much easier for me to keep track of your issue through GitHub. +2. For __compiler issues__, please provide both a gist for the __source definition(s)__ as well as the __generated output__ (if any). +3. For __existing issues or functionality__, please use the latest stable branch (currently __`3-5-stable`__) as the base branch for the pull request. This helps us maintain a stable gem release strategy. All commits merged to stable will also be merged down to `master`. +4. For __new functionality__, please use __`master`__ as the base branch for the pull request. The `master` branch is used to stage all "next iteration" work. +5. Be patient with me as I work on your issue. + +Following these simple guidelines really will help me help you. And really, +that's what we're here for. I'm on @localshred on twitter, let's be friends. :) + +## Happy Contributing! diff --git a/Gemfile b/Gemfile index a1b93f3e..ea1fdd91 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,26 @@ -source :rubygems +source 'https://rubygems.org/' gemspec + +group :development do + # debuggers only work in MRI + if RUBY_ENGINE.to_sym == :ruby + if RUBY_VERSION < '2.0.0' + gem 'pry-debugger' + elsif RUBY_VERSION < '2.4.0' + gem 'pry', '~> 0.12.0' + gem 'pry-byebug' + else + gem 'pry', '~> 0.13.0' + gem 'pry-byebug', '~> 3.9.0' + end + + gem 'pry-stack_explorer' + + gem 'ruby-prof' + gem 'varint' + elsif RUBY_PLATFORM =~ /java/i + gem 'fast_blank_java' + gem 'pry' + end +end diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 15ab5a69..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,44 +0,0 @@ -PATH - remote: . - specs: - protobuf (1.1.3) - eventmachine (~> 0.12.10) - eventually (~> 0.1.0) - json_pure (~> 1.6.4) - -GEM - remote: http://rubygems.org/ - specs: - diff-lcs (1.1.3) - eventmachine (0.12.10) - eventmachine (0.12.10-java) - eventually (0.1.0) - json_pure (1.6.5) - multi_json (1.0.4) - rake (0.8.7) - redcarpet (1.17.2) - rspec (2.8.0) - rspec-core (~> 2.8.0) - rspec-expectations (~> 2.8.0) - rspec-mocks (~> 2.8.0) - rspec-core (2.8.0) - rspec-expectations (2.8.0) - diff-lcs (~> 1.1.2) - rspec-mocks (2.8.0) - simplecov (0.5.4) - multi_json (~> 1.0.3) - simplecov-html (~> 0.5.3) - simplecov-html (0.5.3) - yard (0.7.4) - -PLATFORMS - java - ruby - -DEPENDENCIES - protobuf! - rake (~> 0.8.7) - redcarpet (~> 1.17.2) - rspec (~> 2.8.0) - simplecov (~> 0.5.4) - yard (~> 0.7.4) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..cddcfe29 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 BJ Neilsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/README.md b/README.md index ea7f11fc..45d6a762 100644 --- a/README.md +++ b/README.md @@ -1,228 +1,33 @@ # protobuf -Protobuf is an implementation of [Google's protocol buffers][google-pb] in ruby. It's a gem for managing 3 things: +[![Gem Version](https://badge.fury.io/rb/protobuf.svg)](http://badge.fury.io/rb/protobuf) +[![Build Status](https://secure.travis-ci.org/ruby-protobuf/protobuf.svg?branch=master)](https://travis-ci.org/ruby-protobuf/protobuf) +[![Gitter chat](https://badges.gitter.im/ruby-protobuf/protobuf.svg)](https://gitter.im/ruby-protobuf/protobuf) +[![protobuf API Documentation](https://www.omniref.com/ruby/gems/protobuf.png)](https://www.omniref.com/ruby/gems/protobuf) -1. Compiling `.proto` definitions to ruby -2. Provide a Socket-RPC mechanism for calling services -3. Provide RPC interop between ruby and other protobuf-rpc aware implementations for different languages (e.g. [protobuf-socket-rpc][]) +Protobuf is an implementation of [Google's protocol buffers][google-pb] in ruby, version 2.5.0 is currently supported. -So let's dive in and see how to work with all three. +## Install -## 1. Compile `.proto` definitions to ruby +See our [Installation Guide][] on the [wiki][]. -Protocol Buffers are great because they allow you to clearly define data storage or data transfer packets. Google officially supports Java, C++, and Python for compilation and usage. Let's make it ruby aware! +## Usage -Let's say you have a `defs.proto` file that defines a User message. +The [wiki][] contains in-depth guides on the various ways to use this gem +including [compiling definitions][], [object APIs][], [services][], [clients][], and even +an [API roadmap][]. -``` -package mycompany; -message User { - required string first_name = 1; - required string last_name = 1; -} +## Changelog -Now let's compile that definition to ruby: +See recent changes in the [release notes][] or the [changelog][]. -$ rprotoc defs.proto -o ./lib -``` - -The previous line will take whatever is defined in defs.proto and output ruby classes to the `./lib` directory, obeying the package directive. Assuming that's all defs.proto had defined, `./lib` should now look like this: - -``` -- lib - |- mycompany - |- defs.pb.rb -``` - -And `defs.pb.rb` should look like this: - -```ruby -module Mycompany - class User - required :string, :first_name, 1 - required :string, :last_name, 2 - end -end -``` - -You can then use that class just like normal: - -```ruby -require 'lib/mycompany/user.pb' - -# dot notation reading/writing fields -user = Mycompany::User.new -user.first_name = "Lloyd" -user.last_name = "Christmas" -user.first_name # => "Lloyd" - -# or pass in the fields as a hash to the initializer -user = Mycompany::User.new :first_name => "Lloyd", :last_name => "Christmas" -user.first_name # => Lloyd -user.last_name # => Christmas -``` - -## 2. RPC - -RPC is one of many technologies that tries to solve the problem of getting smaller pieces of data from one place to another. Many will argue for or against RPC and its usefulness, but I'm not going to do that here. Google's Protocol Buffers relies on RPC and that's why you're here. - -Any discussion about RPC leads to a discussion about clients and servers and the remote procedures themselves. For our purposes, we'll talk about a `Client` (process that is calling the server/service), a `Service` (the remote procedure), and a `Server` (the process that manages one or more services). We'll start with the Service first. - -### Services - -Services are simply classes that have endpoint methods defined. Here's what one looks like in protobuf: - -``` -message UserRequest { - optional string email = 1; -} -message UserList { - repeated User users = 1; -} -service UserService { - rpc Find (UserRequest) returns (UserList); -} -``` - -And the equivalent ruby stub for the service (generated with `rprotoc`): - -```ruby -# lib/mycompany/user_service.rb -module Mycompany - class UserService < Protobuf::Rpc::Service - rpc :find, UserRequest, UserList - end -end -``` - -Recognize that the extra messages would actually have gone into the `defs.pb.rb` file while the service stub would receive it's own file at `user_service.rb`. - -**Important Note: The *stubbed* class here is a *stub*. You should not alter it directly in any way as it will break your definition. Read on to learn how to use this stub.** - -Did you read the note above? Go read it. I'll wait. - -Ok, now that you have a compiled service stub, you'll want to require it from `lib` and implement the methods. You'll notice when you compile the stub there is a large comment at the top of the file. You can use this code comment to start your real implementation. Go ahead and copy it to your services directory (probably `app/services` if we're in rails). - -```ruby -# app/services/user_service.rb -require 'lib/mycompany/user_service' -module Mycompany - class UserService - - # request -> Mycompany::UserRequest - # response -> Mycompany::UserResponse - def find - # request.email will be the unpacked string that was sent by the client request - User.find_by_email(request.email).each do |user| - # must only use a proto instance of Mycompany::User when appending to the `users` field - response.users << user.to_proto - end - end - - end -end -``` - -Simply implement the instance method for the defined rpc. No other methods will be allowed in the class (even helpers or private methods). An implicit `request` and `response` object are provided for you, pre-instantiated, and in the case of the request, already are populated with the data that was sent by the client. - -If you need to create your own response object (a valid case), be sure to assign it back to the instance by using `self.response = your_response_obj`. The object you assign **MUST** be of the defined return type, in this case `Mycompany::UserList`. Any other type will result in an error. - -Triggering an error from the service is simple: - -```ruby -#... -def find - if request.email.blank? - rpc_failed 'Unable to find user without an email' - else - # query/populate response - end -end -#... -``` - -This means that the client's `on_failure` callback will be invoked instead of the `on_success` callback. Read more below on client callbacks. - -I find it very convenient to use a CRUD-style interface when defining certain data services, though this is certainly not always the case. - -### Servers - -A service is nothing without being hooked up to a socket. It's the nerdy kid waiting by the telephone for someone to call without knowing that the phone company disconnected their house. Sad and pathetic. So hook the phone lines! - -``` -$ rpc_server -o myserver.com -p 9939 -e production -l ./log/protobuf.log config/environment.rb -``` - -The previous call will start an EventMachine server running on the given host and port which will load your application into memory. You certainly don't have to run rails or any other framework, just make sure you have some kind of file that will load your services all into memory. The server doesn't know where you put your code, so tell it. - -Be aware that server needs to be able to translate the socket stream of bytes into an actual protobuf request object. If the definition for that request object aren't known to the server, you're going to have a long day getting this going. It's necessary to store all your definitions and their generated classes in a shared repository (read: gem) that both client and server have access to in their respective load paths. - -Once the server starts, you should see it as a running process with `ps`. Sending a KILL, QUIT, or TERM signal to the pid will result in shutting the server down gracefully. - -``` -$ ps aux | grep rpc_server -1234 ... rpc_server myservice.com:9939 - -$ kill -QUIT 1234 -rpc_server shutdown -``` - -### Clients - -A lot of work has gone into making the client calls simple and easy to use yet still powerful. Clients have a DSL that feels very ajax-ish, mostly because of the nature of EventMachine, but I also think it works quite well. - -```ruby -# require the defs from the shared gem/repo -require 'sharedgem/mycompany/user.pb' -require 'sharedgem/mycompany/user_service' - -# Create a request object for the method we are invoking -req = Mycompany::UserRequest.new(:email => 'jeff@gmail.com') - -# Use the UserService class to generate a client, invoke the rpc method -# while passing the request object -Mycompany::UserService.client.find(req) do |c| - # This block will be executed (registering the callbacks) - # before the request actualy occurs. - # the `c` param in this block is the `.client` object - # that is generated from the call above - - # Register a block for execution when the response - # is deemed successful from the service. Accepts - # the unpacked response as its only parameter - c.on_success do |response| - response.users.each do |u| - puts u.inspect - end - end - - # Register a block for execution when the response - # is deemed a failure. This can be either a client-side - # or server-side failure. The object passed the to the - # block has a `message` and a `code` attribute - # to aid in logging/diagnosing the failure. - c.on_failure do |err| - puts 'It failed: ' + err.message - end -end -``` - -Many different options can be passed to the `.client` call above (such as `:async => true` or `:timeout => 600`). See the `lib/protobuf/rpc/client.rb` and `lib/protobuf/rpc/service.rb` files for more documentation. It should be noted that the default behavior of `UserService.client` is to return a blocking client. The nature of using Client calls within a framework like Rails demands a blocking call if the response of a web request is dependent on data returned from the service. - -## 3. RPC Interop - -The main reason I wrote this gem was to provide a ruby implementation to google's protobuf that worked on the RPC layer with a Java Service layer that was already running [protobuf-socket-rpc][], the supported socket rpc library for protobuf from Google. The [old gem][] did not provide a very robust RPC implementation and it most certainly did not work with the Java stack. - -## Accreditation & Caveats - -It must be noted a large amount of the code in this library was taken from the [ruby-protobuf][old gem] gem. Its authors and I were unable to reach a communication point to be able to merge all of my RPC updates in with their master. Unfortunately I just simply couldn't use their RPC code and so I've decided to diverge from their codeset. I take no credit whatsoever for the (de)serialization and `rprotoc` code generation original work, though I have modified it slightly to be more compliant with my understanding of the pb spec. I want to say thanks to the original devs for the good work they did to get me most of the way there. The code was initially diverged at their 0.4.0 version. - -It should also be noted that there are many more features I haven't really shown here, so please let me know if you have any questions on usage or support for various features. Happy protobufing. - --- BJ Neilsen, [@localshred][], [rand9.com][] - - [google-pb]: http://code.google.com/p/protobuf "Google Protocol Buffers" - [protobuf-socket-rpc]: http://code.google.com/p/protobuf-socket-rpc/ "Google's official Socket-RPC library for protobuf" - [old gem]: https://github.com/macks/ruby-protobuf "Macks ruby-protobuf on github" - [@localshred]: http://twitter.com/localshred "Follow on twitter @localshred" - [rand9.com]: http://rand9.com "Blog" + [google-pb]: http://code.google.com/p/protobuf "Google Protocol Buffers" + [wiki]: https://github.com/ruby-protobuf/protobuf/wiki "Wiki home page" + [Installation Guide]: https://github.com/ruby-protobuf/protobuf/wiki/Installation "Installation guide" + [compiling definitions]: https://github.com/ruby-protobuf/protobuf/wiki/Compiling-Definitions "Compiling guide" + [object APIs]: https://github.com/ruby-protobuf/protobuf/wiki/Messages-&-Enums "Message & Enum object APIs guide" + [services]: https://github.com/ruby-protobuf/protobuf/wiki/Services "Services object API guide" + [clients]: https://github.com/ruby-protobuf/protobuf/wiki/Clients "Client object API guide" + [API roadmap]: https://github.com/ruby-protobuf/protobuf/wiki/API-Roadmap "API Roadmap guide" + [release notes]: https://github.com/ruby-protobuf/protobuf/releases "Release notes" + [changelog]: https://github.com/ruby-protobuf/protobuf/blob/master/CHANGES.md "CHANGES.md" diff --git a/Rakefile b/Rakefile index 97f1c2d5..cecc48a4 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,66 @@ -$:.push File.expand_path("./", File.dirname(__FILE__)) -$:.push File.expand_path("./spec", File.dirname(__FILE__)) +$LOAD_PATH << ::File.expand_path('../', __FILE__) +$LOAD_PATH << ::File.expand_path('../spec', __FILE__) -require "bundler/gem_tasks" -require "benchmark/tasks" +require 'fileutils' +require 'rubygems' +require 'rubygems/package_task' +require 'bundler/gem_tasks' +require 'benchmark/tasks' + +require 'rspec/core/rake_task' +require 'rubocop/rake_task' + +RSpec::Core::RakeTask.new(:spec) +RuboCop::RakeTask.new + +task :default => ['compile:spec', 'compile:rpc', :spec, :rubocop] + +desc 'Run specs' +namespace :compile do + + desc 'Compile spec protos in spec/supprt/ directory' + task :spec do + proto_path = ::File.expand_path('../spec/support/', __FILE__) + proto_files = Dir[File.join(proto_path, '**', '*.proto')] + + proto_files.each do |proto_file| + cmd = %(protoc --plugin=protoc-gen-ruby-protobuf=./bin/protoc-gen-ruby --ruby-protobuf_out=#{proto_path} -I #{proto_path} #{proto_file}) + puts cmd + system(cmd) || fail("Failed to compile spec proto: #{proto_file}") + end + end + + desc 'Compile rpc protos in protos/ directory' + task :rpc do + proto_path = ::File.expand_path('../proto', __FILE__) + proto_files = Dir[File.join(proto_path, '**', '*.proto')] + output_dir = ::File.expand_path('../tmp/rpc', __FILE__) + ::FileUtils.mkdir_p(output_dir) + + cmd = %(protoc --plugin=protoc-gen-ruby-protobuf=./bin/protoc-gen-ruby --ruby-protobuf_out=#{output_dir} -I #{proto_path} #{proto_files.join(' ')}) + + puts cmd + system(cmd) || fail("Failed to compile rpc protos!") + + files = { + 'tmp/rpc/dynamic_discovery.pb.rb' => 'lib/protobuf/rpc', + 'tmp/rpc/rpc.pb.rb' => 'lib/protobuf/rpc', + 'tmp/rpc/google/protobuf/descriptor.pb.rb' => 'lib/protobuf/descriptors/google/protobuf', + 'tmp/rpc/google/protobuf/compiler/plugin.pb.rb' => 'lib/protobuf/descriptors/google/protobuf/compiler', + } + + files.each_pair do |source_file, destination_dir| + source_file = ::File.expand_path("../#{source_file}", __FILE__) + destination_dir = ::File.expand_path("../#{destination_dir}", __FILE__) + ::FileUtils::Verbose.cp(source_file, destination_dir) + end + end + +end + +task :console do + require 'pry' + require 'protobuf' + ARGV.clear + ::Pry.start +end diff --git a/bin/protoc-gen-ruby b/bin/protoc-gen-ruby new file mode 100755 index 00000000..a84b1c1c --- /dev/null +++ b/bin/protoc-gen-ruby @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +# Before requiring protobuf, ensure that we will not load any +# server or client code. +# +ENV['PB_NO_NETWORKING'] = '1' + +$LOAD_PATH << ::File.expand_path("../../lib", __FILE__) +require 'protobuf' +require 'protobuf/descriptors' +require 'protobuf/code_generator' + +# Ensure that no encoding conversions are done on STDIN and STDOUT since +# we are passing binary data back and forth. Otherwise these streams +# will be mangled on Windows. +STDIN.binmode +STDOUT.binmode + +request_bytes = STDIN.read +code_generator = ::Protobuf::CodeGenerator.new(request_bytes) +code_generator.eval_unknown_extensions! +STDOUT.print(code_generator.response_bytes) diff --git a/bin/rpc_server b/bin/rpc_server index 1accf8ba..7aed6c92 100755 --- a/bin/rpc_server +++ b/bin/rpc_server @@ -1,122 +1,5 @@ #!/usr/bin/env ruby -require 'optparse' -require 'ostruct' -require 'logger' -require 'protobuf/version' -require 'protobuf/rpc/servers/evented_runner' -require 'protobuf/rpc/servers/socket_runner' - -# Default options -server = OpenStruct.new({ - :app => nil, - :env => ENV['RPC_SERVER_ENV'] || 'development', - :host => '127.0.0.1', - :port => 9595, - :backlog => 100, - :threshold => 100, - :log => File.expand_path('./protobuf.log'), - :level => ::Logger::INFO, - :runner => Protobuf::Rpc::EventedRunner, - :debug => false -}) - -parser = OptionParser.new do |opts| - opts.banner = "Usage: rpc_server [options] app_file.rb" - - opts.on("-e ENVIRONMENT", "--env=ENVIRONMENT", "Environment to run the server") do |v| - server.env = ENV['RACK_ENV'] = ENV['RAILS_ENV'] = ENV['APP_ENV'] = v - end - - opts.on("-o HOST", "--host=HOST", "Server host") do |v| - server.host = v - end - - opts.on("-p PORT", "--port=PORT", Integer, "Server port") do |v| - server.port = v - end - - opts.on("-l LOG_FILE", "--log=LOG_FILE", "Log file or device") do |v| - server.log = v - end - - opts.on("-v N", "--level=N", Integer, "Log level to use, 0-5 (see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/)") do |v| - server.level = v.to_i - end - - opts.on("-b N", "--backlog=N", Integer, "Backlog for listening socket when using Socket Server") do |v| - server.backlog = v.to_i - end - - opts.on("-t N", "--threshold=N", Integer, "Multi-threaded Socket Server cleanup threshold") do |v| - server.threshold = v.to_i - end - - opts.on("-c", "--client_socket", "Socket Mode for client connections (No EventMachine)") do |v| - Protobuf::ConnectorType = "Socket" - end - - opts.on("-s", "--socket", "Socket Server Mode (No EventMachine)") do |v| - Protobuf::ServerType = "SocketServer" - server.runner = Protobuf::Rpc::SocketRunner - end - - opts.on("-d", "--[no-]debug", "Debug Mode. Override log level to DEBUG.") do |v| - server.debug = v - server.level = ::Logger::DEBUG if v === true - end - - opts.separator "" - opts.separator "Common options:" - - opts.on_tail("-h", "--help", "Show this message") do - puts opts - exit - end - - opts.on_tail("--version", "Show version") do - puts Protobuf::VERSION - exit - end -end - -parser.parse! -require 'protobuf' - -[:INT, :QUIT, :TERM].each do |sig| - trap(sig) do - server.runner.stop - end -end - -begin - if ARGV.empty? - puts 'You must specify an app file to use.' - puts parser.help - exit - else - server.app = ARGV.pop - raise 'Invalid app file specified (%s).' % server.app unless File.exists?(server.app) - end - - # Configure the Protobuf::Logger - Protobuf::Logger.configure :file => server.log, :level => server.debug ? ::Logger::DEBUG : server.level - - # Output the server opts - Protobuf::Logger.debug 'Debugging options:' - Protobuf::Logger.debug server.inspect - - # Set the name of the process - $0 = 'rpc_server %s:%d %s' % [server.host, server.port, server.app] - - # Require the given application file - require server.app - - server.runner.run(server) -rescue - msg = 'ERROR: RPC Server failed to start. %s' % $!.inspect - $stderr.puts msg, *($!.backtrace) - Protobuf::Logger.error msg - Protobuf::Logger.error $!.backtrace.join("\n") - exit 1 -end +require 'protobuf/cli' +::Protobuf::CLI.start(ARGV) +exit diff --git a/bin/rprotoc b/bin/rprotoc deleted file mode 100755 index e0b32b9a..00000000 --- a/bin/rprotoc +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env ruby - -require 'optparse' - -if File.directory?("#{File.dirname(__FILE__)}/../lib") - $: << "#{File.dirname(__FILE__)}/../lib" -else - require 'rubygems' - gem 'protobuf' -end -require 'protobuf' -require 'protobuf/version' -require 'protobuf/compiler/compiler' - -::Version = Protobuf::VERSION - -options = { - :proto_path => '.', - :out => '.', -} -opts = OptionParser.new("#{$0} [OPTIONS] PROTO_FILE") -opts.on('-p', '--proto_path ', 'Specify the directory in which to search for imports. The current directory is default.'){|v| options[:proto_path] = v} -opts.on('-o', '--out ', 'Specify the directory in which Ruby source file is generated. The current directory is default.'){|v| options[:out] = v} -opts.on_tail('-v', '--version', 'Show version.'){ puts(opts.ver); exit } -opts.on_tail('-h', '--help', 'Show this message.'){ puts(opts.help); exit } - -begin - opts.order! -rescue OptionParser::ParseError - $stderr.puts $!.to_s - exit 1 -end - -unless ARGV.size > 0 - puts opts - exit -end - -begin - ARGV.each do |proto_file| - Protobuf::Compiler.compile(proto_file, options[:proto_path], options[:out]) - end -rescue - $stderr.puts $!.message - exit 1 -end \ No newline at end of file diff --git a/examples/addressbook.pb.rb b/examples/addressbook.pb.rb deleted file mode 100644 index 95621914..00000000 --- a/examples/addressbook.pb.rb +++ /dev/null @@ -1,55 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# package tutorial; -# -# message Person { -# required string name = 1; -# required int32 id = 2; -# optional string email = 3; -# -# enum PhoneType { -# MOBILE = 0; -# HOME = 1; -# WORK = 2; -# } -# -# message PhoneNumber { -# required string number = 1; -# optional PhoneType type = 2 [default = HOME]; -# } -# -# repeated PhoneNumber phone = 4; -# } -# -# message AddressBook { -# repeated Person person = 1; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Tutorial - class Person < ::Protobuf::Message - defined_in __FILE__ - required :string, :name, 1 - required :int32, :id, 2 - optional :string, :email, 3 - class PhoneType < ::Protobuf::Enum - defined_in __FILE__ - MOBILE = 0 - HOME = 1 - WORK = 2 - end - class PhoneNumber < ::Protobuf::Message - defined_in __FILE__ - required :string, :number, 1 - optional :PhoneType, :type, 2, :default => :HOME - end - repeated :PhoneNumber, :phone, 4 - end - class AddressBook < ::Protobuf::Message - defined_in __FILE__ - repeated :Person, :person, 1 - end -end \ No newline at end of file diff --git a/examples/addressbook.proto b/examples/addressbook.proto deleted file mode 100644 index 13913b80..00000000 --- a/examples/addressbook.proto +++ /dev/null @@ -1,24 +0,0 @@ -package tutorial; - -message Person { - required string name = 1; - required int32 id = 2; - optional string email = 3; - - enum PhoneType { - MOBILE = 0; - HOME = 1; - WORK = 2; - } - - message PhoneNumber { - required string number = 1; - optional PhoneType type = 2 [default = HOME]; - } - - repeated PhoneNumber phone = 4; -} - -message AddressBook { - repeated Person person = 1; -} diff --git a/examples/reading_a_message.rb b/examples/reading_a_message.rb deleted file mode 100644 index 10bf49c9..00000000 --- a/examples/reading_a_message.rb +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env ruby - -require 'addressbook.pb' - -def list_people(address_book) - address_book.person.each do |person| - puts "Person ID: #{person.id}" - puts " Name: #{person.name}" - puts " E-mail: #{person.email}" unless person.email.empty? - person.phone.each do |phone_number| - print(case phone_number.type - when Tutorial::Person::PhoneType::MOBILE then - ' Mobile phone #: ' - when Tutorial::Person::PhoneType::HOME then - ' Home phone #: ' - when Tutorial::Person::PhoneType::WORK then - ' Work phone #: ' - end) - puts phone_number.number - end - end -end - -unless ARGV.size == 1 - puts "Usage: #{$0} ADDRESS_BOOK_FILE" - exit -end - -address_book = Tutorial::AddressBook.new -address_book.parse_from_file ARGV[0] - -list_people address_book diff --git a/examples/writing_a_message.rb b/examples/writing_a_message.rb deleted file mode 100644 index ab09b18a..00000000 --- a/examples/writing_a_message.rb +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env ruby - -require 'addressbook.pb' - -def prompt_for_address(person) - print 'Enter person ID number: ' - person.id = STDIN.gets.strip.to_i - print 'Enter name: ' - person.name = STDIN.gets.strip - print 'Enter email address (blank for none): ' - email = STDIN.gets.strip - person.email = email unless email.empty? - - loop do - print 'Enter a phone number (or leave blank to finish): ' - break if (number = STDIN.gets.strip).empty? - - person.phone << Tutorial::Person::PhoneNumber.new - person.phone.last.number = number - - print 'Is this a mobile, home, or work phone? ' - person.phone.last.type = - case type = STDIN.gets.strip - when 'mobile' then - Tutorial::Person::PhoneType::MOBILE - when 'home' then - Tutorial::Person::PhoneType::HOME - when 'work' then - Tutorial::Person::PhoneType::WORK - else - puts 'Unknown phone type; leaving as default value.' - nil - end - end -end - -unless ARGV.size == 1 - puts "Usage: #{$0} ADDRESS_BOOK_FILE" - exit -end - -address_book = Tutorial::AddressBook.new -address_book.parse_from_file ARGV[0] if File.exist? ARGV[0] -address_book.person << Tutorial::Person.new -prompt_for_address address_book.person.last -address_book.serialize_to_file ARGV[0] diff --git a/lib/protobuf.rb b/lib/protobuf.rb index 81598f9f..c6040fd4 100644 --- a/lib/protobuf.rb +++ b/lib/protobuf.rb @@ -1,32 +1,130 @@ +require 'base64' require 'logger' -require 'socket' require 'pp' +require 'socket' require 'stringio' -module Protobuf +require 'active_support' +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/object/try' +require 'active_support/inflector' +require 'active_support/json' +require 'active_support/notifications' +# Under MRI, this optimizes proto decoding by around 15% in tests. +# When unavailable, we fall to pure Ruby. +# rubocop:disable Lint/HandleExceptions +begin + require 'varint/varint' +rescue LoadError end +# rubocop:enable Lint/HandleExceptions +# rubocop:disable Lint/HandleExceptions +begin + require 'protobuf_java_helpers' +rescue LoadError +end +# rubocop:enable Lint/HandleExceptions +# All top-level run time code requires, ordered by necessity +require 'protobuf/wire_type' -Protobuf::ClientType = ENV['PB_CLIENT_TYPE'] if ENV['PB_CLIENT_TYPE'] -Protobuf::ServerType = ENV['PB_SERVER_TYPE'] if ENV['PB_SERVER_TYPE'] +require 'protobuf/varint_pure' +require 'protobuf/varint' -# When setting up a client -unless defined?(Protobuf::ClientType) && Protobuf::ClientType == "Socket" - Protobuf::ClientType = "EventMachine" - require 'eventmachine' - require 'protobuf/ext/eventmachine' - require 'protobuf/rpc/connectors/eventmachine' -end +require 'protobuf/exceptions' +require 'protobuf/deprecation' +require 'protobuf/logging' + +require 'protobuf/encoder' +require 'protobuf/decoder' + +require 'protobuf/optionable' +require 'protobuf/field' +require 'protobuf/enum' +require 'protobuf/message' +require 'protobuf/descriptors' + +module Protobuf + + class << self + # Client Host + # + # Default: `hostname` of the system + # + # The name or address of the host to use during client RPC calls. + attr_writer :client_host + end + + def self.after_server_bind(&block) + ::ActiveSupport::Notifications.subscribe('after_server_bind') do |*args| + block.call(*args) + end + end -# For running the rpc_server -unless defined?(Protobuf::ServerType) && Protobuf::ServerType == "SocketServer" - Protobuf::ServerType = "EventedServer" - require 'eventmachine' - require 'protobuf/ext/eventmachine' - require 'protobuf/rpc/servers/evented_server' + def self.before_server_bind(&block) + ::ActiveSupport::Notifications.subscribe('before_server_bind') do |*args| + block.call(*args) + end + end + + def self.client_host + @client_host ||= Socket.gethostname + end + + def self.connector_type_class + @connector_type_class ||= ::Protobuf::Rpc::Connectors::Socket + end + + def self.connector_type_class=(type_class) + @connector_type_class = type_class + end + + # GC Pause during server requests + # + # Default: false + # + # Boolean value to tell the server to disable + # the Garbage Collector when handling an rpc request. + # Once the request is completed, the GC is enabled again. + # This optomization provides a huge boost in speed to rpc requests. + def self.gc_pause_server_request? + return @gc_pause_server_request unless @gc_pause_server_request.nil? + self.gc_pause_server_request = false + end + + def self.gc_pause_server_request=(value) + @gc_pause_server_request = !!value + end + + # Permit unknown field on Message initialization + # + # Default: true + # + # Simple boolean to define whether we want to permit unknown fields + # on Message intialization; otherwise a ::Protobuf::FieldNotDefinedError is thrown. + def self.ignore_unknown_fields? + !defined?(@ignore_unknown_fields) || @ignore_unknown_fields + end + + def self.ignore_unknown_fields=(value) + @ignore_unknown_fields = !!value + end end -require 'protobuf/rpc/client' -require 'protobuf/rpc/connectors/socket' -require 'protobuf/rpc/service' -require 'protobuf/rpc/servers/socket_server' +unless ENV.key?('PB_NO_NETWORKING') + require 'protobuf/rpc/client' + require 'protobuf/rpc/service' + + env_connector_type = ENV.fetch('http://github.com/PB_CLIENT_TYPE') do + :socket + end + + symbolized_connector_type = env_connector_type.to_s.downcase.strip.to_sym + if [:zmq, :socket].include?(symbolized_connector_type) + require "protobuf/#{symbolized_connector_type}" + else + require "#{env_connector_type}" # rubocop:disable Style/UnneededInterpolation + classified = env_connector_type.classify + ::Protobuf.connector_type_class = classified.constantize + end +end diff --git a/lib/protobuf/cli.rb b/lib/protobuf/cli.rb new file mode 100644 index 00000000..0a84443b --- /dev/null +++ b/lib/protobuf/cli.rb @@ -0,0 +1,258 @@ +require 'active_support' +require 'active_support/core_ext/hash/keys' +require 'active_support/inflector' + +require 'thor' +require 'protobuf/version' +require 'protobuf/logging' +require 'protobuf/rpc/servers/socket_runner' +require 'protobuf/rpc/servers/zmq_runner' + +module Protobuf + class CLI < ::Thor + include ::Thor::Actions + include ::Protobuf::Logging + + attr_accessor :runner, :mode, :exit_requested + + no_commands do + alias_method :exit_requested?, :exit_requested + end + + default_task :start + + desc 'start APP_FILE', 'Run the RPC server in the given mode, preloading the given APP_FILE. This is the default task.' + + option :host, :type => :string, :default => '127.0.0.1', :aliases => %w(-o), :desc => 'Host to bind.' + option :port, :type => :numeric, :default => 9399, :aliases => %w(-p), :desc => 'Master Port to bind.' + + option :backlog, :type => :numeric, :default => 100, :aliases => %w(-b), :desc => 'Backlog for listening socket when using Socket Server.' + option :threshold, :type => :numeric, :default => 100, :aliases => %w(-t), :desc => 'Multi-threaded Socket Server cleanup threshold.' + option :threads, :type => :numeric, :default => 5, :aliases => %w(-r), :desc => 'Number of worker threads to run. Only applicable in --zmq mode.' + + option :log, :type => :string, :default => STDOUT, :aliases => %w(-l), :desc => 'Log file or device. Default is STDOUT.' + option :level, :type => :numeric, :default => ::Logger::INFO, :aliases => %w(-v), :desc => 'Log level to use, 0-5 (see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/)' + + option :socket, :type => :boolean, :aliases => %w(-s), :desc => 'Socket Mode for server and client connections.' + option :zmq, :type => :boolean, :aliases => %w(-z), :desc => 'ZeroMQ Socket Mode for server and client connections.' + + option :beacon_interval, :type => :numeric, :desc => 'Broadcast beacons every N seconds. (default: 5)' + option :beacon_port, :type => :numeric, :desc => 'Broadcast beacons to this port (default: value of ServiceDirectory.port)' + option :broadcast_beacons, :type => :boolean, :desc => 'Broadcast beacons for dynamic discovery (Currently only available with ZeroMQ).' + option :broadcast_busy, :type => :boolean, :default => false, :desc => 'Remove busy nodes from cluster when all workers are busy (Currently only available with ZeroMQ).' + option :debug, :type => :boolean, :default => false, :aliases => %w(-d), :desc => 'Debug Mode. Override log level to DEBUG.' + option :gc_pause_request, :type => :boolean, :default => false, :desc => 'DEPRECATED: Enable/Disable GC pause during request.' + option :print_deprecation_warnings, :type => :boolean, :default => nil, :desc => 'Cause use of deprecated fields to be printed or ignored.' + option :workers_only, :type => :boolean, :default => false, :desc => "Starts process with only workers (no broker/frontend is started) only relevant for Zmq Server" + option :worker_port, :type => :numeric, :default => nil, :desc => "Port for 'backend' where workers connect (defaults to port + 1)" + option :zmq_inproc, :type => :boolean, :default => true, :desc => 'Use inproc protocol for zmq Server/Broker/Worker' + + def start(app_file) + debug_say('Configuring the rpc_server process') + + configure_logger + configure_traps + configure_runner_mode + create_runner + configure_process_name(app_file) + configure_gc + configure_deprecation_warnings + + require_application(app_file) unless exit_requested? + start_server unless exit_requested? + rescue => e + say_and_exit('ERROR: RPC Server failed to start.', e) + end + + desc 'version', 'Print ruby and protoc versions and exit.' + def version + say("Ruby Protobuf v#{::Protobuf::VERSION}") + end + + no_tasks do + + # Tell protobuf how to handle the printing of deprecated field usage. + def configure_deprecation_warnings + ::Protobuf.print_deprecation_warnings = + if options.print_deprecation_warnings.nil? + !ENV.key?("PB_IGNORE_DEPRECATIONS") + else + options.print_deprecation_warnings? + end + end + + # If we pause during request we don't need to pause in serialization + def configure_gc + say "DEPRECATED: The gc_pause_request option is deprecated and will be removed in 4.0." if options.gc_pause_request? + + debug_say('Configuring gc') + + ::Protobuf.gc_pause_server_request = + if defined?(JRUBY_VERSION) + # GC.enable/disable are noop's on Jruby + false + else + options.gc_pause_request? + end + end + + # Setup the protobuf logger. + def configure_logger + debug_say('Configuring logger') + + log_level = options.debug? ? ::Logger::DEBUG : options.level + + ::Protobuf::Logging.initialize_logger(options.log, log_level) + + # Debug output the server options to the log file. + logger.debug { 'Debugging options:' } + logger.debug { options.inspect } + end + + # Re-write the $0 var to have a nice process name in ps. + def configure_process_name(app_file) + debug_say('Configuring process name') + $0 = "rpc_server --#{mode} #{options.host}:#{options.port} #{app_file}" + end + + # Configure the mode of the server and the runner class. + def configure_runner_mode + debug_say('Configuring runner mode') + server_type = ENV["PB_SERVER_TYPE"] + + self.mode = if multi_mode? + say('WARNING: You have provided multiple mode options. Defaulting to socket mode.', :yellow) + :socket + elsif options.zmq? + :zmq + else + case server_type + when nil, /\Asocket[[:space:]]*\z/i + :socket + when /\Azmq[[:space:]]*\z/i + :zmq + else + require server_type.to_s + server_type + end + end + end + + # Configure signal traps. + # TODO: add signal handling for hot-reloading the application. + def configure_traps + debug_say('Configuring traps') + + exit_signals = [:INT, :TERM] + exit_signals << :QUIT unless defined?(JRUBY_VERSION) + + exit_signals.each do |signal| + debug_say("Registering trap for exit signal #{signal}", :blue) + + trap(signal) do + self.exit_requested = true + shutdown_server + end + end + end + + # Create the runner for the configured mode + def create_runner + debug_say("Creating #{mode} runner") + self.runner = case mode + when :zmq + create_zmq_runner + when :socket + create_socket_runner + else + say("Extension runner mode: #{mode}") + create_extension_server_runner + end + end + + # Say something if we're in debug mode. + def debug_say(message, color = :yellow) + say(message, color) if options.debug? + end + + # Internal helper to determine if the modes are multi-set which is not valid. + def multi_mode? + options.zmq? && options.socket? + end + + # Require the application file given, exiting if the file doesn't exist. + def require_application(app_file) + debug_say('Requiring app file') + require app_file + rescue LoadError => e + say_and_exit("Failed to load application file #{app_file}", e) + end + + def runner_options + opt = options.to_hash.symbolize_keys + + opt[:workers_only] = (!!ENV['PB_WORKERS_ONLY']) || options.workers_only + + opt + end + + def say_and_exit(message, exception = nil) + message = set_color(message, :red) if options.log == STDOUT + + logger.error { message } + + if exception + $stderr.puts "[#{exception.class.name}] #{exception.message}" + $stderr.puts exception.backtrace.join("\n") + + logger.error { "[#{exception.class.name}] #{exception.message}" } + logger.debug { exception.backtrace.join("\n") } + end + + exit(1) + end + + def create_extension_server_runner + classified = mode.classify + extension_server_class = classified.constantize + + self.runner = extension_server_class.new(runner_options) + end + + def create_socket_runner + require 'protobuf/socket' + + self.runner = ::Protobuf::Rpc::SocketRunner.new(runner_options) + end + + def create_zmq_runner + require 'protobuf/zmq' + + self.runner = ::Protobuf::Rpc::ZmqRunner.new(runner_options) + end + + def shutdown_server + logger.info { 'RPC Server shutting down...' } + runner.stop + ::Protobuf::Rpc::ServiceDirectory.instance.stop + end + + # Start the runner and log the relevant options. + def start_server + debug_say('Running server') + + ::ActiveSupport::Notifications.instrument("before_server_bind") + + runner.run do + logger.info do + "pid #{::Process.pid} -- #{mode} RPC Server listening at #{options.host}:#{options.port}" + end + + ::ActiveSupport::Notifications.instrument("after_server_bind") + end + + logger.info { 'Shutdown complete' } + end + end + end +end diff --git a/lib/protobuf/code_generator.rb b/lib/protobuf/code_generator.rb new file mode 100644 index 00000000..0a006f56 --- /dev/null +++ b/lib/protobuf/code_generator.rb @@ -0,0 +1,130 @@ +require 'active_support' +require 'active_support/core_ext/module/aliasing' +require 'protobuf/generators/file_generator' + +module Protobuf + class CodeGenerator + + CodeGeneratorFatalError = Class.new(RuntimeError) + + def self.fatal(message) + fail CodeGeneratorFatalError, message + end + + def self.print_tag_warning_suppress + STDERR.puts "Suppress tag warning output with PB_NO_TAG_WARNINGS=1." + def self.print_tag_warning_suppress; end # rubocop:disable Lint/DuplicateMethods, Lint/NestedMethodDefinition + end + + def self.warn(message) + STDERR.puts("[WARN] #{message}") + end + + private + + attr_accessor :request + + public + + def initialize(request_bytes) + @request_bytes = request_bytes + self.request = ::Google::Protobuf::Compiler::CodeGeneratorRequest.decode(request_bytes) + end + + def eval_unknown_extensions! + request.proto_file.each do |file_descriptor| + ::Protobuf::Generators::FileGenerator.new(file_descriptor).eval_unknown_extensions! + end + self.request = ::Google::Protobuf::Compiler::CodeGeneratorRequest.decode(@request_bytes) + end + + def generate_file(file_descriptor) + ::Protobuf::Generators::FileGenerator.new(file_descriptor).generate_output_file + end + + def response_bytes + generated_files = request.proto_file.map do |file_descriptor| + generate_file(file_descriptor) + end + + ::Google::Protobuf::Compiler::CodeGeneratorResponse.encode( + :file => generated_files, + :supported_features => supported_features, + ) + end + + def supported_features + # The only available feature is proto3 with optional fields. + # This is backwards compatible with proto2 optional fields. + ::Google::Protobuf::Compiler::CodeGeneratorResponse::Feature::FEATURE_PROTO3_OPTIONAL.to_i + end + + Protobuf::Field::BaseField.module_eval do + def define_set_method! + end + + def set_without_options(message_instance, bytes) + return message_instance[name] = decode(bytes) unless repeated? + + if map? + hash = message_instance[name] + entry = decode(bytes) + # decoded value could be nil for an + # enum value that is not recognized + hash[entry.key] = entry.value unless entry.value.nil? + return hash[entry.key] + end + + return message_instance[name] << decode(bytes) unless packed? + + array = message_instance[name] + stream = StringIO.new(bytes) + + if wire_type == ::Protobuf::WireType::VARINT + array << decode(Varint.decode(stream)) until stream.eof? + elsif wire_type == ::Protobuf::WireType::FIXED64 + array << decode(stream.read(8)) until stream.eof? + elsif wire_type == ::Protobuf::WireType::FIXED32 + array << decode(stream.read(4)) until stream.eof? + end + end + + # Sets a MessageField that is known to be an option. + # We must allow fields to be set one at a time, as option syntax allows us to + # set each field within the option using a separate "option" line. + def set_with_options(message_instance, bytes) + if message_instance[name].is_a?(::Protobuf::Message) + gp = Google::Protobuf + if message_instance.is_a?(gp::EnumOptions) || message_instance.is_a?(gp::EnumValueOptions) || + message_instance.is_a?(gp::FieldOptions) || message_instance.is_a?(gp::FileOptions) || + message_instance.is_a?(gp::MethodOptions) || message_instance.is_a?(gp::ServiceOptions) || + message_instance.is_a?(gp::MessageOptions) + + original_field = message_instance[name] + decoded_field = decode(bytes) + decoded_field.each_field do |subfield, subvalue| + option_set(original_field, subfield, subvalue) { decoded_field.field?(subfield.tag) } + end + return + end + end + + set_without_options(message_instance, bytes) + end + alias_method :set, :set_with_options + + def option_set(message_field, subfield, subvalue) + return unless yield + if subfield.repeated? + message_field[subfield.tag].concat(subvalue) + elsif message_field[subfield.tag] && subvalue.is_a?(::Protobuf::Message) + subvalue.each_field do |f, v| + option_set(message_field[subfield.tag], f, v) { subvalue.field?(f.tag) } + end + else + message_field[subfield.tag] = subvalue + end + end + end + end +end diff --git a/lib/protobuf/common/logger.rb b/lib/protobuf/common/logger.rb deleted file mode 100644 index bb9b0d17..00000000 --- a/lib/protobuf/common/logger.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'logger' - -module Protobuf - class Logger < ::Logger - - class << self - attr_accessor :file, :level - - # One-line file/level configuration - def configure(options) - self.file = options[:file] if options[:file] - self.level = options[:level] if options[:level] - end - - # Use to reset the instance - def reset_device! - self.file = self.level = @__instance = nil - end - - # Singleton instance - def instance - @__instance ||= begin - log = nil - if @file and @level - log = new(self.file) - log.level = self.level - end - log - end - end - - # Stub out the log methods for Protobuf::Logger as singleton methods - [:debug, :info, :warn, :error, :fatal, :any, :add, :log].each do |m| - define_method(m) do |*params, &block| - instance && instance.__send__(m, *params, &block) - end - end - end - - # - # LogMethods module for log method including, e.g.: - # - # class MyClass - # include Protobuf::Logger::LogMethods - # ... - # end - # - # Produce a module to allow "include" in other classes to avoid - # cluttering the namespace of the including class with the other methods defined above - # - module LogMethods - [:debug, :info, :warn, :error, :fatal, :any, :add, :log].each do |m| - define_method("log_#{m}") do |*params, &block| - Protobuf::Logger.__send__(m, *params, &block) - end - end - - def self.included(base) - base.extend(LogMethods) - end - end - - end -end diff --git a/lib/protobuf/common/util.rb b/lib/protobuf/common/util.rb deleted file mode 100644 index d0e4d446..00000000 --- a/lib/protobuf/common/util.rb +++ /dev/null @@ -1,59 +0,0 @@ -module Protobuf - module Util - module_function - - # Takes a string or symbol and camelizes it: - # Expects: some_long_name - # Returns: SomeLongName - def camelize(str) - if (str.is_a? Array) - str.map{|p| camelize(p.to_s) }.join('::') - else - str.to_s.gsub(/(?:\A|_)(\w)/) { $1.upcase } - end - end - - # Expects: SomeLongName, SOMELongName - # Returns: some_long_name - def underscore(str) - str.to_s.gsub(/\B[A-Z](?:[a-z])/, '_\&').downcase - end - - # Expects: SomeModule::Path - # Returns: some_module/path - def module_to_path(str) - pkg = str.to_s.split('::') - pkg.map{|e| underscore(e) }.join('/') - end - - # Expects: PackageA.PackageB - # Returns: package_a/package_b - def package_to_path(str) - str.to_s.split('.').map{|e| underscore(e) }.join('/') - end - - # Takes a class constant and converts it to a string resembling a java package path - # Expects: ModA::ModB::MyService - # Returns: mod_a.mod_b.MyService - def packagize(klass) - klass = klass.to_s.split('::') unless klass.is_a? Array - klass_name = klass.pop - klass.map{|e| underscore(e) }.join('.') + ".#{klass_name}" - end - - # The reverse of packagize. Takes a string resembling a java package path - # and converts it into a module constant - # Expects: mod_a.mod_b.MyService - # Returns: ModA::ModB::MyService - def moduleize(str) - str = str.join('.') if str.is_a? Array - str.split('.').map{|e| camelize(e) }.join('::') - end - - def constantize(klass) - constants = moduleize(klass).split('::') - constants.inject(Module.const_get(constants.shift)) {|const, obj| const.const_get(obj) } - end - - end -end diff --git a/lib/protobuf/compiler/compiler.rb b/lib/protobuf/compiler/compiler.rb deleted file mode 100644 index 9e114dfd..00000000 --- a/lib/protobuf/compiler/compiler.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'fileutils' -require 'protobuf/compiler/proto_parser' -require 'protobuf/compiler/nodes' -require 'protobuf/compiler/visitors' - -module Protobuf - class Compiler - def self.compile(proto_file, proto_dir='.', out_dir='.', file_create=true) - self.new.compile(proto_file, proto_dir, out_dir, file_create) - end - - def compile(proto_file, proto_dir='.', out_dir='.', file_create=true) - create_message(proto_file, proto_dir, out_dir, file_create) - create_rpc(proto_file, proto_dir, out_dir, file_create) - end - - def create_message(proto_file, proto_dir='.', out_dir='.', file_create=true) - rb_file = File.join(out_dir, File.basename(proto_file).sub(/\.[^\0]*\z/, '') + '.pb.rb') - proto_path = validate_existence(proto_file, proto_dir) - - message_visitor = Visitor::CreateMessageVisitor.new(proto_file, proto_dir, out_dir) - File.open(proto_path) do |file| - message_visitor.visit(ProtoParser.new.parse(file)) - end - message_visitor.create_files(rb_file, out_dir, file_create) - end - - def create_rpc(proto_file, proto_dir='.', out_dir='.', file_create=true) - message_file = File.join(out_dir, File.basename(proto_file).sub(/\.[^\0]*\z/, '') + '.pb.rb') - proto_path = validate_existence(proto_file, proto_dir) - - rpc_visitor = Visitor::CreateRpcVisitor.new - File.open(proto_path) do |file| - rpc_visitor.visit(ProtoParser.new.parse(file)) - end - rpc_visitor.create_files(message_file, out_dir, file_create) - end - - def validate_existence(path, base_dir) - if File.exist?(path) - path - else - newpath = File.join(base_dir, path) - if File.exist?(newpath) - newpath - else - raise ArgumentError, "File does not exist: #{path}" - end - end - end - end -end diff --git a/lib/protobuf/compiler/nodes.rb b/lib/protobuf/compiler/nodes.rb deleted file mode 100644 index eccddf70..00000000 --- a/lib/protobuf/compiler/nodes.rb +++ /dev/null @@ -1,323 +0,0 @@ -require 'protobuf/common/util' -require 'protobuf/descriptor/descriptor_proto' - -module Protobuf - module Node - class Base - def define_in_the_file(visitor) - visitor.write("defined_in __FILE__") if visitor.attach_proto? - end - - def accept_message_visitor(visitor) - end - - def accept_rpc_visitor(vistor) - end - - def accept_descriptor_visitor(visitor) - end - end - - class ProtoNode < Base - attr_reader :children - - def initialize(children) - @children = children || [] - end - - def accept_message_visitor(visitor) - visitor.write('### Generated by rprotoc. DO NOT EDIT!') - visitor.write("### ") if visitor.attach_proto? - visitor.write(visitor.commented_proto_contents) if visitor.attach_proto? - visitor.write(<<-EOS) -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - EOS - @children.each {|child| child.accept_message_visitor(visitor) } - visitor.close_ruby - end - - def accept_rpc_visitor(visitor) - @children.each {|child| child.accept_rpc_visitor(visitor) } - end - - def accept_descriptor_visitor(visitor) - descriptor = Google::Protobuf::FileDescriptorProto.new(:name => visitor.filename) - visitor.file_descriptor = descriptor - visitor.in_context(descriptor) do - @children.each {|child| child.accept_descriptor_visitor(visitor) } - end - end - end - - class ImportNode < Base - def initialize(path) - @path = path - end - - def accept_message_visitor(visitor) - visitor.write("require '#{visitor.required_message_from_proto(@path)}'") - end - - def accept_descriptor_visitor(visitor) - visitor.current_descriptor.dependency << @path - end - end - - class PackageNode < Base - def initialize(path_list) - @path_list = path_list - end - - def accept_message_visitor(visitor) - visitor.package = @path_list.dup - @path_list.each do |path| - visitor.write("module #{Util.camelize(path)}") - visitor.increment - end - end - - def accept_rpc_visitor(visitor) - visitor.package = @path_list.dup - end - - def accept_descriptor_visitor(visitor) - visitor.current_descriptor.package = @path_list.join('.') - end - end - - class OptionNode < Base - def initialize(name_list, value) - @name_list, @value = name_list, value - end - - def accept_message_visitor(visitor) - visitor.write("::Protobuf::OPTIONS[:#{@name_list.join('.').inspect}] = #{@value.inspect}") - end - - def accept_descriptor_visitor(visitor) - visitor.add_option(@name_list.join('.'), @value) - end - end - - class MessageNode < Base - def initialize(name, children) - @name, @children = name, children - end - - def accept_message_visitor(visitor) - class_name = @name.to_s - class_name.gsub!(/\A[a-z]/) {|c| c.upcase} - visitor.write("class #{class_name} < ::Protobuf::Message") - visitor.in_context(self.class) do - define_in_the_file(visitor) - @children.each {|child| child.accept_message_visitor(visitor) } - end - visitor.write('end') - end - - def accept_descriptor_visitor(visitor) - descriptor = Google::Protobuf::DescriptorProto.new(:name => @name.to_s) - visitor.descriptor = descriptor - visitor.in_context(descriptor) do - @children.each {|child| child.accept_descriptor_visitor(visitor) } - end - end - end - - class ExtendNode < Base - def initialize(name, children) - @name, @children = name, children - end - - def accept_message_visitor(visitor) - name = @name.is_a?(Array) ? @name.join : name.to_s - visitor.write("class #{name} < ::Protobuf::Message") - visitor.in_context(self.class) do - define_in_the_file(visitor) - @children.each {|child| child.accept_message_visitor(visitor) } - end - visitor.write('end') - end - - def accept_descriptor_visitor(visitor) - # TODO: how should i handle this? - end - end - - class EnumNode < Base - def initialize(name, children) - @name, @children = name, children - end - - def accept_message_visitor(visitor) - visitor.write("class #{@name} < ::Protobuf::Enum") - visitor.in_context(self.class) do - define_in_the_file(visitor) - @children.each {|child| child.accept_message_visitor(visitor) } - end - visitor.write('end') - end - - def accept_descriptor_visitor(visitor) - descriptor = Google::Protobuf::EnumDescriptorProto.new(:name => @name.to_s) - visitor.enum_descriptor = descriptor - visitor.in_context(descriptor) do - @children.each {|child| child.accept_descriptor_visitor(visitor) } - end - end - end - - class EnumFieldNode < Base - def initialize(name, value) - @name, @value = name, value - end - - def accept_message_visitor(visitor) - visitor.write("define :#{@name}, #{@value}") - end - - def accept_descriptor_visitor(visitor) - descriptor = Google::Protobuf::EnumValueDescriptorProto.new(:name => @name.to_s, :number => @value) - visitor.enum_value_descriptor = descriptor - end - end - - class ServiceNode < Base - def initialize(name, children) - @name, @children = name, children - end - - def accept_message_visitor(visitor) - # do nothing - end - - def accept_rpc_visitor(visitor) - visitor.current_service = @name - @children.each {|child| child.accept_rpc_visitor(visitor) } - end - - def accept_descriptor_visitor(visitor) - descriptor = Google::Protobuf::ServiceDescriptorProto.new(:name => @name.to_s) - visitor.service_descriptor = descriptor - visitor.in_context(descriptor) do - @children.each {|child| child.accept_descriptor_visitor(visitor) } - end - end - end - - class RpcNode < Base - def initialize(name, request, response) - @name, @request, @response = name, request, response - end - - def accept_message_visitor(visitor) - # do nothing - end - - def accept_rpc_visitor(visitor) - visitor.add_rpc(@name, @request, @response) - end - - def accept_descriptor_visitor(visitor) - descriptor = Google::Protobuf::MethodDescriptorProto.new(:name => @name.to_s, :input_type => @request.to_s, :output_type => @response.to_s) - visitor.method_descriptor = descriptor - end - end - - class GroupNode < Base - def initialize(label, name, value, children) - @label, @name, @value, @children = label, name, value, children - end - - def accept_message_visitor(visitor) - raise NotImplementedError - end - - def accept_descriptor_visitor(visitor) - raise NotImplementedError - end - end - - class FieldNode < Base - def initialize(label, type, name, value, opts={}) - @label, @type, @name, @value, @opts = label, type, name, value, opts - end - - def accept_message_visitor(visitor) - opts = @opts.empty? ? '' : ", #{@opts.map{|k, v| ":#{k} => #{v.inspect}" }.join(', ')}" - if visitor.context.first == ExtendNode - opts << ', :extension => true' - end - type = if @type.is_a?(Array) - then (@type.size > 1) ? "'#{@type.map{|e| Util.camelize(e) }.join('::')}'" : @type[0] - else @type - end - visitor.write("#{@label} :#{type}, :#{@name}, #{@value}#{opts}") - end - - def accept_descriptor_visitor(visitor) - descriptor = Google::Protobuf::FieldDescriptorProto.new(:name => @name.to_s, :number => @value) - descriptor.label = Google::Protobuf::FieldDescriptorProto::Label.const_get("LABEL_#{@label.to_s.upcase}") - descriptor.type = Google::Protobuf::FieldDescriptorProto::Type.const_get("TYPE_#{@type.to_s.upcase}") if predefined_type? - descriptor.type_name = @type.is_a?(Array) ? @type.join : @type.to_s - @opts.each do |key, val| - case key.to_sym - when :default then - descriptor.default_value = val.to_s - end - end - visitor.field_descriptor = descriptor - end - - private - - def predefined_type? - # TODO: constantize - %w{double float int64 uint64 int32 fixed64 fixed32 bool string group message bytes uint32 enum sfixed32 sfixed64 sint32 sint64}.include?(@type.to_s) - end - end - - class ExtensionsNode < Base - def initialize(range) - @range = range - end - - def accept_message_visitor(visitor) - visitor.write("extensions #{@range.first.to_s}") - end - - def accept_descriptor_visitor(visitor) - descriptor = Google::Protobuf::DescriptorProto::ExtensionRange.new(:start => @range.first.low) - case @range.first.high - when NilClass then # ignore - when :max then descriptor.end = 1 - else descriptor.end = @range.first.high - end - visitor.extension_range_descriptor = descriptor - end - end - - class ExtensionRangeNode < Base - attr_reader :low, :high - - def initialize(low, high=nil) - @low, @high = low, high - end - - #def accept_message_visitor(visitor) - #end - - def to_s - if @high.nil? - @low.to_s - elsif @high == :max - "#{@low}..::Protobuf::Extend::MAX" - else - "#{@low}..#{@high}" - end - end - end - end -end diff --git a/lib/protobuf/compiler/proto.y b/lib/protobuf/compiler/proto.y deleted file mode 100644 index 242e7dd9..00000000 --- a/lib/protobuf/compiler/proto.y +++ /dev/null @@ -1,216 +0,0 @@ -class Protobuf::ProtoParser -rule - proto : proto_item - { result = Protobuf::Node::ProtoNode.new(val) } - | proto proto_item - { result.children << val[1] if val[1]} - - proto_item : message - | extend - | enum - | import - | package - | option - | service - | ';' { result = nil } - - import : 'import' STRING_LITERAL ';' - { result = Protobuf::Node::ImportNode.new(val[1]) } - - package : 'package' IDENT dot_ident_list ';' - { result = Protobuf::Node::PackageNode.new(val[2].unshift(val[1])) } - - dot_ident_list : - { result = [] } - | dot_ident_list '.' IDENT - { result << val[2] } - - option : 'option' option_body ';' - { result = Protobuf::Node::OptionNode.new(*val[1]) } - - option_body : IDENT dot_ident_list '=' constant - { result = [val[1].unshift(val[0]), val[3]] } - - message : 'message' IDENT message_body - { result = Protobuf::Node::MessageNode.new(val[1], val[2]) } - - extend : 'extend' user_type '{' extend_body_list '}' - { result = Protobuf::Node::ExtendNode.new(val[1], val[3]) } - - extend_body_list : - { result = [] } - | extend_body_list extend_body - { result << val[1] if val[1] } - - extend_body : field - | group - | ';' { result = nil } - - enum : 'enum' IDENT '{' enum_body_list '}' - { result = Protobuf::Node::EnumNode.new(val[1], val[3]) } - - enum_body_list : - { result = [] } - | enum_body_list enum_body - { result << val[1] if val[1] } - - enum_body : option - | enum_field - | ';' { result = nil } - - enum_field : IDENT '=' integer_literal ';' - { result = Protobuf::Node::EnumFieldNode.new(val[0], val[2]) } - - service : 'service' IDENT '{' service_body_list '}' - { result = Protobuf::Node::ServiceNode.new(val[1], val[3]) } - - service_body_list : - { result = [] } - | service_body_list service_body - { result << val[1] if val[1] } - - service_body : option - | rpc - | ';' { result = nil} - - rpc : 'rpc' IDENT '(' rpc_arg ')' 'returns' '(' rpc_arg ')' ';' - { result = Protobuf::Node::RpcNode.new(val[1], val[3], val[7]) } - - rpc_arg : - | user_type - - message_body : '{' message_body_body_list '}' - { result = val[1] } - - message_body_body_list : - { result = [] } - | message_body_body_list message_body_body - { result << val[1] if val[1] } - - message_body_body : field - | enum - | message - | extend - | extensions - | group - | option - | ';' { result = nil } - - group : label 'group' CAMEL_IDENT '=' integer_literal message_body - { result = Protobuf::Node::GroupNode.new(val[0], val[2], val[4], val[5]) } - - field : label type field_name '=' integer_literal ';' - { result = Protobuf::Node::FieldNode.new(val[0], val[1], val[2], val[4]) } - | label type field_name '=' integer_literal '[' field_option_list ']' ';' - { result = Protobuf::Node::FieldNode.new(val[0], val[1], val[2], val[4], val[6]) } - - field_name : IDENT | "required" | "optional" | "repeated" | "import" | "package" | "option" | "message" | "extend" | "enum" | "service" | "rpc" | "returns" | "group" | "default" | "extensions" | "to" | "max" | "double" | "float" | "int32" | "int64" | "uint32" | "uint64" | "sint32" | "sint64" | "fixed32" | "fixed64" | "sfixed32" | "sfixed64" | "bool" | "string" | "bytes" - - field_option_list : field_option - { result = val } - | field_option_list ',' field_option - { result << val[2] } - - field_option : option_body - | 'default' '=' constant - { result = [:default, val[2]] } - - extensions : 'extensions' extension comma_extension_list ';' - { result = Protobuf::Node::ExtensionsNode.new(val[2].unshift(val[1])) } - - comma_extension_list : - { result = [] } - | ',' extension - { result << val[1] } - - extension : integer_literal - { result = Protobuf::Node::ExtensionRangeNode.new(val[0]) } - | integer_literal 'to' integer_literal - { result = Protobuf::Node::ExtensionRangeNode.new(val[0], val[2]) } - | integer_literal 'to' 'max' - { result = Protobuf::Node::ExtensionRangeNode.new(val[0], :max) } - - label : 'required' - | 'optional' - | 'repeated' - - type : 'double' | 'float' | 'int32' | 'int64' | 'uint32' | 'uint64' - | 'sint32' | 'sint64' | 'fixed32' | 'fixed64' | 'sfixed32' | 'sfixed64' - | 'bool' | 'string' | 'bytes' | user_type - - user_type : IDENT dot_ident_list - { result = val[1].unshift(val[0]) } - | '.' IDENT dot_ident_list - { result = val[1].unshift(val[0]) } - - constant : IDENT - | integer_literal - | FLOAT_LITERAL - | STRING_LITERAL - | BOOLEAN_LITERAL - - integer_literal : DEC_INTEGER - | HEX_INTEGER - | OCT_INTEGER -end - ----- inner - - require 'strscan' - - def parse(f) - @scanner = StringScanner.new(f.read) - yyparse(self, :scan) - end - - def scan_debug - scan do |token, value| - p [token, value] - yield [token, value] - end - end - - def scan - until @scanner.eos? - case - when match(/\s+/, /\/\/.*/) - # skip - when match(/\/\*/) - # C-like comment - raise 'EOF inside block comment' until @scanner.scan_until(/\*\//) - when match(/(?:required|optional|repeated|import|package|option|message|extend|enum|service|rpc|returns|group|default|extensions|to|max|double|float|int32|int64|uint32|uint64|sint32|sint64|fixed32|fixed64|sfixed32|sfixed64|bool|string|bytes)\b/) - yield [@token, @token.to_sym] - when match(/[+-]?\d*\.\d+([Ee][\+-]?\d+)?/) - yield [:FLOAT_LITERAL, @token.to_f] - when match(/[+-]?[1-9]\d*(?!\.)/, /0(?![.xX0-9])/) - yield [:DEC_INTEGER, @token.to_i] - when match(/0[xX]([A-Fa-f0-9])+/) - yield [:HEX_INTEGER, @token.to_i(0)] - when match(/0[0-7]+/) - yield [:OCT_INTEGER, @token.to_i(0)] - when match(/(true|false)\b/) - yield [:BOOLEAN_LITERAL, @token == 'true'] - when match(/"(?:[^"\\]+|\\.)*"/, /'(?:[^'\\]+|\\.)*'/) - yield [:STRING_LITERAL, eval(@token)] - when match(/[a-zA-Z_]\w*/) - yield [:IDENT, @token.to_sym] - when match(/[A-Z]\w*/) - yield [:CAMEL_IDENT, @token.to_sym] - when match(/./) - yield [@token, @token] - else - raise "parse error around #{@scanner.string[@scanner.pos, 32].inspect}" - end - end - yield [false, nil] - end - - def match(*regular_expressions) - regular_expressions.each do |re| - if @scanner.scan(re) - @token = @scanner[0] - return true - end - end - false - end diff --git a/lib/protobuf/compiler/proto2.ebnf b/lib/protobuf/compiler/proto2.ebnf deleted file mode 100644 index c24c2e2c..00000000 --- a/lib/protobuf/compiler/proto2.ebnf +++ /dev/null @@ -1,79 +0,0 @@ -# See: http://groups.google.com/group/protobuf/browse_thread/thread/1cccfc624cd612da - -proto ::= ( message | extend | enum | import | package | option | ";" )* - -import ::= "import" strLit ";" - -package ::= "package" ident ( "." ident )* ";" - -option ::= "option" optionBody ";" - -optionBody ::= ident ( "." ident )* "=" constant - -message ::= "message" ident messageBody - -extend ::= "extend" userType "{" ( field | group | ";" )* "}" - -enum ::= "enum" ident "{" ( option | enumField | ";" )* "}" - -enumField ::= ident "=" intLit ";" - -service ::= "service" ident "{" ( option | rpc | ";" )* "}" - -rpc ::= "rpc" ident "(" userType ")" "returns" "(" userType ")" ";" - -messageBody ::= "{" ( field | enum | message | extend | extensions | group | option | ":" )* "}" - -group ::= label "group" camelIdent "=" intLit messageBody - -field ::= label type ident "=" intLit ( "[" fieldOption ( "," fieldOption )* "]" )? ";" -# tag number must be 2^29-1 or lower, not 0, and not 19000-19999 (reserved) - -fieldOption ::= optionBody | "default" "=" constant - -# extension numbers must not overlap with field or other extension numbers -extensions ::= "extensions" extension ( "," extension )* ";" - -extension ::= intLit ( "to" ( intLit | "max" ) )? - -label ::= "required" | "optional" | "repeated" - -type ::= "double" | "float" | "int32" | "int64" | "uint32" | "uint64" - | "sint32" | "sint64" | "fixed32" | "fixed64" | "sfixed32" | "sfixed64" - | "bool" | "string" | "bytes" | userType - -# leading dot for identifiers means they're fully qualified -userType ::= "."? ident ( "." ident )* - -constant ::= ident | intLit | floatLit | strLit | boolLit - -ident ::= /[A-Za-z_][\w_]*/ - -# according to parser.cc, group names must start with a capital letter as a -# hack for backwards-compatibility -camelIdent ::= /[A-Z][\w_]*/ - -intLit ::= decInt | hexInt | octInt - -decInt ::= /[1-9]\d*/ - -hexInt ::= /0[xX]([A-Fa-f0-9])+/ - -octInt ::= /0[0-7]+/ - -# allow_f_after_float_ is disabled by default in tokenizer.cc -floatLit ::= /\d+(\.\d+)?([Ee][\+-]?\d+)?/ - -boolLit ::= "true" | "false" - -# contents must not contain unescaped quote character -strLit ::= quote ( hexEscape | octEscape | charEscape | /[^\0\n]/ )* quote - -quote ::= /["']/ - -hexEscape ::= /\\[Xx][A-Fa-f0-9]{1,2}/ - -octEscape ::= /\\0?[0-7]{1,3}/ - -charEscape ::= /\\[abfnrtv\\\?'"]/ - diff --git a/lib/protobuf/compiler/proto_parser.rb b/lib/protobuf/compiler/proto_parser.rb deleted file mode 100644 index 62cb4f47..00000000 --- a/lib/protobuf/compiler/proto_parser.rb +++ /dev/null @@ -1,1425 +0,0 @@ -# -# DO NOT MODIFY!!!! -# This file is automatically generated by racc 1.4.5 -# from racc grammer file "lib/protobuf/compiler/proto.y". -# -# -# lib/protobuf/compiler/proto_parser.rb: generated by racc (runtime embedded) -# -###### racc/parser.rb begin -unless $".index 'racc/parser.rb' -$".push 'racc/parser.rb' - -self.class.module_eval <<'..end racc/parser.rb modeval..id24fd9e97a6', 'racc/parser.rb', 1 -# -# $Id: parser.rb,v 1.7 2005/11/20 17:31:32 aamine Exp $ -# -# Copyright (c) 1999-2005 Minero Aoki -# -# This program is free software. -# You can distribute/modify this program under the same terms of ruby. -# -# As a special exception, when this code is copied by Racc -# into a Racc output file, you may use that output file -# without restriction. -# - -unless defined?(NotImplementedError) - NotImplementedError = NotImplementError -end - -module Racc - class ParseError < StandardError; end -end -unless defined?(::ParseError) - ParseError = Racc::ParseError -end - -module Racc - - unless defined?(Racc_No_Extentions) - Racc_No_Extentions = false - end - - class Parser - - Racc_Runtime_Version = '1.4.5' - Racc_Runtime_Revision = '$Revision: 1.7 $'.split[1] - - Racc_Runtime_Core_Version_R = '1.4.5' - Racc_Runtime_Core_Revision_R = '$Revision: 1.7 $'.split[1] - begin - require 'racc/cparse' - # Racc_Runtime_Core_Version_C = (defined in extention) - Racc_Runtime_Core_Revision_C = Racc_Runtime_Core_Id_C.split[2] - unless new.respond_to?(:_racc_do_parse_c, true) - raise LoadError, 'old cparse.so' - end - if Racc_No_Extentions - raise LoadError, 'selecting ruby version of racc runtime core' - end - - Racc_Main_Parsing_Routine = :_racc_do_parse_c - Racc_YY_Parse_Method = :_racc_yyparse_c - Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_C - Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_C - Racc_Runtime_Type = 'c' - rescue LoadError - $stderr.puts $! - Racc_Main_Parsing_Routine = :_racc_do_parse_rb - Racc_YY_Parse_Method = :_racc_yyparse_rb - Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_R - Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R - Racc_Runtime_Type = 'ruby' - end - - def Parser.racc_runtime_type - Racc_Runtime_Type - end - - private - - def _racc_setup - @yydebug = false unless self.class::Racc_debug_parser - @yydebug = false unless defined?(@yydebug) - if @yydebug - @racc_debug_out = $stderr unless defined?(@racc_debug_out) - @racc_debug_out ||= $stderr - end - arg = self.class::Racc_arg - arg[13] = true if arg.size < 14 - arg - end - - def _racc_init_sysvars - @racc_state = [0] - @racc_tstack = [] - @racc_vstack = [] - - @racc_t = nil - @racc_val = nil - - @racc_read_next = true - - @racc_user_yyerror = false - @racc_error_status = 0 - end - - ### - ### do_parse - ### - - def do_parse - __send__(Racc_Main_Parsing_Routine, _racc_setup(), false) - end - - def next_token - raise NotImplementedError, "#{self.class}\#next_token is not defined" - end - - def _racc_do_parse_rb(arg, in_debug) - action_table, action_check, action_default, action_pointer, - goto_table, goto_check, goto_default, goto_pointer, - nt_base, reduce_table, token_table, shift_n, - reduce_n, use_result, * = arg - - _racc_init_sysvars - tok = act = i = nil - nerr = 0 - - catch(:racc_end_parse) { - while true - if i = action_pointer[@racc_state[-1]] - if @racc_read_next - if @racc_t != 0 # not EOF - tok, @racc_val = next_token() - unless tok # EOF - @racc_t = 0 - else - @racc_t = (token_table[tok] or 1) # error token - end - racc_read_token(@racc_t, tok, @racc_val) if @yydebug - @racc_read_next = false - end - end - i += @racc_t - unless i >= 0 and - act = action_table[i] and - action_check[i] == @racc_state[-1] - act = action_default[@racc_state[-1]] - end - else - act = action_default[@racc_state[-1]] - end - while act = _racc_evalact(act, arg) - ; - end - end - } - end - - ### - ### yyparse - ### - - def yyparse(recv, mid) - __send__(Racc_YY_Parse_Method, recv, mid, _racc_setup(), true) - end - - def _racc_yyparse_rb(recv, mid, arg, c_debug) - action_table, action_check, action_default, action_pointer, - goto_table, goto_check, goto_default, goto_pointer, - nt_base, reduce_table, token_table, shift_n, - reduce_n, use_result, * = arg - - _racc_init_sysvars - tok = nil - act = nil - i = nil - nerr = 0 - - catch(:racc_end_parse) { - until i = action_pointer[@racc_state[-1]] - while act = _racc_evalact(action_default[@racc_state[-1]], arg) - ; - end - end - recv.__send__(mid) do |tok, val| - unless tok - @racc_t = 0 - else - @racc_t = (token_table[tok] or 1) # error token - end - @racc_val = val - @racc_read_next = false - - i += @racc_t - unless i >= 0 and - act = action_table[i] and - action_check[i] == @racc_state[-1] - act = action_default[@racc_state[-1]] - end - while act = _racc_evalact(act, arg) - ; - end - - while not (i = action_pointer[@racc_state[-1]]) or - not @racc_read_next or - @racc_t == 0 # $ - unless i and i += @racc_t and - i >= 0 and - act = action_table[i] and - action_check[i] == @racc_state[-1] - act = action_default[@racc_state[-1]] - end - while act = _racc_evalact(act, arg) - ; - end - end - end - } - end - - ### - ### common - ### - - def _racc_evalact(act, arg) - action_table, action_check, action_default, action_pointer, - goto_table, goto_check, goto_default, goto_pointer, - nt_base, reduce_table, token_table, shift_n, - reduce_n, use_result, * = arg - nerr = 0 # tmp - - if act > 0 and act < shift_n - # - # shift - # - if @racc_error_status > 0 - @racc_error_status -= 1 unless @racc_t == 1 # error token - end - @racc_vstack.push @racc_val - @racc_state.push act - @racc_read_next = true - if @yydebug - @racc_tstack.push @racc_t - racc_shift @racc_t, @racc_tstack, @racc_vstack - end - - elsif act < 0 and act > -reduce_n - # - # reduce - # - code = catch(:racc_jump) { - @racc_state.push _racc_do_reduce(arg, act) - false - } - if code - case code - when 1 then # yyerror - @racc_user_yyerror = true # user_yyerror - return -reduce_n - when 2 then # yyaccept - return shift_n - else - raise '[Racc Bug] unknown jump code' - end - end - - elsif act == shift_n - # - # accept - # - racc_accept if @yydebug - throw :racc_end_parse, @racc_vstack[0] - - elsif act == -reduce_n - # - # error - # - case @racc_error_status - when 0 then - unless arg[21] # user_yyerror - nerr += 1 - on_error @racc_t, @racc_val, @racc_vstack - end - when 3 then - if @racc_t == 0 # is $ - throw :racc_end_parse, nil - end - @racc_read_next = true - end - @racc_user_yyerror = false - @racc_error_status = 3 - while true - if i = action_pointer[@racc_state[-1]] - i += 1 # error token - if i >= 0 and - (act = action_table[i]) and - action_check[i] == @racc_state[-1] - break - end - end - throw :racc_end_parse, nil if @racc_state.size <= 1 - @racc_state.pop - @racc_vstack.pop - if @yydebug - @racc_tstack.pop - racc_e_pop @racc_state, @racc_tstack, @racc_vstack - end - end - return act - - else - raise "[Racc Bug] unknown action #{act.inspect}" - end - - racc_next_state(@racc_state[-1], @racc_state) if @yydebug - - nil - end - - def _racc_do_reduce(arg, act) - action_table, action_check, action_default, action_pointer, - goto_table, goto_check, goto_default, goto_pointer, - nt_base, reduce_table, token_table, shift_n, - reduce_n, use_result, * = arg - state = @racc_state - vstack = @racc_vstack - tstack = @racc_tstack - - i = act * -3 - len = reduce_table[i] - reduce_to = reduce_table[i+1] - method_id = reduce_table[i+2] - void_array = [] - - tmp_t = tstack[-len, len] if @yydebug - tmp_v = vstack[-len, len] - tstack[-len, len] = void_array if @yydebug - vstack[-len, len] = void_array - state[-len, len] = void_array - - # tstack must be updated AFTER method call - if use_result - vstack.push __send__(method_id, tmp_v, vstack, tmp_v[0]) - else - vstack.push __send__(method_id, tmp_v, vstack) - end - tstack.push reduce_to - - racc_reduce(tmp_t, reduce_to, tstack, vstack) if @yydebug - - k1 = reduce_to - nt_base - if i = goto_pointer[k1] - i += state[-1] - if i >= 0 and (curstate = goto_table[i]) and goto_check[i] == k1 - return curstate - end - end - goto_default[k1] - end - - def on_error(t, val, vstack) - raise ParseError, sprintf("\nparse error on value %s (%s)", - val.inspect, token_to_str(t) || '?') - end - - def yyerror - throw :racc_jump, 1 - end - - def yyaccept - throw :racc_jump, 2 - end - - def yyerrok - @racc_error_status = 0 - end - - # - # for debugging output - # - - def racc_read_token(t, tok, val) - @racc_debug_out.print 'read ' - @racc_debug_out.print tok.inspect, '(', racc_token2str(t), ') ' - @racc_debug_out.puts val.inspect - @racc_debug_out.puts - end - - def racc_shift(tok, tstack, vstack) - @racc_debug_out.puts "shift #{racc_token2str tok}" - racc_print_stacks tstack, vstack - @racc_debug_out.puts - end - - def racc_reduce(toks, sim, tstack, vstack) - out = @racc_debug_out - out.print 'reduce ' - if toks.empty? - out.print ' ' - else - toks.each {|t| out.print ' ', racc_token2str(t) } - end - out.puts " --> #{racc_token2str(sim)}" - - racc_print_stacks tstack, vstack - @racc_debug_out.puts - end - - def racc_accept - @racc_debug_out.puts 'accept' - @racc_debug_out.puts - end - - def racc_e_pop(state, tstack, vstack) - @racc_debug_out.puts 'error recovering mode: pop token' - racc_print_states state - racc_print_stacks tstack, vstack - @racc_debug_out.puts - end - - def racc_next_state(curstate, state) - @racc_debug_out.puts "goto #{curstate}" - racc_print_states state - @racc_debug_out.puts - end - - def racc_print_stacks(t, v) - out = @racc_debug_out - out.print ' [' - t.each_index do |i| - out.print ' (', racc_token2str(t[i]), ' ', v[i].inspect, ')' - end - out.puts ' ]' - end - - def racc_print_states(s) - out = @racc_debug_out - out.print ' [' - s.each {|st| out.print ' ', st } - out.puts ' ]' - end - - def racc_token2str(tok) - self.class::Racc_token_to_s_table[tok] or - raise "[Racc Bug] can't convert token #{tok} to string" - end - - def token_to_str(t) - self.class::Racc_token_to_s_table[t] - end - - end - -end -..end racc/parser.rb modeval..id24fd9e97a6 -end -###### racc/parser.rb end - - -module Protobuf - - class ProtoParser < Racc::Parser - -module_eval <<'..end lib/protobuf/compiler/proto.y modeval..id110d2bf917', 'lib/protobuf/compiler/proto.y', 158 - - require 'strscan' - - def parse(f) - @scanner = StringScanner.new(f.read) - yyparse(self, :scan) - end - - def scan_debug - scan do |token, value| - p [token, value] - yield [token, value] - end - end - - def scan - until @scanner.eos? - case - when match(/\s+/, /\/\/.*/) then - # skip - when match(/\/\*/) then - # C-like comment - raise 'EOF inside block comment' until @scanner.scan_until(/\*\//) - when match(/(?:required|optional|repeated|import|package|option|message|extend|enum|service|rpc|returns|group|default|extensions|to|max|double|float|int32|int64|uint32|uint64|sint32|sint64|fixed32|fixed64|sfixed32|sfixed64|bool|string|bytes)\b/) then - yield [@token, @token.to_sym] - when match(/[+-]?\d*\.\d+([Ee][\+-]?\d+)?/) then - yield [:FLOAT_LITERAL, @token.to_f] - when match(/[+-]?[1-9]\d*(?!\.)/, /0(?![.xX0-9])/) then - yield [:DEC_INTEGER, @token.to_i] - when match(/0[xX]([A-Fa-f0-9])+/) then - yield [:HEX_INTEGER, @token.to_i(0)] - when match(/0[0-7]+/) then - yield [:OCT_INTEGER, @token.to_i(0)] - when match(/(true|false)\b/) then - yield [:BOOLEAN_LITERAL, @token == 'true'] - when match(/"(?:[^"\\]+|\\.)*"/, /'(?:[^'\\]+|\\.)*'/) then - yield [:STRING_LITERAL, eval(@token)] - when match(/[a-zA-Z_]\w*/) then - yield [:IDENT, @token.to_sym] - when match(/[A-Z]\w*/) then - yield [:CAMEL_IDENT, @token.to_sym] - when match(/./) then - yield [@token, @token] - else - raise "parse error around #{@scanner.string[@scanner.pos, 32].inspect}" - end - end - yield [false, nil] - end - - def match(*regular_expressions) - regular_expressions.each do |re| - if @scanner.scan(re) - @token = @scanner[0] - return true - end - end - false - end -..end lib/protobuf/compiler/proto.y modeval..id110d2bf917 - -##### racc 1.4.5 generates ### - -racc_reduce_table = [ - 0, 0, :racc_error, - 1, 53, :_reduce_1, - 2, 53, :_reduce_2, - 1, 54, :_reduce_none, - 1, 54, :_reduce_none, - 1, 54, :_reduce_none, - 1, 54, :_reduce_none, - 1, 54, :_reduce_none, - 1, 54, :_reduce_none, - 1, 54, :_reduce_none, - 1, 54, :_reduce_10, - 3, 58, :_reduce_11, - 4, 59, :_reduce_12, - 0, 62, :_reduce_13, - 3, 62, :_reduce_14, - 3, 60, :_reduce_15, - 4, 63, :_reduce_16, - 3, 55, :_reduce_17, - 5, 56, :_reduce_18, - 0, 67, :_reduce_19, - 2, 67, :_reduce_20, - 1, 68, :_reduce_none, - 1, 68, :_reduce_none, - 1, 68, :_reduce_23, - 5, 57, :_reduce_24, - 0, 71, :_reduce_25, - 2, 71, :_reduce_26, - 1, 72, :_reduce_none, - 1, 72, :_reduce_none, - 1, 72, :_reduce_29, - 4, 73, :_reduce_30, - 5, 61, :_reduce_31, - 0, 75, :_reduce_32, - 2, 75, :_reduce_33, - 1, 76, :_reduce_none, - 1, 76, :_reduce_none, - 1, 76, :_reduce_36, - 10, 77, :_reduce_37, - 0, 78, :_reduce_none, - 1, 78, :_reduce_none, - 3, 65, :_reduce_40, - 0, 79, :_reduce_41, - 2, 79, :_reduce_42, - 1, 80, :_reduce_none, - 1, 80, :_reduce_none, - 1, 80, :_reduce_none, - 1, 80, :_reduce_none, - 1, 80, :_reduce_none, - 1, 80, :_reduce_none, - 1, 80, :_reduce_none, - 1, 80, :_reduce_50, - 6, 70, :_reduce_51, - 6, 69, :_reduce_52, - 9, 69, :_reduce_53, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 84, :_reduce_none, - 1, 85, :_reduce_87, - 3, 85, :_reduce_88, - 1, 86, :_reduce_none, - 3, 86, :_reduce_90, - 4, 81, :_reduce_91, - 0, 88, :_reduce_92, - 2, 88, :_reduce_93, - 1, 87, :_reduce_94, - 3, 87, :_reduce_95, - 3, 87, :_reduce_96, - 1, 82, :_reduce_none, - 1, 82, :_reduce_none, - 1, 82, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 1, 83, :_reduce_none, - 2, 66, :_reduce_116, - 3, 66, :_reduce_117, - 1, 64, :_reduce_none, - 1, 64, :_reduce_none, - 1, 64, :_reduce_none, - 1, 64, :_reduce_none, - 1, 64, :_reduce_none, - 1, 74, :_reduce_none, - 1, 74, :_reduce_none, - 1, 74, :_reduce_none ] - -racc_reduce_n = 126 - -racc_shift_n = 184 - -racc_action_table = [ - 74, 51, 77, 19, 20, 74, 25, 77, 67, 60, - 32, 47, 53, 63, 14, 14, 43, 107, 39, 68, - 61, 38, 69, 50, 54, 56, 110, 170, 106, 109, - 94, 96, 97, 98, 99, 100, 101, 103, 104, 105, - 108, 93, 95, 72, 73, 75, 76, 78, 72, 73, - 75, 76, 78, 123, 111, 131, 134, 43, 141, 48, - 147, 116, 19, 20, 122, 126, 130, 19, 20, 140, - 144, 75, 76, 78, 120, 124, 127, 129, 133, 136, - 139, 143, 146, 115, 118, 119, 121, 125, 128, 132, - 135, 138, 142, 145, 114, 117, 83, 167, 176, 75, - 76, 78, 14, 25, 16, 1, 159, 85, 6, 75, - 76, 78, 75, 76, 78, 19, 20, 166, 50, 54, - 56, 177, 91, 35, 170, 75, 76, 78, 27, 34, - 4, 7, 148, 11, 33, 150, 14, 151, 16, 1, - 153, 154, 6, 9, 4, 7, 155, 11, 156, 43, - 14, 40, 16, 1, 161, 30, 6, 9, 75, 76, - 78, 29, 25, 165, 24, 40, 169, 23, 174, 175, - 22, 43, 21, 180, 59, 182, 183 ] - -racc_action_check = [ - 48, 42, 48, 58, 58, 175, 177, 175, 46, 45, - 20, 36, 42, 45, 46, 45, 36, 58, 27, 46, - 45, 26, 46, 42, 42, 42, 63, 177, 58, 58, - 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, - 58, 58, 58, 48, 48, 48, 48, 48, 175, 175, - 175, 175, 175, 102, 69, 102, 102, 37, 102, 37, - 102, 102, 150, 150, 102, 102, 102, 1, 1, 102, - 102, 91, 91, 91, 102, 102, 102, 102, 102, 102, - 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, - 102, 102, 102, 102, 102, 102, 49, 163, 172, 110, - 110, 110, 49, 166, 49, 49, 151, 49, 49, 154, - 154, 154, 153, 153, 153, 174, 174, 163, 49, 49, - 49, 172, 49, 23, 166, 151, 151, 151, 15, 22, - 15, 15, 107, 15, 21, 111, 15, 112, 15, 15, - 113, 137, 15, 15, 0, 0, 148, 0, 149, 44, - 0, 29, 0, 0, 152, 18, 0, 0, 155, 155, - 155, 16, 14, 158, 11, 164, 165, 9, 169, 170, - 7, 31, 6, 176, 43, 178, 182 ] - -racc_action_pointer = [ - 142, 61, nil, nil, nil, nil, 166, 166, nil, 161, - nil, 158, nil, nil, 156, 128, 155, nil, 143, nil, - 4, 122, 127, 111, nil, nil, 19, 18, nil, 139, - nil, 164, nil, nil, nil, nil, 9, 50, nil, nil, - nil, nil, -1, 168, 142, 7, 6, nil, -4, 94, - nil, nil, nil, nil, nil, nil, nil, nil, -3, nil, - nil, nil, nil, 17, nil, nil, nil, nil, nil, 48, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, 22, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, 50, nil, nil, nil, nil, 111, nil, nil, - 50, 118, 108, 94, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 132, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 137, 146, - 56, 76, 152, 63, 60, 109, nil, nil, 145, nil, - nil, nil, nil, 95, 153, 147, 97, nil, nil, 151, - 160, nil, 75, nil, 109, 1, 171, 0, 157, nil, - nil, nil, 174, nil ] - -racc_action_default = [ - -126, -126, -3, -4, -10, -5, -126, -126, -6, -126, - -7, -126, -8, -9, -126, -126, -126, -1, -126, -13, - -126, -126, -126, -126, -13, -13, -126, -126, -2, -126, - -19, -116, -13, -25, -11, -32, -126, -126, -15, 184, - -41, -17, -126, -126, -117, -126, -126, -12, -126, -126, - -97, -23, -20, -18, -98, -21, -99, -22, -126, -14, - -29, -24, -27, -126, -26, -28, -35, -36, -31, -126, - -34, -33, -120, -122, -121, -123, -124, -118, -125, -119, - -16, -45, -46, -50, -44, -40, -43, -42, -48, -47, - -49, -126, -115, -113, -102, -114, -103, -104, -105, -106, - -107, -108, -126, -109, -110, -111, -100, -126, -112, -101, - -126, -126, -94, -92, -85, -74, -62, -86, -75, -76, - -55, -77, -63, -58, -56, -78, -64, -57, -79, -68, - -65, -59, -80, -69, -54, -81, -70, -126, -82, -71, - -66, -60, -83, -72, -67, -84, -73, -61, -126, -126, - -38, -126, -126, -126, -126, -126, -30, -39, -126, -96, - -95, -91, -93, -126, -126, -126, -126, -52, -51, -126, - -126, -89, -126, -87, -38, -126, -126, -126, -126, -90, - -53, -88, -126, -37 ] - -racc_goto_table = [ - 41, 80, 112, 18, 158, 113, 31, 17, 57, 173, - 55, 36, 37, 64, 42, 88, 26, 86, 45, 44, - 181, 149, 28, 62, 70, 52, 65, 90, 178, 84, - 46, 71, 66, 82, 49, 87, 89, 102, 137, 172, - 81, 15, 152, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 92, nil, 160, nil, 112, 163, 164, 162, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 179, nil, - nil, nil, nil, nil, nil, 168 ] - -racc_goto_check = [ - 13, 12, 22, 14, 26, 35, 10, 2, 18, 34, - 17, 10, 10, 20, 15, 18, 11, 17, 19, 10, - 34, 22, 2, 8, 8, 16, 21, 8, 26, 5, - 23, 24, 25, 4, 27, 28, 29, 31, 32, 33, - 3, 1, 36, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 14, nil, 22, nil, 22, 22, 22, 35, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 12, nil, - nil, nil, nil, nil, nil, 13 ] - -racc_goto_pointer = [ - nil, 41, 7, -9, -16, -20, nil, nil, -22, nil, - -13, 2, -47, -29, 2, -16, -17, -32, -34, -15, - -32, -19, -89, -5, -15, -14, -146, -6, -14, -13, - nil, -21, -64, -127, -157, -86, -71 ] - -racc_goto_default = [ - nil, nil, nil, 2, 3, 5, 8, 10, 12, 13, - nil, 171, nil, nil, 157, nil, nil, nil, nil, nil, - nil, nil, 79, nil, nil, nil, nil, nil, nil, nil, - 58, nil, nil, nil, nil, nil, nil ] - -racc_token_table = { - false => 0, - Object.new => 1, - ";" => 2, - "import" => 3, - :STRING_LITERAL => 4, - "package" => 5, - :IDENT => 6, - "." => 7, - "option" => 8, - "=" => 9, - "message" => 10, - "extend" => 11, - "{" => 12, - "}" => 13, - "enum" => 14, - "service" => 15, - "rpc" => 16, - "(" => 17, - ")" => 18, - "returns" => 19, - "group" => 20, - :CAMEL_IDENT => 21, - "[" => 22, - "]" => 23, - "required" => 24, - "optional" => 25, - "repeated" => 26, - "default" => 27, - "extensions" => 28, - "to" => 29, - "max" => 30, - "double" => 31, - "float" => 32, - "int32" => 33, - "int64" => 34, - "uint32" => 35, - "uint64" => 36, - "sint32" => 37, - "sint64" => 38, - "fixed32" => 39, - "fixed64" => 40, - "sfixed32" => 41, - "sfixed64" => 42, - "bool" => 43, - "string" => 44, - "bytes" => 45, - "," => 46, - :FLOAT_LITERAL => 47, - :BOOLEAN_LITERAL => 48, - :DEC_INTEGER => 49, - :HEX_INTEGER => 50, - :OCT_INTEGER => 51 } - -racc_use_result_var = true - -racc_nt_base = 52 - -Racc_arg = [ - racc_action_table, - racc_action_check, - racc_action_default, - racc_action_pointer, - racc_goto_table, - racc_goto_check, - racc_goto_default, - racc_goto_pointer, - racc_nt_base, - racc_reduce_table, - racc_token_table, - racc_shift_n, - racc_reduce_n, - racc_use_result_var ] - -Racc_token_to_s_table = [ -'$end', -'error', -'";"', -'"import"', -'STRING_LITERAL', -'"package"', -'IDENT', -'"."', -'"option"', -'"="', -'"message"', -'"extend"', -'"{"', -'"}"', -'"enum"', -'"service"', -'"rpc"', -'"("', -'")"', -'"returns"', -'"group"', -'CAMEL_IDENT', -'"["', -'"]"', -'"required"', -'"optional"', -'"repeated"', -'"default"', -'"extensions"', -'"to"', -'"max"', -'"double"', -'"float"', -'"int32"', -'"int64"', -'"uint32"', -'"uint64"', -'"sint32"', -'"sint64"', -'"fixed32"', -'"fixed64"', -'"sfixed32"', -'"sfixed64"', -'"bool"', -'"string"', -'"bytes"', -'","', -'FLOAT_LITERAL', -'BOOLEAN_LITERAL', -'DEC_INTEGER', -'HEX_INTEGER', -'OCT_INTEGER', -'$start', -'proto', -'proto_item', -'message', -'extend', -'enum', -'import', -'package', -'option', -'service', -'dot_ident_list', -'option_body', -'constant', -'message_body', -'user_type', -'extend_body_list', -'extend_body', -'field', -'group', -'enum_body_list', -'enum_body', -'enum_field', -'integer_literal', -'service_body_list', -'service_body', -'rpc', -'rpc_arg', -'message_body_body_list', -'message_body_body', -'extensions', -'label', -'type', -'field_name', -'field_option_list', -'field_option', -'extension', -'comma_extension_list'] - -Racc_debug_parser = false - -##### racc system variables end ##### - - # reduce 0 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 3 - def _reduce_1( val, _values, result ) - result = Protobuf::Node::ProtoNode.new(val) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 5 - def _reduce_2( val, _values, result ) - result.children << val[1] if val[1] - result - end -.,., - - # reduce 3 omitted - - # reduce 4 omitted - - # reduce 5 omitted - - # reduce 6 omitted - - # reduce 7 omitted - - # reduce 8 omitted - - # reduce 9 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 14 - def _reduce_10( val, _values, result ) - result = nil - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 17 - def _reduce_11( val, _values, result ) - result = Protobuf::Node::ImportNode.new(val[1]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 20 - def _reduce_12( val, _values, result ) - result = Protobuf::Node::PackageNode.new(val[2].unshift(val[1])) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 23 - def _reduce_13( val, _values, result ) - result = [] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 25 - def _reduce_14( val, _values, result ) - result << val[2] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 28 - def _reduce_15( val, _values, result ) - result = Protobuf::Node::OptionNode.new(*val[1]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 31 - def _reduce_16( val, _values, result ) - result = [val[1].unshift(val[0]), val[3]] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 34 - def _reduce_17( val, _values, result ) - result = Protobuf::Node::MessageNode.new(val[1], val[2]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 37 - def _reduce_18( val, _values, result ) - result = Protobuf::Node::ExtendNode.new(val[1], val[3]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 40 - def _reduce_19( val, _values, result ) - result = [] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 42 - def _reduce_20( val, _values, result ) - result << val[1] if val[1] - result - end -.,., - - # reduce 21 omitted - - # reduce 22 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 46 - def _reduce_23( val, _values, result ) - result = nil - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 49 - def _reduce_24( val, _values, result ) - result = Protobuf::Node::EnumNode.new(val[1], val[3]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 52 - def _reduce_25( val, _values, result ) - result = [] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 54 - def _reduce_26( val, _values, result ) - result << val[1] if val[1] - result - end -.,., - - # reduce 27 omitted - - # reduce 28 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 58 - def _reduce_29( val, _values, result ) - result = nil - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 61 - def _reduce_30( val, _values, result ) - result = Protobuf::Node::EnumFieldNode.new(val[0], val[2]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 64 - def _reduce_31( val, _values, result ) - result = Protobuf::Node::ServiceNode.new(val[1], val[3]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 67 - def _reduce_32( val, _values, result ) - result = [] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 69 - def _reduce_33( val, _values, result ) - result << val[1] if val[1] - result - end -.,., - - # reduce 34 omitted - - # reduce 35 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 73 - def _reduce_36( val, _values, result ) - result = nil - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 76 - def _reduce_37( val, _values, result ) - result = Protobuf::Node::RpcNode.new(val[1], val[3], val[7]) - result - end -.,., - - # reduce 38 omitted - - # reduce 39 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 82 - def _reduce_40( val, _values, result ) - result = val[1] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 85 - def _reduce_41( val, _values, result ) - result = [] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 87 - def _reduce_42( val, _values, result ) - result << val[1] if val[1] - result - end -.,., - - # reduce 43 omitted - - # reduce 44 omitted - - # reduce 45 omitted - - # reduce 46 omitted - - # reduce 47 omitted - - # reduce 48 omitted - - # reduce 49 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 96 - def _reduce_50( val, _values, result ) - result = nil - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 99 - def _reduce_51( val, _values, result ) - result = Protobuf::Node::GroupNode.new(val[0], val[2], val[4], val[5]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 102 - def _reduce_52( val, _values, result ) - result = Protobuf::Node::FieldNode.new(val[0], val[1], val[2], val[4]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 104 - def _reduce_53( val, _values, result ) - result = Protobuf::Node::FieldNode.new(val[0], val[1], val[2], val[4], val[6]) - result - end -.,., - - # reduce 54 omitted - - # reduce 55 omitted - - # reduce 56 omitted - - # reduce 57 omitted - - # reduce 58 omitted - - # reduce 59 omitted - - # reduce 60 omitted - - # reduce 61 omitted - - # reduce 62 omitted - - # reduce 63 omitted - - # reduce 64 omitted - - # reduce 65 omitted - - # reduce 66 omitted - - # reduce 67 omitted - - # reduce 68 omitted - - # reduce 69 omitted - - # reduce 70 omitted - - # reduce 71 omitted - - # reduce 72 omitted - - # reduce 73 omitted - - # reduce 74 omitted - - # reduce 75 omitted - - # reduce 76 omitted - - # reduce 77 omitted - - # reduce 78 omitted - - # reduce 79 omitted - - # reduce 80 omitted - - # reduce 81 omitted - - # reduce 82 omitted - - # reduce 83 omitted - - # reduce 84 omitted - - # reduce 85 omitted - - # reduce 86 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 109 - def _reduce_87( val, _values, result ) - result = val - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 111 - def _reduce_88( val, _values, result ) - result << val[2] - result - end -.,., - - # reduce 89 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 115 - def _reduce_90( val, _values, result ) - result = [:default, val[2]] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 118 - def _reduce_91( val, _values, result ) - result = Protobuf::Node::ExtensionsNode.new(val[2].unshift(val[1])) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 121 - def _reduce_92( val, _values, result ) - result = [] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 123 - def _reduce_93( val, _values, result ) - result << val[1] - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 126 - def _reduce_94( val, _values, result ) - result = Protobuf::Node::ExtensionRangeNode.new(val[0]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 128 - def _reduce_95( val, _values, result ) - result = Protobuf::Node::ExtensionRangeNode.new(val[0], val[2]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 130 - def _reduce_96( val, _values, result ) - result = Protobuf::Node::ExtensionRangeNode.new(val[0], :max) - result - end -.,., - - # reduce 97 omitted - - # reduce 98 omitted - - # reduce 99 omitted - - # reduce 100 omitted - - # reduce 101 omitted - - # reduce 102 omitted - - # reduce 103 omitted - - # reduce 104 omitted - - # reduce 105 omitted - - # reduce 106 omitted - - # reduce 107 omitted - - # reduce 108 omitted - - # reduce 109 omitted - - # reduce 110 omitted - - # reduce 111 omitted - - # reduce 112 omitted - - # reduce 113 omitted - - # reduce 114 omitted - - # reduce 115 omitted - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 141 - def _reduce_116( val, _values, result ) - result = val[1].unshift(val[0]) - result - end -.,., - -module_eval <<'.,.,', 'lib/protobuf/compiler/proto.y', 143 - def _reduce_117( val, _values, result ) - result = val[1].unshift(val[0]) - result - end -.,., - - # reduce 118 omitted - - # reduce 119 omitted - - # reduce 120 omitted - - # reduce 121 omitted - - # reduce 122 omitted - - # reduce 123 omitted - - # reduce 124 omitted - - # reduce 125 omitted - - def _reduce_none( val, _values, result ) - result - end - - end # class ProtoParser - -end # module Protobuf diff --git a/lib/protobuf/compiler/template/rpc_service_implementation.erb b/lib/protobuf/compiler/template/rpc_service_implementation.erb deleted file mode 100644 index 4c295e57..00000000 --- a/lib/protobuf/compiler/template/rpc_service_implementation.erb +++ /dev/null @@ -1,42 +0,0 @@ -<%- -module_array = module_name.split('::') -class_indent = ' '*(module_array.size) --%> -require 'protobuf/rpc/service' -require '<%= required_file %>' - -## !! DO NOT EDIT THIS FILE !! -## -## To implement this service as defined by the protobuf, simply -## reopen <%= module_name %>::<%= service_name %> and implement each service method: -## -<%- module_array.each_with_index do |m, i| -%> -## <%= "#{"\t"*i}module #{m}" %> -<%- end -%> -## <%= class_indent %>class <%= service_name %> -## <%= class_indent %> -<%- rpcs.each do |name, request, response| -%> -## <%= class_indent %> # request -> <%= module_name %>::<%= request %> -## <%= class_indent %> # response -> <%= module_name %>::<%= response %> -## <%= class_indent %> def <%= Util.underscore(name) %> -## <%= class_indent %> # TODO: implement <%= Util.underscore(name) %> -## <%= class_indent %> end -## <%= class_indent %> -<%- end -%> -## <%= class_indent %>end -<%- (module_array.size-1).downto(0) do |i| -%> -## <%= "#{"\t"*i}end" %> -<%- end -%> -## - -<%- module_array.each_with_index do |m, i| -%> -<%= "#{"\t"*i}module #{m}" %> -<%- end -%> -<%= class_indent %>class <%= service_name %> < Protobuf::Rpc::Service -<%- rpcs.each do |name, request, response| -%> -<%= class_indent %> rpc :<%= Util.underscore(name) %>, <%= request %>, <%= response %> -<%- end -%> -<%= class_indent %>end -<%- (module_array.size-1).downto(0) do |i| -%> -<%= "#{"\t"*i}end" %> -<%- end -%> diff --git a/lib/protobuf/compiler/visitors.rb b/lib/protobuf/compiler/visitors.rb deleted file mode 100644 index 807d5c39..00000000 --- a/lib/protobuf/compiler/visitors.rb +++ /dev/null @@ -1,282 +0,0 @@ -require 'erb' -require 'fileutils' -require 'protobuf/descriptor/descriptor_proto' - -module Protobuf - module Visitor - class Base - attr_reader :silent - - def create_file_with_backup(filename, contents, executable=false) - if File.exist?(filename) - if File.read(filename) == contents - # do nothing - return - else - backup_filename = "#{filename}.#{Time.now.to_i}" - log_writing("#{backup_filename}", "backingup...") - FileUtils.copy(filename, backup_filename) - end - end - - FileUtils.mkpath(File.dirname(filename)) - File.open(filename, 'w') do |file| - log_writing(filename) - file.write(contents) - end - FileUtils.chmod(0755, filename) if executable - end - - def log_writing(filename, message="wrote") - puts "#{message} #{filename}" unless silent - end - end - - class CreateMessageVisitor < Base - attr_accessor :package, :indent, :context, :attach_proto, :proto_file - - def initialize(proto_file=nil, proto_dir='.', out_dir='.') - @proto_dir, @out_dir = proto_dir, out_dir - @indent = 0 - @context = [] - @attach_proto = false - @proto_file = proto_file - end - - def attach_proto? - @attach_proto - end - - def commented_proto_contents - if proto_file - proto_filepath = if File.exist?(proto_file) - then proto_file - else "#{@proto_dir}/#{proto_file}" - end - File.read(proto_filepath).gsub(/^/, '# ') - end - end - - def write(str) - ruby << "#{' ' * @indent}#{str}" - end - - def increment - @indent += 1 - end - - def decrement - @indent -= 1 - end - - def close_ruby - while 0 < indent - decrement - write('end') - end - end - - def ruby - @ruby ||= [] - end - - def to_s - @ruby.join("\n") - end - - def in_context(klass, &block) - increment - context.push klass - block.call - context.pop - decrement - end - - def visit(node) - node.accept_message_visitor(self) - self - end - - def required_message_from_proto(proto_file) - rb_path = [ - proto_file.sub(/\.proto\z/, '.pb.rb') - ].join('/').gsub(/\/{2,}/, '/') - - unless File.exist?(rb_path) - Compiler.compile(proto_file, @proto_dir, @out_dir) - end - - rb_path.sub(/\.rb$/, '') - end - - def create_files(filename, out_dir, file_create) - $: << File.expand_path(out_dir) - Class.new.class_eval(to_s) # check the message - $:.delete File.expand_path(out_dir) - rescue LoadError - puts "Error creating file #{filename}" - puts $!.message - exit 1 - else - - file = File.basename(filename) - message_module = Util.module_to_path(package.map{|p| p.to_s.capitalize}.join('::')) - filename = "#{out_dir}/#{message_module}/#{file}" - - if file_create - log_writing(filename) - FileUtils.mkpath(File.dirname(filename)) - File.open(filename, 'w') {|file| file.write(to_s) } - else - to_s - end - end - end - - class CreateRpcVisitor < Base - attr_accessor :package, :services, :current_service, :file_contents - - def initialize - @services = {} - @create_file = true - @file_contents = {} - end - - def visit(node) - node.accept_rpc_visitor(self) - self - end - - def add_rpc(name, request, response) - (@services[@current_service] ||= []) << [name, Util.moduleize(request), Util.moduleize(response)] - end - - def create_files(message_file, out_dir, create_file=true) - @create_file = create_file - @services.each do |service_name, rpcs| - underscored_name = Util.underscore(service_name.to_s) - message_module = package.map{|p| p.to_s.capitalize}.join('::') - required_file = [ - Util.module_to_path(message_module), - File.basename(message_file, '.rb') - ].join('/').gsub(/\/{2,}/, '/') - - create_service(message_file, out_dir, underscored_name, message_module, service_name, rpcs, required_file) - end - @file_contents - end - - def create_service(message_file, out_dir, underscored_name, module_name, service_name, rpcs, required_file) - service_filename = "#{out_dir}/#{Util.module_to_path(module_name)}/#{underscored_name}.rb" - service_contents = template_erb('rpc_service_implementation').result(binding) - create_file_with_backup(service_filename, service_contents) if @create_file - @file_contents[service_filename] = service_contents - end - - private - - def template_erb(template) - ERB.new(File.read("#{File.dirname(__FILE__)}/template/#{template}.erb"), nil, '-') - end - end - - class CreateDescriptorVisitor < Base - attr_reader :file_descriptor - attr_accessor :filename - - def initialize(filename=nil) - @context = [] - @filename = filename - end - - def visit(node) - node.accept_descriptor_visitor(self) - self - end - - def in_context(descriptor, &block) - @context.push descriptor - block.call - @context.pop - end - - def current_descriptor - @context.last - end - - def file_descriptor=(descriptor) - @file_descriptor = descriptor - @file_descriptor.name = @filename - end - - def add_option(name, value) - options = - case current_descriptor - when Google::Protobuf::FileDescriptorProto then - Google::Protobuf::FileOptions.new - when Google::Protobuf::DescriptorProto then - Google::Protobuf::MessageOptions.new - when Google::Protobuf::FieldDescriptorProto then - Google::Protobuf::FieldOptions.new - when Google::Protobuf::EnumDescriptorProto then - Google::Protobuf::EnumOptions.new - when Google::Protobuf::EnumValueDescriptorProto then - Google::Protobuf::EnumValueOptions.new - when Google::Protobuf::ServiceDescriptorProto then - Google::Protobuf::ServiceOptions.new - when Google::Protobuf::MethodDescriptorProto then - Google::Protobuf::MethodOptions.new - else - raise ArgumentError, 'Invalid context' - end - #TODO how should options be handled? - #current_descriptor.options << option - end - - def descriptor=(descriptor) - case current_descriptor - when Google::Protobuf::FileDescriptorProto then - current_descriptor.message_type << descriptor - when Google::Protobuf::DescriptorProto then - current_descriptor.nested_type << descriptor - else - raise ArgumentError, 'Invalid context' - end - end - alias message_descriptor= descriptor= - - def enum_descriptor=(descriptor) - current_descriptor.enum_type << descriptor - end - - def enum_value_descriptor=(descriptor) - current_descriptor.value << descriptor - end - - def service_descriptor=(descriptor) - current_descriptor.service << descriptor - end - - def method_descriptor=(descriptor) - current_descriptor.method << descriptor - end - - def field_descriptor=(descriptor) - case current_descriptor - when Google::Protobuf::FileDescriptorProto then - current_descriptor.extension << descriptor - when Google::Protobuf::DescriptorProto then - current_descriptor.field << descriptor - #TODO: how should i distiguish between field and extension - #current_descriptor.extension << descriptor - else - raise ArgumentError, 'Invalid context' - end - end - - def extension_range_descriptor=(descriptor) - current_descriptor.extension_range << descriptor - end - end - end -end diff --git a/lib/protobuf/decoder.rb b/lib/protobuf/decoder.rb new file mode 100644 index 00000000..a77495d7 --- /dev/null +++ b/lib/protobuf/decoder.rb @@ -0,0 +1,28 @@ +module Protobuf + class Decoder + + # Read bytes from +stream+ and pass to +message+ object. + def self.decode_each_field(stream) + until stream.eof? + bits = Varint.decode(stream) + wire_type = bits & 0x07 + tag = bits >> 3 + + bytes = if wire_type == ::Protobuf::WireType::VARINT + Varint.decode(stream) + elsif wire_type == ::Protobuf::WireType::LENGTH_DELIMITED + value_length = Varint.decode(stream) + stream.read(value_length) + elsif wire_type == ::Protobuf::WireType::FIXED64 + stream.read(8) + elsif wire_type == ::Protobuf::WireType::FIXED32 + stream.read(4) + else + fail InvalidWireType, wire_type + end + + yield(tag, bytes) + end + end + end +end diff --git a/lib/protobuf/deprecation.rb b/lib/protobuf/deprecation.rb new file mode 100644 index 00000000..97b56698 --- /dev/null +++ b/lib/protobuf/deprecation.rb @@ -0,0 +1,118 @@ +require 'active_support' +require 'active_support/deprecation' + +module Protobuf + if ::ActiveSupport::Deprecation.is_a?(Class) + class DeprecationBase < ::ActiveSupport::Deprecation + def deprecate_methods(*args) + deprecation_options = { :deprecator => self } + + if args.last.is_a?(Hash) + args.last.merge!(deprecation_options) + else + args.push(deprecation_options) + end + + super + end + + def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil) + # This ensures ActiveSupport::Deprecation doesn't look for the caller, which is very costly. + super(deprecated_method_name, message, caller_backtrace) unless ENV.key?('PB_IGNORE_DEPRECATIONS') + end + end + + class Deprecation < DeprecationBase + def define_deprecated_methods(target_module, method_hash) + target_module.module_eval do + method_hash.each do |old_method, new_method| + alias_method old_method, new_method + end + end + + deprecate_methods(target_module, method_hash) + end + end + + class FieldDeprecation < DeprecationBase + # this is a convenience deprecator for deprecated proto fields + + def deprecate_method(target_module, method_name) + deprecate_methods(target_module, method_name => target_module) + end + + private + + def deprecated_method_warning(method_name, target_module) + "#{target_module.name}##{method_name} field usage is deprecated" + end + end + else + # TODO: remove this clause when Rails < 4 support is no longer needed + deprecator = ::ActiveSupport::Deprecation.clone + deprecator.instance_eval do + def new(deprecation_horizon = nil, *) + self.deprecation_horizon = deprecation_horizon if deprecation_horizon + self + end + end + Deprecation = deprecator.clone + FieldDeprecation = deprecator.clone + + Deprecation.instance_eval do + def define_deprecated_methods(target_module, method_hash) + target_module.module_eval do + method_hash.each do |old_method, new_method| + alias_method old_method, new_method + end + end + + deprecate_methods(target_module, method_hash) + end + end + + FieldDeprecation.instance_eval do + def deprecate_method(target_module, method_name) + deprecate_methods(target_module, method_name => target_module) + end + + private + + def deprecated_method_warning(method_name, target_module) + "#{target_module.name}##{method_name} field usage is deprecated" + end + end + end + + def self.deprecator + @deprecator ||= Deprecation.new('4.0', to_s).tap do |deprecation| + deprecation.silenced = ENV.key?('PB_IGNORE_DEPRECATIONS') + deprecation.behavior = :stderr + end + end + + def self.field_deprecator + @field_deprecator ||= FieldDeprecation.new.tap do |deprecation| + deprecation.silenced = ENV.key?('PB_IGNORE_DEPRECATIONS') + deprecation.behavior = :stderr + end + end + + # Print Deprecation Warnings + # + # Default: true + # + # Simple boolean to define whether we want field deprecation warnings to + # be printed to stderr or not. The rpc_server has an option to set this value + # explicitly, or you can turn this option off by setting + # ENV['PB_IGNORE_DEPRECATIONS'] to a non-empty value. + # + # The rpc_server option will override the ENV setting. + def self.print_deprecation_warnings? + !field_deprecator.silenced + end + + def self.print_deprecation_warnings=(value) + field_deprecator.silenced = !value + end +end diff --git a/lib/protobuf/descriptor/descriptor.proto b/lib/protobuf/descriptor/descriptor.proto deleted file mode 100755 index 13d94780..00000000 --- a/lib/protobuf/descriptor/descriptor.proto +++ /dev/null @@ -1,286 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. -// http://code.google.com/p/protobuf/ -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Author: kenton@google.com (Kenton Varda) -// Based on original Protocol Buffers design by -// Sanjay Ghemawat, Jeff Dean, and others. -// -// The messages in this file describe the definitions found in .proto files. -// A valid .proto file can be translated directly to a FileDescriptorProto -// without any other information (e.g. without reading its imports). - - - -package google.protobuf; -option java_package = "com.google.protobuf"; -option java_outer_classname = "DescriptorProtos"; - -// descriptor.proto must be optimized for speed because reflection-based -// algorithms don't work during bootstrapping. -option optimize_for = SPEED; - -// Describes a complete .proto file. -message FileDescriptorProto { - optional string name = 1; // file name, relative to root of source tree - optional string package = 2; // e.g. "foo", "foo.bar", etc. - - // Names of files imported by this file. - repeated string dependency = 3; - - // All top-level definitions in this file. - repeated DescriptorProto message_type = 4; - repeated EnumDescriptorProto enum_type = 5; - repeated ServiceDescriptorProto service = 6; - repeated FieldDescriptorProto extension = 7; - - optional FileOptions options = 8; -} - -// Describes a message type. -message DescriptorProto { - optional string name = 1; - - repeated FieldDescriptorProto field = 2; - repeated FieldDescriptorProto extension = 6; - - repeated DescriptorProto nested_type = 3; - repeated EnumDescriptorProto enum_type = 4; - - message ExtensionRange { - optional int32 start = 1; - optional int32 end = 2; - } - repeated ExtensionRange extension_range = 5; - - optional MessageOptions options = 7; -} - -// Describes a field within a message. -message FieldDescriptorProto { - enum Type { - // 0 is reserved for errors. - // Order is weird for historical reasons. - TYPE_DOUBLE = 1; - TYPE_FLOAT = 2; - TYPE_INT64 = 3; // Not ZigZag encoded. Negative numbers - // take 10 bytes. Use TYPE_SINT64 if negative - // values are likely. - TYPE_UINT64 = 4; - TYPE_INT32 = 5; // Not ZigZag encoded. Negative numbers - // take 10 bytes. Use TYPE_SINT32 if negative - // values are likely. - TYPE_FIXED64 = 6; - TYPE_FIXED32 = 7; - TYPE_BOOL = 8; - TYPE_STRING = 9; - TYPE_GROUP = 10; // Tag-delimited aggregate. - TYPE_MESSAGE = 11; // Length-delimited aggregate. - - // New in version 2. - TYPE_BYTES = 12; - TYPE_UINT32 = 13; - TYPE_ENUM = 14; - TYPE_SFIXED32 = 15; - TYPE_SFIXED64 = 16; - TYPE_SINT32 = 17; // Uses ZigZag encoding. - TYPE_SINT64 = 18; // Uses ZigZag encoding. - }; - - enum Label { - // 0 is reserved for errors - LABEL_OPTIONAL = 1; - LABEL_REQUIRED = 2; - LABEL_REPEATED = 3; - // TODO(sanjay): Should we add LABEL_MAP? - }; - - optional string name = 1; - optional int32 number = 3; - optional Label label = 4; - - // If type_name is set, this need not be set. If both this and type_name - // are set, this must be either TYPE_ENUM or TYPE_MESSAGE. - optional Type type = 5; - - // For message and enum types, this is the name of the type. If the name - // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping - // rules are used to find the type (i.e. first the nested types within this - // message are searched, then within the parent, on up to the root - // namespace). - optional string type_name = 6; - - // For extensions, this is the name of the type being extended. It is - // resolved in the same manner as type_name. - optional string extendee = 2; - - // For numeric types, contains the original text representation of the value. - // For booleans, "true" or "false". - // For strings, contains the default text contents (not escaped in any way). - // For bytes, contains the C escaped value. All bytes >= 128 are escaped. - // TODO(kenton): Base-64 encode? - optional string default_value = 7; - - optional FieldOptions options = 8; -} - -// Describes an enum type. -message EnumDescriptorProto { - optional string name = 1; - - repeated EnumValueDescriptorProto value = 2; - - optional EnumOptions options = 3; -} - -// Describes a value within an enum. -message EnumValueDescriptorProto { - optional string name = 1; - optional int32 number = 2; - - optional EnumValueOptions options = 3; -} - -// Describes a service. -message ServiceDescriptorProto { - optional string name = 1; - repeated MethodDescriptorProto method = 2; - - optional ServiceOptions options = 3; -} - -// Describes a method of a service. -message MethodDescriptorProto { - optional string name = 1; - - // Input and output type names. These are resolved in the same way as - // FieldDescriptorProto.type_name, but must refer to a message type. - optional string input_type = 2; - optional string output_type = 3; - - optional MethodOptions options = 4; -} - -// =================================================================== -// Options - -// Each of the definitions above may have "options" attached. These are -// just annotations which may cause code to be generated slightly differently -// or may contain hints for code that manipulates protocol messages. - -// TODO(kenton): Allow extensions to options. - -message FileOptions { - - // Sets the Java package where classes generated from this .proto will be - // placed. By default, the proto package is used, but this is often - // inappropriate because proto packages do not normally start with backwards - // domain names. - optional string java_package = 1; - - - // If set, all the classes from the .proto file are wrapped in a single - // outer class with the given name. This applies to both Proto1 - // (equivalent to the old "--one_java_file" option) and Proto2 (where - // a .proto always translates to a single class, but you may want to - // explicitly choose the class name). - optional string java_outer_classname = 8; - - // If set true, then the Java code generator will generate a separate .java - // file for each top-level message, enum, and service defined in the .proto - // file. Thus, these types will *not* be nested inside the outer class - // named by java_outer_classname. However, the outer class will still be - // generated to contain the file's getDescriptor() method as well as any - // top-level extensions defined in the file. - optional bool java_multiple_files = 10 [default=false]; - - // Generated classes can be optimized for speed or code size. - enum OptimizeMode { - SPEED = 1; // Generate complete code for parsing, serialization, etc. - CODE_SIZE = 2; // Use ReflectionOps to implement these methods. - } - optional OptimizeMode optimize_for = 9 [default=CODE_SIZE]; -} - -message MessageOptions { - // Set true to use the old proto1 MessageSet wire format for extensions. - // This is provided for backwards-compatibility with the MessageSet wire - // format. You should not use this for any other reason: It's less - // efficient, has fewer features, and is more complicated. - // - // The message must be defined exactly as follows: - // message Foo { - // option message_set_wire_format = true; - // extensions 4 to max; - // } - // Note that the message cannot have any defined fields; MessageSets only - // have extensions. - // - // All extensions of your type must be singular messages; e.g. they cannot - // be int32s, enums, or repeated messages. - // - // Because this is an option, the above two restrictions are not enforced by - // the protocol compiler. - optional bool message_set_wire_format = 1 [default=false]; -} - -message FieldOptions { - // The ctype option instructs the C++ code generator to use a different - // representation of the field than it normally would. See the specific - // options below. This option is not yet implemented in the open source - // release -- sorry, we'll try to include it in a future version! - optional CType ctype = 1; - enum CType { - CORD = 1; - - STRING_PIECE = 2; - } - - // EXPERIMENTAL. DO NOT USE. - // For "map" fields, the name of the field in the enclosed type that - // is the key for this map. For example, suppose we have: - // message Item { - // required string name = 1; - // required string value = 2; - // } - // message Config { - // repeated Item items = 1 [experimental_map_key="name"]; - // } - // In this situation, the map key for Item will be set to "name". - // TODO: Fully-implement this, then remove the "experimental_" prefix. - optional string experimental_map_key = 9; -} - -message EnumOptions { -} - -message EnumValueOptions { -} - -message ServiceOptions { - - // Note: Field numbers 1 through 32 are reserved for Google's internal RPC - // framework. We apologize for hoarding these numbers to ourselves, but - // we were already using them long before we decided to release Protocol - // Buffers. -} - -message MethodOptions { - - // Note: Field numbers 1 through 32 are reserved for Google's internal RPC - // framework. We apologize for hoarding these numbers to ourselves, but - // we were already using them long before we decided to release Protocol - // Buffers. -} diff --git a/lib/protobuf/descriptor/descriptor.rb b/lib/protobuf/descriptor/descriptor.rb deleted file mode 100644 index 0344b2b6..00000000 --- a/lib/protobuf/descriptor/descriptor.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Protobuf - module Descriptor - class Descriptor - def initialize(message_class) - @message_class = message_class - end - - def proto_type - 'Google::Protobuf::DescriptorProto' - end - - def build(proto, opt={}) - mod = opt[:module] - cls = mod.const_set(proto.name, Class.new(@message_class)) - proto.nested_type.each do |message_proto| - Protobuf::Message.descriptor.build(message_proto, :module => cls) - end - proto.enum_type.each do |enum_proto| - Protobuf::Enum.descriptor.build(enum_proto, :module => cls) - end - proto.field.each do |field_proto| - Protobuf::Field::BaseField.descriptor.build(field_proto, :class => cls) - end - end - - def unbuild(parent_proto) - message_proto = Google::Protobuf::DescriptorProto.new - message_proto.name = @message_class.to_s.split('::').last - @message_class.fields.each do |tag, field| - field.descriptor.unbuild(message_proto) - end - ObjectSpace.each_object(Class) do |cls| - if innerclass?(@message_class, cls) - cls.descriptor.unbuild(message_proto) - end - end - - case parent_proto - when Google::Protobuf::FileDescriptorProto - parent_proto.message_type << message_proto - when Google::Protobuf::DescriptorProto - parent_proto.nested_type << message_proto - else - raise TypeError, parent_proto.class.name - end - end - - private - - def innerclass?(parent, child) - child.name =~ /::/ && child.name.split('::')[0..-2].join('::') == parent.name - end - end - end -end diff --git a/lib/protobuf/descriptor/descriptor_builder.rb b/lib/protobuf/descriptor/descriptor_builder.rb deleted file mode 100644 index 3eea9c58..00000000 --- a/lib/protobuf/descriptor/descriptor_builder.rb +++ /dev/null @@ -1,143 +0,0 @@ -require 'protobuf/descriptor/file_descriptor' - -module Protobuf - module Descriptor - def self.id2type(type_id) - require 'protobuf/descriptor/descriptor_proto' - case type_id - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_DOUBLE then - :double - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_FLOAT then - :float - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_INT64 then - :int64 - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT64 then - :unit64 - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_INT32 then - :int64 - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_FIXED64 then - :fixed64 - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_FIXED32 then - :fixed32 - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_BOOL then - :bool - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_STRING then - :string - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_GROUP then - :group - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_MESSAGE then - :message - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_BYTES then - :bytes - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT32 then - :uint32 - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_ENUM then - :enum - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_SFIXED32 then - :sfixed32 - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_SFIXED64 then - :sfixed64 - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_SINT32 then - :sint32 - when Google::Protobuf::FieldDescriptorProto::Type::TYPE_SINT64 then - :sint64 - else - raise ArgumentError, "Invalid type: #{proto.type}" - end - end - - def self.type2id(type) - require 'protobuf/descriptor/descriptor_proto' - case type - when :double then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_DOUBLE - when :float then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_FLOAT - when :int64 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_INT64 - when :unit64 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT64 - when :int64 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_INT32 - when :fixed64 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_FIXED64 - when :fixed32 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_FIXED32 - when :bool then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_BOOL - when :string then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_STRING - when :group then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_GROUP - when :message then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_MESSAGE - when :bytes then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_BYTES - when :uint32 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT32 - when :enum then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_ENUM - when :sfixed32 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_SFIXED32 - when :sfixed64 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_SFIXED64 - when :sint32 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_SINT32 - when :sint64 then - Google::Protobuf::FieldDescriptorProto::Type::TYPE_SINT64 - else - Google::Protobuf::FieldDescriptorProto::Type::TYPE_MESSAGE - end - end - - def self.id2label(label_id) - require 'protobuf/descriptor/descriptor_proto' - case label_id - when Google::Protobuf::FieldDescriptorProto::Label::LABEL_REQUIRED then - :required - when Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL then - :optional - when Google::Protobuf::FieldDescriptorProto::Label::LABEL_REPEATED then - :repeated - else - raise ArgumentError, "Invalid label: #{proto.label}" - end - end - - def self.label2id(label) - require 'protobuf/descriptor/descriptor_proto' - case label - when :required then - Google::Protobuf::FieldDescriptorProto::Label::LABEL_REQUIRED - when :optional then - Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL - when :repeated then - Google::Protobuf::FieldDescriptorProto::Label::LABEL_REPEATED - else - raise ArgumentError, "Invalid label: #{label}" - end - end - - class DescriptorBuilder - class < false - class OptimizeMode < ::Protobuf::Enum - defined_in __FILE__ - define :SPEED, 1 - define :CODE_SIZE, 2 - end - optional :OptimizeMode, :optimize_for, 9, :default => :CODE_SIZE - end - class MessageOptions < ::Protobuf::Message - defined_in __FILE__ - optional :bool, :message_set_wire_format, 1, :default => false - end - class FieldOptions < ::Protobuf::Message - defined_in __FILE__ - optional :CType, :ctype, 1 - class CType < ::Protobuf::Enum - defined_in __FILE__ - define :CORD, 1 - define :STRING_PIECE, 2 - end - optional :string, :experimental_map_key, 9 - end - class EnumOptions < ::Protobuf::Message - defined_in __FILE__ - end - class EnumValueOptions < ::Protobuf::Message - defined_in __FILE__ - end - class ServiceOptions < ::Protobuf::Message - defined_in __FILE__ - end - class MethodOptions < ::Protobuf::Message - defined_in __FILE__ - end - end -end diff --git a/lib/protobuf/descriptor/enum_descriptor.rb b/lib/protobuf/descriptor/enum_descriptor.rb deleted file mode 100644 index 9e1e0b12..00000000 --- a/lib/protobuf/descriptor/enum_descriptor.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Protobuf - module Descriptor - class EnumDescriptor - def initialize(enum_class) - @enum_class = enum_class - end - - def proto_type - Google::Protobuf::EnumDescriptorProto - end - - def build(proto, opt) - mod = opt[:module] - cls = mod.const_set(proto.name, Class.new(Protobuf::Enum)) - proto.value.each do |value_proto| - cls.class_eval { define value_proto.name, value_proto.number } - end - end - - def unbuild(parent_proto) - enum_proto = Google::Protobuf::EnumDescriptorProto.new - enum_proto.name = @enum_class.name.split('::').last - @enum_class.values.each do |name, value| - enum_value_proto = Google::Protobuf::EnumValueDescriptorProto.new - enum_value_proto.name = name.to_s - enum_value_proto.number = value.value - enum_proto.value << enum_value_proto - end - parent_proto.enum_type << enum_proto - end - end - end -end diff --git a/lib/protobuf/descriptor/field_descriptor.rb b/lib/protobuf/descriptor/field_descriptor.rb deleted file mode 100644 index 051e7d02..00000000 --- a/lib/protobuf/descriptor/field_descriptor.rb +++ /dev/null @@ -1,49 +0,0 @@ -module Protobuf - module Descriptor - class FieldDescriptor - def initialize(field_instance=nil) - @field_instance = field_instance - end - - def proto_type - 'Google::Protobuf::FieldDescriptorProto' - end - - def build(proto, opt={}) - cls = opt[:class] - rule = Protobuf::Descriptor.id2label(proto.label) - type = Protobuf::Descriptor.id2type(proto.type) - type = proto.type_name.to_sym if [:message, :enum].include?(type) - opts = {} - opts[:default] = proto.default_value if proto.default_value - cls.define_field(rule, type, proto.name, proto.number, opts) - end - - def unbuild(parent_proto, extension=false) - field_proto = Google::Protobuf::FieldDescriptorProto.new - field_proto.name = @field_instance.name.to_s - field_proto.number = @field_instance.tag - field_proto.label = Protobuf::Descriptor.label2id(@field_instance.rule) - field_proto.type = Protobuf::Descriptor.type2id(@field_instance.type) - if [Google::Protobuf::FieldDescriptorProto::Type::TYPE_MESSAGE, - Google::Protobuf::FieldDescriptorProto::Type::TYPE_ENUM].include? field_proto.type - field_proto.type_name = @field_instance.type.to_s.split('::').last - end - field_proto.default_value = @field_instance.default.to_s if @field_instance.default - - case parent_proto - when Google::Protobuf::FileDescriptorProto then - parent_proto.extension << field_proto - when Google::Protobuf::DescriptorProto then - if extension - parent_proto.extension << field_proto - else - parent_proto.field << field_proto - end - else - raise TypeError, parent_proto.class.name - end - end - end - end -end diff --git a/lib/protobuf/descriptor/file_descriptor.rb b/lib/protobuf/descriptor/file_descriptor.rb deleted file mode 100644 index b933eb0d..00000000 --- a/lib/protobuf/descriptor/file_descriptor.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Protobuf - module Descriptor - class FileDescriptor - class < mod - end - proto.enum_type.each do |enum_proto| - Protobuf::Enum.descriptor.build enum_proto, :module => mod - end - end - - def unbuild(messages) - messages = [messages] unless messages.is_a? Array - proto = Google::Protobuf::FileDescriptorProto.new - proto.package = messages.first.to_s.split('::')[0..-2].join('::') if messages.first.to_s =~ /::/ - messages.each do |message| - message.descriptor.unbuild proto - end - proto - end - end - end - end -end diff --git a/lib/protobuf/descriptors.rb b/lib/protobuf/descriptors.rb new file mode 100644 index 00000000..947e41ec --- /dev/null +++ b/lib/protobuf/descriptors.rb @@ -0,0 +1,3 @@ +$LOAD_PATH << ::File.expand_path("../descriptors", __FILE__) +require 'google/protobuf/descriptor.pb' +require 'google/protobuf/compiler/plugin.pb' diff --git a/lib/protobuf/descriptors/google/protobuf/compiler/plugin.pb.rb b/lib/protobuf/descriptors/google/protobuf/compiler/plugin.pb.rb new file mode 100644 index 00000000..62e17f56 --- /dev/null +++ b/lib/protobuf/descriptors/google/protobuf/compiler/plugin.pb.rb @@ -0,0 +1,79 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + + +## +# Imports +# +require 'google/protobuf/descriptor.pb' + +module Google + module Protobuf + module Compiler + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Message Classes + # + class Version < ::Protobuf::Message; end + class CodeGeneratorRequest < ::Protobuf::Message; end + class CodeGeneratorResponse < ::Protobuf::Message + class Feature < ::Protobuf::Enum + define :FEATURE_NONE, 0 + define :FEATURE_PROTO3_OPTIONAL, 1 + end + + class File < ::Protobuf::Message; end + + end + + + + ## + # File Options + # + set_option :java_package, "com.google.protobuf.compiler" + set_option :java_outer_classname, "PluginProtos" + set_option :go_package, "google.golang.org/protobuf/types/pluginpb" + + + ## + # Message Fields + # + class Version + optional :int32, :major, 1 + optional :int32, :minor, 2 + optional :int32, :patch, 3 + optional :string, :suffix, 4 + end + + class CodeGeneratorRequest + repeated :string, :file_to_generate, 1 + optional :string, :parameter, 2 + repeated ::Google::Protobuf::FileDescriptorProto, :proto_file, 15 + optional ::Google::Protobuf::Compiler::Version, :compiler_version, 3 + end + + class CodeGeneratorResponse + class File + optional :string, :name, 1 + optional :string, :insertion_point, 2 + optional :string, :content, 15 + optional ::Google::Protobuf::GeneratedCodeInfo, :generated_code_info, 16 + end + + optional :string, :error, 1 + optional :uint64, :supported_features, 2 + repeated ::Google::Protobuf::Compiler::CodeGeneratorResponse::File, :file, 15 + end + + end + + end + +end + diff --git a/lib/protobuf/descriptors/google/protobuf/descriptor.pb.rb b/lib/protobuf/descriptors/google/protobuf/descriptor.pb.rb new file mode 100644 index 00000000..acff3ebd --- /dev/null +++ b/lib/protobuf/descriptors/google/protobuf/descriptor.pb.rb @@ -0,0 +1,360 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + +module Google + module Protobuf + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Message Classes + # + class FileDescriptorSet < ::Protobuf::Message; end + class FileDescriptorProto < ::Protobuf::Message; end + class DescriptorProto < ::Protobuf::Message + class ExtensionRange < ::Protobuf::Message; end + class ReservedRange < ::Protobuf::Message; end + + end + + class ExtensionRangeOptions < ::Protobuf::Message; end + class FieldDescriptorProto < ::Protobuf::Message + class Type < ::Protobuf::Enum + define :TYPE_DOUBLE, 1 + define :TYPE_FLOAT, 2 + define :TYPE_INT64, 3 + define :TYPE_UINT64, 4 + define :TYPE_INT32, 5 + define :TYPE_FIXED64, 6 + define :TYPE_FIXED32, 7 + define :TYPE_BOOL, 8 + define :TYPE_STRING, 9 + define :TYPE_GROUP, 10 + define :TYPE_MESSAGE, 11 + define :TYPE_BYTES, 12 + define :TYPE_UINT32, 13 + define :TYPE_ENUM, 14 + define :TYPE_SFIXED32, 15 + define :TYPE_SFIXED64, 16 + define :TYPE_SINT32, 17 + define :TYPE_SINT64, 18 + end + + class Label < ::Protobuf::Enum + define :LABEL_OPTIONAL, 1 + define :LABEL_REQUIRED, 2 + define :LABEL_REPEATED, 3 + end + + end + + class OneofDescriptorProto < ::Protobuf::Message; end + class EnumDescriptorProto < ::Protobuf::Message + class EnumReservedRange < ::Protobuf::Message; end + + end + + class EnumValueDescriptorProto < ::Protobuf::Message; end + class ServiceDescriptorProto < ::Protobuf::Message; end + class MethodDescriptorProto < ::Protobuf::Message; end + class FileOptions < ::Protobuf::Message + class OptimizeMode < ::Protobuf::Enum + define :SPEED, 1 + define :CODE_SIZE, 2 + define :LITE_RUNTIME, 3 + end + + end + + class MessageOptions < ::Protobuf::Message; end + class FieldOptions < ::Protobuf::Message + class CType < ::Protobuf::Enum + define :STRING, 0 + define :CORD, 1 + define :STRING_PIECE, 2 + end + + class JSType < ::Protobuf::Enum + define :JS_NORMAL, 0 + define :JS_STRING, 1 + define :JS_NUMBER, 2 + end + + end + + class OneofOptions < ::Protobuf::Message; end + class EnumOptions < ::Protobuf::Message; end + class EnumValueOptions < ::Protobuf::Message; end + class ServiceOptions < ::Protobuf::Message; end + class MethodOptions < ::Protobuf::Message + class IdempotencyLevel < ::Protobuf::Enum + define :IDEMPOTENCY_UNKNOWN, 0 + define :NO_SIDE_EFFECTS, 1 + define :IDEMPOTENT, 2 + end + + end + + class UninterpretedOption < ::Protobuf::Message + class NamePart < ::Protobuf::Message; end + + end + + class SourceCodeInfo < ::Protobuf::Message + class Location < ::Protobuf::Message; end + + end + + class GeneratedCodeInfo < ::Protobuf::Message + class Annotation < ::Protobuf::Message; end + + end + + + + ## + # File Options + # + set_option :java_package, "com.google.protobuf" + set_option :java_outer_classname, "DescriptorProtos" + set_option :optimize_for, ::Google::Protobuf::FileOptions::OptimizeMode::SPEED + set_option :go_package, "google.golang.org/protobuf/types/descriptorpb" + set_option :cc_enable_arenas, true + set_option :objc_class_prefix, "GPB" + set_option :csharp_namespace, "Google.Protobuf.Reflection" + + + ## + # Message Fields + # + class FileDescriptorSet + repeated ::Google::Protobuf::FileDescriptorProto, :file, 1 + end + + class FileDescriptorProto + optional :string, :name, 1 + optional :string, :package, 2 + repeated :string, :dependency, 3 + repeated :int32, :public_dependency, 10 + repeated :int32, :weak_dependency, 11 + repeated ::Google::Protobuf::DescriptorProto, :message_type, 4 + repeated ::Google::Protobuf::EnumDescriptorProto, :enum_type, 5 + repeated ::Google::Protobuf::ServiceDescriptorProto, :service, 6 + repeated ::Google::Protobuf::FieldDescriptorProto, :extension, 7 + optional ::Google::Protobuf::FileOptions, :options, 8 + optional ::Google::Protobuf::SourceCodeInfo, :source_code_info, 9 + optional :string, :syntax, 12 + end + + class DescriptorProto + class ExtensionRange + optional :int32, :start, 1 + optional :int32, :end, 2 + optional ::Google::Protobuf::ExtensionRangeOptions, :options, 3 + end + + class ReservedRange + optional :int32, :start, 1 + optional :int32, :end, 2 + end + + optional :string, :name, 1 + repeated ::Google::Protobuf::FieldDescriptorProto, :field, 2 + repeated ::Google::Protobuf::FieldDescriptorProto, :extension, 6 + repeated ::Google::Protobuf::DescriptorProto, :nested_type, 3 + repeated ::Google::Protobuf::EnumDescriptorProto, :enum_type, 4 + repeated ::Google::Protobuf::DescriptorProto::ExtensionRange, :extension_range, 5 + repeated ::Google::Protobuf::OneofDescriptorProto, :oneof_decl, 8 + optional ::Google::Protobuf::MessageOptions, :options, 7 + repeated ::Google::Protobuf::DescriptorProto::ReservedRange, :reserved_range, 9 + repeated :string, :reserved_name, 10 + end + + class ExtensionRangeOptions + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class FieldDescriptorProto + optional :string, :name, 1 + optional :int32, :number, 3 + optional ::Google::Protobuf::FieldDescriptorProto::Label, :label, 4 + optional ::Google::Protobuf::FieldDescriptorProto::Type, :type, 5 + optional :string, :type_name, 6 + optional :string, :extendee, 2 + optional :string, :default_value, 7 + optional :int32, :oneof_index, 9 + optional :string, :json_name, 10 + optional ::Google::Protobuf::FieldOptions, :options, 8 + optional :bool, :proto3_optional, 17 + end + + class OneofDescriptorProto + optional :string, :name, 1 + optional ::Google::Protobuf::OneofOptions, :options, 2 + end + + class EnumDescriptorProto + class EnumReservedRange + optional :int32, :start, 1 + optional :int32, :end, 2 + end + + optional :string, :name, 1 + repeated ::Google::Protobuf::EnumValueDescriptorProto, :value, 2 + optional ::Google::Protobuf::EnumOptions, :options, 3 + repeated ::Google::Protobuf::EnumDescriptorProto::EnumReservedRange, :reserved_range, 4 + repeated :string, :reserved_name, 5 + end + + class EnumValueDescriptorProto + optional :string, :name, 1 + optional :int32, :number, 2 + optional ::Google::Protobuf::EnumValueOptions, :options, 3 + end + + class ServiceDescriptorProto + optional :string, :name, 1 + repeated ::Google::Protobuf::MethodDescriptorProto, :method, 2 + optional ::Google::Protobuf::ServiceOptions, :options, 3 + end + + class MethodDescriptorProto + optional :string, :name, 1 + optional :string, :input_type, 2 + optional :string, :output_type, 3 + optional ::Google::Protobuf::MethodOptions, :options, 4 + optional :bool, :client_streaming, 5, :default => false + optional :bool, :server_streaming, 6, :default => false + end + + class FileOptions + optional :string, :java_package, 1 + optional :string, :java_outer_classname, 8 + optional :bool, :java_multiple_files, 10, :default => false + optional :bool, :java_generate_equals_and_hash, 20, :deprecated => true + optional :bool, :java_string_check_utf8, 27, :default => false + optional ::Google::Protobuf::FileOptions::OptimizeMode, :optimize_for, 9, :default => ::Google::Protobuf::FileOptions::OptimizeMode::SPEED + optional :string, :go_package, 11 + optional :bool, :cc_generic_services, 16, :default => false + optional :bool, :java_generic_services, 17, :default => false + optional :bool, :py_generic_services, 18, :default => false + optional :bool, :php_generic_services, 42, :default => false + optional :bool, :deprecated, 23, :default => false + optional :bool, :cc_enable_arenas, 31, :default => true + optional :string, :objc_class_prefix, 36 + optional :string, :csharp_namespace, 37 + optional :string, :swift_prefix, 39 + optional :string, :php_class_prefix, 40 + optional :string, :php_namespace, 41 + optional :string, :php_metadata_namespace, 44 + optional :string, :ruby_package, 45 + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class MessageOptions + optional :bool, :message_set_wire_format, 1, :default => false + optional :bool, :no_standard_descriptor_accessor, 2, :default => false + optional :bool, :deprecated, 3, :default => false + optional :bool, :map_entry, 7 + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class FieldOptions + optional ::Google::Protobuf::FieldOptions::CType, :ctype, 1, :default => ::Google::Protobuf::FieldOptions::CType::STRING + optional :bool, :packed, 2 + optional ::Google::Protobuf::FieldOptions::JSType, :jstype, 6, :default => ::Google::Protobuf::FieldOptions::JSType::JS_NORMAL + optional :bool, :lazy, 5, :default => false + optional :bool, :deprecated, 3, :default => false + optional :bool, :weak, 10, :default => false + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class OneofOptions + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class EnumOptions + optional :bool, :allow_alias, 2 + optional :bool, :deprecated, 3, :default => false + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class EnumValueOptions + optional :bool, :deprecated, 1, :default => false + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class ServiceOptions + optional :bool, :deprecated, 33, :default => false + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class MethodOptions + optional :bool, :deprecated, 33, :default => false + optional ::Google::Protobuf::MethodOptions::IdempotencyLevel, :idempotency_level, 34, :default => ::Google::Protobuf::MethodOptions::IdempotencyLevel::IDEMPOTENCY_UNKNOWN + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class UninterpretedOption + class NamePart + required :string, :name_part, 1 + required :bool, :is_extension, 2 + end + + repeated ::Google::Protobuf::UninterpretedOption::NamePart, :name, 2 + optional :string, :identifier_value, 3 + optional :uint64, :positive_int_value, 4 + optional :int64, :negative_int_value, 5 + optional :double, :double_value, 6 + optional :bytes, :string_value, 7 + optional :string, :aggregate_value, 8 + end + + class SourceCodeInfo + class Location + repeated :int32, :path, 1, :packed => true + repeated :int32, :span, 2, :packed => true + optional :string, :leading_comments, 3 + optional :string, :trailing_comments, 4 + repeated :string, :leading_detached_comments, 6 + end + + repeated ::Google::Protobuf::SourceCodeInfo::Location, :location, 1 + end + + class GeneratedCodeInfo + class Annotation + repeated :int32, :path, 1, :packed => true + optional :string, :source_file, 2 + optional :int32, :begin, 3 + optional :int32, :end, 4 + end + + repeated ::Google::Protobuf::GeneratedCodeInfo::Annotation, :annotation, 1 + end + + end + +end + diff --git a/lib/protobuf/encoder.rb b/lib/protobuf/encoder.rb new file mode 100644 index 00000000..33e2d8ad --- /dev/null +++ b/lib/protobuf/encoder.rb @@ -0,0 +1,11 @@ +module Protobuf + class Encoder + def self.encode(message, stream) + message.each_field_for_serialization do |field, value| + field.encode_to_stream(value, stream) + end + + stream + end + end +end diff --git a/lib/protobuf/enum.rb b/lib/protobuf/enum.rb new file mode 100644 index 00000000..c0a2a809 --- /dev/null +++ b/lib/protobuf/enum.rb @@ -0,0 +1,365 @@ +require 'delegate' + +## +# Adding extension to Numeric until +# we can get people to stop calling #value +# on Enum instances. +# +::Protobuf.deprecator.define_deprecated_methods(Numeric, :value => :to_int) + +module Protobuf + class Enum < SimpleDelegator + # Public: Allows setting Options on the Enum class. + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::EnumOptions } + + def self.aliases_allowed? + get_option(:allow_alias) + end + + # Public: Get all integer tags defined by this enum. + # + # Examples + # + # class StateMachine < ::Protobuf::Enum + # set_option :allow_alias + # define :ON, 1 + # define :STARTED, 1 + # define :OFF, 2 + # end + # + # StateMachine.all_tags #=> [ 1, 2 ] + # + # Returns an array of unique integers. + # + def self.all_tags + @all_tags ||= enums.map(&:to_i).uniq + end + + # Internal: DSL method to create a new Enum. The given name will + # become a constant for this Enum pointing to the new instance. + # + # Examples + # + # class StateMachine < ::Protobuf::Enum + # define :ON, 1 + # define :OFF, 2 + # end + # + # StateMachine::ON #=> # + # StateMachine::OFF #=> # + # + # Returns nothing. + # + def self.define(name, tag) + enum = new(self, name, tag) + @enums ||= [] + @enums << enum + # defining a new field for the enum will cause cached @values and @mapped_enums + # to be incorrect; reset them + @mapped_enums = @values = nil + const_set(name, enum) + mapped_enums + end + + # Internal: A mapping of enum number -> enums defined + # used for speeding up our internal enum methods. + def self.mapped_enums + @mapped_enums ||= enums.each_with_object({}) do |enum, hash| + list = hash[enum.to_i] ||= [] + list << enum + end + end + + # Public: All defined enums. + # + class << self + attr_reader :enums + end + + # Public: Get an array of Enum objects with the given tag. + # + # tag - An object that responds to to_i. + # + # Examples + # + # class StateMachine < ::Protobuf::Enum + # set_option :allow_alias + # define :ON, 1 + # define :STARTED, 1 + # define :OFF, 2 + # end + # + # StateMachine.enums_for_tag(1) #=> [ #, # ] + # StateMachine.enums_for_tag(2) #=> [ # ] + # + # Returns an array with zero or more Enum objects or nil. + # + def self.enums_for_tag(tag) + tag && mapped_enums[tag.to_i] || [] + end + + # Public: Get the Enum associated with the given name. + # + # name - A string-like object. Case-sensitive. + # + # Example + # + # class StateMachine < ::Protobuf::Enum + # define :ON, 1 + # define :OFF, 2 + # end + # + # StateMachine.enum_for_name(:ON) # => # + # StateMachine.enum_for_name("ON") # => # + # StateMachine.enum_for_name(:on) # => nil + # StateMachine.enum_for_name(:FOO) # => nil + # + # Returns the Enum object with the given name or nil. + # + def self.enum_for_name(name) + const_get(name) + rescue ::NameError + nil + end + + # Public: Get the Enum object corresponding to the given tag. + # + # tag - An object that responds to to_i. + # + # Returns an Enum object or nil. If the tag corresponds to multiple + # Enums, the first enum defined will be returned. + # + def self.enum_for_tag(tag) + tag && (mapped_enums[tag.to_i] || []).first + end + + def self.enum_for_tag_integer(tag) + (@mapped_enums[tag] || []).first + end + + # Public: Get an Enum by a variety of type-checking mechanisms. + # + # candidate - An Enum, Numeric, String, or Symbol object. + # + # Example + # + # class StateMachine < ::Protobuf::Enum + # set_option :allow_alias + # define :ON, 1 + # define :STARTED, 1 + # define :OFF, 2 + # end + # + # StateMachine.fetch(StateMachine::ON) # => # + # StateMachine.fetch(:ON) # => # + # StateMachine.fetch("http://github.com/STARTED") # => # + # StateMachine.fetch(1) # => [ #, # ] + # + # Returns an Enum object or nil. + # + def self.fetch(candidate) + return enum_for_tag_integer(candidate) if candidate.is_a?(::Integer) + + case candidate + when self + candidate + when ::Numeric + enum_for_tag(candidate.to_i) + when ::String, ::Symbol + enum_for_name(candidate) + else + nil + end + end + + # Public: Get the Symbol name associated with the given number. + # + # number - An object that responds to to_i. + # + # Examples + # + # # Without aliases + # class StateMachine < ::Protobuf::Enum + # define :ON, 1 + # define :OFF, 2 + # end + # + # StateMachine.name_for_tag(1) # => :ON + # StateMachine.name_for_tag(2) # => :OFF + # StateMachine.name_for_tag(3) # => nil + # + # # With aliases + # class StateMachine < ::Protobuf::Enum + # set_option :allow_alias + # define :STARTED, 1 + # define :ON, 1 + # define :OFF, 2 + # end + # + # StateMachine.name_for_tag(1) # => :STARTED + # StateMachine.name_for_tag(2) # => :OFF + # + # Returns the symbol name of the enum constant given it's associated tag or nil. + # If the given tag has multiple names associated (due to allow_alias) + # the first defined name will be returned. + # + def self.name_for_tag(tag) + enum_for_tag(tag).try(:name) + end + + # Public: Check if the given tag is defined by this Enum. + # + # number - An object that responds to to_i. + # + # Returns a boolean. + # + def self.valid_tag?(tag) + tag.respond_to?(:to_i) && mapped_enums.key?(tag.to_i) + end + + # Public: [DEPRECATED] Return a hash of Enum objects keyed + # by their :name. + # + def self.values + @values ||= enums.each_with_object({}) do |enum, hash| + hash[enum.name] = enum + end + end + + ## + # Class Deprecations + # + class << self + ::Protobuf.deprecator.define_deprecated_methods( + self, + :enum_by_value => :enum_for_tag, + :name_by_value => :name_for_tag, + :get_name_by_tag => :name_for_tag, + :value_by_name => :enum_for_name, + ) + + ::Protobuf.deprecator.deprecate_methods(self, :values => :enums) + end + + ## + # Attributes + # + + private + + attr_writer :parent_class, :name, :tag + + public + + attr_reader :parent_class, :name, :tag + + ## + # Instance Methods + # + + def initialize(parent_class, name, tag) + self.parent_class = parent_class + self.name = name + self.tag = tag + super(tag) + end + + # Custom equality method since otherwise identical values from different + # enums will be considered equal by Integer's equality method (or + # Fixnum's on Ruby < 2.4.0). + # + def ==(other) + if other.is_a?(Protobuf::Enum) + parent_class == other.parent_class && tag == other.tag + elsif tag.class == other.class + tag == other + else + false + end + end + + # Overriding the class so ActiveRecord/Arel visitor will visit the enum as an + # Integer. + # + def class + # This is done for backward compatibility for < 2.4.0. This ensures that + # if Ruby version >= 2.4.0, this will return Integer. If below, then will + # return Fixnum. + # + # This prevents the constant deprecation warnings on Fixnum. + tag.class + end + + # Protobuf::Enum delegates methods to Fixnum, which has a custom hash equality method (`eql?`) + # This causes enum values to be equivalent when using `==`, `===`, `equals?`, but not `eql?`**: + # + # 2.3.7 :002 > ::Test::EnumTestType::ZERO.eql?(::Test::EnumTestType::ZERO) + # => false + # + # However, they are equilvalent to their tag value: + # + # 2.3.7 :002 > ::Test::EnumTestType::ZERO.eql?(::Test::EnumTestType::ZERO.tag) + # => true + # + # **The implementation changed in Ruby 2.5, so this only affects Ruby versions less than v2.4. + # + # Use the hash equality implementation from Object#eql?, which is equivalent to == instead. + # + def eql?(other) + self == other + end + + def inspect + "\#" + end + + def to_int + tag.to_int + end + + # This fixes a reflection bug in JrJackson RubyAnySerializer that does not + # render Protobuf enums correctly because to_json is not defined. It takes + # any number of arguments to support the JSON gem trying to pass an argument. + # NB: This method is required to return a string and not an integer. + # + def to_json(*) + to_s + end + + def to_s(format = :tag) + case format + when :tag + to_i.to_s + when :name + name.to_s + else + to_i.to_s + end + end + + # Re-implement `try` in order to fix the problem where + # the underlying fixnum doesn't respond to all methods (e.g. name or tag). + # If we respond to the first argument, `__send__` the args. Otherwise, + # delegate the `try` call to the underlying vlaue fixnum. + # + def try(*args, &block) + case + when args.empty? && block_given? + yield self + when respond_to?(args.first) + __send__(*args, &block) + else + @tag.try(*args, &block) + end + end + + ## + # Instance Aliases + # + alias :to_i tag + alias :to_hash_value tag + alias :to_json_hash_value tag + + ::Protobuf.deprecator.define_deprecated_methods(self, :value => :to_i) + end +end diff --git a/lib/protobuf/common/exceptions.rb b/lib/protobuf/exceptions.rb similarity index 53% rename from lib/protobuf/common/exceptions.rb rename to lib/protobuf/exceptions.rb index 77b633ff..223b2fb9 100644 --- a/lib/protobuf/common/exceptions.rb +++ b/lib/protobuf/exceptions.rb @@ -1,11 +1,9 @@ module Protobuf - class Error < StandardError; end - class InvalidWireType < Error; end - class NotInitializedError < Error; end - class TagCollisionError < Error; end - + class SerializationError < StandardError; end + class FieldNotDefinedError < StandardError; end + class DuplicateFieldNameError < StandardError; end end diff --git a/lib/protobuf/ext/eventmachine.rb b/lib/protobuf/ext/eventmachine.rb deleted file mode 100644 index e49d986e..00000000 --- a/lib/protobuf/ext/eventmachine.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'fiber' - -# Method from em-synchrony -# https://github.com/igrigorik/em-synchrony -# -# A convenience method for wrapping EM.run body within -# a Ruby Fiber such that async operations can be transparently -# paused and resumed based on IO scheduling -module EventMachine - def self.fiber_run(blk=nil, tail=nil, &block) - context = Proc.new{ Fiber.new{ (b = blk || block) and b.call }.resume } - self.run(context, tail) - end -end diff --git a/lib/protobuf/field.rb b/lib/protobuf/field.rb new file mode 100644 index 00000000..f13aa9d4 --- /dev/null +++ b/lib/protobuf/field.rb @@ -0,0 +1,74 @@ +require 'protobuf/field/base_field' +require 'protobuf/field/bytes_field' +require 'protobuf/field/float_field' +require 'protobuf/field/message_field' +require 'protobuf/field/varint_field' +require 'protobuf/field/string_field' +require 'protobuf/field/double_field' +require 'protobuf/field/enum_field' +require 'protobuf/field/integer_field' +require 'protobuf/field/signed_integer_field' +require 'protobuf/field/uint32_field' +require 'protobuf/field/uint64_field' +require 'protobuf/field/int32_field' +require 'protobuf/field/int64_field' +require 'protobuf/field/sint32_field' +require 'protobuf/field/sint64_field' +require 'protobuf/field/bool_field' +require 'protobuf/field/sfixed32_field' +require 'protobuf/field/sfixed64_field' +require 'protobuf/field/fixed32_field' +require 'protobuf/field/fixed64_field' + +module Protobuf + module Field + + PRIMITIVE_FIELD_MAP = { + :double => ::Protobuf::Field::DoubleField, + :float => ::Protobuf::Field::FloatField, + :int32 => ::Protobuf::Field::Int32Field, + :int64 => ::Protobuf::Field::Int64Field, + :uint32 => ::Protobuf::Field::Uint32Field, + :uint64 => ::Protobuf::Field::Uint64Field, + :sint32 => ::Protobuf::Field::Sint32Field, + :sint64 => ::Protobuf::Field::Sint64Field, + :fixed32 => ::Protobuf::Field::Fixed32Field, + :fixed64 => ::Protobuf::Field::Fixed64Field, + :sfixed32 => ::Protobuf::Field::Sfixed32Field, + :sfixed64 => ::Protobuf::Field::Sfixed64Field, + :string => ::Protobuf::Field::StringField, + :bytes => ::Protobuf::Field::BytesField, + :bool => ::Protobuf::Field::BoolField, + }.freeze + + def self.build(message_class, rule, type, name, tag, simple_name, options = {}) + field_class(type).new(message_class, rule, field_type(type), name, tag, simple_name, options) + end + + # Returns the field class for primitives, + # EnumField for types that inherit from Protobuf::Enum, + # and MessageField for types that inherit from Protobuf::Message. + # + def self.field_class(type) + if PRIMITIVE_FIELD_MAP.key?(type) + PRIMITIVE_FIELD_MAP[type] + elsif type < ::Protobuf::Enum + EnumField + elsif type < ::Protobuf::Message + MessageField + elsif type < ::Protobuf::Field::BaseField + type + else + fail ArgumentError, "Invalid field type #{type}" + end + end + + # Returns the mapped type for primitives, + # otherwise the given type is returned. + # + def self.field_type(type) + PRIMITIVE_FIELD_MAP.fetch(type) { type } + end + + end +end diff --git a/lib/protobuf/field/base_field.rb b/lib/protobuf/field/base_field.rb new file mode 100644 index 00000000..86472738 --- /dev/null +++ b/lib/protobuf/field/base_field.rb @@ -0,0 +1,381 @@ +require 'active_support' +require 'active_support/core_ext/hash/slice' +require 'protobuf/field/field_array' +require 'protobuf/field/field_hash' +require 'protobuf/field/base_field_object_definitions' + +module Protobuf + module Field + class BaseField + include ::Protobuf::Logging + ::Protobuf::Optionable.inject(self, false) { ::Google::Protobuf::FieldOptions } + + ## + # Constants + # + OBJECT_MODULE = ::Protobuf::Field::BaseFieldObjectDefinitions + PACKED_TYPES = [ + ::Protobuf::WireType::VARINT, + ::Protobuf::WireType::FIXED32, + ::Protobuf::WireType::FIXED64, + ].freeze + + ## + # Attributes + # + attr_reader :default_value, :message_class, :name, :fully_qualified_name, :options, :rule, :tag, :type_class + + ## + # Class Methods + # + + def self.default + nil + end + + ## + # Constructor + # + + def initialize(message_class, rule, type_class, fully_qualified_name, tag, simple_name, options) + @message_class = message_class + @name = simple_name || fully_qualified_name + @fully_qualified_name = fully_qualified_name + @rule = rule + @tag = tag + @type_class = type_class + # Populate the option hash with all the original default field options, for backwards compatibility. + # However, both default and custom options should ideally be accessed through the Optionable .{get,get!}_option functions. + @options = options.slice(:ctype, :packed, :deprecated, :lazy, :jstype, :weak, :uninterpreted_option, :default, :extension) + options.each do |option_name, value| + set_option(option_name, value) + end + + @extension = options.key?(:extension) + @deprecated = options.key?(:deprecated) + @required = rule == :required + @repeated = rule == :repeated + @optional = rule == :optional + @packed = @repeated && options.key?(:packed) + + validate_packed_field if packed? + define_accessor(simple_name, fully_qualified_name) if simple_name + set_repeated_message! + set_map! + @value_from_values = nil + @value_from_values_for_serialization = nil + @field_predicate = nil + @field_and_present_predicate = nil + @set_field = nil + @set_method = nil + @to_message_hash = nil + @to_message_hash_string_keys = nil + @encode_to_stream = nil + + define_value_from_values! + define_value_from_values_for_serialization! + define_field_predicate! + define_field_and_present_predicate! + define_set_field! + define_set_method! + define_to_message_hash! + define_encode_to_stream! + set_default_value! + end + + ## + # Public Instance Methods + # + + def acceptable?(_value) + true + end + + def coerce!(value) + value + end + + def decode(_bytes) + fail NotImplementedError, "#{self.class.name}##{__method__}" + end + + def default + options[:default] + end + + def set_default_value! + @default_value ||= if optional? || required? + typed_default_value + elsif map? + ::Protobuf::Field::FieldHash.new(self).freeze + elsif repeated? + ::Protobuf::Field::FieldArray.new(self).freeze + else + fail "Unknown field label -- something went very wrong" + end + end + + def define_encode_to_stream! + @encode_to_stream = if repeated? && packed? + OBJECT_MODULE::RepeatedPackedEncodeToStream.new(self) + elsif repeated? + OBJECT_MODULE::RepeatedNotPackedEncodeToStream.new(self) + elsif message? || type_class == ::Protobuf::Field::BytesField + OBJECT_MODULE::BytesEncodeToStream.new(self) + elsif type_class == ::Protobuf::Field::StringField + OBJECT_MODULE::StringEncodeToStream.new(self) + else + OBJECT_MODULE::BaseEncodeToStream.new(self) + end + end + + def encode_to_stream(value, stream) + @encode_to_stream.call(value, stream) + end + + def define_field_predicate! + @field_predicate = if repeated? + OBJECT_MODULE::RepeatedFieldPredicate.new(self) + else + OBJECT_MODULE::BaseFieldPredicate.new(self) + end + end + + def field?(values) + @field_predicate.call(values) + end + + def define_field_and_present_predicate! + @field_and_present_predicate = if !repeated? && type_class == ::Protobuf::Field::BoolField # boolean present check + OBJECT_MODULE::BoolFieldAndPresentPredicate.new(self) + else + OBJECT_MODULE::BaseFieldAndPresentPredicate.new(self) + end + end + + def field_and_present?(values) + @field_and_present_predicate.call(values) + end + + def define_value_from_values! + @value_from_values = if map? + OBJECT_MODULE::MapValueFromValues.new(self) + elsif repeated? + OBJECT_MODULE::RepeatedFieldValueFromValues.new(self) + elsif type_class == ::Protobuf::Field::BoolField # boolean present check + OBJECT_MODULE::BoolFieldValueFromValues.new(self) + else + OBJECT_MODULE::BaseFieldValueFromValues.new(self) + end + end + + def value_from_values(values) + @value_from_values.call(values) + end + + def define_value_from_values_for_serialization! + @value_from_values_for_serialization = if map? + OBJECT_MODULE::MapValueFromValuesForSerialization.new(self) + elsif repeated? + OBJECT_MODULE::RepeatedFieldValueFromValuesForSerialization.new(self) + elsif type_class == ::Protobuf::Field::BoolField # boolean present check + OBJECT_MODULE::BoolFieldValueFromValuesForSerialization.new(self) + else + OBJECT_MODULE::BaseFieldValueFromValuesForSerialization.new(self) + end + end + + def value_from_values_for_serialization(values) + @value_from_values_for_serialization.call(values) + end + + def define_set_field! + @set_field = if map? && required? + OBJECT_MODULE::RequiredMapSetField.new(self) + elsif repeated? && required? + OBJECT_MODULE::RequiredRepeatedSetField.new(self) + elsif type_class == ::Protobuf::Field::StringField && required? + OBJECT_MODULE::RequiredStringSetField.new(self) + elsif required? + OBJECT_MODULE::RequiredBaseSetField.new(self) + elsif map? + OBJECT_MODULE::MapSetField.new(self) + elsif repeated? + OBJECT_MODULE::RepeatedSetField.new(self) + elsif type_class == ::Protobuf::Field::StringField + OBJECT_MODULE::StringSetField.new(self) + else + OBJECT_MODULE::BaseSetField.new(self) + end + end + + def set_field(values, value, ignore_nil_for_repeated, message_instance) + @set_field.call(values, value, ignore_nil_for_repeated, message_instance) + end + + def define_to_message_hash! + if message? || enum? || repeated? || map? + @to_message_hash = OBJECT_MODULE::ToHashValueToMessageHash.new(self) + @to_message_hash_string_keys = OBJECT_MODULE::ToHashValueToMessageHashWithStringKey.new(self) + else + @to_message_hash = OBJECT_MODULE::BaseToMessageHash.new(self) + @to_message_hash_string_keys = OBJECT_MODULE::BaseToMessageHashWithStringKey.new(self) + end + end + + def to_message_hash(values, result) + @to_message_hash.call(values, result) + end + + def to_message_hash_with_string_key(values, result) + @to_message_hash_string_keys.call(values, result) + end + + def deprecated? + @deprecated + end + + def encode(_value) + fail NotImplementedError, "#{self.class.name}##{__method__}" + end + + def extension? + @extension + end + + def enum? + false + end + + def message? + false + end + + def set_map! + set_repeated_message! + @is_map = repeated_message? && type_class.get_option!(:map_entry) + end + + def map? + @is_map + end + + def optional? + @optional + end + + def packed? + @packed + end + + def repeated? + @repeated + end + + def set_repeated_message! + @repeated_message = repeated? && message? + end + + def repeated_message? + @repeated_message + end + + def required? + @required + end + + def define_set_method! + @set_method = if map? + OBJECT_MODULE::MapSetMethod.new(self) + elsif repeated? && packed? + OBJECT_MODULE::RepeatedPackedSetMethod.new(self) + elsif repeated? + OBJECT_MODULE::RepeatedNotPackedSetMethod.new(self) + else + OBJECT_MODULE::BaseSetMethod.new(self) + end + end + + def set(message_instance, bytes) + @set_method.call(message_instance, bytes) + end + + def tag_encoded + @tag_encoded ||= begin + case + when repeated? && packed? + ::Protobuf::Field::VarintField.encode((tag << 3) | ::Protobuf::WireType::LENGTH_DELIMITED) + else + ::Protobuf::Field::VarintField.encode((tag << 3) | wire_type) + end + end + end + + # FIXME: add packed, deprecated, extension options to to_s output + def to_s + "#{rule} #{type_class} #{name} = #{tag} #{default ? "[default=#{default.inspect}]" : ''}" + end + + ::Protobuf.deprecator.define_deprecated_methods(self, :type => :type_class) + + def wire_type + ::Protobuf::WireType::VARINT + end + + def fully_qualified_name_only! + @name = @fully_qualified_name + + ## + # Recreate all of the meta methods as they may have used the original `name` value + # + define_value_from_values! + define_value_from_values_for_serialization! + define_field_predicate! + define_field_and_present_predicate! + define_set_field! + define_set_method! + define_to_message_hash! + define_encode_to_stream! + end + + private + + ## + # Private Instance Methods + # + + def define_accessor(simple_field_name, fully_qualified_field_name) + message_class.class_eval do + define_method("#{simple_field_name}!") do + @values[fully_qualified_field_name] if field?(fully_qualified_field_name) + end + end + + message_class.class_eval do + define_method(simple_field_name) { self[fully_qualified_field_name] } + define_method("#{simple_field_name}=") { |v| set_field(fully_qualified_field_name, v, false) } + end + + return unless deprecated? + + ::Protobuf.field_deprecator.deprecate_method(message_class, simple_field_name) + ::Protobuf.field_deprecator.deprecate_method(message_class, "#{simple_field_name}!") + ::Protobuf.field_deprecator.deprecate_method(message_class, "#{simple_field_name}=") + end + + def typed_default_value + if default.nil? + self.class.default + else + default + end + end + + def validate_packed_field + if packed? && ! ::Protobuf::Field::BaseField::PACKED_TYPES.include?(wire_type) + fail "Can't use packed encoding for '#{type_class}' type" + end + end + end + end +end diff --git a/lib/protobuf/field/base_field_object_definitions.rb b/lib/protobuf/field/base_field_object_definitions.rb new file mode 100644 index 00000000..93a93226 --- /dev/null +++ b/lib/protobuf/field/base_field_object_definitions.rb @@ -0,0 +1,504 @@ +module Protobuf + module Field + module BaseFieldObjectDefinitions + + class ToHashValueToMessageHashWithStringKey + def initialize(selph) + @selph = selph + @name = selph.name.to_s + end + + def call(values, result) + result[@name] = @selph.value_from_values(values).to_hash_value + end + end + + class BaseToMessageHashWithStringKey + def initialize(selph) + @selph = selph + @name = selph.name.to_s + end + + def call(values, result) + result[@name] = @selph.value_from_values(values) + end + end + + class ToHashValueToMessageHash + def initialize(selph) + @selph = selph + @name = selph.name.to_sym + end + + def call(values, result) + result[@name] = @selph.value_from_values(values).to_hash_value + end + end + + class BaseToMessageHash + def initialize(selph) + @selph = selph + @name = selph.name.to_sym + end + + def call(values, result) + result[@name] = @selph.value_from_values(values) + end + end + + class RepeatedPackedEncodeToStream + def initialize(selph) + @selph = selph + @tag_encoded = selph.tag_encoded + end + + def call(value, stream) + packed_value = value.map { |val| @selph.encode(val) }.join + stream << @tag_encoded << "#{::Protobuf::Field::VarintField.encode(packed_value.size)}#{packed_value}" + end + end + + class BytesEncodeToStream + def initialize(selph) + @selph = selph + @tag_encoded = selph.tag_encoded + end + + def call(value, stream) + value = value.encode if value.is_a?(::Protobuf::Message) + byte_size = ::Protobuf::Field::VarintField.encode(value.bytesize) + + stream << @tag_encoded << byte_size << value + end + end + + class StringEncodeToStream + def initialize(selph) + @selph = selph + @tag_encoded = selph.tag_encoded + end + + def call(value, stream) + new_value = "" + value + if new_value.encoding != ::Protobuf::Field::StringField::ENCODING + new_value.encode!(::Protobuf::Field::StringField::ENCODING, :invalid => :replace, :undef => :replace, :replace => "") + end + + stream << @tag_encoded << ::Protobuf::Field::VarintField.encode(new_value.bytesize) << new_value + end + end + + class BaseEncodeToStream + def initialize(selph) + @selph = selph + @tag_encoded = selph.tag_encoded + end + + def call(value, stream) + stream << @tag_encoded << @selph.encode(value) + end + end + + class RepeatedNotPackedEncodeToStream + def initialize(selph) + @selph = selph + @tag_encoded = selph.tag_encoded + end + + def call(value, stream) + value.each do |val| + stream << @tag_encoded << @selph.encode(val) + end + end + end + + class BaseSetMethod + def initialize(selph) + @selph = selph + @name = selph.name + end + + def call(message_instance, bytes) + message_instance.set_field(@name, @selph.decode(bytes), true, @selph) + end + end + + class MapSetMethod + def initialize(selph) + @selph = selph + @name = selph.name + end + + def call(message_instance, bytes) + hash = message_instance[@name] + entry = @selph.decode(bytes) + # decoded value could be nil for an + # enum value that is not recognized + hash[entry.key] = entry.value unless entry.value.nil? + hash[entry.key] + end + end + + class RepeatedNotPackedSetMethod + def initialize(selph) + @selph = selph + @name = selph.name + end + + def call(message_instance, bytes) + message_instance[@name] << @selph.decode(bytes) + end + end + + class RepeatedPackedSetMethod + def initialize(selph) + @selph = selph + @name = selph.name + @wire_type = selph.wire_type + end + + def call(message_instance, bytes) + array = message_instance[@name] + stream = ::StringIO.new(bytes) + + if @wire_type == ::Protobuf::WireType::VARINT + array << @selph.decode(Varint.decode(stream)) until stream.eof? + elsif @wire_type == ::Protobuf::WireType::FIXED64 + array << @selph.decode(stream.read(8)) until stream.eof? + elsif @wire_type == ::Protobuf::WireType::FIXED32 + array << @selph.decode(stream.read(4)) until stream.eof? + end + end + end + + class RequiredMapSetField + def initialize(selph) + @selph = selph + @tag = selph.tag + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values, value, _ignore_nil_for_repeated, message_instance) + unless value.is_a?(Hash) + fail TypeError, <<-TYPE_ERROR + Expected map value + Got '#{value.class}' for map protobuf field #{@selph.name} + TYPE_ERROR + end + + if value.empty? + values.delete(@fully_qualified_name) + message_instance._protobuf_message_unset_required_field_tags << @tag + else + message_instance._protobuf_message_unset_required_field_tags.delete(@tag) + values[@fully_qualified_name] ||= ::Protobuf::Field::FieldHash.new(@selph) + values[@fully_qualified_name].replace(value) + end + end + end + + class MapSetField + def initialize(selph) + @selph = selph + @tag = selph.tag + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values, value, _ignore_nil_for_repeated, _message_instance) + unless value.is_a?(Hash) + fail TypeError, <<-TYPE_ERROR + Expected map value + Got '#{value.class}' for map protobuf field #{@selph.name} + TYPE_ERROR + end + + if value.empty? + values.delete(@fully_qualified_name) + else + values[@fully_qualified_name] ||= ::Protobuf::Field::FieldHash.new(@selph) + values[@fully_qualified_name].replace(value) + end + end + end + + class RequiredRepeatedSetField + def initialize(selph) + @selph = selph + @tag = selph.tag + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values, value, ignore_nil_for_repeated, message_instance) + if value.nil? && ignore_nil_for_repeated + ::Protobuf.deprecator.deprecation_warning("['#{@fully_qualified_name}']=nil", "use an empty array instead of nil") + return + end + + unless value.is_a?(Array) + fail TypeError, <<-TYPE_ERROR + Expected repeated value of type '#{@selph.type_class}' + Got '#{value.class}' for repeated protobuf field #{@selph.name} + TYPE_ERROR + end + + value = value.compact + + if value.empty? + values.delete(@fully_qualified_name) + message_instance._protobuf_message_unset_required_field_tags << @tag + else + message_instance._protobuf_message_unset_required_field_tags.delete(@tag) + values[@fully_qualified_name] ||= ::Protobuf::Field::FieldArray.new(@selph) + values[@fully_qualified_name].replace(value) + end + end + end + + class RepeatedSetField + def initialize(selph) + @selph = selph + @tag = selph.tag + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values, value, ignore_nil_for_repeated, _message_instance) + if value.nil? && ignore_nil_for_repeated + ::Protobuf.deprecator.deprecation_warning("['#{@fully_qualified_name}']=nil", "use an empty array instead of nil") + return + end + + unless value.is_a?(Array) + fail TypeError, <<-TYPE_ERROR + Expected repeated value of type '#{@selph.type_class}' + Got '#{value.class}' for repeated protobuf field #{@selph.name} + TYPE_ERROR + end + + value = value.compact + + if value.empty? + values.delete(@fully_qualified_name) + else + values[@fully_qualified_name] ||= ::Protobuf::Field::FieldArray.new(@selph) + values[@fully_qualified_name].replace(value) + end + end + end + + class RequiredStringSetField + def initialize(selph) + @selph = selph + @tag = selph.tag + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values, value, _ignore_nil_for_repeated, message_instance) + if value + message_instance._protobuf_message_unset_required_field_tags.delete(@tag) + values[@fully_qualified_name] = if value.is_a?(String) + value + else + @selph.coerce!(value) + end + else + values.delete(@fully_qualified_name) + message_instance._protobuf_message_unset_required_field_tags << @tag + end + end + end + + class StringSetField + def initialize(selph) + @selph = selph + @tag = selph.tag + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values, value, _ignore_nil_for_repeated, _message_instance) + if value + values[@fully_qualified_name] = if value.is_a?(String) + value + else + @selph.coerce!(value) + end + else + values.delete(@fully_qualified_name) + end + end + end + + class RequiredBaseSetField + def initialize(selph) + @selph = selph + @tag = selph.tag + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values, value, _ignore_nil_for_repeated, message_instance) + if value.nil? + values.delete(@fully_qualified_name) + message_instance._protobuf_message_unset_required_field_tags << @tag + else + message_instance._protobuf_message_unset_required_field_tags.delete(@tag) + values[@fully_qualified_name] = @selph.coerce!(value) + end + end + end + + class BaseSetField + def initialize(selph) + @selph = selph + @tag = selph.tag + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values, value, _ignore_nil_for_repeated, _message_instance) + if value.nil? + values.delete(@fully_qualified_name) + else + values[@fully_qualified_name] = @selph.coerce!(value) + end + end + end + + class BaseFieldAndPresentPredicate + def initialize(selph) + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values[@fully_qualified_name].present? + end + end + + class BoolFieldAndPresentPredicate + BOOL_VALUES = [true, false].freeze + + def initialize(selph) + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + BOOL_VALUES.include?(values[@fully_qualified_name]) + end + end + + class BaseFieldPredicate + def initialize(selph) + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values.key?(@fully_qualified_name) + end + end + + class RepeatedFieldPredicate + def initialize(selph) + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values.key?(@fully_qualified_name) && + values[@fully_qualified_name].present? + end + end + + class BoolFieldValueFromValues + def initialize(selph) + @selph = selph + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values.fetch(@fully_qualified_name) { @selph.default_value } + end + end + + class BoolFieldValueFromValuesForSerialization + def initialize(selph) + @selph = selph + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values.fetch(@fully_qualified_name) { @selph.default_value } + end + end + + class BaseFieldValueFromValues + def initialize(selph) + @selph = selph + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values[@fully_qualified_name] || @selph.default_value + end + end + + class BaseFieldValueFromValuesForSerialization + def initialize(selph) + @selph = selph + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values[@fully_qualified_name] || @selph.default_value + end + end + + class MapValueFromValues + def initialize(selph) + @selph = selph + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values[@fully_qualified_name] ||= ::Protobuf::Field::FieldHash.new(@selph) + end + end + + class MapValueFromValuesForSerialization + def initialize(selph) + @selph = selph + @fully_qualified_name = selph.fully_qualified_name + @type_class = selph.type_class + end + + def call(values) + value = values[@fully_qualified_name] ||= ::Protobuf::Field::FieldHash.new(@selph) + + array = [] + value.each do |k, v| + array << @type_class.new(:key => k, :value => v) + end + + array + end + end + + class RepeatedFieldValueFromValues + def initialize(selph) + @selph = selph + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values[@fully_qualified_name] ||= ::Protobuf::Field::FieldArray.new(@selph) + end + end + + class RepeatedFieldValueFromValuesForSerialization + def initialize(selph) + @selph = selph + @fully_qualified_name = selph.fully_qualified_name + end + + def call(values) + values[@fully_qualified_name] ||= ::Protobuf::Field::FieldArray.new(@selph) + end + end + end + end +end diff --git a/lib/protobuf/field/bool_field.rb b/lib/protobuf/field/bool_field.rb new file mode 100644 index 00000000..bba089f4 --- /dev/null +++ b/lib/protobuf/field/bool_field.rb @@ -0,0 +1,64 @@ +require 'protobuf/field/varint_field' + +module Protobuf + module Field + class BoolField < VarintField + ONE = 1 + FALSE_ENCODE = [0].pack('C') + FALSE_STRING = "false".freeze + FALSE_VALUES = [false, FALSE_STRING].freeze + TRUE_ENCODE = [1].pack('C') + TRUE_STRING = "true".freeze + TRUE_VALUES = [true, TRUE_STRING].freeze + ACCEPTABLES = [true, false, TRUE_STRING, FALSE_STRING].freeze + + ## + # Class Methods + # + + def self.default + false + end + + ## + # Public Instance Methods + # # + + def acceptable?(val) + ACCEPTABLES.include?(val) + end + + def coerce!(val) + if TRUE_VALUES.include?(val) + true + elsif FALSE_VALUES.include?(val) + false + else + fail TypeError, "Expected value of type '#{type_class}' for field #{name}, but got '#{val.class}'" + end + end + + def decode(value) + value == ONE + end + + def encode(value) + value ? TRUE_ENCODE : FALSE_ENCODE + end + + private + + ## + # Private Instance Methods + # + + def define_accessor(simple_field_name, _fully_qualified_field_name) + super + message_class.class_eval do + alias_method "#{simple_field_name}?", simple_field_name + end + end + + end + end +end diff --git a/lib/protobuf/field/bytes_field.rb b/lib/protobuf/field/bytes_field.rb new file mode 100644 index 00000000..81a3634d --- /dev/null +++ b/lib/protobuf/field/bytes_field.rb @@ -0,0 +1,67 @@ +require 'protobuf/wire_type' + +module Protobuf + module Field + class BytesField < BaseField + + ## + # Constants + # + + BYTES_ENCODING = Encoding::BINARY + + ## + # Class Methods + # + + def self.default + '' + end + + ## + # Public Instance Methods + # + + def acceptable?(val) + val.is_a?(String) || val.nil? || val.is_a?(Symbol) || val.is_a?(::Protobuf::Message) + end + + def decode(bytes) + bytes.force_encoding(::Protobuf::Field::BytesField::BYTES_ENCODING) + bytes + end + + def encode(value) + value_to_encode = if value.is_a?(::Protobuf::Message) + value.encode + else + "" + value + end + + value_to_encode.force_encoding(::Protobuf::Field::BytesField::BYTES_ENCODING) + "#{::Protobuf::Field::VarintField.encode(value_to_encode.bytesize)}#{value_to_encode}" + end + + def wire_type + ::Protobuf::WireType::LENGTH_DELIMITED + end + + def coerce!(value) + case value + when String, Symbol + value.to_s + when NilClass + nil + when ::Protobuf::Message + value.dup + else + fail TypeError, "Unacceptable value #{value} for field #{name} of type #{type_class}" + end + end + + def json_encode(value) + Base64.strict_encode64(value) + end + end + end +end diff --git a/lib/protobuf/field/double_field.rb b/lib/protobuf/field/double_field.rb new file mode 100644 index 00000000..31eb1b34 --- /dev/null +++ b/lib/protobuf/field/double_field.rb @@ -0,0 +1,25 @@ +require 'protobuf/field/float_field' + +module Protobuf + module Field + class DoubleField < FloatField + + ## + # Public Instance Methods + # + + def decode(bytes) + bytes.unpack('E').first + end + + def encode(value) + [value].pack('E') + end + + def wire_type + WireType::FIXED64 + end + + end + end +end diff --git a/lib/protobuf/field/enum_field.rb b/lib/protobuf/field/enum_field.rb new file mode 100644 index 00000000..6993faff --- /dev/null +++ b/lib/protobuf/field/enum_field.rb @@ -0,0 +1,56 @@ +require 'protobuf/field/integer_field' + +module Protobuf + module Field + class EnumField < IntegerField + + ## + # Class Methods + # + + def self.default + fail NoMethodError, "#{self}.#{__method__} must be called on an instance" + end + + ## + # Public Instance Methods + # + def encode(value) + # original Google's library uses 64bits integer for negative value + ::Protobuf::Field::VarintField.encode(value.to_i & 0xffff_ffff_ffff_ffff) + end + + def decode(value) + value -= 0x1_0000_0000_0000_0000 if (value & 0x8000_0000_0000_0000).nonzero? + value if acceptable?(value) + end + + def acceptable?(val) + !type_class.fetch(val).nil? + end + + def enum? + true + end + + def coerce!(value) + type_class.fetch(value) || fail(TypeError, "Invalid Enum value: #{value.inspect} for #{name}") + end + + private + + ## + # Private Instance Methods + # + + def typed_default_value + if default.is_a?(Symbol) + type_class.const_get(default) + else + type_class.fetch(default) || type_class.enums.first + end + end + + end + end +end diff --git a/lib/protobuf/field/field_array.rb b/lib/protobuf/field/field_array.rb new file mode 100644 index 00000000..e4f2eb1a --- /dev/null +++ b/lib/protobuf/field/field_array.rb @@ -0,0 +1,102 @@ +module Protobuf + module Field + class FieldArray < Array + + ## + # Attributes + # + + attr_reader :field + + ## + # Constructor + # + + def initialize(field) + @field = field + end + + ## + # Public Instance Methods + # + + def []=(nth, val) + super(nth, normalize(val)) unless val.nil? + end + + def <<(val) + super(normalize(val)) unless val.nil? + end + + def push(val) + super(normalize(val)) unless val.nil? + end + + def replace(val) + raise_type_error(val) unless val.is_a?(Array) + val.map! { |v| normalize(v) } + super(val) + end + + # Return a hash-representation of the given values for this field type. + # The value in this case would be an array. + def to_hash_value + map do |value| + value.respond_to?(:to_hash_value) ? value.to_hash_value : value + end + end + + # Return a hash-representation of the given values for this field type + # that is safe to convert to JSON. + # The value in this case would be an array. + def to_json_hash_value + if field.respond_to?(:json_encode) + map do |value| + field.json_encode(value) + end + else + map do |value| + value.respond_to?(:to_json_hash_value) ? value.to_json_hash_value : value + end + end + end + + def to_s + "[#{field.name}]" + end + + def unshift(val) + super(normalize(val)) unless val.nil? + end + + private + + ## + # Private Instance Methods + # + + def normalize(value) + value = value.to_proto if value.respond_to?(:to_proto) + fail TypeError, "Unacceptable value #{value} for field #{field.name} of type #{field.type_class}" unless field.acceptable?(value) + + if field.is_a?(::Protobuf::Field::EnumField) + field.type_class.fetch(value) + elsif field.is_a?(::Protobuf::Field::MessageField) && value.is_a?(field.type_class) + value + elsif field.is_a?(::Protobuf::Field::MessageField) && value.respond_to?(:to_hash) + field.type_class.new(value.to_hash) + else + value + end + end + + def raise_type_error(val) + fail TypeError, <<-TYPE_ERROR + Expected repeated value of type '#{field.type_class}' + Got '#{val.class}' for repeated protobuf field #{field.name} + TYPE_ERROR + end + + end + end +end diff --git a/lib/protobuf/field/field_hash.rb b/lib/protobuf/field/field_hash.rb new file mode 100644 index 00000000..36b26447 --- /dev/null +++ b/lib/protobuf/field/field_hash.rb @@ -0,0 +1,122 @@ +module Protobuf + module Field + class FieldHash < Hash + + ## + # Attributes + # + + attr_reader :field, :key_field, :value_field + + ## + # Constructor + # + + def initialize(field) + @field = field + @key_field = field.type_class.get_field(:key) + @value_field = field.type_class.get_field(:value) + end + + ## + # Public Instance Methods + # + + def []=(key, val) + super(normalize_key(key), normalize_val(val)) + end + + alias store []= + + def replace(val) + raise_type_error(val) unless val.is_a?(Hash) + clear + update(val) + end + + def merge!(other) + raise_type_error(other) unless other.is_a?(Hash) + # keys and values will be normalized by []= above + other.each { |k, v| self[k] = v } + end + + alias update merge! + + # Return a hash-representation of the given values for this field type. + # The value in this case would be the hash itself, right? Unfortunately + # not because the values of the map could be messages themselves that we + # need to transform. + def to_hash_value + each_with_object({}) do |(key, value), hash| + hash[key] = value.respond_to?(:to_hash_value) ? value.to_hash_value : value + end + end + + # Return a hash-representation of the given values for this field type + # that is safe to convert to JSON. + # + # The value in this case would be the hash itself, right? Unfortunately + # not because the values of the map could be messages themselves that we + # need to transform. + def to_json_hash_value + if field.respond_to?(:json_encode) + each_with_object({}) do |(key, value), hash| + hash[key] = field.json_encode(value) + end + else + each_with_object({}) do |(key, value), hash| + hash[key] = value.respond_to?(:to_json_hash_value) ? value.to_json_hash_value : value + end + end + end + + def to_s + "{#{field.name}}" + end + + private + + ## + # Private Instance Methods + # + + def normalize_key(key) + normalize(:key, key, key_field) + end + + def normalize_val(value) + normalize(:value, value, value_field) + end + + def normalize(what, value, normalize_field) + raise_type_error(value) if value.nil? + value = value.to_proto if value.respond_to?(:to_proto) + fail TypeError, "Unacceptable #{what} #{value} for field #{field.name} of type #{normalize_field.type_class}" unless normalize_field.acceptable?(value) + + if normalize_field.is_a?(::Protobuf::Field::EnumField) + fetch_enum(normalize_field.type_class, value) + elsif normalize_field.is_a?(::Protobuf::Field::MessageField) && value.is_a?(normalize_field.type_class) + value + elsif normalize_field.is_a?(::Protobuf::Field::MessageField) && value.respond_to?(:to_hash) + normalize_field.type_class.new(value.to_hash) + else + value + end + end + + def fetch_enum(type, val) + en = type.fetch(val) + raise_type_error(val) if en.nil? + en + end + + def raise_type_error(val) + fail TypeError, <<-TYPE_ERROR + Expected map value of type '#{key_field.type_class} -> #{value_field.type_class}' + Got '#{val.class}' for map protobuf field #{field.name} + TYPE_ERROR + end + + end + end +end diff --git a/lib/protobuf/field/fixed32_field.rb b/lib/protobuf/field/fixed32_field.rb new file mode 100644 index 00000000..74d0f384 --- /dev/null +++ b/lib/protobuf/field/fixed32_field.rb @@ -0,0 +1,25 @@ +require 'protobuf/field/uint32_field' + +module Protobuf + module Field + class Fixed32Field < Uint32Field + + ## + # Public Instance Methods + # + + def decode(bytes) + bytes.unpack('V').first + end + + def encode(value) + [value].pack('V') + end + + def wire_type + ::Protobuf::WireType::FIXED32 + end + + end + end +end diff --git a/lib/protobuf/field/fixed64_field.rb b/lib/protobuf/field/fixed64_field.rb new file mode 100644 index 00000000..ef825433 --- /dev/null +++ b/lib/protobuf/field/fixed64_field.rb @@ -0,0 +1,28 @@ +require 'protobuf/field/uint64_field' + +module Protobuf + module Field + class Fixed64Field < Uint64Field + + ## + # Public Instance Methods + # + + def decode(bytes) + # we don't use 'Q' for pack/unpack. 'Q' is machine-dependent. + values = bytes.unpack('VV') + values[0] + (values[1] << 32) + end + + def encode(value) + # we don't use 'Q' for pack/unpack. 'Q' is machine-dependent. + [value & 0xffff_ffff, value >> 32].pack('VV') + end + + def wire_type + ::Protobuf::WireType::FIXED64 + end + + end + end +end diff --git a/lib/protobuf/field/float_field.rb b/lib/protobuf/field/float_field.rb new file mode 100644 index 00000000..e5bfcb70 --- /dev/null +++ b/lib/protobuf/field/float_field.rb @@ -0,0 +1,43 @@ +require 'protobuf/field/base_field' + +module Protobuf + module Field + class FloatField < BaseField + + ## + # Class Methods + # + + def self.default + 0.0 + end + + ## + # Public Instance Methods + # + + def acceptable?(val) + val.respond_to?(:to_f) + end + + def coerce!(val) + Float(val) + rescue ArgumentError + fail TypeError, "Expected value of type '#{type_class}' for field #{name}, but got '#{val.class}'" + end + + def decode(bytes) + bytes.unpack('e').first + end + + def encode(value) + [value].pack('e') + end + + def wire_type + WireType::FIXED32 + end + + end + end +end diff --git a/lib/protobuf/field/int32_field.rb b/lib/protobuf/field/int32_field.rb new file mode 100644 index 00000000..4a7865f9 --- /dev/null +++ b/lib/protobuf/field/int32_field.rb @@ -0,0 +1,21 @@ +require 'protobuf/field/integer_field' + +module Protobuf + module Field + class Int32Field < IntegerField + + ## + # Class Methods + # + + def self.max + INT32_MAX + end + + def self.min + INT32_MIN + end + + end + end +end diff --git a/lib/protobuf/field/int64_field.rb b/lib/protobuf/field/int64_field.rb new file mode 100644 index 00000000..3b338894 --- /dev/null +++ b/lib/protobuf/field/int64_field.rb @@ -0,0 +1,34 @@ +require 'protobuf/field/integer_field' + +module Protobuf + module Field + class Int64Field < IntegerField + + ## + # Class Methods + # + + def self.max + INT64_MAX + end + + def self.min + INT64_MIN + end + + ## + # Instance Methods + # + def acceptable?(val) + if val.is_a?(Integer) || val.is_a?(Numeric) + val >= INT64_MIN && val <= INT64_MAX + else + Integer(val, 10) >= INT64_MIN && Integer(val, 10) <= INT64_MAX + end + rescue + return false + end + + end + end +end diff --git a/lib/protobuf/field/integer_field.rb b/lib/protobuf/field/integer_field.rb new file mode 100644 index 00000000..5eb3b064 --- /dev/null +++ b/lib/protobuf/field/integer_field.rb @@ -0,0 +1,23 @@ +require 'protobuf/field/varint_field' + +module Protobuf + module Field + class IntegerField < VarintField + + ## + # Public Instance Methods + # + + def decode(value) + value -= 0x1_0000_0000_0000_0000 if (value & 0x8000_0000_0000_0000).nonzero? + value + end + + def encode(value) + # original Google's library uses 64bits integer for negative value + ::Protobuf::Field::VarintField.encode(value & 0xffff_ffff_ffff_ffff) + end + + end + end +end diff --git a/lib/protobuf/field/message_field.rb b/lib/protobuf/field/message_field.rb new file mode 100644 index 00000000..34f45fd2 --- /dev/null +++ b/lib/protobuf/field/message_field.rb @@ -0,0 +1,51 @@ +require 'protobuf/field/base_field' + +module Protobuf + module Field + class MessageField < BaseField + + ## + # Public Instance Methods + # + + def acceptable?(val) + val.is_a?(type_class) || val.respond_to?(:to_hash) || val.respond_to?(:to_proto) + end + + def decode(bytes) + type_class.decode(bytes) + end + + def encode(value) + bytes = value.encode + result = ::Protobuf::Field::VarintField.encode(bytes.bytesize) + result << bytes + end + + def message? + true + end + + def wire_type + ::Protobuf::WireType::LENGTH_DELIMITED + end + + def coerce!(value) + return nil if value.nil? + + coerced_value = if value.respond_to?(:to_proto) + value.to_proto + elsif value.respond_to?(:to_hash) + type_class.new(value.to_hash) + else + value + end + + return coerced_value if coerced_value.is_a?(type_class) + + fail TypeError, "Expected value of type '#{type_class}' for field #{name}, but got '#{value.class}'" + end + + end + end +end diff --git a/lib/protobuf/field/sfixed32_field.rb b/lib/protobuf/field/sfixed32_field.rb new file mode 100644 index 00000000..9f9b64e9 --- /dev/null +++ b/lib/protobuf/field/sfixed32_field.rb @@ -0,0 +1,27 @@ +require 'protobuf/field/int32_field' + +module Protobuf + module Field + class Sfixed32Field < Int32Field + + ## + # Public Instance Methods + # + + def decode(bytes) + value = bytes.unpack('V').first + value -= 0x1_0000_0000 if (value & 0x8000_0000).nonzero? + value + end + + def encode(value) + [value].pack('V') + end + + def wire_type + ::Protobuf::WireType::FIXED32 + end + + end + end +end diff --git a/lib/protobuf/field/sfixed64_field.rb b/lib/protobuf/field/sfixed64_field.rb new file mode 100644 index 00000000..fc296483 --- /dev/null +++ b/lib/protobuf/field/sfixed64_field.rb @@ -0,0 +1,28 @@ +require 'protobuf/field/int64_field' + +module Protobuf + module Field + class Sfixed64Field < Int64Field + + ## + # Public Instance Methods + # + + def decode(bytes) + values = bytes.unpack('VV') # 'Q' is machine-dependent, don't use + value = values[0] + (values[1] << 32) + value -= 0x1_0000_0000_0000_0000 if (value & 0x8000_0000_0000_0000).nonzero? + value + end + + def encode(value) + [value & 0xffff_ffff, value >> 32].pack('VV') # 'Q' is machine-dependent, don't use + end + + def wire_type + ::Protobuf::WireType::FIXED64 + end + + end + end +end diff --git a/lib/protobuf/field/signed_integer_field.rb b/lib/protobuf/field/signed_integer_field.rb new file mode 100644 index 00000000..8945f771 --- /dev/null +++ b/lib/protobuf/field/signed_integer_field.rb @@ -0,0 +1,29 @@ +require 'protobuf/field/varint_field' + +module Protobuf + module Field + class SignedIntegerField < VarintField + + ## + # Public Instance Methods + # + + def decode(value) + if (value & 1).zero? + value >> 1 # positive value + else + ~value >> 1 # negative value + end + end + + def encode(value) + if value >= 0 + VarintField.encode(value << 1) + else + VarintField.encode(~(value << 1)) + end + end + + end + end +end diff --git a/lib/protobuf/field/sint32_field.rb b/lib/protobuf/field/sint32_field.rb new file mode 100644 index 00000000..3af0ce98 --- /dev/null +++ b/lib/protobuf/field/sint32_field.rb @@ -0,0 +1,21 @@ +require 'protobuf/field/signed_integer_field' + +module Protobuf + module Field + class Sint32Field < SignedIntegerField + + ## + # Class Methods + # + + def self.max + INT32_MAX + end + + def self.min + INT32_MIN + end + + end + end +end diff --git a/lib/protobuf/field/sint64_field.rb b/lib/protobuf/field/sint64_field.rb new file mode 100644 index 00000000..2aba7dfa --- /dev/null +++ b/lib/protobuf/field/sint64_field.rb @@ -0,0 +1,21 @@ +require 'protobuf/field/signed_integer_field' + +module Protobuf + module Field + class Sint64Field < SignedIntegerField + + ## + # Class Methods + # + + def self.max + INT64_MAX + end + + def self.min + INT64_MIN + end + + end + end +end diff --git a/lib/protobuf/field/string_field.rb b/lib/protobuf/field/string_field.rb new file mode 100644 index 00000000..6c9c278f --- /dev/null +++ b/lib/protobuf/field/string_field.rb @@ -0,0 +1,51 @@ +require 'protobuf/field/bytes_field' + +module Protobuf + module Field + class StringField < BytesField + + ## + # Constants + # + + ENCODING = Encoding::UTF_8 + + ## + # Public Instance Methods + # + + def acceptable?(val) + val.is_a?(String) || val.nil? || val.is_a?(Symbol) + end + + def coerce!(value) + if value.nil? + nil + elsif acceptable?(value) + value.to_s + else + fail TypeError, "Unacceptable value #{value} for field #{name} of type #{type_class}" + end + end + + def decode(bytes) + bytes.force_encoding(::Protobuf::Field::StringField::ENCODING) + bytes + end + + def encode(value) + value_to_encode = "" + value # dup is slower + unless value_to_encode.encoding == ENCODING + value_to_encode.encode!(::Protobuf::Field::StringField::ENCODING, :invalid => :replace, :undef => :replace, :replace => "") + end + value_to_encode.force_encoding(::Protobuf::Field::BytesField::BYTES_ENCODING) + + "#{::Protobuf::Field::VarintField.encode(value_to_encode.bytesize)}#{value_to_encode}" + end + + def json_encode(value) + value + end + end + end +end diff --git a/lib/protobuf/field/uint32_field.rb b/lib/protobuf/field/uint32_field.rb new file mode 100644 index 00000000..50fa8fef --- /dev/null +++ b/lib/protobuf/field/uint32_field.rb @@ -0,0 +1,21 @@ +require 'protobuf/field/varint_field' + +module Protobuf + module Field + class Uint32Field < VarintField + + ## + # Class Methods + # + + def self.max + UINT32_MAX + end + + def self.min + 0 + end + + end + end +end diff --git a/lib/protobuf/field/uint64_field.rb b/lib/protobuf/field/uint64_field.rb new file mode 100644 index 00000000..8a060f14 --- /dev/null +++ b/lib/protobuf/field/uint64_field.rb @@ -0,0 +1,21 @@ +require 'protobuf/field/varint_field' + +module Protobuf + module Field + class Uint64Field < VarintField + + ## + # Class Methods + # + + def self.max + UINT64_MAX + end + + def self.min + 0 + end + + end + end +end diff --git a/lib/protobuf/field/varint_field.rb b/lib/protobuf/field/varint_field.rb new file mode 100644 index 00000000..0a183f0f --- /dev/null +++ b/lib/protobuf/field/varint_field.rb @@ -0,0 +1,77 @@ +require 'protobuf/field/base_field' + +module Protobuf + module Field + class VarintField < BaseField + + ## + # Constants + # + INT32_MAX = 2**31 - 1 + INT32_MIN = -2**31 + INT64_MAX = 2**63 - 1 + INT64_MIN = -2**63 + UINT32_MAX = 2**32 - 1 + UINT64_MAX = 2**64 - 1 + + ## + # Class Methods + # + + def self.default + 0 + end + + def self.encode(value) + ::Protobuf::Varint.encode(value) + end + + ## + # Public Instance Methods + # + def acceptable?(val) + int_val = if val.is_a?(Integer) + return true if val >= 0 && val < INT32_MAX # return quickly for smallest integer size, hot code path + val + elsif val.is_a?(Numeric) + val.to_i + else + Integer(val, 10) + end + + int_val >= self.class.min && int_val <= self.class.max + rescue + false + end + + def coerce!(val) + if val.is_a?(Integer) && val >= 0 && val <= INT32_MAX + val + else + fail TypeError, "Expected value of type '#{type_class}' for field #{name}, but got '#{val.class}'" unless acceptable?(val) + + if val.is_a?(Integer) || val.is_a?(Numeric) + val.to_i + else + Integer(val, 10) + end + end + rescue ArgumentError + fail TypeError, "Expected value of type '#{type_class}' for field #{name}, but got '#{val.class}'" + end + + def decode(value) + value + end + + def encode(value) + ::Protobuf::Field::VarintField.encode(value) + end + + def wire_type + ::Protobuf::WireType::VARINT + end + + end + end +end diff --git a/lib/protobuf/generators/base.rb b/lib/protobuf/generators/base.rb new file mode 100644 index 00000000..f3a5b472 --- /dev/null +++ b/lib/protobuf/generators/base.rb @@ -0,0 +1,85 @@ +require 'protobuf/generators/printable' + +module Protobuf + module Generators + class Base + include ::Protobuf::Generators::Printable + + def self.validate_tags(type_name, tags) + return if tags.empty? + + unique_tags = tags.uniq + + if unique_tags.size < tags.size + ::Protobuf::CodeGenerator.fatal("#{type_name} object has duplicate tags. Expected #{unique_tags.size} tags, but got #{tags.size}. Suppress with PB_NO_TAG_WARNINGS=1.") + end + + unless ENV.key?('PB_NO_TAG_WARNINGS') + expected_size = tags.max - tags.min + 1 + if tags.size < expected_size + ::Protobuf::CodeGenerator.print_tag_warning_suppress + ::Protobuf::CodeGenerator.warn("#{type_name} object should have #{expected_size} tags (#{tags.min}..#{tags.max}), but found #{tags.size} tags.") + end + end + end + + attr_reader :descriptor, :namespace, :options + + def initialize(descriptor, indent_level = 0, options = {}) + @descriptor = descriptor + @options = options + @namespace = @options.fetch(:namespace) { [] } + init_printer(indent_level) + end + + def fully_qualified_type_namespace + ".#{type_namespace.join('.')}" + end + + def run_once(label) + tracker_ivar = "@_#{label}_compiled" + value_ivar = "@_#{label}_compiled_value" + + if instance_variable_get(tracker_ivar) + return instance_variable_get(value_ivar) + end + + return_value = yield + instance_variable_set(tracker_ivar, true) + instance_variable_set(value_ivar, return_value) + return_value + end + + def to_s + compile + print_contents # see Printable + end + + def type_namespace + @type_namespace ||= @namespace + [descriptor.name] + end + + def serialize_value(value) + case value + when Message + fields = value.each_field.map do |field, inner_value| + next unless value.field?(field.name) + serialized_inner_value = serialize_value(inner_value) + "#{field.fully_qualified_name.inspect} => #{serialized_inner_value}" + end.compact + "{ #{fields.join(', ')} }" + when Enum + "::#{value.parent_class}::#{value.name}" + when String + value.inspect + when nil + "nil" + when Array + '[' + value.map { |x| serialize_value(x) }.join(', ') + ']' + else + value + end + end + end + end +end diff --git a/lib/protobuf/generators/enum_generator.rb b/lib/protobuf/generators/enum_generator.rb new file mode 100644 index 00000000..b8522171 --- /dev/null +++ b/lib/protobuf/generators/enum_generator.rb @@ -0,0 +1,40 @@ +require 'protobuf/generators/base' +require 'protobuf/generators/option_generator' + +module Protobuf + module Generators + class EnumGenerator < Base + + def compile + run_once(:compile) do + tags = [] + + print_class(descriptor.name, :enum) do + if descriptor.options + print OptionGenerator.new(descriptor.options, current_indent).to_s + puts + end + + descriptor.value.each do |enum_value_descriptor| + tags << enum_value_descriptor.number + puts build_value(enum_value_descriptor) + end + end + + unless descriptor.options.try(:allow_alias) + self.class.validate_tags(fully_qualified_type_namespace, tags) + end + end + end + + def build_value(enum_value_descriptor) + name = enum_value_descriptor.name + name.capitalize! if ENV.key?('PB_CAPITALIZE_ENUMS') + name.upcase! if ENV.key?('PB_UPCASE_ENUMS') + number = enum_value_descriptor.number + "define :#{name}, #{number}" + end + + end + end +end diff --git a/lib/protobuf/generators/extension_generator.rb b/lib/protobuf/generators/extension_generator.rb new file mode 100644 index 00000000..632753ee --- /dev/null +++ b/lib/protobuf/generators/extension_generator.rb @@ -0,0 +1,27 @@ +require 'protobuf/generators/base' +require 'protobuf/generators/group_generator' + +module Protobuf + module Generators + class ExtensionGenerator < Base + + def initialize(message_type, field_descriptors, indent_level) + super(nil, indent_level) + @message_type = modulize(message_type) + @field_descriptors = field_descriptors + end + + def compile + run_once(:compile) do + print_class(@message_type, :message) do + group = GroupGenerator.new(current_indent) + group.add_extension_fields(@field_descriptors) + group.order = [:extension_field] + print group.to_s + end + end + end + + end + end +end diff --git a/lib/protobuf/generators/field_generator.rb b/lib/protobuf/generators/field_generator.rb new file mode 100644 index 00000000..1642308f --- /dev/null +++ b/lib/protobuf/generators/field_generator.rb @@ -0,0 +1,195 @@ +require 'protobuf/generators/base' + +module Protobuf + module Generators + class FieldGenerator < Base + + ## + # Constants + # + PROTO_INFINITY_DEFAULT = /^inf$/i + PROTO_NEGATIVE_INFINITY_DEFAULT = /^-inf$/i + PROTO_NAN_DEFAULT = /^nan$/i + RUBY_INFINITY_DEFAULT = '::Float::INFINITY'.freeze + RUBY_NEGATIVE_INFINITY_DEFAULT = '-::Float::INFINITY'.freeze + RUBY_NAN_DEFAULT = '::Float::NAN'.freeze + + ## + # Attributes + # + attr_reader :field_options + + def initialize(field_descriptor, enclosing_msg_descriptor, indent_level) + super(field_descriptor, indent_level) + @enclosing_msg_descriptor = enclosing_msg_descriptor + end + + def applicable_options + # Note on the strange use of `#inspect`: + # :boom.inspect #=> ":boom" + # :".boom.foo".inspect #=> ":\".boom.foo\"" + # An alternative to `#inspect` would be always adding double quotes, + # but the generatated code looks un-idiomatic: + # ":\"#{:boom}\"" #=> ":\"boom\"" <-- Note the unnecessary double quotes + @applicable_options ||= field_options.map { |k, v| "#{k.inspect} => #{v}" } + end + + def default_value + @default_value ||= begin + if defaulted? + case descriptor.type.name + when :TYPE_ENUM + enum_default_value + when :TYPE_STRING, :TYPE_BYTES + string_default_value + when :TYPE_FLOAT, :TYPE_DOUBLE + float_double_default_value + else + verbatim_default_value + end + end + end + end + + def defaulted? + descriptor.respond_to_has_and_present?(:default_value) + end + + def deprecated? + descriptor.options.try(:deprecated?) { false } + end + + def extension? + descriptor.respond_to_has_and_present?(:extendee) + end + + def compile + run_once(:compile) do + field_definition = if map? + ["map #{map_key_type_name}", map_value_type_name, name, number, applicable_options] + else + ["#{label} #{type_name}", name, number, applicable_options] + end + puts field_definition.flatten.compact.join(', ') + end + end + + def label + @label ||= descriptor.label.name.to_s.downcase.sub(/label_/, '') # required, optional, repeated + end + + def name + @name ||= descriptor.name.to_sym.inspect + end + + def number + @number ||= descriptor.number + end + + def field_options + @field_options ||= begin + opts = {} + opts[:default] = default_value if defaulted? + opts[:packed] = 'true' if packed? + opts[:deprecated] = 'true' if deprecated? + opts[:extension] = 'true' if extension? + if descriptor.options + descriptor.options.each_field do |field_option| + next unless descriptor.options.field?(field_option.name) + option_value = descriptor.options[field_option.name] + opts[field_option.fully_qualified_name] = serialize_value(option_value) + end + end + opts + end + end + + def packed? + descriptor.options.try(:packed?) { false } + end + + # Determine the field type + def type_name + @type_name ||= determine_type_name(descriptor) + end + + # If this field is a map field, this returns a message descriptor that + # represents the entries in the map. Returns nil if this field is not + # a map field. + def map_entry + @map_entry ||= determine_map_entry + end + + def map? + !map_entry.nil? + end + + def map_key_type_name + return nil if map_entry.nil? + determine_type_name(map_entry.field.find { |v| v.name == 'key' && v.number == 1 }) + end + + def map_value_type_name + return nil if map_entry.nil? + determine_type_name(map_entry.field.find { |v| v.name == 'value' && v.number == 2 }) + end + + private + + def enum_default_value + optionally_upcased_default = + if ENV.key?('PB_UPCASE_ENUMS') + verbatim_default_value.upcase + elsif ENV.key?('PB_CAPITALIZE_ENUMS') + verbatim_default_value.capitalize + else + verbatim_default_value + end + "#{type_name}::#{optionally_upcased_default}" + end + + def float_double_default_value + case verbatim_default_value + when PROTO_INFINITY_DEFAULT then + RUBY_INFINITY_DEFAULT + when PROTO_NEGATIVE_INFINITY_DEFAULT then + RUBY_NEGATIVE_INFINITY_DEFAULT + when PROTO_NAN_DEFAULT then + RUBY_NAN_DEFAULT + else + verbatim_default_value + end + end + + def string_default_value + %("#{verbatim_default_value.gsub(/'/, '\\\\\'')}") + end + + def verbatim_default_value + descriptor.default_value + end + + def determine_type_name(descriptor) + case descriptor.type.name + when :TYPE_MESSAGE, :TYPE_ENUM, :TYPE_GROUP then + modulize(descriptor.type_name) + else + type_name = descriptor.type.name.to_s.downcase.sub(/^type_/, '') + ":#{type_name}" + end + end + + def determine_map_entry + return nil if @enclosing_msg_descriptor.nil? + return nil unless descriptor.label.name == :LABEL_REPEATED && descriptor.type.name == :TYPE_MESSAGE + # find nested message type + name_parts = descriptor.type_name.split(".") + return nil if name_parts.size < 2 || name_parts[-2] != @enclosing_msg_descriptor.name + nested = @enclosing_msg_descriptor.nested_type.find { |e| e.name == name_parts[-1] } + return nested if !nested.nil? && nested.options.try(:map_entry?) + nil + end + + end + end +end diff --git a/lib/protobuf/generators/file_generator.rb b/lib/protobuf/generators/file_generator.rb new file mode 100644 index 00000000..f94d2264 --- /dev/null +++ b/lib/protobuf/generators/file_generator.rb @@ -0,0 +1,262 @@ +require 'set' +require 'protobuf/generators/base' +require 'protobuf/generators/group_generator' + +module Protobuf + module Generators + class FileGenerator < Base + + attr_reader :output_file + + def initialize(*args) + super + @output_file = ::Google::Protobuf::Compiler::CodeGeneratorResponse::File.new(:name => file_name) + @extension_fields = Hash.new { |h, k| h[k] = [] } + @known_messages = {} + @known_enums = {} + @dangling_messages = {} + end + + def file_name + convert_filename(descriptor.name, false) + end + + def compile + run_once(:compile) do + map_extensions(descriptor, [descriptor.package]) + + print_file_comment + print_generic_requires + print_import_requires + + print_package do + inject_optionable + group = GroupGenerator.new(current_indent) + group.add_options(descriptor.options) if descriptor.options + group.add_enums(descriptor.enum_type, :namespace => [descriptor.package]) + group.add_message_declarations(descriptor.message_type) + group.add_messages(descriptor.message_type, :extension_fields => @extension_fields, :namespace => [descriptor.package]) + group.add_extended_messages(unknown_extensions) + group.add_services(descriptor.service) + + group.add_header(:enum, 'Enum Classes') + group.add_header(:message_declaration, 'Message Classes') + group.add_header(:options, 'File Options') + group.add_header(:message, 'Message Fields') + group.add_header(:extended_message, 'Extended Message Fields') + group.add_header(:service, 'Service Classes') + print group.to_s + end + + end + end + + def unknown_extensions + @unknown_extensions ||= @extension_fields.map do |message_name, fields| + message_klass = modulize(message_name).safe_constantize + if message_klass + unknown_fields = fields.reject do |field| + @known_messages[message_name] && message_klass.get_field(field.name, true) + end + [message_name, unknown_fields] + else + [message_name, fields] + end + end + end + + def generate_output_file + compile + output_file.content = to_s + output_file + end + + # Recursively map out all extensions known in this file. + # The key is the type_name of the message being extended, and + # the value is an array of field descriptors. + # + def map_extensions(descriptor, namespaces) + if fully_qualified_token?(descriptor.name) + fully_qualified_namespace = descriptor.name + elsif !(namespace = namespaces.reject(&:empty?).join(".")).empty? + fully_qualified_namespace = ".#{namespace}" + end + # Record all the message descriptor name's we encounter (should be the whole tree). + if descriptor.is_a?(::Google::Protobuf::DescriptorProto) + @known_messages[fully_qualified_namespace || descriptor.name] = descriptor + elsif descriptor.is_a?(::Google::Protobuf::EnumDescriptorProto) + @known_enums[fully_qualified_namespace || descriptor.name] = descriptor + return + end + + descriptor.extension.each do |field_descriptor| + unless fully_qualified_token?(field_descriptor.name) && fully_qualified_namespace + field_descriptor.name = "#{fully_qualified_namespace}.#{field_descriptor.name}" + end + @extension_fields[field_descriptor.extendee] << field_descriptor + end + + [:message_type, :nested_type, :enum_type].each do |type| + next unless descriptor.respond_to_has_and_present?(type) + + descriptor.public_send(type).each do |type_descriptor| + map_extensions(type_descriptor, (namespaces + [type_descriptor.name])) + end + end + end + + def print_file_comment + puts "# encoding: utf-8" + puts + puts "##" + puts "# This file is auto-generated. DO NOT EDIT!" + puts "#" + end + + def print_generic_requires + print_require("protobuf") + print_require("protobuf/rpc/service") if descriptor.service.count > 0 + puts + end + + def print_import_requires + return if descriptor.dependency.empty? + + header "Imports" + + descriptor.dependency.each do |dependency| + print_require(convert_filename(dependency), ENV.key?('PB_REQUIRE_RELATIVE')) + end + + puts + end + + def print_package(&block) + namespaces = descriptor.package.split('.') + if namespaces.empty? && ENV.key?('PB_ALLOW_DEFAULT_PACKAGE_NAME') + namespaces = [File.basename(descriptor.name).sub('.proto', '')] + end + namespaces.reverse.reduce(block) do |previous, namespace| + -> { print_module(namespace, &previous) } + end.call + end + + def eval_unknown_extensions! + @@evaled_dependencies ||= Set.new # rubocop:disable Style/ClassVars + @@all_messages ||= {} # rubocop:disable Style/ClassVars + @@all_enums ||= {} # rubocop:disable Style/ClassVars + + map_extensions(descriptor, [descriptor.package]) + @known_messages.each do |name, descriptor| + @@all_messages[name] = descriptor + end + @known_enums.each do |name, descriptor| + @@all_enums[name] = descriptor + end + + # create package namespace + print_package {} + eval_code + + unknown_extensions.each do |extendee, fields| + eval_dependencies(extendee) + fields.each do |field| + eval_dependencies(field.type_name) + end + end + group = GroupGenerator.new(0) + group.add_extended_messages(unknown_extensions, false) + print group.to_s + eval_code + rescue => e + warn "Error loading unknown extensions #{descriptor.name.inspect} error=#{e}" + raise e + end + + private + + def convert_filename(filename, for_require = true) + filename.sub(/\.proto/, (for_require ? '.pb' : '.pb.rb')) + end + + def fully_qualified_token?(token) + token[0] == '.' + end + + def eval_dependencies(name, namespace = nil) + name = "#{namespace}.#{name}" if namespace && !fully_qualified_token?(name) + return if name.empty? || @@evaled_dependencies.include?(name) || modulize(name).safe_constantize + + # if name = .foo.bar.Baz look for classes / modules named ::Foo::Bar and ::Foo + # module == pure namespace (e.g. the descriptor package name) + # class == nested messages + create_ruby_namespace_heiarchy(name) + + if (message = @@all_messages[name]) + # Create the blank namespace in case there are nested types + eval_message_code(name) + + message.nested_type.each do |nested_type| + eval_dependencies(nested_type.name, name) unless nested_type.name.empty? + end + message.field.each do |field| + eval_dependencies(field.type_name, name) unless field.type_name.empty? + end + message.enum_type.each do |enum_type| + eval_dependencies(enum_type.name, name) + end + + # Check @@evaled_dependencies again in case there was a dependency + # loop that already loaded this message + return if @@evaled_dependencies.include?(name) + eval_message_code(name, message.field) + @@evaled_dependencies << name + + elsif (enum = @@all_enums[name]) + # Check @@evaled_dependencies again in case there was a dependency + # loop that already loaded this enum + return if @@evaled_dependencies.include?(name) + namespace = name.split(".") + eval_enum_code(enum, namespace[0..-2].join(".")) + @@evaled_dependencies << name + else + fail "Error loading unknown dependencies, could not find message or enum #{name.inspect}" + end + end + + def eval_message_code(fully_qualified_namespace, fields = []) + group = GroupGenerator.new(0) + group.add_extended_messages({ fully_qualified_namespace => fields }, false) + print group.to_s + eval_code + end + + def eval_enum_code(enum, fully_qualified_namespace) + group = GroupGenerator.new(0) + group.add_enums([enum], :namespace => [fully_qualified_namespace]) + print group.to_s + eval_code(modulize(fully_qualified_namespace).safe_constantize || Object) + end + + def eval_code(context = Object) + warn "#{context.inspect}.module_eval #{print_contents.inspect}" if ENV['PB_DEBUG'] + context.module_eval print_contents.to_s + @io.truncate(0) + @io.rewind + end + + def create_ruby_namespace_heiarchy(namespace) + loop do + namespace, _match, _tail = namespace.rpartition(".") + break if namespace.empty? + eval_dependencies(namespace) + end + end + + def inject_optionable + return if descriptor.package.empty? && !ENV.key?('PB_ALLOW_DEFAULT_PACKAGE_NAME') + puts "::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions }" + end + end + end +end diff --git a/lib/protobuf/generators/group_generator.rb b/lib/protobuf/generators/group_generator.rb new file mode 100644 index 00000000..33e7b5be --- /dev/null +++ b/lib/protobuf/generators/group_generator.rb @@ -0,0 +1,122 @@ +require 'protobuf/generators/enum_generator' +require 'protobuf/generators/extension_generator' +require 'protobuf/generators/field_generator' +require 'protobuf/generators/message_generator' +require 'protobuf/generators/option_generator' +require 'protobuf/generators/service_generator' + +module Protobuf + module Generators + class GroupGenerator + include ::Protobuf::Generators::Printable + + attr_reader :groups, :indent_level + attr_writer :order + + def initialize(indent_level = 0) + @groups = Hash.new { |h, k| h[k] = [] } + @headers = {} + @comments = {} + @handlers = {} + @indent_level = indent_level + @order = [:enum, :message_declaration, :options, :message, :extended_message, :service] + init_printer(indent_level) + end + + def add_options(option_descriptor) + @groups[:options] << OptionGenerator.new(option_descriptor, indent_level) + end + + def add_enums(enum_descriptors, options) + enum_descriptors.each do |enum_descriptor| + @groups[:enum] << EnumGenerator.new(enum_descriptor, indent_level, options) + end + end + + def add_comment(type, message) + @comments[type] = message + end + + def add_extended_messages(extended_messages, skip_empty_fields = true) + extended_messages.each do |message_type, field_descriptors| + next if skip_empty_fields && field_descriptors.empty? + @groups[:extended_message] << ExtensionGenerator.new(message_type, field_descriptors, indent_level) + end + end + + def add_extension_fields(field_descriptors) + field_descriptors.each do |field_descriptor| + @groups[:extension_field] << FieldGenerator.new(field_descriptor, nil, indent_level) + end + end + + def add_extension_ranges(extension_ranges, &item_handler) + @groups[:extension_range] = extension_ranges + @handlers[:extension_range] = item_handler + end + + def add_header(type, message) + @headers[type] = message + end + + def add_message_declarations(descriptors) + descriptors.each do |descriptor| + # elide synthetic map entry messages (we handle map fields differently) + next if descriptor.options.try(:map_entry?) { false } + @groups[:message_declaration] << MessageGenerator.new(descriptor, indent_level, :declaration => true) + end + end + + def add_message_fields(field_descriptors, msg_descriptor) + field_descriptors.each do |field_descriptor| + @groups[:field] << FieldGenerator.new(field_descriptor, msg_descriptor, indent_level) + end + end + + def add_messages(descriptors, options = {}) + descriptors.each do |descriptor| + # elide synthetic map entry message (we handle map fields differently) + next if descriptor.options.try(:map_entry?) { false } + @groups[:message] << MessageGenerator.new(descriptor, indent_level, options) + end + end + + def add_services(service_descriptors) + service_descriptors.each do |service_descriptor| + @groups[:service] << ServiceGenerator.new(service_descriptor, indent_level) + end + end + + def compile + @order.each do |type| + items = @groups[type] + next if items.empty? + + item_handler = @handlers[type] + + item_header = @headers[type] + header(item_header) if item_header + + item_comment = @comments[type] + comment(item_comment) if item_comment + + items.each do |item| + if item_handler + puts item_handler.call(item) + else + print item.to_s + end + end + + puts if type == :message_declaration || type == :options + end + end + + def to_s + compile + print_contents + end + + end + end +end diff --git a/lib/protobuf/generators/message_generator.rb b/lib/protobuf/generators/message_generator.rb new file mode 100644 index 00000000..bbaf84d5 --- /dev/null +++ b/lib/protobuf/generators/message_generator.rb @@ -0,0 +1,104 @@ +require 'protobuf/generators/base' +require 'protobuf/generators/group_generator' + +module Protobuf + module Generators + class MessageGenerator < Base + + def initialize(descriptor, indent_level, options = {}) + super + @only_declarations = options.fetch(:declaration) { false } + @extension_fields = options.fetch(:extension_fields) { {} } + end + + def compile + run_once(:compile) do + if @only_declarations + compile_declaration + else + compile_message + end + end + end + + def compile_declaration + run_once(:compile_declaration) do + if printable? + print_class(descriptor.name, :message) do + group = GroupGenerator.new(current_indent) + group.add_enums(descriptor.enum_type, :namespace => type_namespace) + group.add_message_declarations(descriptor.nested_type) + print group.to_s + end + else + print_class(descriptor.name, :message) + end + end + end + + def compile_message + run_once(:compile_message) do + if printable? + print_class(descriptor.name, nil) do + group = GroupGenerator.new(current_indent) + group.add_messages(descriptor.nested_type, :extension_fields => @extension_fields, :namespace => type_namespace) + group.add_comment(:options, 'Message Options') + group.add_options(descriptor.options) if options? + group.add_message_fields(descriptor.field, descriptor) + self.class.validate_tags(fully_qualified_type_namespace, descriptor.field.map(&:number)) + + group.add_comment(:extension_range, 'Extension Fields') + group.add_extension_ranges(descriptor.extension_range) do |extension_range| + "extensions #{extension_range.start}...#{extension_range.end}" + end + + group.add_extension_fields(message_extension_fields) + + group.order = [:message, :options, :field, :extension_range, :extension_field] + print group.to_s + end + end + end + end + + private + + def extensions? + !message_extension_fields.empty? + end + + def fields? + descriptor.field.count > 0 + end + + def options? + descriptor.options + end + + def nested_enums? + descriptor.enum_type.count > 0 + end + + def nested_messages? + descriptor.nested_type.count > 0 + end + + def nested_types? + nested_enums? || nested_messages? + end + + def printable? + if @only_declarations + nested_types? + else + fields? || nested_messages? || extensions? || options? + end + end + + def message_extension_fields + @extension_fields.fetch(fully_qualified_type_namespace) { [] } + end + + end + end +end diff --git a/lib/protobuf/generators/option_generator.rb b/lib/protobuf/generators/option_generator.rb new file mode 100644 index 00000000..71cec42e --- /dev/null +++ b/lib/protobuf/generators/option_generator.rb @@ -0,0 +1,17 @@ +require 'protobuf/generators/base' + +module Protobuf + module Generators + class OptionGenerator < Base + def compile + run_once(:compile) do + descriptor.each_field.map do |field, value| + next unless descriptor.field?(field.name) + serialized_value = serialize_value(value) + puts "set_option #{field.fully_qualified_name.inspect}, #{serialized_value}" + end + end + end + end + end +end diff --git a/lib/protobuf/generators/printable.rb b/lib/protobuf/generators/printable.rb new file mode 100644 index 00000000..3d8fe770 --- /dev/null +++ b/lib/protobuf/generators/printable.rb @@ -0,0 +1,160 @@ +module Protobuf + module Generators + module Printable + + PARENT_CLASS_MESSAGE = "::Protobuf::Message".freeze + PARENT_CLASS_ENUM = "::Protobuf::Enum".freeze + PARENT_CLASS_SERVICE = "::Protobuf::Rpc::Service".freeze + + # Initialize the printer. + # Must be called by any class/module that includes the Printable module. + # + def init_printer(indent_level) + @io = ::StringIO.new + self.current_indent = indent_level.to_i + end + + protected + + attr_accessor :current_indent + + private + + # Print a one-line comment. + # + def comment(message) + puts "# #{message}" + end + + # Print a "header" comment. + # + # header("Lorem ipsum dolor") + # ## + # # Lorem ipsum dolor + # # + def header(message) + puts + puts "##" + puts "# #{message}" + puts "#" + end + + # Increase the indent level. An outdent will only occur if given a block + # (after the block is finished). + # + def indent + self.current_indent += 1 + yield + outdent + end + + # Take a string and upcase the first character of each namespace. + # Due to the nature of varying standards about how class/modules are named + # (e.g. CamelCase, Underscore_Case, SCREAMING_SNAKE_CASE), we only want + # to capitalize the first character to ensure ruby will treat the value + # as a constant. Otherwise we do not attempt to change the + # token's definition. + # + # modulize("foo.bar.Baz") -> "::Foo::Bar::Baz" + # modulize("foo.bar.baz") -> "::Foo::Bar::Baz" + # modulize("foo.bar.BAZ") -> "::Foo::Bar::BAZ" + # + def modulize(name) + name = name.gsub(/\./, '::') + name = name.gsub(/(^(?:::)?[a-z]|::[a-z])/, &:upcase) + name + end + + # Decrease the indent level. Cannot be negative. + # + def outdent + self.current_indent -= 1 unless current_indent.zero? + end + + # Return the parent class for a given type. + # Valid types are :message, :enum, and :service, otherwise an error + # will be thrown. + # + def parent_class(type) + case type + when :message then + PARENT_CLASS_MESSAGE + when :enum then + PARENT_CLASS_ENUM + when :service then + PARENT_CLASS_SERVICE + else + fail "Unknown parent class type #{type}: #{caller[0..5].join("\n")}" + end + end + + # Print a class or module block, indicated by type. + # If a class, can be given a parent class to inherit from. + # If a block is given, call the block from within an indent block. + # Otherwise, end the block on the same line. + # + def print_block(name, parent_klass, type) + name = modulize(name) + block_def = "#{type} #{name}" + block_def += " < #{parent_class(parent_klass)}" if parent_klass + + if block_given? + puts block_def + indent { yield } + puts "end" + puts + else + block_def += "; end" + puts block_def + end + end + + # Use print_block to print a class, with optional parent class + # to inherit from. Accepts a block for use with print_block. + # + def print_class(name, parent_klass, &block) + print_block(name, parent_klass, :class, &block) + end + + # Use print_block to print a module. + # Accepts a block for use with print_block. + # + def print_module(name, &block) + print_block(name, nil, :module, &block) + end + + # Print a file require. + # + # print_require('foo/bar/baz') -> "require 'foo/bar/baz'" + # + def print_require(file, relative = false) + puts "require#{'_relative' if relative} '#{file}'" + end + + # Puts the given message prefixed by the indent level. + # If no message is given print a newline. + # + def puts(message = nil) + if message + @io.puts((" " * current_indent) + message) + else + @io.puts + end + end + + # Print the given message raw, no indent. + # + def print(contents) + @io.print(contents) + end + + # Returns the contents of the underlying StringIO object. + # + def print_contents + @io.rewind + @io.read + end + + end + end +end diff --git a/lib/protobuf/generators/service_generator.rb b/lib/protobuf/generators/service_generator.rb new file mode 100644 index 00000000..f7e9febd --- /dev/null +++ b/lib/protobuf/generators/service_generator.rb @@ -0,0 +1,50 @@ +require 'protobuf/generators/base' +require 'protobuf/generators/option_generator' + +module Protobuf + module Generators + class ServiceGenerator < Base + + def compile + run_once(:compile) do + print_class(descriptor.name, :service) do + print OptionGenerator.new(descriptor.options, current_indent).to_s if descriptor.options + descriptor.method.each do |method_descriptor| + print_method(method_descriptor) + end + end + end + end + + private + + def print_method(method_descriptor) + request_klass = modulize(method_descriptor.input_type) + response_klass = modulize(method_descriptor.output_type) + name = ENV.key?('PB_USE_RAW_RPC_NAMES') ? method_descriptor.name : method_descriptor.name.underscore + options = {} + if method_descriptor.options + method_descriptor.options.each_field do |field_option| + option_value = method_descriptor.options[field_option.name] + next unless method_descriptor.options.field?(field_option.name) + options[field_option.fully_qualified_name] = serialize_value(option_value) + end + end + + rpc = "rpc :#{name}, #{request_klass}, #{response_klass}" + + if options.empty? + puts rpc + return + end + + puts rpc + " do" + options.each do |option_name, value| + indent { puts "set_option #{option_name.inspect}, #{value}" } + end + puts "end" + end + + end + end +end diff --git a/lib/protobuf/lifecycle.rb b/lib/protobuf/lifecycle.rb new file mode 100644 index 00000000..a17aac5c --- /dev/null +++ b/lib/protobuf/lifecycle.rb @@ -0,0 +1,33 @@ +module Protobuf + class Lifecycle + class << self + def register(event_name) + fail "Lifecycle register must have a block" unless block_given? + event_name = normalized_event_name(event_name) + + ::ActiveSupport::Notifications.subscribe(event_name) do |_name, _start, _finish, _id, args| + yield(*args) + end + end + alias :on register + + def trigger(event_name, *args) + event_name = normalized_event_name(event_name) + + ::ActiveSupport::Notifications.instrument(event_name, args) + end + + replacement = ::ActiveSupport::Notifications + + ::Protobuf.deprecator.deprecate_methods( + self, + :register => "#{replacement}.#{replacement.method(:subscribe).name}".to_sym, + :trigger => "#{replacement}.#{replacement.method(:instrument).name}".to_sym, + ) + + def normalized_event_name(event_name) + event_name.to_s.downcase + end + end + end +end diff --git a/lib/protobuf/logging.rb b/lib/protobuf/logging.rb new file mode 100644 index 00000000..59fc9997 --- /dev/null +++ b/lib/protobuf/logging.rb @@ -0,0 +1,39 @@ +require 'logger' + +module Protobuf + module Logging + def self.initialize_logger(log_target = $stdout, log_level = ::Logger::INFO) + @logger = Logger.new(log_target) + @logger.level = log_level + @logger + end + + def self.logger + defined?(@logger) ? @logger : initialize_logger + end + + class << self + attr_writer :logger + end + + def logger + ::Protobuf::Logging.logger + end + + def log_exception(ex) + logger.error { ex.message } + logger.error { ex.backtrace[0..5].join("\n") } + logger.debug { ex.backtrace.join("\n") } + end + + def log_signature + @_log_signature ||= "[#{self.class == Class ? name : self.class.name}]" + end + + def sign_message(message) + "#{log_signature} #{message}" + end + end +end + +# Inspired by [mperham](https://github.com/mperham/sidekiq) diff --git a/lib/protobuf/message.rb b/lib/protobuf/message.rb new file mode 100644 index 00000000..a13c0d19 --- /dev/null +++ b/lib/protobuf/message.rb @@ -0,0 +1,241 @@ +require 'protobuf/message/fields' +require 'protobuf/message/serialization' +require 'protobuf/varint' + +module Protobuf + class Message + + ## + # Includes & Extends + # + + extend ::Protobuf::Message::Fields + include ::Protobuf::Message::Serialization + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::MessageOptions } + + ## + # Class Methods + # + + def self.to_json + name + end + + ## + # Constructor + # + + def initialize(fields = {}) + @values = {} + fields.to_hash.each do |name, value| + set_field(name, value, true) + end + + yield self if block_given? + end + + ## + # Public Instance Methods + # + + def clear! + @values.delete_if do |_, value| + if value.is_a?(::Protobuf::Field::FieldArray) || value.is_a?(::Protobuf::Field::FieldHash) + value.clear + false + else + true + end + end + self + end + + def clone + copy_to(super, :clone) + end + + def dup + copy_to(super, :dup) + end + + # Iterate over every field, invoking the given block + # + def each_field + return to_enum(:each_field) unless block_given? + + self.class.all_fields.each do |field| + value = self[field.name] + yield(field, value) + end + end + + def each_field_for_serialization + _protobuf_message_unset_required_field_tags.each do |tag| + fail ::Protobuf::SerializationError, "Required field #{self.class.name}##{_protobuf_message_field[tag].name} does not have a value." + end + + @values.each_key do |fully_qualified_name| + field = _protobuf_message_field[fully_qualified_name] + yield(field, field.value_from_values_for_serialization(@values)) + end + end + + def field?(name) + field = _protobuf_message_field[name] + + if field + field.field?(@values) + else + false + end + end + alias :respond_to_has? field? + ::Protobuf.deprecator.define_deprecated_methods(self, :has_field? => :field?) + + def inspect + attrs = self.class.fields.map do |field| + [field.name, self[field.name].inspect].join('=') + end.join(' ') + + "#<#{self.class} #{attrs}>" + end + + def respond_to_has_and_present?(key) + field = _protobuf_message_field[key] + + if field + field.field_and_present?(@values) + else + false + end + end + + # Return a hash-representation of the given fields for this message type. + def to_hash + result = {} + + @values.each_key do |field_name| + field = _protobuf_message_field[field_name] + field.to_message_hash(@values, result) + end + + result + end + + def to_hash_with_string_keys + result = {} + + @values.each_key do |field_name| + field = _protobuf_message_field[field_name] + field.to_message_hash_with_string_key(@values, result) + end + + result + end + + def to_json(options = {}) + to_json_hash.to_json(options) + end + + # Return a hash-representation of the given fields for this message type that + # is safe to convert to JSON. + def to_json_hash + result = {} + + @values.each_key do |field_name| + value = self[field_name] + field = self.class.get_field(field_name, true) + + # NB: to_json_hash_value should come before json_encode so as to handle + # repeated fields without extra logic. + hashed_value = if value.respond_to?(:to_json_hash_value) + value.to_json_hash_value + elsif field.respond_to?(:json_encode) + field.json_encode(value) + else + value + end + + result[field.name] = hashed_value + end + + result + end + + def to_proto + self + end + + def ==(other) + return false unless other.is_a?(self.class) + each_field do |field, value| + return false unless value == other[field.name] + end + true + end + + def [](name) + field = _protobuf_message_field[name] + field.value_from_values(@values) + rescue # not having a field should be the exceptional state + raise if field + fail ArgumentError, "invalid field name=#{name.inspect}" + end + + def []=(name, value) + set_field(name, value, true) + end + + def set_field(name, value, ignore_nil_for_repeated, field = nil) + field ||= _protobuf_message_field[name] + + if field + field.set_field(@values, value, ignore_nil_for_repeated, self) + else + fail(::Protobuf::FieldNotDefinedError, name) unless ::Protobuf.ignore_unknown_fields? + end + end + + ## + # Instance Aliases + # + alias :to_hash_value to_hash + alias :to_json_hash_value to_json_hash + alias :to_proto_hash to_hash + alias :responds_to_has? respond_to_has? + alias :respond_to_and_has? respond_to_has? + alias :responds_to_and_has? respond_to_has? + alias :respond_to_has_present? respond_to_has_and_present? + alias :respond_to_and_has_present? respond_to_has_and_present? + alias :respond_to_and_has_and_present? respond_to_has_and_present? + alias :responds_to_has_present? respond_to_has_and_present? + alias :responds_to_and_has_present? respond_to_has_and_present? + alias :responds_to_and_has_and_present? respond_to_has_and_present? + + ## + # Private Instance Methods + # + + private + + def copy_to(object, method) + duplicate = proc do |obj| + case obj + when Message, String then obj.__send__(method) + else obj + end + end + + object.__send__(:initialize) + @values.each do |name, value| + if value.is_a?(::Protobuf::Field::FieldArray) + object[name].replace(value.map { |v| duplicate.call(v) }) + else + object[name] = duplicate.call(value) + end + end + object + end + + end +end diff --git a/lib/protobuf/message/decoder.rb b/lib/protobuf/message/decoder.rb deleted file mode 100644 index c8c9d250..00000000 --- a/lib/protobuf/message/decoder.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'protobuf/common/wire_type' -require 'protobuf/common/exceptions' - -module Protobuf - - module Decoder - - module_function - - # Read bytes from +stream+ and pass to +message+ object. - def decode(stream, message) - until stream.eof? - tag, wire_type = read_key(stream) - bytes = - case wire_type - when WireType::VARINT then - read_varint(stream) - when WireType::FIXED64 then - read_fixed64(stream) - when WireType::LENGTH_DELIMITED then - read_length_delimited(stream) - when WireType::START_GROUP then - read_start_group(stream) - when WireType::END_GROUP then - read_end_group(stream) - when WireType::FIXED32 then - read_fixed32(stream) - else - raise InvalidWireType, wire_type - end - message.set_field(tag, bytes) - end - message - end - - # Read key pair (tag and wire-type) from +stream+. - def read_key(stream) - bits = read_varint(stream) - wire_type = bits & 0x07 - tag = bits >> 3 - [tag, wire_type] - end - - # Read varint integer value from +stream+. - def read_varint(stream) - read_method = stream.respond_to?(:readbyte) ? :readbyte : :readchar - value = index = 0 - begin - byte = stream.__send__(read_method) - value |= (byte & 0x7f) << (7 * index) - index += 1 - end while (byte & 0x80).nonzero? - value - end - - # Read 32-bit string value from +stream+. - def read_fixed32(stream) - stream.read(4) - end - - # Read 64-bit string value from +stream+. - def read_fixed64(stream) - stream.read(8) - end - - # Read length-delimited string value from +stream+. - def read_length_delimited(stream) - value_length = read_varint(stream) - stream.read(value_length) - end - - # Not implemented. - def read_start_group(stream) - raise NotImplementedError, 'Group is deprecated.' - end - - # Not implemented. - def read_end_group(stream) - raise NotImplementedError, 'Group is deprecated.' - end - - end -end diff --git a/lib/protobuf/message/encoder.rb b/lib/protobuf/message/encoder.rb deleted file mode 100644 index 65557a43..00000000 --- a/lib/protobuf/message/encoder.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'protobuf/common/wire_type' -require 'protobuf/common/exceptions' - -module Protobuf - - module Encoder - - module_function - - # Encode +message+ and write to +stream+. - def encode(stream, message) - # FIXME make this not as ghetto - raise NotInitializedError, "Message %s is not initialized (one or more fields is improperly set): %s" % [message.class.name, JSON.parse(message.to_json)] unless message.initialized? - - message.each_field do |field, value| - next unless message.has_field?(field.name) - - if field.repeated? - if field.packed? - key = (field.tag << 3) | WireType::LENGTH_DELIMITED - packed_value = value.map {|val| field.encode(val) }.join - stream.write(Field::VarintField.encode(key)) - stream.write(Field::VarintField.encode(packed_value.size)) - stream.write(packed_value) - else - value.each do |val| - write_pair(stream, field, val) - end - end - else - write_pair(stream, field, value) - end - end - end - - # Encode key and value, and write to +stream+. - def write_pair(stream, field, value) - key = (field.tag << 3) | field.wire_type - key_bytes = Field::VarintField.encode(key) - stream.write(key_bytes) - bytes = field.encode(value) - stream.write(bytes) - end - - end -end diff --git a/lib/protobuf/message/enum.rb b/lib/protobuf/message/enum.rb deleted file mode 100644 index b3573081..00000000 --- a/lib/protobuf/message/enum.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'delegate' -require 'protobuf/descriptor/enum_descriptor' -require 'protobuf/message/protoable' - -module Protobuf - class Enum - class <" - end - end - -end diff --git a/lib/protobuf/message/extend.rb b/lib/protobuf/message/extend.rb deleted file mode 100644 index 3cc52181..00000000 --- a/lib/protobuf/message/extend.rb +++ /dev/null @@ -1,8 +0,0 @@ -require 'protobuf/message/message' - -module Protobuf - class Extend < Message - MIN = -1.0/0 - MAX = 1.0/0 - end -end diff --git a/lib/protobuf/message/field.rb b/lib/protobuf/message/field.rb deleted file mode 100644 index 6dbcd3f4..00000000 --- a/lib/protobuf/message/field.rb +++ /dev/null @@ -1,701 +0,0 @@ -require 'protobuf/common/util' -require 'protobuf/common/wire_type' -require 'protobuf/descriptor/field_descriptor' - -module Protobuf - module Field - - PREDEFINED_TYPES = [ - :double, :float, - :int32, :int64, - :uint32, :uint64, - :sint32, :sint64, - :fixed32, :fixed64, - :sfixed32, :sfixed64, - :string, :bytes, - :bool, - ].freeze - - def self.build(message_class, rule, type, name, tag, options={}) - field_class = \ - if PREDEFINED_TYPES.include?(type) - const_get("#{type.to_s.capitalize}Field") - else - FieldProxy - end - field_class.new(message_class, rule, type, name, tag, options) - end - - class BaseField - - def self.descriptor - @descriptor ||= Descriptor::FieldDescriptor.new - end - - def self.default - nil - end - - attr_reader :message_class, :rule, :type, :name, :tag, :default - attr_reader :default_value - - def descriptor - @descriptor ||= Descriptor::FieldDescriptor.new(self) - end - - def initialize(message_class, rule, type, name, tag, options) - @message_class, @rule, @type, @name, @tag = \ - message_class, rule, type, name, tag - - @default = options.delete(:default) - @extension = options.delete(:extension) - @packed = repeated? && options.delete(:packed) - unless options.empty? - warn "WARNING: Invalid options: #{options.inspect} (in #{@message_class.name.split('::').last}.#{@name})" - end - if packed? && ! [WireType::VARINT, WireType::FIXED32, WireType::FIXED64].include?(wire_type) - raise "Can't use packed encoding for `#{@type}' type" - end - - @default_value = \ - case @rule - when :repeated then - FieldArray.new(self).freeze - when :required then - nil - when :optional then - typed_default_value - end - - define_accessor - end - - def ready? - true - end - - def initialized?(message_instance) - value = message_instance.__send__(@name) - case @rule - when :required then - ! value.nil? && (! kind_of?(MessageField) || value.initialized?) - when :repeated then - value.all? {|msg| ! kind_of?(MessageField) || msg.initialized? } - when :optional then - value.nil? || ! kind_of?(MessageField) || value.initialized? - end - end - - # Decode +bytes+ and pass to +message_instance+. - def set(message_instance, bytes) - if packed? - array = message_instance.__send__(@name) - method = \ - case wire_type - when WireType::FIXED32 then :read_fixed32 - when WireType::FIXED64 then :read_fixed64 - when WireType::VARINT then :read_varint - end - stream = StringIO.new(bytes) - until stream.eof? - array << decode(Decoder.__send__(method, stream)) - end - else - value = decode(bytes) - if repeated? - message_instance.__send__(@name) << value - else - message_instance.__send__("#{@name}=", value) - end - end - end - - # Decode +bytes+ and return a field value. - def decode(bytes) - raise NotImplementedError, "#{self.class.name}\#decode" - end - - # Encode +value+ and return a byte string. - def encode(value) - raise NotImplementedError, "#{self.class.name}\#encode" - end - - # Merge +value+ with +message_instance+. - def merge(message_instance, value) - if repeated? - merge_array(message_instance, value) - else - merge_value(message_instance, value) - end - end - - # Is this a repeated field? - def repeated? - @rule == :repeated - end - - # Is this a required field? - def required? - @rule == :required - end - - # Is this a optional field? - def optional? - @rule == :optional - end - - # Is this a packed repeated field? - def packed? - !!@packed - end - - # Upper limit for this field. - def max - self.class.max - end - - # Lower limit for this field. - def min - self.class.min - end - - # Is a +value+ acceptable for this field? - def acceptable?(value) - true - end - - def to_s - "#{@rule} #{@type} #{@name} = #{@tag} #{@default ? "[default=#{@default.inspect}]" : ''}" - end - - private - - def define_accessor - define_getter - if repeated? - define_array_setter - else - define_setter - end - end - - def define_getter - field = self - @message_class.class_eval do - define_method(field.name) do - if @values.has_key?(field.name) - @values[field.name] - else - field.default_value - end - end - end - end - - def define_setter - field = self - @message_class.class_eval do - define_method("#{field.name}=") do |val| - if val.nil? - @values.delete(field.name) - elsif field.acceptable?(val) - @values[field.name] = val - end - end - end - end - - def define_array_setter - field = self - @message_class.class_eval do - define_method("#{field.name}=") do |val| - @values[field.name].replace(val) - end - end - end - - def merge_array(message_instance, value) - message_instance.__send__(@name).concat(value) - end - - def merge_value(message_instance, value) - message_instance.__send__("#{@name}=", value) - end - - def typed_default_value - if @default.nil? - self.class.default - else - @default - end - end - - end # BaseField - - - class FieldProxy - - def initialize(message_class, rule, type, name, tag, options) - @message_class, @rule, @type, @name, @tag, @options = - message_class, rule, type, name, tag, options - end - - def ready? - false - end - - def setup - type = typename_to_class(@message_class, @type) - field_class = \ - if type < Enum - EnumField - elsif type < Message - MessageField - else - raise TypeError, type.inspect - end - field_class.new(@message_class, @rule, type, @name, @tag, @options) - end - - private - - def typename_to_class(message_class, type) - names = type.to_s.split('::').map {|s| Util.camelize(s) } - outer = message_class.to_s.split('::') - args = (Object.method(:const_defined?).arity == 1) ? [] : [false] - while - mod = outer.empty? ? Object : eval(outer.join('::')) - mod = names.inject(mod) {|m, s| - m && m.const_defined?(s, *args) && m.const_get(s) - } - break if mod - raise NameError.new("type not found: #{type}", type) if outer.empty? - outer.pop - end - mod - end - - end - - class FieldArray < Array - - def initialize(field) - @field = field - end - - def []=(nth, val) - super(nth, normalize(val)) - end - - def <<(val) - super(normalize(val)) - end - - def push(val) - super(normalize(val)) - end - - def unshift(val) - super(normalize(val)) - end - - def replace(val) - raise TypeError unless val.is_a?(Array) - val = val.map {|v| normalize(v)} - super(val) - end - - def to_s - "[#{@field.name}]" - end - - private - - def normalize(val) - raise TypeError unless @field.acceptable?(val) - if @field.is_a?(MessageField) && val.is_a?(Hash) - @field.type.new(val) - else - val - end - end - - end - - # Field class for +bytes+ type. - class BytesField < BaseField - def self.default - '' - end - - def wire_type - WireType::LENGTH_DELIMITED - end - - def acceptable?(val) - raise TypeError unless val.instance_of?(String) - true - end - - def decode(bytes) - bytes.force_encoding('ASCII-8BIT') if bytes.respond_to?(:force_encoding) - bytes - end - - def encode(value) - value = value.dup - value.force_encoding('ASCII-8BIT') if value.respond_to?(:force_encoding) - string_size = VarintField.encode(value.size) - string_size << value - end - end - - class StringField < BytesField - def decode(bytes) - bytes.force_encoding('UTF-8') if bytes.respond_to?(:force_encoding) - bytes - end - end - - class VarintField < BaseField - INT32_MAX = 2**31 - 1 - INT32_MIN = -2**31 - INT64_MAX = 2**63 - 1 - INT64_MIN = -2**63 - UINT32_MAX = 2**32 - 1 - UINT64_MAX = 2**64 - 1 - - class <>= 7 - end - bytes[-1] &= 0x7f - bytes.pack('C*') - end - end - - def wire_type - WireType::VARINT - end - - def decode(value) - value - end - - def encode(value) - self.class.encode(value) - end - - def acceptable?(val) - raise TypeError, val.class.name unless val.is_a?(Integer) - raise RangeError if val < min || max < val - true - end - end - - # Base class for int32 and int64 - class IntegerField < VarintField - def encode(value) - # original Google's library uses 64bits integer for negative value - VarintField.encode(value & 0xffff_ffff_ffff_ffff) - end - - def decode(value) - value -= 0x1_0000_0000_0000_0000 if (value & 0x8000_0000_0000_0000).nonzero? - value - end - end - - class Int32Field < IntegerField - def self.max; INT32_MAX; end - def self.min; INT32_MIN; end - end - - class Int64Field < IntegerField - def self.max; INT64_MAX; end - def self.min; INT64_MIN; end - end - - class Uint32Field < VarintField - def self.max; UINT32_MAX; end - def self.min; 0; end - end - - class Uint64Field < VarintField - def self.max; UINT64_MAX; end - def self.min; 0; end - end - - # Base class for sint32 and sint64 - class SignedIntegerField < VarintField - def decode(value) - if (value & 1).zero? - value >> 1 # positive value - else - ~value >> 1 # negative value - end - end - - def encode(value) - if value >= 0 - VarintField.encode(value << 1) - else - VarintField.encode(~(value << 1)) - end - end - end - - class Sint32Field < SignedIntegerField - def self.max; INT32_MAX; end - def self.min; INT32_MIN; end - end - - class Sint64Field < SignedIntegerField - def self.max; INT64_MAX; end - def self.min; INT64_MIN; end - end - - class FloatField < BaseField - def self.default; 0.0; end - def self.max; 1.0/0; end - def self.min; -1.0/0; end - - def wire_type - WireType::FIXED32 - end - - def decode(bytes) - bytes.unpack('e').first - end - - def encode(value) - [value].pack('e') - end - - def acceptable?(val) - raise TypeError, val.class.name unless val.is_a?(Numeric) - raise RangeError if val < min || max < val - true - end - end - - class DoubleField < FloatField - def wire_type - WireType::FIXED64 - end - - def decode(bytes) - bytes.unpack('E').first - end - - def encode(value) - [value].pack('E') - end - end - - class Fixed32Field < Uint32Field - def wire_type - WireType::FIXED32 - end - - def decode(bytes) - bytes.unpack('V').first - end - - def encode(value) - [value].pack('V') - end - end - - class Fixed64Field < Uint64Field - def wire_type - WireType::FIXED64 - end - - def decode(bytes) - # we don't use 'Q' for pack/unpack. 'Q' is machine-dependent. - values = bytes.unpack('VV') - values[0] + (values[1] << 32) - end - - def encode(value) - # we don't use 'Q' for pack/unpack. 'Q' is machine-dependent. - [value & 0xffff_ffff, value >> 32].pack('VV') - end - end - - class Sfixed32Field < Int32Field - def wire_type - WireType::FIXED32 - end - - def decode(bytes) - value = bytes.unpack('V').first - value -= 0x1_0000_0000 if (value & 0x8000_0000).nonzero? - value - end - - def encode(value) - [value].pack('V') - end - end - - class Sfixed64Field < Int64Field - def wire_type - WireType::FIXED64 - end - - def decode(bytes) - # we don't use 'Q' for pack/unpack. 'Q' is machine-dependent. - values = bytes.unpack('VV') - value = values[0] + (values[1] << 32) - value -= 0x1_0000_0000_0000_0000 if (value & 0x8000_0000_0000_0000).nonzero? - value - end - - def encode(value) - # we don't use 'Q' for pack/unpack. 'Q' is machine-dependent. - [value & 0xffff_ffff, value >> 32].pack('VV') - end - end - - class BoolField < VarintField - def self.default - false - end - - def acceptable?(val) - raise TypeError unless [true, false].include?(val) - true - end - - def decode(value) - value == 1 - end - - def encode(value) - [value ? 1 : 0].pack('C') - end - end - - - class MessageField < BaseField - def wire_type - WireType::LENGTH_DELIMITED - end - - def acceptable?(val) - raise TypeError unless val.instance_of?(type) || val.instance_of?(Hash) - true - end - - def decode(bytes) - message = type.new - message.parse_from_string(bytes) - message - end - - def encode(value) - bytes = value.serialize_to_string - result = VarintField.encode(bytes.size) - result << bytes - end - - private - - def define_setter - field = self - @message_class.class_eval do - define_method("#{field.name}=") do |val| - case val - when nil then - @values.delete(field.name) - when Hash then - @values[field.name] = field.type.new(val) - when field.type then - @values[field.name] = val - else - raise TypeError, "Expected value of type '#{field.type}', but got '#{val.class}'" - end - end - end - end - - def merge_value(message_instance, value) - message_instance.__send__(@name).merge_from(value) - end - end - - - class EnumField < VarintField - def acceptable?(val) - case val - when Symbol then - raise TypeError unless @type.const_defined?(val) - when EnumValue then - raise TypeError if val.parent_class != @type - else - raise TypeError unless @type.valid_tag?(val) - end - true - end - - def encode(value) - super(value.to_i) - end - - private - - def typed_default_value - if @default.is_a?(Symbol) - @type.const_get(@default) - else - self.class.default - end - end - - def define_setter - field = self - @message_class.class_eval do - define_method("#{field.name}=") do |val| - if val.nil? - @values.delete(field.name) - else - val = \ - case val - when Symbol then - field.type.const_get(val) rescue nil - when Integer then - field.type.const_get(field.type.name_by_value(val)) rescue nil - when EnumValue then - raise TypeError, "Invalid value: #{val.inspect} for #{field.name}" if val.parent_class != field.type - val - end - raise TypeError, "Invalid value: #{val.inspect} for #{field.name}" unless val - - @values[field.name] = val - end - end - end - end - - end - end -end diff --git a/lib/protobuf/message/fields.rb b/lib/protobuf/message/fields.rb new file mode 100644 index 00000000..d07e1eff --- /dev/null +++ b/lib/protobuf/message/fields.rb @@ -0,0 +1,233 @@ +require "set" + +module Protobuf + class Message + module Fields + + ACCESSOR_SUFFIXES = ["", "=", "!", "?"].freeze + + def self.extended(other) + other.extend(ClassMethods) + ::Protobuf.deprecator.define_deprecated_methods( + other.singleton_class, + :get_ext_field_by_name => :get_extension_field, + :get_ext_field_by_tag => :get_extension_field, + :get_field_by_name => :get_field, + :get_field_by_tag => :get_field, + ) + end + + module ClassMethods + def inherited(subclass) + inherit_fields!(subclass) + subclass.const_set("PROTOBUF_MESSAGE_REQUIRED_FIELD_TAGS", subclass.required_field_tags) + subclass.const_set("PROTOBUF_MESSAGE_GET_FIELD", subclass.field_store) + subclass.class_eval <<-RUBY, __FILE__, __LINE__ + def _protobuf_message_field + PROTOBUF_MESSAGE_GET_FIELD + end + + def _protobuf_message_unset_required_field_tags + @_protobuf_message_unset_required_field_tags ||= PROTOBUF_MESSAGE_REQUIRED_FIELD_TAGS.dup + end + RUBY + end + + ## + # Field Definition Methods + # + + # Define an optional field. + # + def optional(type_class, name, tag, options = {}) + define_field(:optional, type_class, name, tag, options) + end + + # Define a repeated field. + # + def repeated(type_class, name, tag, options = {}) + define_field(:repeated, type_class, name, tag, options) + end + + # Define a required field. + # + def required(type_class, name, tag, options = {}) + required_field_tags << tag + define_field(:required, type_class, name, tag, options) + end + + # Define a map field. + # + def map(key_type_class, value_type_class, name, tag, options = {}) + # manufacture a message that represents the map entry, used for + # serialization and deserialization + entry_type = Class.new(::Protobuf::Message) do + set_option :map_entry, true + optional key_type_class, :key, 1 + optional value_type_class, :value, 2 + end + define_field(:repeated, entry_type, name, tag, options) + end + + # Define an extension range. + # + def extensions(range) + extension_ranges << range + end + + ## + # Field Access Methods + # + def all_fields + @all_fields ||= field_store.values.uniq.sort_by(&:tag) + end + + def extension_fields + @extension_fields ||= all_fields.select(&:extension?) + end + + def extension_ranges + @extension_ranges ||= [] + end + + def required_field_tags + @required_field_tags ||= [] + end + + def extension_tag?(tag) + tag.respond_to?(:to_i) && get_extension_field(tag).present? + end + + def field_store + @field_store ||= {} + end + + def fields + @fields ||= all_fields.reject(&:extension?) + end + + def field_tag?(tag, allow_extension = false) + tag.respond_to?(:to_i) && get_field(tag, allow_extension).present? + end + + def get_extension_field(name_or_tag) + field = field_store[name_or_tag] + field if field.try(:extension?) { false } + end + + def get_field(name_or_tag, allow_extension = false) + field = field_store[name_or_tag] + + if field && (allow_extension || !field.extension?) + field + else + nil + end + end + + def define_field(rule, type_class, fully_qualified_field_name, tag, options) + raise_if_tag_collision(tag, fully_qualified_field_name) + raise_if_name_collision(fully_qualified_field_name) + + # Determine appropirate accessor for fields depending on name collisions via extensions: + + # Case 1: Base field = "string_field" and no extensions of the same name + # Result: + # message.string_field #=> @values["string_field"] + # message[:string_field] #=> @values["string_field"] + # message['string_field'] #=> @values["string_field"] + + # Case 2: Base field = "string_field" and extension 1 = ".my_package.string_field", extension N = ".package_N.string_field"... + # Result: + # message.string_field #=> @values["string_field"] + # message[:string_field] #=> @values["string_field"] + # message['string_field'] #=> @values["string_field"] + # message[:'.my_package.string_field'] #=> @values[".my_package.string_field"] + # message['.my_package.string_field'] #=> @values[".my_package.string_field"] + + # Case 3: No base field, extension 1 = ".my_package.string_field", extension 2 = ".other_package.string_field", extension N... + # Result: + # message.string_field #=> raise NoMethodError (no simple accessor allowed) + # message[:string_field] #=> raise NoMethodError (no simple accessor allowed) + # message['string_field'] #=> raise NoMethodError (no simple accessor allowed) + # message[:'.my_package.string_field'] #=> @values[".my_package.string_field"] + # message['.my_package.string_field'] #=> @values[".my_package.string_field"] + # message[:'.other_package.string_field'] #=> @values[".other_package.string_field"] + # message['.other_package.string_field'] #=> @values[".other_package.string_field"] + + # Case 4: No base field, extension = ".my_package.string_field", no other extensions + # Result: + # message.string_field #=> @values[".my_package.string_field"] + # message[:string_field] #=> @values[".my_package.string_field"] + # message['string_field'] #=> @values[".my_package.string_field"] + # message[:'.my_package.string_field'] #=> @values[".my_package.string_field"] + # message[:'.my_package.string_field'] #=> @values[".my_package.string_field"] + + simple_name = + if options[:extension] + base_name = fully_qualified_field_name.to_s.split('.').last.to_sym + if field_store[base_name] + # Case 3 + if field_store[base_name].extension? + remove_existing_accessors(base_name) + end + nil + # Case 4 + else + base_name + end + else + # Case 1 + fully_qualified_field_name + end + + field = ::Protobuf::Field.build(self, rule, type_class, fully_qualified_field_name, + tag, simple_name, options) + field_store[tag] = field + field_store[fully_qualified_field_name.to_sym] = field + field_store[fully_qualified_field_name.to_s] = field + if simple_name && simple_name != fully_qualified_field_name + field_store[simple_name.to_sym] = field + field_store[simple_name.to_s] = field + end + # defining a new field for the message will cause cached @all_fields, @extension_fields, + # and @fields to be incorrect; reset them + @all_fields = @extension_fields = @fields = nil + end + + def remove_existing_accessors(accessor) + field_store.delete(accessor.to_sym).try(:fully_qualified_name_only!) + field_store.delete(accessor.to_s) + ACCESSOR_SUFFIXES.each do |modifier| + begin + remove_method("#{accessor}#{modifier}") + # rubocop: disable Lint/HandleExceptions + rescue NameError + # Do not remove the method + end + end + end + + def raise_if_tag_collision(tag, field_name) + if get_field(tag, true) + fail TagCollisionError, %(Field number #{tag} has already been used in "#{name}" by field "#{field_name}".) + end + end + + def raise_if_name_collision(field_name) + if get_field(field_name, true) + fail DuplicateFieldNameError, %(Field name #{field_name} has already been used in "#{name}".) + end + end + + def inherit_fields!(subclass) + instance_variables.each do |iv| + subclass.instance_variable_set(iv, instance_variable_get(iv)) + end + end + private :inherit_fields! + + end + end + end +end diff --git a/lib/protobuf/message/message.rb b/lib/protobuf/message/message.rb deleted file mode 100644 index d06e41d7..00000000 --- a/lib/protobuf/message/message.rb +++ /dev/null @@ -1,400 +0,0 @@ -require 'protobuf/descriptor/descriptor' -require 'protobuf/message/decoder' -require 'protobuf/message/encoder' -require 'protobuf/message/field' -require 'protobuf/message/protoable' -require 'json' - -module Protobuf - OPTIONS = {} - - class Message - - class ExtensionFields < Hash - def initialize(key_range=0..-1) - @key_range = key_range - end - - def []=(key, value) - raise RangeError, "#{key} is not in #{@key_range}" unless @key_range.include?(key) - super - end - - def include_tag?(tag) - @key_range.include?(tag) - end - end - - class << self - include Protoable - - # Reserve field numbers for extensions. Don't use this method directly. - def extensions(range) - @extension_fields = ExtensionFields.new(range) - end - - # Define a required field. Don't use this method directly. - def required(type, name, tag, options={}) - define_field(:required, type, name, tag, options) - end - - # Define a optional field. Don't use this method directly. - def optional(type, name, tag, options={}) - define_field(:optional, type, name, tag, options) - end - - # Define a repeated field. Don't use this method directly. - def repeated(type, name, tag, options={}) - define_field(:repeated, type, name, tag, options) - end - - # Define a field. Don't use this method directly. - def define_field(rule, type, fname, tag, options) - field_hash = options[:extension] ? extension_fields : fields - if field_hash.keys.include?(tag) - raise TagCollisionError, %!{Field number #{tag} has already been used in "#{self.name}" by field "#{fname}".! - end - field_hash[tag] = Field.build(self, rule, type, fname, tag, options) - end - - def extension_tag?(tag) - extension_fields.include_tag?(tag) - end - - # A collection of field object. - def fields - @fields ||= {} - end - - # An extension field object. - def extension_fields - @extension_fields ||= ExtensionFields.new - end - - # Find a field object by +name+. - def get_field_by_name(name) - name = name.to_sym - fields.values.find {|field| field.name == name} - end - - # Find a field object by +tag+ number. - def get_field_by_tag(tag) - fields[tag] - end - - # Find a field object by +tag_or_name+. - def get_field(tag_or_name) - case tag_or_name - when Integer then get_field_by_tag(tag_or_name) - when String, Symbol then get_field_by_name(tag_or_name) - else raise TypeError, tag_or_name.class - end - end - - #TODO merge to get_field_by_name - def get_ext_field_by_name(name) - name = name.to_sym - extension_fields.values.find {|field| field.name == name} - end - - #TODO merge to get_field_by_tag - def get_ext_field_by_tag(tag) - extension_fields[tag] - end - - #TODO merge to get_field - def get_ext_field(tag_or_name) - case tag_or_name - when Integer then get_ext_field_by_tag(tag_or_name) - when String, Symbol then get_ext_field_by_name(tag_or_name) - else raise TypeError, tag_or_name.class - end - end - - def descriptor - @descriptor ||= Descriptor::Descriptor.new(self) - end - end - - def initialize(values={}) - @values = {} - - self.class.fields.each do |tag, field| - unless field.ready? - field = field.setup - self.class.class_eval {@fields[tag] = field} - end - if field.repeated? - @values[field.name] = Field::FieldArray.new(field) - end - end - - # TODO - self.class.extension_fields.each do |tag, field| - unless field.ready? - field = field.setup - self.class.class_eval {@extension_fields[tag] = field} - end - if field.repeated? - @values[field.name] = Field::FieldArray.new(field) - end - end - - values.each {|tag, val| self[tag] = val} - end - - def initialized? - fields.all? {|tag, field| field.initialized?(self) } && \ - extension_fields.all? {|tag, field| field.initialized?(self) } - end - - def has_field?(tag_or_name) - field = get_field(tag_or_name) || get_ext_field(tag_or_name) - raise ArgumentError, "unknown field: #{tag_or_name.inspect}" unless field - @values.has_key?(field.name) - end - - def ==(obj) - return false unless obj.is_a?(self.class) - each_field do |field, value| - return false unless value == obj.__send__(field.name) - end - true - end - - def clear! - @values.delete_if do |_, value| - if value.is_a?(Field::FieldArray) - value.clear - false - else - true - end - end - self - end - - def dup - copy_to(super, :dup) - end - - def clone - copy_to(super, :clone) - end - - def copy_to(object, method) - duplicate = proc {|obj| - case obj - when Message, String then obj.__send__(method) - else obj - end - } - - object.__send__(:initialize) - @values.each do |name, value| - if value.is_a?(Field::FieldArray) - object.__send__(name).replace(value.map {|v| duplicate.call(v)}) - else - object.__send__("#{name}=", duplicate.call(value)) - end - end - object - end - private :copy_to - - def inspect(indent=0) - result = [] - i = ' ' * indent - field_value_to_string = lambda {|field, value| - result << \ - if field.optional? && ! has_field?(field.name) - '' - else - case field - when Field::MessageField then - if value.nil? - "#{i}#{field.name} {}\n" - else - "#{i}#{field.name} {\n#{value.inspect(indent + 1)}#{i}}\n" - end - when Field::EnumField then - if value.is_a?(EnumValue) - "#{i}#{field.name}: #{value.name}\n" - else - "#{i}#{field.name}: #{field.type.name_by_value(value)}\n" - end - else - "#{i}#{field.name}: #{value.inspect}\n" - end - end - } - each_field do |field, value| - if field.repeated? - value.each do |v| - field_value_to_string.call(field, v) - end - else - field_value_to_string.call(field, value) - end - end - result.join - end - - def parse_from_string(string) - parse_from(StringIO.new(string)) - end - - def parse_from_file(filename) - if filename.is_a?(File) - parse_from(filename) - else - File.open(filename, 'rb') do |f| - parse_from(f) - end - end - end - - def parse_from(stream) - Decoder.decode(stream, self) - end - - def serialize_to_string(string='') - io = StringIO.new(string) - serialize_to(io) - result = io.string - result.force_encoding('ASCII-8BIT') if result.respond_to?(:force_encoding) - result - end - alias to_s serialize_to_string - - def serialize_to_file(filename) - if filename.is_a?(File) - serialize_to(filename) - else - File.open(filename, 'wb') do |f| - serialize_to(f) - end - end - end - - def serialize_to(stream) - Encoder.encode(stream, self) - end - - def merge_from(message) - # TODO - fields.each {|tag, field| merge_field(tag, message.__send__(field.name))} - extension_fields.each {|tag, field| merge_field(tag, message.__send__(field.name))} - end - - def set_field(tag, bytes) - field = (get_field_by_tag(tag) || get_ext_field_by_tag(tag)) - field.set(self, bytes) if field - end - - def merge_field(tag, value) - #get_field_by_tag(tag).merge self, bytes #TODO - (get_field_by_tag(tag) || get_ext_field_by_tag(tag)).merge(self, value) - end - - def [](tag_or_name) - if field = get_field(tag_or_name) || get_ext_field(tag_or_name) - __send__(field.name) - else - raise NoMethodError, "No such field: #{tag_or_name.inspect}" - end - end - - def []=(tag_or_name, value) - if field = get_field(tag_or_name) || get_ext_field(tag_or_name) - __send__("#{field.name}=", value) - else - raise NoMethodError, "No such field: #{tag_or_name.inspect}" - end - end - - # Returns a hash; which key is a tag number, and value is a field object. - def fields - self.class.fields - end - - # Returns field object or +nil+. - def get_field_by_name(name) - self.class.get_field_by_name(name) - end - - # Returns field object or +nil+. - def get_field_by_tag(tag) - self.class.get_field_by_tag(tag) - end - - # Returns field object or +nil+. - def get_field(tag_or_name) - self.class.get_field(tag_or_name) - end - - # Returns extension fields. See Message#fields method. - def extension_fields - self.class.extension_fields - end - - def get_ext_field_by_name(name) # :nodoc: - self.class.get_ext_field_by_name(name) - end - - def get_ext_field_by_tag(tag) # :nodoc: - self.class.get_ext_field_by_tag(tag) - end - - def get_ext_field(tag_or_name) # :nodoc: - self.class.get_ext_field(tag_or_name) - end - - # Iterate over a field collection. - # message.each_field do |field_object, value| - # # do something - # end - def each_field - fields.merge(extension_fields).sort_by {|tag, _| tag}.each do |_, field| - value = __send__(field.name) - yield(field, value) - end - end - - def to_hash - result = {} - build_value = lambda {|field, value| - if !field.optional? || (field.optional? && has_field?(field.name)) - case field - when Field::MessageField then - value.to_hash - when Field::EnumField then - if value.is_a?(EnumValue) - value.to_i - elsif value.is_a?(Symbol) - field.type[value].to_i - else - value - end - else - value - end - end - } - each_field do |field, value| - if field.repeated? - result[field.name] = value.map do |v| - build_value.call(field, v) - end - else - result[field.name] = build_value.call(field, value) - end - end - result - end - - def to_json - to_hash.to_json - end - end -end diff --git a/lib/protobuf/message/protoable.rb b/lib/protobuf/message/protoable.rb deleted file mode 100644 index 3ef8b95c..00000000 --- a/lib/protobuf/message/protoable.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Protobuf - module Protoable - def defined_filenames - @defined_filenames ||= [] - end - - def defined_in(filename) - path = File.expand_path(filename) - defined_filenames << path unless defined_filenames.include?(path) - end - - def proto_filenames - defined_filenames.map do |filename| - retrieve_header(File.read(filename)).first - end - end - - def proto_contents - #TODO: temporary implementation because the result includes not only this message but also all messages - ret = {} - defined_filenames.each do |filename| - header = retrieve_header(File.read(filename)) - ret[header.first] = header.last - end - ret - end - - def retrieve_header(contents) - if contents =~ /### Generated by rprotoc\. DO NOT EDIT!\n### \n((# .*\n)+)/ - proto_filename = $1 - proto_contents = $2.gsub(/^# /, '') - [proto_filename, proto_contents] - else - [nil, nil] - end - end - end -end diff --git a/lib/protobuf/message/serialization.rb b/lib/protobuf/message/serialization.rb new file mode 100644 index 00000000..9da948ca --- /dev/null +++ b/lib/protobuf/message/serialization.rb @@ -0,0 +1,85 @@ +require 'stringio' +require 'protobuf/decoder' +require 'protobuf/encoder' + +module Protobuf + class Message + module Serialization + + module ClassMethods + def decode(bytes) + new.decode(bytes) + end + + def decode_from(stream) + new.decode_from(stream) + end + + # Create a new object with the given values and return the encoded bytes. + def encode(fields = {}) + new(fields).encode + end + end + + def self.included(other) + other.extend(ClassMethods) + end + + ## + # Instance Methods + # + + # Decode the given non-stream bytes into this message. + # + def decode(bytes) + decode_from(::StringIO.new(bytes)) + end + + # Decode the given stream into this message. + # + def decode_from(stream) + ::Protobuf::Decoder.decode_each_field(stream) do |tag, bytes| + set_field_bytes(tag, bytes) + end + + self + end + + # Encode this message + # + def encode + stream = ::StringIO.new + stream.set_encoding(::Protobuf::Field::BytesField::BYTES_ENCODING) + encode_to(stream) + stream.string + end + + # Encode this message to the given stream. + # + def encode_to(stream) + ::Protobuf::Encoder.encode(self, stream) + end + + ## + # Instance Aliases + # + alias :parse_from_string decode + alias :deserialize decode + alias :parse_from decode_from + alias :deserialize_from decode_from + alias :to_s encode + alias :bytes encode + alias :serialize encode + alias :serialize_to_string encode + alias :serialize_to encode_to + + private + + def set_field_bytes(tag, bytes) + field = _protobuf_message_field[tag] + field.set(self, bytes) if field + end + + end + end +end diff --git a/lib/protobuf/optionable.rb b/lib/protobuf/optionable.rb new file mode 100644 index 00000000..34b00c93 --- /dev/null +++ b/lib/protobuf/optionable.rb @@ -0,0 +1,70 @@ +module Protobuf + module Optionable + module ClassMethods + def get_option(name) + name = name.to_s + option = optionable_descriptor_class.get_field(name, true) + fail ArgumentError, "invalid option=#{name}" unless option + unless option.fully_qualified_name.to_s == name + # Eventually we'll deprecate the use of simple names of fields completely, but for now make sure people + # are accessing options correctly. We allow simple names in other places for backwards compatibility. + fail ArgumentError, "must access option using its fully qualified name: #{option.fully_qualified_name.inspect}" + end + value = + if @_optionable_options.try(:key?, name) + @_optionable_options[name] + else + option.default_value + end + if option.type_class < ::Protobuf::Message + option.type_class.new(value) + else + value + end + end + + def get_option!(name) + get_option(name) if @_optionable_options.try(:key?, name.to_s) + end + + private + + def set_option(name, value = true) + @_optionable_options ||= {} + @_optionable_options[name.to_s] = value + end + end + + def get_option(name) + self.class.get_option(name) + end + + def get_option!(name) + self.class.get_option!(name) + end + + def self.inject(base_class, extend_class = true, &block) + unless block_given? + fail ArgumentError, 'missing option class block (e.g: ::Google::Protobuf::MessageOptions)' + end + if extend_class + # Check if optionable_descriptor_class is already defined and short circuit if so. + # File options are injected per module, and since a module can be defined more than once, + # we will get a warning if we try to define optionable_descriptor_class twice. + if base_class.respond_to?(:optionable_descriptor_class) + # Don't define optionable_descriptor_class twice + return if base_class.optionable_descriptor_class == block.call + + fail 'A class is being defined with two different descriptor classes, something is very wrong' + end + + base_class.extend(ClassMethods) + base_class.__send__(:include, self) + base_class.define_singleton_method(:optionable_descriptor_class, block) + else + base_class.__send__(:include, ClassMethods) + base_class.module_eval { define_method(:optionable_descriptor_class, block) } + end + end + end +end diff --git a/lib/protobuf/rpc/buffer.rb b/lib/protobuf/rpc/buffer.rb index de494ebe..afadec42 100644 --- a/lib/protobuf/rpc/buffer.rb +++ b/lib/protobuf/rpc/buffer.rb @@ -1,37 +1,41 @@ module Protobuf module Rpc class Buffer - - attr_accessor :mode - attr_reader :data, :size - - MODES = [:read, :write] - - def initialize(mode=:read, data='') - @data = data.is_a?(Protobuf::Message) ? data.serialize_to_string : data.to_s + + attr_accessor :mode, :data, :size + + MODES = [:read, :write].freeze + + # constantize this so we don't re-initialize the regex every time we need it + SIZE_REGEX = /^\d+-/ + + def initialize(mode = :read) @flush = false + @data = "" + @size = 0 self.mode = mode end - + def mode=(mode) - if MODES.include?(mode) - @mode = mode - else - @mode = :read - end + @mode = + if MODES.include?(mode) + mode + else + :read + end end - - def write(force_mode=true) - if force_mode and reading? - mode = :write - elsif not force_mode and reading? - raise = 'You chose to write the buffer when in read mode' + + def write(force_mode = true) + if force_mode && reading? + self.mode = :write + elsif !force_mode && reading? + fail 'You chose to write the buffer when in read mode' end - + @size = @data.length - '%d-%s' % [@size, @data] + "#{@size}-#{@data}" end - + def <<(data) @data << data if reading? @@ -39,36 +43,36 @@ def <<(data) check_for_flush end end - + + def set_data(data) # rubocop:disable Naming/AccessorMethodName + @data = data.to_s + @size = @data.size + end + def reading? mode == :read end - + def writing? mode == :write end - + def flushed? @flush end - - private - - def get_data_size - if @size.nil? - sliced_size = @data.slice! /^\d+-/ - unless sliced_size.nil? - @size = sliced_size.gsub(/-/, '').to_i - end + + def get_data_size # rubocop:disable Naming/AccessorMethodName + if @size == 0 || @data.match(SIZE_REGEX) + sliced_size = @data.slice!(SIZE_REGEX) + @size = sliced_size.delete('-').to_i unless sliced_size.nil? end end - + + private + def check_for_flush - if not @size.nil? and @data.length == @size - @flush = true - end + @flush = true if !@size.nil? && @data.length == @size end - end end end diff --git a/lib/protobuf/rpc/client.rb b/lib/protobuf/rpc/client.rb index 189ba982..74791d23 100644 --- a/lib/protobuf/rpc/client.rb +++ b/lib/protobuf/rpc/client.rb @@ -1,22 +1,21 @@ require 'forwardable' require 'protobuf' -require 'protobuf/common/logger' +require 'protobuf/logging' require 'protobuf/rpc/error' -require 'protobuf/rpc/connector' module Protobuf module Rpc class Client extend Forwardable - include Protobuf::Logger::LogMethods - - delegate [:options, :complete_cb, :success_cb, :failure_cb, :async?] => :@connector + include Protobuf::Logging + + def_delegators :@connector, :options, :complete_cb, :success_cb, :failure_cb, :send_request attr_reader :connector - + # Create a new client with default options (defined in ClientConnection) # See Service#client for a more convenient way to create a client, as well # as Client#method_missing defined below. - # + # # request = WidgetFindRequest.new # client = Client.new({ # :service => WidgetService, @@ -26,74 +25,71 @@ class Client # :request => request # }) # - def initialize(opts={}) - raise "Invalid client configuration. Service must be defined." if opts[:service].nil? - @connector = Connector.connector_for_client.new(opts) - log_debug "[#{log_signature}] Initialized with options: %s" % opts.inspect + def initialize(options = {}) + fail "Invalid client configuration. Service must be defined." if options[:service].nil? + @connector = ::Protobuf.connector_type_class.new(options) + logger.debug { sign_message("Initialized with options: #{options.inspect}") } end def log_signature - @log_signature ||= "client-#{self.class}" + @_log_signature ||= "[client-#{self.class}]" end - - # Set a complete callback on the client to return the object (self). - # Callback is called regardless of :async setting. - # + + # Set a complete callback on the client to return the object (self). + # # client = Client.new(:service => WidgetService) # client.on_complete {|obj| ... } - # + # def on_complete(&complete_cb) - @connector.complete_cb = complete_cb + @connector.complete_cb = complete_cb end def on_complete=(callable) - if callable != nil && !callable.respond_to?(:call) && callable.arity != 1 - raise "callable must take a single argument and respond to :call" + if !callable.nil? && !callable.respond_to?(:call) && callable.arity != 1 + fail "callable must take a single argument and respond to :call" end - - @connector.complete_cb = callable + + @connector.complete_cb = callable end - + # Set a failure callback on the client to return the # error returned by the service, if any. If this callback # is called, success_cb will NOT be called. - # Callback is called regardless of :async setting. - # + # # client = Client.new(:service => WidgetService) # client.on_failure {|err| ... } - # + # def on_failure(&failure_cb) @connector.failure_cb = failure_cb end def on_failure=(callable) - if callable != nil && !callable.respond_to?(:call) && callable.arity != 1 - raise "callable must take a single argument and respond to :call" + if !callable.nil? && !callable.respond_to?(:call) && callable.arity != 1 + fail "Callable must take a single argument and respond to :call" end - @connector.failure_cb = callable + @connector.failure_cb = callable end - + # Set a success callback on the client to return the # successful response from the service when it is returned. # If this callback is called, failure_cb will NOT be called. - # Callback is called regardless of :async setting. - # + # # client = Client.new(:service => WidgetService) # client.on_success {|res| ... } - # + # def on_success(&success_cb) @connector.success_cb = success_cb end def on_success=(callable) - if callable != nil && !callable.respond_to?(:call) && callable.arity != 1 - raise "callable must take a single argument and respond to :call" + if !callable.nil? && !callable.respond_to?(:call) && callable.arity != 1 + fail "Callable must take a single argument and respond to :call" end - @connector.success_cb = callable + @connector.success_cb = callable end - + # Provides a mechanism to call the service method against the client # which will automatically setup the service_class and method_name # in the wrapper protobuf request. @@ -105,62 +101,40 @@ def on_success=(callable) # c.on_success {|res| ... } # c.on_failure {|err| ... } # end - # - def method_missing(method, *params) + # + def method_missing(method_name, *params) service = options[:service] - unless service.rpcs[service].keys.include?(method) - log_error "[#{log_signature}] %s#%s not rpc method, passing to super" % [service.name, method.to_s] - super(method, *params) - else - log_debug "[#{log_signature}] %s#%s" % [service.name, method.to_s] - rpc = service.rpcs[service][method.to_sym] + if service.rpc_method?(method_name) + logger.debug { sign_message("#{service.name}##{method_name}") } + rpc = service.rpcs[method_name.to_sym] + options[:request_type] = rpc.request_type - log_debug "[#{log_signature}] Request Type: %s" % options[:request_type].name + logger.debug { sign_message("Request Type: #{options[:request_type].name}") } + options[:response_type] = rpc.response_type - log_debug "[#{log_signature}] Response Type: %s" % options[:response_type].name - options[:method] = method.to_s + logger.debug { sign_message("Response Type: #{options[:response_type].name}") } + + options[:method] = method_name.to_s options[:request] = params[0].is_a?(Hash) ? options[:request_type].new(params[0]) : params[0] - log_debug "[#{log_signature}] Request Data: %s" % options[:request].inspect - + logger.debug { sign_message("Request Data: #{options[:request].inspect}") } + # Call client to setup on_success and on_failure event callbacks if block_given? - log_debug "[#{log_signature}] client setup callback given, invoking" + logger.debug { sign_message("client setup callback given, invoking") } yield(self) else - log_debug "[#{log_signature}] no block given for callbacks" + logger.debug { sign_message("no block given for callbacks") } end - + send_request + else + logger.error { sign_message("#{service.name}##{method_name} not rpc method, passing to super") } + super(method_name, *params) end end - - # Send the request to the service through eventmachine. - # This method is usually never called directly - # but is invoked by method_missing (see docs above). - # - # request = WidgetFindRequest.new - # client = Client.new({ - # :service => WidgetService, - # :method => "find", - # :request_type => "WidgetFindRequest", - # :response_type => "WidgetList", - # :request => request - # }) - # - # client.on_success do |res| - # res.widgets.each{|w| puts w.inspect } - # end - # - # client.on_failure do |err| - # puts err.message - # end - # - # client.send_request - # - def send_request - @connector.send_request - end - + end + + ActiveSupport.run_load_hooks(:protobuf_rpc_client, Client) end end diff --git a/lib/protobuf/rpc/connector.rb b/lib/protobuf/rpc/connector.rb deleted file mode 100644 index d7fd06cc..00000000 --- a/lib/protobuf/rpc/connector.rb +++ /dev/null @@ -1,20 +0,0 @@ -module Protobuf - module Rpc - class Connector - - def self.connector_for_client - if defined?(Protobuf::ClientType) - case Protobuf::ClientType - when "Socket" then - ::Protobuf::Rpc::Connectors::Socket - else - ::Protobuf::Rpc::Connectors::EventMachine - end - else - ::Protobuf::Rpc::Connectors::EventMachine - end - end - - end - end -end diff --git a/lib/protobuf/rpc/connectors/base.rb b/lib/protobuf/rpc/connectors/base.rb index cb5786d6..969d777c 100644 --- a/lib/protobuf/rpc/connectors/base.rb +++ b/lib/protobuf/rpc/connectors/base.rb @@ -1,43 +1,220 @@ -require 'protobuf/common/logger' +require 'timeout' +require 'protobuf/logging' require 'protobuf/rpc/rpc.pb' require 'protobuf/rpc/buffer' require 'protobuf/rpc/error' require 'protobuf/rpc/stat' -require 'protobuf/rpc/connectors/common' module Protobuf module Rpc module Connectors DEFAULT_OPTIONS = { - :service => nil, # Service to invoke - :method => nil, # Service method to call - :host => '127.0.0.1', # A default host (usually overridden) - :port => '9399', # A default port (usually overridden) - :request => nil, # The request object sent by the client - :request_type => nil, # The request type expected by the client - :response_type => nil, # The response type expected by the client - :async => false, # Whether or not to block a client call, this is actually handled by client.rb - :timeout => 30 # The default timeout for the request, also handled by client.rb - } + :service => nil, # Fully-qualified Service class + :method => nil, # Service method to invoke + :host => '127.0.0.1', # The hostname or address of the service (usually overridden) + :port => '9399', # The port of the service (usually overridden or pre-configured) + :request => nil, # The request object sent by the client + :request_type => nil, # The request type expected by the client + :response_type => nil, # The response type expected by the client + :timeout => nil, # The timeout for the request, also handled by client.rb + :client_host => nil, # The hostname or address of this client + :first_alive_load_balance => false, # Do we want to use check_avail frames before request + }.freeze class Base - include Protobuf::Logger::LogMethods - - attr_reader :options - attr_accessor :success_cb, :failure_cb, :complete_cb + include Protobuf::Logging + + attr_reader :options, :error + attr_accessor :success_cb, :failure_cb, :complete_cb, :stats def initialize(options) @options = DEFAULT_OPTIONS.merge(options) + @stats = ::Protobuf::Rpc::Stat.new(:CLIENT) + end + + def any_callbacks? + [@complete_cb, @failure_cb, @success_cb].any? + end + + def close_connection + fail 'If you inherit a Connector from Base you must implement close_connection' + end + + def complete + @stats.stop + logger.info { @stats.to_s } + logger.debug { sign_message('Response proceessing complete') } + @complete_cb.call(self) unless @complete_cb.nil? + rescue => e + logger.error { sign_message('Complete callback error encountered') } + log_exception(e) + raise + end + + def data_callback(data) + logger.debug { sign_message('Using data_callback') } + @used_data_callback = true + @data = data + end + + # All failures should be routed through this method. + # + # @param [Symbol] code The code we're using (see ::Protobuf::Socketrpc::ErrorReason) + # @param [String] message The error message + def failure(code, message) + @error = ClientError.new + @stats.status = @error.code = ::Protobuf::Socketrpc::ErrorReason.fetch(code) + @error.message = message + + logger.debug { sign_message("Server failed request (invoking on_failure): #{@error.inspect}") } + + @failure_cb.call(@error) unless @failure_cb.nil? + rescue => e + logger.error { sign_message("Failure callback error encountered") } + log_exception(e) + raise + ensure + complete + end + + def first_alive_load_balance? + ENV.key?("PB_FIRST_ALIVE_LOAD_BALANCE") || + options[:first_alive_load_balance] + end + + def initialize_stats + @stats = ::Protobuf::Rpc::Stat.new(:CLIENT) + @stats.server = [@options[:port], @options[:host]] + @stats.service = @options[:service].name + @stats.method_name = @options[:method].to_s + rescue => ex + log_exception(ex) + failure(:RPC_ERROR, "Invalid stats configuration. #{ex.message}") + end + + def log_signature + @_log_signature ||= "[client-#{self.class}]" + end + + def parse_response + # Close up the connection as we no longer need it + close_connection + + logger.debug { sign_message("Parsing response from server (connection closed)") } + + # Parse out the raw response + @stats.response_size = @response_data.size unless @response_data.nil? + response_wrapper = ::Protobuf::Socketrpc::Response.decode(@response_data) + @stats.server = response_wrapper.server if response_wrapper.field?(:server) + + # Determine success or failure based on parsed data + if response_wrapper.field?(:error_reason) + logger.debug { sign_message("Error response parsed") } + + # fail the call if we already know the client is failed + # (don't try to parse out the response payload) + failure(response_wrapper.error_reason, response_wrapper.error) + else + logger.debug { sign_message("Successful response parsed") } + + # Ensure client_response is an instance + parsed = @options[:response_type].decode(response_wrapper.response_proto.to_s) + + if parsed.nil? && !response_wrapper.field?(:error_reason) + failure(:BAD_RESPONSE_PROTO, 'Unable to parse response from server') + else + verify_callbacks + succeed(parsed) + return @data if @used_data_callback + end + end + end + + def ping_port + @ping_port ||= ENV["PB_RPC_PING_PORT"] + end + + def ping_port_enabled? + ENV.key?("PB_RPC_PING_PORT") + end + + def request_bytes + validate_request_type! + return ::Protobuf::Socketrpc::Request.encode(request_fields) + rescue => e + failure(:INVALID_REQUEST_PROTO, "Could not set request proto: #{e.message}") end - + + def request_caller + @options[:client_host] || ::Protobuf.client_host + end + + def request_fields + { :service_name => @options[:service].name, + :method_name => @options[:method].to_s, + :request_proto => @options[:request], + :caller => request_caller } + end + def send_request - raise 'If you inherit a Connector from Base you must implement send_request' + fail 'If you inherit a Connector from Base you must implement send_request' + end + + def setup_connection + initialize_stats + @request_data = request_bytes + @stats.request_size = @request_data.size + end + + def succeed(response) + logger.debug { sign_message("Server succeeded request (invoking on_success)") } + @success_cb.call(response) unless @success_cb.nil? + rescue => e + logger.error { sign_message("Success callback error encountered") } + log_exception(e) + failure(:RPC_ERROR, "An exception occurred while calling on_success: #{e.message}") + ensure + complete end - - def async? - !!@options[:async] + + def timeout + if options[:timeout] + options[:timeout] + else + 300 # seconds + end end - + + # Wrap the given block in a timeout of the configured number of seconds. + # + def timeout_wrap(&block) + ::Timeout.timeout(timeout, &block) + rescue ::Timeout::Error + failure(:RPC_FAILED, "The server took longer than #{timeout} seconds to respond") + end + + def validate_request_type! + unless @options[:request].class == @options[:request_type] + expected = @options[:request_type].name + actual = @options[:request].class.name + failure(:INVALID_REQUEST_PROTO, "Expected request type to be type of #{expected}, got #{actual} instead") + end + end + + def verify_callbacks + unless any_callbacks? + logger.debug { sign_message("No callbacks set, using data_callback") } + @success_cb = @failure_cb = method(:data_callback) + end + end + + def verify_options! + # Verify the options that are necessary and merge them in + [:service, :method, :host, :port].each do |opt| + failure(:RPC_ERROR, "Invalid client connection configuration. #{opt} must be a defined option.") if @options[opt].nil? + end + end + end end end diff --git a/lib/protobuf/rpc/connectors/common.rb b/lib/protobuf/rpc/connectors/common.rb deleted file mode 100644 index 6969e61e..00000000 --- a/lib/protobuf/rpc/connectors/common.rb +++ /dev/null @@ -1,155 +0,0 @@ -module Protobuf - module Rpc - module Connectors - module Common - - attr_reader :error - - def any_callbacks? - return [@complete_cb, @failure_cb, @success_cb].inject(false) do |reduction, cb| - reduction = (reduction || !cb.nil?) - end - end - - def complete - @stats.end - @stats.log_stats - log_debug "[#{log_signature}] Response proceessing complete" - @complete_cb.call(self) unless @complete_cb.nil? - rescue - log_error "[#{log_signature}] Complete callback error encountered: %s" % $!.message - log_error "[#{log_signature}] %s" % $!.backtrace.join("\n") - raise - end - - def data_callback(data) - log_debug "[#{log_signature}] Using data_callback" - @used_data_callback = true - @data = data - end - - def fail(code, message) - @error = ClientError.new - @error.code = code.is_a?(Symbol) ? Protobuf::Socketrpc::ErrorReason.values[code] : code - @error.message = message - log_debug "[#{log_signature}] Server failed request (invoking on_failure): %s" % @error.inspect - - @failure_cb.call(@error) unless @failure_cb.nil? - rescue - log_error "[#{log_signature}] Failure callback error encountered: %s" % $!.message - log_error "[#{log_signature}] %s" % $!.backtrace.join("\n") - raise - ensure - complete - end - - def initialize_stats - @stats = Protobuf::Rpc::Stat.new(:CLIENT, true) - @stats.server = [@options[:port], @options[:host]] - @stats.service = @options[:service].name - @stats.method = @options[:method] - self - rescue => ex - fail(:RPC_ERROR, "Invalid stats configuration. #{ex.message}") - end - - def log_signature - @log_signature ||= "client-#{self.class}" - end - - def parse_response - # Close up the connection as we no longer need it - close_connection - - log_debug "[#{log_signature}] Parsing response from server (connection closed)" - @stats.response_size = @buffer.size - - # Parse out the raw response - response_wrapper = Protobuf::Socketrpc::Response.new - response_wrapper.parse_from_string(@buffer.data) - - # Determine success or failure based on parsed data - if response_wrapper.has_field?(:error_reason) - log_debug "[#{log_signature}] Error response parsed" - - # fail the call if we already know the client is failed - # (don't try to parse out the response payload) - fail(response_wrapper.error_reason, response_wrapper.error) - else - log_debug "[#{log_signature}] Successful response parsed" - - # Ensure client_response is an instance - response_type = @options[:response_type].new - parsed = response_type.parse_from_string(response_wrapper.response_proto.to_s) - - if parsed.nil? and not response_wrapper.has_field?(:error_reason) - fail(:BAD_RESPONSE_PROTO, 'Unable to parse response from server') - else - verify_callbacks - succeed(parsed) - return @data if @used_data_callback - end - end - end - - # Setup the read buffer for data coming back - def post_init - # Setup an object for reponses without callbacks - @data = nil - log_debug "[#{log_signature}] Post init, new read buffer created" - @buffer = Protobuf::Rpc::Buffer.new(:read) - _send_request unless error? - log_debug "[#{log_signature}] Post init, new read buffer created just sent" - rescue - fail(:RPC_ERROR, 'Connection error: %s' % $!.message) - end - - # Sends the request to the server, invoked by the connection_completed event - def _send_request - request_wrapper = Protobuf::Socketrpc::Request.new - request_wrapper.service_name = @options[:service].name - request_wrapper.method_name = @options[:method].to_s - - if @options[:request].class == @options[:request_type] - request_wrapper.request_proto = @options[:request].serialize_to_string - else - expected = @options[:request_type].name - actual = @options[:request].class.name - fail :INVALID_REQUEST_PROTO, 'Expected request type to be type of %s, got %s instead' % [expected, actual] - end - - log_debug "[#{log_signature}] Sending Request: %s" % request_wrapper.inspect - request_buffer = Protobuf::Rpc::Buffer.new(:write, request_wrapper) - @stats.request_size = request_buffer.size - send_data(request_buffer.write) - end - - def succeed(response) - log_debug "[#{log_signature}] Server succeeded request (invoking on_success)" - @success_cb.call(response) unless @success_cb.nil? - rescue - log_error "[#{log_signature}] Success callback error encountered: %s" % $!.message - log_error "[#{log_signature}] %s" % $!.backtrace.join("\n") - fail :RPC_ERROR, 'An exception occurred while calling on_success: %s' % $!.message - ensure - complete - end - - def verify_callbacks - if !any_callbacks? - log_debug "[#{log_signature}] No callbacks set, using data_callback" - @success_cb = @failure_cb = self.method(:data_callback) - end - end - - def verify_options - # Verify the options that are necessary and merge them in - [:service, :method, :host, :port].each do |opt| - fail(:RPC_ERROR, "Invalid client connection configuration. #{opt} must be a defined option.") if @options[opt].nil? - end - end - - end - end - end -end diff --git a/lib/protobuf/rpc/connectors/em_client.rb b/lib/protobuf/rpc/connectors/em_client.rb deleted file mode 100644 index eb7c5cfe..00000000 --- a/lib/protobuf/rpc/connectors/em_client.rb +++ /dev/null @@ -1,58 +0,0 @@ -# Handles client connections to the server -module Protobuf - module Rpc - module Connectors - - class EMClient < EM::Connection - include Protobuf::Logger::LogMethods - include Protobuf::Rpc::Connectors::Common - - attr_reader :options, :request, :response - attr_reader :error, :error_reason, :error_message - - class << self - - def connect(options={}) - options = DEFAULT_OPTIONS.merge(options) - log_debug "[client-#{self}] Connecting to server: %s" % options.inspect - EM.connect(options[:host], options[:port], self, options) - end - - end - - def initialize(options={}, &failure_cb) - @failure_cb = failure_cb - @options = DEFAULT_OPTIONS.merge(options) - verify_options - initialize_stats - - log_debug "[#{log_signature}] Client Initialized: %s" % options.inspect - rescue - fail(:RPC_ERROR, 'Failed to initialize connection: %s' % $!.message) - end - - # Success callback registration - def on_success(&success_cb) - @success_cb = success_cb - end - - # Failure callback registration - def on_failure(&failure_cb) - @failure_cb = failure_cb - end - - # Completion callback registration - def on_complete(&complete_cb) - @complete_cb = complete_cb - end - - def receive_data(data) - log_debug "[#{log_signature}] receive_data: %s" % data - @buffer << data - parse_response if @buffer.flushed? - end - - end - end - end -end diff --git a/lib/protobuf/rpc/connectors/eventmachine.rb b/lib/protobuf/rpc/connectors/eventmachine.rb deleted file mode 100644 index 99f61df8..00000000 --- a/lib/protobuf/rpc/connectors/eventmachine.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'protobuf/rpc/connectors/base' -require 'protobuf/rpc/connectors/em_client' - -module Protobuf - module Rpc - module Connectors - class EventMachine < Base - - def send_request - ensure_em_running do - f = Fiber.current - - EM.next_tick do - log_debug "[#{log_signature}] Scheduling EventMachine client request to be created on next tick" - cnxn = EMClient.connect(options, &ensure_cb) - cnxn.on_success(&success_cb) if success_cb - cnxn.on_failure(&ensure_cb) - cnxn.on_complete { resume_fiber(f) } unless async? - log_debug "[#{log_signature}] Connection scheduled" - end - - async? ? true : set_timeout_and_validate_fiber - end - end - - # Returns a callable that ensures any errors will be returned to the client - # - # If a failure callback was set, just use that as a direct assignment - # otherwise implement one here that simply throws an exception, since we - # don't want to swallow the black holes. - # - def ensure_cb - @ensure_cb ||= (@failure_cb || lambda { |error| raise '%s: %s' % [error.code.name, error.message] } ) - end - - def log_signature - @log_signature ||= "client-#{self.class}" - end - - private - - def ensure_em_running(&blk) - if EM.reactor_running? - @global_reactor = true - yield - else - @global_reactor = false - EM.fiber_run do - blk.call - EM.stop - end - end - end - - def resume_fiber(fib) - EM::cancel_timer(@timeout_timer) - fib.resume(true) - rescue => ex - log_error "[#{log_signature}] An exception occurred while waiting for server response:" - log_error ex.message - log_error ex.backtrace.join("\n") - - message = 'Synchronous client failed: %s' % ex.message - err = Protobuf::Rpc::ClientError.new(Protobuf::Socketrpc::ErrorReason::RPC_ERROR, message) - ensure_cb.call(err) - EM.stop if !@global_reactor - end - - def set_timeout_and_validate_fiber - @timeout_timer = EM::add_timer(@options[:timeout]) do - message = 'Client timeout of %d seconds expired' % @options[:timeout] - err = Protobuf::Rpc::ClientError.new(Protobuf::Socketrpc::ErrorReason::RPC_ERROR, message) - ensure_cb.call(err) - EM.stop if !@global_reactor - end - - Fiber.yield - rescue FiberError - message = "Synchronous calls must be in 'EM.fiber_run' block" - err = Protobuf::Rpc::ClientError.new(Protobuf::Socketrpc::ErrorReason::RPC_ERROR, message) - ensure_cb.call(err) - EM.stop if !@global_reactor - end - - end - end - end -end diff --git a/lib/protobuf/rpc/connectors/ping.rb b/lib/protobuf/rpc/connectors/ping.rb new file mode 100644 index 00000000..93c75868 --- /dev/null +++ b/lib/protobuf/rpc/connectors/ping.rb @@ -0,0 +1,89 @@ +require "socket" + +module Protobuf + module Rpc + module Connectors + class Ping + attr_reader :host, :port + + def initialize(host, port) + @host = host + @port = port + end + + def online? + socket = tcp_socket + socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_LINGER, [1, 0].pack('ii')) + + true + rescue + false + ensure + begin + socket && socket.close + rescue IOError + nil + end + end + + def timeout + @timeout ||= begin + if ::ENV.key?("PB_RPC_PING_PORT_TIMEOUT") + ::ENV["PB_RPC_PING_PORT_TIMEOUT"].to_f / 1000 + else + 0.2 # 200 ms + end + end + end + + private + + def tcp_socket + # Reference: http://stackoverflow.com/a/21014439/1457934 + socket = ::Socket.new(family, ::Socket::SOCK_STREAM, 0) + socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) + socket.connect_nonblock(sockaddr) + socket + rescue ::IO::WaitWritable + # IO.select will block until the socket is writable or the timeout + # is exceeded - whichever comes first. + if ::IO.select(nil, [socket], nil, timeout) + begin + # Verify there is now a good connection + socket.connect_nonblock(sockaddr) + socket + rescue ::Errno::EISCONN + # Socket is connected. + socket + rescue + # An unexpected exception was raised - the connection is no good. + socket.close + raise + end + else + # IO.select returns nil when the socket is not ready before timeout + # seconds have elapsed + socket.close + raise "Connection Timeout" + end + end + + def family + @family ||= ::Socket.const_get(addrinfo[0][0]) + end + + def addrinfo + @addrinfo ||= ::Socket.getaddrinfo(host, nil) + end + + def ip + @ip ||= addrinfo[0][3] + end + + def sockaddr + @sockaddr ||= ::Socket.pack_sockaddr_in(port, ip) + end + end + end + end +end diff --git a/lib/protobuf/rpc/connectors/socket.rb b/lib/protobuf/rpc/connectors/socket.rb index 3346384a..2e3e6022 100644 --- a/lib/protobuf/rpc/connectors/socket.rb +++ b/lib/protobuf/rpc/connectors/socket.rb @@ -4,48 +4,50 @@ module Protobuf module Rpc module Connectors class Socket < Base - include Protobuf::Rpc::Connectors::Common - include Protobuf::Logger::LogMethods - + include Protobuf::Logging + def send_request - check_async - initialize_stats - connect_to_rpc_server - post_init # calls _send_request - read_response + timeout_wrap do + setup_connection + connect_to_rpc_server + post_init + read_response + end end - private - - def check_async - if async? - log_error "[client-#{self.class}] Cannot run in async mode" - raise "Cannot use Socket client in async mode" - else - log_debug "[client-#{self.class}] Async check passed" - end + def log_signature + @_log_signature ||= "[client-#{self.class}]" end + private + def close_connection @socket.close - log_debug "[client-#{self.class}] Connector closed" + logger.debug { sign_message('Connector closed') } end def connect_to_rpc_server - @socket = TCPSocket.new(options[:host], options[:port]) - log_debug "[client-#{self.class}] Connection established #{options[:host]}:#{options[:port]}" + @socket ||= TCPSocket.new(options[:host], options[:port]) + logger.debug { sign_message("Connection established #{options[:host]}:#{options[:port]}") } end # Method to determine error state, must be used with Connector api def error? - log_debug "[client-#{self.class}] Error state : #{@socket.closed?}" + return true if @error + logger.debug { sign_message("Error state : #{@socket.closed?}") } @socket.closed? end + def post_init + send_data unless error? + rescue => e + failure(:RPC_ERROR, "Connection error: #{e.message}") + end + def read_data size_io = StringIO.new - while((size_reader = @socket.getc) != "-") + until (size_reader = @socket.getc) == "-" size_io << size_reader end str_size_io = size_io.string @@ -54,16 +56,22 @@ def read_data end def read_response - @buffer << read_data - parse_response if @buffer.flushed? + logger.debug { sign_message("error? is #{error?}") } + return if error? + response_buffer = ::Protobuf::Rpc::Buffer.new(:read) + response_buffer << read_data + @response_data = response_buffer.data + parse_response if response_buffer.flushed? end - def send_data(data) - @socket.write(data) + def send_data + return if error? + request_buffer = ::Protobuf::Rpc::Buffer.new(:write) + request_buffer.set_data(@request_data) + @socket.write(request_buffer.write) @socket.flush - log_debug "[client-#{self.class}] write closed" + logger.debug { sign_message("write closed") } end - end end end diff --git a/lib/protobuf/rpc/connectors/zmq.rb b/lib/protobuf/rpc/connectors/zmq.rb new file mode 100644 index 00000000..3d99fb96 --- /dev/null +++ b/lib/protobuf/rpc/connectors/zmq.rb @@ -0,0 +1,319 @@ +require "thread_safe" +require "protobuf/rpc/connectors/base" +require "protobuf/rpc/connectors/ping" +require "protobuf/rpc/service_directory" + +module Protobuf + module Rpc + module Connectors + class Zmq < Base + RequestTimeout = Class.new(RuntimeError) + ZmqRecoverableError = Class.new(RuntimeError) + ZmqEagainError = Class.new(RuntimeError) + + ## + # Included Modules + # + include Protobuf::Logging + + ## + # Class Constants + # + CLIENT_RETRIES = (ENV['PB_CLIENT_RETRIES'] || 3) + + ## + # Class Methods + # + def self.ping_port_responses + @ping_port_responses ||= ::ThreadSafe::Cache.new + end + + def self.zmq_context(reload = false) + @zmq_contexts = nil if reload + @zmq_contexts ||= Hash.new do |hash, key| + hash[key] = ZMQ::Context.new + end + + @zmq_contexts[Process.pid] + end + + ## + # Instance methods + # + def log_signature + @_log_signature ||= "[client-#{self.class}]" + end + + # Start the request/response cycle. We implement the Lazy Pirate + # req/reply reliability pattern as laid out in the ZMQ Guide, Chapter 4. + # + # @see http://zguide.zeromq.org/php:chapter4#Client-side-Reliability-Lazy-Pirate-Pattern + # + def send_request + setup_connection + send_request_with_lazy_pirate unless error? + end + + private + + ## + # Private Instance methods + # + def check_available_rcv_timeout + @check_available_rcv_timeout ||= begin + case + when ENV.key?("PB_ZMQ_CLIENT_CHECK_AVAILABLE_RCV_TIMEOUT") then + ENV["PB_ZMQ_CLIENT_CHECK_AVAILABLE_RCV_TIMEOUT"].to_i + else + 200 # ms + end + end + end + + def check_available_snd_timeout + @check_available_snd_timeout ||= begin + case + when ENV.key?("PB_ZMQ_CLIENT_CHECK_AVAILABLE_SND_TIMEOUT") then + ENV["PB_ZMQ_CLIENT_CHECK_AVAILABLE_SND_TIMEOUT"].to_i + else + 200 # ms + end + end + end + + def close_connection + # The socket is automatically closed after every request. + end + + # Create a socket connected to a server that can handle the current + # service. The LINGER is set to 0 so we can close immediately in + # the event of a timeout + def create_socket + attempt_number = 0 + + begin + attempt_number += 1 + socket = zmq_context.socket(::ZMQ::REQ) + + if socket # Make sure the context builds the socket + server_uri = lookup_server_uri + socket.setsockopt(::ZMQ::LINGER, 0) + zmq_error_check(socket.connect(server_uri), :socket_connect) + socket = socket_to_available_server(socket) if first_alive_load_balance? + end + end while socket.try(:socket).nil? && attempt_number < socket_creation_attempts + + raise RequestTimeout, "Cannot create new ZMQ client socket" if socket.try(:socket).nil? + socket + end + + # Method to determine error state, must be used with Connector API. + # + def error? + !! @error + end + + def host_alive?(host) + return true unless ping_port_enabled? + + if (last_response = self.class.ping_port_responses[host]) + if (Time.now.to_i - last_response[:at]) <= host_alive_check_interval + return last_response[:ping_port_open] + end + end + + ping_port_open = ping_port_open?(host) + self.class.ping_port_responses[host] = { + :at => Time.now.to_i, + :ping_port_open => ping_port_open, + } + ping_port_open + end + + def host_alive_check_interval + @host_alive_check_interval ||= [ENV["PB_ZMQ_CLIENT_HOST_ALIVE_CHECK_INTERVAL"].to_i, 1].max + end + + # Lookup a server uri for the requested service in the service + # directory. If the service directory is not running, default + # to the host and port in the options + # + def lookup_server_uri + server_lookup_attempts.times do + first_alive_listing = service_directory.all_listings_for(service).find do |listing| + host_alive?(listing.try(:address)) + end + + if first_alive_listing + host = first_alive_listing.try(:address) + port = first_alive_listing.try(:port) + @stats.server = [port, host] + return "tcp://#{host}:#{port}" + end + + host = options[:host] + port = options[:port] + + if host_alive?(host) + @stats.server = [port, host] + return "tcp://#{host}:#{port}" + end + + sleep(1.0 / 100.0) + end + + fail "Host not found for service #{service}" + end + + def ping_port_open?(host) + Ping.new(host, ping_port.to_i).online? + end + + def rcv_timeout + @rcv_timeout ||= begin + case + when options[:timeout] then + options[:timeout] + when ENV.key?("PB_ZMQ_CLIENT_RCV_TIMEOUT") then + ENV["PB_ZMQ_CLIENT_RCV_TIMEOUT"].to_i + else + 300_000 # 300 seconds + end + end + end + + # Trying a number of times, attempt to get a response from the server. + # If we haven't received a legitimate response in the CLIENT_RETRIES number + # of retries, fail the request. + # + def send_request_with_lazy_pirate + attempt = 0 + + begin + attempt += 1 + send_request_with_timeout(attempt) + parse_response + rescue RequestTimeout + retry if attempt < CLIENT_RETRIES + failure(:RPC_FAILED, "The server repeatedly failed to respond within #{timeout} seconds") + end + end + + def send_request_with_timeout(attempt = 0) + socket = create_socket + socket.setsockopt(::ZMQ::RCVTIMEO, rcv_timeout) + socket.setsockopt(::ZMQ::SNDTIMEO, snd_timeout) + + logger.debug { sign_message("Sending Request (attempt #{attempt}, #{socket})") } + zmq_eagain_error_check(socket.send_string(@request_data), :socket_send_string) + logger.debug { sign_message("Waiting #{rcv_timeout}ms for response (attempt #{attempt}, #{socket})") } + zmq_eagain_error_check(socket.recv_string(@response_data = ""), :socket_recv_string) + logger.debug { sign_message("Response received (attempt #{attempt}, #{socket})") } + rescue ZmqEagainError + logger.debug { sign_message("Timed out waiting for response (attempt #{attempt}, #{socket})") } + raise RequestTimeout + ensure + logger.debug { sign_message("Closing Socket") } + zmq_error_check(socket.close, :socket_close) if socket + logger.debug { sign_message("Socket closed") } + end + + def server_lookup_attempts + @server_lookup_attempts ||= [ENV["PB_ZMQ_CLIENT_SERVER_LOOKUP_ATTEMPTS"].to_i, 5].max + end + + # The service we're attempting to connect to + # + def service + options[:service] + end + + # Alias for ::Protobuf::Rpc::ServiceDirectory.instance + def service_directory + ::Protobuf::Rpc.service_directory + end + + def snd_timeout + @snd_timeout ||= begin + case + when options[:timeout] then + options[:timeout] + when ENV.key?("PB_ZMQ_CLIENT_SND_TIMEOUT") then + ENV["PB_ZMQ_CLIENT_SND_TIMEOUT"].to_i + else + 300_000 # 300 seconds + end + end + end + + def socket_creation_attempts + @socket_creation_attempts ||= (ENV["PB_ZMQ_CLIENT_SOCKET_CREATION_ATTEMPTS"] || 5).to_i + end + + def socket_to_available_server(socket) + check_available_response = "" + socket.setsockopt(::ZMQ::RCVTIMEO, check_available_rcv_timeout) + socket.setsockopt(::ZMQ::SNDTIMEO, check_available_snd_timeout) + zmq_recoverable_error_check(socket.send_string(::Protobuf::Rpc::Zmq::CHECK_AVAILABLE_MESSAGE), :socket_send_string) + zmq_recoverable_error_check(socket.recv_string(check_available_response), :socket_recv_string) + + if check_available_response == ::Protobuf::Rpc::Zmq::NO_WORKERS_AVAILABLE + zmq_recoverable_error_check(socket.close, :socket_close) + end + + socket.setsockopt(::ZMQ::RCVTIMEO, -1) + socket.setsockopt(::ZMQ::SNDTIMEO, -1) + socket + rescue ZmqRecoverableError + return nil # couldn't make a connection and need to try again + end + + # Return the ZMQ Context to use for this process. + # If the context does not exist, create it, then register + # an exit block to ensure the context is terminated correctly. + # + def zmq_context(reload = false) + self.class.zmq_context(reload) + end + + def zmq_eagain_error_check(return_code, source) + return if ::ZMQ::Util.resultcode_ok?(return_code || -1) + + if ::ZMQ::Util.errno == ::ZMQ::EAGAIN # rubocop:disable Style/GuardClause + fail ZmqEagainError, <<-ERROR + Last ZMQ API call to #{source} failed with "#{::ZMQ::Util.error_string}". + + #{caller(1).join($INPUT_RECORD_SEPARATOR)} + ERROR + else + fail <<-ERROR + Last ZMQ API call to #{source} failed with "#{::ZMQ::Util.error_string}". + + #{caller(1).join($INPUT_RECORD_SEPARATOR)} + ERROR + end + end + + def zmq_error_check(return_code, source) + return if ::ZMQ::Util.resultcode_ok?(return_code || -1) + + fail <<-ERROR + Last ZMQ API call to #{source} failed with "#{::ZMQ::Util.error_string}". + + #{caller(1).join($INPUT_RECORD_SEPARATOR)} + ERROR + end + + def zmq_recoverable_error_check(return_code, source) + return if ::ZMQ::Util.resultcode_ok?(return_code || -1) + + fail ZmqRecoverableError, <<-ERROR + Last ZMQ API call to #{source} failed with "#{::ZMQ::Util.error_string}". + + #{caller(1).join($INPUT_RECORD_SEPARATOR)} + ERROR + end + end + end + end +end diff --git a/lib/protobuf/rpc/dynamic_discovery.pb.rb b/lib/protobuf/rpc/dynamic_discovery.pb.rb new file mode 100644 index 00000000..b8dd5869 --- /dev/null +++ b/lib/protobuf/rpc/dynamic_discovery.pb.rb @@ -0,0 +1,50 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + +module Protobuf + module Rpc + module DynamicDiscovery + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Enum Classes + # + class BeaconType < ::Protobuf::Enum + define :HEARTBEAT, 0 + define :FLATLINE, 1 + end + + + ## + # Message Classes + # + class Server < ::Protobuf::Message; end + class Beacon < ::Protobuf::Message; end + + + ## + # Message Fields + # + class Server + optional :string, :uuid, 1 + optional :string, :address, 2 + optional :string, :port, 3 + optional :int32, :ttl, 4 + repeated :string, :services, 5 + end + + class Beacon + optional ::Protobuf::Rpc::DynamicDiscovery::BeaconType, :beacon_type, 1 + optional ::Protobuf::Rpc::DynamicDiscovery::Server, :server, 2 + end + + end + + end + +end + diff --git a/lib/protobuf/rpc/env.rb b/lib/protobuf/rpc/env.rb new file mode 100644 index 00000000..3d7c166e --- /dev/null +++ b/lib/protobuf/rpc/env.rb @@ -0,0 +1,60 @@ +module Protobuf + module Rpc + class Env < Hash + # Creates an accessor that simply sets and reads a key in the hash: + # + # class Config < Hash + # hash_accessor :app + # end + # + # config = Config.new + # config.app = Foo + # config[:app] #=> Foo + # + # config[:app] = Bar + # config.app #=> Bar + # + def self.hash_accessor(*names) #:nodoc: + names.each do |name| + name_str = name.to_s.freeze + + define_method name do + self[name_str] + end + + define_method "#{name}=" do |value| + self[name_str] = value + end + + define_method "#{name}?" do + !self[name_str].nil? + end + end + end + + # TODO: Add extra info about the environment (i.e. variables) and other + # information that might be useful + hash_accessor :client_host, + :encoded_request, + :encoded_response, + :log_signature, + :method_name, + :request, + :request_type, + :request_wrapper, + :response, + :response_type, + :rpc_method, + :rpc_service, + :server, + :service_name, + :worker_id + + def initialize(options = {}) + merge!(options) + + self['worker_id'] = ::Thread.current.object_id.to_s(16) + end + end + end +end diff --git a/lib/protobuf/rpc/error.rb b/lib/protobuf/rpc/error.rb index c8c6b235..584a0425 100644 --- a/lib/protobuf/rpc/error.rb +++ b/lib/protobuf/rpc/error.rb @@ -3,22 +3,24 @@ module Protobuf module Rpc ClientError = Struct.new("ClientError", :code, :message) - + # Base PbError class for client and server errors class PbError < StandardError attr_reader :error_type - - def initialize message='An unknown RpcError occurred', error_type='RPC_ERROR' - @error_type = error_type.is_a?(String) ? Protobuf::Socketrpc::ErrorReason.const_get(error_type) : error_type + + def initialize(message = 'An unknown RpcError occurred', error_type = 'RPC_ERROR') + @error_type = error_type.is_a?(String) ? ::Protobuf::Socketrpc::ErrorReason.const_get(error_type) : error_type super message end - - def to_response response - response.error = message - response.error_reason = @error_type + + def encode(args = {}) + to_response(args).encode + end + + def to_response(args = {}) + ::Protobuf::Socketrpc::Response.new({ :error => message, :error_reason => error_type }.merge(args)) end end - end end diff --git a/lib/protobuf/rpc/error/client_error.rb b/lib/protobuf/rpc/error/client_error.rb index 15a41aba..7efe27f5 100644 --- a/lib/protobuf/rpc/error/client_error.rb +++ b/lib/protobuf/rpc/error/client_error.rb @@ -2,30 +2,30 @@ module Protobuf module Rpc - + class InvalidRequestProto < PbError - def initialize message='Invalid request type given' + def initialize(message = 'Invalid request type given') super message, 'INVALID_REQUEST_PROTO' end end - + class BadResponseProto < PbError - def initialize message='Bad response type from server' + def initialize(message = 'Bad response type from server') super message, 'BAD_RESPONSE_PROTO' end end - + class UnkownHost < PbError - def initialize message='Unknown host or port' + def initialize(message = 'Unknown host or port') super message, 'UNKNOWN_HOST' end end - + class IOError < PbError - def initialize message='IO Error occurred' + def initialize(message = 'IO Error occurred') super message, 'IO_ERROR' end end - + end end diff --git a/lib/protobuf/rpc/error/server_error.rb b/lib/protobuf/rpc/error/server_error.rb index f0191776..990c858b 100644 --- a/lib/protobuf/rpc/error/server_error.rb +++ b/lib/protobuf/rpc/error/server_error.rb @@ -2,42 +2,42 @@ module Protobuf module Rpc - + class BadRequestData < PbError - def initialize message='Unable to parse request' + def initialize(message = 'Unable to parse request') super message, 'BAD_REQUEST_DATA' end end - + class BadRequestProto < PbError - def initialize message='Request is of wrong type' + def initialize(message = 'Request is of wrong type') super message, 'BAD_REQUEST_PROTO' end end - + class ServiceNotFound < PbError - def initialize message='Service class not found' + def initialize(message = 'Service class not found') super message, 'SERVICE_NOT_FOUND' end end - + class MethodNotFound < PbError - def initialize message='Service method not found' + def initialize(message = 'Service method not found') super message, 'METHOD_NOT_FOUND' end end - + class RpcError < PbError - def initialize message='RPC exception occurred' + def initialize(message = 'RPC exception occurred') super message, 'RPC_ERROR' end end - + class RpcFailed < PbError - def initialize message='RPC failed' + def initialize(message = 'RPC failed') super message, 'RPC_FAILED' end end - + end end diff --git a/lib/protobuf/rpc/middleware.rb b/lib/protobuf/rpc/middleware.rb new file mode 100644 index 00000000..3757b646 --- /dev/null +++ b/lib/protobuf/rpc/middleware.rb @@ -0,0 +1,25 @@ +require 'middleware' + +require 'protobuf/rpc/middleware/exception_handler' +require 'protobuf/rpc/middleware/logger' +require 'protobuf/rpc/middleware/request_decoder' +require 'protobuf/rpc/middleware/response_encoder' +require 'protobuf/rpc/middleware/runner' + +module Protobuf + module Rpc + def self.middleware + @middleware ||= ::Middleware::Builder.new(:runner_class => ::Protobuf::Rpc::Middleware::Runner) + end + + # Ensure the middleware stack is initialized + middleware + end + + Rpc.middleware.use(Rpc::Middleware::ExceptionHandler) + Rpc.middleware.use(Rpc::Middleware::RequestDecoder) + Rpc.middleware.use(Rpc::Middleware::Logger) + Rpc.middleware.use(Rpc::Middleware::ResponseEncoder) + + ActiveSupport.run_load_hooks(:protobuf_rpc_middleware, Rpc) +end diff --git a/lib/protobuf/rpc/middleware/exception_handler.rb b/lib/protobuf/rpc/middleware/exception_handler.rb new file mode 100644 index 00000000..376e0375 --- /dev/null +++ b/lib/protobuf/rpc/middleware/exception_handler.rb @@ -0,0 +1,51 @@ +module Protobuf + module Rpc + module Middleware + class ExceptionHandler + include ::Protobuf::Logging + + attr_reader :app + + def initialize(app) + @app = app + end + + def call(env) + dup._call(env) + end + + def _call(env) + app.call(env) + rescue => exception + log_exception(exception) + + # Rescue exceptions, re-wrap them as generic Protobuf errors, + # and encode them + env.response = wrap_exception(exception) + env.encoded_response = wrap_and_encode_with_server(env.response, env) + env + end + + private + + # Wrap exceptions in a generic Protobuf errors unless they already are + # + def wrap_exception(exception) + exception = RpcFailed.new(exception.message) unless exception.is_a?(PbError) + exception + end + + # If the response is a PbError, it won't have the server merged into the response proto. + # We should add it here since exception handler is always at the bottom of the middleware + # stack. Without this, the server hostname in the client rpc log will not be set. + def wrap_and_encode_with_server(response, env) + if response.is_a?(PbError) + response.encode(:server => env.server) + else + response.encode + end + end + end + end + end +end diff --git a/lib/protobuf/rpc/middleware/logger.rb b/lib/protobuf/rpc/middleware/logger.rb new file mode 100644 index 00000000..2c02b9a4 --- /dev/null +++ b/lib/protobuf/rpc/middleware/logger.rb @@ -0,0 +1,95 @@ +module Protobuf + module Rpc + module Middleware + class Logger + def initialize(app) + @app = app + end + + # TODO: Figure out how to control when logs are flushed + def call(env) + dup._call(env) + end + + def _call(env) + instrumenter.start + instrumenter.flush(env) # Log request stats + + env = @app.call(env) + + instrumenter.stop + instrumenter.flush(env) # Log response stats + + env + end + + private + + def instrumenter + @instrumenter ||= Instrumenter.new + end + + # TODO: Replace this with ActiveSupport::Notifications and log subscribers + # TODO: Consider adopting Rails-style logging so we can track serialization + # time as well as ActiveRecord time, etc.: + # + # Started GET "/" for 127.0.0.1 at 2014-02-12 09:40:29 -0700 + # Processing by ReleasesController#index as HTML + # Rendered releases/_release.html.erb (0.0ms) + # Rendered releases/_release.html.erb (0.0ms) + # Rendered releases/_release.html.erb (0.0ms) + # Rendered releases/_release.html.erb (0.0ms) + # Rendered releases/index.html.erb within layouts/application (11.0ms) + # Completed 200 OK in 142ms (Views: 117.6ms | ActiveRecord: 1.7ms) + # + class Instrumenter + attr_reader :env + + def flush(env) + ::Protobuf::Logging.logger.info { to_s(env) } + end + + def start + @start_time = ::Time.now.utc + end + + def stop + @end_time = ::Time.now.utc + end + + def to_s(env) + @env = env + + [ + "[SRV]", + env.client_host, + env.worker_id, + rpc, + sizes, + elapsed_time, + @end_time.try(:iso8601), + ].compact.join(' - ') + end + + private + + def elapsed_time + (@start_time && @end_time ? "#{(@end_time - @start_time).round(4)}s" : nil) + end + + def rpc + env.service_name && env.method_name ? "#{env.service_name}##{env.method_name}" : nil + end + + def sizes + if env.encoded_response? + "#{env.encoded_request.size}B/#{env.encoded_response.size}B" + else + "#{env.encoded_request.size}B/-" + end + end + end + end + end + end +end diff --git a/lib/protobuf/rpc/middleware/request_decoder.rb b/lib/protobuf/rpc/middleware/request_decoder.rb new file mode 100644 index 00000000..c4efe823 --- /dev/null +++ b/lib/protobuf/rpc/middleware/request_decoder.rb @@ -0,0 +1,79 @@ +module Protobuf + module Rpc + module Middleware + class RequestDecoder + include ::Protobuf::Logging + + attr_reader :app, :env + + def initialize(app) + @app = app + end + + def call(env) + dup._call(env) + end + + def _call(env) + @env = env + + logger.debug { sign_message("Decoding request: #{env.encoded_request}") } + env.service_name = service_name + env.method_name = method_name + env.request = request + env.request_wrapper = request_wrapper + env.client_host = request_wrapper.caller + + env.rpc_service = service + env.rpc_method = rpc_method + env.request_type = rpc_method.request_type + env.response_type = rpc_method.response_type + + app.call(env) + end + + def log_signature + env.log_signature || super + end + + private + + def method_name + return @method_name unless @method_name.nil? + + @method_name = request_wrapper.method_name.underscore.to_sym + fail MethodNotFound, "#{service.name}##{@method_name} is not a defined RPC method." unless service.rpc_method?(@method_name) + @method_name + end + + def request + @request ||= rpc_method.request_type.decode(request_wrapper.request_proto) + rescue => exception + raise BadRequestData, "Unable to decode request: #{exception.message}" + end + + # Decode the incoming request object into our expected request object + # + def request_wrapper + @request_wrapper ||= ::Protobuf::Socketrpc::Request.decode(env.encoded_request) + rescue => exception + raise BadRequestData, "Unable to decode request: #{exception.message}" + end + + def rpc_method + @rpc_method ||= service.rpcs[method_name] + end + + def service + @service ||= service_name.constantize + rescue NameError + raise ServiceNotFound, "Service class #{service_name} is not defined." + end + + def service_name + @service_name ||= request_wrapper.service_name + end + end + end + end +end diff --git a/lib/protobuf/rpc/middleware/response_encoder.rb b/lib/protobuf/rpc/middleware/response_encoder.rb new file mode 100644 index 00000000..ae79daf4 --- /dev/null +++ b/lib/protobuf/rpc/middleware/response_encoder.rb @@ -0,0 +1,83 @@ +module Protobuf + module Rpc + module Middleware + class ResponseEncoder + include ::Protobuf::Logging + + attr_reader :app, :env + + def initialize(app) + @app = app + end + + def call(env) + dup._call(env) + end + + def _call(env) + @env = app.call(env) + + env.response = response + env.encoded_response = encoded_response + env + end + + def log_signature + env.log_signature || super + end + + private + + # Encode the response wrapper to return to the client + # + def encoded_response + logger.debug { sign_message("Encoding response: #{response.inspect}") } + + env.encoded_response = wrapped_response.encode + rescue => exception + log_exception(exception) + + # Rescue encoding exceptions, re-wrap them as generic protobuf errors, + # and re-raise them + raise PbError, exception.message + end + + # Prod the object to see if we can produce a proto object as a response + # candidate. Validate the candidate protos. + def response + return @response unless @response.nil? + + candidate = env.response + return @response = validate!(candidate) if candidate.is_a?(Message) + return @response = validate!(candidate.to_proto) if candidate.respond_to?(:to_proto) + return @response = env.response_type.new(candidate.to_hash) if candidate.respond_to?(:to_hash) + return @response = candidate if candidate.is_a?(PbError) + + @response = validate!(candidate) + end + + # Ensure that the response candidate we've been given is of the type + # we expect so that deserialization on the client side works. + # + def validate!(candidate) + if candidate.class != env.response_type + fail BadResponseProto, "Expected response to be of type #{env.response_type.name} but was #{candidate.class.name}" + end + + candidate + end + + # The middleware stack returns either an error or response proto. Package + # it up so that it's in the correct spot in the response wrapper + # + def wrapped_response + if response.is_a?(::Protobuf::Rpc::PbError) + ::Protobuf::Socketrpc::Response.new(:error => response.message, :error_reason => response.error_type, :server => env.server) + else + ::Protobuf::Socketrpc::Response.new(:response_proto => response.encode, :server => env.server) + end + end + end + end + end +end diff --git a/lib/protobuf/rpc/middleware/runner.rb b/lib/protobuf/rpc/middleware/runner.rb new file mode 100644 index 00000000..4438dab4 --- /dev/null +++ b/lib/protobuf/rpc/middleware/runner.rb @@ -0,0 +1,18 @@ +require 'middleware/runner' + +module Protobuf + module Rpc + module Middleware + class Runner < ::Middleware::Runner + # Override the default middleware runner so we can ensure that the + # service dispatcher is the last thing called in the stack. + # + def initialize(stack) + stack << Protobuf::Rpc::ServiceDispatcher + + super(stack) + end + end + end + end +end diff --git a/lib/protobuf/rpc/rpc.pb.rb b/lib/protobuf/rpc/rpc.pb.rb index 9874d3d0..0d2937a7 100644 --- a/lib/protobuf/rpc/rpc.pb.rb +++ b/lib/protobuf/rpc/rpc.pb.rb @@ -1,96 +1,17 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# // Copyright (c) 2009 Shardul Deo -# // -# // Permission is hereby granted, free of charge, to any person obtaining a copy -# // of this software and associated documentation files (the "Software"), to deal -# // in the Software without restriction, including without limitation the rights -# // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# // copies of the Software, and to permit persons to whom the Software is -# // furnished to do so, subject to the following conditions: -# // -# // The above copyright notice and this permission notice shall be included in -# // all copies or substantial portions of the Software. -# // -# // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# // THE SOFTWARE. -# -# // Author: Shardul Deo -# // -# // Protobufs needed for socket rpcs. -# -# package protobuf.socketrpc; -# -# message Request { -# -# // RPC service full name -# required string service_name = 1; -# -# // RPC method name -# required string method_name = 2; -# -# // RPC request proto -# required bytes request_proto = 3; -# } -# -# message Response { -# -# // RPC response proto -# optional bytes response_proto = 1; -# -# // Error, if any -# optional string error = 2; -# -# // Was callback invoked -# optional bool callback = 3 [default = false]; -# -# // Error Reason -# optional ErrorReason error_reason = 4; -# } -# -# // Possible error reasons -# // The server-side errors are returned in the response from the server. -# // The client-side errors are returned by the client-side code when it doesn't -# // have a response from the server. -# enum ErrorReason { -# -# // Server-side errors -# BAD_REQUEST_DATA = 0; // Server received bad request data -# BAD_REQUEST_PROTO = 1; // Server received bad request proto -# SERVICE_NOT_FOUND = 2; // Service not found on server -# METHOD_NOT_FOUND = 3; // Method not found on server -# RPC_ERROR = 4; // Rpc threw exception on server -# RPC_FAILED = 5; // Rpc failed on server -# -# // Client-side errors (these are returned by the client-side code) -# INVALID_REQUEST_PROTO = 6; // Rpc was called with invalid request proto -# BAD_RESPONSE_PROTO = 7; // Server returned a bad response proto -# UNKNOWN_HOST = 8; // Could not find supplied host -# IO_ERROR = 9; // I/O error while communicating with server -# } +# encoding: utf-8 -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' module Protobuf module Socketrpc - class Request < ::Protobuf::Message - required :string, :service_name, 1 - required :string, :method_name, 2 - required :bytes, :request_proto, 3 - end - class Response < ::Protobuf::Message - optional :bytes, :response_proto, 1 - optional :string, :error, 2 - optional :bool, :callback, 3, :default => false - optional :ErrorReason, :error_reason, 4 - end + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Enum Classes + # class ErrorReason < ::Protobuf::Enum define :BAD_REQUEST_DATA, 0 define :BAD_REQUEST_PROTO, 1 @@ -103,5 +24,41 @@ class ErrorReason < ::Protobuf::Enum define :UNKNOWN_HOST, 8 define :IO_ERROR, 9 end + + + ## + # Message Classes + # + class Request < ::Protobuf::Message; end + class Response < ::Protobuf::Message; end + class Header < ::Protobuf::Message; end + + + ## + # Message Fields + # + class Request + required :string, :service_name, 1 + required :string, :method_name, 2 + optional :bytes, :request_proto, 3 + optional :string, :caller, 4 + repeated ::Protobuf::Socketrpc::Header, :headers, 5 + end + + class Response + optional :bytes, :response_proto, 1 + optional :string, :error, 2 + optional :bool, :callback, 3, :default => false + optional ::Protobuf::Socketrpc::ErrorReason, :error_reason, 4 + optional :string, :server, 5 + end + + class Header + required :string, :key, 1 + optional :string, :value, 2 + end + end -end \ No newline at end of file + +end + diff --git a/lib/protobuf/rpc/rpc_method.rb b/lib/protobuf/rpc/rpc_method.rb new file mode 100644 index 00000000..dfb4a196 --- /dev/null +++ b/lib/protobuf/rpc/rpc_method.rb @@ -0,0 +1,16 @@ +module Protobuf + module Rpc + class RpcMethod + ::Protobuf::Optionable.inject(self, false) { ::Google::Protobuf::MethodOptions } + + attr_reader :method, :request_type, :response_type + + def initialize(method, request_type, response_type, &options_block) + @method = method + @request_type = request_type + @response_type = response_type + instance_eval(&options_block) if options_block + end + end + end +end diff --git a/lib/protobuf/rpc/server.rb b/lib/protobuf/rpc/server.rb index 4fba9ef5..f988e341 100644 --- a/lib/protobuf/rpc/server.rb +++ b/lib/protobuf/rpc/server.rb @@ -1,157 +1,39 @@ -# require 'protobuf' -require 'protobuf/common/logger' +require 'protobuf' +require 'protobuf/logging' require 'protobuf/rpc/rpc.pb' require 'protobuf/rpc/buffer' +require 'protobuf/rpc/env' require 'protobuf/rpc/error' -require 'protobuf/rpc/stat' +require 'protobuf/rpc/middleware' +require 'protobuf/rpc/service_dispatcher' module Protobuf module Rpc - module Server - - # Invoke the service method dictated by the proto wrapper request object - def handle_client - @stats.request_size = @buffer.size - - # Setup the initial request and response - @request = Protobuf::Socketrpc::Request.new - @response = Protobuf::Socketrpc::Response.new - - # Parse the protobuf request from the socket - log_debug "[#{log_signature}] Parsing request from client" - parse_request_from_buffer - - # Determine the service class and method name from the request - log_debug "[#{log_signature}] Extracting procedure call info from request" - parse_service_info - - # Call the service method - log_debug "[#{log_signature}] Dispatching client request to service" - invoke_rpc_method - rescue => error - # Ensure we're handling any errors that try to slip out the back door - log_error error.message - log_error error.backtrace.join("\n") - handle_error(error) - send_response - end + module Server + def gc_pause + ::GC.disable if ::Protobuf.gc_pause_server_request? - # Client error handler. Receives an exception object and writes it into the @response - def handle_error(error) - log_debug "[#{log_signature}] handle_error: %s" % error.inspect - if error.respond_to?(:to_response) - error.to_response(@response) - else - message = error.respond_to?(:message) ? error.message : error.to_s - code = error.respond_to?(:code) ? error.code.to_s : "RPC_ERROR" - PbError.new(message, code).to_response(@response) - end - end - - # Assuming all things check out, we can call the service method - def invoke_rpc_method - # Get a new instance of the service - @service = @klass.new - - # Define our response callback to perform the "successful" response to our client - # This decouples the service's rpc method from our response to the client, - # allowing the service to be the dictator for when the response should be sent back. - # - # In other words, we don't send the response once the service method finishes executing - # since the service may perform it's own operations asynchronously. - @service.on_send_response do |response| - unless @did_respond - parse_response_from_service(response) - send_response - end - end - - @service.on_rpc_failed do |error| - unless @did_respond - handle_error(error) - send_response - end - end - - # Call the service method - log_debug "[#{log_signature}] Invoking %s#%s with request %s" % [@klass.name, @method, @request.inspect] - @service.__send__(@method, @request) - end + yield - def log_signature - @log_signature ||= "server-#{self.class}" - end - - # Parse the incoming request object into our expected request object - def parse_request_from_buffer - log_debug "[#{log_signature}] parsing request from buffer: %s" % @buffer.data.inspect - @request.parse_from_string(@buffer.data) - rescue => error - exc = BadRequestData.new 'Unable to parse request: %s' % error.message - log_error exc.message - raise exc + ::GC.enable if ::Protobuf.gc_pause_server_request? end - # Read out the response from the service method, - # setting it on the pb request, and serializing the - # response to the protobuf response wrapper - def parse_response_from_service(response) - expected = @klass.rpcs[@klass][@method].response_type - - # Cannibalize the response if it's a Hash - response = expected.new(response) if response.is_a?(Hash) - actual = response.class - log_debug "[#{log_signature}] response (should/actual): %s/%s" % [expected.name, actual.name] - - # Determine if the service tried to change response types on us - if expected == actual - serialize_response(response) - else - # response types do not match, throw the appropriate error - raise BadResponseProto, 'Response proto changed from %s to %s' % [expected.name, actual.name] - end - rescue => error - log_error error.message - log_error error.backtrace.join("\n") - handle_error(error) - end - - # Parses and returns the service and method name from the request wrapper proto - def parse_service_info - @klass = Util.constantize(@request.service_name) - @method = Util.underscore(@request.method_name).to_sym + # Invoke the service method dictated by the proto wrapper request object + # + def handle_request(request_data, env_data = {}) + # Create an env object that holds different parts of the environment and + # is available to all of the middlewares + env = Env.new(env_data.merge('encoded_request' => request_data, 'log_signature' => log_signature)) - unless @klass.instance_methods.include?(@method) - raise MethodNotFound, "Service method #{@request.method_name} is not defined by the service" - end - - @stats.service = @klass.name - @stats.method = @method - rescue NameError - raise ServiceNotFound, "Service class #{@request.service_name} is not found" - end + # Invoke the middleware stack, the last of which is the service dispatcher + env = Rpc.middleware.call(env) - # Write the response wrapper to the client - def send_response - raise 'Response already sent to client' if @did_respond - log_debug "[#{log_signature}] Sending response to client: %s" % @response.inspect - response_buffer = Protobuf::Rpc::Buffer.new(:write, @response) - send_data(response_buffer.write) - @stats.response_size = response_buffer.size - @stats.end - @stats.log_stats - @did_respond = true + env.encoded_response end - def serialize_response(response) - log_debug "[#{log_signature}] serializing response: %s" % response.inspect - @response.response_proto = response.serialize_to_string - rescue - raise BadResponseProto, $!.message + def log_signature + @_log_signature ||= "[server-#{self.class.name}]" end - end - end - end diff --git a/lib/protobuf/rpc/servers/evented_runner.rb b/lib/protobuf/rpc/servers/evented_runner.rb deleted file mode 100644 index 951f09c1..00000000 --- a/lib/protobuf/rpc/servers/evented_runner.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Protobuf - module Rpc - class EventedRunner - - def self.stop - EventMachine.stop_event_loop if EventMachine.reactor_running? - Protobuf::Logger.info 'Shutdown complete' - end - - def self.run(server) - # Ensure errors thrown within EM are caught and logged appropriately - EventMachine.error_handler do |error| - if error.message == 'no acceptor' - raise 'Failed binding to %s:%d (%s)' % [server.host, server.port, error.message] - else - Protobuf::Logger.error error.message - Protobuf::Logger.error error.backtrace.join("\n") - end - end - - # Startup and run the rpc server - EM.schedule do - EventMachine.start_server(server.host, server.port, Protobuf::Rpc::EventedServer) && \ - Protobuf::Logger.info('RPC Server listening at %s:%d in %s' % [server.host, server.port, server.env]) - end - - # Join or start the reactor - EM.reactor_running? ? EM.reactor_thread.join : EM.run - end - end - end -end diff --git a/lib/protobuf/rpc/servers/evented_server.rb b/lib/protobuf/rpc/servers/evented_server.rb deleted file mode 100644 index d34c8108..00000000 --- a/lib/protobuf/rpc/servers/evented_server.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'protobuf/rpc/server' - -module Protobuf - module Rpc - class EventedServer < EventMachine::Connection - include Protobuf::Rpc::Server - include Protobuf::Logger::LogMethods - - # Initialize a new read buffer for storing client request info - def post_init - log_debug '[server] Post init, new read buffer created' - @stats = Protobuf::Rpc::Stat.new(:SERVER, true) - @stats.client = Socket.unpack_sockaddr_in(get_peername) - - @buffer = Protobuf::Rpc::Buffer.new(:read) - @did_respond = false - end - - # Receive a chunk of data, potentially flushed to handle_client - def receive_data(data) - log_debug '[server] receive_data: %s' % data - @buffer << data - handle_client if @buffer.flushed? - end - - end - end -end diff --git a/lib/protobuf/rpc/servers/socket/server.rb b/lib/protobuf/rpc/servers/socket/server.rb new file mode 100644 index 00000000..28cde2eb --- /dev/null +++ b/lib/protobuf/rpc/servers/socket/server.rb @@ -0,0 +1,121 @@ +require 'set' + +require 'protobuf/rpc/servers/socket/worker' + +module Protobuf + module Rpc + module Socket + class Server + include ::Protobuf::Logging + + AUTO_COLLECT_TIMEOUT = 5 # seconds + + private + + attr_accessor :threshold, :host, :port, :backlog + attr_writer :running + + public + + attr_reader :running + alias :running? running + + def initialize(options) + self.running = false + self.host = options.fetch(:host) + self.port = options.fetch(:port) + self.backlog = options.fetch(:backlog, 100) + self.threshold = options.fetch(:threshold, 100) + end + + def threads + @threads ||= [] + end + + def working + @working ||= Set.new + end + + def cleanup? + # every `threshold` connections run a cleanup routine after closing the response + !threads.empty? && threads.size % threshold == 0 + end + + def cleanup_threads + logger.debug { sign_message("Thread cleanup - #{threads.size} - start") } + + threads.delete_if do |hash| + unless (thread = hash.fetch(:thread)).alive? + thread.join + working.delete(hash.fetch(:socket)) + end + end + + logger.debug { sign_message("Thread cleanup - #{threads.size} - complete") } + end + + def log_signature + @_log_signature ||= "[server-#{self.class.name}]" + end + + def new_worker(socket) + Thread.new(socket) do |sock| + ::Protobuf::Rpc::Socket::Worker.new(sock, &:close) + end + end + + def run + logger.debug { sign_message("Run") } + + server = ::TCPServer.new(host, port) + fail "The server was unable to start properly." if server.closed? + + begin + server.listen(backlog) + listen_fds = [server] + self.running = true + + while running? + logger.debug { sign_message("Waiting for connections") } + ready_cnxns = begin + IO.select(listen_fds, [], [], AUTO_COLLECT_TIMEOUT) + rescue IOError + nil + end + + if ready_cnxns + ready_cnxns.first.each do |client| + case + when !running? + # no-op + when client == server + logger.debug { sign_message("Accepted new connection") } + client, _sockaddr = server.accept + listen_fds << client + else + unless working.include?(client) + working << listen_fds.delete(client) + logger.debug { sign_message("Working") } + threads << { :thread => new_worker(client), :socket => client } + + cleanup_threads if cleanup? + end + end + end + elsif threads.size > 1 + # Run a cleanup if select times out while waiting + cleanup_threads + end + end + ensure + server.close + end + end + + def stop + self.running = false + end + end + end + end +end diff --git a/lib/protobuf/rpc/servers/socket/worker.rb b/lib/protobuf/rpc/servers/socket/worker.rb new file mode 100644 index 00000000..6297d20a --- /dev/null +++ b/lib/protobuf/rpc/servers/socket/worker.rb @@ -0,0 +1,56 @@ +require 'protobuf/rpc/server' +require 'protobuf/logging' + +module Protobuf + module Rpc + module Socket + class Worker + include ::Protobuf::Rpc::Server + include ::Protobuf::Logging + + def initialize(sock, &complete_cb) + @socket = sock + @complete_cb = complete_cb + + data = read_data + return unless data + + gc_pause do + encoded_response = handle_request(data) + send_data(encoded_response) + end + end + + def read_data + size_io = StringIO.new + + until (size_reader = @socket.getc) == "-" + size_io << size_reader + end + str_size_io = size_io.string + + @socket.read(str_size_io.to_i) + end + + def send_data(data) + fail 'Socket closed unexpectedly' unless socket_writable? + response_buffer = Protobuf::Rpc::Buffer.new(:write) + response_buffer.set_data(data) + + @socket.write(response_buffer.write) + @socket.flush + + @complete_cb.call(@socket) + end + + def log_signature + @_log_signature ||= "[server-#{self.class}-#{object_id}]" + end + + def socket_writable? + ! @socket.nil? && ! @socket.closed? + end + end + end + end +end diff --git a/lib/protobuf/rpc/servers/socket_runner.rb b/lib/protobuf/rpc/servers/socket_runner.rb index 54c748f9..8d36182d 100644 --- a/lib/protobuf/rpc/servers/socket_runner.rb +++ b/lib/protobuf/rpc/servers/socket_runner.rb @@ -1,28 +1,46 @@ module Protobuf module Rpc - class SocketRunner + class SocketRunner - def self.stop - Protobuf::Rpc::SocketServer.stop - Protobuf::Logger.info 'Shutdown complete' + private + + attr_accessor :server + + public + + def initialize(options) + options = case + when options.is_a?(OpenStruct) then + options.marshal_dump + when options.respond_to?(:to_hash) then + options.to_hash.symbolize_keys + else + fail "Cannot parser Socket Server - server options" + end + + self.server = ::Protobuf::Rpc::Socket::Server.new(options) end - def self.run(server) - Protobuf::Logger.info "SocketServer Running" - server_config = case - when server.is_a?(OpenStruct) then - server.marshal_dump - when server.is_a?(Hash) then - server - when server.respond_to?(:to_hash) then - server.to_hash - else - raise "Cannot parser Socket Server - server options" - end - - Protobuf::Rpc::SocketServer.run(server_config) + def run + yield if block_given? + server.run + end + + def running? + server.running? + end + + def stop + server.stop end end + end +end +module Protobuf + module Rpc + module Servers # bad file namespacing + SocketRunner = ::Protobuf::Rpc::SocketRunner + end end end diff --git a/lib/protobuf/rpc/servers/socket_server.rb b/lib/protobuf/rpc/servers/socket_server.rb deleted file mode 100644 index d7b5ea60..00000000 --- a/lib/protobuf/rpc/servers/socket_server.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'protobuf/rpc/server' - -module Protobuf - module Rpc - class SocketServer - include Protobuf::Rpc::Server - include Protobuf::Logger::LogMethods - - - def self.cleanup? - # every 10 connections run a cleanup routine after closing the response - @threads.size > (@thread_threshold - 1) && (@threads.size % @thread_threshold) == 0 - end - - def self.cleanup_threads - log_debug "[#{log_signature}] Thread cleanup - #{@threads.size} - start" - - @threads = @threads.select do |t| - if t[:thread].alive? - true - else - t[:thread].join - @working.delete(t[:socket]) - false - end - end - - log_debug "[#{log_signature}] Thread cleanup - #{@threads.size} - complete" - end - - def self.log_signature - @log_signature ||= "server-#{self}" - end - - def self.new_worker(socket) - Thread.new(socket) do |sock| - Protobuf::Rpc::SocketServer::Worker.new(sock) do |s| - s.close - end - end - end - - def self.run(opts = {}) - log_debug "[#{log_signature}] Run" - host = opts.fetch(:host, "127.0.0.1") - port = opts.fetch(:port, 9399) - backlog = opts.fetch(:backlog, 100) - thread_threshold = opts.fetch(:thread_threshold, 100) - auto_collect_timeout = opts.fetch(:auto_collect_timeout, 20) - - @running = true - @threads = [] - @thread_threshold = thread_threshold - @server = TCPServer.new(host, port) - @server.listen(backlog) - @working = [] - @listen_fds = [@server] - - while running? - log_debug "[#{log_signature}] Waiting for connections" - - if ready_cnxns = IO.select(@listen_fds, [], [], auto_collect_timeout) - cnxns = ready_cnxns.first - cnxns.each do |client| - case - when !running? then - # no-op - when client == @server then - log_debug "[#{log_signature}] Accepted new connection" - client, sockaddr = @server.accept - @listen_fds << client - else - if !@working.include?(client) - @working << @listen_fds.delete(client) - log_debug "[#{log_signature}] Working" - @threads << { :thread => new_worker(client), :socket => client } - - cleanup_threads if cleanup? - end - end - end - else - # Run a cleanup if select times out while waiting - cleanup_threads if @threads.size > 1 - end - end - - rescue - # Closing the server causes the loop to raise an exception here - raise if running? - end - - def self.running? - @running - end - - def self.stop - @running = false - @server.close if @server - end - - class Worker - include Protobuf::Rpc::Server - include Protobuf::Logger::LogMethods - - def initialize(sock, &complete_cb) - @did_response = false - @socket = sock - @request = Protobuf::Socketrpc::Request.new - @response = Protobuf::Socketrpc::Response.new - @buffer = Protobuf::Rpc::Buffer.new(:read) - @stats = Protobuf::Rpc::Stat.new(:SERVER, true) - @complete_cb = complete_cb - log_debug "[#{log_signature}] Post init, new read buffer created" - - @stats.client = Socket.unpack_sockaddr_in(@socket.getpeername) - @buffer << read_data - log_debug "[#{log_signature}] handling request" - handle_client if @buffer.flushed? - end - - def log_signature - @log_signature ||= "server-#{self.class}-#{object_id}" - end - - def read_data - size_io = StringIO.new - - while((size_reader = @socket.getc) != "-") - size_io << size_reader - end - str_size_io = size_io.string - - "#{str_size_io}-#{@socket.read(str_size_io.to_i)}" - end - - def send_data(data) - log_debug "[#{log_signature}] sending data : %s" % data - @socket.write(data) - @socket.flush - @complete_cb.call(@socket) - end - end - end - end -end diff --git a/lib/protobuf/rpc/servers/zmq/broker.rb b/lib/protobuf/rpc/servers/zmq/broker.rb new file mode 100644 index 00000000..2b12aee8 --- /dev/null +++ b/lib/protobuf/rpc/servers/zmq/broker.rb @@ -0,0 +1,194 @@ +require 'thread' + +module Protobuf + module Rpc + module Zmq + class Broker + include ::Protobuf::Rpc::Zmq::Util + + attr_reader :local_queue + + def initialize(server) + @server = server + + init_zmq_context + init_local_queue + init_backend_socket + init_frontend_socket + init_poller + rescue + teardown + raise + end + + def run + @idle_workers = [] + @running = true + + loop do + process_local_queue + rc = @poller.poll(broker_polling_milliseconds) + + # The server was shutdown and no requests are pending + break if rc == 0 && !running? && @server.workers.empty? + # Something went wrong + break if rc == -1 + + check_and_process_backend + process_local_queue # Fair ordering so queued requests get in before new requests + check_and_process_frontend + end + ensure + teardown + @running = false + end + + def running? + @running && @server.running? + end + + private + + def backend_poll_weight + @backend_poll_weight ||= [ENV["PB_ZMQ_SERVER_BACKEND_POLL_WEIGHT"].to_i, 1].max + end + + def broker_polling_milliseconds + @broker_polling_milliseconds ||= [ENV["PB_ZMQ_BROKER_POLLING_MILLISECONDS"].to_i, 500].max + end + + def check_and_process_backend + readables_include_backend = @poller.readables.include?(@backend_socket) + message_count_read_from_backend = 0 + + while readables_include_backend && message_count_read_from_backend < backend_poll_weight + message_count_read_from_backend += 1 + process_backend + @poller.poll_nonblock + readables_include_backend = @poller.readables.include?(@backend_socket) + end + end + + def check_and_process_frontend + readables_include_frontend = @poller.readables.include?(@frontend_socket) + message_count_read_from_frontend = 0 + + while readables_include_frontend && message_count_read_from_frontend < frontend_poll_weight + message_count_read_from_frontend += 1 + process_frontend + break unless local_queue_available? # no need to read frontend just to throw away messages, will prioritize backend when full + @poller.poll_nonblock + readables_include_frontend = @poller.readables.include?(@frontend_socket) + end + end + + def frontend_poll_weight + @frontend_poll_weight ||= [ENV["PB_ZMQ_SERVER_FRONTEND_POLL_WEIGHT"].to_i, 1].max + end + + def init_backend_socket + @backend_socket = @zmq_context.socket(ZMQ::ROUTER) + zmq_error_check(@backend_socket.bind(@server.backend_uri)) + end + + def init_frontend_socket + @frontend_socket = @zmq_context.socket(ZMQ::ROUTER) + zmq_error_check(@frontend_socket.bind(@server.frontend_uri)) + end + + def init_local_queue + @local_queue = [] + end + + def init_poller + @poller = ZMQ::Poller.new + @poller.register_readable(@frontend_socket) + @poller.register_readable(@backend_socket) + end + + def init_zmq_context + @zmq_context = + if inproc? + @server.zmq_context + else + ZMQ::Context.new + end + end + + def inproc? + !!@server.try(:inproc?) + end + + def local_queue_available? + local_queue.size < local_queue_max_size && running? + end + + def local_queue_max_size + @local_queue_max_size ||= [ENV["PB_ZMQ_SERVER_QUEUE_MAX_SIZE"].to_i, 5].max + end + + def process_backend + worker, _ignore, *frames = read_from_backend + + @idle_workers << worker + + unless frames == [::Protobuf::Rpc::Zmq::WORKER_READY_MESSAGE] + write_to_frontend(frames) + end + end + + def process_frontend + address, _, message, *frames = read_from_frontend + + if message == ::Protobuf::Rpc::Zmq::CHECK_AVAILABLE_MESSAGE + if local_queue_available? + write_to_frontend([address, ::Protobuf::Rpc::Zmq::EMPTY_STRING, ::Protobuf::Rpc::Zmq::WORKERS_AVAILABLE]) + else + write_to_frontend([address, ::Protobuf::Rpc::Zmq::EMPTY_STRING, ::Protobuf::Rpc::Zmq::NO_WORKERS_AVAILABLE]) + end + else + if @idle_workers.empty? # rubocop:disable Style/IfInsideElse + local_queue << [address, ::Protobuf::Rpc::Zmq::EMPTY_STRING, message].concat(frames) + else + write_to_backend([@idle_workers.shift, ::Protobuf::Rpc::Zmq::EMPTY_STRING].concat([address, ::Protobuf::Rpc::Zmq::EMPTY_STRING, message]).concat(frames)) + end + end + end + + def process_local_queue + return if local_queue.empty? + return if @idle_workers.empty? + + write_to_backend([@idle_workers.shift, ::Protobuf::Rpc::Zmq::EMPTY_STRING].concat(local_queue.shift)) + process_local_queue + end + + def read_from_backend + frames = [] + zmq_error_check(@backend_socket.recv_strings(frames)) + frames + end + + def read_from_frontend + frames = [] + zmq_error_check(@frontend_socket.recv_strings(frames)) + frames + end + + def teardown + @frontend_socket.try(:close) + @backend_socket.try(:close) + @zmq_context.try(:terminate) unless inproc? + end + + def write_to_backend(frames) + zmq_error_check(@backend_socket.send_strings(frames)) + end + + def write_to_frontend(frames) + zmq_error_check(@frontend_socket.send_strings(frames)) + end + end + end + end +end diff --git a/lib/protobuf/rpc/servers/zmq/server.rb b/lib/protobuf/rpc/servers/zmq/server.rb new file mode 100644 index 00000000..b5dce25a --- /dev/null +++ b/lib/protobuf/rpc/servers/zmq/server.rb @@ -0,0 +1,321 @@ +require 'protobuf/rpc/servers/zmq/util' +require 'protobuf/rpc/servers/zmq/worker' +require 'protobuf/rpc/servers/zmq/broker' +require 'protobuf/rpc/dynamic_discovery.pb' +require 'securerandom' +require 'thread' + +module Protobuf + module Rpc + module Zmq + class Server + include ::Protobuf::Rpc::Zmq::Util + + DEFAULT_OPTIONS = { + :beacon_interval => 5, + :broadcast_beacons => false, + :broadcast_busy => false, + :zmq_inproc => true, + }.freeze + + attr_accessor :options, :workers + attr_reader :zmq_context + + def initialize(options) + @options = DEFAULT_OPTIONS.merge(options) + @workers = [] + + init_zmq_context + init_beacon_socket if broadcast_beacons? + init_shutdown_pipe + rescue + teardown + raise + end + + def add_worker + @total_workers = total_workers + 1 + end + + def all_workers_busy? + workers.all? { |thread| !!thread[:busy] } + end + + def backend_port + options[:worker_port] || frontend_port + 1 + end + + def backend_uri + if inproc? + "inproc://#{backend_ip}:#{backend_port}" + else + "tcp://#{backend_ip}:#{backend_port}" + end + end + + def beacon_interval + [options[:beacon_interval].to_i, 1].max + end + + def beacon_ip + "255.255.255.255" + end + + def beacon_port + @beacon_port ||= options.fetch( + :beacon_port, + ::Protobuf::Rpc::ServiceDirectory.port, + ).to_i + end + + def beacon_uri + "udp://#{beacon_ip}:#{beacon_port}" + end + + def broadcast_beacons? + !brokerless? && options[:broadcast_beacons] + end + + def broadcast_busy? + broadcast_beacons? && options[:broadcast_busy] + end + + def broadcast_flatline + flatline = ::Protobuf::Rpc::DynamicDiscovery::Beacon.new( + :beacon_type => ::Protobuf::Rpc::DynamicDiscovery::BeaconType::FLATLINE, + :server => to_proto, + ) + + @beacon_socket.send(flatline.encode, 0) + end + + def broadcast_heartbeat + @last_beacon = Time.now.to_i + + heartbeat = ::Protobuf::Rpc::DynamicDiscovery::Beacon.new( + :beacon_type => ::Protobuf::Rpc::DynamicDiscovery::BeaconType::HEARTBEAT, + :server => to_proto, + ) + + @beacon_socket.send(heartbeat.encode, 0) + + logger.debug { sign_message("sent heartbeat to #{beacon_uri}") } + end + + def broadcast_heartbeat? + Time.now.to_i >= next_beacon && broadcast_beacons? + end + + def brokerless? + !!options[:workers_only] + end + + def busy_worker_count + workers.count { |thread| !!thread[:busy] } + end + + def frontend_ip + @frontend_ip ||= resolve_ip(options[:host]) + end + alias :backend_ip frontend_ip + + def frontend_port + options[:port] + end + + def frontend_uri + "tcp://#{frontend_ip}:#{frontend_port}" + end + + def inproc? + !!options[:zmq_inproc] + end + + def maintenance_timeout + next_maintenance - Time.now.to_i + end + + def next_maintenance + cycles = [next_reaping] + cycles << next_beacon if broadcast_beacons? + + cycles.min + end + + def minimum_timeout + 0.1 + end + + def next_beacon + if @last_beacon.nil? + 0 + else + @last_beacon + beacon_interval + end + end + + def next_reaping + if @last_reaping.nil? + 0 + else + @last_reaping + reaping_interval + end + end + + def reap_dead_workers + @last_reaping = Time.now.to_i + + @workers.keep_if do |worker| + worker.alive? || worker.join && false + end + end + + def reap_dead_workers? + Time.now.to_i >= next_reaping + end + + def reaping_interval + 5 + end + + def run + @running = true + yield if block_given? # runs on startup + wait_for_shutdown_signal + broadcast_flatline if broadcast_beacons? + Thread.pass until reap_dead_workers.empty? + @broker_thread.join unless brokerless? + ensure + @running = false + teardown + end + + def running? + !!@running + end + + def start_missing_workers + missing_workers = total_workers - @workers.size + + if missing_workers > 0 + missing_workers.times { start_worker } + logger.debug { sign_message("#{total_workers} workers started") } + end + end + + def stop + @running = false + @shutdown_w.write('.') + end + + def teardown + @shutdown_r.try(:close) + @shutdown_w.try(:close) + @beacon_socket.try(:close) + @zmq_context.try(:terminate) + @last_reaping = @last_beacon = @timeout = nil + end + + def timeout + @timeout = + if @timeout.nil? + 0 + else + [minimum_timeout, maintenance_timeout].max + end + end + + def total_workers + @total_workers ||= [@options[:threads].to_i, 1].max + end + + def to_proto + @proto ||= ::Protobuf::Rpc::DynamicDiscovery::Server.new( + :uuid => uuid, + :address => frontend_ip, + :port => frontend_port.to_s, + :ttl => (beacon_interval * 1.5).ceil, + :services => ::Protobuf::Rpc::Service.implemented_services, + ) + end + + def uuid + @uuid ||= SecureRandom.uuid + end + + def wait_for_shutdown_signal + loop do + break if IO.select([@shutdown_r], nil, nil, timeout) + + start_broker unless brokerless? + reap_dead_workers if reap_dead_workers? + start_missing_workers + + next unless broadcast_heartbeat? + + if broadcast_busy? && all_workers_busy? + broadcast_flatline + else + broadcast_heartbeat + end + end + end + + private + + def init_beacon_socket + @beacon_socket = UDPSocket.new + @beacon_socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_BROADCAST, true) + @beacon_socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, true) + + if defined?(::Socket::SO_REUSEPORT) + @beacon_socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, true) + end + + @beacon_socket.bind(frontend_ip, beacon_port) + @beacon_socket.connect(beacon_ip, beacon_port) + end + + def init_shutdown_pipe + @shutdown_r, @shutdown_w = IO.pipe + end + + def init_zmq_context + @zmq_context = ZMQ::Context.new + end + + def start_broker + return if @broker && @broker.running? && @broker_thread.alive? + if @broker && !@broker.running? + broadcast_flatline if broadcast_busy? + @broker_thread.join if @broker_thread + init_zmq_context # need a new context to restart the broker + end + + @broker = ::Protobuf::Rpc::Zmq::Broker.new(self) + @broker_thread = Thread.new(@broker) do |broker| + begin + broker.run + rescue => e + message = "Broker failed: #{e.inspect}\n #{e.backtrace.join($INPUT_RECORD_SEPARATOR)}" + $stderr.puts(message) + logger.error { message } + end + end + end + + def start_worker + @workers << Thread.new(self, @broker) do |server, broker| + begin + ::Protobuf::Rpc::Zmq::Worker.new(server, broker).run + rescue => e + message = "Worker failed: #{e.inspect}\n #{e.backtrace.join($INPUT_RECORD_SEPARATOR)}" + $stderr.puts(message) + logger.error { message } + end + end + end + end + end + end +end diff --git a/lib/protobuf/rpc/servers/zmq/util.rb b/lib/protobuf/rpc/servers/zmq/util.rb new file mode 100644 index 00000000..37926464 --- /dev/null +++ b/lib/protobuf/rpc/servers/zmq/util.rb @@ -0,0 +1,48 @@ +require 'resolv' + +module Protobuf + module Rpc + module Zmq + + ADDRESS_MATCH = /\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/ + WORKER_READY_MESSAGE = "\1".freeze + CHECK_AVAILABLE_MESSAGE = "\3".freeze + NO_WORKERS_AVAILABLE = "\4".freeze + WORKERS_AVAILABLE = "\5".freeze + EMPTY_STRING = "".freeze + + module Util + include ::Protobuf::Logging + + def self.included(base) + base.extend(::Protobuf::Rpc::Zmq::Util) + end + + def zmq_error_check(return_code, source = nil) + return if ::ZMQ::Util.resultcode_ok?(return_code) + + fail <<-ERROR + Last ZMQ API call #{source ? "to #{source}" : ''} failed with "#{::ZMQ::Util.error_string}". + + #{caller(1).join($INPUT_RECORD_SEPARATOR)} + ERROR + end + + def log_signature + unless @_log_signature + name = (self.class == Class ? self.name : self.class.name) + @_log_signature = "[server-#{name}-#{object_id}]" + end + + @_log_signature + end + + def resolve_ip(hostname) + ::Resolv.getaddresses(hostname).find do |address| + address =~ ADDRESS_MATCH + end + end + end + end + end +end diff --git a/lib/protobuf/rpc/servers/zmq/worker.rb b/lib/protobuf/rpc/servers/zmq/worker.rb new file mode 100644 index 00000000..85e4aa2a --- /dev/null +++ b/lib/protobuf/rpc/servers/zmq/worker.rb @@ -0,0 +1,105 @@ +require 'protobuf/rpc/server' +require 'protobuf/rpc/servers/zmq/util' +require 'thread' + +module Protobuf + module Rpc + module Zmq + class Worker + include ::Protobuf::Rpc::Server + include ::Protobuf::Rpc::Zmq::Util + + ## + # Constructor + # + def initialize(server, broker) + @server = server + @broker = broker + + init_zmq_context + init_backend_socket + rescue + teardown + raise + end + + ## + # Instance Methods + # + def process_request + client_address, _, data = read_from_backend + return unless data + + gc_pause do + encoded_response = handle_request(data) + write_to_backend([client_address, ::Protobuf::Rpc::Zmq::EMPTY_STRING, encoded_response]) + end + end + + def run + poller = ::ZMQ::Poller.new + poller.register_readable(@backend_socket) + poller.register_readable(@shutdown_socket) + + # Send request to broker telling it we are ready + write_to_backend([::Protobuf::Rpc::Zmq::WORKER_READY_MESSAGE]) + + loop do + rc = poller.poll(500) + + if rc == 0 && !running? # rubocop:disable Style/GuardClause + break # The server was shutdown and no requests are pending + elsif rc == -1 + break # Something went wrong + elsif rc > 0 + ::Thread.current[:busy] = true + process_request + ::Thread.current[:busy] = false + end + end + ensure + teardown + end + + def running? + @broker.running? && @server.running? + end + + private + + def init_zmq_context + @zmq_context = + if inproc? + @server.zmq_context + else + ZMQ::Context.new + end + end + + def init_backend_socket + @backend_socket = @zmq_context.socket(ZMQ::REQ) + zmq_error_check(@backend_socket.connect(@server.backend_uri)) + end + + def inproc? + !!@server.try(:inproc?) + end + + def read_from_backend + frames = [] + zmq_error_check(@backend_socket.recv_strings(frames)) + frames + end + + def teardown + @backend_socket.try(:close) + @zmq_context.try(:terminate) unless inproc? + end + + def write_to_backend(frames) + zmq_error_check(@backend_socket.send_strings(frames)) + end + end + end + end +end diff --git a/lib/protobuf/rpc/servers/zmq_runner.rb b/lib/protobuf/rpc/servers/zmq_runner.rb new file mode 100644 index 00000000..bfab6738 --- /dev/null +++ b/lib/protobuf/rpc/servers/zmq_runner.rb @@ -0,0 +1,70 @@ +require 'ostruct' +require 'thread' + +module Protobuf + module Rpc + class ZmqRunner + include ::Protobuf::Logging + + def initialize(options) + @options = case + when options.is_a?(OpenStruct) then + options.marshal_dump + when options.respond_to?(:to_hash) then + options.to_hash.symbolize_keys + else + fail "Cannot parser Zmq Server - server options" + end + end + + def run + @server = ::Protobuf::Rpc::Zmq::Server.new(@options) + register_signals + @server.run do + yield if block_given? + end + end + + def running? + @server.try :running? + end + + def stop + @server.try :stop + end + + private + + def register_signals + trap(:TRAP) do + ::Thread.list.each do |thread| + logger.info do + <<-THREAD_TRACE + #{thread.inspect}: + #{thread.backtrace.try(:join, $INPUT_RECORD_SEPARATOR)}" + THREAD_TRACE + end + end + end + + trap(:TTIN) do + @server.add_worker + logger.info { "Increased worker size to: #{@server.total_workers}" } + end + + trap(:TTOU) do + logger.info { "Current worker size: #{@server.workers.size}" } + logger.info { "Current busy worker size: #{@server.busy_worker_count}" } + end + end + end + end +end + +module Protobuf + module Rpc + module Servers # bad file namespacing + ZmqRunner = ::Protobuf::Rpc::ZmqRunner + end + end +end diff --git a/lib/protobuf/rpc/service.rb b/lib/protobuf/rpc/service.rb index f8ceae12..cc1974ad 100644 --- a/lib/protobuf/rpc/service.rb +++ b/lib/protobuf/rpc/service.rb @@ -1,243 +1,172 @@ -require 'protobuf/common/logger' +require 'protobuf/logging' +require 'protobuf/message' require 'protobuf/rpc/client' require 'protobuf/rpc/error' +require 'protobuf/rpc/rpc_method' +require 'protobuf/rpc/service_filters' module Protobuf module Rpc # Object to encapsulate the request/response types for a given service method - # - RpcMethod = Struct.new("RpcMethod", :service, :method, :request_type, :response_type) - + class Service - include Protobuf::Logger::LogMethods - - attr_reader :request - attr_accessor :response, :async_responder - private :request, :response, :response= - - DEFAULT_LOCATION = { - :host => '127.0.0.1', - :port => 9399 - } - - # Class methods are intended for use on the client-side. + include ::Protobuf::Logging + include ::Protobuf::Rpc::ServiceFilters + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::ServiceOptions } + + DEFAULT_HOST = '127.0.0.1'.freeze + DEFAULT_PORT = 9399 + + attr_reader :env, :request + + ## + # Constructor! + # + # Initialize a service with the rpc endpoint name and the bytes + # for the request. + def initialize(env) + @env = env.dup # Dup the env so it doesn't change out from under us + @request = env.request + end + + ## + # Class Methods + # + # Create a new client for the given service. + # See Client#initialize and ClientConnection::DEFAULT_OPTIONS + # for all available options. + # + def self.client(options = {}) + ::Protobuf::Rpc::Client.new({ :service => self, + :host => host, + :port => port }.merge(options)) + end + + # Allows service-level configuration of location. + # Useful for system-startup configuration of a service + # so that any Clients using the Service.client sugar + # will not have to configure the location each time. + # + def self.configure(config = {}) + self.host = config[:host] if config.key?(:host) + self.port = config[:port] if config.key?(:port) + end + + # The host location of the service. + # + def self.host + @host ||= DEFAULT_HOST + end + + # The host location setter. # class << self - - # You MUST add the method name to this list if you are adding - # instance methods below, otherwise stuff will definitely break - NON_RPC_METHODS = %w( rpcs call_rpc on_rpc_failed rpc_failed request response method_missing async_responder on_send_response send_response log_signature ) - - # Override methods being added to the class - # If the method isn't already a private instance method, or it doesn't start with rpc_, - # or it isn't in the reserved method list (NON_RPC_METHODS), - # We want to remap the method such that we can wrap it in before and after behavior, - # most notably calling call_rpc against the method. See call_rpc for more info. - def method_added(old) - new_method = :"rpc_#{old}" - return if private_instance_methods.include?(new_method) or old =~ /^rpc_/ or NON_RPC_METHODS.include?(old.to_s) - - alias_method new_method, old - private new_method - - define_method(old) do |pb_request| - call_rpc(old.to_sym, pb_request) + attr_writer :host + end + + # An array of defined service classes that contain implementation + # code + def self.implemented_services + classes = (subclasses || []).select do |subclass| + subclass.rpcs.any? do |(name, _)| + subclass.method_defined? name end - rescue ArgumentError => e - # Wrap a known issue where an instance method was defined in the class without - # it being ignored with NON_RPC_METHODS. - raise ArgumentError, "#{e.message} (Note: This could mean that you need to add the method #{old} to the NON_RPC_METHODS list)" - end - - # Generated service classes should call this method on themselves to add rpc methods - # to the stack with a given request and response type - def rpc(method, request_type, response_type) - rpcs[self] ||= {} - rpcs[self][method] = RpcMethod.new self, method, request_type, response_type end - # Shorthand for @rpcs class instance var - def rpcs - @rpcs ||= {} - end - - # Create a new client for the given service. - # See Client#initialize and ClientConnection::DEFAULT_OPTIONS - # for all available options. - # - def client(options={}) - configure - Client.new({ - :service => self, - :async => false, - :host => self.host, - :port => self.port - }.merge(options)) - end - - # Allows service-level configuration of location. - # Useful for system-startup configuration of a service - # so that any Clients using the Service.client sugar - # will not have to configure the location each time. - # - def configure(config={}) - locations[self] ||= {} - locations[self][:host] = config[:host] if config.key?(:host) - locations[self][:port] = config[:port] if config.key?(:port) - end - - # Shorthand call to configure, passing a string formatted as hostname:port - # e.g. 127.0.0.1:9933 - # e.g. localhost:0 - # - def located_at(location) - return if location.nil? or location.downcase.strip !~ /[a-z0-9.]+:\d+/ - host, port = location.downcase.strip.split ':' - configure :host => host, :port => port.to_i - end - - # The host location of the service - def host - configure - locations[self][:host] || DEFAULT_LOCATION[:host] - end - - # The port of the service on the destination server - def port - configure - locations[self][:port] || DEFAULT_LOCATION[:port] - end - - # Shorthand for @locations class instance var - def locations - @locations ||= {} - end - - end - - def log_signature - @log_signature ||= "service-#{self.class}" - end - - # If a method comes through that hasn't been found, and it - # is defined in the rpcs method list, we know that the rpc - # stub has been created, but no implementing method provides the - # functionality, so throw an appropriate error, otherwise go to super - # - def method_missing m, *params - if rpcs.key?(m) - exc = MethodNotFound.new "#{self}##{m} was defined as a valid rpc method, but was not implemented." - log_error exc.message - raise exc - else - log_error "-------------- [#{log_signature}] %s#%s not rpc method, passing to super" % [self.class.name, m.to_s] - super m, params - end + classes.map(&:name) + end + + # Shorthand call to configure, passing a string formatted as hostname:port + # e.g. 127.0.0.1:9933 + # e.g. localhost:0 + # + def self.located_at(location) + return if location.nil? || location.downcase.strip !~ /.+:\d+/ + host, port = location.downcase.strip.split ':' + configure(:host => host, :port => port.to_i) + end + + # The port of the service on the destination server. + # + def self.port + @port ||= DEFAULT_PORT end - # Convenience wrapper around the rpc method list for a given class + # The port location setter. + # + class << self + attr_writer :port + end + + # Define an rpc method with the given request and response types. + # This methods is only used by the generated service definitions + # and not useful for user code. + # + def self.rpc(method, request_type, response_type, &options_block) + rpcs[method] = RpcMethod.new(method, request_type, response_type, &options_block) + end + + # Hash containing the set of methods defined via `rpc`. + # + def self.rpcs + @rpcs ||= {} + end + + # Check if the given method name is a known rpc endpoint. + # + def self.rpc_method?(name) + rpcs.key?(name) + end + + def call(method_name) + run_filters(method_name) + end + + # Response object for this rpc cycle. Not assignable. + # + def response + @response ||= response_type.new + end + + # Convenience method to get back to class method. + # + def rpc_method?(name) + self.class.rpc_method?(name) + end + + # Convenience method to get back to class rpcs hash. + # def rpcs - self.class.rpcs[self.class] + self.class.rpcs end - - # Callback register for the server when a service - # method calls rpc_failed. Called by Service#rpc_failed. - def on_rpc_failed(&rpc_failure_cb) - @rpc_failure_cb = rpc_failure_cb + + private + + def request_type + @request_type ||= env.request_type end - + + # Sugar to make an rpc method feel like a controller method. + # If this method is not called, the response will be the memoized + # object returned by the response reader. + # + def respond_with(candidate) + @response = candidate + end + alias :return_from_whence_you_came respond_with + + def response_type + @response_type ||= env.response_type + end + # Automatically fail a service method. - # NOTE: This shortcuts the @async_responder paradigm. There is - # not any way to get around this currently (and I'm not sure you should want to). - # - def rpc_failed(message="RPC Failed while executing service method #{@current_method}") - error_message = 'Unable to invoke rpc_failed, no failure callback is setup.' - log_and_raise_error(error_message) if @rpc_failure_cb.nil? - error = message.is_a?(String) ? RpcFailed.new(message) : message - log_warn "[#{log_signature}] RPC Failed: %s" % error.message - @rpc_failure_cb.call(error) - end - - # Callback register for the server to be notified - # when it is appropriate to generate a response to the client. - # Used in conjunciton with Service#send_response. - # - def on_send_response(&responder) - @responder = responder - end - - # Tell the server to generate response and send it to the client. - # - # NOTE: If @async_responder is set to true, this MUST be called by - # the implementing service method, otherwise the connection - # will timeout since no data will be sent. - # - def send_response - error_message = "Unable to send response, responder is nil. It appears you aren't inside of an RPC request/response cycle." - log_and_raise_error(error_message) if @responder.nil? - @responder.call(@response) - end - - private - - def log_and_raise_error(error_message) - log_error(error_message) - raise error_message - end - - # Call the rpc method that was previously privatized. - # call_rpc allows us to wrap the normal method call with - # before and after behavior, most notably setting up the request - # and response instances. - # - # Implementing rpc methods should be aware - # that request and response are implicitly available, and - # that response should be manipulated during the rpc method, - # as there is no way to reliably determine the response like - # a normal (http-based) controller method would be able to. - # - # Async behavior of responding can be achieved in the rpc method - # by explicitly setting self.async_responder = true. It is then - # the responsibility of the service method to send the response, - # by calling self.send_response without any arguments. The rpc - # server is setup to handle synchronous and asynchronous responses. - # - def call_rpc(method, pb_request) - @current_method = method - - # Allows the service to set whether or not - # it would like to asynchronously respond to the connected client(s) - @async_responder = false - - # Setup the request - @request = rpcs[method].request_type.new - @request.parse_from_string(pb_request.request_proto) - rescue - exc = BadRequestProto.new 'Unable to parse request: %s' % $!.message - log_error exc.message - log_error $!.backtrace.join("\n") - raise exc - else # when no Exception was thrown - # Setup the response - @response = rpcs[method].response_type.new - - log_debug "[#{log_signature}] calling service method %s#%s" % [self.class, method] - # Call the aliased rpc method (e.g. :rpc_find for :find) - __send__("rpc_#{method}".to_sym) - log_debug "[#{log_signature}] completed service method %s#%s" % [self.class, method] - - # Pass the populated response back to the server - # Note this will only get called if the rpc method didn't explode (by design) - if @async_responder - log_debug "[#{log_signature}] async request, not sending response (yet)" - else - log_debug "[#{log_signature}] trigger server send_response" - send_response - end + # + def rpc_failed(message) + message = message.message if message.respond_to?(:message) + fail RpcFailed, message end - end - - end + ActiveSupport.run_load_hooks(:protobuf_rpc_service, Service) + end end diff --git a/lib/protobuf/rpc/service_directory.rb b/lib/protobuf/rpc/service_directory.rb new file mode 100644 index 00000000..7c467358 --- /dev/null +++ b/lib/protobuf/rpc/service_directory.rb @@ -0,0 +1,261 @@ +require 'delegate' +require 'singleton' +require 'socket' +require 'set' +require 'thread' +require 'timeout' + +require 'protobuf/rpc/dynamic_discovery.pb' + +module Protobuf + module Rpc + def self.service_directory + @service_directory ||= ::Protobuf::Rpc::ServiceDirectory.instance + end + + def self.service_directory=(directory) + @service_directory = directory + end + + class ServiceDirectory + include ::Singleton + include ::Protobuf::Logging + + DEFAULT_ADDRESS = '0.0.0.0'.freeze + DEFAULT_PORT = 53000 + DEFAULT_TIMEOUT = 1 + + class Listing < SimpleDelegator + attr_reader :expires_at + + def initialize(server) + update(server) + end + + def current? + !expired? + end + + def eql?(other) + uuid.eql?(other.uuid) + end + + def expired? + Time.now.to_i >= @expires_at + end + + def hash + uuid.hash + end + + def ttl + [super.to_i, 1].max + end + + def update(server) + __setobj__(server) + @expires_at = Time.now.to_i + ttl + end + end + + # Class Methods + # + class << self + attr_writer :address, :port + end + + def self.address + @address ||= DEFAULT_ADDRESS + end + + def self.port + @port ||= DEFAULT_PORT + end + + def self.start + yield(self) if block_given? + instance.start + end + + def self.stop + instance.stop + end + + # + # Instance Methods + # + def initialize + reset + end + + def all_listings_for(service) + if running? && @listings_by_service.key?(service.to_s) + start_listener_thread if listener_dead? + @listings_by_service[service.to_s].entries.shuffle + else + [] + end + end + + def each_listing(&block) + start_listener_thread if listener_dead? + @listings_by_uuid.each_value(&block) + end + + def lookup(service) + return unless running? + start_listener_thread if listener_dead? + return unless @listings_by_service.key?(service.to_s) + @listings_by_service[service.to_s].entries.sample + end + + def listener_dead? + @thread.nil? || !@thread.alive? + end + + def restart + stop + start + end + + def running? + !!@running + end + + def start + unless running? + init_socket + logger.info { sign_message("listening to udp://#{self.class.address}:#{self.class.port}") } + @running = true + end + + start_listener_thread if listener_dead? + self + end + + def start_listener_thread + return if @thread.try(:alive?) + @thread = Thread.new { send(:run) } + end + + def stop + logger.info { sign_message("Stopping directory") } + + @running = false + @thread.try(:kill).try(:join) + @socket.try(:close) + + reset + end + + private + + def add_or_update_listing(uuid, server) + listing = @listings_by_uuid[uuid] + + if listing + action = :updated + listing.update(server) + else + action = :added + listing = Listing.new(server) + @listings_by_uuid[uuid] = listing + end + + listing.services.each do |service| + @listings_by_service[service] << listing + end + + trigger(action, listing) + logger.debug { sign_message("#{action} server: #{server.inspect}") } + end + + def init_socket + @socket = UDPSocket.new + @socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEADDR, true) + + if defined?(::Socket::SO_REUSEPORT) + @socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, true) + end + + @socket.bind(self.class.address, self.class.port.to_i) + end + + def process_beacon(beacon) + server = beacon.server + uuid = server.try(:uuid) + + if server && uuid + case beacon.beacon_type + when ::Protobuf::Rpc::DynamicDiscovery::BeaconType::HEARTBEAT + add_or_update_listing(uuid, server) + when ::Protobuf::Rpc::DynamicDiscovery::BeaconType::FLATLINE + remove_listing(uuid) + end + else + logger.info { sign_message("Ignoring incomplete beacon: #{beacon.inspect}") } + end + end + + def read_beacon + data, addr = @socket.recvfrom(2048) + + beacon = ::Protobuf::Rpc::DynamicDiscovery::Beacon.decode(data) + + # Favor the address captured by the socket + beacon.try(:server).try(:address=, addr[3]) + + beacon + end + + def remove_expired_listings + logger.debug { sign_message("Removing expired listings") } + @listings_by_uuid.each do |uuid, listing| + remove_listing(uuid) if listing.expired? + end + end + + def remove_listing(uuid) + listing = @listings_by_uuid[uuid] || return + + logger.debug { sign_message("Removing listing: #{listing.inspect}") } + + @listings_by_service.each_value do |listings| + listings.delete(listing) + end + + trigger(:removed, @listings_by_uuid.delete(uuid)) + end + + def reset + @thread = nil + @socket = nil + @listings_by_uuid = {} + @listings_by_service = Hash.new { |h, k| h[k] = Set.new } + end + + def run + sweep_interval = 5 # sweep expired listings every 5 seconds + next_sweep = Time.now.to_i + sweep_interval + + loop do + timeout = [next_sweep - Time.now.to_i, 0.1].max + readable = IO.select([@socket], nil, nil, timeout) + process_beacon(read_beacon) if readable + + if Time.now.to_i >= next_sweep + remove_expired_listings + next_sweep = Time.now.to_i + sweep_interval + end + end + rescue => e + logger.debug { sign_message("ERROR: (#{e.class}) #{e.message}\n#{e.backtrace.join("\n")}") } + retry + end + + def trigger(action, listing) + ::ActiveSupport::Notifications.instrument("directory.listing.#{action}", :listing => listing) + end + end + end +end diff --git a/lib/protobuf/rpc/service_dispatcher.rb b/lib/protobuf/rpc/service_dispatcher.rb new file mode 100644 index 00000000..8144b41d --- /dev/null +++ b/lib/protobuf/rpc/service_dispatcher.rb @@ -0,0 +1,45 @@ +require 'protobuf/logging' + +module Protobuf + module Rpc + class ServiceDispatcher + include ::Protobuf::Logging + + attr_reader :env + + def initialize(_app) + # End of the line... + end + + def call(env) + dup._call(env) + end + + def _call(env) + @env = env + + env.response = dispatch_rpc_request + env + end + + def rpc_service + @rpc_service ||= env.rpc_service.new(env) + end + + private + + def dispatch_rpc_request + rpc_service.call(method_name) + rpc_service.response + end + + def method_name + env.method_name + end + + def service_name + env.service_name + end + end + end +end diff --git a/lib/protobuf/rpc/service_filters.rb b/lib/protobuf/rpc/service_filters.rb new file mode 100644 index 00000000..c461512d --- /dev/null +++ b/lib/protobuf/rpc/service_filters.rb @@ -0,0 +1,250 @@ +module Protobuf + module Rpc + module ServiceFilters + + def self.included(other) + other.class_eval do + extend Protobuf::Rpc::ServiceFilters::ClassMethods + include Protobuf::Rpc::ServiceFilters::InstanceMethods + end + end + + module ClassMethods + + [:after, :around, :before].each do |type| + # Setter DSL method for given filter types. + # + define_method "#{type}_filter" do |*args| + set_filters(type, *args) + end + alias_method "#{type}_action", "#{type}_filter" + end + + # Filters hash keyed based on filter type (e.g. :before, :after, :around), + # whose values are Sets. + # + def filters + @filters ||= Hash.new { |h, k| h[k] = [] } + end + + # Filters hash keyed based on filter type (e.g. :before, :after, :around), + # whose values are Sets. + # + def rescue_filters + @rescue_filters ||= {} + end + + def rescue_from(*ex_klasses, &block) + options = ex_klasses.last.is_a?(Hash) ? ex_klasses.pop : {} + callable = options.delete(:with) { block } + fail ArgumentError, 'Option :with missing from rescue_from options' if callable.nil? + ex_klasses.each { |ex_klass| rescue_filters[ex_klass] = callable } + end + + private + + def define_filter(type, filter, options = {}) + return if filter_defined?(type, filter) + filters[type] << options.merge(:callable => filter) + remember_filter(type, filter) + end + + def defined_filters + @defined_filters ||= Hash.new { |h, k| h[k] = Set.new } + end + + # Check to see if the filter has been defined. + # + def filter_defined?(type, filter) + defined_filters[type].include?(filter) + end + + # Remember that we stored the filter. + # + def remember_filter(type, filter) + defined_filters[type] << filter + end + + # Takes a list of actually (or potentially) callable objects. + # TODO: add support for if/unless + # TODO: add support for only/except sub-filters + # + def set_filters(type, *args) + options = args.last.is_a?(Hash) ? args.pop : {} + args.each do |filter| + define_filter(type, filter, options) + end + end + + end + + module InstanceMethods + + private + + # Get back to class filters. + # + def filters + self.class.filters + end + + # Predicate which uses the filter options to determine if the filter + # should be called. Specifically checks the :if, :unless, :only, and :except + # options for every filter. Each option check is expected to return false + # if the filter should not be invoked, true if invocation should occur. + # + def invoke_filter?(rpc_method, filter) + invoke_via_only?(rpc_method, filter) && + invoke_via_except?(rpc_method, filter) && + invoke_via_if?(rpc_method, filter) && + invoke_via_unless?(rpc_method, filter) + end + + # If the target rpc endpoint method is listed under an :except option, + # return false to indicate that the filter should not be invoked. Any + # other target rpc endpoint methods not listed should be invoked. + # This option is the opposite of :only. + # + # Value should be a symbol/string or an array of symbols/strings. + # + def invoke_via_except?(rpc_method, filter) + except = [filter.fetch(:except) { [] }].flatten + except.empty? || !except.include?(rpc_method) + end + + # Invoke the given :if callable (if any) and return its return value. + # Used by `invoke_filter?` which expects a true/false + # return value to determine if we should invoke the target filter. + # + # Value can either be a symbol/string indicating an instance method to call + # or an object that responds to `call`. + # + def invoke_via_if?(_rpc_method, filter) + if_check = filter.fetch(:if, nil) + return true if if_check.nil? + call_or_send(if_check) + end + + # If the target rpc endpoint method is listed in the :only option, + # it should be invoked. Any target rpc endpoint methods not listed in this + # option should not be invoked. This option is the opposite of :except. + # + # Value should be a symbol/string or an array of symbols/strings. + # + def invoke_via_only?(rpc_method, filter) + only = [filter.fetch(:only) { [] }].flatten + only.empty? || only.include?(rpc_method) + end + + # Invoke the given :unless callable (if any) and return the opposite + # of it's return value. Used by `invoke_filter?` which expects a true/false + # return value to determine if we should invoke the target filter. + # + # Value can either be a symbol/string indicating an instance method to call + # or an object that responds to `call`. + # + def invoke_via_unless?(_rpc_method, filter) + unless_check = filter.fetch(:unless, nil) + return true if unless_check.nil? + !call_or_send(unless_check) + end + + def rescue_filters + self.class.rescue_filters + end + + # Loop over the unwrapped filters and invoke them. An unwrapped filter + # is either a before or after filter, not an around filter. + # + def run_unwrapped_filters(unwrapped_filters, rpc_method, stop_on_false_return = false) + unwrapped_filters.each do |filter| + if invoke_filter?(rpc_method, filter) + return_value = call_or_send(filter[:callable]) + return false if stop_on_false_return && return_value == false + end + end + + true + end + + # Reverse build a chain of around filters. To implement an around chain, + # simply build a method that yields control when it expects the underlying + # method to be invoked. If the endpoint should not be run (due to some + # condition), simply do not yield. + # + # Around filters are invoked in the order they are defined, outer to inner, + # with the inner-most method called being the actual rpc endpoint. + # + # Let's say you have a class defined with the following filters: + # + # class MyService + # around_filter :filter1, :filter2, :filter3 + # + # def my_endpoint + # # do stuff + # end + # end + # + # When the my_endpoint method is invoked using Service#callable_rpc_method, + # It is similar to this call chain: + # + # filter1 do + # filter2 do + # filter3 do + # my_endpoint + # end + # end + # end + # + def run_around_filters(rpc_method) + final = -> { __send__(rpc_method) } + filters[:around].reverse.reduce(final) do |previous, filter| + if invoke_filter?(rpc_method, filter) + -> { call_or_send(filter[:callable], &previous) } + else + previous + end + end.call + end + + # Entry method to call each filter type in the appropriate order. This should + # be used instead of the other run methods directly. + # + def run_filters(rpc_method) + run_rescue_filters do + continue = run_unwrapped_filters(filters[:before], rpc_method, true) + if continue + run_around_filters(rpc_method) + run_unwrapped_filters(filters[:after], rpc_method) + end + end + end + + def run_rescue_filters + if rescue_filters.keys.empty? + yield + else + begin + yield + rescue *rescue_filters.keys => ex + callable = rescue_filters.fetch(ex.class) do + mapped_klass = rescue_filters.keys.find { |child_klass| ex.class < child_klass } + rescue_filters[mapped_klass] + end + + call_or_send(callable, ex) + end + end + end + + # Call the object if it is callable, otherwise invoke the method using + # __send__ assuming that we respond_to it. Return the call's return value. + # + def call_or_send(callable, *args, &block) + return callable.call(self, *args, &block) if callable.respond_to?(:call) + __send__(callable, *args, &block) + end + end + end + end +end diff --git a/lib/protobuf/rpc/stat.rb b/lib/protobuf/rpc/stat.rb index c4ba56f5..841181a3 100644 --- a/lib/protobuf/rpc/stat.rb +++ b/lib/protobuf/rpc/stat.rb @@ -1,70 +1,119 @@ require 'date' -require 'protobuf/common/logger' +require 'time' +require 'protobuf/logging' +require 'protobuf/rpc/rpc.pb' module Protobuf module Rpc class Stat - attr_accessor :type, :start_time, :end_time, :request_size, :response_size, :client, :server, :service, :method - - TYPES = [:SERVER, :CLIENT] - - def initialize type=:SERVER, do_start=true - @type = type - start if do_start - end - - def client= peer - @client = {:port => peer[0], :ip => peer[1]} - end - + attr_accessor :mode, :start_time, :end_time, :request_size, :dispatcher + attr_accessor :response_size, :client, :service, :method_name, :status + attr_reader :server + + MODES = [:SERVER, :CLIENT].freeze + + ERROR_TRANSLATIONS = { + ::Protobuf::Socketrpc::ErrorReason::BAD_REQUEST_DATA => "BAD_REQUEST_DATA", + ::Protobuf::Socketrpc::ErrorReason::BAD_REQUEST_PROTO => "BAD_REQUEST_PROTO", + ::Protobuf::Socketrpc::ErrorReason::SERVICE_NOT_FOUND => "SERVICE_NOT_FOUND", + ::Protobuf::Socketrpc::ErrorReason::METHOD_NOT_FOUND => "METHOD_NOT_FOUND", + ::Protobuf::Socketrpc::ErrorReason::RPC_ERROR => "RPC_ERROR", + ::Protobuf::Socketrpc::ErrorReason::RPC_FAILED => "RPC_FAILED", + ::Protobuf::Socketrpc::ErrorReason::INVALID_REQUEST_PROTO => "INVALID_REQUEST_PROTO", + ::Protobuf::Socketrpc::ErrorReason::BAD_RESPONSE_PROTO => "BAD_RESPONSE_PROTO", + ::Protobuf::Socketrpc::ErrorReason::UNKNOWN_HOST => "UNKNOWN_HOST", + ::Protobuf::Socketrpc::ErrorReason::IO_ERROR => "IO_ERROR", + }.freeze + + def initialize(mode = :SERVER) + @mode = mode + @request_size = 0 + @response_size = 0 + start + end + + attr_writer :client + def client - @client ? '%s:%d' % [@client[:ip], @client[:port]] : nil + @client || nil + end + + def elapsed_time + (start_time && end_time ? "#{(end_time - start_time).round(4)}s" : nil) + end + + def method_name + @method_name ||= @dispatcher.try(:service).try(:method_name) end - - def server= peer - @server = {:port => peer[0], :ip => peer[1]} + + def server=(peer) + case peer + when Array + @server = "#{peer[1]}:#{peer[0]}" + when String + @server = peer + end end - - def server - @server ? '%s:%d' % [@server[:ip], @server[:port]] : nil + + def service + @service ||= @dispatcher.try(:service).class.name end - + def sizes - '%dB/%dB' % [@request_size || 0, @response_size || 0] + if stopped? + "#{@request_size}B/#{@response_size}B" + else + "#{@request_size}B/-" + end end - + def start - @start_time ||= Time.now + @start_time ||= ::Time.now end - - def end - start if !@start_time - @end_time ||= Time.now + + def stop + start unless @start_time + @end_time ||= ::Time.now end - + + def stopped? + !end_time.nil? + end + def rpc - service && method ? '%s#%s' % [service, method] : nil + service && method_name ? "#{service}##{method_name}" : nil end - - def elapsed_time - (start_time && end_time ? '%ss' % (end_time - start_time).round(4) : nil) + + def server? + @mode == :SERVER end - - def log_stats - Protobuf::Logger.info(self.to_s) + + def client? + @mode == :CLIENT end - + + def status_string + return "OK" if status.nil? + + ERROR_TRANSLATIONS.fetch(status, "UNKNOWN_ERROR") + end + def to_s [ - @type == :SERVER ? "[SRV-#{self.class}]" : "[CLT-#{self.class}]", + server? ? "[SRV]" : "[CLT]", + server? ? client : server, + trace_id, rpc, - elapsed_time, sizes, - @type == :SERVER ? server : client - ].delete_if{|v| v.nil? }.join(' - ') + elapsed_time, + status_string, + @end_time.try(:iso8601), + ].compact.join(' - ') + end + + def trace_id + ::Thread.current.object_id.to_s(16) end - end end end - diff --git a/lib/protobuf/socket.rb b/lib/protobuf/socket.rb new file mode 100644 index 00000000..6bc49e7e --- /dev/null +++ b/lib/protobuf/socket.rb @@ -0,0 +1,21 @@ +## +## Socket Mode +## +# +# Require this file if you wish to run your server and/or client RPC +# with the raw socket handlers. This is the default run mode for bin/rpc_server. +# +# To run with rpc_server either omit any mode switches, or explicitly pass `socket`: +# +# rpc_server myapp.rb +# rpc_server --socket myapp.rb +# +# To run for client-side only override the require in your Gemfile: +# +# gem 'protobuf', :require => 'protobuf/socket' +# +require 'protobuf' +require 'protobuf/rpc/servers/socket/server' +require 'protobuf/rpc/connectors/socket' + +::Protobuf.connector_type_class = ::Protobuf::Rpc::Connectors::Socket diff --git a/lib/protobuf/tasks.rb b/lib/protobuf/tasks.rb new file mode 100644 index 00000000..2a5b8052 --- /dev/null +++ b/lib/protobuf/tasks.rb @@ -0,0 +1 @@ +load 'protobuf/tasks/compile.rake' diff --git a/lib/protobuf/tasks/compile.rake b/lib/protobuf/tasks/compile.rake new file mode 100644 index 00000000..9e8c34cc --- /dev/null +++ b/lib/protobuf/tasks/compile.rake @@ -0,0 +1,80 @@ +require "fileutils" + +namespace :protobuf do + + desc "Clean & Compile the protobuf source to ruby classes. Pass PB_NO_CLEAN=1 if you do not want to force-clean first." + task :compile, [:package, :source, :destination, :plugin, :file_extension] do |_tasks, args| + binpath = ::File.expand_path("../../../../bin", __FILE__) + + args.with_defaults(:destination => "lib") + args.with_defaults(:source => "definitions") + args.with_defaults(:plugin => "protoc-gen-ruby-protobuf=#{binpath}/protoc-gen-ruby") + args.with_defaults(:file_extension => ".pb.rb") + + # The local Ruby generator collides with the builtin Ruby generator + # + # From the protoc docs: + # + # --plugin=EXECUTABLE + # + # ...EXECUTABLE may be of the form NAME=PATH, in which case the given plugin name + # is mapped to the given executable even if the executable"s own name differs. + # + # Use the NAME=PATH form to specify an alternative plugin name that avoids the name collision + # + plugin_name, _plugin_path = args[:plugin].split("=") + + # The plugin name MUST have the protoc-gen- prefix in order to work, but that prefix is dropped + # when using the plugin to generate definitions + plugin_name.gsub!("protoc-gen-", "") + + unless do_not_clean? + force_clean! + ::Rake::Task[:clean].invoke(args[:package], args[:destination], args[:file_extension]) + end + + command = [] + command << "protoc" + command << "--plugin=#{args[:plugin]}" + command << "--#{plugin_name}_out=#{args[:destination]}" + command << "-I #{args[:source]}" + command << Dir["#{args[:source]}/#{args[:package]}/**/*.proto"].join(" ") + full_command = command.join(" ") + + puts full_command + system(full_command) + end + + desc "Clean the generated *.pb.rb files from the destination package. Pass PB_FORCE_CLEAN=1 to skip confirmation step." + task :clean, [:package, :destination, :file_extension] do |_task, args| + args.with_defaults(:destination => "lib") + args.with_defaults(:file_extension => ".pb.rb") + + file_extension = args[:file_extension].sub(/\*?\.+/, "") + files_to_clean = ::File.join(args[:destination], args[:package], "**", "*.#{file_extension}") + + if force_clean? || permission_to_clean?(files_to_clean) + ::Dir.glob(files_to_clean).each do |file| + ::FileUtils.rm(file) + end + end + end + + def do_not_clean? + ! ::ENV.key?("PB_NO_CLEAN") + end + + def force_clean? + ::ENV.key?("PB_FORCE_CLEAN") + end + + def force_clean! + ::ENV["PB_FORCE_CLEAN"] = "1" + end + + def permission_to_clean?(files_to_clean) + puts "Do you really want to remove files matching pattern #{files_to_clean}? (y/n)" + ::STDIN.gets.chomp =~ /y(es)?/i + end + +end diff --git a/lib/protobuf/varint.rb b/lib/protobuf/varint.rb new file mode 100644 index 00000000..be70cca3 --- /dev/null +++ b/lib/protobuf/varint.rb @@ -0,0 +1,20 @@ +module Protobuf + class Varint + if defined?(::Varint) + extend ::Varint + + def self.encode(value) + bytes = [] + until value < 128 + bytes << (0x80 | (value & 0x7f)) + value >>= 7 + end + (bytes << value).pack('C*') + end + elsif defined?(::ProtobufJavaHelpers) + extend ::ProtobufJavaHelpers::EncodeDecode + else + extend VarintPure + end + end +end diff --git a/lib/protobuf/varint_pure.rb b/lib/protobuf/varint_pure.rb new file mode 100644 index 00000000..6013241c --- /dev/null +++ b/lib/protobuf/varint_pure.rb @@ -0,0 +1,31 @@ +module Protobuf + module VarintPure + CACHE_LIMIT = 2048 + + def cached_varint(value) + @_varint_cache ||= {} + (@_varint_cache[value] ||= encode(value, false)).dup + end + + def decode(stream) + value = index = 0 + begin + byte = stream.readbyte + value |= (byte & 0x7f) << (7 * index) + index += 1 + end while (byte & 0x80).nonzero? + value + end + + def encode(value, use_cache = true) + return cached_varint(value) if use_cache && value >= 0 && value <= CACHE_LIMIT + + bytes = [] + until value < 128 + bytes << (0x80 | (value & 0x7f)) + value >>= 7 + end + (bytes << value).pack('C*') + end + end +end diff --git a/lib/protobuf/version.rb b/lib/protobuf/version.rb index 721ac30f..0d7d5f38 100644 --- a/lib/protobuf/version.rb +++ b/lib/protobuf/version.rb @@ -1,3 +1,3 @@ module Protobuf - VERSION = '1.1.3' + VERSION = '3.10.9' # rubocop:disable Style/MutableConstant end diff --git a/lib/protobuf/common/wire_type.rb b/lib/protobuf/wire_type.rb similarity index 100% rename from lib/protobuf/common/wire_type.rb rename to lib/protobuf/wire_type.rb diff --git a/lib/protobuf/zmq.rb b/lib/protobuf/zmq.rb new file mode 100644 index 00000000..e469c8c8 --- /dev/null +++ b/lib/protobuf/zmq.rb @@ -0,0 +1,21 @@ +## +## ZMQ Mode +## +# +# Require this file if you wish to run your server and/or client RPC +# with the ZeroMQ handlers. +# +# To run with rpc_server specify the switch `zmq`: +# +# rpc_server --zmq myapp.rb +# +# To run for client-side only override the require in your Gemfile: +# +# gem 'protobuf', :require => 'protobuf/zmq' +# +require 'protobuf' +require 'ffi-rzmq' +require 'protobuf/rpc/servers/zmq/server' +require 'protobuf/rpc/connectors/zmq' + +Protobuf.connector_type_class = ::Protobuf::Rpc::Connectors::Zmq diff --git a/profile.html b/profile.html new file mode 100644 index 00000000..6ac4b900 --- /dev/null +++ b/profile.html @@ -0,0 +1,5103 @@ + + + + + + + +

Profile Report: main

+

Total time: 123.51

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
%Total %Self Total Self Children CallsName
100%0%123.510.00123.510(top)
120.190.00120.191/1Benchmark::IPS.ips
1.030.001.022/115Kernel.require
0.910.040.871/5##times
0.870.000.876/1883Kernel.require
0.500.060.4417/19Kernel.load
0.910.040.871/5(top)
120.190.00120.192/5Benchmark::IPS::Job#run
98%0%121.120.05121.075##times
80.180.0080.181/1Benchmark::IPS::Job#run_benchmark
40.000.0040.001/1Benchmark::IPS::Job#run_warmup
0.450.010.4410000/2123384Protobuf::Message::Serialization::ClassMethods.decode_from
0.280.010.2710000/4103921Protobuf::Encoder.encode
0.120.000.121/4105546Gem::Specification.each_gemspec
0.250.010.2419/4105546Kernel.require
40.003.5136.491/4105546Benchmark::IPS::Job#run_warmup
80.180.0280.171/4105546Benchmark::IPS::Job#run_benchmark
97%2%120.963.56117.404105546##each
114.335.40108.926794724/6794724Benchmark::IPS::Job::Entry#call_times
1.670.810.866795778/6795778Benchmark::Timing.now
0.340.340.006794732/6794732##<
0.300.300.006794740/46949692##+
120.190.00120.191/1(top)
97%0%120.190.00120.191Benchmark::IPS.ips
120.190.00120.191/1Benchmark::IPS::Job#run
120.190.00120.191/1Benchmark::IPS.ips
97%0%120.190.00120.191Benchmark::IPS::Job#run
120.190.00120.192/5##times
114.335.40108.926794724/6794724##each
92%4%114.335.40108.926794724Benchmark::IPS::Job::Entry#call_times
106.564.94101.6225271252/25271834Proc#call
1.281.280.0032065976/32065992##<
1.081.080.0025271252/46949692##+
106.564.94101.6225271252/25271834Benchmark::IPS::Job::Entry#call_times
86%4%106.604.95101.6425271834Proc#call
27.520.4627.062113384/2123384Protobuf::Message::Serialization::ClassMethods.decode_from
25.390.8124.584093920/4103921Protobuf::Encoder.encode
23.631.9021.739393746/9393746Protobuf::Message#to_hash_with_string_keys
23.521.9321.609670202/9670202Protobuf::Message#to_hash
1.060.700.376207304/6217305StringIO.new
0.320.320.004093920/4103921Protobuf::Message#to_proto
0.180.180.002113384/4247921Kernel.dup
80.180.0080.181/1##times
64%0%80.180.0080.181Benchmark::IPS::Job#run_benchmark
80.180.0280.171/4105546##each
21.604.5317.069670202/23167870Protobuf::Message#to_hash
21.734.3317.399393746/23167870Protobuf::Message#to_hash_with_string_keys
22.783.5319.244103921/23167870Protobuf::Message#each_field_for_serialization
53%10%66.1012.4053.7023167870##each_key
15.223.2511.9618787492/18787492Protobuf::Field::BaseField#to_message_hash_with_string_key
14.753.3011.4519340404/19340404Protobuf::Field::BaseField#to_message_hash
14.731.6313.118217842/8217842Protobuf::Field::BaseField#encode_to_stream
3.341.691.668217842/8217842Protobuf::Field::BaseField#value_from_values_for_serialization
3.203.200.0046345738/97013401##[]
2.462.460.0046345738/50632510Test::Resource#_protobuf_message_field
40.000.0040.001/1##times
32%0%40.000.0040.001Benchmark::IPS::Job#run_warmup
40.003.5136.491/4105546##each
0.450.010.4410000/2123384##times
27.520.4627.062113384/2123384Proc#call
22%0%27.960.4627.502123384Protobuf::Message::Serialization::ClassMethods.decode_from
26.260.4325.832123384/2123384Protobuf::Message::Serialization.decode_from
1.240.310.942123384/2136384Class#new
26.260.4325.832123384/2123384Protobuf::Message::Serialization::ClassMethods.decode_from
21%0%26.260.4325.832123384Protobuf::Message::Serialization.decode_from
25.832.3923.442123384/2123384Protobuf::Decoder.decode_each_field
25.832.3923.442123384/2123384Protobuf::Message::Serialization.decode_from
20%1%25.832.3923.442123384Protobuf::Decoder.decode_each_field
11.705.835.888513536/8513536ProtobufJavaHelpers::EncodeDecode.decode
10.641.419.234256768/4256768Protobuf::Message::Serialization.set_field_bytes
0.310.310.006380152/6380152StringIO#eof
0.250.250.006380152/6383519##==
0.230.230.002123384/2123384StringIO#read
0.180.180.004256768/4256768##>>
0.120.120.004256768/40271449##&
0.280.010.2710000/4103921##times
25.390.8124.584093920/4103921Proc#call
20%0%25.670.8224.854103921Protobuf::Encoder.encode
24.851.3223.524103921/4103921Protobuf::Message#each_field_for_serialization
24.851.3223.524103921/4103921Protobuf::Encoder.encode
20%1%24.851.3223.524103921Protobuf::Message#each_field_for_serialization
22.783.5319.244103921/23167870##each_key
0.560.560.004103921/6237307Test::Resource#_protobuf_message_unset_required_field_tags
23.631.9021.739393746/9393746Proc#call
19%1%23.631.9021.739393746Protobuf::Message#to_hash_with_string_keys
21.734.3317.399393746/23167870##each_key
23.521.9321.609670202/9670202Proc#call
19%1%23.521.9321.609670202Protobuf::Message#to_hash
21.604.5317.069670202/23167870##each_key
15.223.2511.9618787492/18787492##each_key
12%2%15.223.2511.9618787492Protobuf::Field::BaseField#to_message_hash_with_string_key
11.963.658.3118787492/18787492Protobuf::Field::BaseFieldObjectDefinitions::BaseToMessageHashWithStringKey#call
14.753.3011.4519340404/19340404##each_key
11%2%14.753.3011.4519340404Protobuf::Field::BaseField#to_message_hash
11.453.647.8219340404/19340404Protobuf::Field::BaseFieldObjectDefinitions::BaseToMessageHash#call
14.731.6313.118217842/8217842##each_key
11%1%14.731.6313.118217842Protobuf::Field::BaseField#encode_to_stream
7.092.095.004103921/4103921Protobuf::Field::BaseFieldObjectDefinitions::StringEncodeToStream#call
6.021.034.994113921/4113921Protobuf::Field::BaseFieldObjectDefinitions::BaseEncodeToStream#call
6.273.133.1418787492/38127896Protobuf::Field::BaseFieldObjectDefinitions::BaseToMessageHashWithStringKey#call
6.453.193.2619340404/38127896Protobuf::Field::BaseFieldObjectDefinitions::BaseToMessageHash#call
10%5%12.726.326.4038127896Protobuf::Field::BaseField#value_from_values
6.404.142.2638127896/38127896Protobuf::Field::BaseFieldObjectDefinitions::BaseFieldValueFromValues#call
11.963.658.3118787492/18787492Protobuf::Field::BaseField#to_message_hash_with_string_key
9%2%11.963.658.3118787492Protobuf::Field::BaseFieldObjectDefinitions::BaseToMessageHashWithStringKey#call
6.273.133.1418787492/38127896Protobuf::Field::BaseField#value_from_values
2.042.040.0018787492/42419312##[]=
11.705.835.888513536/8513536Protobuf::Decoder.decode_each_field
9%4%11.705.835.888513536ProtobufJavaHelpers::EncodeDecode.decode
2.041.380.6614883688/14883688IO::GenericReadable.readbyte
1.001.000.0029767376/40271449##&
0.590.590.0014883688/17017539Numeric#nonzero?
0.580.580.0014883688/14883841##<<
0.560.560.0014883688/46949692##+
0.560.560.0014883688/14883841##|
0.540.540.0014883688/14884832##*
11.453.647.8219340404/19340404Protobuf::Field::BaseField#to_message_hash
9%2%11.453.647.8219340404Protobuf::Field::BaseFieldObjectDefinitions::BaseToMessageHash#call
6.453.193.2619340404/38127896Protobuf::Field::BaseField#value_from_values
1.371.370.0019340404/42419312##[]=
10.641.419.234256768/4256768Protobuf::Decoder.decode_each_field
8%1%10.641.419.234256768Protobuf::Message::Serialization.set_field_bytes
8.480.877.604256768/4256768Protobuf::Field::BaseField#set
0.420.420.004256768/97013401##[]
0.330.330.004256768/50632510Test::Resource#_protobuf_message_field
8.480.877.604256768/4256768Protobuf::Message::Serialization.set_field_bytes
6%0%8.480.877.604256768Protobuf::Field::BaseField#set
7.601.026.584256768/4256768Protobuf::Field::BaseFieldObjectDefinitions::BaseSetMethod#call
7.601.026.584256768/4256768Protobuf::Field::BaseField#set
6%0%7.601.026.584256768Protobuf::Field::BaseFieldObjectDefinitions::BaseSetMethod#call
5.010.624.394256768/4286772Protobuf::Message#set_field
1.080.780.302123384/2123384Protobuf::Field::IntegerField#decode
0.460.300.162123384/2123384Protobuf::Field::StringField#decode
7.092.095.004103921/4103921Protobuf::Field::BaseField#encode_to_stream
5%1%7.092.095.004103921Protobuf::Field::BaseFieldObjectDefinitions::StringEncodeToStream#call
2.361.201.1612311763/20539605IO::GenericWritable.<<
1.410.530.884103921/8217995Protobuf::Field::VarintField.encode
0.580.400.184103921/4104743BasicObject#!=
0.280.280.004103921/4104065##+
0.210.210.004103921/4103921##encoding
0.170.170.004103921/4103921##bytesize
6.404.142.2638127896/38127896Protobuf::Field::BaseField#value_from_values
5%3%6.404.142.2638127896Protobuf::Field::BaseFieldObjectDefinitions::BaseFieldValueFromValues#call
2.262.260.0038127896/97013401##[]
0.420.420.004256768/97013401Protobuf::Message::Serialization.set_field_bytes
0.480.480.008217842/97013401Protobuf::Field::BaseFieldObjectDefinitions::BaseFieldValueFromValuesForSerialization#call
2.262.260.0038127896/97013401Protobuf::Field::BaseFieldObjectDefinitions::BaseFieldValueFromValues#call
3.203.200.0046345738/97013401##each_key
5%5%6.396.360.0397013401##[]
6.021.034.994113921/4113921Protobuf::Field::BaseField#encode_to_stream
4%0%6.021.034.994113921Protobuf::Field::BaseFieldObjectDefinitions::BaseEncodeToStream#call
3.351.481.864103921/4103921Protobuf::Field::IntegerField#encode
1.610.800.818227842/20539605IO::GenericWritable.<<
5.010.624.394256768/4286772Protobuf::Field::BaseFieldObjectDefinitions::BaseSetMethod#call
4%0%5.100.634.474286772Protobuf::Message#set_field
4.460.903.564286772/4286772Protobuf::Field::BaseField#set_field
4.460.903.564286772/4286772Protobuf::Message#set_field
3%0%4.460.903.564286772Protobuf::Field::BaseField#set_field
1.780.571.212153386/2153386Protobuf::Field::BaseFieldObjectDefinitions::BaseSetField#call
1.780.691.092133386/2133386Protobuf::Field::BaseFieldObjectDefinitions::RequiredStringSetField#call
1.610.800.818227842/20539605Protobuf::Field::BaseFieldObjectDefinitions::BaseEncodeToStream#call
2.361.201.1612311763/20539605Protobuf::Field::BaseFieldObjectDefinitions::StringEncodeToStream#call
3%1%3.972.001.9720539605IO::GenericWritable.<<
1.971.970.0020539605/20539605StringIO#write
0.180.180.002153386/42419312Protobuf::Field::BaseFieldObjectDefinitions::BaseSetField#call
0.200.200.002133386/42419312Protobuf::Field::BaseFieldObjectDefinitions::RequiredStringSetField#call
1.371.370.0019340404/42419312Protobuf::Field::BaseFieldObjectDefinitions::BaseToMessageHash#call
2.042.040.0018787492/42419312Protobuf::Field::BaseFieldObjectDefinitions::BaseToMessageHashWithStringKey#call
3%3%3.803.790.0142419312##[]=
3.351.481.864103921/4103921Protobuf::Field::BaseFieldObjectDefinitions::BaseEncodeToStream#call
2%1%3.351.481.864103921Protobuf::Field::IntegerField#encode
1.450.500.954103921/8217995Protobuf::Field::VarintField.encode
0.410.410.004103921/40271449##&
3.341.691.668217842/8217842##each_key
2%1%3.341.691.668217842Protobuf::Field::BaseField#value_from_values_for_serialization
1.661.180.488217842/8217842Protobuf::Field::BaseFieldObjectDefinitions::BaseFieldValueFromValuesForSerialization#call
1.410.530.884103921/8217995Protobuf::Field::BaseFieldObjectDefinitions::StringEncodeToStream#call
1.450.500.954103921/8217995Protobuf::Field::IntegerField#encode
2%0%2.871.031.848217995Protobuf::Field::VarintField.encode
1.840.930.918217995/8217995ProtobufJavaHelpers::EncodeDecode.encode
0.330.330.004256768/50632510Protobuf::Message::Serialization.set_field_bytes
2.462.460.0046345738/50632510##each_key
2%2%2.792.790.0050632510Test::Resource#_protobuf_message_field
0.410.030.382/1883Kernel.load
0.870.000.876/1883(top)
1.020.001.02115/1883Kernel.require
1%0%2.300.032.271883Kernel.require
0.860.000.861/1Bundler.setup
0.250.010.2419/4105546##each
0.120.000.121/1Gem::Specification.load_defaults
0.280.010.271/2136384Bundler::Dsl#to_definition
1.240.310.942123384/2136384Protobuf::Message::Serialization::ClassMethods.decode_from
1%0%2.060.501.562136384Class#new
1.020.690.342133386/2133386Protobuf::Message#initialize
0.270.000.271/1Bundler::Definition#initialize
2.041.380.6614883688/14883688ProtobufJavaHelpers::EncodeDecode.decode
1%1%2.041.380.6614883688IO::GenericReadable.readbyte
0.660.660.0014883688/14883688StringIO#getbyte
1.971.970.0020539605/20539605IO::GenericWritable.<<
1%1%1.971.970.0020539605StringIO#write
0.300.300.006794740/46949692##each
0.560.560.0014883688/46949692ProtobufJavaHelpers::EncodeDecode.decode
1.081.080.0025271252/46949692Benchmark::IPS::Job::Entry#call_times
1%1%1.941.940.0046949692##+
1.840.930.918217995/8217995Protobuf::Field::VarintField.encode
1%0%1.840.930.918217995ProtobufJavaHelpers::EncodeDecode.encode
0.910.910.008217995/8217995ProtobufJavaHelpers::Varinter.to_varint
1.780.571.212153386/2153386Protobuf::Field::BaseField#set_field
1%0%1.780.571.212153386Protobuf::Field::BaseFieldObjectDefinitions::BaseSetField#call
0.920.630.292133386/2133386Protobuf::Field::VarintField#coerce!
0.180.180.002153386/42419312##[]=
1.780.691.092133386/2133386Protobuf::Field::BaseField#set_field
1%0%1.780.691.092133386Protobuf::Field::BaseFieldObjectDefinitions::RequiredStringSetField#call
0.610.430.182133386/6237307Test::Resource#_protobuf_message_unset_required_field_tags
0.200.200.002133386/42419312##[]=
0.160.160.002133386/2133570##delete
0.120.120.002133386/4298337Kernel.kind_of?
0.120.120.004256768/40271449Protobuf::Decoder.decode_each_field
0.210.210.002123384/40271449Protobuf::Field::IntegerField#decode
0.410.410.004103921/40271449Protobuf::Field::IntegerField#encode
1.001.000.0029767376/40271449ProtobufJavaHelpers::EncodeDecode.decode
1%1%1.761.760.0040271449##&
1.670.810.866795778/6795778##each
1%0%1.670.810.866795778Benchmark::Timing.now
0.860.860.006795778/6795779Process.clock_gettime
1.661.180.488217842/8217842Protobuf::Field::BaseField#value_from_values_for_serialization
1%0%1.661.180.488217842Protobuf::Field::BaseFieldObjectDefinitions::BaseFieldValueFromValuesForSerialization#call
0.480.480.008217842/97013401##[]
1.281.280.0032065976/32065992Benchmark::IPS::Job::Entry#call_times
1%1%1.281.280.0032065992##<
0.560.560.004103921/6237307Protobuf::Message#each_field_for_serialization
0.610.430.182133386/6237307Protobuf::Field::BaseFieldObjectDefinitions::RequiredStringSetField#call
0%0%1.181.000.186237307Test::Resource#_protobuf_message_unset_required_field_tags
0.180.180.002133386/4247921Kernel.dup
1.080.780.302123384/2123384Protobuf::Field::BaseFieldObjectDefinitions::BaseSetMethod#call
0%0%1.080.780.302123384Protobuf::Field::IntegerField#decode
0.210.210.002123384/40271449##&
1.060.700.376207304/6217305Proc#call
0%0%1.070.700.376217305StringIO.new
0.370.370.006217305/6217305StringIO#initialize
1.030.001.022/115(top)
0%0%1.030.001.02115Kernel.require
1.020.001.02115/1883Kernel.require
1.020.690.342133386/2133386Class#new
0%0%1.020.690.342133386Protobuf::Message#initialize
0.160.100.062133386/2133600##each
0.920.630.292133386/2133386Protobuf::Field::BaseFieldObjectDefinitions::BaseSetField#call
0%0%0.920.630.292133386Protobuf::Field::VarintField#coerce!
0.110.110.002133386/4298337Kernel.kind_of?
0.910.910.008217995/8217995ProtobufJavaHelpers::EncodeDecode.encode
0%0%0.910.910.008217995ProtobufJavaHelpers::Varinter.to_varint
0.860.860.006795778/6795779Benchmark::Timing.now
0%0%0.860.860.006795779Process.clock_gettime
0.860.000.861/1Kernel.require
0%0%0.860.000.861Bundler.setup
0.480.010.481/2Bundler.definition
0.370.000.371/1Bundler::Runtime#setup
0.590.590.0014883688/17017539ProtobufJavaHelpers::EncodeDecode.decode
0%0%0.680.680.0017017539Numeric#nonzero?
0.660.660.0014883688/14883688IO::GenericReadable.readbyte
0%0%0.660.660.0014883688StringIO#getbyte
0.580.400.184103921/4104743Protobuf::Field::BaseFieldObjectDefinitions::StringEncodeToStream#call
0%0%0.590.400.184104743BasicObject#!=
0.180.180.004104167/4104503BasicObject#==
0.580.580.0014883688/14883841ProtobufJavaHelpers::EncodeDecode.decode
0%0%0.580.580.0014883841##<<
0.560.560.0014883688/14883841ProtobufJavaHelpers::EncodeDecode.decode
0%0%0.560.560.0014883841##|
0.540.540.0014883688/14884832ProtobufJavaHelpers::EncodeDecode.decode
0%0%0.540.540.0014884832##*
0.500.060.4417/19(top)
0%0%0.500.060.4419Kernel.load
0.410.030.382/1883Kernel.require
0.480.010.481/2Bundler.setup
0%0%0.480.010.482Bundler.definition
0.390.000.381/1Bundler::Definition.build
0.460.300.162123384/2123384Protobuf::Field::BaseFieldObjectDefinitions::BaseSetMethod#call
0%0%0.460.300.162123384Protobuf::Field::StringField#decode
0.160.160.002123384/2123384##force_encoding
0.340.000.341/874Bundler::Runtime#requested_specs
0%0%0.420.000.42874Kernel.send
0.340.000.341/1Bundler::Definition#requested_specs
0.390.000.381/1Bundler.definition
0%0%0.390.000.381Bundler::Definition.build
0.370.000.371/1Bundler::Dsl.evaluate
0.370.000.371/1Bundler::Definition.build
0%0%0.370.000.371Bundler::Dsl.evaluate
0.280.000.281/1Bundler::Dsl#to_definition
0.370.370.006217305/6217305StringIO.new
0%0%0.370.370.006217305StringIO#initialize
0.370.000.371/1Bundler.setup
0%0%0.370.000.371Bundler::Runtime#setup
0.340.000.341/1Bundler::Runtime#requested_specs
0.180.180.002113384/4247921Proc#call
0.180.180.002133386/4247921Test::Resource#_protobuf_message_unset_required_field_tags
0%0%0.360.360.004247921Kernel.dup
0.340.340.006794732/6794732##each
0%0%0.340.340.006794732##<
0.340.000.341/1Bundler::Runtime#setup
0%0%0.340.000.341Bundler::Runtime#requested_specs
0.340.000.341/874Kernel.send
0.340.000.341/1Kernel.send
0%0%0.340.000.341Bundler::Definition#requested_specs
0.340.000.341/1Bundler::Definition#specs_for
0.340.000.341/1Bundler::Definition#requested_specs
0%0%0.340.000.341Bundler::Definition#specs_for
0.330.000.331/1Bundler::Definition#specs
0.330.000.331/1Bundler::Definition#specs_for
0%0%0.330.000.331Bundler::Definition#specs
0.220.000.221/1Bundler::SpecSet#materialize
0.320.320.004093920/4103921Proc#call
0%0%0.320.320.004103921Protobuf::Message#to_proto
0.310.310.006380152/6380152Protobuf::Decoder.decode_each_field
0%0%0.310.310.006380152StringIO#eof
0.280.280.004103921/4104065Protobuf::Field::BaseFieldObjectDefinitions::StringEncodeToStream#call
0%0%0.280.280.004104065##+
0.280.000.281/1Bundler::Dsl.evaluate
0%0%0.280.000.281Bundler::Dsl#to_definition
0.280.010.271/2136384Class#new
0.270.000.271/1Class#new
0%0%0.270.000.271Bundler::Definition#initialize
0.100.000.101/1Bundler::Definition#converge_paths
0.250.250.006380152/6383519Protobuf::Decoder.decode_each_field
0%0%0.250.250.006383519##==
0.230.230.002123384/2123384Protobuf::Decoder.decode_each_field
0%0%0.230.230.002123384StringIO#read
0.190.000.191/425Bundler::SpecSet#materialize
0%0%0.230.020.21425##map!
0.190.000.1939/39Bundler::LazySpecification#__materialize__
0.110.110.002133386/4298337Protobuf::Field::VarintField#coerce!
0.120.120.002133386/4298337Protobuf::Field::BaseFieldObjectDefinitions::RequiredStringSetField#call
0%0%0.230.230.004298337Kernel.kind_of?
0.220.000.221/1Bundler::Definition#specs
0%0%0.220.000.221Bundler::SpecSet#materialize
0.190.000.191/425##map!
0.210.210.004103921/4103921Protobuf::Field::BaseFieldObjectDefinitions::StringEncodeToStream#call
0%0%0.210.210.004103921##encoding
0.190.000.1939/39##map!
0%0%0.190.000.1939Bundler::LazySpecification#__materialize__
0.160.000.1637/37Bundler::Source::Rubygems#specs
0.160.100.062133386/2133600Protobuf::Message#initialize
0%0%0.190.120.072133600##each
0.180.180.004256768/4256768Protobuf::Decoder.decode_each_field
0%0%0.180.180.004256768##>>
0%0%0.180.020.16697##map
0.180.180.004104167/4104503BasicObject#!=
0%0%0.180.180.004104503BasicObject#==
0.170.170.004103921/4103921Protobuf::Field::BaseFieldObjectDefinitions::StringEncodeToStream#call
0%0%0.170.170.004103921##bytesize
0.150.000.151/2Bundler::Source::Rubygems#installed_specs
0%0%0.160.000.162Bundler::Index.build
0.150.000.151/1Bundler::RubygemsIntegration::MoreFuture#all_specs
0.160.160.002133386/2133570Protobuf::Field::BaseFieldObjectDefinitions::RequiredStringSetField#call
0%0%0.160.160.002133570##delete
0.160.160.002123384/2123384Protobuf::Field::StringField#decode
0%0%0.160.160.002123384##force_encoding
0.160.000.1637/37Bundler::LazySpecification#__materialize__
0%0%0.160.000.1637Bundler::Source::Rubygems#specs
0.150.000.151/1Bundler::Source::Rubygems#installed_specs
0.150.000.151/1Bundler::Source::Rubygems#specs
0%0%0.150.000.151Bundler::Source::Rubygems#installed_specs
0.150.000.151/2Bundler::Index.build
0.150.000.151/1Bundler::Index.build
0%0%0.150.000.151Bundler::RubygemsIntegration::MoreFuture#all_specs
0.140.000.141/1Gem::Specification.stubs
0.140.000.141/1Bundler::RubygemsIntegration::MoreFuture#all_specs
0%0%0.140.000.141Gem::Specification.stubs
0%0%0.140.030.1161Kernel.eval
0%0%0.130.000.133Gem::Specification.gemspec_stubs_in
0.120.000.121/1Kernel.require
0%0%0.120.000.121Gem::Specification.load_defaults
0.120.000.121/1Gem::Specification.each_spec
0.120.000.121/1Gem::Specification.load_defaults
0%0%0.120.000.121Gem::Specification.each_spec
0.120.000.121/1Gem::Specification.each_gemspec
0.120.000.121/1Gem::Specification.each_spec
0%0%0.120.000.121Gem::Specification.each_gemspec
0.120.000.121/4105546##each
0.100.000.101/178Bundler::Definition#converge_paths
0%0%0.110.000.11178##any?
0.100.000.102/2Bundler::Definition#specs_changed?
0.100.000.101/1Bundler::Definition#initialize
0%0%0.100.000.101Bundler::Definition#converge_paths
0.100.000.101/178##any?
0.100.000.102/2##any?
0%0%0.100.000.102Bundler::Definition#specs_changed?
0%0%0.100.000.10153Protobuf::Message::Fields::ClassMethods.define_field
+ + diff --git a/proto/dynamic_discovery.proto b/proto/dynamic_discovery.proto new file mode 100644 index 00000000..a2301b5a --- /dev/null +++ b/proto/dynamic_discovery.proto @@ -0,0 +1,46 @@ +// Copyright (c) 2013 MoneyDesktop, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Authors: Devin Christensen +// +// Protobufs needed for dynamic discovery zmq server and client. + +syntax = "proto2"; + +package protobuf.rpc.dynamicDiscovery; + +enum BeaconType { + HEARTBEAT = 0; + FLATLINE = 1; +} + +message Server { + optional string uuid = 1; + optional string address = 2; + optional string port = 3; + optional int32 ttl = 4; + repeated string services = 5; +} + +message Beacon { + optional BeaconType beacon_type = 1; + optional Server server = 2; +} + diff --git a/proto/google/protobuf/compiler/plugin.proto b/proto/google/protobuf/compiler/plugin.proto new file mode 100644 index 00000000..9242aacc --- /dev/null +++ b/proto/google/protobuf/compiler/plugin.proto @@ -0,0 +1,183 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// +// WARNING: The plugin interface is currently EXPERIMENTAL and is subject to +// change. +// +// protoc (aka the Protocol Compiler) can be extended via plugins. A plugin is +// just a program that reads a CodeGeneratorRequest from stdin and writes a +// CodeGeneratorResponse to stdout. +// +// Plugins written using C++ can use google/protobuf/compiler/plugin.h instead +// of dealing with the raw protocol defined here. +// +// A plugin executable needs only to be placed somewhere in the path. The +// plugin should be named "protoc-gen-$NAME", and will then be used when the +// flag "--${NAME}_out" is passed to protoc. + +syntax = "proto2"; + +package google.protobuf.compiler; +option java_package = "com.google.protobuf.compiler"; +option java_outer_classname = "PluginProtos"; + +option go_package = "google.golang.org/protobuf/types/pluginpb"; + +import "google/protobuf/descriptor.proto"; + +// The version number of protocol compiler. +message Version { + optional int32 major = 1; + optional int32 minor = 2; + optional int32 patch = 3; + // A suffix for alpha, beta or rc release, e.g., "alpha-1", "rc2". It should + // be empty for mainline stable releases. + optional string suffix = 4; +} + +// An encoded CodeGeneratorRequest is written to the plugin's stdin. +message CodeGeneratorRequest { + // The .proto files that were explicitly listed on the command-line. The + // code generator should generate code only for these files. Each file's + // descriptor will be included in proto_file, below. + repeated string file_to_generate = 1; + + // The generator parameter passed on the command-line. + optional string parameter = 2; + + // FileDescriptorProtos for all files in files_to_generate and everything + // they import. The files will appear in topological order, so each file + // appears before any file that imports it. + // + // protoc guarantees that all proto_files will be written after + // the fields above, even though this is not technically guaranteed by the + // protobuf wire format. This theoretically could allow a plugin to stream + // in the FileDescriptorProtos and handle them one by one rather than read + // the entire set into memory at once. However, as of this writing, this + // is not similarly optimized on protoc's end -- it will store all fields in + // memory at once before sending them to the plugin. + // + // Type names of fields and extensions in the FileDescriptorProto are always + // fully qualified. + repeated FileDescriptorProto proto_file = 15; + + // The version number of protocol compiler. + optional Version compiler_version = 3; + +} + +// The plugin writes an encoded CodeGeneratorResponse to stdout. +message CodeGeneratorResponse { + // Error message. If non-empty, code generation failed. The plugin process + // should exit with status code zero even if it reports an error in this way. + // + // This should be used to indicate errors in .proto files which prevent the + // code generator from generating correct code. Errors which indicate a + // problem in protoc itself -- such as the input CodeGeneratorRequest being + // unparseable -- should be reported by writing a message to stderr and + // exiting with a non-zero status code. + optional string error = 1; + + // A bitmask of supported features that the code generator supports. + // This is a bitwise "or" of values from the Feature enum. + optional uint64 supported_features = 2; + + // Sync with code_generator.h. + enum Feature { + FEATURE_NONE = 0; + FEATURE_PROTO3_OPTIONAL = 1; + } + + // Represents a single generated file. + message File { + // The file name, relative to the output directory. The name must not + // contain "." or ".." components and must be relative, not be absolute (so, + // the file cannot lie outside the output directory). "/" must be used as + // the path separator, not "\". + // + // If the name is omitted, the content will be appended to the previous + // file. This allows the generator to break large files into small chunks, + // and allows the generated text to be streamed back to protoc so that large + // files need not reside completely in memory at one time. Note that as of + // this writing protoc does not optimize for this -- it will read the entire + // CodeGeneratorResponse before writing files to disk. + optional string name = 1; + + // If non-empty, indicates that the named file should already exist, and the + // content here is to be inserted into that file at a defined insertion + // point. This feature allows a code generator to extend the output + // produced by another code generator. The original generator may provide + // insertion points by placing special annotations in the file that look + // like: + // @@protoc_insertion_point(NAME) + // The annotation can have arbitrary text before and after it on the line, + // which allows it to be placed in a comment. NAME should be replaced with + // an identifier naming the point -- this is what other generators will use + // as the insertion_point. Code inserted at this point will be placed + // immediately above the line containing the insertion point (thus multiple + // insertions to the same point will come out in the order they were added). + // The double-@ is intended to make it unlikely that the generated code + // could contain things that look like insertion points by accident. + // + // For example, the C++ code generator places the following line in the + // .pb.h files that it generates: + // // @@protoc_insertion_point(namespace_scope) + // This line appears within the scope of the file's package namespace, but + // outside of any particular class. Another plugin can then specify the + // insertion_point "namespace_scope" to generate additional classes or + // other declarations that should be placed in this scope. + // + // Note that if the line containing the insertion point begins with + // whitespace, the same whitespace will be added to every line of the + // inserted text. This is useful for languages like Python, where + // indentation matters. In these languages, the insertion point comment + // should be indented the same amount as any inserted code will need to be + // in order to work correctly in that context. + // + // The code generator that generates the initial file and the one which + // inserts into it must both run as part of a single invocation of protoc. + // Code generators are executed in the order in which they appear on the + // command line. + // + // If |insertion_point| is present, |name| must also be present. + optional string insertion_point = 2; + + // The file contents. + optional string content = 15; + + // Information describing the file content being inserted. If an insertion + // point is used, this information will be appropriately offset and inserted + // into the code generation metadata for the generated files. + optional GeneratedCodeInfo generated_code_info = 16; + } + repeated File file = 15; +} diff --git a/proto/google/protobuf/descriptor.proto b/proto/google/protobuf/descriptor.proto new file mode 100644 index 00000000..156e410a --- /dev/null +++ b/proto/google/protobuf/descriptor.proto @@ -0,0 +1,911 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// The messages in this file describe the definitions found in .proto files. +// A valid .proto file can be translated directly to a FileDescriptorProto +// without any other information (e.g. without reading its imports). + + +syntax = "proto2"; + +package google.protobuf; + +option go_package = "google.golang.org/protobuf/types/descriptorpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DescriptorProtos"; +option csharp_namespace = "Google.Protobuf.Reflection"; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; + +// descriptor.proto must be optimized for speed because reflection-based +// algorithms don't work during bootstrapping. +option optimize_for = SPEED; + +// The protocol compiler can output a FileDescriptorSet containing the .proto +// files it parses. +message FileDescriptorSet { + repeated FileDescriptorProto file = 1; +} + +// Describes a complete .proto file. +message FileDescriptorProto { + optional string name = 1; // file name, relative to root of source tree + optional string package = 2; // e.g. "foo", "foo.bar", etc. + + // Names of files imported by this file. + repeated string dependency = 3; + // Indexes of the public imported files in the dependency list above. + repeated int32 public_dependency = 10; + // Indexes of the weak imported files in the dependency list. + // For Google-internal migration only. Do not use. + repeated int32 weak_dependency = 11; + + // All top-level definitions in this file. + repeated DescriptorProto message_type = 4; + repeated EnumDescriptorProto enum_type = 5; + repeated ServiceDescriptorProto service = 6; + repeated FieldDescriptorProto extension = 7; + + optional FileOptions options = 8; + + // This field contains optional information about the original source code. + // You may safely remove this entire field without harming runtime + // functionality of the descriptors -- the information is needed only by + // development tools. + optional SourceCodeInfo source_code_info = 9; + + // The syntax of the proto file. + // The supported values are "proto2" and "proto3". + optional string syntax = 12; +} + +// Describes a message type. +message DescriptorProto { + optional string name = 1; + + repeated FieldDescriptorProto field = 2; + repeated FieldDescriptorProto extension = 6; + + repeated DescriptorProto nested_type = 3; + repeated EnumDescriptorProto enum_type = 4; + + message ExtensionRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + + optional ExtensionRangeOptions options = 3; + } + repeated ExtensionRange extension_range = 5; + + repeated OneofDescriptorProto oneof_decl = 8; + + optional MessageOptions options = 7; + + // Range of reserved tag numbers. Reserved tag numbers may not be used by + // fields or extension ranges in the same message. Reserved ranges may + // not overlap. + message ReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + } + repeated ReservedRange reserved_range = 9; + // Reserved field names, which may not be used by fields in the same message. + // A given name may only be reserved once. + repeated string reserved_name = 10; +} + +message ExtensionRangeOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// Describes a field within a message. +message FieldDescriptorProto { + enum Type { + // 0 is reserved for errors. + // Order is weird for historical reasons. + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if + // negative values are likely. + TYPE_INT64 = 3; + TYPE_UINT64 = 4; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if + // negative values are likely. + TYPE_INT32 = 5; + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + // Tag-delimited aggregate. + // Group type is deprecated and not supported in proto3. However, Proto3 + // implementations should still be able to parse the group wire format and + // treat group fields as unknown fields. + TYPE_GROUP = 10; + TYPE_MESSAGE = 11; // Length-delimited aggregate. + + // New in version 2. + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; // Uses ZigZag encoding. + TYPE_SINT64 = 18; // Uses ZigZag encoding. + } + + enum Label { + // 0 is reserved for errors + LABEL_OPTIONAL = 1; + LABEL_REQUIRED = 2; + LABEL_REPEATED = 3; + } + + optional string name = 1; + optional int32 number = 3; + optional Label label = 4; + + // If type_name is set, this need not be set. If both this and type_name + // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. + optional Type type = 5; + + // For message and enum types, this is the name of the type. If the name + // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + // rules are used to find the type (i.e. first the nested types within this + // message are searched, then within the parent, on up to the root + // namespace). + optional string type_name = 6; + + // For extensions, this is the name of the type being extended. It is + // resolved in the same manner as type_name. + optional string extendee = 2; + + // For numeric types, contains the original text representation of the value. + // For booleans, "true" or "false". + // For strings, contains the default text contents (not escaped in any way). + // For bytes, contains the C escaped value. All bytes >= 128 are escaped. + // TODO(kenton): Base-64 encode? + optional string default_value = 7; + + // If set, gives the index of a oneof in the containing type's oneof_decl + // list. This field is a member of that oneof. + optional int32 oneof_index = 9; + + // JSON name of this field. The value is set by protocol compiler. If the + // user has set a "json_name" option on this field, that option's value + // will be used. Otherwise, it's deduced from the field's name by converting + // it to camelCase. + optional string json_name = 10; + + optional FieldOptions options = 8; + + // If true, this is a proto3 "optional". When a proto3 field is optional, it + // tracks presence regardless of field type. + // + // When proto3_optional is true, this field must be belong to a oneof to + // signal to old proto3 clients that presence is tracked for this field. This + // oneof is known as a "synthetic" oneof, and this field must be its sole + // member (each proto3 optional field gets its own synthetic oneof). Synthetic + // oneofs exist in the descriptor only, and do not generate any API. Synthetic + // oneofs must be ordered after all "real" oneofs. + // + // For message fields, proto3_optional doesn't create any semantic change, + // since non-repeated message fields always track presence. However it still + // indicates the semantic detail of whether the user wrote "optional" or not. + // This can be useful for round-tripping the .proto file. For consistency we + // give message fields a synthetic oneof also, even though it is not required + // to track presence. This is especially important because the parser can't + // tell if a field is a message or an enum, so it must always create a + // synthetic oneof. + // + // Proto2 optional fields do not set this flag, because they already indicate + // optional with `LABEL_OPTIONAL`. + optional bool proto3_optional = 17; +} + +// Describes a oneof. +message OneofDescriptorProto { + optional string name = 1; + optional OneofOptions options = 2; +} + +// Describes an enum type. +message EnumDescriptorProto { + optional string name = 1; + + repeated EnumValueDescriptorProto value = 2; + + optional EnumOptions options = 3; + + // Range of reserved numeric values. Reserved values may not be used by + // entries in the same enum. Reserved ranges may not overlap. + // + // Note that this is distinct from DescriptorProto.ReservedRange in that it + // is inclusive such that it can appropriately represent the entire int32 + // domain. + message EnumReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Inclusive. + } + + // Range of reserved numeric values. Reserved numeric values may not be used + // by enum values in the same enum declaration. Reserved ranges may not + // overlap. + repeated EnumReservedRange reserved_range = 4; + + // Reserved enum value names, which may not be reused. A given name may only + // be reserved once. + repeated string reserved_name = 5; +} + +// Describes a value within an enum. +message EnumValueDescriptorProto { + optional string name = 1; + optional int32 number = 2; + + optional EnumValueOptions options = 3; +} + +// Describes a service. +message ServiceDescriptorProto { + optional string name = 1; + repeated MethodDescriptorProto method = 2; + + optional ServiceOptions options = 3; +} + +// Describes a method of a service. +message MethodDescriptorProto { + optional string name = 1; + + // Input and output type names. These are resolved in the same way as + // FieldDescriptorProto.type_name, but must refer to a message type. + optional string input_type = 2; + optional string output_type = 3; + + optional MethodOptions options = 4; + + // Identifies if client streams multiple client messages + optional bool client_streaming = 5 [default = false]; + // Identifies if server streams multiple server messages + optional bool server_streaming = 6 [default = false]; +} + + +// =================================================================== +// Options + +// Each of the definitions above may have "options" attached. These are +// just annotations which may cause code to be generated slightly differently +// or may contain hints for code that manipulates protocol messages. +// +// Clients may define custom options as extensions of the *Options messages. +// These extensions may not yet be known at parsing time, so the parser cannot +// store the values in them. Instead it stores them in a field in the *Options +// message called uninterpreted_option. This field must have the same name +// across all *Options messages. We then use this field to populate the +// extensions when we build a descriptor, at which point all protos have been +// parsed and so all extensions are known. +// +// Extension numbers for custom options may be chosen as follows: +// * For options which will only be used within a single application or +// organization, or for experimental options, use field numbers 50000 +// through 99999. It is up to you to ensure that you do not use the +// same number for multiple options. +// * For options which will be published and used publicly by multiple +// independent entities, e-mail protobuf-global-extension-registry@google.com +// to reserve extension numbers. Simply provide your project name (e.g. +// Objective-C plugin) and your project website (if available) -- there's no +// need to explain how you intend to use them. Usually you only need one +// extension number. You can declare multiple options with only one extension +// number by putting them in a sub-message. See the Custom Options section of +// the docs for examples: +// https://developers.google.com/protocol-buffers/docs/proto#options +// If this turns out to be popular, a web service will be set up +// to automatically assign option numbers. + +message FileOptions { + + // Sets the Java package where classes generated from this .proto will be + // placed. By default, the proto package is used, but this is often + // inappropriate because proto packages do not normally start with backwards + // domain names. + optional string java_package = 1; + + + // Controls the name of the wrapper Java class generated for the .proto file. + // That class will always contain the .proto file's getDescriptor() method as + // well as any top-level extensions defined in the .proto file. + // If java_multiple_files is disabled, then all the other classes from the + // .proto file will be nested inside the single wrapper outer class. + optional string java_outer_classname = 8; + + // If enabled, then the Java code generator will generate a separate .java + // file for each top-level message, enum, and service defined in the .proto + // file. Thus, these types will *not* be nested inside the wrapper class + // named by java_outer_classname. However, the wrapper class will still be + // generated to contain the file's getDescriptor() method as well as any + // top-level extensions defined in the file. + optional bool java_multiple_files = 10 [default = false]; + + // This option does nothing. + optional bool java_generate_equals_and_hash = 20 [deprecated=true]; + + // If set true, then the Java2 code generator will generate code that + // throws an exception whenever an attempt is made to assign a non-UTF-8 + // byte sequence to a string field. + // Message reflection will do the same. + // However, an extension field still accepts non-UTF-8 byte sequences. + // This option has no effect on when used with the lite runtime. + optional bool java_string_check_utf8 = 27 [default = false]; + + + // Generated classes can be optimized for speed or code size. + enum OptimizeMode { + SPEED = 1; // Generate complete code for parsing, serialization, + // etc. + CODE_SIZE = 2; // Use ReflectionOps to implement these methods. + LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. + } + optional OptimizeMode optimize_for = 9 [default = SPEED]; + + // Sets the Go package where structs generated from this .proto will be + // placed. If omitted, the Go package will be derived from the following: + // - The basename of the package import path, if provided. + // - Otherwise, the package statement in the .proto file, if present. + // - Otherwise, the basename of the .proto file, without extension. + optional string go_package = 11; + + + + + // Should generic services be generated in each language? "Generic" services + // are not specific to any particular RPC system. They are generated by the + // main code generators in each language (without additional plugins). + // Generic services were the only kind of service generation supported by + // early versions of google.protobuf. + // + // Generic services are now considered deprecated in favor of using plugins + // that generate code specific to your particular RPC system. Therefore, + // these default to false. Old code which depends on generic services should + // explicitly set them to true. + optional bool cc_generic_services = 16 [default = false]; + optional bool java_generic_services = 17 [default = false]; + optional bool py_generic_services = 18 [default = false]; + optional bool php_generic_services = 42 [default = false]; + + // Is this file deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for everything in the file, or it will be completely ignored; in the very + // least, this is a formalization for deprecating files. + optional bool deprecated = 23 [default = false]; + + // Enables the use of arenas for the proto messages in this file. This applies + // only to generated classes for C++. + optional bool cc_enable_arenas = 31 [default = true]; + + + // Sets the objective c class prefix which is prepended to all objective c + // generated classes from this .proto. There is no default. + optional string objc_class_prefix = 36; + + // Namespace for generated classes; defaults to the package. + optional string csharp_namespace = 37; + + // By default Swift generators will take the proto package and CamelCase it + // replacing '.' with underscore and use that to prefix the types/symbols + // defined. When this options is provided, they will use this value instead + // to prefix the types/symbols defined. + optional string swift_prefix = 39; + + // Sets the php class prefix which is prepended to all php generated classes + // from this .proto. Default is empty. + optional string php_class_prefix = 40; + + // Use this option to change the namespace of php generated classes. Default + // is empty. When this option is empty, the package name will be used for + // determining the namespace. + optional string php_namespace = 41; + + // Use this option to change the namespace of php generated metadata classes. + // Default is empty. When this option is empty, the proto file name will be + // used for determining the namespace. + optional string php_metadata_namespace = 44; + + // Use this option to change the package of ruby generated classes. Default + // is empty. When this option is not set, the package name will be used for + // determining the ruby package. + optional string ruby_package = 45; + + + // The parser stores options it doesn't recognize here. + // See the documentation for the "Options" section above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. + // See the documentation for the "Options" section above. + extensions 1000 to max; + + reserved 38; +} + +message MessageOptions { + // Set true to use the old proto1 MessageSet wire format for extensions. + // This is provided for backwards-compatibility with the MessageSet wire + // format. You should not use this for any other reason: It's less + // efficient, has fewer features, and is more complicated. + // + // The message must be defined exactly as follows: + // message Foo { + // option message_set_wire_format = true; + // extensions 4 to max; + // } + // Note that the message cannot have any defined fields; MessageSets only + // have extensions. + // + // All extensions of your type must be singular messages; e.g. they cannot + // be int32s, enums, or repeated messages. + // + // Because this is an option, the above two restrictions are not enforced by + // the protocol compiler. + optional bool message_set_wire_format = 1 [default = false]; + + // Disables the generation of the standard "descriptor()" accessor, which can + // conflict with a field of the same name. This is meant to make migration + // from proto1 easier; new code should avoid fields named "descriptor". + optional bool no_standard_descriptor_accessor = 2 [default = false]; + + // Is this message deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the message, or it will be completely ignored; in the very least, + // this is a formalization for deprecating messages. + optional bool deprecated = 3 [default = false]; + + reserved 4, 5, 6; + + // Whether the message is an automatically generated map entry type for the + // maps field. + // + // For maps fields: + // map map_field = 1; + // The parsed descriptor looks like: + // message MapFieldEntry { + // option map_entry = true; + // optional KeyType key = 1; + // optional ValueType value = 2; + // } + // repeated MapFieldEntry map_field = 1; + // + // Implementations may choose not to generate the map_entry=true message, but + // use a native map in the target language to hold the keys and values. + // The reflection APIs in such implementations still need to work as + // if the field is a repeated message field. + // + // NOTE: Do not set the option in .proto files. Always use the maps syntax + // instead. The option should only be implicitly set by the proto compiler + // parser. + optional bool map_entry = 7; + + reserved 8; // javalite_serializable + reserved 9; // javanano_as_lite + + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message FieldOptions { + // The ctype option instructs the C++ code generator to use a different + // representation of the field than it normally would. See the specific + // options below. This option is not yet implemented in the open source + // release -- sorry, we'll try to include it in a future version! + optional CType ctype = 1 [default = STRING]; + enum CType { + // Default mode. + STRING = 0; + + CORD = 1; + + STRING_PIECE = 2; + } + // The packed option can be enabled for repeated primitive fields to enable + // a more efficient representation on the wire. Rather than repeatedly + // writing the tag and type for each element, the entire array is encoded as + // a single length-delimited blob. In proto3, only explicit setting it to + // false will avoid using packed encoding. + optional bool packed = 2; + + // The jstype option determines the JavaScript type used for values of the + // field. The option is permitted only for 64 bit integral and fixed types + // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING + // is represented as JavaScript string, which avoids loss of precision that + // can happen when a large value is converted to a floating point JavaScript. + // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to + // use the JavaScript "number" type. The behavior of the default option + // JS_NORMAL is implementation dependent. + // + // This option is an enum to permit additional types to be added, e.g. + // goog.math.Integer. + optional JSType jstype = 6 [default = JS_NORMAL]; + enum JSType { + // Use the default type. + JS_NORMAL = 0; + + // Use JavaScript strings. + JS_STRING = 1; + + // Use JavaScript numbers. + JS_NUMBER = 2; + } + + // Should this field be parsed lazily? Lazy applies only to message-type + // fields. It means that when the outer message is initially parsed, the + // inner message's contents will not be parsed but instead stored in encoded + // form. The inner message will actually be parsed when it is first accessed. + // + // This is only a hint. Implementations are free to choose whether to use + // eager or lazy parsing regardless of the value of this option. However, + // setting this option true suggests that the protocol author believes that + // using lazy parsing on this field is worth the additional bookkeeping + // overhead typically needed to implement it. + // + // This option does not affect the public interface of any generated code; + // all method signatures remain the same. Furthermore, thread-safety of the + // interface is not affected by this option; const methods remain safe to + // call from multiple threads concurrently, while non-const methods continue + // to require exclusive access. + // + // + // Note that implementations may choose not to check required fields within + // a lazy sub-message. That is, calling IsInitialized() on the outer message + // may return true even if the inner message has missing required fields. + // This is necessary because otherwise the inner message would have to be + // parsed in order to perform the check, defeating the purpose of lazy + // parsing. An implementation which chooses not to check required fields + // must be consistent about it. That is, for any particular sub-message, the + // implementation must either *always* check its required fields, or *never* + // check its required fields, regardless of whether or not the message has + // been parsed. + optional bool lazy = 5 [default = false]; + + // Is this field deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for accessors, or it will be completely ignored; in the very least, this + // is a formalization for deprecating fields. + optional bool deprecated = 3 [default = false]; + + // For Google-internal migration only. Do not use. + optional bool weak = 10 [default = false]; + + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; + + reserved 4; // removed jtype +} + +message OneofOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumOptions { + + // Set this option to true to allow mapping different tag names to the same + // value. + optional bool allow_alias = 2; + + // Is this enum deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum, or it will be completely ignored; in the very least, this + // is a formalization for deprecating enums. + optional bool deprecated = 3 [default = false]; + + reserved 5; // javanano_as_lite + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumValueOptions { + // Is this enum value deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum value, or it will be completely ignored; in the very least, + // this is a formalization for deprecating enum values. + optional bool deprecated = 1 [default = false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message ServiceOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this service deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the service, or it will be completely ignored; in the very least, + // this is a formalization for deprecating services. + optional bool deprecated = 33 [default = false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message MethodOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this method deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the method, or it will be completely ignored; in the very least, + // this is a formalization for deprecating methods. + optional bool deprecated = 33 [default = false]; + + // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, + // or neither? HTTP based RPC implementation may choose GET verb for safe + // methods, and PUT verb for idempotent methods instead of the default POST. + enum IdempotencyLevel { + IDEMPOTENCY_UNKNOWN = 0; + NO_SIDE_EFFECTS = 1; // implies idempotent + IDEMPOTENT = 2; // idempotent, but may have side effects + } + optional IdempotencyLevel idempotency_level = 34 + [default = IDEMPOTENCY_UNKNOWN]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + + +// A message representing a option the parser does not recognize. This only +// appears in options protos created by the compiler::Parser class. +// DescriptorPool resolves these when building Descriptor objects. Therefore, +// options protos in descriptor objects (e.g. returned by Descriptor::options(), +// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions +// in them. +message UninterpretedOption { + // The name of the uninterpreted option. Each string represents a segment in + // a dot-separated name. is_extension is true iff a segment represents an + // extension (denoted with parentheses in options specs in .proto files). + // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents + // "foo.(bar.baz).qux". + message NamePart { + required string name_part = 1; + required bool is_extension = 2; + } + repeated NamePart name = 2; + + // The value of the uninterpreted option, in whatever type the tokenizer + // identified it as during parsing. Exactly one of these should be set. + optional string identifier_value = 3; + optional uint64 positive_int_value = 4; + optional int64 negative_int_value = 5; + optional double double_value = 6; + optional bytes string_value = 7; + optional string aggregate_value = 8; +} + +// =================================================================== +// Optional source code info + +// Encapsulates information about the original source file from which a +// FileDescriptorProto was generated. +message SourceCodeInfo { + // A Location identifies a piece of source code in a .proto file which + // corresponds to a particular definition. This information is intended + // to be useful to IDEs, code indexers, documentation generators, and similar + // tools. + // + // For example, say we have a file like: + // message Foo { + // optional string foo = 1; + // } + // Let's look at just the field definition: + // optional string foo = 1; + // ^ ^^ ^^ ^ ^^^ + // a bc de f ghi + // We have the following locations: + // span path represents + // [a,i) [ 4, 0, 2, 0 ] The whole field definition. + // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + // + // Notes: + // - A location may refer to a repeated field itself (i.e. not to any + // particular index within it). This is used whenever a set of elements are + // logically enclosed in a single code segment. For example, an entire + // extend block (possibly containing multiple extension definitions) will + // have an outer location whose path refers to the "extensions" repeated + // field without an index. + // - Multiple locations may have the same path. This happens when a single + // logical declaration is spread out across multiple places. The most + // obvious example is the "extend" block again -- there may be multiple + // extend blocks in the same scope, each of which will have the same path. + // - A location's span is not always a subset of its parent's span. For + // example, the "extendee" of an extension declaration appears at the + // beginning of the "extend" block and is shared by all extensions within + // the block. + // - Just because a location's span is a subset of some other location's span + // does not mean that it is a descendant. For example, a "group" defines + // both a type and a field in a single declaration. Thus, the locations + // corresponding to the type and field and their components will overlap. + // - Code which tries to interpret locations should probably be designed to + // ignore those that it doesn't understand, as more types of locations could + // be recorded in the future. + repeated Location location = 1; + message Location { + // Identifies which part of the FileDescriptorProto was defined at this + // location. + // + // Each element is a field number or an index. They form a path from + // the root FileDescriptorProto to the place where the definition. For + // example, this path: + // [ 4, 3, 2, 7, 1 ] + // refers to: + // file.message_type(3) // 4, 3 + // .field(7) // 2, 7 + // .name() // 1 + // This is because FileDescriptorProto.message_type has field number 4: + // repeated DescriptorProto message_type = 4; + // and DescriptorProto.field has field number 2: + // repeated FieldDescriptorProto field = 2; + // and FieldDescriptorProto.name has field number 1: + // optional string name = 1; + // + // Thus, the above path gives the location of a field name. If we removed + // the last element: + // [ 4, 3, 2, 7 ] + // this path refers to the whole field declaration (from the beginning + // of the label to the terminating semicolon). + repeated int32 path = 1 [packed = true]; + + // Always has exactly three or four elements: start line, start column, + // end line (optional, otherwise assumed same as start line), end column. + // These are packed into a single field for efficiency. Note that line + // and column numbers are zero-based -- typically you will want to add + // 1 to each before displaying to a user. + repeated int32 span = 2 [packed = true]; + + // If this SourceCodeInfo represents a complete declaration, these are any + // comments appearing before and after the declaration which appear to be + // attached to the declaration. + // + // A series of line comments appearing on consecutive lines, with no other + // tokens appearing on those lines, will be treated as a single comment. + // + // leading_detached_comments will keep paragraphs of comments that appear + // before (but not connected to) the current element. Each paragraph, + // separated by empty lines, will be one comment element in the repeated + // field. + // + // Only the comment content is provided; comment markers (e.g. //) are + // stripped out. For block comments, leading whitespace and an asterisk + // will be stripped from the beginning of each line other than the first. + // Newlines are included in the output. + // + // Examples: + // + // optional int32 foo = 1; // Comment attached to foo. + // // Comment attached to bar. + // optional int32 bar = 2; + // + // optional string baz = 3; + // // Comment attached to baz. + // // Another line attached to baz. + // + // // Comment attached to qux. + // // + // // Another line attached to qux. + // optional double qux = 4; + // + // // Detached comment for corge. This is not leading or trailing comments + // // to qux or corge because there are blank lines separating it from + // // both. + // + // // Detached comment for corge paragraph 2. + // + // optional string corge = 5; + // /* Block comment attached + // * to corge. Leading asterisks + // * will be removed. */ + // /* Block comment attached to + // * grault. */ + // optional int32 grault = 6; + // + // // ignored detached comments. + optional string leading_comments = 3; + optional string trailing_comments = 4; + repeated string leading_detached_comments = 6; + } +} + +// Describes the relationship between generated code and its original source +// file. A GeneratedCodeInfo message is associated with only one generated +// source file, but may contain references to different source .proto files. +message GeneratedCodeInfo { + // An Annotation connects some span of text in generated code to an element + // of its generating .proto file. + repeated Annotation annotation = 1; + message Annotation { + // Identifies the element in the original source .proto file. This field + // is formatted the same as SourceCodeInfo.Location.path. + repeated int32 path = 1 [packed = true]; + + // Identifies the filesystem path to the original source .proto. + optional string source_file = 2; + + // Identifies the starting offset in bytes in the generated code + // that relates to the identified object. + optional int32 begin = 3; + + // Identifies the ending offset in bytes in the generated code that + // relates to the identified offset. The end offset should be one past + // the last relevant byte (so the length of the text = end - begin). + optional int32 end = 4; + } +} diff --git a/proto/rpc.proto b/proto/rpc.proto index 1ab035dc..87fef057 100644 --- a/proto/rpc.proto +++ b/proto/rpc.proto @@ -18,56 +18,54 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -// Author: Shardul Deo +// Authors: Shardul Deo, BJ Neilsen // // Protobufs needed for socket rpcs. -package protobuf.socketrpc; +syntax = "proto2"; -message Request { +package protobuf.socketrpc; - // RPC service full name - required string service_name = 1; - - // RPC method name - required string method_name = 2; - - // RPC request proto - required bytes request_proto = 3; +message Request +{ + required string service_name = 1; // Fully- qualified Service class name + required string method_name = 2; // Service method to invoke + optional bytes request_proto = 3; // Serialized request bytes + optional string caller = 4; // Calling hostname or address + repeated Header headers = 5; // General purpose request headers } -message Response { +message Response +{ + optional bytes response_proto = 1; // Serialized response + optional string error = 2; // Error message, if any + optional bool callback = 3 [default = false]; // Was callback invoked (not sure what this is for) + optional ErrorReason error_reason = 4; // Error Reason + optional string server = 5; // Server hostname or address +} - // RPC response proto - optional bytes response_proto = 1; - - // Error, if any - optional string error = 2; - - // Was callback invoked - optional bool callback = 3 [default = false]; - - // Error Reason - optional ErrorReason error_reason = 4; +message Header { + required string key = 1; + optional string value = 2; } // Possible error reasons // The server-side errors are returned in the response from the server. -// The client-side errors are returned by the client-side code when it doesn't +// The client-side errors are returned by the client-side code when it doesn't // have a response from the server. -enum ErrorReason { - +enum ErrorReason +{ // Server-side errors - BAD_REQUEST_DATA = 0; // Server received bad request data - BAD_REQUEST_PROTO = 1; // Server received bad request proto - SERVICE_NOT_FOUND = 2; // Service not found on server - METHOD_NOT_FOUND = 3; // Method not found on server - RPC_ERROR = 4; // Rpc threw exception on server - RPC_FAILED = 5; // Rpc failed on server - + BAD_REQUEST_DATA = 0; // Server received bad request data + BAD_REQUEST_PROTO = 1; // Server received bad request proto + SERVICE_NOT_FOUND = 2; // Service not found on server + METHOD_NOT_FOUND = 3; // Method not found on server + RPC_ERROR = 4; // Rpc threw exception on server + RPC_FAILED = 5; // Rpc failed on server + // Client-side errors (these are returned by the client-side code) - INVALID_REQUEST_PROTO = 6; // Rpc was called with invalid request proto - BAD_RESPONSE_PROTO = 7; // Server returned a bad response proto - UNKNOWN_HOST = 8; // Could not find supplied host - IO_ERROR = 9; // I/O error while communicating with server + INVALID_REQUEST_PROTO = 6; // Rpc was called with invalid request proto + BAD_RESPONSE_PROTO = 7; // Server returned a bad response proto + UNKNOWN_HOST = 8; // Could not find supplied host + IO_ERROR = 9; // I/O error while communicating with server } diff --git a/protobuf.gemspec b/protobuf.gemspec index 3528c0b5..698c434b 100644 --- a/protobuf.gemspec +++ b/protobuf.gemspec @@ -1,30 +1,36 @@ -# encoding: UTF-8 -$:.push File.expand_path("./lib", File.dirname(__FILE__)) +$LOAD_PATH.push ::File.expand_path("../lib", __FILE__) require "protobuf/version" -Gem::Specification.new do |s| +::Gem::Specification.new do |s| s.name = 'protobuf' - s.version = Protobuf::VERSION - s.date = Time.now.strftime('%Y-%m-%d') + s.version = ::Protobuf::VERSION + s.date = ::Time.now.strftime('%Y-%m-%d') + s.license = 'MIT' + + s.authors = ['BJ Neilsen', 'Brandon Dewitt', 'Devin Christensen', 'Adam Hutchison'] + s.email = ['bj.neilsen+protobuf@gmail.com', 'brandonsdewitt+protobuf@gmail.com', 'quixoten@gmail.com', 'liveh2o@gmail.com'] + s.homepage = 'https://github.com/localshred/protobuf' + s.summary = "Google Protocol Buffers serialization and RPC implementation for Ruby." + s.description = s.summary - s.authors = ['BJ Neilsen', 'Brandon Dewitt'] - s.email = ["bj.neilsen@gmail.com", "brandonsdewitt+protobuf@gmail.com"] - s.homepage = %q{https://github.com/localshred/protobuf} - s.summary = 'Ruby implementation for Protocol Buffers. Works with other protobuf rpc implementations (e.g. Java, Python, C++).' - s.description = s.summary + "\n\nThis gem has diverged from https://github.com/macks/ruby-protobuf. All credit for serialization and rprotoc work most certainly goes to the original authors. All RPC implementation code (client/server/service) was written and is maintained by this author. Attempts to reconcile the original codebase with the current RPC implementation went unsuccessful." - s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } s.require_paths = ["lib"] - - s.add_dependency 'eventmachine', '~> 0.12.10' - s.add_dependency 'eventually', '~> 0.1.0' - s.add_dependency 'json_pure', '~> 1.6.4' - s.add_development_dependency 'rake', '~> 0.8.7' - s.add_development_dependency 'rspec', '~> 2.8.0' - s.add_development_dependency 'yard', '~> 0.7.4' - s.add_development_dependency 'redcarpet', '~> 1.17.2' - s.add_development_dependency 'simplecov', '~> 0.5.4' + # Hack, as Rails 5 requires Ruby version >= 2.2.2. + active_support_max_version = "< 5" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.2.2") + s.add_dependency "activesupport", '>= 3.2', active_support_max_version + s.add_dependency 'middleware' + s.add_dependency 'thor' + s.add_dependency 'thread_safe' + + s.add_development_dependency 'benchmark-ips' + s.add_development_dependency 'ffi-rzmq' + s.add_development_dependency 'rake', '~> 13.0' + s.add_development_dependency 'rspec', '~> 3.5' + s.add_development_dependency "rubocop", "~> 0.81.0" + s.add_development_dependency 'simplecov' + s.add_development_dependency 'timecop' + s.add_development_dependency 'yard' end diff --git a/script/mk_parser b/script/mk_parser deleted file mode 100755 index 2b4a25e5..00000000 --- a/script/mk_parser +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -racc -E lib/protobuf/compiler/proto.y -o lib/protobuf/compiler/proto_parser.rb diff --git a/spec/benchmark/tasks.rb b/spec/benchmark/tasks.rb index 4c50c0e9..5d7f93a3 100644 --- a/spec/benchmark/tasks.rb +++ b/spec/benchmark/tasks.rb @@ -1,9 +1,27 @@ require 'benchmark' -require 'helper/all' -require 'proto/test_service_impl' +require 'protobuf' +require 'protobuf/socket' +require 'support/all' +require 'spec_helper' +require SUPPORT_PATH.join('resource_service') + +case RUBY_ENGINE.to_sym +when :ruby + require 'ruby-prof' +when :rbx + require 'rubinius/profiler' +when :jruby + require 'jruby/profiler' +end + +# Including a way to turn on debug logger for spec runs +if ENV["DEBUG"] + puts 'debugging' + debug_log = File.expand_path('../../../debug_bench.log', __FILE__) + Protobuf::Logging.initialize_logger(debug_log, ::Logger::DEBUG) +end namespace :benchmark do - include SilentConstants def benchmark_wrapper(global_bench = nil) if global_bench @@ -15,112 +33,111 @@ def benchmark_wrapper(global_bench = nil) end end - def em_client_em_server(number_tests, test_length, global_bench = nil) - EM.stop if EM.reactor_running? + def sock_client_sock_server(number_tests, test_length, global_bench = nil) + load "protobuf/socket.rb" + + port = 1000 + Kernel.rand(2**16 - 1000) - EventMachine.fiber_run do - StubServer.new do |server| - client = Spec::Proto::TestService.client(:async => false) + StubServer.new(:server => Protobuf::Rpc::Socket::Server, :port => port) do + client = ::Test::ResourceService.client(:port => port) - benchmark_wrapper(global_bench) do |bench| - bench.report("ES / EC") do - (1..number_tests.to_i).each { client.find(:name => "Test Name" * test_length.to_i, :active => true) } - end + benchmark_wrapper(global_bench) do |bench| + bench.report("SS / SC") do + Integer(number_tests).times { client.find(:name => "Test Name" * Integer(test_length), :active => true) } end end - - EM.stop end end - def em_client_sock_server(number_tests, test_length, global_bench = nil) - EM.stop if EM.reactor_running? + def zmq_client_zmq_server(number_tests, test_length, global_bench = nil) + load "protobuf/zmq.rb" - EventMachine.fiber_run do - StubServer.new(:server => Protobuf::Rpc::SocketServer, :port => 9399) do |server| - client = Spec::Proto::TestService.client(:async => false, :port => 9399) + port = 1000 + Kernel.rand(2**16 - 1000) - benchmark_wrapper(global_bench) do |bench| - bench.report("SS / EC") do - (1..number_tests.to_i).each { client.find(:name => "Test Name" * test_length.to_i, :active => true) } - end + StubServer.new(:port => port, :server => Protobuf::Rpc::Zmq::Server) do + client = ::Test::ResourceService.client(:port => port) + + benchmark_wrapper(global_bench) do |bench| + bench.report("ZS / ZC") do + Integer(number_tests).times { client.find(:name => "Test Name" * Integer(test_length), :active => true) } end end - - EM.stop end end - def sock_client_sock_server(number_tests, test_length, global_bench = nil) - EM.stop if EM.reactor_running? + desc "benchmark ZMQ client with ZMQ server" + task :zmq_client_zmq_server, [:number, :length] do |_task, args| + args.with_defaults(:number => 1000, :length => 100) + zmq_client_zmq_server(args[:number], args[:length]) + end - StubServer.new(:server => Protobuf::Rpc::SocketServer, :port => 9399) do |server| - with_constants "Protobuf::ClientType" => "Socket" do - client = Spec::Proto::TestService.client(:async => false, :port => 9399) + desc "benchmark ZMQ client with ZMQ server and profile" + task :zmq_profile, [:number, :length, :profile_output] do |_task, args| + args.with_defaults(:number => 1000, :length => 100, :profile_output => "/tmp/zmq_profiler_#{Time.now.to_i}") - benchmark_wrapper(global_bench) do |bench| - bench.report("SS / SC") do - (1..number_tests.to_i).each { client.find(:name => "Test Name" * test_length.to_i, :active => true) } - end - end - end + profile_code(args[:profile_output]) do + zmq_client_zmq_server(args[:number], args[:length]) end - end - - def sock_client_em_server(number_tests, test_length, global_bench = nil) - EM.stop if EM.reactor_running? - em_thread = Thread.new { EM.run } - Thread.pass until EM.reactor_running? - StubServer.new(:port => 9399) do |server| - with_constants "Protobuf::ClientType" => "Socket" do - client = Spec::Proto::TestService.client(:async => false, :port => 9399) + puts args[:profile_output] + end - benchmark_wrapper(global_bench) do |bench| - bench.report("ES / SC") do - (1..number_tests.to_i).each { client.find(:name => "Test Name" * test_length.to_i, :active => true) } - end - end - end + desc "benchmark Protobuf Message #new" + task :profile_protobuf_new, [:number, :profile_output] do |_task, args| + args.with_defaults(:number => 1000, :profile_output => "/tmp/profiler_new_#{Time.now.to_i}") + create_params = { :name => "The name that we set", :date_created => Time.now.to_i, :status => 2 } + profile_code(args[:profile_output]) do + Integer(args[:number]).times { Test::Resource.new(create_params) } end - EM.stop - Thread.kill(em_thread) if em_thread + puts args[:profile_output] end - desc "benchmark EventMachine client with EventMachine server" - task :em_client_em_server, [:number, :length] do |t, args| - args.with_defaults(:number => 1000, :length => 100) - em_client_em_server(args[:number], args[:length]) + desc "benchmark Protobuf Message #serialize" + task :profile_protobuf_serialize, [:number, :profile_output] do |_task, args| + args.with_defaults(:number => 1000, :profile_output => "/tmp/profiler_new_#{Time.now.to_i}") + create_params = { :name => "The name that we set", :date_created => Time.now.to_i, :status => 2 } + profile_code(args[:profile_output]) do + Integer(args[:number]).times { Test::Resource.decode(Test::Resource.new(create_params).serialize) } + end + + puts args[:profile_output] end - desc "benchmark EventMachine client with Socket server" - task :em_client_sock_server, [:number, :length] do |t, args| - args.with_defaults(:number => 1000, :length => 100) - em_client_sock_server(args[:number], args[:length]) + def profile_code(output, &block) + case RUBY_ENGINE.to_sym + when :ruby + profile_data = RubyProf.profile(&block) + ::File.open(output, "w") do |output_file| + RubyProf::FlatPrinter.new(profile_data).print(output_file) + end + when :rbx + profiler = Rubinius::Profiler::Instrumenter.new + profiler.profile(false, &block) + File.open(output, 'w') do |f| + profiler.show(f) + end + when :jruby + profile_data = JRuby::Profiler.profile(&block) + File.open(output, 'w') do |f| + JRuby::Profiler::FlatProfilePrinter.new(profile_data).printProfile(f) + end + end end desc "benchmark Socket client with Socket server" - task :sock_client_sock_server, [:number, :length] do |t, args| + task :sock_client_sock_server, [:number, :length] do |_task, args| args.with_defaults(:number => 1000, :length => 100) sock_client_sock_server(args[:number], args[:length]) end - desc "benchmark Socket client with EventMachine server" - task :sock_client_em_server, [:number, :length] do |t, args| - args.with_defaults(:number => 1000, :length => 100) - sock_client_em_server(args[:number], args[:length]) - end - desc "benchmark server performance" - task :servers, [:number, :length] do |t, args| + task :servers, [:number, :length] do |_task, args| args.with_defaults(:number => 1000, :length => 100) Benchmark.bm(10) do |bench| - em_client_em_server(args[:number], args[:length], bench) - em_client_sock_server(args[:number], args[:length], bench) + zmq_client_zmq_server(args[:number], args[:length], bench) sock_client_sock_server(args[:number], args[:length], bench) - sock_client_em_server(args[:number], args[:length], bench) end end end diff --git a/spec/bin/protoc-gen-ruby_spec.rb b/spec/bin/protoc-gen-ruby_spec.rb new file mode 100644 index 00000000..1df8b155 --- /dev/null +++ b/spec/bin/protoc-gen-ruby_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +require 'protobuf/code_generator' + +RSpec.describe 'protoc-gen-ruby' do + let(:binpath) { ::File.expand_path('../../../bin/protoc-gen-ruby', __FILE__) } + let(:package) { 'test' } + let(:request_bytes) do + ::Google::Protobuf::Compiler::CodeGeneratorRequest.encode( + :proto_file => [{ :package => package }], + ) + end + + it 'reads the serialized request bytes and outputs serialized response bytes' do + ::IO.popen(binpath, 'w+') do |pipe| + pipe.write(request_bytes) + pipe.close_write # needed so we can implicitly read until EOF + response_bytes = pipe.read + response = ::Google::Protobuf::Compiler::CodeGeneratorResponse.decode(response_bytes) + expect(response.file.first.content).to include("module #{package.titleize}") + end + end +end diff --git a/spec/encoding/all_types_spec.rb b/spec/encoding/all_types_spec.rb new file mode 100644 index 00000000..fbd38b46 --- /dev/null +++ b/spec/encoding/all_types_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' +require PROTOS_PATH.join('google_unittest.pb') + +RSpec.describe ::Protobuf do + it "correctly encodes all types" do + message = Protobuf_unittest::TestAllTypes.new( + :optional_int32 => 101, + :optional_int64 => 102, + :optional_uint32 => 103, + :optional_uint64 => 104, + :optional_sint32 => 105, + :optional_sint64 => 106, + :optional_fixed32 => 107, + :optional_fixed64 => 108, + :optional_sfixed32 => 109, + :optional_sfixed64 => 110, + :optional_float => 111, + :optional_double => 112, + :optional_bool => true, + :optional_string => "115", + :optional_bytes => "116", + :optional_nested_message => Protobuf_unittest::TestAllTypes::NestedMessage.new(:bb => 118), + :optional_foreign_message => Protobuf_unittest::ForeignMessage.new(:c => 119), + :optional_import_message => Protobuf_unittest_import::ImportMessage.new(:d => 120), + :optional_nested_enum => Protobuf_unittest::TestAllTypes::NestedEnum::BAZ, + :optional_foreign_enum => Protobuf_unittest::ForeignEnum::FOREIGN_BAZ, + :optional_import_enum => Protobuf_unittest_import::ImportEnum::IMPORT_BAZ, + :optional_string_piece => "124", + :optional_cord => "125", + :optional_public_import_message => Protobuf_unittest_import::PublicImportMessage.new(:e => 126), + :optional_lazy_message => Protobuf_unittest::TestAllTypes::NestedMessage.new(:bb => 127), + :repeated_int32 => [201, 301], + :repeated_int64 => [202, 302], + :repeated_uint32 => [203, 303], + :repeated_uint64 => [204, 304], + :repeated_sint32 => [205, 305], + :repeated_sint64 => [206, 306], + :repeated_fixed32 => [207, 307], + :repeated_fixed64 => [208, 308], + :repeated_sfixed32 => [209, 309], + :repeated_sfixed64 => [210, 310], + :repeated_float => [211, 311], + :repeated_double => [212, 312], + :repeated_bool => [true, false], + :repeated_string => ["215", "315"], + :repeated_bytes => ["216", "316"], + :repeated_nested_message => [ + ::Protobuf_unittest::TestAllTypes::NestedMessage.new(:bb => 218), + ::Protobuf_unittest::TestAllTypes::NestedMessage.new(:bb => 318), + ], + :repeated_foreign_message => [ + ::Protobuf_unittest::ForeignMessage.new(:c => 219), + ::Protobuf_unittest::ForeignMessage.new(:c => 319), + ], + :repeated_import_message => [ + ::Protobuf_unittest_import::ImportMessage.new(:d => 220), + ::Protobuf_unittest_import::ImportMessage.new(:d => 320), + ], + :repeated_nested_enum => [ + ::Protobuf_unittest::TestAllTypes::NestedEnum::BAR, + ::Protobuf_unittest::TestAllTypes::NestedEnum::BAZ, + ], + :repeated_foreign_enum => [ + ::Protobuf_unittest::ForeignEnum::FOREIGN_BAR, + ::Protobuf_unittest::ForeignEnum::FOREIGN_BAZ, + ], + :repeated_import_enum => [ + ::Protobuf_unittest_import::ImportEnum::IMPORT_BAR, + ::Protobuf_unittest_import::ImportEnum::IMPORT_BAZ, + ], + :repeated_string_piece => ["224", "324"], + :repeated_cord => ["225", "325"], + :repeated_lazy_message => [ + ::Protobuf_unittest::TestAllTypes::NestedMessage.new(:bb => 227), + ::Protobuf_unittest::TestAllTypes::NestedMessage.new(:bb => 327), + ], + :default_int32 => 401, + :default_int64 => 402, + :default_uint32 => 403, + :default_uint64 => 404, + :default_sint32 => 405, + :default_sint64 => 406, + :default_fixed32 => 407, + :default_fixed64 => 408, + :default_sfixed32 => 409, + :default_sfixed64 => 410, + :default_float => 411, + :default_double => 412, + :default_bool => false, + :default_string => "415", + :default_bytes => "416", + :default_nested_enum => ::Protobuf_unittest::TestAllTypes::NestedEnum::FOO, + :default_foreign_enum => ::Protobuf_unittest::ForeignEnum::FOREIGN_FOO, + :default_import_enum => ::Protobuf_unittest_import::ImportEnum::IMPORT_FOO, + :default_string_piece => "424", + :default_cord => "425", + ) + + data_file_path = PROTOS_PATH.join('all_types.data.bin') + data = File.open(data_file_path, 'rb', &:read) + expect(data).to eq(message.serialize_to_string) + end +end diff --git a/spec/encoding/extreme_values_spec.rb b/spec/encoding/extreme_values_spec.rb new file mode 100644 index 00000000..477e695a Binary files /dev/null and b/spec/encoding/extreme_values_spec.rb differ diff --git a/spec/functional/class_inheritance_spec.rb b/spec/functional/class_inheritance_spec.rb new file mode 100644 index 00000000..5a191b52 --- /dev/null +++ b/spec/functional/class_inheritance_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' +require SUPPORT_PATH.join('resource_service') + +RSpec.describe 'works through class inheritance' do + module Corp + module Protobuf + class Error < ::Protobuf::Message + required :string, :foo, 1 + end + end + end + module Corp + class ErrorHandler < Corp::Protobuf::Error + end + end + + let(:args) { { :foo => 'bar' } } + let(:parent_class) { Corp::Protobuf::Error } + let(:inherited_class) { Corp::ErrorHandler } + + specify '#encode' do + expected_result = "\n\x03bar" + expected_result.force_encoding(Encoding::BINARY) + expect(parent_class.new(args).encode).to eq(expected_result) + expect(inherited_class.new(args).encode).to eq(expected_result) + end + + specify '#to_hash' do + expect(parent_class.new(args).to_hash).to eq(args) + expect(inherited_class.new(args).to_hash).to eq(args) + end + + specify '#to_json' do + expect(parent_class.new(args).to_json).to eq(args.to_json) + expect(inherited_class.new(args).to_json).to eq(args.to_json) + end + + specify '.encode' do + expected_result = "\n\x03bar" + expected_result.force_encoding(Encoding::BINARY) + expect(parent_class.encode(args)).to eq(expected_result) + expect(inherited_class.encode(args)).to eq(expected_result) + end + + specify '.decode' do + raw_value = "\n\x03bar" + raw_value.force_encoding(Encoding::BINARY) + expect(parent_class.decode(raw_value).to_hash).to eq(args) + expect(inherited_class.decode(raw_value).to_hash).to eq(args) + end + +end diff --git a/spec/functional/code_generator_spec.rb b/spec/functional/code_generator_spec.rb new file mode 100644 index 00000000..3f767b23 --- /dev/null +++ b/spec/functional/code_generator_spec.rb @@ -0,0 +1,58 @@ +# encoding: binary + +require 'spec_helper' +require 'protobuf/code_generator' + +RSpec.describe 'code generation' do + it "generates code for google's unittest.proto" do + bytes = IO.read(PROTOS_PATH.join('google_unittest.bin'), :mode => 'rb') + + expected_files = + ["google_unittest_import_public.pb.rb", "google_unittest_import.pb.rb", "google_unittest.pb.rb"] + + expected_file_descriptors = expected_files.map do |file_name| + file_content = File.open(PROTOS_PATH.join(file_name), "r:UTF-8", &:read) + ::Google::Protobuf::Compiler::CodeGeneratorResponse::File.new( + :name => "protos/" + file_name, :content => file_content) + end + + expected_output = + ::Google::Protobuf::Compiler::CodeGeneratorResponse.encode(:file => expected_file_descriptors, :supported_features => 1) + + code_generator = ::Protobuf::CodeGenerator.new(bytes) + code_generator.eval_unknown_extensions! + expect(code_generator.response_bytes).to eq(expected_output) + end + + it "generates code for map types" do + input_descriptor = ::Google::Protobuf::FileDescriptorSet.decode( + IO.read(PROTOS_PATH.join('map-test.bin'), :mode => 'rb')) + request = ::Google::Protobuf::Compiler::CodeGeneratorRequest.new(:file_to_generate => ['map-test.proto'], + :proto_file => input_descriptor.file) + + file_name = "map-test.pb.rb" + file_content = File.open(PROTOS_PATH.join(file_name), "r:UTF-8", &:read) + expected_file_output = + ::Google::Protobuf::Compiler::CodeGeneratorResponse::File.new( + :name => file_name, :content => file_content) + + expected_response = + ::Google::Protobuf::Compiler::CodeGeneratorResponse.encode(:file => [expected_file_output], :supported_features => 1) + + code_generator = ::Protobuf::CodeGenerator.new(request.encode) + code_generator.eval_unknown_extensions! + expect(code_generator.response_bytes).to eq(expected_response) + end + + it "generates code (including service stubs) with custom field and method options" do + expected_unittest_custom_options = + File.open(PROTOS_PATH.join('google_unittest_custom_options.pb.rb'), "r:UTF-8", &:read) + + bytes = IO.read(PROTOS_PATH.join('google_unittest_custom_options.bin'), :mode => 'rb') + code_generator = ::Protobuf::CodeGenerator.new(bytes) + code_generator.eval_unknown_extensions! + response = ::Google::Protobuf::Compiler::CodeGeneratorResponse.decode(code_generator.response_bytes) + expect(response.file.find { |f| f.name == 'protos/google_unittest_custom_options.pb.rb' }.content) + .to eq(expected_unittest_custom_options) + end +end diff --git a/spec/functional/embedded_service_spec.rb b/spec/functional/embedded_service_spec.rb deleted file mode 100644 index e2a5c529..00000000 --- a/spec/functional/embedded_service_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'spec_helper' - -describe 'An Embedded Service Call Hierarchy' do - - - -end \ No newline at end of file diff --git a/spec/functional/socket_server_spec.rb b/spec/functional/socket_server_spec.rb new file mode 100644 index 00000000..b586fff5 --- /dev/null +++ b/spec/functional/socket_server_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +require SUPPORT_PATH.join('resource_service') + +RSpec.describe 'Functional Socket Client' do + before(:all) do + load "protobuf/socket.rb" + @options = OpenStruct.new(:host => "127.0.0.1", :port => 9399, :backlog => 100, :threshold => 100) + @runner = ::Protobuf::Rpc::SocketRunner.new(@options) + @server_thread = Thread.new(@runner, &:run) + Thread.pass until @runner.running? + end + + after(:all) do + @runner.stop + @server_thread.join + end + + it 'runs fine when required fields are set' do + expect do + client = ::Test::ResourceService.client + + client.find(:name => 'Test Name', :active => true) do |c| + c.on_success do |succ| + expect(succ.name).to eq("Test Name") + expect(succ.status).to eq(::Test::StatusType::ENABLED) + end + + c.on_failure do |err| + fail err.inspect + end + end + end.to_not raise_error + end + + it 'calls the on_failure callback when a message is malformed' do + error = nil + request = ::Test::ResourceFindRequest.new(:active => true) + client = ::Test::ResourceService.client + + client.find(request) do |c| + c.on_success { fail "shouldn't pass" } + c.on_failure { |e| error = e } + end + + expect(error.message).to match(/Required field.*does not have a value/) + end + + it 'calls the on_failure callback when the request type is wrong' do + error = nil + request = ::Test::Resource.new(:name => 'Test Name') + client = ::Test::ResourceService.client + + client.find(request) do |c| + c.on_success { fail "shouldn't pass" } + c.on_failure { |e| error = e } + end + expect(error.message).to match(/expected request.*ResourceFindRequest.*Resource instead/i) + end +end diff --git a/spec/functional/zmq_server_spec.rb b/spec/functional/zmq_server_spec.rb new file mode 100644 index 00000000..6809f631 --- /dev/null +++ b/spec/functional/zmq_server_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +require 'protobuf/rpc/service_directory' +require SUPPORT_PATH.join('resource_service') + +RSpec.describe 'Functional ZMQ Client' do + before(:all) do + load "protobuf/zmq.rb" + @runner = ::Protobuf::Rpc::ZmqRunner.new( + 'host' => '127.0.0.1', + 'port' => 9399, + 'worker_port' => 9408, + 'backlog' => 100, + 'threshold' => 100, + 'threads' => 5, + ) + @server_thread = Thread.new(@runner, &:run) + Thread.pass until @runner.running? + end + + after(:all) do + @runner.stop + @server_thread.join + end + + it 'runs fine when required fields are set' do + expect do + client = ::Test::ResourceService.client + + client.find(:name => 'Test Name', :active => true) do |c| + c.on_success do |succ| + expect(succ.name).to eq("Test Name") + expect(succ.status).to eq(::Test::StatusType::ENABLED) + end + + c.on_failure do |err| + fail err.inspect + end + end + end.to_not raise_error + end + + it 'runs under heavy load' do + 10.times do + 5.times.map do + Thread.new do + client = ::Test::ResourceService.client + + client.find(:name => 'Test Name', :active => true) do |c| + c.on_success do |succ| + expect(succ.name).to eq("Test Name") + expect(succ.status).to eq(::Test::StatusType::ENABLED) + end + + c.on_failure do |err| + fail err.inspect + end + end + end + end.each(&:join) + end + end + + context 'when a message is malformed' do + it 'calls the on_failure callback' do + error = nil + request = ::Test::ResourceFindRequest.new(:active => true) + client = ::Test::ResourceService.client + + client.find(request) do |c| + c.on_success { fail "shouldn't pass" } + c.on_failure { |e| error = e } + end + expect(error.message).to match(/Required field.*does not have a value/) + end + end + + context 'when the request type is wrong' do + it 'calls the on_failure callback' do + error = nil + request = ::Test::Resource.new(:name => 'Test Name') + client = ::Test::ResourceService.client + + client.find(request) do |c| + c.on_success { fail "shouldn't pass" } + c.on_failure { |e| error = e } + end + expect(error.message).to match(/expected request.*ResourceFindRequest.*Resource instead/i) + end + end + + context 'when the server takes too long to respond' do + it 'responds with a timeout error' do + error = nil + client = ::Test::ResourceService.client(:timeout => 1) + + client.find_with_sleep(:sleep => 2) do |c| + c.on_success { fail "shouldn't pass" } + c.on_failure { |e| error = e } + end + expect(error.message).to match(/The server repeatedly failed to respond/) + end + end + +end diff --git a/spec/helper/all.rb b/spec/helper/all.rb deleted file mode 100644 index 802cc96a..00000000 --- a/spec/helper/all.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'helper/tolerance_matcher' -require 'helper/server' -require 'helper/silent_constants' - -def now - Time.new.to_f -end diff --git a/spec/helper/server.rb b/spec/helper/server.rb deleted file mode 100644 index aea334d7..00000000 --- a/spec/helper/server.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'ostruct' -require 'protobuf/common/logger' -require 'protobuf/rpc/server' -require 'protobuf/rpc/servers/socket_server' -require 'protobuf/rpc/servers/socket_runner' -require 'spec/proto/test_service_impl' - -module StubProtobufServerFactory - def self.build(delay) - new_server = Class.new(Protobuf::Rpc::EventedServer) do - def self.sleep_interval - @sleep_interval - end - - def self.sleep_interval=(si) - @sleep_interval = si - end - - def post_init - sleep self.class.sleep_interval - super - end - end - - new_server.sleep_interval = delay - return new_server - end -end - -class StubServer - include Protobuf::Logger::LogMethods - - attr_accessor :options - - def initialize(opts = {}) - @running = true - @options = OpenStruct.new({ - :host => "127.0.0.1", - :port => 9399, - :delay => 0, - :server => Protobuf::Rpc::EventedServer - }.merge(opts)) - - start - yield self - ensure - stop if @running - end - - def start - if @options.server == Protobuf::Rpc::EventedServer - start_em_server - else - @sock_server = Thread.new(@options) { |opt| Protobuf::Rpc::SocketRunner.run(opt) } - @sock_server.abort_on_exception = true # Set for testing purposes - Thread.pass until Protobuf::Rpc::SocketServer.running? - end - log_debug "[stub-server] Server started #{@options.host}:#{@options.port}" - rescue => ex - if ex =~ /no acceptor/ # Means EM didn't shutdown in the next_tick yet - stop - retry - end - end - - def start_em_server - @server_handle = EventMachine::start_server(@options.host, @options.port, StubProtobufServerFactory.build(@options.delay)) - end - - def stop - if @options.server == Protobuf::Rpc::EventedServer - EventMachine.stop_server(@server_handle) if @server_handle - else - Protobuf::Rpc::SocketRunner.stop - Thread.kill(@sock_server) if @sock_server - end - @running = false - end -end diff --git a/spec/helper/silent_constants.rb b/spec/helper/silent_constants.rb deleted file mode 100644 index 2b6b93a3..00000000 --- a/spec/helper/silent_constants.rb +++ /dev/null @@ -1,40 +0,0 @@ -# Just a way to keep warnings from being flagged in rename of constants during tests -module Kernel - def silence_warnings - orig_verbosity = $VERBOSE - $VERBOSE = nil - yield - $VERBOSE = orig_verbosity - end -end - -module SilentConstants - def parse(constant) - source, _, constant_name = constant.to_s.rpartition('::') - [Object.const_get(source.to_sym), constant_name.to_sym] - end - - # Examples - # with_constants "Something" => "nothing" do - # end - # - # with_constants "Something::Foo" => "something else" do - # end - # - def with_constants(constants, &block) - saved_constants = {} - constants.each do |constant, val| - source_object, const_name = parse(constant) - saved_constants[constant] = source_object.const_get(const_name) - Kernel::silence_warnings { source_object.const_set(const_name, val) } - end - - block.call - ensure - constants.each do |constant, val| - source_object, const_name = parse(constant) - Kernel::silence_warnings { source_object.const_set(const_name, saved_constants[constant]) } - end - end - -end diff --git a/spec/helper/tolerance_matcher.rb b/spec/helper/tolerance_matcher.rb deleted file mode 100644 index 50eba4eb..00000000 --- a/spec/helper/tolerance_matcher.rb +++ /dev/null @@ -1,40 +0,0 @@ -# -# courtesy of sander 6 -# - http://github.com/sander6/custom_matchers/blob/master/lib/matchers/tolerance_matchers.rb -# -module Sander6 - module CustomMatchers - - class ToleranceMatcher - def initialize(tolerance) - @tolerance = tolerance - @target = 0 - end - - def of(target) - @target = target - self - end - - def matches?(value) - @value = value - ((@target - @tolerance)..(@target + @tolerance)).include?(@value) - end - - def failure_message - "Expected #{@value.inspect} to be within #{@tolerance.inspect} of #{@target.inspect}, but it wasn't.\n" + - "Difference: #{@value - @target}" - end - - def negative_failure_message - "Expected #{@value.inspect} not to be within #{@tolerance.inspect} of #{@target.inspect}, but it was.\n" + - "Difference: #{@value - @target}" - end - end - - def be_within(value) - Sander6::CustomMatchers::ToleranceMatcher.new(value) - end - - end -end diff --git a/spec/lib/protobuf/cli_spec.rb b/spec/lib/protobuf/cli_spec.rb new file mode 100644 index 00000000..ea78ed0d --- /dev/null +++ b/spec/lib/protobuf/cli_spec.rb @@ -0,0 +1,317 @@ +require 'spec_helper' +require 'protobuf/cli' + +RSpec.describe ::Protobuf::CLI do + + let(:app_file) do + File.expand_path('../../../support/test_app_file.rb', __FILE__) + end + + let(:sock_runner) do + runner = double("SocketRunner", :register_signals => nil) + allow(runner).to receive(:run).and_return(::ActiveSupport::Notifications.publish("after_server_bind")) + runner + end + + let(:zmq_runner) do + runner = double("ZmqRunner", :register_signals => nil) + allow(runner).to receive(:run).and_return(::ActiveSupport::Notifications.publish("after_server_bind")) + runner + end + + around(:each) do |example| + logger = ::Protobuf::Logging.logger + example.run + ::Protobuf::Logging.logger = logger + end + + before(:each) do + allow(::Protobuf::Rpc::SocketRunner).to receive(:new).and_return(sock_runner) + allow(::Protobuf::Rpc::ZmqRunner).to receive(:new).and_return(zmq_runner) + end + + describe '#start' do + let(:base_args) { ['start', app_file] } + let(:test_args) { [] } + let(:args) { base_args + test_args } + + context 'host option' do + let(:test_args) { ['--host=123.123.123.123'] } + + it 'sends the host option to the runner' do + expect(::Protobuf::Rpc::SocketRunner).to receive(:new) do |options| + expect(options[:host]).to eq '123.123.123.123' + end.and_return(sock_runner) + described_class.start(args) + end + end + + context 'port option' do + let(:test_args) { ['--port=12345'] } + + it 'sends the port option to the runner' do + expect(::Protobuf::Rpc::SocketRunner).to receive(:new) do |options| + expect(options[:port]).to eq 12345 + end.and_return(sock_runner) + described_class.start(args) + end + end + + context 'threads option' do + let(:test_args) { ['--threads=500'] } + + it 'sends the threads option to the runner' do + expect(::Protobuf::Rpc::SocketRunner).to receive(:new) do |options| + expect(options[:threads]).to eq 500 + end.and_return(sock_runner) + described_class.start(args) + end + end + + context 'backlog option' do + let(:test_args) { ['--backlog=500'] } + + it 'sends the backlog option to the runner' do + expect(::Protobuf::Rpc::SocketRunner).to receive(:new) do |options| + expect(options[:backlog]).to eq 500 + end.and_return(sock_runner) + described_class.start(args) + end + end + + context 'threshold option' do + let(:test_args) { ['--threshold=500'] } + + it 'sends the backlog option to the runner' do + expect(::Protobuf::Rpc::SocketRunner).to receive(:new) do |options| + expect(options[:threshold]).to eq 500 + end.and_return(sock_runner) + described_class.start(args) + end + end + + context 'log options' do + let(:test_args) { ['--log=mylog.log', '--level=0'] } + + it 'sends the log file and level options to the runner' do + expect(::Protobuf::Logging).to receive(:initialize_logger) do |file, level| + expect(file).to eq 'mylog.log' + expect(level).to eq 0 + end + described_class.start(args) + end + end + + context 'gc options' do + + context 'when gc options are not present' do + let(:test_args) { [] } + + it 'sets both request and serialization pausing to false' do + described_class.start(args) + expect(::Protobuf).to_not be_gc_pause_server_request + end + end + + unless defined?(JRUBY_VERSION) + context 'request pausing' do + let(:test_args) { ['--gc_pause_request'] } + + it 'sets the configuration option to GC pause server request' do + described_class.start(args) + expect(::Protobuf).to be_gc_pause_server_request + end + end + end + end + + context 'deprecation options' do + context 'when not given' do + let(:test_args) { [] } + + context 'when no ENV is present and no command line option' do + before { ENV.delete("PB_IGNORE_DEPRECATIONS") } + + it 'sets the deprecation warning flag to true' do + described_class.start(args) + expect(::Protobuf.print_deprecation_warnings?).to be true + end + end + + context 'if ENV["PB_IGNORE_DEPRECATIONS"] is present' do + before { ENV["PB_IGNORE_DEPRECATIONS"] = "1" } + after { ENV.delete("PB_IGNORE_DEPRECATIONS") } + + it 'sets the deprecation warning flag to false ' do + described_class.start(args) + expect(::Protobuf.print_deprecation_warnings?).to be false + end + end + end + + context 'when enabled' do + let(:test_args) { ['--print_deprecation_warnings'] } + + it 'sets the deprecation warning flag to true' do + described_class.start(args) + expect(::Protobuf.print_deprecation_warnings?).to be true + end + end + + context 'when disabled' do + let(:test_args) { ['--no-print_deprecation_warnings'] } + + it 'sets the deprecation warning flag to false' do + described_class.start(args) + expect(::Protobuf.print_deprecation_warnings?).to be false + end + end + end + + context 'run modes' do + + context "extension" do + let(:runner) { ::Protobuf::Rpc::Servers::SocketRunner } + + it "loads the runner specified by PB_SERVER_TYPE" do + ENV['PB_SERVER_TYPE'] = "protobuf/rpc/servers/socket_runner" + expect(runner).to receive(:new).and_return(sock_runner) + described_class.start(args) + ENV.delete('PB_SERVER_TYPE') + end + + context "without extension loaded" do + it "will throw a LoadError when extension is not loaded" do + ENV['PB_SERVER_TYPE'] = "socket_to_load" + expect { described_class.start(args) }.to raise_error(LoadError, /socket_to_load/) + ENV.delete("PB_SERVER_TYPE") + end + end + end + + context 'socket' do + let(:test_args) { ['--socket'] } + let(:runner) { ::Protobuf::Rpc::SocketRunner } + + before do + expect(::Protobuf::Rpc::ZmqRunner).not_to receive(:new) + end + + it 'is activated by the --socket switch' do + expect(runner).to receive(:new) + described_class.start(args) + end + + it 'is activated by PB_SERVER_TYPE=Socket ENV variable' do + ENV['PB_SERVER_TYPE'] = "Socket" + expect(runner).to receive(:new).and_return(sock_runner) + described_class.start(args) + ENV.delete('PB_SERVER_TYPE') + end + end + + context 'zmq workers only' do + let(:test_args) { ['--workers_only', '--zmq'] } + let(:runner) { ::Protobuf::Rpc::ZmqRunner } + + before do + expect(::Protobuf::Rpc::SocketRunner).not_to receive(:new) + end + + it 'is activated by the --workers_only switch' do + expect(runner).to receive(:new) do |options| + expect(options[:workers_only]).to be true + end.and_return(zmq_runner) + + described_class.start(args) + end + + it 'is activated by PB_WORKERS_ONLY=1 ENV variable' do + ENV['PB_WORKERS_ONLY'] = "1" + expect(runner).to receive(:new) do |options| + expect(options[:workers_only]).to be true + end.and_return(zmq_runner) + + described_class.start(args) + ENV.delete('PB_WORKERS_ONLY') + end + end + + context 'zmq worker port' do + let(:test_args) { ['--worker_port=1234', '--zmq'] } + let(:runner) { ::Protobuf::Rpc::ZmqRunner } + + before do + expect(::Protobuf::Rpc::SocketRunner).not_to receive(:new) + end + + it 'is activated by the --worker_port switch' do + expect(runner).to receive(:new) do |options| + expect(options[:worker_port]).to eq(1234) + end.and_return(zmq_runner) + + described_class.start(args) + end + end + + context 'zmq' do + let(:test_args) { ['--zmq'] } + let(:runner) { ::Protobuf::Rpc::ZmqRunner } + + before do + expect(::Protobuf::Rpc::SocketRunner).not_to receive(:new) + end + + it 'is activated by the --zmq switch' do + expect(runner).to receive(:new) + described_class.start(args) + end + + it 'is activated by PB_SERVER_TYPE=Zmq ENV variable' do + ENV['PB_SERVER_TYPE'] = "Zmq" + expect(runner).to receive(:new) + described_class.start(args) + ENV.delete('PB_SERVER_TYPE') + end + end + + context 'after server bind' do + let(:sock_runner) { double("FakeRunner", :run => nil) } + + before { allow(sock_runner).to receive(:run).and_yield } + + it 'publishes when using the lib/protobuf callback' do + message_after_bind = false + ::Protobuf.after_server_bind do + message_after_bind = true + end + described_class.start(args) + expect(message_after_bind).to eq(true) + end + end + + context 'before server bind' do + it 'publishes a message before the runner runs' do + message_before_bind = false + ::ActiveSupport::Notifications.subscribe('before_server_bind') do + message_before_bind = true + end + described_class.start(args) + expect(message_before_bind).to eq(true) + end + + it 'publishes when using the lib/protobuf callback' do + message_before_bind = false + ::Protobuf.before_server_bind do + message_before_bind = true + end + described_class.start(args) + expect(message_before_bind).to eq(true) + end + end + + end + + end + +end diff --git a/spec/lib/protobuf/code_generator_spec.rb b/spec/lib/protobuf/code_generator_spec.rb new file mode 100644 index 00000000..44b8dd16 --- /dev/null +++ b/spec/lib/protobuf/code_generator_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +require 'protobuf/code_generator' + +RSpec.describe ::Protobuf::CodeGenerator do + + # Some constants to shorten things up + DESCRIPTOR = ::Google::Protobuf + COMPILER = ::Google::Protobuf::Compiler + + describe '#response_bytes' do + let(:input_file1) { DESCRIPTOR::FileDescriptorProto.new(:name => 'test/foo.proto') } + let(:input_file2) { DESCRIPTOR::FileDescriptorProto.new(:name => 'test/bar.proto') } + + let(:output_file1) { COMPILER::CodeGeneratorResponse::File.new(:name => 'test/foo.pb.rb') } + let(:output_file2) { COMPILER::CodeGeneratorResponse::File.new(:name => 'test/bar.pb.rb') } + + let(:file_generator1) { double('file generator 1', :generate_output_file => output_file1) } + let(:file_generator2) { double('file generator 2', :generate_output_file => output_file2) } + + let(:request_bytes) do + COMPILER::CodeGeneratorRequest.encode(:proto_file => [input_file1, input_file2]) + end + + let(:expected_response_bytes) do + COMPILER::CodeGeneratorResponse.encode(:file => [output_file1, output_file2], :supported_features => 1) + end + + before do + expect(::Protobuf::Generators::FileGenerator).to receive(:new).with(input_file1).and_return(file_generator1) + expect(::Protobuf::Generators::FileGenerator).to receive(:new).with(input_file2).and_return(file_generator2) + end + + it 'returns the serialized CodeGeneratorResponse which contains the generated file contents' do + generator = described_class.new(request_bytes) + expect(generator.response_bytes).to eq expected_response_bytes + end + end + + describe '#eval_unknown_extensions' do + let(:input_file) do + DESCRIPTOR::FileDescriptorProto.new( + :name => 'test/boom.proto', + :package => 'test.pkg.code_generator_spec', + :extension => [{ + :name => 'boom', + :number => 20100, + :label => Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL, + :type => Google::Protobuf::FieldDescriptorProto::Type::TYPE_STRING, + :extendee => '.google.protobuf.FieldOptions', + }], + ) + end + let(:request_bytes) { COMPILER::CodeGeneratorRequest.encode(:proto_file => [input_file]) } + + it 'evals files as they are generated' do + described_class.new(request_bytes).eval_unknown_extensions! + expect(Google::Protobuf::FieldOptions.extension_fields.map(&:fully_qualified_name)).to include(:'.test.pkg.code_generator_spec.boom') + expect(Google::Protobuf::FieldOptions.extension_fields.map(&:name)).to include(:boom) + added_extension = Google::Protobuf::FieldOptions.extension_fields.detect { |f| f.fully_qualified_name == :'.test.pkg.code_generator_spec.boom' } + expect(added_extension.name).to eq(:boom) + expect(added_extension.rule).to eq(:optional) + expect(added_extension.type_class).to eq(::Protobuf::Field::StringField) + expect(added_extension.tag).to eq(20100) + end + end + + context 'class-level printing methods' do + describe '.fatal' do + it 'raises a CodeGeneratorFatalError error' do + expect do + described_class.fatal("something is wrong") + end.to raise_error( + ::Protobuf::CodeGenerator::CodeGeneratorFatalError, + "something is wrong", + ) + end + end + + describe '.warn' do + it 'prints a warning to stderr' do + expect(STDERR).to receive(:puts).with("[WARN] a warning") + described_class.warn("a warning") + end + end + end +end diff --git a/spec/lib/protobuf/enum_spec.rb b/spec/lib/protobuf/enum_spec.rb new file mode 100644 index 00000000..f0ace1d6 --- /dev/null +++ b/spec/lib/protobuf/enum_spec.rb @@ -0,0 +1,307 @@ +require 'spec_helper' +require PROTOS_PATH.join('enum.pb') + +RSpec.describe Protobuf::Enum do + + describe 'class dsl' do + let(:name) { :THREE } + let(:tag) { 3 } + + before(:all) do + Test::EnumTestType.define(:MINUS_ONE, -1) + Test::EnumTestType.define(:THREE, 3) + end + + before(:all) do + EnumAliasTest = ::Class.new(::Protobuf::Enum) do + set_option :allow_alias + define :FOO, 1 + define :BAR, 1 + define :BAZ, 2 + end + end + + describe '.==' do + it 'is true for identical values' do + expect(Test::EnumTestType::THREE).to eq(Test::EnumTestType::THREE) + end + + it 'is false for different values in the same enum' do + expect(Test::EnumTestType::TWO).to_not eq(Test::EnumTestType::THREE) + end + + it 'is false for values from different enums' do + expect(Test::EnumTestType::THREE).to_not eq(Test::AliasedEnum::THREE) + end + end + + describe '.aliases_allowed?' do + it 'is false when the option is not set' do + expect(Test::EnumTestType.aliases_allowed?).to be false + end + end + + describe '.define' do + it 'defines a constant enum on the parent class' do + expect(Test::EnumTestType.constants).to include(name) + expect(Test::EnumTestType::THREE).to be_a(Protobuf::Enum) + end + + context 'when enum allows aliases' do + before(:all) do + DefineEnumAlias = ::Class.new(::Protobuf::Enum) do + set_option :allow_alias + end + end + + it 'allows defining enums with the same tag number' do + expect do + DefineEnumAlias.define(:FOO, 1) + DefineEnumAlias.define(:BAR, 1) + end.not_to raise_error + end + end + end + + describe '.enums' do + it 'provides an array of defined Enums' do + expect(Test::EnumTestType.enums).to eq( + [ + Test::EnumTestType::ZERO, + Test::EnumTestType::ONE, + Test::EnumTestType::TWO, + Test::EnumTestType::MINUS_ONE, + Test::EnumTestType::THREE, + ], + ) + end + + context 'when enum allows aliases' do + it 'treats aliased enums as valid' do + expect(EnumAliasTest.enums).to eq( + [ + EnumAliasTest::FOO, + EnumAliasTest::BAR, + EnumAliasTest::BAZ, + ], + ) + end + end + end + + describe '.enums_for_tag' do + it 'returns an array of Enums for the given tag, if any' do + expect(EnumAliasTest.enums_for_tag(nil)).to eq([]) + expect(EnumAliasTest.enums_for_tag(1)).to eq([EnumAliasTest::FOO, EnumAliasTest::BAR]) + expect(EnumAliasTest.enums_for_tag(2)).to eq([EnumAliasTest::BAZ]) + expect(EnumAliasTest.enums_for_tag(3)).to eq([]) + end + end + + describe '.fetch' do + context 'when candidate is an Enum' do + it 'responds with the Enum' do + expect(Test::EnumTestType.fetch(Test::EnumTestType::THREE)).to eq(Test::EnumTestType::THREE) + end + end + + context 'when candidate can be coerced to a symbol' do + it 'fetches based on the symbol name' do + expect(Test::EnumTestType.fetch("http://github.com/ONE")).to eq(Test::EnumTestType::ONE) + expect(Test::EnumTestType.fetch(:ONE)).to eq(Test::EnumTestType::ONE) + end + end + + context 'when candidate can be coerced to an integer' do + it 'fetches based on the integer tag' do + expect(Test::EnumTestType.fetch(3.0)).to eq(Test::EnumTestType::THREE) + expect(Test::EnumTestType.fetch(3)).to eq(Test::EnumTestType::THREE) + end + + context 'when enum allows aliases' do + it 'fetches the first defined Enum' do + expect(EnumAliasTest.fetch(1)).to eq(EnumAliasTest::FOO) + end + end + end + + context 'when candidate is not an applicable type' do + it 'returns a nil' do + expect(Test::EnumTestType.fetch(EnumAliasTest::FOO)).to be_nil + expect(Test::EnumTestType.fetch(Test::Resource.new)).to be_nil + expect(Test::EnumTestType.fetch(nil)).to be_nil + expect(Test::EnumTestType.fetch(false)).to be_nil + expect(Test::EnumTestType.fetch(-10)).to be_nil + end + end + end + + describe '.enum_for_tag' do + it 'gets the Enum corresponding to the given tag' do + expect(Test::EnumTestType.enum_for_tag(tag)).to eq(Test::EnumTestType.const_get(name)) + expect(Test::EnumTestType.enum_for_tag(-5)).to be_nil + expect(Test::EnumTestType.enum_for_tag(nil)).to be_nil + end + end + + describe '.name_for_tag' do + it 'get the name of the enum given the enum' do + expect(Test::EnumTestType.name_for_tag(::Test::EnumTestType::THREE)).to eq(name) + end + + it 'gets the name of the enum corresponding to the given tag' do + expect(Test::EnumTestType.name_for_tag(tag)).to eq(name) + end + + it 'gets the name when the tag is coercable to an int' do + expect(Test::EnumTestType.name_for_tag("3")).to eq(name) + end + + it 'returns nil when tag does not correspond to a name' do + expect(Test::EnumTestType.name_for_tag(12345)).to be_nil + end + + context 'when given name is nil' do + it 'returns a nil' do + expect(Test::EnumTestType.name_for_tag(nil)).to be_nil + end + end + + context 'when enum allows aliases' do + it 'returns the first defined name for the given tag' do + expect(EnumAliasTest.name_for_tag(1)).to eq(:FOO) + end + end + end + + describe '.to_json' do + it 'renders the enum value' do + expect(Test::EnumTestType::ONE.to_json).to eq("1") + expect({ :value => Test::EnumTestType::ONE }.to_json).to eq(%({"value":1})) + # JSON.dump passes arguments to the to_json method which broke in the 3.8.3 release. + expect(JSON.dump(:value => Test::EnumTestType::ONE)).to eq(%({"value":1})) + end + end + + describe '.valid_tag?' do + context 'when tag is defined' do + specify { expect(Test::EnumTestType.valid_tag?(tag)).to be true } + end + + context 'when tag is not defined' do + specify { expect(Test::EnumTestType.valid_tag?(300)).to be false } + end + + context 'is true for aliased enums' do + specify { expect(EnumAliasTest.valid_tag?(1)).to be true } + end + end + + describe '.enum_for_name' do + it 'gets the Enum corresponding to the given name' do + expect(Test::EnumTestType.enum_for_name(name)).to eq(Test::EnumTestType::THREE) + end + end + + describe '.values' do + around do |example| + # this method is deprecated + ::Protobuf.deprecator.silence(&example) + end + + it 'provides a hash of defined Enums' do + expect(Test::EnumTestType.values).to eq( + :MINUS_ONE => Test::EnumTestType::MINUS_ONE, + :ZERO => Test::EnumTestType::ZERO, + :ONE => Test::EnumTestType::ONE, + :TWO => Test::EnumTestType::TWO, + :THREE => Test::EnumTestType::THREE, + ) + end + + it 'contains aliased Enums' do + expect(EnumAliasTest.values).to eq( + :FOO => EnumAliasTest::FOO, + :BAR => EnumAliasTest::BAR, + :BAZ => EnumAliasTest::BAZ, + ) + end + end + + describe '.all_tags' do + it 'provides a unique array of defined tags' do + expect(Test::EnumTestType.all_tags).to include(1, 2, -1, 3) + expect(EnumAliasTest.all_tags).to include(1, 2) + end + end + end + + subject { Test::EnumTestType::ONE } + specify { expect(subject.class).to eq(1.class) } + specify { expect(subject.parent_class).to eq(Test::EnumTestType) } + specify { expect(subject.name).to eq(:ONE) } + specify { expect(subject.tag).to eq(1) } + + context 'deprecated' do + around do |example| + # this method is deprecated + ::Protobuf.deprecator.silence(&example) + end + + specify { expect(subject.value).to eq(1) } + end + + specify { expect(subject.to_hash_value).to eq(1) } + specify { expect(subject.to_s).to eq("1") } + specify { expect(subject.inspect).to eq('#') } + specify { expect(subject.to_s(:tag)).to eq("1") } + specify { expect(subject.to_s(:name)).to eq("ONE") } + + it "can be used as the index to an array" do + array = [0, 1, 2, 3] + expect(array[::Test::EnumTestType::ONE]).to eq(1) + end + + describe '#try' do + specify { expect(subject.try(:parent_class)).to eq(subject.parent_class) } + specify { expect(subject.try(:class)).to eq(subject.class) } + specify { expect(subject.try(:name)).to eq(subject.name) } + specify { expect(subject.try(:tag)).to eq(subject.tag) } + + context 'deprecated' do + around do |example| + # this method is deprecated + ::Protobuf.deprecator.silence(&example) + end + + specify { expect(subject.try(:value)).to eq(subject.value) } + end + + specify { expect(subject.try(:to_i)).to eq(subject.to_i) } + specify { expect(subject.try(:to_int)).to eq(subject.to_int) } + specify { subject.try { |yielded| expect(yielded).to eq(subject) } } + end + + describe '#eql?' do + it "is equal to itself" do + expect(::Test::EnumTestType::ZERO.eql?(::Test::EnumTestType::ZERO)).to be(true) + end + + it "is equal to it's tag" do + expect(::Test::EnumTestType::ZERO.eql?(::Test::EnumTestType::ZERO.tag)).to be(true) + end + + it "is not equal to it's name" do + expect(::Test::EnumTestType::ZERO.eql?(::Test::EnumTestType::ZERO.name)).to be(false) + end + end + + context 'when coercing from enum' do + subject { Test::StatusType::PENDING } + it { is_expected.to eq(0) } + end + + context 'when coercing from integer' do + specify { expect(0).to eq(Test::StatusType::PENDING) } + end +end diff --git a/spec/lib/protobuf/field/bool_field_spec.rb b/spec/lib/protobuf/field/bool_field_spec.rb new file mode 100644 index 00000000..abe25369 --- /dev/null +++ b/spec/lib/protobuf/field/bool_field_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::BoolField do + + it_behaves_like :packable_field, described_class do + let(:value) { [true, false] } + end + + class SomeBoolMessage < ::Protobuf::Message + optional :bool, :some_bool, 1 + required :bool, :required_bool, 2 + end + + let(:instance) { SomeBoolMessage.new } + + describe 'setting and getting field' do + subject { instance.some_bool = value; instance.some_bool } + + [true, false].each do |val| + context "when set with #{val}" do + let(:value) { val } + + it 'is readable as a bool' do + expect(subject).to eq(val) + end + end + end + + [['true', true], ['false', false]].each do |val, expected| + context "when set with a string of #{val}" do + let(:value) { val } + + it 'is readable as a bool' do + expect(subject).to eq(expected) + end + end + end + + context 'when set with a non-bool string' do + let(:value) { "aaaa" } + + it 'throws an error' do + expect { subject }.to raise_error(TypeError) + end + end + + context 'when set with something that is not a bool' do + let(:value) { [1, 2, 3] } + + it 'throws an error' do + expect { subject }.to raise_error(TypeError) + end + end + end + + it 'defines ? method' do + instance.required_bool = false + expect(instance.required_bool?).to be(false) + end + + describe '#default_value' do + context 'optional and required fields' do + it 'returns the class default' do + expect(SomeBoolMessage.get_field('some_bool').default).to be nil + expect(::Protobuf::Field::BoolField.default).to be false + expect(instance.some_bool).to be false + end + + context 'with field default' do + class AnotherBoolMessage < ::Protobuf::Message + optional :bool, :set_bool, 1, :default => true + end + + it 'returns the set default' do + expect(AnotherBoolMessage.get_field('set_bool').default).to be true + expect(AnotherBoolMessage.new.set_bool).to be true + end + end + end + + context 'repeated field' do + class RepeatedBoolMessage < ::Protobuf::Message + repeated :bool, :repeated_bool, 1 + end + + it 'returns the set default' do + expect(RepeatedBoolMessage.new.repeated_bool).to eq [] + end + end + end +end diff --git a/spec/lib/protobuf/field/double_field_spec.rb b/spec/lib/protobuf/field/double_field_spec.rb new file mode 100644 index 00000000..e6ae9fd1 --- /dev/null +++ b/spec/lib/protobuf/field/double_field_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::DoubleField do + + it_behaves_like :packable_field, described_class do + let(:value) { [1.0, 2.0, 3.0] } + end + +end diff --git a/spec/lib/protobuf/field/enum_field_spec.rb b/spec/lib/protobuf/field/enum_field_spec.rb new file mode 100644 index 00000000..cd72760d --- /dev/null +++ b/spec/lib/protobuf/field/enum_field_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::EnumField do + let(:message) do + Class.new(::Protobuf::Message) do + enum_class = Class.new(::Protobuf::Enum) do + define :POSITIVE, 22 + define :NEGATIVE, -33 + end + + optional enum_class, :enum, 1 + end + end + + describe '.{encode, decode}' do + it 'handles positive enum constants' do + instance = message.new(:enum => :POSITIVE) + expect(message.decode(instance.encode).enum).to eq(22) + end + + it 'handles negative enum constants' do + instance = message.new(:enum => :NEGATIVE) + expect(message.decode(instance.encode).enum).to eq(-33) + end + end +end diff --git a/spec/lib/protobuf/field/field_array_spec.rb b/spec/lib/protobuf/field/field_array_spec.rb new file mode 100644 index 00000000..0c93b65e --- /dev/null +++ b/spec/lib/protobuf/field/field_array_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::FieldArray do + + let(:basic_message) do + Class.new(::Protobuf::Message) do + optional :string, :field, 1 + end + end + + let(:more_complex_message) do + Class.new(BasicMessage) do + end + end + + let(:some_enum) do + Class.new(::Protobuf::Enum) do + define :FOO, 1 + define :BAR, 2 + define :BAZ, 3 + end + end + + let(:repeat_message) do + Class.new(::Protobuf::Message) do + optional :string, :some_string, 1 + repeated :string, :multiple_strings, 2 + repeated BasicMessage, :multiple_basic_msgs, 3 + repeated SomeEnum, :multiple_enums, 4 + end + end + + before do + stub_const('BasicMessage', basic_message) + stub_const('MoreComplexMessage', more_complex_message) + stub_const('SomeEnum', some_enum) + stub_const('SomeRepeatMessage', repeat_message) + end + + let(:instance) { SomeRepeatMessage.new } + + %w(<< push).each do |method| + describe "\##{method}" do + context 'when applied to a string field array' do + it 'adds a string' do + expect(instance.multiple_strings).to be_empty + instance.multiple_strings.send(method, 'string 1') + expect(instance.multiple_strings).to eq(['string 1']) + instance.multiple_strings.send(method, 'string 2') + expect(instance.multiple_strings).to eq(['string 1', 'string 2']) + end + + it 'fails if not adding a string' do + expect { instance.multiple_strings.send(method, 100.0) }.to raise_error(TypeError) + end + end + + context 'when applied to a MessageField field array' do + it 'adds a MessageField object' do + expect(instance.multiple_basic_msgs).to be_empty + basic_msg1 = BasicMessage.new + instance.multiple_basic_msgs.send(method, basic_msg1) + expect(instance.multiple_basic_msgs).to eq([basic_msg1]) + basic_msg2 = BasicMessage.new + instance.multiple_basic_msgs.send(method, basic_msg2) + expect(instance.multiple_basic_msgs).to eq([basic_msg1, basic_msg2]) + end + + it 'fails if not adding a MessageField' do + expect { instance.multiple_basic_msgs.send(method, 100.0) }.to raise_error(TypeError) + end + + it 'adds a Hash from a MessageField object' do + expect(instance.multiple_basic_msgs).to be_empty + basic_msg1 = BasicMessage.new + basic_msg1.field = 'my value' + instance.multiple_basic_msgs.send(method, basic_msg1.to_hash) + expect(instance.multiple_basic_msgs).to eq([basic_msg1]) + end + + it 'does not downcast a MessageField' do + expect(instance.multiple_basic_msgs).to be_empty + basic_msg1 = MoreComplexMessage.new + instance.multiple_basic_msgs.send(method, basic_msg1) + expect(instance.multiple_basic_msgs).to eq([basic_msg1]) + expect(instance.multiple_basic_msgs.first).to be_a(MoreComplexMessage) + end + end + + context 'when applied to an EnumField field array' do + it 'adds an EnumField object' do + expect(instance.multiple_enums).to be_empty + instance.multiple_enums.send(method, SomeEnum::FOO) + expect(instance.multiple_enums).to eq([SomeEnum::FOO]) + instance.multiple_enums.send(method, SomeEnum::BAR) + expect(instance.multiple_enums).to eq([SomeEnum::FOO, SomeEnum::BAR]) + end + + it 'fails if not adding an EnumField' do + expect { instance.multiple_basic_msgs.send(method, 100.0) }.to raise_error(TypeError) + end + end + end + end +end diff --git a/spec/lib/protobuf/field/field_hash_spec.rb b/spec/lib/protobuf/field/field_hash_spec.rb new file mode 100644 index 00000000..2b328755 --- /dev/null +++ b/spec/lib/protobuf/field/field_hash_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::FieldHash do + + let(:basic_message) do + Class.new(::Protobuf::Message) do + optional :string, :field, 1 + end + end + + let(:more_complex_message) do + Class.new(BasicMessage) do + end + end + + let(:some_enum) do + Class.new(::Protobuf::Enum) do + define :FOO, 1 + define :BAR, 2 + define :BAZ, 3 + end + end + + let(:map_message) do + Class.new(::Protobuf::Message) do + optional :string, :some_string, 1 + map :int32, :string, :map_int32_to_string, 2 + map :string, BasicMessage, :map_string_to_msg, 3 + map :string, SomeEnum, :map_string_to_enum, 4 + end + end + + before do + stub_const('BasicMessage', basic_message) + stub_const('MoreComplexMessage', more_complex_message) + stub_const('SomeEnum', some_enum) + stub_const('SomeMapMessage', map_message) + end + + let(:instance) { SomeMapMessage.new } + + %w([]= store).each do |method| + describe "##{method}" do + context 'when applied to an int32->string field hash' do + it 'adds an int -> string entry' do + expect(instance.map_int32_to_string).to be_empty + instance.map_int32_to_string.send(method, 1, 'string 1') + expect(instance.map_int32_to_string).to eq(1 => 'string 1') + instance.map_int32_to_string.send(method, 2, 'string 2') + expect(instance.map_int32_to_string).to eq(1 => 'string 1', 2 => 'string 2') + end + + it 'fails if not adding an int -> string' do + expect { instance.map_int32_to_string.send(method, 1, 100.0) }.to raise_error(TypeError) + expect { instance.map_int32_to_string.send(method, 'foo', 100.0) }.to raise_error(TypeError) + expect { instance.map_int32_to_string.send(method, BasicMessage.new, 100.0) }.to raise_error(TypeError) + expect { instance.map_int32_to_string.send(method, 'foo', 'bar') }.to raise_error(TypeError) + expect { instance.map_int32_to_string.send(method, nil, 'foo') }.to raise_error(TypeError) + expect { instance.map_int32_to_string.send(method, 1, nil) }.to raise_error(TypeError) + end + end + + context 'when applied to a string->MessageField field hash' do + it 'adds a string -> MessageField entry' do + expect(instance.map_string_to_msg).to be_empty + basic_msg1 = BasicMessage.new + instance.map_string_to_msg.send(method, 'msg1', basic_msg1) + expect(instance.map_string_to_msg).to eq('msg1' => basic_msg1) + basic_msg2 = BasicMessage.new + instance.map_string_to_msg.send(method, 'msg2', basic_msg2) + expect(instance.map_string_to_msg).to eq('msg1' => basic_msg1, 'msg2' => basic_msg2) + end + + it 'fails if not adding a string -> MessageField entry' do + expect { instance.map_string_to_msg.send(method, 1, 100.0) }.to raise_error(TypeError) + expect { instance.map_string_to_msg.send(method, 'foo', SomeEnum::FOO) }.to raise_error(TypeError) + expect { instance.map_string_to_msg.send(method, SomeEnum::FOO, BasicMessage.new) }.to raise_error(TypeError) + expect { instance.map_string_to_msg.send(method, nil, BasicMessage.new) }.to raise_error(TypeError) + expect { instance.map_string_to_msg.send(method, 'foo', nil) }.to raise_error(TypeError) + end + + it 'adds a Hash from a MessageField object' do + expect(instance.map_string_to_msg).to be_empty + basic_msg1 = BasicMessage.new + basic_msg1.field = 'my value' + instance.map_string_to_msg.send(method, 'foo', basic_msg1.to_hash) + expect(instance.map_string_to_msg).to eq('foo' => basic_msg1) + end + + it 'does not downcast a MessageField' do + expect(instance.map_string_to_msg).to be_empty + basic_msg1 = MoreComplexMessage.new + instance.map_string_to_msg.send(method, 'foo', basic_msg1) + expect(instance.map_string_to_msg).to eq('foo' => basic_msg1) + expect(instance.map_string_to_msg['foo']).to be_a(MoreComplexMessage) + end + end + + context 'when applied to a string->EnumField field hash' do + it 'adds a string -> EnumField entry' do + expect(instance.map_string_to_enum).to be_empty + instance.map_string_to_enum.send(method, 'msg1', SomeEnum::FOO) + expect(instance.map_string_to_enum).to eq('msg1' => SomeEnum::FOO) + instance.map_string_to_enum.send(method, 'msg2', SomeEnum::BAR) + expect(instance.map_string_to_enum).to eq('msg1' => SomeEnum::FOO, 'msg2' => SomeEnum::BAR) + end + + it 'fails if not adding a string -> EnumField entry' do + expect { instance.map_string_to_enum.send(method, 1, 100.0) }.to raise_error(TypeError) + expect { instance.map_string_to_enum.send(method, nil, 100.0) }.to raise_error(TypeError) + expect { instance.map_string_to_enum.send(method, 1, nil) }.to raise_error(TypeError) + expect { instance.map_string_to_enum.send(method, 'foo', BasicMessage.new) }.to raise_error(TypeError) + expect { instance.map_string_to_enum.send(method, 1, SomeEnum::FOO) }.to raise_error(TypeError) + expect { instance.map_string_to_enum.send(method, nil, SomeEnum::FOO) }.to raise_error(TypeError) + expect { instance.map_string_to_enum.send(method, 'foo', nil) }.to raise_error(TypeError) + end + end + end + + describe '#to_hash_value' do + context 'when applied to an int32->string field hash' do + before do + instance.map_int32_to_string[1] = 'string 1' + instance.map_int32_to_string[2] = 'string 2' + end + + it 'converts properly' do + expect(instance.to_hash_value).to eq(:map_int32_to_string => { + 1 => 'string 1', + 2 => 'string 2', + }) + end + end + + context 'when applied to a string->MessageField field hash' do + before do + instance.map_string_to_msg['msg1'] = BasicMessage.new(:field => 'string 1') + instance.map_string_to_msg['msg2'] = BasicMessage.new(:field => 'string 2') + end + + it 'converts properly' do + expect(instance.to_hash_value).to eq(:map_string_to_msg => { + 'msg1' => { + :field => 'string 1', + }, + 'msg2' => { + :field => 'string 2', + }, + }) + end + end + + context 'when applied to a string->EnumField field hash' do + before do + instance.map_string_to_enum['msg1'] = SomeEnum::FOO + instance.map_string_to_enum['msg2'] = SomeEnum::BAR + end + + it 'converts properly' do + expect(instance.to_hash_value).to eq(:map_string_to_enum => { + 'msg1' => 1, + 'msg2' => 2, + }) + end + end + end + end +end diff --git a/spec/lib/protobuf/field/fixed32_field_spec.rb b/spec/lib/protobuf/field/fixed32_field_spec.rb new file mode 100644 index 00000000..751b0bdb --- /dev/null +++ b/spec/lib/protobuf/field/fixed32_field_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Fixed32Field do + + it_behaves_like :packable_field, described_class + +end diff --git a/spec/lib/protobuf/field/fixed64_field_spec.rb b/spec/lib/protobuf/field/fixed64_field_spec.rb new file mode 100644 index 00000000..d7feb120 --- /dev/null +++ b/spec/lib/protobuf/field/fixed64_field_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Fixed64Field do + + it_behaves_like :packable_field, described_class + +end diff --git a/spec/lib/protobuf/field/float_field_spec.rb b/spec/lib/protobuf/field/float_field_spec.rb new file mode 100644 index 00000000..818f0954 --- /dev/null +++ b/spec/lib/protobuf/field/float_field_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::FloatField do + + it_behaves_like :packable_field, described_class do + let(:value) { [1.0, 2.0, 3.0] } + end + + class SomeFloatMessage < ::Protobuf::Message + optional :float, :some_float, 1 + end + + let(:instance) { SomeFloatMessage.new } + + describe 'setting and getting field' do + subject { instance.some_float = value; instance.some_float } + + context 'when set with an int' do + let(:value) { 100 } + + it 'is readable as a float' do + expect(subject).to eq(100.0) + end + end + + context 'when set with a float' do + let(:value) { 100.1 } + + it 'is readable as a float' do + expect(subject).to eq(100.1) + end + end + + context 'when set with a string of a float' do + let(:value) { "101.1" } + + it 'is readable as a float' do + expect(subject).to eq(101.1) + end + end + + context 'when set with a non-numeric string' do + let(:value) { "aaaa" } + + it 'throws an error' do + expect { subject }.to raise_error(TypeError) + end + end + + context 'when set with something that is not a float' do + let(:value) { [1, 2, 3] } + + it 'throws an error' do + expect { subject }.to raise_error(TypeError) + end + end + end + + describe '#default_value' do + context 'optional and required fields' do + it 'returns the class default' do + expect(SomeFloatMessage.get_field('some_float').default).to be nil + expect(::Protobuf::Field::FloatField.default).to eq 0.0 + expect(instance.some_float).to eq 0.0 + end + + context 'with field default' do + class AnotherFloatMessage < ::Protobuf::Message + optional :float, :set_float, 1, :default => 3.6 + end + + it 'returns the set default' do + expect(AnotherFloatMessage.get_field('set_float').default).to eq 3.6 + expect(AnotherFloatMessage.new.set_float).to eq 3.6 + end + end + end + + context 'repeated field' do + class RepeatedFloatMessage < ::Protobuf::Message + repeated :float, :repeated_float, 1 + end + + it 'returns the set default' do + expect(RepeatedFloatMessage.new.repeated_float).to eq [] + end + end + end + +end diff --git a/spec/lib/protobuf/field/int32_field_spec.rb b/spec/lib/protobuf/field/int32_field_spec.rb new file mode 100644 index 00000000..d851e019 --- /dev/null +++ b/spec/lib/protobuf/field/int32_field_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Int32Field do + + it_behaves_like :packable_field, described_class + + class SomeInt32Message < ::Protobuf::Message + optional :int32, :some_int, 1 + end + + let(:instance) { SomeInt32Message.new } + + describe 'setting and getting a field' do + subject { instance.some_int = value; instance.some_int } + + context 'when set with an int' do + let(:value) { 100 } + + it 'is readable as an int' do + expect(subject).to eq(100) + end + end + + context 'when set with a float' do + let(:value) { 100.1 } + + it 'is readable as an int' do + expect(subject).to eq(100) + end + end + + context 'when set with a string of an int' do + let(:value) { "101" } + + it 'is readable as an int' do + expect(subject).to eq(101) + end + end + + context 'when set with a negative representation of an int as string' do + let(:value) { "-101" } + + it 'is readable as a negative int' do + expect(subject).to eq(-101) + end + end + + context 'when set with a non-numeric string' do + let(:value) { "aaaa" } + + it 'throws an error' do + expect { subject }.to raise_error(TypeError) + end + end + + context 'when set with a string of an int in hex format' do + let(:value) { "0x101" } + + it 'throws an error' do + expect { subject }.to raise_error(TypeError) + end + end + + context 'when set with a string of an int larger than int32 max' do + let(:value) { (described_class.max + 1).to_s } + + it 'throws an error' do + expect { subject }.to raise_error(TypeError) + end + end + + context 'when set with something that is not an int' do + let(:value) { [1, 2, 3] } + + it 'throws an error' do + expect { subject }.to raise_error(TypeError) + end + end + + context 'when set with a timestamp' do + let(:value) { Time.now } + + it 'throws an error' do + expect { subject }.to raise_error(TypeError) + end + end + end + + describe '#default_value' do + context 'optional and required fields' do + it 'returns the class default' do + expect(SomeInt32Message.get_field('some_int').default).to be nil + expect(::Protobuf::Field::Int32Field.default).to eq 0 + expect(instance.some_int).to eq 0 + end + + context 'with field default' do + class AnotherIntMessage < ::Protobuf::Message + optional :int32, :set_int, 1, :default => 3 + end + + it 'returns the set default' do + expect(AnotherIntMessage.get_field('set_int').default).to eq 3 + expect(AnotherIntMessage.new.set_int).to eq 3 + end + end + end + + context 'repeated field' do + class RepeatedIntMessage < ::Protobuf::Message + repeated :int32, :repeated_int, 1 + end + + it 'returns the set default' do + expect(RepeatedIntMessage.new.repeated_int).to eq [] + end + end + end + +end diff --git a/spec/lib/protobuf/field/int64_field_spec.rb b/spec/lib/protobuf/field/int64_field_spec.rb new file mode 100644 index 00000000..1bbe7c04 --- /dev/null +++ b/spec/lib/protobuf/field/int64_field_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Int64Field do + + it_behaves_like :packable_field, described_class + +end diff --git a/spec/lib/protobuf/field/message_field_spec.rb b/spec/lib/protobuf/field/message_field_spec.rb new file mode 100644 index 00000000..cfc4f8b4 --- /dev/null +++ b/spec/lib/protobuf/field/message_field_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::MessageField do + let(:inner_message) do + Class.new(::Protobuf::Message) do + optional :int32, :field, 0 + optional :int32, :field2, 1 + end + end + + let(:field_message) do + Class.new(::Protobuf::Message) do + optional :int32, :field, 1 + repeated :int64, :repeated_field, 2 + optional InnerMessage, :message_field, 3 + end + end + + let(:message) do + Class.new(::Protobuf::Message) do + optional FieldMessage, :message_field, 1 + end + end + + before do + stub_const('InnerMessage', inner_message) + stub_const('FieldMessage', field_message) + stub_const('Message', message) + end + + let(:instance) { message.new } + + describe 'setting and getting field' do + context "when set with the message type" do + it 'is readable as a message' do + value = field_message.new(:field => 34) + instance.message_field = value + expect(instance.message_field).to eq(value) + end + end + + context "when set with #to_proto" do + let(:to_proto_message) do + Class.new do + def to_proto + FieldMessage.new(:field => 42) + end + end + end + + it 'is readable as a message' do + value = to_proto_message.new + instance.message_field = value + expect(instance.message_field).to eq(value.to_proto) + end + end + + context "when set with #to_proto that returns the wrong message type" do + let(:to_proto_is_wrong_message) do + Class.new do + def to_proto + Message.new + end + end + end + + it 'fails' do + value = to_proto_is_wrong_message.new + expect { instance.message_field = value }.to raise_error TypeError + end + end + + context "when set with #to_hash" do + let(:to_hash_message) do + Class.new do + def to_hash + { :field => 989 } + end + end + end + + it 'is readable as a message' do + value = to_hash_message.new + instance.message_field = value + expect(instance.message_field).to eq(field_message.new(value.to_hash)) + end + end + end + + describe '#option_set' do + let(:message_field) { Message.fields[0] } + it 'returns unless yield' do + # No Error thrown + message_field.__send__(:option_set, nil, nil, nil) { false } + expect do + message_field.__send__(:option_set, nil, nil, nil) { true } + end.to raise_error StandardError + end + + it 'sets repeated fields' do + repeated = field_message.fields[1] + instance = field_message.new + expect(instance.repeated_field!).to eq(nil) + message_field.__send__(:option_set, instance, repeated, [53]) { true } + expect(instance.repeated_field!).to eq([53]) + message_field.__send__(:option_set, instance, repeated, [54]) { true } + expect(instance.repeated_field!).to eq([53, 54]) + end + + it 'sets optional non-message fields' do + optional = field_message.fields[0] + instance = field_message.new + expect(instance.field!).to eq(nil) + message_field.__send__(:option_set, instance, optional, 53) { true } + expect(instance.field!).to eq(53) + message_field.__send__(:option_set, instance, optional, 52) { true } + expect(instance.field!).to eq(52) + end + + it 'sets nested inner messages fields one at a time' do + inner = field_message.fields[2] + inner_val = InnerMessage.new(:field => 21) + inner_val2 = InnerMessage.new(:field2 => 9) + instance = field_message.new + expect(instance.message_field!).to eq(nil) + message_field.__send__(:option_set, instance, inner, inner_val) { true } + expect(instance.message_field!).to eq(InnerMessage.new(:field => 21)) + message_field.__send__(:option_set, instance, inner, inner_val2) { true } + expect(instance.message_field!).to eq(InnerMessage.new(:field => 21, :field2 => 9)) + end + end +end diff --git a/spec/lib/protobuf/field/sfixed32_field_spec.rb b/spec/lib/protobuf/field/sfixed32_field_spec.rb new file mode 100644 index 00000000..9d582fee --- /dev/null +++ b/spec/lib/protobuf/field/sfixed32_field_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Sfixed32Field do + + it_behaves_like :packable_field, described_class do + let(:value) { [-1, 0, 1] } + end + +end diff --git a/spec/lib/protobuf/field/sfixed64_field_spec.rb b/spec/lib/protobuf/field/sfixed64_field_spec.rb new file mode 100644 index 00000000..f380ed5d --- /dev/null +++ b/spec/lib/protobuf/field/sfixed64_field_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Sfixed64Field do + + it_behaves_like :packable_field, described_class do + let(:value) { [-1, 0, 1] } + end + +end diff --git a/spec/lib/protobuf/field/sint32_field_spec.rb b/spec/lib/protobuf/field/sint32_field_spec.rb new file mode 100644 index 00000000..f19d934a --- /dev/null +++ b/spec/lib/protobuf/field/sint32_field_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Sint32Field do + + it_behaves_like :packable_field, described_class do + let(:value) { [-1, 0, 1] } + end + +end diff --git a/spec/lib/protobuf/field/sint64_field_spec.rb b/spec/lib/protobuf/field/sint64_field_spec.rb new file mode 100644 index 00000000..84ca7304 --- /dev/null +++ b/spec/lib/protobuf/field/sint64_field_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Sint64Field do + + it_behaves_like :packable_field, described_class do + let(:value) { [-1, 0, 1] } + end + +end diff --git a/spec/lib/protobuf/field/string_field_spec.rb b/spec/lib/protobuf/field/string_field_spec.rb new file mode 100644 index 00000000..4fe358a3 --- /dev/null +++ b/spec/lib/protobuf/field/string_field_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +RSpec.describe ::Protobuf::Field::StringField do + + describe '#encode' do + context 'when a repeated string field contains frozen strings' do + it 'does not raise an encoding error' do + expect do + frozen_strings = ["foo".freeze, "bar".freeze, "baz".freeze] + ::Test::ResourceFindRequest.encode(:name => 'resource', :widgets => frozen_strings) + end.not_to raise_error + end + end + + context 'when a repeated bytes field contains frozen strings' do + it 'does not raise an encoding error' do + expect do + frozen_strings = ["foo".freeze, "bar".freeze, "baz".freeze] + ::Test::ResourceFindRequest.encode(:name => 'resource', :widget_bytes => frozen_strings) + end.not_to raise_error + end + end + + it 'does not alter string values after encoding multiple times' do + source_string = "foo" + proto = ::Test::Resource.new(:name => source_string) + proto.encode + expect(proto.name).to eq source_string + proto.encode + expect(proto.name).to eq source_string + end + + it 'does not alter unicode string values after encoding multiple times' do + source_string = "¢" + proto = ::Test::Resource.new(:name => source_string) + proto.encode + expect(proto.name).to eq source_string + proto.encode + expect(proto.name).to eq source_string + end + end + + describe '#default_value' do + context 'optional and required fields' do + it 'returns the class default' do + class SomeStringMessage < ::Protobuf::Message + optional :string, :some_string, 1 + end + expect(SomeStringMessage.get_field('some_string').default).to be nil + expect(::Protobuf::Field::StringField.default).to eq "" + expect(SomeStringMessage.new.some_string).to eq "" + end + + context 'with field default' do + class AnotherStringMessage < ::Protobuf::Message + optional :string, :set_string, 1, :default => "default value this is" + end + + it 'returns the set default' do + expect(AnotherStringMessage.get_field('set_string').default).to eq "default value this is" + expect(AnotherStringMessage.new.set_string).to eq "default value this is" + end + end + end + + context 'repeated field' do + class RepeatedStringMessage < ::Protobuf::Message + repeated :string, :repeated_string, 1 + end + + it 'returns the set default' do + expect(RepeatedStringMessage.new.repeated_string).to eq [] + end + end + end + +end diff --git a/spec/lib/protobuf/field/uint32_field_spec.rb b/spec/lib/protobuf/field/uint32_field_spec.rb new file mode 100644 index 00000000..af484a47 --- /dev/null +++ b/spec/lib/protobuf/field/uint32_field_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Uint32Field do + + it_behaves_like :packable_field, described_class + +end diff --git a/spec/lib/protobuf/field/uint64_field_spec.rb b/spec/lib/protobuf/field/uint64_field_spec.rb new file mode 100644 index 00000000..61b8d1b9 --- /dev/null +++ b/spec/lib/protobuf/field/uint64_field_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Field::Uint64Field do + + it_behaves_like :packable_field, described_class + +end diff --git a/spec/lib/protobuf/field_spec.rb b/spec/lib/protobuf/field_spec.rb new file mode 100644 index 00000000..37e82087 --- /dev/null +++ b/spec/lib/protobuf/field_spec.rb @@ -0,0 +1,192 @@ +require 'spec_helper' +require 'protobuf/field' +require PROTOS_PATH.join('resource.pb') + +RSpec.describe ::Protobuf::Field do + + describe '.build' do + pending + end + + describe '.field_class' do + context 'when type is an enum class' do + it 'returns an enum field' do + expect(subject.field_class(::Test::EnumTestType)).to eq(::Protobuf::Field::EnumField) + expect(subject.field_type(::Test::EnumTestType)).to eq(::Test::EnumTestType) + end + end + + context 'when type is a message class' do + it 'returns a message field' do + expect(subject.field_class(::Test::Resource)).to eq(::Protobuf::Field::MessageField) + expect(subject.field_type(::Test::Resource)).to eq(::Test::Resource) + end + end + + context 'when type is a base field class' do + it 'returns that class' do + expect(subject.field_class(::Protobuf::Field::BoolField)).to eq(::Protobuf::Field::BoolField) + end + end + + context 'when type is a double field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::DoubleField + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:double)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:double)).to eq(expected_field) + end + end + + context 'when type is a float field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::FloatField + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:float)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:float)).to eq(expected_field) + end + end + + context 'when type is a int32 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Int32Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:int32)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:int32)).to eq(expected_field) + end + end + + context 'when type is a int64 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Int64Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:int64)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:int64)).to eq(expected_field) + end + end + + context 'when type is a uint32 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Uint32Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:uint32)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:uint32)).to eq(expected_field) + end + end + + context 'when type is a uint64 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Uint64Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:uint64)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:uint64)).to eq(expected_field) + end + end + + context 'when type is a sint32 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Sint32Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:sint32)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:sint32)).to eq(expected_field) + end + end + + context 'when type is a sint64 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Sint64Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:sint64)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:sint64)).to eq(expected_field) + end + end + + context 'when type is a fixed32 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Fixed32Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:fixed32)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:fixed32)).to eq(expected_field) + end + end + + context 'when type is a fixed64 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Fixed64Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:fixed64)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:fixed64)).to eq(expected_field) + end + end + + context 'when type is a sfixed32 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Sfixed32Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:sfixed32)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:sfixed32)).to eq(expected_field) + end + end + + context 'when type is a sfixed64 field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::Sfixed64Field + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:sfixed64)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:sfixed64)).to eq(expected_field) + end + end + + context 'when type is a string field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::StringField + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:string)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:string)).to eq(expected_field) + end + end + + context 'when type is a bytes field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::BytesField + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:bytes)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:bytes)).to eq(expected_field) + end + end + + context 'when type is a bool field class or symbol' do + it 'returns that class' do + expected_field = ::Protobuf::Field::BoolField + expect(subject.field_class(expected_field)).to eq(expected_field) + expect(subject.field_class(:bool)).to eq(expected_field) + expect(subject.field_type(expected_field)).to eq(expected_field) + expect(subject.field_type(:bool)).to eq(expected_field) + end + end + + context 'when type is not mapped' do + it 'raises an ArgumentError' do + expect do + subject.field_class("boom") + end.to raise_error(ArgumentError) + end + end + + end + +end diff --git a/spec/lib/protobuf/generators/base_spec.rb b/spec/lib/protobuf/generators/base_spec.rb new file mode 100644 index 00000000..d6128ac1 --- /dev/null +++ b/spec/lib/protobuf/generators/base_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +require 'protobuf/code_generator' +require 'protobuf/generators/base' + +RSpec.describe ::Protobuf::Generators::Base do + + subject(:generator) { described_class.new(double) } + + context 'namespaces' do + let(:descriptor) { double(:name => 'Baz') } + subject { described_class.new(descriptor, 0, :namespace => [:foo, :bar]) } + specify { expect(subject.type_namespace).to eq([:foo, :bar, 'Baz']) } + specify { expect(subject.fully_qualified_type_namespace).to eq('.foo.bar.Baz') } + end + + describe '#run_once' do + it 'protects the block from being entered more than once' do + foo = 0 + bar = 0 + + test_run_once = lambda do + bar += 1 + subject.run_once(:foo_test) do + foo += 1 + end + end + + 10.times { test_run_once.call } + expect(foo).to eq(1) + expect(bar).to eq(10) + end + + it 'always returns the same object' do + rv = subject.run_once(:foo_test) do + "foo bar" + end + expect(rv).to eq("foo bar") + + rv = subject.run_once(:foo_test) do + "baz quux" + end + expect(rv).to eq("foo bar") + end + end + + describe '#to_s' do + before do + class ToStringTest < ::Protobuf::Generators::Base + def compile + run_once(:compile) do + puts "this is a test" + end + end + end + end + + subject { ToStringTest.new(double) } + + it 'compiles and returns the contents' do + 10.times do + expect(subject.to_s).to eq("this is a test\n") + end + end + end + + describe '#validate_tags' do + context 'when tags are duplicated' do + it 'fails with a GeneratorFatalError' do + expect(::Protobuf::CodeGenerator).to receive(:fatal).with(/FooBar object has duplicate tags\. Expected 3 tags, but got 4/) + described_class.validate_tags("FooBar", [1, 2, 2, 3]) + end + end + + context 'when tags are missing in the range' do + it 'prints a warning' do + allow(ENV).to receive(:key?).and_call_original + allow(ENV).to receive(:key?).with("PB_NO_TAG_WARNINGS").and_return(false) + expect(::Protobuf::CodeGenerator).to receive(:print_tag_warning_suppress) + expect(::Protobuf::CodeGenerator).to receive(:warn).with(/FooBar object should have 5 tags \(1\.\.5\), but found 4 tags/) + described_class.validate_tags("FooBar", [1, 2, 4, 5]) + end + end + end + + describe '#serialize_value' do + before do + stub_const("MyEnum", Class.new(::Protobuf::Enum) do + define :FOO, 1 + define :BOO, 2 + end) + stub_const("MyMessage1", Class.new(Protobuf::Message) do + optional :string, :foo, 1 + end) + stub_const("MyMessage2", Class.new(Protobuf::Message) do + optional :string, :foo, 1 + optional MyMessage1, :bar, 2 + optional :int32, :boom, 3 + optional MyEnum, :goat, 4 + optional :bool, :bam, 5 + optional :float, :fire, 6 + end) + stub_const("MyMessage3", Class.new(Protobuf::Message) do + optional :string, :foo, 1 + repeated MyMessage2, :bar, 2 + optional :int32, :boom, 3 + optional MyEnum, :goat, 4 + optional :bool, :bam, 5 + optional :float, :fire, 6 + end) + end + + it 'serializes messages' do + output_string = <<-STRING + { :foo => "space", + :bar => [{ + :foo => "station", + :bar => { :foo => "orbit" }, + :boom => 123, + :goat => ::MyEnum::FOO, + :bam => false, + :fire => 3.5 }], + :boom => 456, + :goat => ::MyEnum::BOO, + :bam => true, :fire => 1.2 } + STRING + + output_string.lstrip! + output_string.rstrip! + output_string.delete!("\n") + output_string.squeeze!(" ") + expect(generator.serialize_value(MyMessage3.new( + :foo => 'space', + :bar => [MyMessage2.new( + :foo => 'station', + :bar => MyMessage1.new(:foo => 'orbit'), + :boom => 123, + :goat => MyEnum::FOO, + :bam => false, + :fire => 3.5, + )], + :boom => 456, + :goat => MyEnum::BOO, + :bam => true, + :fire => 1.2, + ))).to eq(output_string) + end + + it 'serializes enums' do + expect(generator.serialize_value(MyEnum::FOO)).to eq("::MyEnum::FOO") + expect(generator.serialize_value(MyEnum::BOO)).to eq("::MyEnum::BOO") + end + end +end diff --git a/spec/lib/protobuf/generators/enum_generator_spec.rb b/spec/lib/protobuf/generators/enum_generator_spec.rb new file mode 100644 index 00000000..767a5d9d --- /dev/null +++ b/spec/lib/protobuf/generators/enum_generator_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +require 'protobuf/generators/enum_generator' + +RSpec.describe ::Protobuf::Generators::EnumGenerator do + + let(:values) do + [ + { :name => 'FOO', :number => 1 }, + { :name => 'BAR', :number => 2 }, + { :name => 'BAZ', :number => 3 }, + ] + end + let(:options) { nil } + let(:enum_fields) do + { + :name => 'TestEnum', + :value => values, + :options => options, + } + end + + let(:enum) { ::Google::Protobuf::EnumDescriptorProto.new(enum_fields) } + + subject { described_class.new(enum) } + + describe '#compile' do + let(:compiled) do + <<-RUBY +class TestEnum < ::Protobuf::Enum + define :FOO, 1 + define :BAR, 2 + define :BAZ, 3 +end + + RUBY + end + + it 'compiles the enum and its field values' do + subject.compile + expect(subject.to_s).to eq(compiled) + end + + context 'when allow_alias option is set' do + let(:compiled) do + <<-RUBY +class TestEnum < ::Protobuf::Enum + set_option :allow_alias, true + + define :FOO, 1 + define :BAR, 2 + define :BAZ, 3 +end + + RUBY + end + + let(:options) { { :allow_alias => true } } + + it 'sets the allow_alias option' do + subject.compile + expect(subject.to_s).to eq(compiled) + end + end + end + + describe '#build_value' do + it 'returns a string identifying the given enum value' do + expect(subject.build_value(enum.value.first)).to eq("define :FOO, 1") + end + + context 'with PB_UPCASE_ENUMS set' do + before { allow(ENV).to receive(:key?).with('PB_UPCASE_ENUMS').and_return(true) } + before { allow(ENV).to receive(:key?).with('PB_CAPITALIZE_ENUMS').and_return(false) } + let(:values) { [{ :name => 'boom', :number => 1 }] } + + it 'returns a string with the given enum name in ALL CAPS' do + expect(subject.build_value(enum.value.first)).to eq("define :BOOM, 1") + end + end + + context 'with PB_CAPITALIZE_ENUMS set' do + before { allow(ENV).to receive(:key?).with('PB_UPCASE_ENUMS').and_return(false) } + before { allow(ENV).to receive(:key?).with('PB_CAPITALIZE_ENUMS').and_return(true) } + let(:values) { [{ :name => 'boom', :number => 1 }] } + + it 'returns a string with the given enum name in ALL CAPS' do + expect(subject.build_value(enum.value.first)).to eq("define :Boom, 1") + end + end + end + +end diff --git a/spec/lib/protobuf/generators/extension_generator_spec.rb b/spec/lib/protobuf/generators/extension_generator_spec.rb new file mode 100644 index 00000000..9f241921 --- /dev/null +++ b/spec/lib/protobuf/generators/extension_generator_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +require 'protobuf/code_generator' +require 'protobuf/generators/extension_generator' + +RSpec.describe ::Protobuf::Generators::ExtensionGenerator do + + let(:field_descriptors) do + [ + double('field descriptor 1', :to_s => " field 1\n"), + double('field descriptor 2', :to_s => " field 2\n"), + double('field descriptor 3', :to_s => " field 3\n"), + ] + end + let(:message_type) { 'FooBar' } + + before do + expect(::Protobuf::Generators::FieldGenerator).to receive(:new).with(field_descriptors[0], nil, 1).and_return(field_descriptors[0]) + expect(::Protobuf::Generators::FieldGenerator).to receive(:new).with(field_descriptors[1], nil, 1).and_return(field_descriptors[1]) + expect(::Protobuf::Generators::FieldGenerator).to receive(:new).with(field_descriptors[2], nil, 1).and_return(field_descriptors[2]) + end + + subject { described_class.new(message_type, field_descriptors, 0) } + + describe '#compile' do + let(:compiled) do + 'class FooBar < ::Protobuf::Message + field 1 + field 2 + field 3 +end + +' + end + + it 'compiles the a class with the extension fields' do + subject.compile + expect(subject.to_s).to eq(compiled) + end + end + +end diff --git a/spec/lib/protobuf/generators/field_generator_spec.rb b/spec/lib/protobuf/generators/field_generator_spec.rb new file mode 100644 index 00000000..fc7c3def --- /dev/null +++ b/spec/lib/protobuf/generators/field_generator_spec.rb @@ -0,0 +1,208 @@ +require 'spec_helper' + +require 'protobuf/generators/field_generator' + +RSpec.describe ::Protobuf::Generators::FieldGenerator do + + let(:label_enum) { :LABEL_OPTIONAL } + let(:name) { 'foo_bar' } + let(:number) { 3 } + let(:type_enum) { :TYPE_STRING } + let(:type_name) { nil } + let(:default_value) { nil } + let(:extendee) { nil } + let(:field_options) { {} } + + let(:field_fields) do + { + :label => label_enum, + :name => name, + :number => number, + :type => type_enum, + :type_name => type_name, + :default_value => default_value, + :extendee => extendee, + :options => field_options, + } + end + let(:field) { ::Google::Protobuf::FieldDescriptorProto.new(field_fields) } + + let(:nested_types) { [] } + let(:owner_fields) do + { + :name => 'Baz', + :field => [field], + :nested_type => nested_types, + } + end + let(:owner_msg) { ::Google::Protobuf::DescriptorProto.new(owner_fields) } + + describe '#compile' do + subject { described_class.new(field, owner_msg, 1).to_s } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3\n" } + + context 'when the type is another message' do + let(:type_enum) { :TYPE_MESSAGE } + let(:type_name) { '.foo.bar.Baz' } + + specify { expect(subject).to eq " optional ::Foo::Bar::Baz, :foo_bar, 3\n" } + end + + context 'when a default value is used' do + let(:type_enum) { :TYPE_INT32 } + let(:default_value) { '42' } + specify { expect(subject).to eq " optional :int32, :foo_bar, 3, :default => 42\n" } + + context 'when type is an enum' do + let(:type_enum) { :TYPE_ENUM } + let(:type_name) { '.foo.bar.Baz' } + let(:default_value) { 'QUUX' } + + specify { expect(subject).to eq " optional ::Foo::Bar::Baz, :foo_bar, 3, :default => ::Foo::Bar::Baz::QUUX\n" } + end + + context 'when type is an enum with lowercase default value with PB_UPCASE_ENUMS set' do + let(:type_enum) { :TYPE_ENUM } + let(:type_name) { '.foo.bar.Baz' } + let(:default_value) { 'quux' } + before { allow(ENV).to receive(:key?).with('PB_UPCASE_ENUMS').and_return(true) } + before { allow(ENV).to receive(:key?).with('PB_CAPITALIZE_ENUMS').and_return(false) } + + specify { expect(subject).to eq " optional ::Foo::Bar::Baz, :foo_bar, 3, :default => ::Foo::Bar::Baz::QUUX\n" } + end + + context 'when type is an enum with lowercase default value with PB_CAPITALIZE_ENUMS set' do + let(:type_enum) { :TYPE_ENUM } + let(:type_name) { '.foo.bar.Baz' } + let(:default_value) { 'quux' } + before { allow(ENV).to receive(:key?).with('PB_UPCASE_ENUMS').and_return(false) } + before { allow(ENV).to receive(:key?).with('PB_CAPITALIZE_ENUMS').and_return(true) } + + specify { expect(subject).to eq " optional ::Foo::Bar::Baz, :foo_bar, 3, :default => ::Foo::Bar::Baz::Quux\n" } + end + + context 'when the type is a string' do + let(:type_enum) { :TYPE_STRING } + let(:default_value) { "a default \"string\"" } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3, :default => \"a default \"string\"\"\n" } + end + + context 'when float or double field type' do + let(:type_enum) { :TYPE_DOUBLE } + + context 'when the default value is "nan"' do + let(:default_value) { 'nan' } + specify { expect(subject).to match(/::Float::NAN/) } + end + + context 'when the default value is "inf"' do + let(:default_value) { 'inf' } + specify { expect(subject).to match(/::Float::INFINITY/) } + end + + context 'when the default value is "-inf"' do + let(:default_value) { '-inf' } + specify { expect(subject).to match(/-::Float::INFINITY/) } + end + end + end + + context 'when the field is an extension' do + let(:extendee) { 'foo.bar.Baz' } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3, :extension => true\n" } + end + + context 'when field is packed' do + let(:field_options) { { :packed => true } } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3, :packed => true\n" } + end + + context 'when field is a map' do + let(:type_enum) { :TYPE_MESSAGE } + let(:type_name) { '.foo.bar.Baz.FooBarEntry' } + let(:label_enum) { :LABEL_REPEATED } + let(:nested_types) do + [::Google::Protobuf::DescriptorProto.new( + :name => 'FooBarEntry', + :field => [ + ::Google::Protobuf::FieldDescriptorProto.new( + :label => :LABEL_OPTIONAL, + :name => 'key', + :number => 1, + :type => :TYPE_STRING, + :type_name => nil), + ::Google::Protobuf::FieldDescriptorProto.new( + :label => :LABEL_OPTIONAL, + :name => 'value', + :number => 2, + :type => :TYPE_ENUM, + :type_name => '.foo.bar.SnafuState'), + ], + :options => ::Google::Protobuf::MessageOptions.new(:map_entry => true)), + ] + end + + specify { expect(subject).to eq " map :string, ::Foo::Bar::SnafuState, :foo_bar, 3\n" } + end + + context 'when field is deprecated' do + let(:field_options) { { :deprecated => true } } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3, :deprecated => true\n" } + end + + context 'when field uses a custom option that is an extension' do + class ::CustomFieldEnum < ::Protobuf::Enum + define :BOOM, 1 + define :BAM, 2 + end + + class ::CustomFieldMessage < ::Protobuf::Message + optional :string, :foo, 1 + end + + class ::Google::Protobuf::FieldOptions < ::Protobuf::Message + optional :string, :custom_string_option, 22000, :extension => true + optional :bool, :custom_bool_option, 22001, :extension => true + optional :int32, :custom_int32_option, 22002, :extension => true + optional ::CustomFieldEnum, :custom_enum_option, 22003, :extension => true + optional ::CustomFieldMessage, :custom_message_option, 22004, :extension => true + end + + describe 'option has a string value' do + let(:field_options) { { :custom_string_option => 'boom' } } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3, :custom_string_option => \"boom\"\n" } + end + + describe 'option has a bool value' do + let(:field_options) { { :custom_bool_option => true } } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3, :custom_bool_option => true\n" } + end + + describe 'option has a int32 value' do + let(:field_options) { { :custom_int32_option => 123 } } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3, :custom_int32_option => 123\n" } + end + + describe 'option has a message value' do + let(:field_options) { { :custom_message_option => CustomFieldMessage.new(:foo => 'boom') } } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3, :custom_message_option => { :foo => \"boom\" }\n" } + end + + describe 'option has a enum value' do + let(:field_options) { { :custom_enum_option => CustomFieldEnum::BAM } } + + specify { expect(subject).to eq " optional :string, :foo_bar, 3, :custom_enum_option => ::CustomFieldEnum::BAM\n" } + end + end + end + +end diff --git a/spec/lib/protobuf/generators/file_generator_spec.rb b/spec/lib/protobuf/generators/file_generator_spec.rb new file mode 100644 index 00000000..51a6156f --- /dev/null +++ b/spec/lib/protobuf/generators/file_generator_spec.rb @@ -0,0 +1,119 @@ +require 'spec_helper' + +require 'protobuf/generators/file_generator' + +RSpec.describe ::Protobuf::Generators::FileGenerator do + + let(:base_descriptor_fields) { { :name => 'test/foo.proto' } } + let(:descriptor_fields) { base_descriptor_fields } + let(:file_descriptor) { ::Google::Protobuf::FileDescriptorProto.new(descriptor_fields) } + + subject { described_class.new(file_descriptor) } + specify { expect(subject.file_name).to eq('test/foo.pb.rb') } + + describe '#print_import_requires' do + let(:descriptor_fields) do + base_descriptor_fields.merge( + :dependency => [ + 'test/bar.proto', + 'test/baz.proto', + ], + ) + end + + it 'prints a ruby require for each dependency' do + expect(subject).to receive(:print_require).with('test/bar.pb', false) + expect(subject).to receive(:print_require).with('test/baz.pb', false) + subject.print_import_requires + end + + it 'prints a ruby require_relative for each dependency if environment variable was set' do + expect(subject).to receive(:print_require).with('test/bar.pb', true) + expect(subject).to receive(:print_require).with('test/baz.pb', true) + ENV['PB_REQUIRE_RELATIVE'] = 'true' + subject.print_import_requires + ENV.delete('PB_REQUIRE_RELATIVE') + end + end + + describe '#compile' do + it 'generates the file contents' do + subject.compile + expect(subject.to_s).to eq < 'test.pkg.file_generator_spec', + :extension => [{ + :name => 'boom', + :number => 20_000, + :label => Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL, + :type => Google::Protobuf::FieldDescriptorProto::Type::TYPE_STRING, + :extendee => '.google.protobuf.FieldOptions', + }], + ) + end + + it 'generates the file contents that include the namespaced extension name' do + subject.compile + expect(subject.to_s).to eq < true + end + + end + + end + +end + +EOF + end + end + + end +end diff --git a/spec/unit/rpc/connectors/eventmachine_client_spec.rb b/spec/lib/protobuf/generators/message_generator_spec.rb similarity index 100% rename from spec/unit/rpc/connectors/eventmachine_client_spec.rb rename to spec/lib/protobuf/generators/message_generator_spec.rb diff --git a/spec/lib/protobuf/generators/service_generator_spec.rb b/spec/lib/protobuf/generators/service_generator_spec.rb new file mode 100644 index 00000000..07d1c9af --- /dev/null +++ b/spec/lib/protobuf/generators/service_generator_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +require 'protobuf/generators/service_generator' + +RSpec.describe ::Protobuf::Generators::ServiceGenerator do + + class ::CustomMethodEnum < ::Protobuf::Enum + define :BOOM, 1 + define :BAM, 2 + end + + class ::CustomMethodMessage < ::Protobuf::Message + optional :string, :foo, 1 + end + + class ::Google::Protobuf::MethodOptions < ::Protobuf::Message + optional :string, :custom_string_option, 22000, :extension => true + optional :bool, :custom_bool_option, 22001, :extension => true + optional :int32, :custom_int32_option, 22002, :extension => true + optional ::CustomMethodEnum, :custom_enum_option, 22003, :extension => true + optional ::CustomMethodMessage, :custom_message_option, 22004, :extension => true + end + + let(:methods) do + [ + { :name => 'Search', :input_type => 'FooRequest', :output_type => 'FooResponse' }, + { + :name => 'FooBar', + :input_type => '.foo.Request', + :output_type => '.bar.Response', + :options => { + :custom_string_option => 'boom', + :custom_bool_option => true, + :custom_int32_option => 123, + :custom_message_option => CustomMethodMessage.new(:foo => 'bam'), + :custom_enum_option => CustomMethodEnum::BAM, + }, + }, + ] + end + let(:service_fields) do + { + :name => 'TestService', + :method => methods, + } + end + + let(:service) { ::Google::Protobuf::ServiceDescriptorProto.new(service_fields) } + + subject(:service_generator) { described_class.new(service) } + + describe '#compile' do + let(:compiled) do + < "bam" } + end +end + +EOF + end + + it "compiles the service and it's rpc methods" do + subject.compile + expect(subject.to_s).to eq(compiled) + end + + context 'with PB_USE_RAW_RPC_NAMES in the environemnt' do + let(:compiled) do + < "bam" } + end +end + +EOF + end + + before { allow(ENV).to receive(:key?).with('PB_USE_RAW_RPC_NAMES').and_return(true) } + + it 'uses the raw RPC name and does not underscore it' do + subject.compile + expect(subject.to_s).to eq(compiled) + end + end + end +end diff --git a/spec/lib/protobuf/lifecycle_spec.rb b/spec/lib/protobuf/lifecycle_spec.rb new file mode 100644 index 00000000..d1414477 --- /dev/null +++ b/spec/lib/protobuf/lifecycle_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' +require 'protobuf/lifecycle' + +RSpec.describe ::Protobuf::Lifecycle do + subject { described_class } + + around do |example| + # this entire class is deprecated + ::Protobuf.deprecator.silence(&example) + end + + before do + ::ActiveSupport::Notifications.notifier = ::ActiveSupport::Notifications::Fanout.new + end + + it "registers a string as the event_name" do + expect(::ActiveSupport::Notifications).to receive(:subscribe).with("something") + subject.register("something") { true } + end + + it "only registers blocks for event callbacks" do + expect do + subject.register("something") + end.to raise_error(/block/) + end + + it "calls the registered block when triggered" do + this = nil + subject.register("this") do + this = "not nil" + end + + subject.trigger("this") + expect(this).to_not be_nil + expect(this).to eq("not nil") + end + + it "calls multiple registered blocks when triggered" do + this = nil + that = nil + + subject.register("this") do + this = "not nil" + end + + subject.register("this") do + that = "not nil" + end + + subject.trigger("this") + expect(this).to_not be_nil + expect(this).to eq("not nil") + expect(that).to_not be_nil + expect(that).to eq("not nil") + end + + context 'when the registered block has arity' do + context 'and the triggered event does not have args' do + it 'does not pass the args' do + outer_bar = nil + + subject.register('foo') do |bar| + expect(bar).to be_nil + outer_bar = 'triggered' + end + + subject.trigger('foo') + expect(outer_bar).to eq 'triggered' + end + end + + context 'and the triggered event has arguments' do + it 'does not pass the args' do + outer_bar = nil + + subject.register('foo') do |bar| + expect(bar).to_not be_nil + outer_bar = bar + end + + subject.trigger('foo', 'baz') + expect(outer_bar).to eq 'baz' + end + end + end + + context "normalized event names" do + specify { expect(subject.normalized_event_name(:derp)).to eq("derp") } + specify { expect(subject.normalized_event_name(:Derp)).to eq("derp") } + specify { expect(subject.normalized_event_name("DERP")).to eq("derp") } + specify { expect(subject.normalized_event_name("derp")).to eq("derp") } + end + +end diff --git a/spec/lib/protobuf/message_spec.rb b/spec/lib/protobuf/message_spec.rb new file mode 100644 index 00000000..50513f14 --- /dev/null +++ b/spec/lib/protobuf/message_spec.rb @@ -0,0 +1,912 @@ +require 'stringio' +require 'spec_helper' +require PROTOS_PATH.join('resource.pb') +require PROTOS_PATH.join('enum.pb') + +RSpec.describe Protobuf::Message do + + describe '.decode' do + let(:message) { ::Test::Resource.new(:name => "Jim") } + + it 'creates a new message object decoded from the given bytes' do + expect(::Test::Resource.decode(message.encode)).to eq message + end + + context 'with a new enum value' do + let(:older_message) do + Class.new(Protobuf::Message) do + enum_class = Class.new(::Protobuf::Enum) do + define :YAY, 1 + end + + optional enum_class, :enum_field, 1 + repeated enum_class, :enum_list, 2 + end + end + + let(:newer_message) do + Class.new(Protobuf::Message) do + enum_class = Class.new(::Protobuf::Enum) do + define :YAY, 1 + define :HOORAY, 2 + end + + optional enum_class, :enum_field, 1 + repeated enum_class, :enum_list, 2 + end + end + + context 'with a singular field' do + it 'treats the field as if it was unset when decoding' do + newer = newer_message.new(:enum_field => :HOORAY).serialize + + expect(older_message.decode(newer).enum_field!).to be_nil + end + + it 'rejects an unknown value when using the constructor' do + expect { older_message.new(:enum_field => :HOORAY) }.to raise_error(TypeError) + end + + it 'rejects an unknown value when the setter' do + older = older_message.new + expect { older.enum_field = :HOORAY }.to raise_error(TypeError) + end + end + + context 'with a repeated field' do + it 'treats the field as if it was unset when decoding' do + newer = newer_message.new(:enum_list => [:HOORAY]).serialize + + expect(older_message.decode(newer).enum_list).to eq([]) + end + + it 'rejects an unknown value when using the constructor' do + expect { older_message.new(:enum_list => [:HOORAY]) }.to raise_error(TypeError) + end + + it 'rejects an unknown value when the setter' do + older = older_message.new + expect { older.enum_field = [:HOORAY] }.to raise_error(TypeError) + end + end + end + end + + describe '.decode_from' do + let(:message) { ::Test::Resource.new(:name => "Jim") } + + it 'creates a new message object decoded from the given byte stream' do + stream = ::StringIO.new(message.encode) + expect(::Test::Resource.decode_from(stream)).to eq message + end + end + + describe 'defining a new field' do + context 'when defining a field with a tag that has already been used' do + it 'raises a TagCollisionError' do + expect do + Class.new(Protobuf::Message) do + optional ::Protobuf::Field::Int32Field, :foo, 1 + optional ::Protobuf::Field::Int32Field, :bar, 1 + end + end.to raise_error(Protobuf::TagCollisionError, /Field number 1 has already been used/) + end + end + + context 'when defining an extension field with a tag that has already been used' do + it 'raises a TagCollisionError' do + expect do + Class.new(Protobuf::Message) do + extensions 100...110 + optional ::Protobuf::Field::Int32Field, :foo, 100 + optional ::Protobuf::Field::Int32Field, :bar, 100, :extension => true + end + end.to raise_error(Protobuf::TagCollisionError, /Field number 100 has already been used/) + end + end + + context 'when defining a field with a name that has already been used' do + it 'raises a DuplicateFieldNameError' do + expect do + Class.new(Protobuf::Message) do + optional ::Protobuf::Field::Int32Field, :foo, 1 + optional ::Protobuf::Field::Int32Field, :foo, 2 + end + end.to raise_error(Protobuf::DuplicateFieldNameError, /Field name foo has already been used/) + end + end + + context 'when defining an extension field with a name that has already been used' do + it 'raises a DuplicateFieldNameError' do + expect do + Class.new(Protobuf::Message) do + extensions 100...110 + optional ::Protobuf::Field::Int32Field, :foo, 1 + optional ::Protobuf::Field::Int32Field, :foo, 100, :extension => true + end + end.to raise_error(Protobuf::DuplicateFieldNameError, /Field name foo has already been used/) + end + end + end + + describe '.encode' do + let(:values) { { :name => "Jim" } } + + it 'creates a new message object with the given values and returns the encoded bytes' do + expect(::Test::Resource.encode(values)).to eq ::Test::Resource.new(values).encode + end + end + + describe '#initialize' do + it "defaults to the first value listed in the enum's type definition" do + test_enum = Test::EnumTestMessage.new + expect(test_enum.non_default_enum).to eq(Test::EnumTestType.enums.first) + end + + it "defaults to a a value with a name" do + test_enum = Test::EnumTestMessage.new + expect(test_enum.non_default_enum.name).to eq(Test::EnumTestType.enums.first.name) + end + + it "exposes the enum getter raw value through ! method" do + test_enum = Test::EnumTestMessage.new + expect(test_enum.non_default_enum!).to be_nil + end + + it "exposes the enum getter raw value through ! method (when set)" do + test_enum = Test::EnumTestMessage.new + test_enum.non_default_enum = 1 + expect(test_enum.non_default_enum!).to eq(1) + end + + it "does not try to set attributes which have nil values" do + expect_any_instance_of(Test::EnumTestMessage).not_to receive("non_default_enum=") + Test::EnumTestMessage.new(:non_default_enum => nil) + end + + it "takes a hash as an initialization argument" do + test_enum = Test::EnumTestMessage.new(:non_default_enum => 2) + expect(test_enum.non_default_enum).to eq(2) + end + + it "initializes with an object that responds to #to_hash" do + hashie_object = OpenStruct.new(:to_hash => { :non_default_enum => 2 }) + test_enum = Test::EnumTestMessage.new(hashie_object) + expect(test_enum.non_default_enum).to eq(2) + end + + it "initializes with an object with a block" do + test_enum = Test::EnumTestMessage.new { |p| p.non_default_enum = 2 } + expect(test_enum.non_default_enum).to eq(2) + end + + # to be deprecated + it "allows you to pass nil to repeated fields" do + test = Test::Resource.new(:repeated_enum => nil) + expect(test.repeated_enum).to eq([]) + end + end + + describe '#encode' do + context "encoding" do + it "accepts UTF-8 strings into string fields" do + message = ::Test::Resource.new(:name => "Kyle Redfearn\u0060s iPad") + + expect { message.encode }.to_not raise_error + end + + it "keeps utf-8 when utf-8 is input for string fields" do + name = 'my name💩' + name.force_encoding(Encoding::UTF_8) + + message = ::Test::Resource.new(:name => name) + new_message = ::Test::Resource.decode(message.encode) + expect(new_message.name == name).to be true + end + + it "trims binary when binary is input for string fields" do + name = "my name\xC3" + name.force_encoding(Encoding::BINARY) + + message = ::Test::Resource.new(:name => name) + new_message = ::Test::Resource.decode(message.encode) + expect(new_message.name == "my name").to be true + end + end + + context "when there's no value for a required field" do + let(:message) { ::Test::ResourceWithRequiredField.new } + + it "raises a 'message not initialized' error" do + expect do + message.encode + end.to raise_error(Protobuf::SerializationError, /required/i) + end + end + + context "repeated fields" do + let(:message) { ::Test::Resource.new(:name => "something") } + + it "does not raise an error when repeated fields are []" do + expect do + message.repeated_enum = [] + message.encode + end.to_not raise_error + end + + it "sets the value to nil when empty array is passed" do + message.repeated_enum = [] + expect(message.instance_variable_get("@values")[:repeated_enum]).to be_nil + end + + it "does not compact the edit original array" do + a = [nil].freeze + message.repeated_enum = a + expect(message.repeated_enum).to eq([]) + expect(a).to eq([nil].freeze) + end + + it "compacts the set array" do + message.repeated_enum = [nil] + expect(message.repeated_enum).to eq([]) + end + + it "raises TypeError when a non-array replaces it" do + expect do + message.repeated_enum = 2 + end.to raise_error(/value of type/) + end + end + end + + describe "boolean predicate methods" do + subject { Test::ResourceFindRequest.new(:name => "resource") } + + it { is_expected.to respond_to(:active?) } + + it "sets the predicate to true when the boolean value is true" do + subject.active = true + expect(subject.active?).to be true + end + + it "sets the predicate to false when the boolean value is false" do + subject.active = false + expect(subject.active?).to be false + end + + it "does not put predicate methods on non-boolean fields" do + expect(Test::ResourceFindRequest.new(:name => "resource")).to_not respond_to(:name?) + end + end + + describe "#respond_to_and_has?" do + subject { Test::EnumTestMessage.new(:non_default_enum => 2) } + + it "is false when the message does not have the field" do + expect(subject.respond_to_and_has?(:other_field)).to be false + end + + it "is true when the message has the field" do + expect(subject.respond_to_and_has?(:non_default_enum)).to be true + end + end + + describe "#respond_to_has_and_present?" do + subject { Test::EnumTestMessage.new(:non_default_enum => 2) } + + it "is false when the message does not have the field" do + expect(subject.respond_to_and_has_and_present?(:other_field)).to be false + end + + it "is false when the field is repeated and a value is not present" do + expect(subject.respond_to_and_has_and_present?(:repeated_enums)).to be false + end + + it "is false when the field is repeated and the value is empty array" do + subject.repeated_enums = [] + expect(subject.respond_to_and_has_and_present?(:repeated_enums)).to be false + end + + it "is true when the field is repeated and a value is present" do + subject.repeated_enums = [2] + expect(subject.respond_to_and_has_and_present?(:repeated_enums)).to be true + end + + it "is true when the message has the field" do + expect(subject.respond_to_and_has_and_present?(:non_default_enum)).to be true + end + + context "#API" do + subject { Test::EnumTestMessage.new(:non_default_enum => 2) } + + specify { expect(subject).to respond_to(:respond_to_and_has_and_present?) } + specify { expect(subject).to respond_to(:responds_to_and_has_and_present?) } + specify { expect(subject).to respond_to(:responds_to_has?) } + specify { expect(subject).to respond_to(:respond_to_has?) } + specify { expect(subject).to respond_to(:respond_to_has_present?) } + specify { expect(subject).to respond_to(:responds_to_has_present?) } + specify { expect(subject).to respond_to(:respond_to_and_has_present?) } + specify { expect(subject).to respond_to(:responds_to_and_has_present?) } + end + + end + + describe '#inspect' do + let(:klass) do + Class.new(Protobuf::Message) do |klass| + enum_class = Class.new(Protobuf::Enum) do + define :YAY, 1 + end + + klass.const_set(:EnumKlass, enum_class) + + optional :string, :name, 1 + repeated :int32, :counts, 2 + optional enum_class, :enum, 3 + end + end + + before { stub_const('MyMessage', klass) } + + it 'lists the fields' do + proto = klass.new(:name => 'wooo', :counts => [1, 2, 3], :enum => klass::EnumKlass::YAY) + expect(proto.inspect).to eq('#>') + end + end + + describe '#to_hash' do + context 'generating values for an ENUM field' do + it 'converts the enum to its tag representation' do + hash = Test::EnumTestMessage.new(:non_default_enum => :TWO).to_hash + expect(hash).to eq(:non_default_enum => 2) + end + + it 'does not populate default values' do + hash = Test::EnumTestMessage.new.to_hash + expect(hash).to eq({}) + end + + it 'converts repeated enum fields to an array of the tags' do + hash = Test::EnumTestMessage.new(:repeated_enums => [:ONE, :TWO, :TWO, :ONE]).to_hash + expect(hash).to eq(:repeated_enums => [1, 2, 2, 1]) + end + end + + context 'generating values for a Message field' do + it 'recursively hashes field messages' do + hash = Test::Nested.new(:resource => { :name => 'Nested' }).to_hash + expect(hash).to eq(:resource => { :name => 'Nested' }) + end + + it 'recursively hashes a repeated set of messages' do + proto = Test::Nested.new( + :multiple_resources => [ + Test::Resource.new(:name => 'Resource 1'), + Test::Resource.new(:name => 'Resource 2'), + ], + ) + + expect(proto.to_hash).to eq( + :multiple_resources => [ + { :name => 'Resource 1' }, + { :name => 'Resource 2' }, + ], + ) + end + end + + it 'uses simple field names as keys when possible and fully qualified names otherwise' do + message = Class.new(::Protobuf::Message) do + optional :int32, :field, 1 + optional :int32, :colliding_field, 2 + extensions 100...200 + optional :int32, :".ext.normal_ext_field", 100, :extension => true + optional :int32, :".ext.colliding_field", 101, :extension => true + optional :int32, :".ext.colliding_field2", 102, :extension => true + optional :int32, :".ext2.colliding_field2", 103, :extension => true + end + + hash = { + :field => 1, + :colliding_field => 2, + :normal_ext_field => 3, + :".ext.colliding_field" => 4, + :".ext.colliding_field2" => 5, + :".ext2.colliding_field2" => 6, + } + instance = message.new(hash) + expect(instance.to_hash).to eq(hash) + end + end + + describe '#to_json' do + subject do + ::Test::ResourceFindRequest.new(:name => 'Test Name', :active => false) + end + + specify { expect(subject.to_json).to eq '{"name":"Test Name","active":false}' } + + context 'for byte fields' do + let(:bytes) { "\x06\x8D1HP\x17:b" } + + subject do + ::Test::ResourceFindRequest.new(:widget_bytes => [bytes]) + end + + specify { expect(subject.to_json).to eq '{"widget_bytes":["Bo0xSFAXOmI="]}' } + end + end + + describe '.to_json' do + it 'returns the class name of the message for use in json encoding' do + expect do + ::Timeout.timeout(0.1) do + expect(::Test::Resource.to_json).to eq("Test::Resource") + end + end.not_to raise_error + end + end + + describe "#define_accessor" do + subject { ::Test::Resource.new } + + it 'allows string fields to be set to nil' do + expect { subject.name = nil }.to_not raise_error + end + + it 'does not allow string fields to be set to Numeric' do + expect { subject.name = 1 }.to raise_error(/name/) + end + + it 'does not allow a repeated field is set to nil' do + expect { subject.repeated_enum = nil }.to raise_error(TypeError) + end + + context '#{simple_field_name}!' do + it 'returns value of set field' do + expect(::Test::Resource.new(:name => "Joe").name!).to eq("Joe") + end + + it 'returns value of set field with default' do + expect(::Test::Resource.new(:name => "").name!).to eq("") + end + + it 'returns nil if extension field is unset' do + expect(subject.ext_is_searchable!).to be_nil + end + + it 'returns value of set extension field' do + message = ::Test::Resource.new(:ext_is_searchable => true) + expect(message.ext_is_searchable!).to be(true) + end + + it 'returns value of set extension field with default' do + message = ::Test::Resource.new(:ext_is_searchable => false) + expect(message.ext_is_searchable!).to be(false) + end + + it 'returns nil for an unset repeated field that has only be read' do + message = ::Test::Resource.new + expect(message.repeated_enum!).to be_nil + message.repeated_enum + expect(message.repeated_enum!).to be_nil + end + + it 'returns value for an unset repeated field has been read and appended to' do + message = ::Test::Resource.new + message.repeated_enum << 1 + expect(message.repeated_enum!).to eq([1]) + end + + it 'returns value for an unset repeated field has been explicitly set' do + message = ::Test::Resource.new + message.repeated_enum = [1] + expect(message.repeated_enum!).to eq([1]) + end + end + end + + describe '.get_extension_field' do + it 'fetches an extension field by its tag' do + field = ::Test::Resource.get_extension_field(100) + expect(field).to be_a(::Protobuf::Field::BoolField) + expect(field.tag).to eq(100) + expect(field.name).to eq(:ext_is_searchable) + expect(field.fully_qualified_name).to eq(:'.test.Searchable.ext_is_searchable') + expect(field).to be_extension + end + + it 'fetches an extension field by its symbolized name' do + expect(::Test::Resource.get_extension_field(:ext_is_searchable)).to be_a(::Protobuf::Field::BoolField) + expect(::Test::Resource.get_extension_field('ext_is_searchable')).to be_a(::Protobuf::Field::BoolField) + expect(::Test::Resource.get_extension_field(:'.test.Searchable.ext_is_searchable')).to be_a(::Protobuf::Field::BoolField) + expect(::Test::Resource.get_extension_field('.test.Searchable.ext_is_searchable')).to be_a(::Protobuf::Field::BoolField) + end + + it 'returns nil when attempting to get a non-extension field' do + expect(::Test::Resource.get_extension_field(1)).to be_nil + end + + it 'returns nil when field is not found' do + expect(::Test::Resource.get_extension_field(-1)).to be_nil + expect(::Test::Resource.get_extension_field(nil)).to be_nil + end + end + + describe '#field?' do + it 'returns false for non-existent field' do + expect(::Test::Resource.get_field('doesnotexist')).to be_nil + expect(::Test::Resource.new.field?('doesnotexist')).to be(false) + end + + it 'returns false for unset field' do + expect(::Test::Resource.get_field('name')).to be + expect(::Test::Resource.new.field?('name')).to be(false) + end + + it 'returns false for unset field from tag' do + expect(::Test::Resource.get_field(1)).to be + expect(::Test::Resource.new.field?(1)).to be(false) + end + + it 'returns true for set field' do + expect(::Test::Resource.new(:name => "Joe").field?('name')).to be(true) + end + + it 'returns true for set field with default' do + expect(::Test::Resource.new(:name => "").field?('name')).to be(true) + end + + it 'returns true from field tag value' do + expect(::Test::Resource.new(:name => "Joe").field?(1)).to be(true) + end + + it 'returns false for unset extension field' do + ext_field = :".test.Searchable.ext_is_searchable" + expect(::Test::Resource.get_extension_field(ext_field)).to be + expect(::Test::Resource.new.field?(ext_field)).to be(false) + end + + it 'returns false for unset extension field from tag' do + expect(::Test::Resource.get_extension_field(100)).to be + expect(::Test::Resource.new.field?(100)).to be(false) + end + + it 'returns true for set extension field' do + ext_field = :".test.Searchable.ext_is_searchable" + message = ::Test::Resource.new(ext_field => true) + expect(message.field?(ext_field)).to be(true) + end + + it 'returns true for set extension field with default' do + ext_field = :".test.Searchable.ext_is_searchable" + message = ::Test::Resource.new(ext_field => false) + expect(message.field?(ext_field)).to be(true) + end + + it 'returns true for set extension field from tag' do + ext_field = :".test.Searchable.ext_is_searchable" + message = ::Test::Resource.new(ext_field => false) + expect(message.field?(100)).to be(true) + end + + it 'returns false for repeated field that has been read from' do + message = ::Test::Resource.new + expect(message.field?(:repeated_enum)).to be(false) + message.repeated_enum + expect(message.field?(:repeated_enum)).to be(false) + end + + it 'returns true for a repeated field that has been read from and appended to' do + message = ::Test::Resource.new + message.repeated_enum << 1 + expect(message.field?(:repeated_enum)).to be(true) + end + + it 'returns true for a repeated field that has been set with the setter' do + message = ::Test::Resource.new + message.repeated_enum = [1] + expect(message.field?(:repeated_enum)).to be(true) + end + + it 'returns false for a repeated field that has been replaced with []' do + message = ::Test::Resource.new + message.repeated_enum.replace([]) + expect(message.field?(:repeated_enum)).to be(false) + end + end + + describe '.get_field' do + it 'fetches a non-extension field by its tag' do + field = ::Test::Resource.get_field(1) + expect(field).to be_a(::Protobuf::Field::StringField) + expect(field.tag).to eq(1) + expect(field.name).to eq(:name) + expect(field.fully_qualified_name).to eq(:name) + expect(field).not_to be_extension + end + + it 'fetches a non-extension field by its symbolized name' do + expect(::Test::Resource.get_field(:name)).to be_a(::Protobuf::Field::StringField) + expect(::Test::Resource.get_field('name')).to be_a(::Protobuf::Field::StringField) + end + + it 'fetches an extension field when forced' do + expect(::Test::Resource.get_field(100, true)).to be_a(::Protobuf::Field::BoolField) + expect(::Test::Resource.get_field(:'.test.Searchable.ext_is_searchable', true)).to be_a(::Protobuf::Field::BoolField) + expect(::Test::Resource.get_field('.test.Searchable.ext_is_searchable', true)).to be_a(::Protobuf::Field::BoolField) + end + + it 'returns nil when attempting to get an extension field' do + expect(::Test::Resource.get_field(100)).to be_nil + end + + it 'returns nil when field is not defined' do + expect(::Test::Resource.get_field(-1)).to be_nil + expect(::Test::Resource.get_field(nil)).to be_nil + end + end + + describe 'defining a field' do + # Case 1 + context 'single base field' do + let(:klass) do + Class.new(Protobuf::Message) do + optional :string, :foo, 1 + end + end + + it 'has an accessor for foo' do + message = klass.new(:foo => 'bar') + expect(message.foo).to eq('bar') + expect(message[:foo]).to eq('bar') + expect(message['foo']).to eq('bar') + end + end + + # Case 2 + context 'base field and extension field name collision' do + let(:klass) do + Class.new(Protobuf::Message) do + optional :string, :foo, 1 + optional :string, :".boom.foo", 2, :extension => true + end + end + + it 'has an accessor for foo that refers to the base field' do + message = klass.new(:foo => 'bar', '.boom.foo' => 'bam') + expect(message.foo).to eq('bar') + expect(message[:foo]).to eq('bar') + expect(message['foo']).to eq('bar') + expect(message[:'.boom.foo']).to eq('bam') + expect(message['.boom.foo']).to eq('bam') + end + end + + # Case 3 + context 'no base field with extension fields with name collision' do + let(:klass) do + Class.new(Protobuf::Message) do + optional :string, :".boom.foo", 2, :extension => true + optional :string, :".goat.foo", 3, :extension => true + end + end + + it 'has an accessor for foo that refers to the extension field' do + message = klass.new('.boom.foo' => 'bam', '.goat.foo' => 'red') + expect { message.foo }.to raise_error(NoMethodError) + expect { message[:foo] }.to raise_error(ArgumentError) + expect { message['foo'] }.to raise_error(ArgumentError) + expect(message[:'.boom.foo']).to eq('bam') + expect(message['.boom.foo']).to eq('bam') + expect(message[:'.goat.foo']).to eq('red') + expect(message['.goat.foo']).to eq('red') + end + end + + # Case 4 + context 'no base field with an extension field' do + let(:klass) do + Class.new(Protobuf::Message) do + optional :string, :".boom.foo", 2, :extension => true + end + end + + it 'has an accessor for foo that refers to the extension field' do + message = klass.new('.boom.foo' => 'bam') + expect(message.foo).to eq('bam') + expect(message[:foo]).to eq('bam') + expect(message['foo']).to eq('bam') + expect(message[:'.boom.foo']).to eq('bam') + expect(message['.boom.foo']).to eq('bam') + end + end + end + + describe 'map fields' do + it 'serializes the same as equivalent non-map-field' do + class MessageWithMapField < ::Protobuf::Message + map :int32, :string, :map, 1 + end + + class MessageWithoutMapField < ::Protobuf::Message + class MapEntry < ::Protobuf::Message + optional :int32, :key, 1 + optional :string, :value, 2 + end + repeated MapEntry, :map, 1 + end + + map_msg = MessageWithMapField.new(:map => + { + 1 => 'one', + 2 => 'two', + 3 => 'three', + 4 => 'four', + }) + mapless_msg = MessageWithoutMapField.new(:map => + [{ :key => 1, :value => 'one' }, + { :key => 2, :value => 'two' }, + { :key => 3, :value => 'three' }, + { :key => 4, :value => 'four' }, + ]) + + map_bytes = map_msg.encode + mapless_bytes = mapless_msg.encode + expect(map_bytes).to eq(mapless_bytes) + + expect(MessageWithMapField.decode(mapless_bytes)).to eq(map_msg) + expect(MessageWithoutMapField.decode(map_bytes)).to eq(mapless_msg) + end + end + + describe '.[]=' do + context 'clearing fields' do + it 'clears repeated fields with an empty array' do + instance = ::Test::Resource.new(:repeated_enum => [::Test::StatusType::ENABLED]) + expect(instance.field?(:repeated_enum)).to be(true) + instance[:repeated_enum] = [] + expect(instance.field?(:repeated_enum)).to be(false) + end + + it 'clears optional fields with nil' do + instance = ::Test::Resource.new(:name => "Joe") + expect(instance.field?(:name)).to be(true) + instance[:name] = nil + expect(instance.field?(:name)).to be(false) + end + + it 'clears optional extenstion fields with nil' do + instance = ::Test::Resource.new(:ext_is_searchable => true) + expect(instance.field?(:ext_is_searchable)).to be(true) + instance[:ext_is_searchable] = nil + expect(instance.field?(:ext_is_searchable)).to be(false) + end + end + + context 'setting fields' do + let(:instance) { ::Test::Resource.new } + + it 'sets and replaces repeated fields' do + initial = [::Test::StatusType::ENABLED, ::Test::StatusType::DISABLED] + instance[:repeated_enum] = initial + expect(instance[:repeated_enum]).to eq(initial) + replacement = [::Test::StatusType::DELETED] + instance[:repeated_enum] = replacement + expect(instance[:repeated_enum]).to eq(replacement) + end + + it 'sets acceptable optional field values' do + instance[:name] = "Joe" + expect(instance[:name]).to eq("Joe") + instance[1] = "Tom" + expect(instance[:name]).to eq("Tom") + end + + it 'sets acceptable empty string field values' do + instance[:name] = "" + expect(instance.name!).to eq("") + end + + it 'sets acceptable empty message field values' do + instance = ::Test::Nested.new + instance[:resource] = {} + expect(instance.resource!).to eq(::Test::Resource.new) + end + + it 'sets acceptable extension field values' do + instance[:ext_is_searchable] = true + expect(instance[:ext_is_searchable]).to eq(true) + instance[:".test.Searchable.ext_is_searchable"] = false + expect(instance[:ext_is_searchable]).to eq(false) + instance[100] = true + expect(instance[:ext_is_searchable]).to eq(true) + end + + # to be deprecated + it 'does nothing when sent an empty array' do + instance[:repeated_enum] = nil + expect(instance[:repeated_enum]).to eq([]) + instance[:repeated_enum] = [1, 2] + expect(instance[:repeated_enum]).to eq([1, 2]) + instance[:repeated_enum] = nil + # Yes this is very silly, but backwards compatible + expect(instance[:repeated_enum]).to eq([1, 2]) + end + end + + context 'throwing TypeError' do + let(:instance) { ::Test::Resource.new } + + it 'throws when a repeated value is set with a non array' do + expect { instance[:repeated_enum] = "string" }.to raise_error(TypeError) + end + + it 'throws when a repeated value is set with an array of the wrong type' do + expect { instance[:repeated_enum] = [true, false] }.to raise_error(TypeError) + end + + it 'throws when an optional value is not #acceptable?' do + expect { instance[:name] = 1 }.to raise_error(TypeError) + end + end + + context 'ignoring unknown fields' do + around do |example| + orig = ::Protobuf.ignore_unknown_fields? + ::Protobuf.ignore_unknown_fields = true + example.call + ::Protobuf.ignore_unknown_fields = orig + end + + context 'with valid fields' do + let(:values) { { :name => "Jim" } } + + it "does not raise an error" do + expect { ::Test::Resource.new(values) }.to_not raise_error + end + end + + context 'with non-existent field' do + let(:values) { { :name => "Jim", :othername => "invalid" } } + + it "does not raise an error" do + expect { ::Test::Resource.new(values) }.to_not raise_error + end + end + end + + context 'not ignoring unknown fields' do + around do |example| + orig = ::Protobuf.ignore_unknown_fields? + ::Protobuf.ignore_unknown_fields = false + example.call + ::Protobuf.ignore_unknown_fields = orig + end + + context 'with valid fields' do + let(:values) { { :name => "Jim" } } + + it "does not raise an error" do + expect { ::Test::Resource.new(values) }.to_not raise_error + end + end + + context 'with non-existent field' do + let(:values) { { :name => "Jim", :othername => "invalid" } } + + it "raises an error and mentions the erroneous field" do + expect { ::Test::Resource.new(values) }.to raise_error(::Protobuf::FieldNotDefinedError, /othername/) + end + + context 'with a nil value' do + let(:values) { { :name => "Jim", :othername => nil } } + + it "raises an error and mentions the erroneous field" do + expect { ::Test::Resource.new(values) }.to raise_error(::Protobuf::FieldNotDefinedError, /othername/) + end + end + end + end + end +end diff --git a/spec/lib/protobuf/optionable_spec.rb b/spec/lib/protobuf/optionable_spec.rb new file mode 100644 index 00000000..c7480c03 --- /dev/null +++ b/spec/lib/protobuf/optionable_spec.rb @@ -0,0 +1,265 @@ +require 'spec_helper' +require 'protobuf/optionable' +require 'protobuf/field/message_field' +require PROTOS_PATH.join('resource.pb') + +RSpec.describe 'Optionable' do + describe '.{get,get!}_option' do + before do + stub_const("OptionableGetOptionTest", Class.new(::Protobuf::Message) do + set_option :deprecated, true + set_option :".package.message_field", :field => 33 + + optional :int32, :field, 1 + end) + end + + it '.get_option retrieves the option as a symbol' do + expect(OptionableGetOptionTest.get_option(:deprecated)).to be(true) + end + + it '.get_option returns the default value for unset options' do + expect(OptionableGetOptionTest.get_option(:message_set_wire_format)).to be(false) + end + + it '.get_option retrieves the option as a string' do + expect(OptionableGetOptionTest.get_option('deprecated')).to be(true) + end + + it '.get_option errors if the option does not exist' do + expect { OptionableGetOptionTest.get_option(:baz) }.to raise_error(ArgumentError) + end + + it '.get_option errors if the option is not accessed by its fully qualified name' do + message_field = ::Protobuf::Field::MessageField.new( + OptionableGetOptionTest, :optional, OptionableGetOptionTest, '.package.message_field', 2, '.message_field', {}) + allow(::Google::Protobuf::MessageOptions).to receive(:get_field).and_return(message_field) + expect { OptionableGetOptionTest.get_option(message_field.name) }.to raise_error(ArgumentError) + end + + it '.get_option can return an option representing a message' do + message_field = ::Protobuf::Field::MessageField.new( + OptionableGetOptionTest, :optional, OptionableGetOptionTest, '.package.message_field', 2, 'message_field', {}) + allow(::Google::Protobuf::MessageOptions).to receive(:get_field).and_return(message_field) + expect(OptionableGetOptionTest.get_option(message_field.fully_qualified_name)).to eq(OptionableGetOptionTest.new(:field => 33)) + end + + it '.get_option! retrieves explicitly an set option' do + expect(OptionableGetOptionTest.get_option!(:deprecated)).to be(true) + end + + it '.get_option! returns nil for unset options' do + expect(OptionableGetOptionTest.get_option!(:message_set_wire_format)).to be(nil) + end + + it '.get_option! errors if the option does not exist' do + expect { OptionableGetOptionTest.get_option(:baz) }.to raise_error(ArgumentError) + end + + it '#get_option retrieves the option as a symbol' do + expect(OptionableGetOptionTest.new.get_option(:deprecated)).to be(true) + end + + it '#get_option returns the default value for unset options' do + expect(OptionableGetOptionTest.new.get_option(:message_set_wire_format)).to be(false) + end + + it '#get_option retrieves the option as a string' do + expect(OptionableGetOptionTest.new.get_option('deprecated')).to be(true) + end + + it '#get_option errors if the option is not accessed by its fully qualified name' do + message_field = ::Protobuf::Field::MessageField.new( + OptionableGetOptionTest, :optional, OptionableGetOptionTest, '.package.message_field', 2, 'message_field', {}) + allow(::Google::Protobuf::MessageOptions).to receive(:get_field).and_return(message_field) + expect { OptionableGetOptionTest.new.get_option(message_field.name) }.to raise_error(ArgumentError) + end + + it '#get_option can return an option representing a message' do + message_field = ::Protobuf::Field::MessageField.new( + OptionableGetOptionTest, :optional, OptionableGetOptionTest, '.package.message_field', 2, 'message_field', {}) + allow(::Google::Protobuf::MessageOptions).to receive(:get_field).and_return(message_field) + expect(OptionableGetOptionTest.new.get_option(message_field.fully_qualified_name)).to eq(OptionableGetOptionTest.new(:field => 33)) + end + + it '#get_option errors if the option does not exist' do + expect { OptionableGetOptionTest.new.get_option(:baz) }.to raise_error(ArgumentError) + end + + it '#get_option! retrieves explicitly an set option' do + expect(OptionableGetOptionTest.new.get_option!(:deprecated)).to be(true) + end + + it '#get_option! returns nil for unset options' do + expect(OptionableGetOptionTest.new.get_option!(:message_set_wire_format)).to be(nil) + end + + it '#get_option! errors if the option does not exist' do + expect { OptionableGetOptionTest.new.get_option(:baz) }.to raise_error(ArgumentError) + end + end + + describe '.inject' do + let(:klass) { Class.new } + + it 'adds klass.{set,get}_option' do + expect { klass.get_option(:deprecated) }.to raise_error(NoMethodError) + expect { klass.__send__(:set_option, :deprecated, true) }.to raise_error(NoMethodError) + ::Protobuf::Optionable.inject(klass) { ::Google::Protobuf::MessageOptions } + expect(klass.get_option(:deprecated)).to eq(false) + expect { klass.set_option(:deprecated, true) }.to raise_error(NoMethodError) + klass.__send__(:set_option, :deprecated, true) + expect(klass.get_option(:deprecated)).to eq(true) + end + + it 'adds klass#get_option' do + expect { klass.new.get_option(:deprecated) }.to raise_error(NoMethodError) + ::Protobuf::Optionable.inject(klass) { ::Google::Protobuf::MessageOptions } + expect(klass.new.get_option(:deprecated)).to eq(false) + end + + it 'adds klass.optionable_descriptor_class' do + expect { klass.optionable_descriptor_class }.to raise_error(NoMethodError) + ::Protobuf::Optionable.inject(klass) { ::Google::Protobuf::MessageOptions } + expect(klass.optionable_descriptor_class).to eq(::Google::Protobuf::MessageOptions) + end + + it 'does not add klass.optionable_descriptor_class twice' do + expect(klass).to receive(:define_singleton_method).once + ::Protobuf::Optionable.inject(klass) { ::Google::Protobuf::MessageOptions } + klass.instance_eval do + def optionable_descriptor_class + ::Google::Protobuf::MessageOptions + end + end + ::Protobuf::Optionable.inject(klass) { ::Google::Protobuf::MessageOptions } + end + + it 'throws error when klass.optionable_descriptor_class defined twice with different args' do + ::Protobuf::Optionable.inject(klass) { ::Google::Protobuf::MessageOptions } + expect { ::Protobuf::Optionable.inject(klass) { ::Google::Protobuf::FileOptions } } + .to raise_error('A class is being defined with two different descriptor classes, something is very wrong') + end + + context 'extend_class = false' do + let(:object) { klass.new } + it 'adds object.{get,set}_option' do + expect { object.get_option(:deprecated) }.to raise_error(NoMethodError) + expect { object.__send__(:set_option, :deprecated, true) }.to raise_error(NoMethodError) + ::Protobuf::Optionable.inject(klass, false) { ::Google::Protobuf::MessageOptions } + expect(object.get_option(:deprecated)).to eq(false) + expect { object.set_option(:deprecated, true) }.to raise_error(NoMethodError) + object.__send__(:set_option, :deprecated, true) + expect(object.get_option(:deprecated)).to eq(true) + end + + it 'does not add klass.{get,set}_option' do + expect { object.get_option(:deprecated) }.to raise_error(NoMethodError) + ::Protobuf::Optionable.inject(klass, false) { ::Google::Protobuf::MessageOptions } + expect { klass.get_option(:deprecated) }.to raise_error(NoMethodError) + expect { klass.__send__(:set_option, :deprecated) }.to raise_error(NoMethodError) + end + + it 'creates an instance method optionable_descriptor_class' do + expect { object.optionable_descriptor_class }.to raise_error(NoMethodError) + ::Protobuf::Optionable.inject(klass, false) { ::Google::Protobuf::MessageOptions } + expect(object.optionable_descriptor_class).to eq(::Google::Protobuf::MessageOptions) + end + end + end + + describe 'getting options from generated code' do + context 'file options' do + it 'gets base options' do + expect(::Test.get_option!(:cc_generic_services)).to eq(true) + end + + it 'gets unset options' do + expect(::Test.get_option!(:java_multiple_files)).to eq(nil) + expect(::Test.get_option(:java_multiple_files)).to eq(false) + end + + it 'gets custom options' do + expect(::Test.get_option!(:".test.file_option")).to eq(9876543210) + end + end + + context 'field options' do + subject { ::Test::Resource.fields[0] } + + it 'gets base options' do + expect(subject.get_option!(:ctype)) + .to eq(::Google::Protobuf::FieldOptions::CType::CORD) + end + + it 'gets unset options' do + expect(subject.get_option!(:lazy)).to eq(nil) + expect(subject.get_option(:lazy)).to eq(false) + end + + it 'gets custom options' do + expect(subject.get_option!(:".test.field_option")).to eq(8765432109) + end + end + + context 'enum options' do + subject { ::Test::StatusType } + + it 'gets base options' do + expect(subject.get_option!(:allow_alias)).to eq(true) + end + + it 'gets unset options' do + expect(subject.get_option!(:deprecated)).to eq(nil) + expect(subject.get_option(:deprecated)).to eq(false) + end + + it 'gets custom options' do + expect(subject.get_option!(:".test.enum_option")).to eq(-789) + end + end + + context 'message options' do + subject { ::Test::Resource } + + it 'gets base options' do + expect(subject.get_option!(:map_entry)).to eq(false) + end + + it 'gets unset options' do + expect(subject.get_option!(:deprecated)).to eq(nil) + expect(subject.get_option(:deprecated)).to eq(false) + end + + it 'gets custom options' do + expect(subject.get_option!(:".test.message_option")).to eq(-56) + end + end + + context 'service options' do + subject { ::Test::ResourceService } + + it 'gets unset options' do + expect(subject.get_option!(:deprecated)).to eq(nil) + expect(subject.get_option(:deprecated)).to eq(false) + end + + it 'gets custom options' do + expect(subject.get_option!(:".test.service_option")).to eq(-9876543210) + end + end + + context 'method options' do + subject { ::Test::ResourceService.rpcs[:find] } + + it 'gets unset options' do + expect(subject.get_option!(:deprecated)).to eq(nil) + expect(subject.get_option(:deprecated)).to eq(false) + end + + it 'gets custom options' do + expect(subject.get_option!(:".test.method_option")).to eq(2) + end + end + end +end diff --git a/spec/lib/protobuf/rpc/client_spec.rb b/spec/lib/protobuf/rpc/client_spec.rb new file mode 100644 index 00000000..b7d67004 --- /dev/null +++ b/spec/lib/protobuf/rpc/client_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' +require SUPPORT_PATH.join('resource_service') + +RSpec.describe Protobuf::Rpc::Client do + before(:each) do + load 'protobuf/socket.rb' + end + + context 'when creating a client from a service' do + before { reset_service_location(Test::ResourceService) } + + it 'should be able to get a client through the Service#client helper method' do + expect(::Test::ResourceService.client(:port => 9191)).to eq(Protobuf::Rpc::Client.new(:service => Test::ResourceService, :port => 9191)) + end + + it "should be able to override a service location's host and port" do + Test::ResourceService.located_at 'somewheregreat.com:12345' + clean_client = Test::ResourceService.client + expect(clean_client.options[:host]).to eq('somewheregreat.com') + expect(clean_client.options[:port]).to eq(12345) + + updated_client = Test::ResourceService.client(:host => 'amazing.com', :port => 54321) + expect(updated_client.options[:host]).to eq('amazing.com') + expect(updated_client.options[:port]).to eq(54321) + end + + it 'should be able to define which service to create itself for' do + client = Protobuf::Rpc::Client.new :service => Test::ResourceService + expect(client.options[:service]).to eq(Test::ResourceService) + end + + it 'should have a hard default for host and port on a service that has not been configured' do + client = Test::ResourceService.client + expect(client.options[:host]).to eq(Protobuf::Rpc::Service::DEFAULT_HOST) + expect(client.options[:port]).to eq(Protobuf::Rpc::Service::DEFAULT_PORT) + end + + end + + context 'when calling methods on a service client' do + + # NOTE: we are assuming the service methods are accurately + # defined inside spec/proto/test_service.rb, + # namely the :find method + + it 'should respond to defined service methods' do + client = Test::ResourceService.client + expect(client).to receive(:send_request).and_return(nil) + expect { client.find(nil) }.to_not raise_error + end + end + + context 'when receiving request objects' do + + it 'should be able to create the correct request object if passed a hash' do + client = Test::ResourceService.client + expect(client).to receive(:send_request) + client.find(:name => 'Test Name', :active => false) + expect(client.options[:request]).to be_a(Test::ResourceFindRequest) + expect(client.options[:request].name).to eq('Test Name') + expect(client.options[:request].active).to eq(false) + end + + end + +end diff --git a/spec/lib/protobuf/rpc/connectors/base_spec.rb b/spec/lib/protobuf/rpc/connectors/base_spec.rb new file mode 100644 index 00000000..32b8cd8b --- /dev/null +++ b/spec/lib/protobuf/rpc/connectors/base_spec.rb @@ -0,0 +1,226 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Rpc::Connectors::Base do + + let(:options) do + { :timeout => 60 } + end + + subject { Protobuf::Rpc::Connectors::Base.new(options) } + + context "API" do + specify { expect(subject.respond_to?(:any_callbacks?)).to be true } + specify { expect(subject.respond_to?(:request_caller)).to be true } + specify { expect(subject.respond_to?(:data_callback)).to be true } + specify { expect(subject.respond_to?(:error)).to be true } + specify { expect(subject.respond_to?(:failure)).to be true } + specify { expect(subject.respond_to?(:complete)).to be true } + specify { expect(subject.respond_to?(:parse_response)).to be true } + specify { expect(subject.respond_to?(:verify_options!)).to be true } + specify { expect(subject.respond_to?(:verify_callbacks)).to be true } + end + + describe "#parse_response" do + let(:options) { { :response_type => Test::Resource, :port => 55589, :host => '127.3.4.5' } } + it "updates stats#server from the response" do + allow(subject).to receive(:close_connection) + subject.instance_variable_set(:@response_data, ::Protobuf::Socketrpc::Response.new(:server => "serverless").encode) + subject.initialize_stats + subject.parse_response + expect(subject.stats.server).to eq("serverless") + end + it "does not override stats#server when response.server is missing" do + allow(subject).to receive(:close_connection) + subject.instance_variable_set(:@response_data, ::Protobuf::Socketrpc::Response.new.encode) + subject.initialize_stats + subject.parse_response + expect(subject.stats.server).to eq("127.3.4.5:55589") + end + it "does not override stats#server when response.server is nil" do + allow(subject).to receive(:close_connection) + subject.instance_variable_set(:@response_data, ::Protobuf::Socketrpc::Response.new(:server => nil).encode) + subject.initialize_stats + subject.parse_response + expect(subject.stats.server).to eq("127.3.4.5:55589") + end + end + + describe "#any_callbacks?" do + [:@complete_cb, :@success_cb, :@failure_cb].each do |cb| + it "returns true if #{cb} is provided" do + subject.instance_variable_set(cb, "something") + expect(subject.any_callbacks?).to be true + end + end + + it "returns false when all callbacks are not provided" do + subject.instance_variable_set(:@complete_cb, nil) + subject.instance_variable_set(:@success_cb, nil) + subject.instance_variable_set(:@failure_cb, nil) + + expect(subject.any_callbacks?).to be false + end + end + + describe "#data_callback" do + it "changes state to use the data callback" do + subject.data_callback("data") + expect(subject.instance_variable_get(:@used_data_callback)).to be true + end + + it "sets the data var when using the data_callback" do + subject.data_callback("data") + expect(subject.instance_variable_get(:@data)).to eq("data") + end + end + + describe "#send_request" do + it "raising an error when 'send_request' is not overridden" do + expect { subject.send_request }.to raise_error(RuntimeError, /inherit a Connector/) + end + + it "does not raise error when 'send_request' is overridden" do + new_sub = Class.new(subject.class) { def send_request; end }.new(options) + expect { new_sub.send_request }.to_not raise_error + end + end + + describe '.new' do + it 'assigns passed options and initializes success/failure callbacks' do + expect(subject.options).to eq(Protobuf::Rpc::Connectors::DEFAULT_OPTIONS.merge(options)) + expect(subject.success_cb).to be_nil + expect(subject.failure_cb).to be_nil + end + end + + describe '#success_cb' do + it 'allows setting the success callback and calling it' do + expect(subject.success_cb).to be_nil + cb = proc { |res| fail res } + subject.success_cb = cb + expect(subject.success_cb).to eq(cb) + expect { subject.success_cb.call('an error from cb') }.to raise_error 'an error from cb' + end + end + + describe '#failure_cb' do + it 'allows setting the failure callback and calling it' do + expect(subject.failure_cb).to be_nil + cb = proc { |res| fail res } + subject.failure_cb = cb + expect(subject.failure_cb).to eq(cb) + expect { subject.failure_cb.call('an error from cb') }.to raise_error 'an error from cb' + end + end + + describe '#request_bytes' do + let(:service) { Test::ResourceService } + let(:method) { :find } + let(:request) { '' } + let(:client_host) { 'myhost.myservice.com' } + let(:options) do + { + :service => service, + :method => method, + :request => request, + :client_host => client_host, + :timeout => 60, + } + end + + let(:expected) do + ::Protobuf::Socketrpc::Request.new( + :service_name => service.name, + :method_name => 'find', + :request_proto => '', + :caller => client_host, + ) + end + + before { allow(subject).to receive(:validate_request_type!).and_return(true) } + before { expect(subject).not_to receive(:failure) } + + specify { expect(subject.request_bytes).to eq expected.encode } + end + + describe '#request_caller' do + specify { expect(subject.request_caller).to eq ::Protobuf.client_host } + + context 'when "client_host" option is given to initializer' do + let(:hostname) { 'myhost.myserver.com' } + let(:options) { { :client_host => hostname, :timeout => 60 } } + + specify { expect(subject.request_caller).to_not eq ::Protobuf.client_host } + specify { expect(subject.request_caller).to eq hostname } + end + end + + describe "#verify_callbacks" do + it "sets @failure_cb to #data_callback when no callbacks are defined" do + subject.verify_callbacks + expect(subject.instance_variable_get(:@failure_cb)).to eq(subject.method(:data_callback)) + end + + it "sets @success_cb to #data_callback when no callbacks are defined" do + subject.verify_callbacks + expect(subject.instance_variable_get(:@success_cb)).to eq(subject.method(:data_callback)) + end + + it "doesn't set @failure_cb when already defined" do + set_cb = -> { true } + subject.instance_variable_set(:@failure_cb, set_cb) + subject.verify_callbacks + expect(subject.instance_variable_get(:@failure_cb)).to eq(set_cb) + expect(subject.instance_variable_get(:@failure_cb)).to_not eq(subject.method(:data_callback)) + end + + it "doesn't set @success_cb when already defined" do + set_cb = -> { true } + subject.instance_variable_set(:@success_cb, set_cb) + subject.verify_callbacks + expect(subject.instance_variable_get(:@success_cb)).to eq(set_cb) + expect(subject.instance_variable_get(:@success_cb)).to_not eq(subject.method(:data_callback)) + end + + end + + shared_examples "a ConnectorDisposition" do |meth, cb, *args| + + it "calls #complete before exit" do + subject.stats = ::Protobuf::Rpc::Stat.new(:stop => true) + + expect(subject).to receive(:complete) + subject.method(meth).call(*args) + end + + it "calls the #{cb} callback when provided" do + stats = ::Protobuf::Rpc::Stat.new + allow(stats).to receive(:stop).and_return(true) + subject.stats = stats + some_cb = double("Object") + + subject.instance_variable_set("@#{cb}", some_cb) + expect(some_cb).to receive(:call).and_return(true) + subject.method(meth).call(*args) + end + + it "calls the complete callback when provided" do + stats = ::Protobuf::Rpc::Stat.new + allow(stats).to receive(:stop).and_return(true) + subject.stats = stats + comp_cb = double("Object") + + subject.instance_variable_set(:@complete_cb, comp_cb) + expect(comp_cb).to receive(:call).and_return(true) + subject.method(meth).call(*args) + end + + end + + it_behaves_like("a ConnectorDisposition", :failure, "failure_cb", :RPC_ERROR, "message") + it_behaves_like("a ConnectorDisposition", :failure, "complete_cb", :RPC_ERROR, "message") + it_behaves_like("a ConnectorDisposition", :succeed, "complete_cb", "response") + it_behaves_like("a ConnectorDisposition", :succeed, "success_cb", "response") + it_behaves_like("a ConnectorDisposition", :complete, "complete_cb") + +end diff --git a/spec/lib/protobuf/rpc/connectors/ping_spec.rb b/spec/lib/protobuf/rpc/connectors/ping_spec.rb new file mode 100644 index 00000000..35931f61 --- /dev/null +++ b/spec/lib/protobuf/rpc/connectors/ping_spec.rb @@ -0,0 +1,69 @@ +require "spec_helper" +require "protobuf/zmq" + +::RSpec.describe ::Protobuf::Rpc::Connectors::Ping do + subject { described_class.new("google.com", 80) } + + let(:host) { "google.com" } + let(:port) { 80 } + + describe ".new" do + it "assigns host" do + expect(subject.host).to eq(host) + end + + it "assigns port" do + expect(subject.port).to eq(port) + end + end + + describe "#online?" do + it "closes the socket" do + socket = double(:close => nil, :setsockopt => nil) + allow(subject).to receive(:tcp_socket).and_return(socket) + expect(socket).to receive(:close) + expect(subject).to be_online + end + + context "when a socket can connect" do + let(:socket) { double(:close => nil, :setsockopt => nil) } + before { allow(subject).to receive(:tcp_socket).and_return(socket) } + + it "returns true" do + expect(subject).to be_online + end + end + + context "when a socket error is raised" do + before { allow(subject).to receive(:tcp_socket).and_raise(::Errno::ECONNREFUSED) } + + it "returns false" do + expect(subject).to_not be_online + end + end + + context "when a select timeout is fired" do + let(:wait_writable_class) { ::Class.new(StandardError) { include ::IO::WaitWritable } } + before { expect_any_instance_of(::Socket).to receive(:connect_nonblock).and_raise(wait_writable_class) } + + it "returns false" do + expect(::IO).to receive(:select).and_return(false) + expect(subject).to_not be_online + end + end + end + + describe "#timeout" do + it "uses the default value" do + expect(subject.timeout).to eq(0.2) + end + + context "when environment variable is set" do + before { ::ENV["PB_RPC_PING_PORT_TIMEOUT"] = "100" } + + it "uses the environmet variable" do + expect(subject.timeout).to eq(0.1) + end + end + end +end diff --git a/spec/lib/protobuf/rpc/connectors/socket_spec.rb b/spec/lib/protobuf/rpc/connectors/socket_spec.rb new file mode 100644 index 00000000..1d0d3187 --- /dev/null +++ b/spec/lib/protobuf/rpc/connectors/socket_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'protobuf/socket' + +RSpec.shared_examples "a Protobuf Connector" do + subject { described_class.new({}) } + + context "API" do + # Check the API + specify { expect(subject.respond_to?(:send_request, true)).to be true } + specify { expect(subject.respond_to?(:post_init, true)).to be true } + specify { expect(subject.respond_to?(:close_connection, true)).to be true } + specify { expect(subject.respond_to?(:error?, true)).to be true } + end +end + +RSpec.describe Protobuf::Rpc::Connectors::Socket do + subject { described_class.new({}) } + + it_behaves_like "a Protobuf Connector" + + context "#read_response" do + let(:data) { "New data" } + + it "fills the buffer with data from the socket" do + socket = StringIO.new("#{data.bytesize}-#{data}") + subject.instance_variable_set(:@socket, socket) + subject.instance_variable_set(:@stats, OpenStruct.new) + expect(subject).to receive(:parse_response).and_return(true) + + subject.__send__(:read_response) + expect(subject.instance_variable_get(:@response_data)).to eq(data) + end + end +end diff --git a/spec/lib/protobuf/rpc/connectors/zmq_spec.rb b/spec/lib/protobuf/rpc/connectors/zmq_spec.rb new file mode 100644 index 00000000..7bf73335 --- /dev/null +++ b/spec/lib/protobuf/rpc/connectors/zmq_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' +require 'protobuf/zmq' + +RSpec.describe ::Protobuf::Rpc::Connectors::Zmq do + subject { described_class.new(options) } + + let(:options) do + { + :service => "Test::Service", + :method => "find", + :timeout => 3, + :host => "127.0.0.1", + :port => "9400", + } + end + + let(:socket_double) { double(::ZMQ::Socket, :connect => 0) } + let(:zmq_context_double) { double(::ZMQ::Context, :socket => socket_double) } + + before do + allow(::ZMQ::Context).to receive(:new).and_return(zmq_context_double) + allow(socket_double).to receive(:setsockopt) + end + + before(:all) do + @ping_port_before = ENV['PB_RPC_PING_PORT'] + end + + after(:all) do + ENV['PB_RPC_PING_PORT'] = @ping_port_before + end + + describe "#lookup_server_uri" do + let(:service_directory) { double('ServiceDirectory', :running? => running?) } + let(:listing) { double('Listing', :address => '127.0.0.2', :port => 9399) } + let(:listings) { [listing] } + let(:running?) { true } + + before { allow(subject).to receive(:service_directory).and_return(service_directory) } + + context "when the service directory is running" do + it "searches the service directory" do + allow(service_directory).to receive(:all_listings_for).and_return(listings) + expect(subject.send(:lookup_server_uri)).to eq "tcp://127.0.0.2:9399" + end + + it "defaults to the options" do + allow(service_directory).to receive(:all_listings_for).and_return([]) + expect(subject.send(:lookup_server_uri)).to eq "tcp://127.0.0.1:9400" + end + end + + context "when the service directory is not running" do + let(:running?) { false } + + it "defaults to the options" do + allow(service_directory).to receive(:all_listings_for).and_return([]) + expect(subject.send(:lookup_server_uri)).to eq "tcp://127.0.0.1:9400" + end + end + + it "checks if the server is alive" do + allow(service_directory).to receive(:all_listings_for).and_return([]) + expect(subject).to receive(:host_alive?).with("127.0.0.1") { true } + expect(subject.send(:lookup_server_uri)).to eq "tcp://127.0.0.1:9400" + end + + context "when no host is alive" do + it "raises an error" do + allow(service_directory).to receive(:all_listings_for).and_return(listings) + allow(subject).to receive(:host_alive?).and_return(false) + expect { subject.send(:lookup_server_uri) }.to raise_error(RuntimeError) + end + end + + end + + describe "#host_alive?" do + context "when the PB_RPC_PING_PORT is not set" do + before do + ENV.delete("PB_RPC_PING_PORT") + end + + it "returns true" do + expect(subject.send(:host_alive?, "yip.yip")).to be true + end + + it "does not attempt a connection" do + expect(TCPSocket).not_to receive(:new) + subject.send(:host_alive?, "blargh.com") + end + end + + context "when the PB_RPC_PING_PORT is set" do + before do + ::ENV["PB_RPC_PING_PORT"] = "3307" + end + + it "returns true when the connection succeeds" do + allow_any_instance_of(::Protobuf::Rpc::Connectors::Ping).to receive(:online?).and_return(true) + expect(subject.send(:host_alive?, "huzzah1.com")).to eq(true) + end + + it "returns false when the connection fails" do + allow_any_instance_of(::Protobuf::Rpc::Connectors::Ping).to receive(:online?).and_return(false) + expect(subject.send(:host_alive?, "huzzah2.com")).to eq(false) + end + end + end +end diff --git a/spec/lib/protobuf/rpc/middleware/exception_handler_spec.rb b/spec/lib/protobuf/rpc/middleware/exception_handler_spec.rb new file mode 100644 index 00000000..01248f40 --- /dev/null +++ b/spec/lib/protobuf/rpc/middleware/exception_handler_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Rpc::Middleware::ExceptionHandler do + let(:app) { proc { |env| env } } + let(:env) { Protobuf::Rpc::Env.new("server" => "cooldude") } + + subject { described_class.new(app) } + + describe "#call" do + it "calls the stack" do + expect(app).to receive(:call).with(env) + subject.call(env) + end + + it "returns the env" do + expect(subject.call(env)).to eq env + end + + context "when exceptions occur" do + let(:encoded_error) { error.encode(:server => "cooldude") } + let(:error) { Protobuf::Rpc::MethodNotFound.new('Boom!') } + + before { allow(app).to receive(:call).and_raise(error, 'Boom!') } + + it "rescues exceptions" do + expect { subject.call(env) }.not_to raise_exception + end + + context "when exception is a Protobuf error" do + it "does not wrap the exception in a generic Protobuf error" do + stack_env = subject.call(env) + + # Can't compare the error instances because the response has been + # raised and thus has a backtrace while the error does not. + expect(stack_env.response.class).to eq error.class + end + + it "encodes the response" do + stack_env = subject.call(env) + expect(stack_env.encoded_response).to eq encoded_error + end + end + + context "when exception is not a Protobuf error" do + let(:encoded_error) { error.encode(:server => "cooldude") } + let(:error) { Protobuf::Rpc::RpcFailed.new('Boom!') } + + before { allow(app).to receive(:call).and_raise(RuntimeError, 'Boom!') } + + it "wraps the exception in a generic Protobuf error" do + stack_env = subject.call(env) + expect(stack_env.response).to eq error + end + + it "encodes the wrapped exception" do + stack_env = subject.call(env) + expect(stack_env.encoded_response).to eq encoded_error + end + end + end + end +end diff --git a/spec/lib/protobuf/rpc/middleware/logger_spec.rb b/spec/lib/protobuf/rpc/middleware/logger_spec.rb new file mode 100644 index 00000000..c30c9c19 --- /dev/null +++ b/spec/lib/protobuf/rpc/middleware/logger_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Rpc::Middleware::Logger do + let(:app) { proc { |inner_env| inner_env } } + let(:env) do + Protobuf::Rpc::Env.new( + 'client_host' => 'client_host.test.co', + 'encoded_request' => request_wrapper.encode, + 'encoded_response' => response_wrapper.encode, + 'method_name' => method_name, + 'request' => request, + 'request_type' => rpc_method.request_type, + 'response' => response, + 'response_type' => rpc_method.response_type, + 'rpc_method' => rpc_method, + 'rpc_service' => service_class, + 'service_name' => service_name, + ) + end + let(:method_name) { :find } + let(:request) { request_type.new(:name => 'required') } + let(:request_type) { rpc_method.request_type } + let(:request_wrapper) do + ::Protobuf::Socketrpc::Request.new( + :service_name => service_name, + :method_name => method_name.to_s, + :request_proto => request, + ) + end + let(:response_wrapper) { ::Protobuf::Socketrpc::Response.new(:response_proto => response) } + let(:response) { rpc_method.response_type.new(:name => 'required') } + let(:rpc_method) { service_class.rpcs[method_name] } + let(:rpc_service) { service_class.new(env) } + let(:service_class) { Test::ResourceService } + let(:service_name) { service_class.to_s } + + subject { described_class.new(app) } + + describe "#call" do + it "calls the stack" do + expect(app).to receive(:call).with(env).and_return(env) + subject.call(env) + end + + it "returns the env" do + expect(subject.call(env)).to eq env + end + end +end diff --git a/spec/lib/protobuf/rpc/middleware/request_decoder_spec.rb b/spec/lib/protobuf/rpc/middleware/request_decoder_spec.rb new file mode 100644 index 00000000..f58be4fc --- /dev/null +++ b/spec/lib/protobuf/rpc/middleware/request_decoder_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Rpc::Middleware::RequestDecoder do + let(:app) { proc { |env| env } } + let(:client_host) { 'client_host.test.co' } + let(:env) do + Protobuf::Rpc::Env.new( + 'encoded_request' => encoded_request, + 'log_signature' => 'log_signature', + ) + end + let(:encoded_request) { request_wrapper.encode } + let(:method_name) { :find } + let(:request) { request_type.new(:name => 'required') } + let(:request_type) { rpc_method.request_type } + let(:request_wrapper) do + ::Protobuf::Socketrpc::Request.new( + :caller => client_host, + :service_name => service_name, + :method_name => method_name.to_s, + :request_proto => request, + ) + end + let(:response_type) { rpc_method.response_type } + let(:rpc_method) { rpc_service.rpcs[method_name] } + let(:rpc_service) { Test::ResourceService } + let(:service_name) { rpc_service.to_s } + + subject { described_class.new(app) } + + describe "#call" do + it "decodes the request" do + stack_env = subject.call(env) + expect(stack_env.request).to eq request + end + + it "calls the stack" do + expect(app).to receive(:call).with(env) + subject.call(env) + end + + it "sets Env#client_host" do + stack_env = subject.call(env) + expect(stack_env.client_host).to eq client_host + end + + it "sets Env#service_name" do + stack_env = subject.call(env) + expect(stack_env.service_name).to eq service_name + end + + it "sets Env#method_name" do + stack_env = subject.call(env) + expect(stack_env.method_name).to eq method_name.to_sym + end + + it "sets Env#request_type" do + stack_env = subject.call(env) + expect(stack_env.request_type).to eq request_type + end + + it "sets Env#response_type" do + stack_env = subject.call(env) + expect(stack_env.response_type).to eq response_type + end + + it "sets Env#rpc_method" do + stack_env = subject.call(env) + expect(stack_env.rpc_method).to eq rpc_method + end + + it "sets Env#rpc_service" do + stack_env = subject.call(env) + expect(stack_env.rpc_service).to eq rpc_service + end + + context "when decoding fails" do + before { allow(::Protobuf::Socketrpc::Request).to receive(:decode).and_raise(RuntimeError) } + + it "raises a bad request data exception" do + expect { subject.call(env) }.to raise_exception(Protobuf::Rpc::BadRequestData) + end + end + + context "when the RPC service is not defined" do + let(:request_wrapper) do + ::Protobuf::Socketrpc::Request.new( + :caller => client_host, + :service_name => 'NonexistantService', + :method_name => method_name.to_s, + :request_proto => request, + ) + end + + it "raises a bad request data exception" do + expect { subject.call(env) }.to raise_exception(Protobuf::Rpc::ServiceNotFound) + end + end + + context "when RPC method is not defined" do + let(:request_wrapper) do + ::Protobuf::Socketrpc::Request.new( + :caller => client_host, + :service_name => service_name, + :method_name => 'foo', + :request_proto => request, + ) + end + + it "raises a bad request data exception" do + expect { subject.call(env) }.to raise_exception(Protobuf::Rpc::MethodNotFound) + end + end + end +end diff --git a/spec/lib/protobuf/rpc/middleware/response_encoder_spec.rb b/spec/lib/protobuf/rpc/middleware/response_encoder_spec.rb new file mode 100644 index 00000000..0ed47e60 --- /dev/null +++ b/spec/lib/protobuf/rpc/middleware/response_encoder_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' + +RSpec.describe Protobuf::Rpc::Middleware::ResponseEncoder do + let(:app) { proc { |env| env.response = response; env } } + let(:env) do + Protobuf::Rpc::Env.new( + 'response_type' => Test::Resource, + 'log_signature' => 'log_signature', + ) + end + let(:encoded_response) { response_wrapper.encode } + let(:response) { Test::Resource.new(:name => 'required') } + let(:response_wrapper) { ::Protobuf::Socketrpc::Response.new(:response_proto => response) } + + subject { described_class.new(app) } + + describe "#call" do + it "encodes the response" do + stack_env = subject.call(env) + expect(stack_env.encoded_response).to eq encoded_response + end + + it "calls the stack" do + stack_env = subject.call(env) + expect(stack_env.response).to eq response + end + + context "when response is responds to :to_hash" do + let(:app) { proc { |env| env.response = hashable; env } } + let(:hashable) { double('hashable', :to_hash => response.to_hash) } + + it "sets Env#response" do + stack_env = subject.call(env) + expect(stack_env.response).to eq response + end + end + + context "when response is responds to :to_proto" do + let(:app) { proc { |env| env.response = protoable; env } } + let(:protoable) { double('protoable', :to_proto => response) } + + it "sets Env#response" do + stack_env = subject.call(env) + expect(stack_env.response).to eq response + end + end + + context "when response is not a valid response type" do + let(:app) { proc { |env| env.response = "I'm not a valid response"; env } } + + it "raises a bad response proto exception" do + expect { subject.call(env) }.to raise_exception(Protobuf::Rpc::BadResponseProto) + end + end + + context "when response is a Protobuf error" do + let(:app) { proc { |env| env.response = error; env } } + let(:error) { Protobuf::Rpc::RpcError.new } + let(:response_wrapper) { error.to_response } + + it "wraps and encodes the response" do + stack_env = subject.call(env) + expect(stack_env.encoded_response).to eq encoded_response + end + end + + context "when encoding fails" do + before { allow_any_instance_of(::Protobuf::Socketrpc::Response).to receive(:encode).and_raise(RuntimeError) } + + it "raises a bad request data exception" do + expect { subject.call(env) }.to raise_exception(Protobuf::Rpc::PbError) + end + end + + context "when server exists in the env" do + let(:env) do + Protobuf::Rpc::Env.new( + 'response_type' => Test::Resource, + 'log_signature' => 'log_signature', + 'server' => 'itsaserver', + ) + end + + it "adds the servers to the response" do + expected_response = ::Protobuf::Socketrpc::Response.new(:response_proto => response, :server => 'itsaserver').encode + subject.call(env) + expect(env.encoded_response).to eq(expected_response) + end + end + end +end diff --git a/spec/lib/protobuf/rpc/servers/socket_server_spec.rb b/spec/lib/protobuf/rpc/servers/socket_server_spec.rb new file mode 100644 index 00000000..42635351 --- /dev/null +++ b/spec/lib/protobuf/rpc/servers/socket_server_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' +require 'protobuf/rpc/servers/socket_runner' +require 'protobuf/socket' +require SUPPORT_PATH.join('resource_service') + +RSpec.describe Protobuf::Rpc::Socket::Server do + before(:each) do + load 'protobuf/socket.rb' + end + + before(:all) do + load 'protobuf/socket.rb' + Thread.abort_on_exception = true + @options = OpenStruct.new(:host => "127.0.0.1", :port => 9399, :backlog => 100, :threshold => 100) + @runner = ::Protobuf::Rpc::SocketRunner.new(@options) + @server = @runner.instance_variable_get(:@server) + @server_thread = Thread.new(@runner, &:run) + Thread.pass until @server.running? + end + + after(:all) do + @server.stop + @server_thread.join + end + + it "Runner provides a stop method" do + expect(@runner).to respond_to(:stop) + end + + it "provides a stop method" do + expect(@server).to respond_to(:stop) + end + + it "signals the Server is running" do + expect(@server).to be_running + end + +end diff --git a/spec/lib/protobuf/rpc/servers/zmq/server_spec.rb b/spec/lib/protobuf/rpc/servers/zmq/server_spec.rb new file mode 100644 index 00000000..97736064 --- /dev/null +++ b/spec/lib/protobuf/rpc/servers/zmq/server_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require 'protobuf/rpc/servers/zmq/server' + +RSpec.describe Protobuf::Rpc::Zmq::Server do + subject { described_class.new(options) } + + let(:options) do + { + :host => '127.0.0.1', + :port => 9399, + :worker_port => 9400, + :workers_only => true, + } + end + + before do + load 'protobuf/zmq.rb' + end + + after do + subject.teardown + end + + describe '.running?' do + it 'returns true if running' do + subject.instance_variable_set(:@running, true) + expect(subject.running?).to be true + end + + it 'returns false if not running' do + subject.instance_variable_set(:@running, false) + expect(subject.running?).to be false + end + end + + describe '.stop' do + it 'sets running to false' do + subject.instance_variable_set(:@workers, []) + subject.stop + expect(subject.instance_variable_get(:@running)).to be false + end + end +end diff --git a/spec/lib/protobuf/rpc/servers/zmq/util_spec.rb b/spec/lib/protobuf/rpc/servers/zmq/util_spec.rb new file mode 100644 index 00000000..002ac056 --- /dev/null +++ b/spec/lib/protobuf/rpc/servers/zmq/util_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +class UtilTest + include ::Protobuf::Rpc::Zmq::Util +end + +RSpec.describe ::Protobuf::Rpc::Zmq::Util do + before(:each) do + load 'protobuf/zmq.rb' + end + + subject { UtilTest.new } + describe '#zmq_error_check' do + it 'raises when the error code is less than 0' do + expect do + subject.zmq_error_check(-1, :test) + end.to raise_error(/test/) + end + + it 'retrieves the error string from ZeroMQ' do + allow(ZMQ::Util).to receive(:error_string).and_return('an error from zmq') + expect do + subject.zmq_error_check(-1, :test) + end.to raise_error(RuntimeError, /an error from zmq/i) + end + + it 'does nothing if the error code is > 0' do + expect do + subject.zmq_error_check(1, :test) + end.to_not raise_error + end + + it 'does nothing if the error code is == 0' do + expect do + subject.zmq_error_check(0, :test) + end.to_not raise_error + end + end + + describe '#log_signature' do + it 'returns the signature for the log' do + expect(subject.log_signature).to include('server', 'UtilTest') + end + end + + describe '.resolve_ip' do + it 'resolves ips' do + expect(subject.resolve_ip('127.0.0.1')).to eq('127.0.0.1') + end + + it 'resolves non ips' do + expect(subject.resolve_ip('localhost')).to eq('127.0.0.1') + end + end +end diff --git a/spec/lib/protobuf/rpc/servers/zmq/worker_spec.rb b/spec/lib/protobuf/rpc/servers/zmq/worker_spec.rb new file mode 100644 index 00000000..71c89282 --- /dev/null +++ b/spec/lib/protobuf/rpc/servers/zmq/worker_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +RSpec.describe ::Protobuf::Rpc::Zmq::Worker do + before(:each) do + load 'protobuf/zmq.rb' + + fake_socket = double + expect(fake_socket).to receive(:connect).and_return(0) + expect(fake_socket).to receive(:send_string).and_return(0) + + fake_context = double + expect(fake_context).to receive(:socket).and_return(fake_socket) + expect(::ZMQ::Context).to receive(:new).and_return(fake_context) + end + + subject do + described_class.new(:host => '127.0.0.1', :port => 9400) + end + + describe '#run' do + # not tested via unit tests + end + + describe '#handle_request' do + # not tested via unit tests + end + + describe '#initialize_buffers' do + # not tested via unit tests + end + + describe '#send_data' do + # not tested via unit tests + end +end diff --git a/spec/lib/protobuf/rpc/service_directory_spec.rb b/spec/lib/protobuf/rpc/service_directory_spec.rb new file mode 100644 index 00000000..268bb021 --- /dev/null +++ b/spec/lib/protobuf/rpc/service_directory_spec.rb @@ -0,0 +1,293 @@ +require 'spec_helper' + +require 'protobuf/rpc/service_directory' + +RSpec.describe ::Protobuf::Rpc::ServiceDirectory do + subject { described_class.instance } + + let(:echo_server) do + ::Protobuf::Rpc::DynamicDiscovery::Server.new( + :uuid => 'echo', + :address => '127.0.0.1', + :port => '1111', + :ttl => 10, + :services => %w(EchoService), + ) + end + + let(:hello_server) do + ::Protobuf::Rpc::DynamicDiscovery::Server.new( + :uuid => "hello", + :address => '127.0.0.1', + :port => "1112", + :ttl => 10, + :services => %w(HelloService), + ) + end + + let(:hello_server_with_short_ttl) do + ::Protobuf::Rpc::DynamicDiscovery::Server.new( + :uuid => "hello_server_with_short_ttl", + :address => '127.0.0.1', + :port => '1113', + :ttl => 1, + :services => %w(HelloService), + ) + end + + let(:combo_server) do + ::Protobuf::Rpc::DynamicDiscovery::Server.new( + :uuid => "combo", + :address => '127.0.0.1', + :port => '1114', + :ttl => 10, + :services => %w(HelloService EchoService), + ) + end + + before(:all) do + @address = "127.0.0.1" + @port = 33333 + @socket = UDPSocket.new + EchoService = Class.new + + described_class.address = @address + described_class.port = @port + end + + def expect_event_trigger(event) + expect(::ActiveSupport::Notifications).to receive(:instrument) + .with(event, hash_including(:listing => an_instance_of(::Protobuf::Rpc::ServiceDirectory::Listing))).once + end + + def send_beacon(type, server) + type = type.to_s.upcase + beacon = ::Protobuf::Rpc::DynamicDiscovery::Beacon.new( + :server => server, + :beacon_type => ::Protobuf::Rpc::DynamicDiscovery::BeaconType.fetch(type), + ) + + @socket.send(beacon.encode, 0, @address, @port) + sleep 0.01 # give the service directory time to process the beacon + end + + it "should be a singleton" do + expect(subject).to be_a_kind_of(Singleton) + end + + it "should be configured to listen to address 127.0.0.1" do + expect(described_class.address).to eq '127.0.0.1' + end + + it "should be configured to listen to port 33333" do + expect(described_class.port).to eq 33333 + end + + it "should defer .start to the instance#start" do + expect(described_class.instance).to receive(:start) + described_class.start + end + + it "should yeild itself to blocks passed to .start" do + allow(described_class.instance).to receive(:start) + expect { |b| described_class.start(&b) }.to yield_with_args(described_class) + end + + it "should defer .stop to the instance#stop" do + expect(described_class.instance).to receive(:stop) + described_class.stop + end + + context "stopped" do + before { subject.stop } + + describe "#lookup" do + it "should return nil" do + send_beacon(:heartbeat, echo_server) + expect(subject.lookup("EchoService")).to be_nil + end + end + + describe "#restart" do + it "should start the service" do + subject.restart + expect(subject).to be_running + end + end + + describe "#running" do + it "should be false" do + expect(subject).to_not be_running + end + end + + describe "#stop" do + it "has no effect" do + subject.stop + end + end + end + + context "started" do + before { subject.start } + after { subject.stop } + + specify { expect(subject).to be_running } + + it "should trigger added events" do + expect_event_trigger("directory.listing.added") + send_beacon(:heartbeat, echo_server) + end + + it "should trigger updated events" do + send_beacon(:heartbeat, echo_server) + expect_event_trigger("directory.listing.updated") + send_beacon(:heartbeat, echo_server) + end + + it "should trigger removed events" do + send_beacon(:heartbeat, echo_server) + expect_event_trigger("directory.listing.removed") + send_beacon(:flatline, echo_server) + end + + describe "#all_listings_for" do + context "when listings are present" do + it "returns all listings for a given service" do + send_beacon(:heartbeat, hello_server) + send_beacon(:heartbeat, combo_server) + + expect(subject.all_listings_for("HelloService").size).to eq(2) + end + end + + context "when no listings are present" do + it "returns and empty array" do + expect(subject.all_listings_for("HelloService").size).to eq(0) + end + end + end + + describe "#each_listing" do + it "should yield to a block for each listing" do + send_beacon(:heartbeat, hello_server) + send_beacon(:heartbeat, echo_server) + send_beacon(:heartbeat, combo_server) + + expect do |block| + subject.each_listing(&block) + end.to yield_control.exactly(3).times + end + end + + describe "#lookup" do + it "should provide listings by service" do + send_beacon(:heartbeat, hello_server) + expect(subject.lookup("HelloService").to_hash).to eq hello_server.to_hash + end + + it "should return random listings" do + send_beacon(:heartbeat, hello_server) + send_beacon(:heartbeat, combo_server) + + uuids = 100.times.map { subject.lookup("HelloService").uuid } + expect(uuids.count("hello")).to be_within(25).of(50) + expect(uuids.count("combo")).to be_within(25).of(50) + end + + it "should not return expired listings" do + send_beacon(:heartbeat, hello_server_with_short_ttl) + sleep 5 + expect(subject.lookup("HelloService")).to be_nil + end + + it "should not return flatlined servers" do + send_beacon(:heartbeat, echo_server) + send_beacon(:heartbeat, combo_server) + send_beacon(:flatline, echo_server) + + uuids = 100.times.map { subject.lookup("EchoService").uuid } + expect(uuids.count("combo")).to eq 100 + end + + it "should return up-to-date listings" do + send_beacon(:heartbeat, echo_server) + echo_server.port = "7777" + send_beacon(:heartbeat, echo_server) + + expect(subject.lookup("EchoService").port).to eq "7777" + end + + context 'when given service identifier is a class name' do + it 'returns the listing corresponding to the class name' do + send_beacon(:heartbeat, echo_server) + expect(subject.lookup(EchoService).uuid).to eq echo_server.uuid + end + end + end + + describe "#restart" do + it "should clear all listings" do + send_beacon(:heartbeat, echo_server) + send_beacon(:heartbeat, combo_server) + subject.restart + expect(subject.lookup("EchoService")).to be_nil + end + end + + describe "#running" do + it "should be true" do + expect(subject).to be_running + end + end + + describe "#stop" do + it "should clear all listings" do + send_beacon(:heartbeat, echo_server) + send_beacon(:heartbeat, combo_server) + subject.stop + expect(subject.lookup("EchoService")).to be_nil + end + + it "should stop the server" do + subject.stop + expect(subject).to_not be_running + end + end + end + + if ENV.key?("BENCH") + context "performance" do + let(:servers) do + 100.times.map do |x| + ::Protobuf::Rpc::DynamicDiscovery::Server.new( + :uuid => "performance_server#{x + 1}", + :address => '127.0.0.1', + :port => (5555 + x).to_s, + :ttl => rand(1..5), + :services => 10.times.map { |y| "PerformanceService#{y}" }, + ) + end + end + + before do + require 'benchmark' + subject.start + servers.each { |server| send_beacon(:heartbeat, server) } + end + + after do + subject.stop + end + + it "should perform lookups in constant time" do + print "\n\n" + Benchmark.bm(17) do |x| + x.report(" 1_000 lookups:") { 1_000.times { subject.lookup("PerformanceService#{rand(0..9)}") } } + x.report(" 10_000 lookups:") { 10_000.times { subject.lookup("PerformanceService#{rand(0..9)}") } } + x.report("100_000 lookups:") { 100_000.times { subject.lookup("PerformanceService#{rand(0..9)}") } } + end + end + end + end +end diff --git a/spec/lib/protobuf/rpc/service_dispatcher_spec.rb b/spec/lib/protobuf/rpc/service_dispatcher_spec.rb new file mode 100644 index 00000000..4e11f24b --- /dev/null +++ b/spec/lib/protobuf/rpc/service_dispatcher_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' +require 'protobuf/rpc/service_dispatcher' + +RSpec.describe Protobuf::Rpc::ServiceDispatcher do + let(:app) { proc { |env| env } } + let(:env) do + Protobuf::Rpc::Env.new( + 'method_name' => method_name, + 'request' => request, + 'rpc_service' => service_class, + 'service_name' => service_name, + ) + end + let(:method_name) { :find } + let(:request) { request_type.new(:name => 'required') } + let(:request_type) { service_class.rpcs[method_name].request_type } + let(:response) { response_type.new(:name => 'required') } + let(:response_type) { service_class.rpcs[method_name].response_type } + let(:rpc_service) { service_class.new(env) } + let(:service_class) { Test::ResourceService } + let(:service_name) { service_class.to_s } + + subject { described_class.new(app) } + + before { allow(subject).to receive(:rpc_service).and_return(rpc_service) } + + describe '#call' do + before { allow(rpc_service).to receive(:response).and_return(response) } + + it "dispatches the request" do + stack_env = subject._call(env) + expect(stack_env.response).to eq response + end + end +end diff --git a/spec/lib/protobuf/rpc/service_filters_spec.rb b/spec/lib/protobuf/rpc/service_filters_spec.rb new file mode 100644 index 00000000..97cc5b2d --- /dev/null +++ b/spec/lib/protobuf/rpc/service_filters_spec.rb @@ -0,0 +1,517 @@ +require 'spec_helper' + +class FilterTest + include Protobuf::Rpc::ServiceFilters + + attr_accessor :called + + # Initialize the hash keys as instance vars + def initialize(ivar_hash) + @called = [] + ivar_hash.each_pair do |key, value| + self.class.class_eval do + attr_accessor key + end + __send__("#{key}=", value) + end + end + + def endpoint + @called << :endpoint + end + + def self.clear_filters! + @defined_filters = nil + @filters = nil + @rescue_filters = nil + end +end + +RSpec.describe Protobuf::Rpc::ServiceFilters do + let(:params) { {} } + subject { FilterTest.new(params) } + after(:each) { FilterTest.clear_filters! } + + describe '#before_filter' do + let(:params) { { :before_filter_calls => 0 } } + + before(:all) do + class FilterTest + private + + def verify_before + @called << :verify_before + @before_filter_calls += 1 + end + + def foo + @called << :foo + end + end + end + + before do + FilterTest.before_filter(:verify_before) + FilterTest.before_filter(:verify_before) + FilterTest.before_filter(:foo) + end + + specify { expect(subject.class).to respond_to(:before_filter) } + specify { expect(subject.class).to respond_to(:before_action) } + + it 'calls filters in the order they were defined' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:verify_before, :foo, :endpoint] + expect(subject.before_filter_calls).to eq 1 + end + + context 'when filter is configured with "only"' do + before(:all) do + class FilterTest + private + + def endpoint_with_verify + @called << :endpoint_with_verify + end + end + end + + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :only => :endpoint_with_verify) + end + + context 'when invoking a method defined in "only" option' do + it 'invokes the filter' do + subject.__send__(:run_filters, :endpoint_with_verify) + expect(subject.called).to eq [:verify_before, :endpoint_with_verify] + end + end + + context 'when invoking a method not defined by "only" option' do + it 'does not invoke the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:endpoint] + end + end + end + + context 'when filter is configured with "except"' do + before(:all) do + class FilterTest + private + + def endpoint_without_verify + @called << :endpoint_without_verify + end + end + end + + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :except => :endpoint_without_verify) + end + + context 'when invoking a method not defined in "except" option' do + it 'invokes the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:verify_before, :endpoint] + end + end + + context 'when invoking a method defined by "except" option' do + it 'does not invoke the filter' do + subject.__send__(:run_filters, :endpoint_without_verify) + expect(subject.called).to eq [:endpoint_without_verify] + end + end + end + + context 'when filter is configured with "if"' do + before(:all) do + class FilterTest + private + + def check_true + true + end + + def check_false + false + end + + def verify_before + @called << :verify_before + end + end + end + + context 'when "if" option is a method that returns true' do + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :if => :check_true) + end + + it 'invokes the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:verify_before, :endpoint] + end + end + + context 'when "if" option is a callable that returns true' do + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :if => ->(_service) { true }) + end + + it 'invokes the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:verify_before, :endpoint] + end + end + + context 'when "if" option is a method that returns false' do + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :if => :check_false) + end + + it 'skips the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:endpoint] + end + end + + context 'when "if" option is a callable that returns false' do + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :if => ->(_service) { false }) + end + + it 'skips the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:endpoint] + end + end + end + + context 'when filter is configured with "unless"' do + before(:all) do + class FilterTest + private + + def check_true + true + end + + def check_false + false + end + + def verify_before + @called << :verify_before + end + end + end + + context 'when "unless" option is a method that returns false' do + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :unless => :check_false) + end + + it 'invokes the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:verify_before, :endpoint] + end + end + + context 'when "unless" option is a callable that returns true' do + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :unless => ->(_service) { false }) + end + + it 'invokes the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:verify_before, :endpoint] + end + end + + context 'when "unless" option is a method that returns false' do + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :unless => :check_true) + end + + it 'skips the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:endpoint] + end + end + + context 'when "unless" option is a callable that returns false' do + before do + FilterTest.clear_filters! + FilterTest.before_filter(:verify_before, :unless => ->(_service) { true }) + end + + it 'skips the filter' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:endpoint] + end + end + end + + context 'when filter returns false' do + before(:all) do + class FilterTest + private + + def short_circuit_filter + @called << :short_circuit_filter + false + end + end + end + + before do + FilterTest.clear_filters! + FilterTest.before_filter(:short_circuit_filter) + end + + it 'does not invoke the rpc method' do + expect(subject).not_to receive(:endpoint) + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:short_circuit_filter] + end + end + end + + describe '#after_filter' do + let(:params) { { :after_filter_calls => 0 } } + + before(:all) do + class FilterTest + private + + def verify_after + @called << :verify_after + @after_filter_calls += 1 + end + + def foo + @called << :foo + end + end + end + + before do + FilterTest.after_filter(:verify_after) + FilterTest.after_filter(:verify_after) + FilterTest.after_filter(:foo) + end + + specify { expect(subject.class).to respond_to(:after_filter) } + specify { expect(subject.class).to respond_to(:after_action) } + + it 'calls filters in the order they were defined' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq [:endpoint, :verify_after, :foo] + expect(subject.after_filter_calls).to eq 1 + end + end + + describe '#around_filter' do + let(:params) { {} } + + before(:all) do + class FilterTest + private + + def outer_around + @called << :outer_around_top + yield + @called << :outer_around_bottom + end + + def inner_around + @called << :inner_around_top + yield + @called << :inner_around_bottom + end + end + end + + before do + FilterTest.around_filter(:outer_around) + FilterTest.around_filter(:inner_around) + FilterTest.around_filter(:outer_around) + FilterTest.around_filter(:inner_around) + end + + specify { expect(subject.class).to respond_to(:around_filter) } + specify { expect(subject.class).to respond_to(:around_action) } + + it 'calls filters in the order they were defined' do + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq( + [ + :outer_around_top, + :inner_around_top, + :endpoint, + :inner_around_bottom, + :outer_around_bottom, + ], + ) + end + + context 'when around_filter does not yield' do + before do + class FilterTest + private + + def inner_around + @called << :inner_around + end + end + end + + before do + FilterTest.around_filter(:outer_around) + FilterTest.around_filter(:inner_around) + end + + it 'cancels calling the rest of the filters and the endpoint' do + expect(subject).not_to receive(:endpoint) + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq( + [ + :outer_around_top, + :inner_around, + :outer_around_bottom, + ], + ) + end + + end + end + + describe '#rescue_from' do + before do + class CustomError1 < StandardError; end + class CustomError2 < StandardError; end + class CustomError3 < StandardError; end + end + + before do + class FilterTest + private + + def filter_with_error1 + @called << :filter_with_error1 + fail CustomError1, 'Filter 1 failed' + end + + def filter_with_error2 + @called << :filter_with_error2 + fail CustomError1, 'Filter 2 failed' + end + + def filter_with_error3 + @called << :filter_with_error3 + fail CustomError3, 'Filter 3 failed' + end + + def filter_with_runtime_error + @called << :filter_with_runtime_error + fail 'Filter with runtime error failed' + end + + def custom_error_occurred(ex) + @ex_class = ex.class + @called << :custom_error_occurred + end + end + end + + let(:params) { { :ex_class => nil } } + + context 'when defining multiple errors with a given callback' do + before do + FilterTest.rescue_from(CustomError1, CustomError2, CustomError3, :with => :custom_error_occurred) + end + before { FilterTest.before_filter(:filter_with_error3) } + + it 'short-circuits the call stack' do + expect do + expect(subject).not_to receive(:endpoint) + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq([:filter_with_error3, :custom_error_occurred]) + expect(subject.ex_class).to eq CustomError3 + end.not_to raise_error + end + end + + context 'when defined with options' do + context 'when :with option is not given' do + specify do + expect { FilterTest.rescue_from(CustomError1) }.to raise_error(ArgumentError, /with/) + end + end + + context 'when error occurs inside filter' do + before { FilterTest.rescue_from(CustomError1, :with => :custom_error_occurred) } + before { FilterTest.before_filter(:filter_with_error1) } + + it 'short-circuits the call stack' do + expect do + expect(subject).not_to receive(:endpoint) + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq([:filter_with_error1, :custom_error_occurred]) + expect(subject.ex_class).to eq CustomError1 + end.not_to raise_error + end + end + end + + context 'when defined with block' do + before do + FilterTest.rescue_from(CustomError1) do |service, ex| + service.ex_class = ex.class + service.called << :block_rescue_handler + end + end + before { FilterTest.before_filter(:filter_with_error1) } + + it 'short-circuits the call stack' do + expect do + expect(subject).not_to receive(:endpoint) + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq([:filter_with_error1, :block_rescue_handler]) + expect(subject.ex_class).to eq CustomError1 + end.not_to raise_error + end + end + + context 'when thrown exception inherits from a mapped exception' do + before do + FilterTest.rescue_from(StandardError) do |service, ex| + service.ex_class = ex.class + service.called << :standard_error_rescue_handler + end + end + before { FilterTest.before_filter(:filter_with_runtime_error) } + + it 'rescues with the given callable' do + expect do + expect(subject).not_to receive(:endpoint) + subject.__send__(:run_filters, :endpoint) + expect(subject.called).to eq([:filter_with_runtime_error, :standard_error_rescue_handler]) + expect(subject.ex_class).to eq RuntimeError + end.not_to raise_error + end + end + end + +end diff --git a/spec/lib/protobuf/rpc/service_spec.rb b/spec/lib/protobuf/rpc/service_spec.rb new file mode 100644 index 00000000..7604e74f --- /dev/null +++ b/spec/lib/protobuf/rpc/service_spec.rb @@ -0,0 +1,162 @@ +require 'spec_helper' +require SUPPORT_PATH.join('resource_service') + +RSpec.describe Protobuf::Rpc::Service do + + context 'class methods' do + subject { Test::ResourceService } + + before :each do + reset_service_location Test::ResourceService + end + + describe '.host' do + specify { expect(subject.host).to eq described_class::DEFAULT_HOST } + end + + describe '.host=' do + before { subject.host = 'mynewhost.com' } + specify { expect(subject.host).to eq 'mynewhost.com' } + end + + describe '.port' do + specify { expect(subject.port).to eq described_class::DEFAULT_PORT } + end + + describe '.port=' do + before { subject.port = 12345 } + specify { expect(subject.port).to eq 12345 } + end + + describe '.configure' do + context 'when providing a host' do + before { subject.configure(:host => 'mynewhost.com') } + specify { expect(subject.host).to eq 'mynewhost.com' } + end + + context 'when providing a port' do + before { subject.configure(:port => 12345) } + specify { expect(subject.port).to eq 12345 } + end + end + + describe '.located_at' do + context 'when given location is empty' do + before { subject.located_at(nil) } + specify { expect(subject.host).to eq described_class::DEFAULT_HOST } + specify { expect(subject.port).to eq described_class::DEFAULT_PORT } + end + + context 'when given location is invalid' do + before { subject.located_at('i like pie') } + specify { expect(subject.host).to eq described_class::DEFAULT_HOST } + specify { expect(subject.port).to eq described_class::DEFAULT_PORT } + end + + context 'when given location contains a host and port' do + before { subject.located_at('mynewdomain.com:12345') } + specify { expect(subject.host).to eq 'mynewdomain.com' } + specify { expect(subject.port).to eq 12345 } + end + end + + describe '.client' do + it 'initializes a client object for this service' do + client = double('client') + expect(::Protobuf::Rpc::Client).to receive(:new) + .with(hash_including( + :service => subject, + :host => subject.host, + :port => subject.port, + )).and_return(client) + expect(subject.client).to eq client + end + end + + describe '.rpc' do + before { Test::ResourceService.rpc(:update, Test::ResourceFindRequest, Test::Resource) } + subject { Test::ResourceService.rpcs[:update] } + specify { expect(subject.method).to eq :update } + specify { expect(subject.request_type).to eq Test::ResourceFindRequest } + specify { expect(subject.response_type).to eq Test::Resource } + end + + describe '.rpc_method?' do + before { Test::ResourceService.rpc(:delete, Test::Resource, Test::Resource) } + + context 'when given name is a pre-defined rpc method' do + it 'returns true' do + expect(subject.rpc_method?(:delete)).to be true + end + end + + context 'when given name is not a pre-defined rpc method' do + it 'returns false' do + expect(subject.rpc_method?(:zoobaboo)).to be false + end + end + end + end + + context 'instance methods' do + context 'when invoking a service call' do + before do + stub_const('NewTestService', Class.new(Protobuf::Rpc::Service) do + rpc :find_with_implied_response, Test::ResourceFindRequest, Test::Resource + def find_with_implied_response + response.name = 'Implicit response' + end + + rpc :find_with_respond_with, Test::ResourceFindRequest, Test::Resource + def find_with_respond_with + custom = Test::Resource.new(:name => 'Custom response') + respond_with(custom) + end + + rpc :find_with_rpc_failed, Test::ResourceFindRequest, Test::Resource + def find_with_rpc_failed + rpc_failed('This is a failed endpoint') + response.name = 'Name will still be set' + end + end) + end + + let(:request) { Test::ResourceFindRequest.new(:name => 'resource') } + let(:response) { Test::Resource.new } + + context 'when calling the rpc method' do + context 'when response is implied' do + let(:env) do + Protobuf::Rpc::Env.new( + 'request' => request, + 'response_type' => response_type, + ) + end + let(:response_type) { service.rpcs[:find_with_implied_response].response_type } + let(:service) { NewTestService } + + subject { NewTestService.new(env) } + + before { subject.find_with_implied_response } + specify { expect(subject.response).to be_a(Test::Resource) } + specify { expect(subject.response.name).to eq 'Implicit response' } + end + + context 'when using respond_with paradigm' do + let(:env) do + Protobuf::Rpc::Env.new( + 'method_name' => :find_with_respond_with, + 'request' => request, + ) + end + + subject { NewTestService.new(env) } + + before { subject.find_with_respond_with } + specify { expect(subject.response).to be_a(Test::Resource) } + specify { expect(subject.response.name).to eq 'Custom response' } + end + end + end + end +end diff --git a/spec/lib/protobuf/rpc/stat_spec.rb b/spec/lib/protobuf/rpc/stat_spec.rb new file mode 100644 index 00000000..10015491 --- /dev/null +++ b/spec/lib/protobuf/rpc/stat_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' +require 'timecop' +require 'active_support/all' + +RSpec.describe ::Protobuf::Rpc::Stat do + + before(:all) do + BarService = ::Struct.new(:method_name) unless defined?(BarService) + end + + describe '#server=' do + it 'understands Array' do + stat = ::Protobuf::Rpc::Stat.new + stat.server = [3333, "127.0.0.1"] + expect(stat.server).to eq("127.0.0.1:3333") + end + + it 'understands String' do + stat = ::Protobuf::Rpc::Stat.new + stat.server = "thatserverthough" + expect(stat.server).to eq("thatserverthough") + end + end + + describe 'server mode' do + it 'describes a server response to a client' do + ::Timecop.freeze(10.minutes.ago) do + stats = ::Protobuf::Rpc::Stat.new(:SERVER) + stats.client = 'myserver1' + stats.dispatcher = double('dispatcher', :service => BarService.new(:find_bars)) + stats.request_size = 43 + stats.response_size = 1302 + + ::Timecop.freeze(1.62.seconds.from_now) do + stats.stop + expect(stats.to_s).to eq "[SRV] - myserver1 - #{stats.trace_id} - BarService#find_bars - 43B/1302B - 1.62s - OK - #{::Time.now.iso8601}" + end + end + end + + context 'when request is still running' do + it 'omits response size, duration, and timestamp' do + stats = ::Protobuf::Rpc::Stat.new(:SERVER) + stats.client = 'myserver1' + stats.dispatcher = double('dispatcher', :service => BarService.new(:find_bars)) + stats.request_size = 43 + expect(stats.to_s).to eq "[SRV] - myserver1 - #{stats.trace_id} - BarService#find_bars - 43B/- - OK" + end + end + end + + describe 'client mode' do + it 'describes a client request to a server' do + ::Timecop.freeze(10.minutes.ago) do + stats = ::Protobuf::Rpc::Stat.new(:CLIENT) + stats.server = ['30000', 'myserver1.myhost.com'] + stats.service = 'Foo::BarService' + stats.method_name = 'find_bars' + stats.request_size = 37 + stats.response_size = 12345 + + ::Timecop.freeze(0.832.seconds.from_now) do + stats.stop + expect(stats.to_s).to eq "[CLT] - myserver1.myhost.com:30000 - #{stats.trace_id} - Foo::BarService#find_bars - 37B/12345B - 0.832s - OK - #{::Time.now.iso8601}" + end + + end + end + + describe 'error log' do + it 'resolves error to a string' do + ::Timecop.freeze(10.minutes.ago) do + stats = ::Protobuf::Rpc::Stat.new(:CLIENT) + stats.server = ['30000', 'myserver1.myhost.com'] + stats.service = 'Foo::BarService' + stats.status = ::Protobuf::Socketrpc::ErrorReason::RPC_ERROR + stats.method_name = 'find_bars' + stats.request_size = 37 + stats.response_size = 12345 + + ::Timecop.freeze(0.832.seconds.from_now) do + stats.stop + expect(stats.to_s).to eq "[CLT] - myserver1.myhost.com:30000 - #{stats.trace_id} - Foo::BarService#find_bars - 37B/12345B - 0.832s - RPC_ERROR - #{::Time.now.iso8601}" + end + end + end + end + + context 'when request is still running' do + it 'omits response size, duration, and timestamp' do + stats = ::Protobuf::Rpc::Stat.new(:CLIENT) + stats.server = ['30000', 'myserver1.myhost.com'] + stats.service = 'Foo::BarService' + stats.method_name = 'find_bars' + stats.request_size = 37 + expect(stats.to_s).to eq "[CLT] - myserver1.myhost.com:30000 - #{stats.trace_id} - Foo::BarService#find_bars - 37B/- - OK" + end + end + end + +end diff --git a/spec/lib/protobuf/varint_spec.rb b/spec/lib/protobuf/varint_spec.rb new file mode 100644 index 00000000..78c2206e --- /dev/null +++ b/spec/lib/protobuf/varint_spec.rb @@ -0,0 +1,29 @@ +require 'base64' +require 'spec_helper' + +RSpec.describe Protobuf::Varint do + VALUES = { + 0 => "AA==", + 5 => "BQ==", + 51 => "Mw==", + 9_192 => "6Ec=", + 80_389 => "hfQE", + 913_389 => "7d83", + 516_192_829_912_693 => "9eyMkpivdQ==", + 9_999_999_999_999_999_999 => "//+fz8jgyOOKAQ==", + }.freeze + + [defined?(::Varint) ? ::Varint : nil, Protobuf::VarintPure].compact.each do |klass| + context "with #{klass}" do + before { described_class.extend(klass) } + after { load ::File.expand_path('../../../../lib/protobuf/varint.rb', __FILE__) } + + VALUES.each do |number, encoded| + it "decodes #{number}" do + io = StringIO.new(Base64.decode64(encoded)) + expect(described_class.decode(io)).to eq(number) + end + end + end + end +end diff --git a/spec/lib/protobuf_spec.rb b/spec/lib/protobuf_spec.rb new file mode 100644 index 00000000..f55e4358 --- /dev/null +++ b/spec/lib/protobuf_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' +require 'protobuf' + +RSpec.describe ::Protobuf do + + describe '.client_host' do + after { ::Protobuf.client_host = nil } + + subject { ::Protobuf.client_host } + + context 'when client_host is not pre-configured' do + it { is_expected.to eq ::Socket.gethostname } + end + + context 'when client_host is pre-configured' do + let(:hostname) { 'override.myhost.com' } + before { ::Protobuf.client_host = hostname } + it { is_expected.to eq hostname } + end + end + + describe '.connector_type_class' do + it "defaults to Socket" do + described_class.connector_type_class = nil + expect(described_class.connector_type_class).to eq(::Protobuf::Rpc::Connectors::Socket) + end + + it 'fails if fails to load the PB_CLIENT_TYPE' do + ENV['PB_CLIENT_TYPE'] = "something_to_autoload" + expect { load 'protobuf.rb' }.to raise_error(LoadError, /something_to_autoload/) + ENV.delete('PB_CLIENT_TYPE') + end + + it 'loads the connector type class from PB_CLIENT_TYPE' do + ENV['PB_CLIENT_TYPE'] = "protobuf/rpc/connectors/zmq" + load 'protobuf.rb' + expect(::Protobuf.connector_type_class).to eq(::Protobuf::Rpc::Connectors::Zmq) + ENV.delete('PB_CLIENT_TYPE') + end + end + + describe '.gc_pause_server_request?' do + before { described_class.instance_variable_set(:@gc_pause_server_request, nil) } + + it 'defaults to a false value' do + expect(described_class.gc_pause_server_request?).to be false + end + + it 'is settable' do + described_class.gc_pause_server_request = true + expect(described_class.gc_pause_server_request?).to be true + end + end + + describe '.print_deprecation_warnings?' do + around do |example| + orig = described_class.print_deprecation_warnings? + example.call + described_class.print_deprecation_warnings = orig + end + + it 'defaults to a true value' do + allow(ENV).to receive(:key?).with('PB_IGNORE_DEPRECATIONS').and_return(false) + described_class.instance_variable_set('@field_deprecator', nil) + expect(described_class.print_deprecation_warnings?).to be true + end + + it 'is settable' do + described_class.print_deprecation_warnings = false + expect(described_class.print_deprecation_warnings?).to be false + end + + context 'when ENV["PB_IGNORE_DEPRECATIONS"] present' do + it 'defaults to a false value' do + allow(ENV).to receive(:key?).with('PB_IGNORE_DEPRECATIONS').and_return(true) + described_class.instance_variable_set('@field_deprecator', nil) + expect(described_class.print_deprecation_warnings?).to be false + end + end + end + + describe '.ignore_unknown_fields?' do + around do |example| + orig = described_class.ignore_unknown_fields? + example.call + described_class.ignore_unknown_fields = orig + end + + it 'defaults to a true value' do + if described_class.instance_variable_defined?('@ignore_unknown_fields') + described_class.send(:remove_instance_variable, '@ignore_unknown_fields') + end + expect(described_class.ignore_unknown_fields?).to be true + end + + it 'is settable' do + expect do + described_class.ignore_unknown_fields = false + end.to change { + described_class.ignore_unknown_fields? + }.from(true).to(false) + end + end + +end diff --git a/spec/proto/test.pb.rb b/spec/proto/test.pb.rb deleted file mode 100644 index 269971c6..00000000 --- a/spec/proto/test.pb.rb +++ /dev/null @@ -1,31 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Spec - module Proto - class StatusType < ::Protobuf::Enum - define :PENDING, 0 - define :ENABLED, 1 - define :DISABLED, 2 - define :DELETED, 3 - end - class ResourceFindRequest < ::Protobuf::Message - optional :string, :name, 1 - optional :bool, :active, 2 - end - class Resource < ::Protobuf::Message - optional :string, :name, 1 - optional :int64, :date_created, 2 - optional :StatusType, :status, 3 - repeated :StatusType, :repeated_enum, 4 - end - class Nested < ::Protobuf::Message - optional :string, :name, 1 - optional :Resource, :resource, 2 - repeated :Resource, :multiple_resources, 3 - optional :StatusType, :status, 4 - end - end -end \ No newline at end of file diff --git a/spec/proto/test.proto b/spec/proto/test.proto deleted file mode 100644 index 31997eff..00000000 --- a/spec/proto/test.proto +++ /dev/null @@ -1,31 +0,0 @@ -package spec.proto; - -enum StatusType { - PENDING = 0; - ENABLED = 1; - DISABLED = 2; - DELETED = 3; -} - -message ResourceFindRequest { - optional string name = 1; - optional bool active = 2; -} - -message Resource { - optional string name = 1; - optional int64 date_created = 2; - optional StatusType status = 3; - repeated StatusType repeated_enum = 4; -} - -message Nested { - optional string name = 1; - optional Resource resource = 2; - repeated Resource multiple_resources = 3; - optional StatusType status = 4; -} - -service TestService { - rpc Find (ResourceFindRequest) returns (Resource); -} \ No newline at end of file diff --git a/spec/proto/test_service.rb b/spec/proto/test_service.rb deleted file mode 100644 index cc146ef7..00000000 --- a/spec/proto/test_service.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'protobuf/rpc/service' - -## !! DO NOT EDIT THIS FILE !! -## -## To implement this service as defined by the protobuf, simply -## reopen Spec::Proto::TestService and implement each service method: -## -## module Spec -## module Proto -## class TestService -## -## # request -> Spec::Proto::ResourceFindRequest -## # response -> Spec::Proto::Resource -## def find -## # TODO: implement find -## end -## -## end -## end -## end -## - -module Spec - module Proto - class TestService < Protobuf::Rpc::Service - rpc :find, ResourceFindRequest, Resource - end - end -end diff --git a/spec/proto/test_service_impl.rb b/spec/proto/test_service_impl.rb deleted file mode 100644 index 0e5bc866..00000000 --- a/spec/proto/test_service_impl.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'protobuf/rpc/service' -require File.dirname(__FILE__) + '/test.pb' -require File.dirname(__FILE__) + '/test_service' - -module Spec - module Proto - class TestService - - # request -> Spec::Proto::ResourceFindRequest - # response -> Spec::Proto::Resource - def find - response.name = request.name - response.status = request.active ? 1 : 0 - end - - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8037d400..bb6c43d1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,37 +1,42 @@ -require 'simplecov' -SimpleCov.start - +require 'timeout' require 'rubygems' require 'bundler' Bundler.setup :default, :development, :test +require 'pry' +require 'pathname' -$:.push File.expand_path('..', File.dirname(__FILE__)) -$:.push File.expand_path('../lib', File.dirname(__FILE__)) +$LOAD_PATH << ::File.expand_path('../..', __FILE__) +SUPPORT_PATH = Pathname.new(::File.expand_path('../support', __FILE__)) +PROTOS_PATH = SUPPORT_PATH.join('protos') +$LOAD_PATH << SUPPORT_PATH require 'protobuf' -require 'protobuf/rpc/client' -require File.dirname(__FILE__) + '/helper/all' +require 'protobuf/rpc/server' +require SUPPORT_PATH.join('all') + +$LOAD_PATH << ::File.expand_path("../../lib/protobuf/descriptors", __FILE__) +require 'google/protobuf/compiler/plugin.pb' # Including a way to turn on debug logger for spec runs -if ENV["DEBUG"] - debug_log = File.expand_path('../debug_specs.log', File.dirname(__FILE__) ) - Protobuf::Logger.configure(:file => debug_log, :level => ::Logger::DEBUG) +if ENV.key?('DEBUG') + debug_log = ::File.expand_path('../../debug_specs.log', __FILE__) + ::Protobuf::Logging.initialize_logger(debug_log, ::Logger::DEBUG) +else + ::Protobuf::Logging.initialize_logger('/dev/null') end -RSpec.configure do |c| - c.include(SilentConstants) - c.include(Sander6::CustomMatchers) - c.mock_with :rspec -end +# Get rid of the deprecation env var if present (messes with specs). +ENV.delete("PB_IGNORE_DEPRECATIONS") -class ::Protobuf::Rpc::Client - def == other +::Protobuf::Rpc::Client.class_eval do + def ==(other) connector.options == other.options && \ success_cb == other.success_cb && \ failure_cb == other.failure_cb end end -def reset_service_location service - service.instance_variable_set :@locations, nil +def reset_service_location(service) + service.host = nil + service.port = nil end diff --git a/spec/support/all.rb b/spec/support/all.rb new file mode 100644 index 00000000..889143a5 --- /dev/null +++ b/spec/support/all.rb @@ -0,0 +1,6 @@ +require 'support/packed_field' +require 'support/server' + +def now + Time.new.to_f +end diff --git a/spec/support/google/protobuf/descriptor.pb.rb b/spec/support/google/protobuf/descriptor.pb.rb new file mode 100644 index 00000000..acff3ebd --- /dev/null +++ b/spec/support/google/protobuf/descriptor.pb.rb @@ -0,0 +1,360 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + +module Google + module Protobuf + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Message Classes + # + class FileDescriptorSet < ::Protobuf::Message; end + class FileDescriptorProto < ::Protobuf::Message; end + class DescriptorProto < ::Protobuf::Message + class ExtensionRange < ::Protobuf::Message; end + class ReservedRange < ::Protobuf::Message; end + + end + + class ExtensionRangeOptions < ::Protobuf::Message; end + class FieldDescriptorProto < ::Protobuf::Message + class Type < ::Protobuf::Enum + define :TYPE_DOUBLE, 1 + define :TYPE_FLOAT, 2 + define :TYPE_INT64, 3 + define :TYPE_UINT64, 4 + define :TYPE_INT32, 5 + define :TYPE_FIXED64, 6 + define :TYPE_FIXED32, 7 + define :TYPE_BOOL, 8 + define :TYPE_STRING, 9 + define :TYPE_GROUP, 10 + define :TYPE_MESSAGE, 11 + define :TYPE_BYTES, 12 + define :TYPE_UINT32, 13 + define :TYPE_ENUM, 14 + define :TYPE_SFIXED32, 15 + define :TYPE_SFIXED64, 16 + define :TYPE_SINT32, 17 + define :TYPE_SINT64, 18 + end + + class Label < ::Protobuf::Enum + define :LABEL_OPTIONAL, 1 + define :LABEL_REQUIRED, 2 + define :LABEL_REPEATED, 3 + end + + end + + class OneofDescriptorProto < ::Protobuf::Message; end + class EnumDescriptorProto < ::Protobuf::Message + class EnumReservedRange < ::Protobuf::Message; end + + end + + class EnumValueDescriptorProto < ::Protobuf::Message; end + class ServiceDescriptorProto < ::Protobuf::Message; end + class MethodDescriptorProto < ::Protobuf::Message; end + class FileOptions < ::Protobuf::Message + class OptimizeMode < ::Protobuf::Enum + define :SPEED, 1 + define :CODE_SIZE, 2 + define :LITE_RUNTIME, 3 + end + + end + + class MessageOptions < ::Protobuf::Message; end + class FieldOptions < ::Protobuf::Message + class CType < ::Protobuf::Enum + define :STRING, 0 + define :CORD, 1 + define :STRING_PIECE, 2 + end + + class JSType < ::Protobuf::Enum + define :JS_NORMAL, 0 + define :JS_STRING, 1 + define :JS_NUMBER, 2 + end + + end + + class OneofOptions < ::Protobuf::Message; end + class EnumOptions < ::Protobuf::Message; end + class EnumValueOptions < ::Protobuf::Message; end + class ServiceOptions < ::Protobuf::Message; end + class MethodOptions < ::Protobuf::Message + class IdempotencyLevel < ::Protobuf::Enum + define :IDEMPOTENCY_UNKNOWN, 0 + define :NO_SIDE_EFFECTS, 1 + define :IDEMPOTENT, 2 + end + + end + + class UninterpretedOption < ::Protobuf::Message + class NamePart < ::Protobuf::Message; end + + end + + class SourceCodeInfo < ::Protobuf::Message + class Location < ::Protobuf::Message; end + + end + + class GeneratedCodeInfo < ::Protobuf::Message + class Annotation < ::Protobuf::Message; end + + end + + + + ## + # File Options + # + set_option :java_package, "com.google.protobuf" + set_option :java_outer_classname, "DescriptorProtos" + set_option :optimize_for, ::Google::Protobuf::FileOptions::OptimizeMode::SPEED + set_option :go_package, "google.golang.org/protobuf/types/descriptorpb" + set_option :cc_enable_arenas, true + set_option :objc_class_prefix, "GPB" + set_option :csharp_namespace, "Google.Protobuf.Reflection" + + + ## + # Message Fields + # + class FileDescriptorSet + repeated ::Google::Protobuf::FileDescriptorProto, :file, 1 + end + + class FileDescriptorProto + optional :string, :name, 1 + optional :string, :package, 2 + repeated :string, :dependency, 3 + repeated :int32, :public_dependency, 10 + repeated :int32, :weak_dependency, 11 + repeated ::Google::Protobuf::DescriptorProto, :message_type, 4 + repeated ::Google::Protobuf::EnumDescriptorProto, :enum_type, 5 + repeated ::Google::Protobuf::ServiceDescriptorProto, :service, 6 + repeated ::Google::Protobuf::FieldDescriptorProto, :extension, 7 + optional ::Google::Protobuf::FileOptions, :options, 8 + optional ::Google::Protobuf::SourceCodeInfo, :source_code_info, 9 + optional :string, :syntax, 12 + end + + class DescriptorProto + class ExtensionRange + optional :int32, :start, 1 + optional :int32, :end, 2 + optional ::Google::Protobuf::ExtensionRangeOptions, :options, 3 + end + + class ReservedRange + optional :int32, :start, 1 + optional :int32, :end, 2 + end + + optional :string, :name, 1 + repeated ::Google::Protobuf::FieldDescriptorProto, :field, 2 + repeated ::Google::Protobuf::FieldDescriptorProto, :extension, 6 + repeated ::Google::Protobuf::DescriptorProto, :nested_type, 3 + repeated ::Google::Protobuf::EnumDescriptorProto, :enum_type, 4 + repeated ::Google::Protobuf::DescriptorProto::ExtensionRange, :extension_range, 5 + repeated ::Google::Protobuf::OneofDescriptorProto, :oneof_decl, 8 + optional ::Google::Protobuf::MessageOptions, :options, 7 + repeated ::Google::Protobuf::DescriptorProto::ReservedRange, :reserved_range, 9 + repeated :string, :reserved_name, 10 + end + + class ExtensionRangeOptions + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class FieldDescriptorProto + optional :string, :name, 1 + optional :int32, :number, 3 + optional ::Google::Protobuf::FieldDescriptorProto::Label, :label, 4 + optional ::Google::Protobuf::FieldDescriptorProto::Type, :type, 5 + optional :string, :type_name, 6 + optional :string, :extendee, 2 + optional :string, :default_value, 7 + optional :int32, :oneof_index, 9 + optional :string, :json_name, 10 + optional ::Google::Protobuf::FieldOptions, :options, 8 + optional :bool, :proto3_optional, 17 + end + + class OneofDescriptorProto + optional :string, :name, 1 + optional ::Google::Protobuf::OneofOptions, :options, 2 + end + + class EnumDescriptorProto + class EnumReservedRange + optional :int32, :start, 1 + optional :int32, :end, 2 + end + + optional :string, :name, 1 + repeated ::Google::Protobuf::EnumValueDescriptorProto, :value, 2 + optional ::Google::Protobuf::EnumOptions, :options, 3 + repeated ::Google::Protobuf::EnumDescriptorProto::EnumReservedRange, :reserved_range, 4 + repeated :string, :reserved_name, 5 + end + + class EnumValueDescriptorProto + optional :string, :name, 1 + optional :int32, :number, 2 + optional ::Google::Protobuf::EnumValueOptions, :options, 3 + end + + class ServiceDescriptorProto + optional :string, :name, 1 + repeated ::Google::Protobuf::MethodDescriptorProto, :method, 2 + optional ::Google::Protobuf::ServiceOptions, :options, 3 + end + + class MethodDescriptorProto + optional :string, :name, 1 + optional :string, :input_type, 2 + optional :string, :output_type, 3 + optional ::Google::Protobuf::MethodOptions, :options, 4 + optional :bool, :client_streaming, 5, :default => false + optional :bool, :server_streaming, 6, :default => false + end + + class FileOptions + optional :string, :java_package, 1 + optional :string, :java_outer_classname, 8 + optional :bool, :java_multiple_files, 10, :default => false + optional :bool, :java_generate_equals_and_hash, 20, :deprecated => true + optional :bool, :java_string_check_utf8, 27, :default => false + optional ::Google::Protobuf::FileOptions::OptimizeMode, :optimize_for, 9, :default => ::Google::Protobuf::FileOptions::OptimizeMode::SPEED + optional :string, :go_package, 11 + optional :bool, :cc_generic_services, 16, :default => false + optional :bool, :java_generic_services, 17, :default => false + optional :bool, :py_generic_services, 18, :default => false + optional :bool, :php_generic_services, 42, :default => false + optional :bool, :deprecated, 23, :default => false + optional :bool, :cc_enable_arenas, 31, :default => true + optional :string, :objc_class_prefix, 36 + optional :string, :csharp_namespace, 37 + optional :string, :swift_prefix, 39 + optional :string, :php_class_prefix, 40 + optional :string, :php_namespace, 41 + optional :string, :php_metadata_namespace, 44 + optional :string, :ruby_package, 45 + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class MessageOptions + optional :bool, :message_set_wire_format, 1, :default => false + optional :bool, :no_standard_descriptor_accessor, 2, :default => false + optional :bool, :deprecated, 3, :default => false + optional :bool, :map_entry, 7 + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class FieldOptions + optional ::Google::Protobuf::FieldOptions::CType, :ctype, 1, :default => ::Google::Protobuf::FieldOptions::CType::STRING + optional :bool, :packed, 2 + optional ::Google::Protobuf::FieldOptions::JSType, :jstype, 6, :default => ::Google::Protobuf::FieldOptions::JSType::JS_NORMAL + optional :bool, :lazy, 5, :default => false + optional :bool, :deprecated, 3, :default => false + optional :bool, :weak, 10, :default => false + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class OneofOptions + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class EnumOptions + optional :bool, :allow_alias, 2 + optional :bool, :deprecated, 3, :default => false + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class EnumValueOptions + optional :bool, :deprecated, 1, :default => false + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class ServiceOptions + optional :bool, :deprecated, 33, :default => false + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class MethodOptions + optional :bool, :deprecated, 33, :default => false + optional ::Google::Protobuf::MethodOptions::IdempotencyLevel, :idempotency_level, 34, :default => ::Google::Protobuf::MethodOptions::IdempotencyLevel::IDEMPOTENCY_UNKNOWN + repeated ::Google::Protobuf::UninterpretedOption, :uninterpreted_option, 999 + # Extension Fields + extensions 1000...536870912 + end + + class UninterpretedOption + class NamePart + required :string, :name_part, 1 + required :bool, :is_extension, 2 + end + + repeated ::Google::Protobuf::UninterpretedOption::NamePart, :name, 2 + optional :string, :identifier_value, 3 + optional :uint64, :positive_int_value, 4 + optional :int64, :negative_int_value, 5 + optional :double, :double_value, 6 + optional :bytes, :string_value, 7 + optional :string, :aggregate_value, 8 + end + + class SourceCodeInfo + class Location + repeated :int32, :path, 1, :packed => true + repeated :int32, :span, 2, :packed => true + optional :string, :leading_comments, 3 + optional :string, :trailing_comments, 4 + repeated :string, :leading_detached_comments, 6 + end + + repeated ::Google::Protobuf::SourceCodeInfo::Location, :location, 1 + end + + class GeneratedCodeInfo + class Annotation + repeated :int32, :path, 1, :packed => true + optional :string, :source_file, 2 + optional :int32, :begin, 3 + optional :int32, :end, 4 + end + + repeated ::Google::Protobuf::GeneratedCodeInfo::Annotation, :annotation, 1 + end + + end + +end + diff --git a/spec/support/packed_field.rb b/spec/support/packed_field.rb new file mode 100644 index 00000000..9b18efee --- /dev/null +++ b/spec/support/packed_field.rb @@ -0,0 +1,23 @@ +if defined?(RSpec) + shared_examples_for :packable_field do |field_klass| + + before(:all) do + unless defined?(PackableFieldTest) + class PackableFieldTest < ::Protobuf::Message; end + end + + field_name = "#{field_klass.name.split('::').last.underscore}_packed_field".to_sym + tag_num = PackableFieldTest.fields.size + 1 + PackableFieldTest.repeated(field_klass, field_name, tag_num, :packed => true) + end + + let(:field_name) { "#{field_klass.name.split('::').last.underscore}_packed_field".to_sym } + let(:value) { [100, 200, 300] } + let(:message_instance) { PackableFieldTest.new(field_name => value) } + + subject { PackableFieldTest.get_field(field_name) } + + specify { expect(subject).to be_packed } + specify { expect(PackableFieldTest.decode(message_instance.encode).send(field_name)).to eq value } + end +end diff --git a/spec/support/protos/all_types.data.bin b/spec/support/protos/all_types.data.bin new file mode 100644 index 00000000..15596ae9 Binary files /dev/null and b/spec/support/protos/all_types.data.bin differ diff --git a/spec/support/protos/all_types.data.txt b/spec/support/protos/all_types.data.txt new file mode 100644 index 00000000..fc2088dd --- /dev/null +++ b/spec/support/protos/all_types.data.txt @@ -0,0 +1,119 @@ +optional_int32: 101 +optional_int64: 102 +optional_uint32: 103 +optional_uint64: 104 +optional_sint32: 105 +optional_sint64: 106 +optional_fixed32: 107 +optional_fixed64: 108 +optional_sfixed32: 109 +optional_sfixed64: 110 +optional_float: 111 +optional_double: 112 +optional_bool: true +optional_string: "115" +optional_bytes: "116" +optional_nested_message { + bb: 118 +} +optional_foreign_message { + c: 119 +} +optional_import_message { + d: 120 +} +optional_nested_enum: BAZ +optional_foreign_enum: FOREIGN_BAZ +optional_import_enum: IMPORT_BAZ +optional_string_piece: "124" +optional_cord: "125" +optional_public_import_message { + e: 126 +} +optional_lazy_message { + bb: 127 +} +repeated_int32: 201 +repeated_int32: 301 +repeated_int64: 202 +repeated_int64: 302 +repeated_uint32: 203 +repeated_uint32: 303 +repeated_uint64: 204 +repeated_uint64: 304 +repeated_sint32: 205 +repeated_sint32: 305 +repeated_sint64: 206 +repeated_sint64: 306 +repeated_fixed32: 207 +repeated_fixed32: 307 +repeated_fixed64: 208 +repeated_fixed64: 308 +repeated_sfixed32: 209 +repeated_sfixed32: 309 +repeated_sfixed64: 210 +repeated_sfixed64: 310 +repeated_float: 211 +repeated_float: 311 +repeated_double: 212 +repeated_double: 312 +repeated_bool: true +repeated_bool: false +repeated_string: "215" +repeated_string: "315" +repeated_bytes: "216" +repeated_bytes: "316" +repeated_nested_message { + bb: 218 +} +repeated_nested_message { + bb: 318 +} +repeated_foreign_message { + c: 219 +} +repeated_foreign_message { + c: 319 +} +repeated_import_message { + d: 220 +} +repeated_import_message { + d: 320 +} +repeated_nested_enum: BAR +repeated_nested_enum: BAZ +repeated_foreign_enum: FOREIGN_BAR +repeated_foreign_enum: FOREIGN_BAZ +repeated_import_enum: IMPORT_BAR +repeated_import_enum: IMPORT_BAZ +repeated_string_piece: "224" +repeated_string_piece: "324" +repeated_cord: "225" +repeated_cord: "325" +repeated_lazy_message { + bb: 227 +} +repeated_lazy_message { + bb: 327 +} +default_int32: 401 +default_int64: 402 +default_uint32: 403 +default_uint64: 404 +default_sint32: 405 +default_sint64: 406 +default_fixed32: 407 +default_fixed64: 408 +default_sfixed32: 409 +default_sfixed64: 410 +default_float: 411 +default_double: 412 +default_bool: false +default_string: "415" +default_bytes: "416" +default_nested_enum: FOO +default_foreign_enum: FOREIGN_FOO +default_import_enum: IMPORT_FOO +default_string_piece: "424" +default_cord: "425" diff --git a/spec/support/protos/enum.pb.rb b/spec/support/protos/enum.pb.rb new file mode 100644 index 00000000..9366beb0 --- /dev/null +++ b/spec/support/protos/enum.pb.rb @@ -0,0 +1,63 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + + +## +# Imports +# +require 'protos/resource.pb' + +module Test + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Enum Classes + # + class EnumTestType < ::Protobuf::Enum + define :ZERO, 0 + define :ONE, 1 + define :TWO, 2 + end + + class AliasedEnum < ::Protobuf::Enum + set_option :allow_alias, true + + define :THREE, 3 + define :TRES, 3 + define :FOUR, 4 + define :CUATRO, 4 + end + + + ## + # Message Classes + # + class EnumTestMessage < ::Protobuf::Message; end + + + ## + # Message Fields + # + class EnumTestMessage + optional ::Test::EnumTestType, :non_default_enum, 1 + optional ::Test::EnumTestType, :default_enum, 2, :default => ::Test::EnumTestType::ONE + repeated ::Test::EnumTestType, :repeated_enums, 3 + optional ::Test::AliasedEnum, :alias_non_default_enum, 4 + optional ::Test::AliasedEnum, :alias_default_enum, 5, :default => ::Test::AliasedEnum::CUATRO + repeated ::Test::AliasedEnum, :alias_repeated_enums, 6 + end + + + ## + # Extended Message Fields + # + class ::Test::Resource < ::Protobuf::Message + optional :int32, :".test.ext_other_file_defined_field", 200, :extension => true + end + +end + diff --git a/spec/support/protos/enum.proto b/spec/support/protos/enum.proto new file mode 100644 index 00000000..1d1abb32 --- /dev/null +++ b/spec/support/protos/enum.proto @@ -0,0 +1,37 @@ +syntax = "proto2"; + +package test; +import 'protos/resource.proto'; + +// Test extending another message from an imported file. + +enum EnumTestType { + ZERO = 0; + ONE = 1; + TWO = 2; +} + +// Uses aliases +enum AliasedEnum { + option allow_alias = true; + + THREE = 3; + TRES = 3; + + FOUR = 4; + CUATRO = 4; +} + +message EnumTestMessage { + optional EnumTestType non_default_enum = 1; + optional EnumTestType default_enum = 2 [default=ONE]; + repeated EnumTestType repeated_enums = 3; + + optional AliasedEnum alias_non_default_enum = 4; + optional AliasedEnum alias_default_enum = 5 [default=CUATRO]; + repeated AliasedEnum alias_repeated_enums = 6; +} + +extend test.Resource { + optional int32 ext_other_file_defined_field = 200; +} diff --git a/spec/support/protos/extreme_values.data.bin b/spec/support/protos/extreme_values.data.bin new file mode 100644 index 00000000..f2f67cfb Binary files /dev/null and b/spec/support/protos/extreme_values.data.bin differ diff --git a/spec/support/protos/google_unittest.bin b/spec/support/protos/google_unittest.bin new file mode 100644 index 00000000..6b22ec1b Binary files /dev/null and b/spec/support/protos/google_unittest.bin differ diff --git a/spec/support/protos/google_unittest.pb.rb b/spec/support/protos/google_unittest.pb.rb new file mode 100644 index 00000000..c2c7d650 --- /dev/null +++ b/spec/support/protos/google_unittest.pb.rb @@ -0,0 +1,798 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' +require 'protobuf/rpc/service' + + +## +# Imports +# +require 'protos/google_unittest_import.pb' + +module Protobuf_unittest + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Enum Classes + # + class ForeignEnum < ::Protobuf::Enum + define :FOREIGN_FOO, 4 + define :FOREIGN_BAR, 5 + define :FOREIGN_BAZ, 6 + end + + class TestEnumWithDupValue < ::Protobuf::Enum + set_option :allow_alias, true + + define :FOO1, 1 + define :BAR1, 2 + define :BAZ, 3 + define :FOO2, 1 + define :BAR2, 2 + end + + class TestSparseEnum < ::Protobuf::Enum + define :SPARSE_A, 123 + define :SPARSE_B, 62374 + define :SPARSE_C, 12589234 + define :SPARSE_D, -15 + define :SPARSE_E, -53452 + define :SPARSE_F, 0 + define :SPARSE_G, 2 + end + + + ## + # Message Classes + # + class TestAllTypes < ::Protobuf::Message + class NestedEnum < ::Protobuf::Enum + define :FOO, 1 + define :BAR, 2 + define :BAZ, 3 + define :NEG, -1 + end + + class NestedMessage < ::Protobuf::Message; end + class OptionalGroup < ::Protobuf::Message; end + class RepeatedGroup < ::Protobuf::Message; end + + end + + class NestedTestAllTypes < ::Protobuf::Message; end + class TestDeprecatedFields < ::Protobuf::Message; end + class ForeignMessage < ::Protobuf::Message; end + class TestReservedFields < ::Protobuf::Message; end + class TestAllExtensions < ::Protobuf::Message; end + class OptionalGroup_extension < ::Protobuf::Message; end + class RepeatedGroup_extension < ::Protobuf::Message; end + class TestNestedExtension < ::Protobuf::Message; end + class TestMoreNestedExtension < ::Protobuf::Message; end + class TestRequired < ::Protobuf::Message; end + class TestRequiredForeign < ::Protobuf::Message; end + class TestForeignNested < ::Protobuf::Message; end + class TestEmptyMessage < ::Protobuf::Message; end + class TestEmptyMessageWithExtensions < ::Protobuf::Message; end + class TestMultipleExtensionRanges < ::Protobuf::Message; end + class TestReallyLargeTagNumber < ::Protobuf::Message; end + class TestRecursiveMessage < ::Protobuf::Message; end + class TestMutualRecursionA < ::Protobuf::Message; end + class TestMutualRecursionB < ::Protobuf::Message; end + class TestDupFieldNumber < ::Protobuf::Message + class Foo < ::Protobuf::Message; end + class Bar < ::Protobuf::Message; end + + end + + class TestEagerMessage < ::Protobuf::Message; end + class TestLazyMessage < ::Protobuf::Message; end + class TestNestedMessageHasBits < ::Protobuf::Message + class NestedMessage < ::Protobuf::Message; end + + end + + class TestCamelCaseFieldNames < ::Protobuf::Message; end + class TestFieldOrderings < ::Protobuf::Message + class NestedMessage < ::Protobuf::Message; end + + end + + class TestExtremeDefaultValues < ::Protobuf::Message; end + class SparseEnumMessage < ::Protobuf::Message; end + class OneString < ::Protobuf::Message; end + class MoreString < ::Protobuf::Message; end + class OneBytes < ::Protobuf::Message; end + class MoreBytes < ::Protobuf::Message; end + class Int32Message < ::Protobuf::Message; end + class Uint32Message < ::Protobuf::Message; end + class Int64Message < ::Protobuf::Message; end + class Uint64Message < ::Protobuf::Message; end + class BoolMessage < ::Protobuf::Message; end + class TestOneof < ::Protobuf::Message + class FooGroup < ::Protobuf::Message; end + + end + + class TestOneofBackwardsCompatible < ::Protobuf::Message + class FooGroup < ::Protobuf::Message; end + + end + + class TestOneof2 < ::Protobuf::Message + class NestedEnum < ::Protobuf::Enum + define :FOO, 1 + define :BAR, 2 + define :BAZ, 3 + end + + class FooGroup < ::Protobuf::Message; end + class NestedMessage < ::Protobuf::Message; end + + end + + class TestRequiredOneof < ::Protobuf::Message + class NestedMessage < ::Protobuf::Message; end + + end + + class TestPackedTypes < ::Protobuf::Message; end + class TestUnpackedTypes < ::Protobuf::Message; end + class TestPackedExtensions < ::Protobuf::Message; end + class TestUnpackedExtensions < ::Protobuf::Message; end + class TestDynamicExtensions < ::Protobuf::Message + class DynamicEnumType < ::Protobuf::Enum + define :DYNAMIC_FOO, 2200 + define :DYNAMIC_BAR, 2201 + define :DYNAMIC_BAZ, 2202 + end + + class DynamicMessageType < ::Protobuf::Message; end + + end + + class TestRepeatedScalarDifferentTagSizes < ::Protobuf::Message; end + class TestParsingMerge < ::Protobuf::Message + class RepeatedFieldsGenerator < ::Protobuf::Message + class Group1 < ::Protobuf::Message; end + class Group2 < ::Protobuf::Message; end + + end + + class OptionalGroup < ::Protobuf::Message; end + class RepeatedGroup < ::Protobuf::Message; end + + end + + class TestCommentInjectionMessage < ::Protobuf::Message; end + class FooRequest < ::Protobuf::Message; end + class FooResponse < ::Protobuf::Message; end + class FooClientMessage < ::Protobuf::Message; end + class FooServerMessage < ::Protobuf::Message; end + class BarRequest < ::Protobuf::Message; end + class BarResponse < ::Protobuf::Message; end + + + ## + # File Options + # + set_option :java_outer_classname, "UnittestProto" + set_option :optimize_for, ::Google::Protobuf::FileOptions::OptimizeMode::SPEED + set_option :cc_generic_services, true + set_option :java_generic_services, true + set_option :py_generic_services, true + set_option :cc_enable_arenas, true + + + ## + # Message Fields + # + class TestAllTypes + class NestedMessage + optional :int32, :bb, 1 + end + + class OptionalGroup + optional :int32, :a, 17 + end + + class RepeatedGroup + optional :int32, :a, 47 + end + + optional :int32, :optional_int32, 1 + optional :int64, :optional_int64, 2 + optional :uint32, :optional_uint32, 3 + optional :uint64, :optional_uint64, 4 + optional :sint32, :optional_sint32, 5 + optional :sint64, :optional_sint64, 6 + optional :fixed32, :optional_fixed32, 7 + optional :fixed64, :optional_fixed64, 8 + optional :sfixed32, :optional_sfixed32, 9 + optional :sfixed64, :optional_sfixed64, 10 + optional :float, :optional_float, 11 + optional :double, :optional_double, 12 + optional :bool, :optional_bool, 13 + optional :string, :optional_string, 14 + optional :bytes, :optional_bytes, 15 + optional ::Protobuf_unittest::TestAllTypes::OptionalGroup, :optionalgroup, 16 + optional ::Protobuf_unittest::TestAllTypes::NestedMessage, :optional_nested_message, 18 + optional ::Protobuf_unittest::ForeignMessage, :optional_foreign_message, 19 + optional ::Protobuf_unittest_import::ImportMessage, :optional_import_message, 20 + optional ::Protobuf_unittest::TestAllTypes::NestedEnum, :optional_nested_enum, 21 + optional ::Protobuf_unittest::ForeignEnum, :optional_foreign_enum, 22 + optional ::Protobuf_unittest_import::ImportEnum, :optional_import_enum, 23 + optional :string, :optional_string_piece, 24, :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + optional :string, :optional_cord, 25, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + optional ::Protobuf_unittest_import::PublicImportMessage, :optional_public_import_message, 26 + optional ::Protobuf_unittest::TestAllTypes::NestedMessage, :optional_lazy_message, 27, :lazy => true + repeated :int32, :repeated_int32, 31 + repeated :int64, :repeated_int64, 32 + repeated :uint32, :repeated_uint32, 33 + repeated :uint64, :repeated_uint64, 34 + repeated :sint32, :repeated_sint32, 35 + repeated :sint64, :repeated_sint64, 36 + repeated :fixed32, :repeated_fixed32, 37 + repeated :fixed64, :repeated_fixed64, 38 + repeated :sfixed32, :repeated_sfixed32, 39 + repeated :sfixed64, :repeated_sfixed64, 40 + repeated :float, :repeated_float, 41 + repeated :double, :repeated_double, 42 + repeated :bool, :repeated_bool, 43 + repeated :string, :repeated_string, 44 + repeated :bytes, :repeated_bytes, 45 + repeated ::Protobuf_unittest::TestAllTypes::RepeatedGroup, :repeatedgroup, 46 + repeated ::Protobuf_unittest::TestAllTypes::NestedMessage, :repeated_nested_message, 48 + repeated ::Protobuf_unittest::ForeignMessage, :repeated_foreign_message, 49 + repeated ::Protobuf_unittest_import::ImportMessage, :repeated_import_message, 50 + repeated ::Protobuf_unittest::TestAllTypes::NestedEnum, :repeated_nested_enum, 51 + repeated ::Protobuf_unittest::ForeignEnum, :repeated_foreign_enum, 52 + repeated ::Protobuf_unittest_import::ImportEnum, :repeated_import_enum, 53 + repeated :string, :repeated_string_piece, 54, :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + repeated :string, :repeated_cord, 55, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + repeated ::Protobuf_unittest::TestAllTypes::NestedMessage, :repeated_lazy_message, 57, :lazy => true + optional :int32, :default_int32, 61, :default => 41 + optional :int64, :default_int64, 62, :default => 42 + optional :uint32, :default_uint32, 63, :default => 43 + optional :uint64, :default_uint64, 64, :default => 44 + optional :sint32, :default_sint32, 65, :default => -45 + optional :sint64, :default_sint64, 66, :default => 46 + optional :fixed32, :default_fixed32, 67, :default => 47 + optional :fixed64, :default_fixed64, 68, :default => 48 + optional :sfixed32, :default_sfixed32, 69, :default => 49 + optional :sfixed64, :default_sfixed64, 70, :default => -50 + optional :float, :default_float, 71, :default => 51.5 + optional :double, :default_double, 72, :default => 52000 + optional :bool, :default_bool, 73, :default => true + optional :string, :default_string, 74, :default => "hello" + optional :bytes, :default_bytes, 75, :default => "world" + optional ::Protobuf_unittest::TestAllTypes::NestedEnum, :default_nested_enum, 81, :default => ::Protobuf_unittest::TestAllTypes::NestedEnum::BAR + optional ::Protobuf_unittest::ForeignEnum, :default_foreign_enum, 82, :default => ::Protobuf_unittest::ForeignEnum::FOREIGN_BAR + optional ::Protobuf_unittest_import::ImportEnum, :default_import_enum, 83, :default => ::Protobuf_unittest_import::ImportEnum::IMPORT_BAR + optional :string, :default_string_piece, 84, :default => "abc", :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + optional :string, :default_cord, 85, :default => "123", :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + optional :uint32, :oneof_uint32, 111 + optional ::Protobuf_unittest::TestAllTypes::NestedMessage, :oneof_nested_message, 112 + optional :string, :oneof_string, 113 + optional :bytes, :oneof_bytes, 114 + end + + class NestedTestAllTypes + optional ::Protobuf_unittest::NestedTestAllTypes, :child, 1 + optional ::Protobuf_unittest::TestAllTypes, :payload, 2 + repeated ::Protobuf_unittest::NestedTestAllTypes, :repeated_child, 3 + end + + class TestDeprecatedFields + optional :int32, :deprecated_int32, 1, :deprecated => true + end + + class ForeignMessage + optional :int32, :c, 1 + end + + class TestAllExtensions + # Extension Fields + extensions 1...536870912 + optional :int32, :".protobuf_unittest.optional_int32_extension", 1, :extension => true + optional :int64, :".protobuf_unittest.optional_int64_extension", 2, :extension => true + optional :uint32, :".protobuf_unittest.optional_uint32_extension", 3, :extension => true + optional :uint64, :".protobuf_unittest.optional_uint64_extension", 4, :extension => true + optional :sint32, :".protobuf_unittest.optional_sint32_extension", 5, :extension => true + optional :sint64, :".protobuf_unittest.optional_sint64_extension", 6, :extension => true + optional :fixed32, :".protobuf_unittest.optional_fixed32_extension", 7, :extension => true + optional :fixed64, :".protobuf_unittest.optional_fixed64_extension", 8, :extension => true + optional :sfixed32, :".protobuf_unittest.optional_sfixed32_extension", 9, :extension => true + optional :sfixed64, :".protobuf_unittest.optional_sfixed64_extension", 10, :extension => true + optional :float, :".protobuf_unittest.optional_float_extension", 11, :extension => true + optional :double, :".protobuf_unittest.optional_double_extension", 12, :extension => true + optional :bool, :".protobuf_unittest.optional_bool_extension", 13, :extension => true + optional :string, :".protobuf_unittest.optional_string_extension", 14, :extension => true + optional :bytes, :".protobuf_unittest.optional_bytes_extension", 15, :extension => true + optional ::Protobuf_unittest::OptionalGroup_extension, :".protobuf_unittest.optionalgroup_extension", 16, :extension => true + optional ::Protobuf_unittest::TestAllTypes::NestedMessage, :".protobuf_unittest.optional_nested_message_extension", 18, :extension => true + optional ::Protobuf_unittest::ForeignMessage, :".protobuf_unittest.optional_foreign_message_extension", 19, :extension => true + optional ::Protobuf_unittest_import::ImportMessage, :".protobuf_unittest.optional_import_message_extension", 20, :extension => true + optional ::Protobuf_unittest::TestAllTypes::NestedEnum, :".protobuf_unittest.optional_nested_enum_extension", 21, :extension => true + optional ::Protobuf_unittest::ForeignEnum, :".protobuf_unittest.optional_foreign_enum_extension", 22, :extension => true + optional ::Protobuf_unittest_import::ImportEnum, :".protobuf_unittest.optional_import_enum_extension", 23, :extension => true + optional :string, :".protobuf_unittest.optional_string_piece_extension", 24, :extension => true, :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + optional :string, :".protobuf_unittest.optional_cord_extension", 25, :extension => true, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + optional ::Protobuf_unittest_import::PublicImportMessage, :".protobuf_unittest.optional_public_import_message_extension", 26, :extension => true + optional ::Protobuf_unittest::TestAllTypes::NestedMessage, :".protobuf_unittest.optional_lazy_message_extension", 27, :extension => true, :lazy => true + repeated :int32, :".protobuf_unittest.repeated_int32_extension", 31, :extension => true + repeated :int64, :".protobuf_unittest.repeated_int64_extension", 32, :extension => true + repeated :uint32, :".protobuf_unittest.repeated_uint32_extension", 33, :extension => true + repeated :uint64, :".protobuf_unittest.repeated_uint64_extension", 34, :extension => true + repeated :sint32, :".protobuf_unittest.repeated_sint32_extension", 35, :extension => true + repeated :sint64, :".protobuf_unittest.repeated_sint64_extension", 36, :extension => true + repeated :fixed32, :".protobuf_unittest.repeated_fixed32_extension", 37, :extension => true + repeated :fixed64, :".protobuf_unittest.repeated_fixed64_extension", 38, :extension => true + repeated :sfixed32, :".protobuf_unittest.repeated_sfixed32_extension", 39, :extension => true + repeated :sfixed64, :".protobuf_unittest.repeated_sfixed64_extension", 40, :extension => true + repeated :float, :".protobuf_unittest.repeated_float_extension", 41, :extension => true + repeated :double, :".protobuf_unittest.repeated_double_extension", 42, :extension => true + repeated :bool, :".protobuf_unittest.repeated_bool_extension", 43, :extension => true + repeated :string, :".protobuf_unittest.repeated_string_extension", 44, :extension => true + repeated :bytes, :".protobuf_unittest.repeated_bytes_extension", 45, :extension => true + repeated ::Protobuf_unittest::RepeatedGroup_extension, :".protobuf_unittest.repeatedgroup_extension", 46, :extension => true + repeated ::Protobuf_unittest::TestAllTypes::NestedMessage, :".protobuf_unittest.repeated_nested_message_extension", 48, :extension => true + repeated ::Protobuf_unittest::ForeignMessage, :".protobuf_unittest.repeated_foreign_message_extension", 49, :extension => true + repeated ::Protobuf_unittest_import::ImportMessage, :".protobuf_unittest.repeated_import_message_extension", 50, :extension => true + repeated ::Protobuf_unittest::TestAllTypes::NestedEnum, :".protobuf_unittest.repeated_nested_enum_extension", 51, :extension => true + repeated ::Protobuf_unittest::ForeignEnum, :".protobuf_unittest.repeated_foreign_enum_extension", 52, :extension => true + repeated ::Protobuf_unittest_import::ImportEnum, :".protobuf_unittest.repeated_import_enum_extension", 53, :extension => true + repeated :string, :".protobuf_unittest.repeated_string_piece_extension", 54, :extension => true, :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + repeated :string, :".protobuf_unittest.repeated_cord_extension", 55, :extension => true, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + repeated ::Protobuf_unittest::TestAllTypes::NestedMessage, :".protobuf_unittest.repeated_lazy_message_extension", 57, :extension => true, :lazy => true + optional :int32, :".protobuf_unittest.default_int32_extension", 61, :default => 41, :extension => true + optional :int64, :".protobuf_unittest.default_int64_extension", 62, :default => 42, :extension => true + optional :uint32, :".protobuf_unittest.default_uint32_extension", 63, :default => 43, :extension => true + optional :uint64, :".protobuf_unittest.default_uint64_extension", 64, :default => 44, :extension => true + optional :sint32, :".protobuf_unittest.default_sint32_extension", 65, :default => -45, :extension => true + optional :sint64, :".protobuf_unittest.default_sint64_extension", 66, :default => 46, :extension => true + optional :fixed32, :".protobuf_unittest.default_fixed32_extension", 67, :default => 47, :extension => true + optional :fixed64, :".protobuf_unittest.default_fixed64_extension", 68, :default => 48, :extension => true + optional :sfixed32, :".protobuf_unittest.default_sfixed32_extension", 69, :default => 49, :extension => true + optional :sfixed64, :".protobuf_unittest.default_sfixed64_extension", 70, :default => -50, :extension => true + optional :float, :".protobuf_unittest.default_float_extension", 71, :default => 51.5, :extension => true + optional :double, :".protobuf_unittest.default_double_extension", 72, :default => 52000, :extension => true + optional :bool, :".protobuf_unittest.default_bool_extension", 73, :default => true, :extension => true + optional :string, :".protobuf_unittest.default_string_extension", 74, :default => "hello", :extension => true + optional :bytes, :".protobuf_unittest.default_bytes_extension", 75, :default => "world", :extension => true + optional ::Protobuf_unittest::TestAllTypes::NestedEnum, :".protobuf_unittest.default_nested_enum_extension", 81, :default => ::Protobuf_unittest::TestAllTypes::NestedEnum::BAR, :extension => true + optional ::Protobuf_unittest::ForeignEnum, :".protobuf_unittest.default_foreign_enum_extension", 82, :default => ::Protobuf_unittest::ForeignEnum::FOREIGN_BAR, :extension => true + optional ::Protobuf_unittest_import::ImportEnum, :".protobuf_unittest.default_import_enum_extension", 83, :default => ::Protobuf_unittest_import::ImportEnum::IMPORT_BAR, :extension => true + optional :string, :".protobuf_unittest.default_string_piece_extension", 84, :default => "abc", :extension => true, :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + optional :string, :".protobuf_unittest.default_cord_extension", 85, :default => "123", :extension => true, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + optional :uint32, :".protobuf_unittest.oneof_uint32_extension", 111, :extension => true + optional ::Protobuf_unittest::TestAllTypes::NestedMessage, :".protobuf_unittest.oneof_nested_message_extension", 112, :extension => true + optional :string, :".protobuf_unittest.oneof_string_extension", 113, :extension => true + optional :bytes, :".protobuf_unittest.oneof_bytes_extension", 114, :extension => true + optional :string, :".protobuf_unittest.TestNestedExtension.test", 1002, :default => "test", :extension => true + optional :string, :".protobuf_unittest.TestNestedExtension.nested_string_extension", 1003, :extension => true + optional :string, :".protobuf_unittest.TestMoreNestedExtension.test", 1004, :default => "a different test", :extension => true + optional ::Protobuf_unittest::TestRequired, :".protobuf_unittest.TestRequired.single", 1000, :extension => true + repeated ::Protobuf_unittest::TestRequired, :".protobuf_unittest.TestRequired.multi", 1001, :extension => true + end + + class OptionalGroup_extension + optional :int32, :a, 17 + end + + class RepeatedGroup_extension + optional :int32, :a, 47 + end + + class TestRequired + required :int32, :a, 1 + optional :int32, :dummy2, 2 + required :int32, :b, 3 + optional :int32, :dummy4, 4 + optional :int32, :dummy5, 5 + optional :int32, :dummy6, 6 + optional :int32, :dummy7, 7 + optional :int32, :dummy8, 8 + optional :int32, :dummy9, 9 + optional :int32, :dummy10, 10 + optional :int32, :dummy11, 11 + optional :int32, :dummy12, 12 + optional :int32, :dummy13, 13 + optional :int32, :dummy14, 14 + optional :int32, :dummy15, 15 + optional :int32, :dummy16, 16 + optional :int32, :dummy17, 17 + optional :int32, :dummy18, 18 + optional :int32, :dummy19, 19 + optional :int32, :dummy20, 20 + optional :int32, :dummy21, 21 + optional :int32, :dummy22, 22 + optional :int32, :dummy23, 23 + optional :int32, :dummy24, 24 + optional :int32, :dummy25, 25 + optional :int32, :dummy26, 26 + optional :int32, :dummy27, 27 + optional :int32, :dummy28, 28 + optional :int32, :dummy29, 29 + optional :int32, :dummy30, 30 + optional :int32, :dummy31, 31 + optional :int32, :dummy32, 32 + required :int32, :c, 33 + end + + class TestRequiredForeign + optional ::Protobuf_unittest::TestRequired, :optional_message, 1 + repeated ::Protobuf_unittest::TestRequired, :repeated_message, 2 + optional :int32, :dummy, 3 + end + + class TestForeignNested + optional ::Protobuf_unittest::TestAllTypes::NestedMessage, :foreign_nested, 1 + end + + class TestReallyLargeTagNumber + optional :int32, :a, 1 + optional :int32, :bb, 268435455 + end + + class TestRecursiveMessage + optional ::Protobuf_unittest::TestRecursiveMessage, :a, 1 + optional :int32, :i, 2 + end + + class TestMutualRecursionA + optional ::Protobuf_unittest::TestMutualRecursionB, :bb, 1 + end + + class TestMutualRecursionB + optional ::Protobuf_unittest::TestMutualRecursionA, :a, 1 + optional :int32, :optional_int32, 2 + end + + class TestDupFieldNumber + class Foo + optional :int32, :a, 1 + end + + class Bar + optional :int32, :a, 1 + end + + optional :int32, :a, 1 + optional ::Protobuf_unittest::TestDupFieldNumber::Foo, :foo, 2 + optional ::Protobuf_unittest::TestDupFieldNumber::Bar, :bar, 3 + end + + class TestEagerMessage + optional ::Protobuf_unittest::TestAllTypes, :sub_message, 1, :lazy => false + end + + class TestLazyMessage + optional ::Protobuf_unittest::TestAllTypes, :sub_message, 1, :lazy => true + end + + class TestNestedMessageHasBits + class NestedMessage + repeated :int32, :nestedmessage_repeated_int32, 1 + repeated ::Protobuf_unittest::ForeignMessage, :nestedmessage_repeated_foreignmessage, 2 + end + + optional ::Protobuf_unittest::TestNestedMessageHasBits::NestedMessage, :optional_nested_message, 1 + end + + class TestCamelCaseFieldNames + optional :int32, :PrimitiveField, 1 + optional :string, :StringField, 2 + optional ::Protobuf_unittest::ForeignEnum, :EnumField, 3 + optional ::Protobuf_unittest::ForeignMessage, :MessageField, 4 + optional :string, :StringPieceField, 5, :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + optional :string, :CordField, 6, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + repeated :int32, :RepeatedPrimitiveField, 7 + repeated :string, :RepeatedStringField, 8 + repeated ::Protobuf_unittest::ForeignEnum, :RepeatedEnumField, 9 + repeated ::Protobuf_unittest::ForeignMessage, :RepeatedMessageField, 10 + repeated :string, :RepeatedStringPieceField, 11, :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + repeated :string, :RepeatedCordField, 12, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + end + + class TestFieldOrderings + class NestedMessage + optional :int64, :oo, 2 + optional :int32, :bb, 1 + end + + optional :string, :my_string, 11 + optional :int64, :my_int, 1 + optional :float, :my_float, 101 + optional ::Protobuf_unittest::TestFieldOrderings::NestedMessage, :optional_nested_message, 200 + # Extension Fields + extensions 2...11 + extensions 12...101 + optional :string, :".protobuf_unittest.my_extension_string", 50, :extension => true + optional :int32, :".protobuf_unittest.my_extension_int", 5, :extension => true + end + + class TestExtremeDefaultValues + optional :bytes, :escaped_bytes, 1, :default => "\000\001\007\010\014\n\r\t\013\\\\'\"\376" + optional :uint32, :large_uint32, 2, :default => 4294967295 + optional :uint64, :large_uint64, 3, :default => 18446744073709551615 + optional :int32, :small_int32, 4, :default => -2147483647 + optional :int64, :small_int64, 5, :default => -9223372036854775807 + optional :int32, :really_small_int32, 21, :default => -2147483648 + optional :int64, :really_small_int64, 22, :default => -9223372036854775808 + optional :string, :utf8_string, 6, :default => "ሴ" + optional :float, :zero_float, 7, :default => 0 + optional :float, :one_float, 8, :default => 1 + optional :float, :small_float, 9, :default => 1.5 + optional :float, :negative_one_float, 10, :default => -1 + optional :float, :negative_float, 11, :default => -1.5 + optional :float, :large_float, 12, :default => 2e+08 + optional :float, :small_negative_float, 13, :default => -8e-28 + optional :double, :inf_double, 14, :default => ::Float::INFINITY + optional :double, :neg_inf_double, 15, :default => -::Float::INFINITY + optional :double, :nan_double, 16, :default => ::Float::NAN + optional :float, :inf_float, 17, :default => ::Float::INFINITY + optional :float, :neg_inf_float, 18, :default => -::Float::INFINITY + optional :float, :nan_float, 19, :default => ::Float::NAN + optional :string, :cpp_trigraph, 20, :default => "? ? ?? ?? ??? ??/ ??-" + optional :string, :string_with_zero, 23, :default => "hello" + optional :bytes, :bytes_with_zero, 24, :default => "wor\000ld" + optional :string, :string_piece_with_zero, 25, :default => "abc", :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + optional :string, :cord_with_zero, 26, :default => "123", :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + optional :string, :replacement_string, 27, :default => "${unknown}" + end + + class SparseEnumMessage + optional ::Protobuf_unittest::TestSparseEnum, :sparse_enum, 1 + end + + class OneString + optional :string, :data, 1 + end + + class MoreString + repeated :string, :data, 1 + end + + class OneBytes + optional :bytes, :data, 1 + end + + class MoreBytes + repeated :bytes, :data, 1 + end + + class Int32Message + optional :int32, :data, 1 + end + + class Uint32Message + optional :uint32, :data, 1 + end + + class Int64Message + optional :int64, :data, 1 + end + + class Uint64Message + optional :uint64, :data, 1 + end + + class BoolMessage + optional :bool, :data, 1 + end + + class TestOneof + class FooGroup + optional :int32, :a, 5 + optional :string, :b, 6 + end + + optional :int32, :foo_int, 1 + optional :string, :foo_string, 2 + optional ::Protobuf_unittest::TestAllTypes, :foo_message, 3 + optional ::Protobuf_unittest::TestOneof::FooGroup, :foogroup, 4 + end + + class TestOneofBackwardsCompatible + class FooGroup + optional :int32, :a, 5 + optional :string, :b, 6 + end + + optional :int32, :foo_int, 1 + optional :string, :foo_string, 2 + optional ::Protobuf_unittest::TestAllTypes, :foo_message, 3 + optional ::Protobuf_unittest::TestOneofBackwardsCompatible::FooGroup, :foogroup, 4 + end + + class TestOneof2 + class FooGroup + optional :int32, :a, 9 + optional :string, :b, 10 + end + + class NestedMessage + optional :int64, :qux_int, 1 + repeated :int32, :corge_int, 2 + end + + optional :int32, :foo_int, 1 + optional :string, :foo_string, 2 + optional :string, :foo_cord, 3, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + optional :string, :foo_string_piece, 4, :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + optional :bytes, :foo_bytes, 5 + optional ::Protobuf_unittest::TestOneof2::NestedEnum, :foo_enum, 6 + optional ::Protobuf_unittest::TestOneof2::NestedMessage, :foo_message, 7 + optional ::Protobuf_unittest::TestOneof2::FooGroup, :foogroup, 8 + optional ::Protobuf_unittest::TestOneof2::NestedMessage, :foo_lazy_message, 11, :lazy => true + optional :int32, :bar_int, 12, :default => 5 + optional :string, :bar_string, 13, :default => "STRING" + optional :string, :bar_cord, 14, :default => "CORD", :ctype => ::Google::Protobuf::FieldOptions::CType::CORD + optional :string, :bar_string_piece, 15, :default => "SPIECE", :ctype => ::Google::Protobuf::FieldOptions::CType::STRING_PIECE + optional :bytes, :bar_bytes, 16, :default => "BYTES" + optional ::Protobuf_unittest::TestOneof2::NestedEnum, :bar_enum, 17, :default => ::Protobuf_unittest::TestOneof2::NestedEnum::BAR + optional :int32, :baz_int, 18 + optional :string, :baz_string, 19, :default => "BAZ" + end + + class TestRequiredOneof + class NestedMessage + required :double, :required_double, 1 + end + + optional :int32, :foo_int, 1 + optional :string, :foo_string, 2 + optional ::Protobuf_unittest::TestRequiredOneof::NestedMessage, :foo_message, 3 + end + + class TestPackedTypes + repeated :int32, :packed_int32, 90, :packed => true + repeated :int64, :packed_int64, 91, :packed => true + repeated :uint32, :packed_uint32, 92, :packed => true + repeated :uint64, :packed_uint64, 93, :packed => true + repeated :sint32, :packed_sint32, 94, :packed => true + repeated :sint64, :packed_sint64, 95, :packed => true + repeated :fixed32, :packed_fixed32, 96, :packed => true + repeated :fixed64, :packed_fixed64, 97, :packed => true + repeated :sfixed32, :packed_sfixed32, 98, :packed => true + repeated :sfixed64, :packed_sfixed64, 99, :packed => true + repeated :float, :packed_float, 100, :packed => true + repeated :double, :packed_double, 101, :packed => true + repeated :bool, :packed_bool, 102, :packed => true + repeated ::Protobuf_unittest::ForeignEnum, :packed_enum, 103, :packed => true + end + + class TestUnpackedTypes + repeated :int32, :unpacked_int32, 90, :packed => false + repeated :int64, :unpacked_int64, 91, :packed => false + repeated :uint32, :unpacked_uint32, 92, :packed => false + repeated :uint64, :unpacked_uint64, 93, :packed => false + repeated :sint32, :unpacked_sint32, 94, :packed => false + repeated :sint64, :unpacked_sint64, 95, :packed => false + repeated :fixed32, :unpacked_fixed32, 96, :packed => false + repeated :fixed64, :unpacked_fixed64, 97, :packed => false + repeated :sfixed32, :unpacked_sfixed32, 98, :packed => false + repeated :sfixed64, :unpacked_sfixed64, 99, :packed => false + repeated :float, :unpacked_float, 100, :packed => false + repeated :double, :unpacked_double, 101, :packed => false + repeated :bool, :unpacked_bool, 102, :packed => false + repeated ::Protobuf_unittest::ForeignEnum, :unpacked_enum, 103, :packed => false + end + + class TestPackedExtensions + # Extension Fields + extensions 1...536870912 + repeated :int32, :".protobuf_unittest.packed_int32_extension", 90, :packed => true, :extension => true + repeated :int64, :".protobuf_unittest.packed_int64_extension", 91, :packed => true, :extension => true + repeated :uint32, :".protobuf_unittest.packed_uint32_extension", 92, :packed => true, :extension => true + repeated :uint64, :".protobuf_unittest.packed_uint64_extension", 93, :packed => true, :extension => true + repeated :sint32, :".protobuf_unittest.packed_sint32_extension", 94, :packed => true, :extension => true + repeated :sint64, :".protobuf_unittest.packed_sint64_extension", 95, :packed => true, :extension => true + repeated :fixed32, :".protobuf_unittest.packed_fixed32_extension", 96, :packed => true, :extension => true + repeated :fixed64, :".protobuf_unittest.packed_fixed64_extension", 97, :packed => true, :extension => true + repeated :sfixed32, :".protobuf_unittest.packed_sfixed32_extension", 98, :packed => true, :extension => true + repeated :sfixed64, :".protobuf_unittest.packed_sfixed64_extension", 99, :packed => true, :extension => true + repeated :float, :".protobuf_unittest.packed_float_extension", 100, :packed => true, :extension => true + repeated :double, :".protobuf_unittest.packed_double_extension", 101, :packed => true, :extension => true + repeated :bool, :".protobuf_unittest.packed_bool_extension", 102, :packed => true, :extension => true + repeated ::Protobuf_unittest::ForeignEnum, :".protobuf_unittest.packed_enum_extension", 103, :packed => true, :extension => true + end + + class TestUnpackedExtensions + # Extension Fields + extensions 1...536870912 + repeated :int32, :".protobuf_unittest.unpacked_int32_extension", 90, :extension => true, :packed => false + repeated :int64, :".protobuf_unittest.unpacked_int64_extension", 91, :extension => true, :packed => false + repeated :uint32, :".protobuf_unittest.unpacked_uint32_extension", 92, :extension => true, :packed => false + repeated :uint64, :".protobuf_unittest.unpacked_uint64_extension", 93, :extension => true, :packed => false + repeated :sint32, :".protobuf_unittest.unpacked_sint32_extension", 94, :extension => true, :packed => false + repeated :sint64, :".protobuf_unittest.unpacked_sint64_extension", 95, :extension => true, :packed => false + repeated :fixed32, :".protobuf_unittest.unpacked_fixed32_extension", 96, :extension => true, :packed => false + repeated :fixed64, :".protobuf_unittest.unpacked_fixed64_extension", 97, :extension => true, :packed => false + repeated :sfixed32, :".protobuf_unittest.unpacked_sfixed32_extension", 98, :extension => true, :packed => false + repeated :sfixed64, :".protobuf_unittest.unpacked_sfixed64_extension", 99, :extension => true, :packed => false + repeated :float, :".protobuf_unittest.unpacked_float_extension", 100, :extension => true, :packed => false + repeated :double, :".protobuf_unittest.unpacked_double_extension", 101, :extension => true, :packed => false + repeated :bool, :".protobuf_unittest.unpacked_bool_extension", 102, :extension => true, :packed => false + repeated ::Protobuf_unittest::ForeignEnum, :".protobuf_unittest.unpacked_enum_extension", 103, :extension => true, :packed => false + end + + class TestDynamicExtensions + class DynamicMessageType + optional :int32, :dynamic_field, 2100 + end + + optional :fixed32, :scalar_extension, 2000 + optional ::Protobuf_unittest::ForeignEnum, :enum_extension, 2001 + optional ::Protobuf_unittest::TestDynamicExtensions::DynamicEnumType, :dynamic_enum_extension, 2002 + optional ::Protobuf_unittest::ForeignMessage, :message_extension, 2003 + optional ::Protobuf_unittest::TestDynamicExtensions::DynamicMessageType, :dynamic_message_extension, 2004 + repeated :string, :repeated_extension, 2005 + repeated :sint32, :packed_extension, 2006, :packed => true + end + + class TestRepeatedScalarDifferentTagSizes + repeated :fixed32, :repeated_fixed32, 12 + repeated :int32, :repeated_int32, 13 + repeated :fixed64, :repeated_fixed64, 2046 + repeated :int64, :repeated_int64, 2047 + repeated :float, :repeated_float, 262142 + repeated :uint64, :repeated_uint64, 262143 + end + + class TestParsingMerge + class RepeatedFieldsGenerator + class Group1 + optional ::Protobuf_unittest::TestAllTypes, :field1, 11 + end + + class Group2 + optional ::Protobuf_unittest::TestAllTypes, :field1, 21 + end + + repeated ::Protobuf_unittest::TestAllTypes, :field1, 1 + repeated ::Protobuf_unittest::TestAllTypes, :field2, 2 + repeated ::Protobuf_unittest::TestAllTypes, :field3, 3 + repeated ::Protobuf_unittest::TestParsingMerge::RepeatedFieldsGenerator::Group1, :group1, 10 + repeated ::Protobuf_unittest::TestParsingMerge::RepeatedFieldsGenerator::Group2, :group2, 20 + repeated ::Protobuf_unittest::TestAllTypes, :ext1, 1000 + repeated ::Protobuf_unittest::TestAllTypes, :ext2, 1001 + end + + class OptionalGroup + optional ::Protobuf_unittest::TestAllTypes, :optional_group_all_types, 11 + end + + class RepeatedGroup + optional ::Protobuf_unittest::TestAllTypes, :repeated_group_all_types, 21 + end + + required ::Protobuf_unittest::TestAllTypes, :required_all_types, 1 + optional ::Protobuf_unittest::TestAllTypes, :optional_all_types, 2 + repeated ::Protobuf_unittest::TestAllTypes, :repeated_all_types, 3 + optional ::Protobuf_unittest::TestParsingMerge::OptionalGroup, :optionalgroup, 10 + repeated ::Protobuf_unittest::TestParsingMerge::RepeatedGroup, :repeatedgroup, 20 + # Extension Fields + extensions 1000...536870912 + optional ::Protobuf_unittest::TestAllTypes, :".protobuf_unittest.TestParsingMerge.optional_ext", 1000, :extension => true + repeated ::Protobuf_unittest::TestAllTypes, :".protobuf_unittest.TestParsingMerge.repeated_ext", 1001, :extension => true + end + + class TestCommentInjectionMessage + optional :string, :a, 1, :default => "*/ <- Neither should this." + end + + + ## + # Service Classes + # + class TestService < ::Protobuf::Rpc::Service + rpc :foo, ::Protobuf_unittest::FooRequest, ::Protobuf_unittest::FooResponse + rpc :bar, ::Protobuf_unittest::BarRequest, ::Protobuf_unittest::BarResponse + end + +end + diff --git a/spec/support/protos/google_unittest.proto b/spec/support/protos/google_unittest.proto new file mode 100644 index 00000000..43169c75 --- /dev/null +++ b/spec/support/protos/google_unittest.proto @@ -0,0 +1,884 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// A proto file we will use for unit testing. + +syntax = "proto2"; + +// Some generic_services option(s) added automatically. +// See: http://go/proto2-generic-services-default +option cc_generic_services = true; // auto-added +option java_generic_services = true; // auto-added +option py_generic_services = true; // auto-added +option cc_enable_arenas = true; + +import "protos/google_unittest_import.proto"; + +// We don't put this in a package within proto2 because we need to make sure +// that the generated code doesn't depend on being in the proto2 namespace. +// In test_util.h we do "using namespace unittest = protobuf_unittest". +package protobuf_unittest; + +// Protos optimized for SPEED use a strict superset of the generated code +// of equivalent ones optimized for CODE_SIZE, so we should optimize all our +// tests for speed unless explicitly testing code size optimization. +option optimize_for = SPEED; + +option java_outer_classname = "UnittestProto"; + +// This proto includes every type of field in both singular and repeated +// forms. +message TestAllTypes { + message NestedMessage { + // The field name "b" fails to compile in proto1 because it conflicts with + // a local variable named "b" in one of the generated methods. Doh. + // This file needs to compile in proto1 to test backwards-compatibility. + optional int32 bb = 1; + } + + enum NestedEnum { + FOO = 1; + BAR = 2; + BAZ = 3; + NEG = -1; // Intentionally negative. + } + + // Singular + optional int32 optional_int32 = 1; + optional int64 optional_int64 = 2; + optional uint32 optional_uint32 = 3; + optional uint64 optional_uint64 = 4; + optional sint32 optional_sint32 = 5; + optional sint64 optional_sint64 = 6; + optional fixed32 optional_fixed32 = 7; + optional fixed64 optional_fixed64 = 8; + optional sfixed32 optional_sfixed32 = 9; + optional sfixed64 optional_sfixed64 = 10; + optional float optional_float = 11; + optional double optional_double = 12; + optional bool optional_bool = 13; + optional string optional_string = 14; + optional bytes optional_bytes = 15; + + optional group OptionalGroup = 16 { + optional int32 a = 17; + } + + optional NestedMessage optional_nested_message = 18; + optional ForeignMessage optional_foreign_message = 19; + optional protobuf_unittest_import.ImportMessage optional_import_message = 20; + + optional NestedEnum optional_nested_enum = 21; + optional ForeignEnum optional_foreign_enum = 22; + optional protobuf_unittest_import.ImportEnum optional_import_enum = 23; + + optional string optional_string_piece = 24 [ctype=STRING_PIECE]; + optional string optional_cord = 25 [ctype=CORD]; + + // Defined in unittest_import_public.proto + optional protobuf_unittest_import.PublicImportMessage + optional_public_import_message = 26; + + optional NestedMessage optional_lazy_message = 27 [lazy=true]; + + // Repeated + repeated int32 repeated_int32 = 31; + repeated int64 repeated_int64 = 32; + repeated uint32 repeated_uint32 = 33; + repeated uint64 repeated_uint64 = 34; + repeated sint32 repeated_sint32 = 35; + repeated sint64 repeated_sint64 = 36; + repeated fixed32 repeated_fixed32 = 37; + repeated fixed64 repeated_fixed64 = 38; + repeated sfixed32 repeated_sfixed32 = 39; + repeated sfixed64 repeated_sfixed64 = 40; + repeated float repeated_float = 41; + repeated double repeated_double = 42; + repeated bool repeated_bool = 43; + repeated string repeated_string = 44; + repeated bytes repeated_bytes = 45; + + repeated group RepeatedGroup = 46 { + optional int32 a = 47; + } + + repeated NestedMessage repeated_nested_message = 48; + repeated ForeignMessage repeated_foreign_message = 49; + repeated protobuf_unittest_import.ImportMessage repeated_import_message = 50; + + repeated NestedEnum repeated_nested_enum = 51; + repeated ForeignEnum repeated_foreign_enum = 52; + repeated protobuf_unittest_import.ImportEnum repeated_import_enum = 53; + + repeated string repeated_string_piece = 54 [ctype=STRING_PIECE]; + repeated string repeated_cord = 55 [ctype=CORD]; + + repeated NestedMessage repeated_lazy_message = 57 [lazy=true]; + + // Singular with defaults + optional int32 default_int32 = 61 [default = 41 ]; + optional int64 default_int64 = 62 [default = 42 ]; + optional uint32 default_uint32 = 63 [default = 43 ]; + optional uint64 default_uint64 = 64 [default = 44 ]; + optional sint32 default_sint32 = 65 [default = -45 ]; + optional sint64 default_sint64 = 66 [default = 46 ]; + optional fixed32 default_fixed32 = 67 [default = 47 ]; + optional fixed64 default_fixed64 = 68 [default = 48 ]; + optional sfixed32 default_sfixed32 = 69 [default = 49 ]; + optional sfixed64 default_sfixed64 = 70 [default = -50 ]; + optional float default_float = 71 [default = 51.5 ]; + optional double default_double = 72 [default = 52e3 ]; + optional bool default_bool = 73 [default = true ]; + optional string default_string = 74 [default = "hello"]; + optional bytes default_bytes = 75 [default = "world"]; + + optional NestedEnum default_nested_enum = 81 [default = BAR ]; + optional ForeignEnum default_foreign_enum = 82 [default = FOREIGN_BAR]; + optional protobuf_unittest_import.ImportEnum + default_import_enum = 83 [default = IMPORT_BAR]; + + optional string default_string_piece = 84 [ctype=STRING_PIECE,default="abc"]; + optional string default_cord = 85 [ctype=CORD,default="123"]; + + // For oneof test + oneof oneof_field { + uint32 oneof_uint32 = 111; + NestedMessage oneof_nested_message = 112; + string oneof_string = 113; + bytes oneof_bytes = 114; + } +} + +// This proto includes a recusively nested message. +message NestedTestAllTypes { + optional NestedTestAllTypes child = 1; + optional TestAllTypes payload = 2; + repeated NestedTestAllTypes repeated_child = 3; +} + +message TestDeprecatedFields { + optional int32 deprecated_int32 = 1 [deprecated=true]; +} + +// Define these after TestAllTypes to make sure the compiler can handle +// that. +message ForeignMessage { + optional int32 c = 1; +} + +enum ForeignEnum { + FOREIGN_FOO = 4; + FOREIGN_BAR = 5; + FOREIGN_BAZ = 6; +} + +message TestReservedFields { + reserved 2, 15, 9 to 11; + reserved "bar", "baz"; +} + +message TestAllExtensions { + extensions 1 to max; +} + +extend TestAllExtensions { + // Singular + optional int32 optional_int32_extension = 1; + optional int64 optional_int64_extension = 2; + optional uint32 optional_uint32_extension = 3; + optional uint64 optional_uint64_extension = 4; + optional sint32 optional_sint32_extension = 5; + optional sint64 optional_sint64_extension = 6; + optional fixed32 optional_fixed32_extension = 7; + optional fixed64 optional_fixed64_extension = 8; + optional sfixed32 optional_sfixed32_extension = 9; + optional sfixed64 optional_sfixed64_extension = 10; + optional float optional_float_extension = 11; + optional double optional_double_extension = 12; + optional bool optional_bool_extension = 13; + optional string optional_string_extension = 14; + optional bytes optional_bytes_extension = 15; + + optional group OptionalGroup_extension = 16 { + optional int32 a = 17; + } + + optional TestAllTypes.NestedMessage optional_nested_message_extension = 18; + optional ForeignMessage optional_foreign_message_extension = 19; + optional protobuf_unittest_import.ImportMessage + optional_import_message_extension = 20; + + optional TestAllTypes.NestedEnum optional_nested_enum_extension = 21; + optional ForeignEnum optional_foreign_enum_extension = 22; + optional protobuf_unittest_import.ImportEnum + optional_import_enum_extension = 23; + + optional string optional_string_piece_extension = 24 [ctype=STRING_PIECE]; + optional string optional_cord_extension = 25 [ctype=CORD]; + + optional protobuf_unittest_import.PublicImportMessage + optional_public_import_message_extension = 26; + + optional TestAllTypes.NestedMessage + optional_lazy_message_extension = 27 [lazy=true]; + + // Repeated + repeated int32 repeated_int32_extension = 31; + repeated int64 repeated_int64_extension = 32; + repeated uint32 repeated_uint32_extension = 33; + repeated uint64 repeated_uint64_extension = 34; + repeated sint32 repeated_sint32_extension = 35; + repeated sint64 repeated_sint64_extension = 36; + repeated fixed32 repeated_fixed32_extension = 37; + repeated fixed64 repeated_fixed64_extension = 38; + repeated sfixed32 repeated_sfixed32_extension = 39; + repeated sfixed64 repeated_sfixed64_extension = 40; + repeated float repeated_float_extension = 41; + repeated double repeated_double_extension = 42; + repeated bool repeated_bool_extension = 43; + repeated string repeated_string_extension = 44; + repeated bytes repeated_bytes_extension = 45; + + repeated group RepeatedGroup_extension = 46 { + optional int32 a = 47; + } + + repeated TestAllTypes.NestedMessage repeated_nested_message_extension = 48; + repeated ForeignMessage repeated_foreign_message_extension = 49; + repeated protobuf_unittest_import.ImportMessage + repeated_import_message_extension = 50; + + repeated TestAllTypes.NestedEnum repeated_nested_enum_extension = 51; + repeated ForeignEnum repeated_foreign_enum_extension = 52; + repeated protobuf_unittest_import.ImportEnum + repeated_import_enum_extension = 53; + + repeated string repeated_string_piece_extension = 54 [ctype=STRING_PIECE]; + repeated string repeated_cord_extension = 55 [ctype=CORD]; + + repeated TestAllTypes.NestedMessage + repeated_lazy_message_extension = 57 [lazy=true]; + + // Singular with defaults + optional int32 default_int32_extension = 61 [default = 41 ]; + optional int64 default_int64_extension = 62 [default = 42 ]; + optional uint32 default_uint32_extension = 63 [default = 43 ]; + optional uint64 default_uint64_extension = 64 [default = 44 ]; + optional sint32 default_sint32_extension = 65 [default = -45 ]; + optional sint64 default_sint64_extension = 66 [default = 46 ]; + optional fixed32 default_fixed32_extension = 67 [default = 47 ]; + optional fixed64 default_fixed64_extension = 68 [default = 48 ]; + optional sfixed32 default_sfixed32_extension = 69 [default = 49 ]; + optional sfixed64 default_sfixed64_extension = 70 [default = -50 ]; + optional float default_float_extension = 71 [default = 51.5 ]; + optional double default_double_extension = 72 [default = 52e3 ]; + optional bool default_bool_extension = 73 [default = true ]; + optional string default_string_extension = 74 [default = "hello"]; + optional bytes default_bytes_extension = 75 [default = "world"]; + + optional TestAllTypes.NestedEnum + default_nested_enum_extension = 81 [default = BAR]; + optional ForeignEnum + default_foreign_enum_extension = 82 [default = FOREIGN_BAR]; + optional protobuf_unittest_import.ImportEnum + default_import_enum_extension = 83 [default = IMPORT_BAR]; + + optional string default_string_piece_extension = 84 [ctype=STRING_PIECE, + default="abc"]; + optional string default_cord_extension = 85 [ctype=CORD, default="123"]; + + // For oneof test + optional uint32 oneof_uint32_extension = 111; + optional TestAllTypes.NestedMessage oneof_nested_message_extension = 112; + optional string oneof_string_extension = 113; + optional bytes oneof_bytes_extension = 114; +} + +message TestNestedExtension { + extend TestAllExtensions { + // Check for bug where string extensions declared in tested scope did not + // compile. + optional string test = 1002 [default="test"]; + // Used to test if generated extension name is correct when there are + // underscores. + optional string nested_string_extension = 1003; + } +} + +message TestMoreNestedExtension { + extend TestAllExtensions { + // Check that duplicate field names in different namespaces work + optional string test = 1004 [default="a different test"]; + } +} + +// We have separate messages for testing required fields because it's +// annoying to have to fill in required fields in TestProto in order to +// do anything with it. Note that we don't need to test every type of +// required filed because the code output is basically identical to +// optional fields for all types. +message TestRequired { + required int32 a = 1; + optional int32 dummy2 = 2; + required int32 b = 3; + + extend TestAllExtensions { + optional TestRequired single = 1000; + repeated TestRequired multi = 1001; + } + + // Pad the field count to 32 so that we can test that IsInitialized() + // properly checks multiple elements of has_bits_. + optional int32 dummy4 = 4; + optional int32 dummy5 = 5; + optional int32 dummy6 = 6; + optional int32 dummy7 = 7; + optional int32 dummy8 = 8; + optional int32 dummy9 = 9; + optional int32 dummy10 = 10; + optional int32 dummy11 = 11; + optional int32 dummy12 = 12; + optional int32 dummy13 = 13; + optional int32 dummy14 = 14; + optional int32 dummy15 = 15; + optional int32 dummy16 = 16; + optional int32 dummy17 = 17; + optional int32 dummy18 = 18; + optional int32 dummy19 = 19; + optional int32 dummy20 = 20; + optional int32 dummy21 = 21; + optional int32 dummy22 = 22; + optional int32 dummy23 = 23; + optional int32 dummy24 = 24; + optional int32 dummy25 = 25; + optional int32 dummy26 = 26; + optional int32 dummy27 = 27; + optional int32 dummy28 = 28; + optional int32 dummy29 = 29; + optional int32 dummy30 = 30; + optional int32 dummy31 = 31; + optional int32 dummy32 = 32; + + required int32 c = 33; +} + +message TestRequiredForeign { + optional TestRequired optional_message = 1; + repeated TestRequired repeated_message = 2; + optional int32 dummy = 3; +} + +// Test that we can use NestedMessage from outside TestAllTypes. +message TestForeignNested { + optional TestAllTypes.NestedMessage foreign_nested = 1; +} + +// TestEmptyMessage is used to test unknown field support. +message TestEmptyMessage { +} + +// Like above, but declare all field numbers as potential extensions. No +// actual extensions should ever be defined for this type. +message TestEmptyMessageWithExtensions { + extensions 1 to max; +} + +message TestMultipleExtensionRanges { + extensions 42; + extensions 4143 to 4243; + extensions 65536 to max; +} + +// Test that really large tag numbers don't break anything. +message TestReallyLargeTagNumber { + // The largest possible tag number is 2^28 - 1, since the wire format uses + // three bits to communicate wire type. + optional int32 a = 1; + optional int32 bb = 268435455; +} + +message TestRecursiveMessage { + optional TestRecursiveMessage a = 1; + optional int32 i = 2; +} + +// Test that mutual recursion works. +message TestMutualRecursionA { + optional TestMutualRecursionB bb = 1; +} + +message TestMutualRecursionB { + optional TestMutualRecursionA a = 1; + optional int32 optional_int32 = 2; +} + +// Test that groups have disjoint field numbers from their siblings and +// parents. This is NOT possible in proto1; only google.protobuf. When attempting +// to compile with proto1, this will emit an error; so we only include it +// in protobuf_unittest_proto. +message TestDupFieldNumber { // NO_PROTO1 + optional int32 a = 1; // NO_PROTO1 + optional group Foo = 2 { optional int32 a = 1; } // NO_PROTO1 + optional group Bar = 3 { optional int32 a = 1; } // NO_PROTO1 +} // NO_PROTO1 + +// Additional messages for testing lazy fields. +message TestEagerMessage { + optional TestAllTypes sub_message = 1 [lazy=false]; +} +message TestLazyMessage { + optional TestAllTypes sub_message = 1 [lazy=true]; +} + +// Needed for a Python test. +message TestNestedMessageHasBits { + message NestedMessage { + repeated int32 nestedmessage_repeated_int32 = 1; + repeated ForeignMessage nestedmessage_repeated_foreignmessage = 2; + } + optional NestedMessage optional_nested_message = 1; +} + + +// Test an enum that has multiple values with the same number. +enum TestEnumWithDupValue { + option allow_alias = true; + + FOO1 = 1; + BAR1 = 2; + BAZ = 3; + FOO2 = 1; + BAR2 = 2; +} + +// Test an enum with large, unordered values. +enum TestSparseEnum { + SPARSE_A = 123; + SPARSE_B = 62374; + SPARSE_C = 12589234; + SPARSE_D = -15; + SPARSE_E = -53452; + SPARSE_F = 0; + SPARSE_G = 2; +} + +// Test message with CamelCase field names. This violates Protocol Buffer +// standard style. +message TestCamelCaseFieldNames { + optional int32 PrimitiveField = 1; + optional string StringField = 2; + optional ForeignEnum EnumField = 3; + optional ForeignMessage MessageField = 4; + optional string StringPieceField = 5 [ctype=STRING_PIECE]; + optional string CordField = 6 [ctype=CORD]; + + repeated int32 RepeatedPrimitiveField = 7; + repeated string RepeatedStringField = 8; + repeated ForeignEnum RepeatedEnumField = 9; + repeated ForeignMessage RepeatedMessageField = 10; + repeated string RepeatedStringPieceField = 11 [ctype=STRING_PIECE]; + repeated string RepeatedCordField = 12 [ctype=CORD]; +} + + +// We list fields out of order, to ensure that we're using field number and not +// field index to determine serialization order. +message TestFieldOrderings { + optional string my_string = 11; + extensions 2 to 10; + optional int64 my_int = 1; + extensions 12 to 100; + optional float my_float = 101; + message NestedMessage { + optional int64 oo = 2; + // The field name "b" fails to compile in proto1 because it conflicts with + // a local variable named "b" in one of the generated methods. Doh. + // This file needs to compile in proto1 to test backwards-compatibility. + optional int32 bb = 1; + } + + optional NestedMessage optional_nested_message = 200; +} + + +extend TestFieldOrderings { + optional string my_extension_string = 50; + optional int32 my_extension_int = 5; +} + + +message TestExtremeDefaultValues { + optional bytes escaped_bytes = 1 [default = "\0\001\a\b\f\n\r\t\v\\\'\"\xfe"]; + optional uint32 large_uint32 = 2 [default = 0xFFFFFFFF]; + optional uint64 large_uint64 = 3 [default = 0xFFFFFFFFFFFFFFFF]; + optional int32 small_int32 = 4 [default = -0x7FFFFFFF]; + optional int64 small_int64 = 5 [default = -0x7FFFFFFFFFFFFFFF]; + optional int32 really_small_int32 = 21 [default = -0x80000000]; + optional int64 really_small_int64 = 22 [default = -0x8000000000000000]; + + // The default value here is UTF-8 for "\u1234". (We could also just type + // the UTF-8 text directly into this text file rather than escape it, but + // lots of people use editors that would be confused by this.) + optional string utf8_string = 6 [default = "\341\210\264"]; + + // Tests for single-precision floating-point values. + optional float zero_float = 7 [default = 0]; + optional float one_float = 8 [default = 1]; + optional float small_float = 9 [default = 1.5]; + optional float negative_one_float = 10 [default = -1]; + optional float negative_float = 11 [default = -1.5]; + // Using exponents + optional float large_float = 12 [default = 2E8]; + optional float small_negative_float = 13 [default = -8e-28]; + + // Text for nonfinite floating-point values. + optional double inf_double = 14 [default = inf]; + optional double neg_inf_double = 15 [default = -inf]; + optional double nan_double = 16 [default = nan]; + optional float inf_float = 17 [default = inf]; + optional float neg_inf_float = 18 [default = -inf]; + optional float nan_float = 19 [default = nan]; + + // Tests for C++ trigraphs. + // Trigraphs should be escaped in C++ generated files, but they should not be + // escaped for other languages. + // Note that in .proto file, "\?" is a valid way to escape ? in string + // literals. + optional string cpp_trigraph = 20 [default = "? \? ?? \?? \??? ??/ ?\?-"]; + + // String defaults containing the character '\000' + optional string string_with_zero = 23 [default = "hel\000lo"]; + optional bytes bytes_with_zero = 24 [default = "wor\000ld"]; + optional string string_piece_with_zero = 25 [ctype=STRING_PIECE, + default="ab\000c"]; + optional string cord_with_zero = 26 [ctype=CORD, + default="12\0003"]; + optional string replacement_string = 27 [default="${unknown}"]; +} + +message SparseEnumMessage { + optional TestSparseEnum sparse_enum = 1; +} + +// Test String and Bytes: string is for valid UTF-8 strings +message OneString { + optional string data = 1; +} + +message MoreString { + repeated string data = 1; +} + +message OneBytes { + optional bytes data = 1; +} + +message MoreBytes { + repeated bytes data = 1; +} + +// Test int32, uint32, int64, uint64, and bool are all compatible +message Int32Message { + optional int32 data = 1; +} + +message Uint32Message { + optional uint32 data = 1; +} + +message Int64Message { + optional int64 data = 1; +} + +message Uint64Message { + optional uint64 data = 1; +} + +message BoolMessage { + optional bool data = 1; +} + +// Test oneofs. +message TestOneof { + oneof foo { + int32 foo_int = 1; + string foo_string = 2; + TestAllTypes foo_message = 3; + group FooGroup = 4 { + optional int32 a = 5; + optional string b = 6; + } + } +} + +message TestOneofBackwardsCompatible { + optional int32 foo_int = 1; + optional string foo_string = 2; + optional TestAllTypes foo_message = 3; + optional group FooGroup = 4 { + optional int32 a = 5; + optional string b = 6; + } +} + +message TestOneof2 { + oneof foo { + int32 foo_int = 1; + string foo_string = 2; + string foo_cord = 3 [ctype=CORD]; + string foo_string_piece = 4 [ctype=STRING_PIECE]; + bytes foo_bytes = 5; + NestedEnum foo_enum = 6; + NestedMessage foo_message = 7; + group FooGroup = 8 { + optional int32 a = 9; + optional string b = 10; + } + NestedMessage foo_lazy_message = 11 [lazy=true]; + } + + oneof bar { + int32 bar_int = 12 [default = 5]; + string bar_string = 13 [default = "STRING"]; + string bar_cord = 14 [ctype=CORD, default = "CORD"]; + string bar_string_piece = 15 [ctype=STRING_PIECE, default = "SPIECE"]; + bytes bar_bytes = 16 [default = "BYTES"]; + NestedEnum bar_enum = 17 [default = BAR]; + } + + optional int32 baz_int = 18; + optional string baz_string = 19 [default = "BAZ"]; + + message NestedMessage { + optional int64 qux_int = 1; + repeated int32 corge_int = 2; + } + + enum NestedEnum { + FOO = 1; + BAR = 2; + BAZ = 3; + } +} + +message TestRequiredOneof { + oneof foo { + int32 foo_int = 1; + string foo_string = 2; + NestedMessage foo_message = 3; + } + message NestedMessage { + required double required_double = 1; + } +} + +// Test messages for packed fields + +message TestPackedTypes { + repeated int32 packed_int32 = 90 [packed = true]; + repeated int64 packed_int64 = 91 [packed = true]; + repeated uint32 packed_uint32 = 92 [packed = true]; + repeated uint64 packed_uint64 = 93 [packed = true]; + repeated sint32 packed_sint32 = 94 [packed = true]; + repeated sint64 packed_sint64 = 95 [packed = true]; + repeated fixed32 packed_fixed32 = 96 [packed = true]; + repeated fixed64 packed_fixed64 = 97 [packed = true]; + repeated sfixed32 packed_sfixed32 = 98 [packed = true]; + repeated sfixed64 packed_sfixed64 = 99 [packed = true]; + repeated float packed_float = 100 [packed = true]; + repeated double packed_double = 101 [packed = true]; + repeated bool packed_bool = 102 [packed = true]; + repeated ForeignEnum packed_enum = 103 [packed = true]; +} + +// A message with the same fields as TestPackedTypes, but without packing. Used +// to test packed <-> unpacked wire compatibility. +message TestUnpackedTypes { + repeated int32 unpacked_int32 = 90 [packed = false]; + repeated int64 unpacked_int64 = 91 [packed = false]; + repeated uint32 unpacked_uint32 = 92 [packed = false]; + repeated uint64 unpacked_uint64 = 93 [packed = false]; + repeated sint32 unpacked_sint32 = 94 [packed = false]; + repeated sint64 unpacked_sint64 = 95 [packed = false]; + repeated fixed32 unpacked_fixed32 = 96 [packed = false]; + repeated fixed64 unpacked_fixed64 = 97 [packed = false]; + repeated sfixed32 unpacked_sfixed32 = 98 [packed = false]; + repeated sfixed64 unpacked_sfixed64 = 99 [packed = false]; + repeated float unpacked_float = 100 [packed = false]; + repeated double unpacked_double = 101 [packed = false]; + repeated bool unpacked_bool = 102 [packed = false]; + repeated ForeignEnum unpacked_enum = 103 [packed = false]; +} + +message TestPackedExtensions { + extensions 1 to max; +} + +extend TestPackedExtensions { + repeated int32 packed_int32_extension = 90 [packed = true]; + repeated int64 packed_int64_extension = 91 [packed = true]; + repeated uint32 packed_uint32_extension = 92 [packed = true]; + repeated uint64 packed_uint64_extension = 93 [packed = true]; + repeated sint32 packed_sint32_extension = 94 [packed = true]; + repeated sint64 packed_sint64_extension = 95 [packed = true]; + repeated fixed32 packed_fixed32_extension = 96 [packed = true]; + repeated fixed64 packed_fixed64_extension = 97 [packed = true]; + repeated sfixed32 packed_sfixed32_extension = 98 [packed = true]; + repeated sfixed64 packed_sfixed64_extension = 99 [packed = true]; + repeated float packed_float_extension = 100 [packed = true]; + repeated double packed_double_extension = 101 [packed = true]; + repeated bool packed_bool_extension = 102 [packed = true]; + repeated ForeignEnum packed_enum_extension = 103 [packed = true]; +} + +message TestUnpackedExtensions { + extensions 1 to max; +} + +extend TestUnpackedExtensions { + repeated int32 unpacked_int32_extension = 90 [packed = false]; + repeated int64 unpacked_int64_extension = 91 [packed = false]; + repeated uint32 unpacked_uint32_extension = 92 [packed = false]; + repeated uint64 unpacked_uint64_extension = 93 [packed = false]; + repeated sint32 unpacked_sint32_extension = 94 [packed = false]; + repeated sint64 unpacked_sint64_extension = 95 [packed = false]; + repeated fixed32 unpacked_fixed32_extension = 96 [packed = false]; + repeated fixed64 unpacked_fixed64_extension = 97 [packed = false]; + repeated sfixed32 unpacked_sfixed32_extension = 98 [packed = false]; + repeated sfixed64 unpacked_sfixed64_extension = 99 [packed = false]; + repeated float unpacked_float_extension = 100 [packed = false]; + repeated double unpacked_double_extension = 101 [packed = false]; + repeated bool unpacked_bool_extension = 102 [packed = false]; + repeated ForeignEnum unpacked_enum_extension = 103 [packed = false]; +} + +// Used by ExtensionSetTest/DynamicExtensions. The test actually builds +// a set of extensions to TestAllExtensions dynamically, based on the fields +// of this message type. +message TestDynamicExtensions { + enum DynamicEnumType { + DYNAMIC_FOO = 2200; + DYNAMIC_BAR = 2201; + DYNAMIC_BAZ = 2202; + } + message DynamicMessageType { + optional int32 dynamic_field = 2100; + } + + optional fixed32 scalar_extension = 2000; + optional ForeignEnum enum_extension = 2001; + optional DynamicEnumType dynamic_enum_extension = 2002; + + optional ForeignMessage message_extension = 2003; + optional DynamicMessageType dynamic_message_extension = 2004; + + repeated string repeated_extension = 2005; + repeated sint32 packed_extension = 2006 [packed = true]; +} + +message TestRepeatedScalarDifferentTagSizes { + // Parsing repeated fixed size values used to fail. This message needs to be + // used in order to get a tag of the right size; all of the repeated fields + // in TestAllTypes didn't trigger the check. + repeated fixed32 repeated_fixed32 = 12; + // Check for a varint type, just for good measure. + repeated int32 repeated_int32 = 13; + + // These have two-byte tags. + repeated fixed64 repeated_fixed64 = 2046; + repeated int64 repeated_int64 = 2047; + + // Three byte tags. + repeated float repeated_float = 262142; + repeated uint64 repeated_uint64 = 262143; +} + +// Test that if an optional or required message/group field appears multiple +// times in the input, they need to be merged. +message TestParsingMerge { + // RepeatedFieldsGenerator defines matching field types as TestParsingMerge, + // except that all fields are repeated. In the tests, we will serialize the + // RepeatedFieldsGenerator to bytes, and parse the bytes to TestParsingMerge. + // Repeated fields in RepeatedFieldsGenerator are expected to be merged into + // the corresponding required/optional fields in TestParsingMerge. + message RepeatedFieldsGenerator { + repeated TestAllTypes field1 = 1; + repeated TestAllTypes field2 = 2; + repeated TestAllTypes field3 = 3; + repeated group Group1 = 10 { + optional TestAllTypes field1 = 11; + } + repeated group Group2 = 20 { + optional TestAllTypes field1 = 21; + } + repeated TestAllTypes ext1 = 1000; + repeated TestAllTypes ext2 = 1001; + } + required TestAllTypes required_all_types = 1; + optional TestAllTypes optional_all_types = 2; + repeated TestAllTypes repeated_all_types = 3; + optional group OptionalGroup = 10 { + optional TestAllTypes optional_group_all_types = 11; + } + repeated group RepeatedGroup = 20 { + optional TestAllTypes repeated_group_all_types = 21; + } + extensions 1000 to max; + extend TestParsingMerge { + optional TestAllTypes optional_ext = 1000; + repeated TestAllTypes repeated_ext = 1001; + } +} + +message TestCommentInjectionMessage { + // */ <- This should not close the generated doc comment + optional string a = 1 [default="*/ <- Neither should this."]; +} + + +// Test that RPC services work. +message FooRequest {} +message FooResponse {} + +message FooClientMessage {} +message FooServerMessage{} + +service TestService { + rpc Foo(FooRequest) returns (FooResponse); + rpc Bar(BarRequest) returns (BarResponse); +} + + +message BarRequest {} +message BarResponse {} diff --git a/spec/support/protos/google_unittest_custom_options.bin b/spec/support/protos/google_unittest_custom_options.bin new file mode 100644 index 00000000..89704431 Binary files /dev/null and b/spec/support/protos/google_unittest_custom_options.bin differ diff --git a/spec/support/protos/google_unittest_custom_options.pb.rb b/spec/support/protos/google_unittest_custom_options.pb.rb new file mode 100644 index 00000000..9ca77aee --- /dev/null +++ b/spec/support/protos/google_unittest_custom_options.pb.rb @@ -0,0 +1,361 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' +require 'protobuf/rpc/service' + + +## +# Imports +# +require 'google/protobuf/descriptor.pb' + +module Protobuf_unittest + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Enum Classes + # + class MethodOpt1 < ::Protobuf::Enum + define :METHODOPT1_VAL1, 1 + define :METHODOPT1_VAL2, 2 + end + + class AggregateEnum < ::Protobuf::Enum + set_option :".protobuf_unittest.enumopt", { :s => "EnumAnnotation" } + + define :VALUE, 1 + end + + + ## + # Message Classes + # + class TestMessageWithCustomOptions < ::Protobuf::Message + class AnEnum < ::Protobuf::Enum + set_option :".protobuf_unittest.enum_opt1", -789 + + define :ANENUM_VAL1, 1 + define :ANENUM_VAL2, 2 + end + + end + + class CustomOptionFooRequest < ::Protobuf::Message; end + class CustomOptionFooResponse < ::Protobuf::Message; end + class CustomOptionFooClientMessage < ::Protobuf::Message; end + class CustomOptionFooServerMessage < ::Protobuf::Message; end + class DummyMessageContainingEnum < ::Protobuf::Message + class TestEnumType < ::Protobuf::Enum + define :TEST_OPTION_ENUM_TYPE1, 22 + define :TEST_OPTION_ENUM_TYPE2, -23 + end + + end + + class DummyMessageInvalidAsOptionType < ::Protobuf::Message; end + class CustomOptionMinIntegerValues < ::Protobuf::Message; end + class CustomOptionMaxIntegerValues < ::Protobuf::Message; end + class CustomOptionOtherValues < ::Protobuf::Message; end + class SettingRealsFromPositiveInts < ::Protobuf::Message; end + class SettingRealsFromNegativeInts < ::Protobuf::Message; end + class ComplexOptionType1 < ::Protobuf::Message; end + class ComplexOptionType2 < ::Protobuf::Message + class ComplexOptionType4 < ::Protobuf::Message; end + + end + + class ComplexOptionType3 < ::Protobuf::Message; end + class VariousComplexOptions < ::Protobuf::Message; end + class AggregateMessageSet < ::Protobuf::Message; end + class AggregateMessageSetElement < ::Protobuf::Message; end + class Aggregate < ::Protobuf::Message; end + class AggregateMessage < ::Protobuf::Message; end + class NestedOptionType < ::Protobuf::Message + class NestedEnum < ::Protobuf::Enum + set_option :".protobuf_unittest.enum_opt1", 1003 + + define :NESTED_ENUM_VALUE, 1 + end + + class NestedMessage < ::Protobuf::Message; end + + end + + class OldOptionType < ::Protobuf::Message + class TestEnum < ::Protobuf::Enum + define :OLD_VALUE, 0 + end + + end + + class NewOptionType < ::Protobuf::Message + class TestEnum < ::Protobuf::Enum + define :OLD_VALUE, 0 + define :NEW_VALUE, 1 + end + + end + + class TestMessageWithRequiredEnumOption < ::Protobuf::Message; end + + + ## + # File Options + # + set_option :cc_generic_services, true + set_option :java_generic_services, true + set_option :py_generic_services, true + set_option :".protobuf_unittest.file_opt1", 9876543210 + set_option :".protobuf_unittest.fileopt", { :i => 100, :s => "FileAnnotation", :sub => { :s => "NestedFileAnnotation" }, :file => { :".protobuf_unittest.fileopt" => { :s => "FileExtensionAnnotation" } }, :mset => { :".protobuf_unittest.AggregateMessageSetElement.message_set_extension" => { :s => "EmbeddedMessageSetElement" } } } + + + ## + # Message Fields + # + class TestMessageWithCustomOptions + # Message Options + set_option :message_set_wire_format, false + set_option :".protobuf_unittest.message_opt1", -56 + + optional :string, :field1, 1, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD, :".protobuf_unittest.field_opt1" => 8765432109 + end + + class CustomOptionMinIntegerValues + # Message Options + set_option :".protobuf_unittest.sfixed64_opt", -9223372036854775808 + set_option :".protobuf_unittest.sfixed32_opt", -2147483648 + set_option :".protobuf_unittest.fixed64_opt", 0 + set_option :".protobuf_unittest.fixed32_opt", 0 + set_option :".protobuf_unittest.sint64_opt", -9223372036854775808 + set_option :".protobuf_unittest.sint32_opt", -2147483648 + set_option :".protobuf_unittest.uint64_opt", 0 + set_option :".protobuf_unittest.uint32_opt", 0 + set_option :".protobuf_unittest.int64_opt", -9223372036854775808 + set_option :".protobuf_unittest.int32_opt", -2147483648 + set_option :".protobuf_unittest.bool_opt", false + + end + + class CustomOptionMaxIntegerValues + # Message Options + set_option :".protobuf_unittest.sfixed64_opt", 9223372036854775807 + set_option :".protobuf_unittest.sfixed32_opt", 2147483647 + set_option :".protobuf_unittest.fixed64_opt", 18446744073709551615 + set_option :".protobuf_unittest.fixed32_opt", 4294967295 + set_option :".protobuf_unittest.sint64_opt", 9223372036854775807 + set_option :".protobuf_unittest.sint32_opt", 2147483647 + set_option :".protobuf_unittest.uint64_opt", 18446744073709551615 + set_option :".protobuf_unittest.uint32_opt", 4294967295 + set_option :".protobuf_unittest.int64_opt", 9223372036854775807 + set_option :".protobuf_unittest.int32_opt", 2147483647 + set_option :".protobuf_unittest.bool_opt", true + + end + + class CustomOptionOtherValues + # Message Options + set_option :".protobuf_unittest.enum_opt", ::Protobuf_unittest::DummyMessageContainingEnum::TestEnumType::TEST_OPTION_ENUM_TYPE2 + set_option :".protobuf_unittest.bytes_opt", "Hello\x00World" + set_option :".protobuf_unittest.string_opt", "Hello, \"World\"" + set_option :".protobuf_unittest.double_opt", 1.2345678901234567 + set_option :".protobuf_unittest.float_opt", 12.34567928314209 + set_option :".protobuf_unittest.int32_opt", -100 + + end + + class SettingRealsFromPositiveInts + # Message Options + set_option :".protobuf_unittest.double_opt", 154.0 + set_option :".protobuf_unittest.float_opt", 12.0 + + end + + class SettingRealsFromNegativeInts + # Message Options + set_option :".protobuf_unittest.double_opt", -154.0 + set_option :".protobuf_unittest.float_opt", -12.0 + + end + + class ComplexOptionType1 + optional :int32, :foo, 1 + optional :int32, :foo2, 2 + optional :int32, :foo3, 3 + repeated :int32, :foo4, 4 + # Extension Fields + extensions 100...536870912 + optional :int32, :".protobuf_unittest.quux", 7663707, :extension => true + optional ::Protobuf_unittest::ComplexOptionType3, :".protobuf_unittest.corge", 7663442, :extension => true + end + + class ComplexOptionType2 + class ComplexOptionType4 + optional :int32, :waldo, 1 + end + + optional ::Protobuf_unittest::ComplexOptionType1, :bar, 1 + optional :int32, :baz, 2 + optional ::Protobuf_unittest::ComplexOptionType2::ComplexOptionType4, :fred, 3 + repeated ::Protobuf_unittest::ComplexOptionType2::ComplexOptionType4, :barney, 4 + # Extension Fields + extensions 100...536870912 + optional :int32, :".protobuf_unittest.grault", 7650927, :extension => true + optional ::Protobuf_unittest::ComplexOptionType1, :".protobuf_unittest.garply", 7649992, :extension => true + end + + class ComplexOptionType3 + optional :int32, :qux, 1 + end + + class VariousComplexOptions + # Message Options + set_option :".protobuf_unittest.ComplexOptionType2.ComplexOptionType4.complex_opt4", { :waldo => 1971 } + set_option :".protobuf_unittest.complex_opt3", { :qux => 9 } + set_option :".protobuf_unittest.repeated_opt1", [1, 2] + set_option :".protobuf_unittest.repeated_opt2", [{ :qux => 3 }, { :qux => 4 }] + set_option :".protobuf_unittest.complex_opt2", { :bar => { :foo => 743, :".protobuf_unittest.corge" => { :qux => 2008 }, :".protobuf_unittest.quux" => 1999 }, :baz => 987, :fred => { :waldo => 321 }, :barney => [{ :waldo => 101 }, { :waldo => 212 }], :".protobuf_unittest.garply" => { :foo => 741, :".protobuf_unittest.corge" => { :qux => 2121 }, :".protobuf_unittest.quux" => 1998 }, :".protobuf_unittest.grault" => 654 } + set_option :".protobuf_unittest.complex_opt1", { :foo => 42, :foo4 => [99, 88], :".protobuf_unittest.corge" => { :qux => 876 }, :".protobuf_unittest.quux" => 324 } + + end + + class AggregateMessageSet + # Message Options + set_option :message_set_wire_format, false + + # Extension Fields + extensions 4...536870912 + optional ::Protobuf_unittest::AggregateMessageSetElement, :".protobuf_unittest.AggregateMessageSetElement.message_set_extension", 15447542, :extension => true + end + + class AggregateMessageSetElement + optional :string, :s, 1 + end + + class Aggregate + optional :int32, :i, 1 + optional :string, :s, 2 + optional ::Protobuf_unittest::Aggregate, :sub, 3 + optional ::Google::Protobuf::FileOptions, :file, 4 + optional ::Protobuf_unittest::AggregateMessageSet, :mset, 5 + end + + class AggregateMessage + # Message Options + set_option :".protobuf_unittest.msgopt", { :i => 101, :s => "MessageAnnotation" } + + optional :int32, :fieldname, 1, :".protobuf_unittest.fieldopt" => { :s => "FieldAnnotation" } + end + + class NestedOptionType + class NestedMessage + # Message Options + set_option :".protobuf_unittest.message_opt1", 1001 + + optional :int32, :nested_field, 1, :".protobuf_unittest.field_opt1" => 1002 + end + + end + + class OldOptionType + required ::Protobuf_unittest::OldOptionType::TestEnum, :value, 1 + end + + class NewOptionType + required ::Protobuf_unittest::NewOptionType::TestEnum, :value, 1 + end + + class TestMessageWithRequiredEnumOption + # Message Options + set_option :".protobuf_unittest.required_enum_opt", { :value => ::Protobuf_unittest::OldOptionType::TestEnum::OLD_VALUE } + + end + + + ## + # Extended Message Fields + # + class ::Google::Protobuf::FileOptions < ::Protobuf::Message + optional :uint64, :".protobuf_unittest.file_opt1", 7736974, :extension => true + optional ::Protobuf_unittest::Aggregate, :".protobuf_unittest.fileopt", 15478479, :extension => true + optional ::Protobuf_unittest::Aggregate, :".protobuf_unittest.Aggregate.nested", 15476903, :extension => true + optional :int32, :".protobuf_unittest.NestedOptionType.nested_extension", 7912573, :extension => true, :".protobuf_unittest.field_opt2" => 1005 + end + + class ::Google::Protobuf::MessageOptions < ::Protobuf::Message + optional :int32, :".protobuf_unittest.message_opt1", 7739036, :extension => true + optional :bool, :".protobuf_unittest.bool_opt", 7706090, :extension => true + optional :int32, :".protobuf_unittest.int32_opt", 7705709, :extension => true + optional :int64, :".protobuf_unittest.int64_opt", 7705542, :extension => true + optional :uint32, :".protobuf_unittest.uint32_opt", 7704880, :extension => true + optional :uint64, :".protobuf_unittest.uint64_opt", 7702367, :extension => true + optional :sint32, :".protobuf_unittest.sint32_opt", 7701568, :extension => true + optional :sint64, :".protobuf_unittest.sint64_opt", 7700863, :extension => true + optional :fixed32, :".protobuf_unittest.fixed32_opt", 7700307, :extension => true + optional :fixed64, :".protobuf_unittest.fixed64_opt", 7700194, :extension => true + optional :sfixed32, :".protobuf_unittest.sfixed32_opt", 7698645, :extension => true + optional :sfixed64, :".protobuf_unittest.sfixed64_opt", 7685475, :extension => true + optional :float, :".protobuf_unittest.float_opt", 7675390, :extension => true + optional :double, :".protobuf_unittest.double_opt", 7673293, :extension => true + optional :string, :".protobuf_unittest.string_opt", 7673285, :extension => true + optional :bytes, :".protobuf_unittest.bytes_opt", 7673238, :extension => true + optional ::Protobuf_unittest::DummyMessageContainingEnum::TestEnumType, :".protobuf_unittest.enum_opt", 7673233, :extension => true + optional ::Protobuf_unittest::DummyMessageInvalidAsOptionType, :".protobuf_unittest.message_type_opt", 7665967, :extension => true + optional ::Protobuf_unittest::ComplexOptionType1, :".protobuf_unittest.complex_opt1", 7646756, :extension => true + optional ::Protobuf_unittest::ComplexOptionType2, :".protobuf_unittest.complex_opt2", 7636949, :extension => true + optional ::Protobuf_unittest::ComplexOptionType3, :".protobuf_unittest.complex_opt3", 7636463, :extension => true + repeated :int32, :".protobuf_unittest.repeated_opt1", 7636464, :extension => true + repeated ::Protobuf_unittest::ComplexOptionType3, :".protobuf_unittest.repeated_opt2", 7636465, :extension => true + optional ::Protobuf_unittest::Aggregate, :".protobuf_unittest.msgopt", 15480088, :extension => true + optional ::Protobuf_unittest::OldOptionType, :".protobuf_unittest.required_enum_opt", 106161807, :extension => true + optional ::Protobuf_unittest::ComplexOptionType2::ComplexOptionType4, :".protobuf_unittest.ComplexOptionType2.ComplexOptionType4.complex_opt4", 7633546, :extension => true + end + + class ::Google::Protobuf::FieldOptions < ::Protobuf::Message + optional :fixed64, :".protobuf_unittest.field_opt1", 7740936, :extension => true + optional :int32, :".protobuf_unittest.field_opt2", 7753913, :default => 42, :extension => true + optional ::Protobuf_unittest::Aggregate, :".protobuf_unittest.fieldopt", 15481374, :extension => true + end + + class ::Google::Protobuf::EnumOptions < ::Protobuf::Message + optional :sfixed32, :".protobuf_unittest.enum_opt1", 7753576, :extension => true + optional ::Protobuf_unittest::Aggregate, :".protobuf_unittest.enumopt", 15483218, :extension => true + end + + class ::Google::Protobuf::EnumValueOptions < ::Protobuf::Message + optional :int32, :".protobuf_unittest.enum_value_opt1", 1560678, :extension => true + optional ::Protobuf_unittest::Aggregate, :".protobuf_unittest.enumvalopt", 15486921, :extension => true + end + + class ::Google::Protobuf::ServiceOptions < ::Protobuf::Message + optional :sint64, :".protobuf_unittest.service_opt1", 7887650, :extension => true + optional ::Protobuf_unittest::Aggregate, :".protobuf_unittest.serviceopt", 15497145, :extension => true + end + + class ::Google::Protobuf::MethodOptions < ::Protobuf::Message + optional ::Protobuf_unittest::MethodOpt1, :".protobuf_unittest.method_opt1", 7890860, :extension => true + optional ::Protobuf_unittest::Aggregate, :".protobuf_unittest.methodopt", 15512713, :extension => true + end + + + ## + # Service Classes + # + class TestServiceWithCustomOptions < ::Protobuf::Rpc::Service + set_option :".protobuf_unittest.service_opt1", -9876543210 + rpc :foo, ::Protobuf_unittest::CustomOptionFooRequest, ::Protobuf_unittest::CustomOptionFooResponse do + set_option :".protobuf_unittest.method_opt1", ::Protobuf_unittest::MethodOpt1::METHODOPT1_VAL2 + end + end + + class AggregateService < ::Protobuf::Rpc::Service + set_option :".protobuf_unittest.serviceopt", { :s => "ServiceAnnotation" } + rpc :method, ::Protobuf_unittest::AggregateMessage, ::Protobuf_unittest::AggregateMessage do + set_option :".protobuf_unittest.methodopt", { :s => "MethodAnnotation" } + end + end + +end + diff --git a/spec/support/protos/google_unittest_custom_options.proto b/spec/support/protos/google_unittest_custom_options.proto new file mode 100644 index 00000000..08fd9ed1 --- /dev/null +++ b/spec/support/protos/google_unittest_custom_options.proto @@ -0,0 +1,424 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: benjy@google.com (Benjy Weinberger) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// A proto file used to test the "custom options" feature of google.protobuf. + +syntax = "proto2"; + +// Some generic_services option(s) added automatically. +// See: http://go/proto2-generic-services-default +option cc_generic_services = true; // auto-added +option java_generic_services = true; // auto-added +option py_generic_services = true; + +// A custom file option (defined below). +option (file_opt1) = 9876543210; + +import "google/protobuf/descriptor.proto"; + +// We don't put this in a package within proto2 because we need to make sure +// that the generated code doesn't depend on being in the proto2 namespace. +package protobuf_unittest; + + +// Some simple test custom options of various types. + +extend google.protobuf.FileOptions { + optional uint64 file_opt1 = 7736974; +} + +extend google.protobuf.MessageOptions { + optional int32 message_opt1 = 7739036; +} + +extend google.protobuf.FieldOptions { + optional fixed64 field_opt1 = 7740936; + // This is useful for testing that we correctly register default values for + // extension options. + optional int32 field_opt2 = 7753913 [default=42]; +} + +extend google.protobuf.EnumOptions { + optional sfixed32 enum_opt1 = 7753576; +} + +extend google.protobuf.EnumValueOptions { + optional int32 enum_value_opt1 = 1560678; +} + +extend google.protobuf.ServiceOptions { + optional sint64 service_opt1 = 7887650; +} + +enum MethodOpt1 { + METHODOPT1_VAL1 = 1; + METHODOPT1_VAL2 = 2; +} + +extend google.protobuf.MethodOptions { + optional MethodOpt1 method_opt1 = 7890860; +} + +// A test message with custom options at all possible locations (and also some +// regular options, to make sure they interact nicely). +message TestMessageWithCustomOptions { + option message_set_wire_format = false; + + option (message_opt1) = -56; + + optional string field1 = 1 [ctype=CORD, + (field_opt1)=8765432109]; + + enum AnEnum { + option (enum_opt1) = -789; + + ANENUM_VAL1 = 1; + ANENUM_VAL2 = 2 [(enum_value_opt1) = 123]; + } +} + + +// A test RPC service with custom options at all possible locations (and also +// some regular options, to make sure they interact nicely). +message CustomOptionFooRequest { +} + +message CustomOptionFooResponse { +} + +message CustomOptionFooClientMessage { +} + +message CustomOptionFooServerMessage { +} + +service TestServiceWithCustomOptions { + option (service_opt1) = -9876543210; + + rpc Foo(CustomOptionFooRequest) returns (CustomOptionFooResponse) { + option (method_opt1) = METHODOPT1_VAL2; + } +} + + + +// Options of every possible field type, so we can test them all exhaustively. + +message DummyMessageContainingEnum { + enum TestEnumType { + TEST_OPTION_ENUM_TYPE1 = 22; + TEST_OPTION_ENUM_TYPE2 = -23; + } +} + +message DummyMessageInvalidAsOptionType { +} + +extend google.protobuf.MessageOptions { + optional bool bool_opt = 7706090; + optional int32 int32_opt = 7705709; + optional int64 int64_opt = 7705542; + optional uint32 uint32_opt = 7704880; + optional uint64 uint64_opt = 7702367; + optional sint32 sint32_opt = 7701568; + optional sint64 sint64_opt = 7700863; + optional fixed32 fixed32_opt = 7700307; + optional fixed64 fixed64_opt = 7700194; + optional sfixed32 sfixed32_opt = 7698645; + optional sfixed64 sfixed64_opt = 7685475; + optional float float_opt = 7675390; + optional double double_opt = 7673293; + optional string string_opt = 7673285; + optional bytes bytes_opt = 7673238; + optional DummyMessageContainingEnum.TestEnumType enum_opt = 7673233; + optional DummyMessageInvalidAsOptionType message_type_opt = 7665967; +} + +message CustomOptionMinIntegerValues { + option (bool_opt) = false; + option (int32_opt) = -0x80000000; + option (int64_opt) = -0x8000000000000000; + option (uint32_opt) = 0; + option (uint64_opt) = 0; + option (sint32_opt) = -0x80000000; + option (sint64_opt) = -0x8000000000000000; + option (fixed32_opt) = 0; + option (fixed64_opt) = 0; + option (sfixed32_opt) = -0x80000000; + option (sfixed64_opt) = -0x8000000000000000; +} + +message CustomOptionMaxIntegerValues { + option (bool_opt) = true; + option (int32_opt) = 0x7FFFFFFF; + option (int64_opt) = 0x7FFFFFFFFFFFFFFF; + option (uint32_opt) = 0xFFFFFFFF; + option (uint64_opt) = 0xFFFFFFFFFFFFFFFF; + option (sint32_opt) = 0x7FFFFFFF; + option (sint64_opt) = 0x7FFFFFFFFFFFFFFF; + option (fixed32_opt) = 0xFFFFFFFF; + option (fixed64_opt) = 0xFFFFFFFFFFFFFFFF; + option (sfixed32_opt) = 0x7FFFFFFF; + option (sfixed64_opt) = 0x7FFFFFFFFFFFFFFF; +} + +message CustomOptionOtherValues { + option (int32_opt) = -100; // To test sign-extension. + option (float_opt) = 12.3456789; + option (double_opt) = 1.234567890123456789; + option (string_opt) = "Hello, \"World\""; + option (bytes_opt) = "Hello\0World"; + option (enum_opt) = TEST_OPTION_ENUM_TYPE2; +} + +message SettingRealsFromPositiveInts { + option (float_opt) = 12; + option (double_opt) = 154; +} + +message SettingRealsFromNegativeInts { + option (float_opt) = -12; + option (double_opt) = -154; +} + +// Options of complex message types, themselves combined and extended in +// various ways. + +// TODO: do we want to support packed ints? +// e.g.: repeated int32 foo4 = 4 [packed = true]; +message ComplexOptionType1 { + optional int32 foo = 1; + optional int32 foo2 = 2; + optional int32 foo3 = 3; + repeated int32 foo4 = 4; + + extensions 100 to max; +} + +message ComplexOptionType2 { + optional ComplexOptionType1 bar = 1; + optional int32 baz = 2; + + message ComplexOptionType4 { + optional int32 waldo = 1; + + extend google.protobuf.MessageOptions { + optional ComplexOptionType4 complex_opt4 = 7633546; + } + } + + optional ComplexOptionType4 fred = 3; + repeated ComplexOptionType4 barney = 4; + + extensions 100 to max; +} + +message ComplexOptionType3 { + optional int32 qux = 1; +} + +extend ComplexOptionType1 { + optional int32 quux = 7663707; + optional ComplexOptionType3 corge = 7663442; +} + +extend ComplexOptionType2 { + optional int32 grault = 7650927; + optional ComplexOptionType1 garply = 7649992; +} + +extend google.protobuf.MessageOptions { + optional protobuf_unittest.ComplexOptionType1 complex_opt1 = 7646756; + optional ComplexOptionType2 complex_opt2 = 7636949; + optional ComplexOptionType3 complex_opt3 = 7636463; + repeated int32 repeated_opt1 = 7636464; + repeated protobuf_unittest.ComplexOptionType3 repeated_opt2 = 7636465; +} + +// Note that we try various different ways of naming the same extension. +message VariousComplexOptions { + option (.protobuf_unittest.complex_opt1).foo = 42; + option (protobuf_unittest.complex_opt1).(.protobuf_unittest.quux) = 324; + option (.protobuf_unittest.complex_opt1).(protobuf_unittest.corge).qux = 876; + option (protobuf_unittest.complex_opt1).foo4 = 99; + option (protobuf_unittest.complex_opt1).foo4 = 88; + option (complex_opt2).baz = 987; + option (complex_opt2).(grault) = 654; + option (complex_opt2).bar.foo = 743; + option (complex_opt2).bar.(quux) = 1999; + option (complex_opt2).bar.(protobuf_unittest.corge).qux = 2008; + option (complex_opt2).(garply).foo = 741; + option (complex_opt2).(garply).(.protobuf_unittest.quux) = 1998; + option (complex_opt2).(protobuf_unittest.garply).(corge).qux = 2121; + option (ComplexOptionType2.ComplexOptionType4.complex_opt4).waldo = 1971; + option (complex_opt2).fred.waldo = 321; + option (complex_opt2).barney = { waldo: 101 }; + option (complex_opt2).barney = { waldo: 212 }; + option (protobuf_unittest.complex_opt3).qux = 9; + option (repeated_opt1) = 1; + option (repeated_opt1) = 2; + option (repeated_opt2) = { qux: 3 }; + option (repeated_opt2) = { qux: 4 }; + /*option (complex_opt3).complexoptiontype5.plugh = 22;*/ + /*option (complexopt6).xyzzy = 24;*/ +} + +// ------------------------------------------------------ +// Definitions for testing aggregate option parsing. +// See descriptor_unittest.cc. + +message AggregateMessageSet { + option message_set_wire_format = false; + extensions 4 to max; +} + +message AggregateMessageSetElement { + extend AggregateMessageSet { + optional AggregateMessageSetElement message_set_extension = 15447542; + } + optional string s = 1; +} + +// A helper type used to test aggregate option parsing +message Aggregate { + optional int32 i = 1; + optional string s = 2; + + // A nested object + optional Aggregate sub = 3; + + // To test the parsing of extensions inside aggregate values + optional google.protobuf.FileOptions file = 4; + extend google.protobuf.FileOptions { + optional Aggregate nested = 15476903; + } + + // An embedded message set + optional AggregateMessageSet mset = 5; +} + +// Allow Aggregate to be used as an option at all possible locations +// in the .proto grammer. +extend google.protobuf.FileOptions { optional Aggregate fileopt = 15478479; } +extend google.protobuf.MessageOptions { optional Aggregate msgopt = 15480088; } +extend google.protobuf.FieldOptions { optional Aggregate fieldopt = 15481374; } +extend google.protobuf.EnumOptions { optional Aggregate enumopt = 15483218; } +extend google.protobuf.EnumValueOptions { optional Aggregate enumvalopt = 15486921; } +extend google.protobuf.ServiceOptions { optional Aggregate serviceopt = 15497145; } +extend google.protobuf.MethodOptions { optional Aggregate methodopt = 15512713; } + +// Try using AggregateOption at different points in the proto grammar +option (fileopt) = { + s: 'FileAnnotation' + // Also test the handling of comments + /* of both types */ i: 100 + + sub { s: 'NestedFileAnnotation' } + + // Include a google.protobuf.FileOptions and recursively extend it with + // another fileopt. + file { + [protobuf_unittest.fileopt] { + s:'FileExtensionAnnotation' + } + } + + // A message set inside an option value + mset { + [protobuf_unittest.AggregateMessageSetElement.message_set_extension] { + s: 'EmbeddedMessageSetElement' + } + } +}; + +message AggregateMessage { + option (msgopt) = { i:101 s:'MessageAnnotation' }; + optional int32 fieldname = 1 [(fieldopt) = { s:'FieldAnnotation' }]; +} + +service AggregateService { + option (serviceopt) = { s:'ServiceAnnotation' }; + rpc Method (AggregateMessage) returns (AggregateMessage) { + option (methodopt) = { s:'MethodAnnotation' }; + } +} + +enum AggregateEnum { + option (enumopt) = { s:'EnumAnnotation' }; + // TODO: support enum value options + VALUE = 1 [(enumvalopt) = { s:'EnumValueAnnotation' }]; +} + +// Test custom options for nested type. +message NestedOptionType { + message NestedMessage { + option (message_opt1) = 1001; + optional int32 nested_field = 1 [(field_opt1) = 1002]; + } + enum NestedEnum { + option (enum_opt1) = 1003; + // TODO: support enum value options + NESTED_ENUM_VALUE = 1 [(enum_value_opt1) = 1004]; + } + extend google.protobuf.FileOptions { + optional int32 nested_extension = 7912573 [(field_opt2) = 1005]; + } +} + +// Custom message option that has a required enum field. +// WARNING: this is strongly discouraged! +message OldOptionType { + enum TestEnum { + OLD_VALUE = 0; + } + required TestEnum value = 1; +} + +// Updated version of the custom option above. +message NewOptionType { + enum TestEnum { + OLD_VALUE = 0; + NEW_VALUE = 1; + } + required TestEnum value = 1; +} + +extend google.protobuf.MessageOptions { + optional OldOptionType required_enum_opt = 106161807; +} + +// Test message using the "required_enum_opt" option defined above. +message TestMessageWithRequiredEnumOption { + option (required_enum_opt) = { value: OLD_VALUE }; +} diff --git a/spec/support/protos/google_unittest_import.pb.rb b/spec/support/protos/google_unittest_import.pb.rb new file mode 100644 index 00000000..99beaf06 --- /dev/null +++ b/spec/support/protos/google_unittest_import.pb.rb @@ -0,0 +1,55 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + + +## +# Imports +# +require 'protos/google_unittest_import_public.pb' + +module Protobuf_unittest_import + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Enum Classes + # + class ImportEnum < ::Protobuf::Enum + define :IMPORT_FOO, 7 + define :IMPORT_BAR, 8 + define :IMPORT_BAZ, 9 + end + + class ImportEnumForMap < ::Protobuf::Enum + define :UNKNOWN, 0 + define :FOO, 1 + define :BAR, 2 + end + + + ## + # Message Classes + # + class ImportMessage < ::Protobuf::Message; end + + + ## + # File Options + # + set_option :java_package, "com.google.protobuf.test" + set_option :optimize_for, ::Google::Protobuf::FileOptions::OptimizeMode::SPEED + set_option :cc_enable_arenas, true + + + ## + # Message Fields + # + class ImportMessage + optional :int32, :d, 1 + end + +end + diff --git a/spec/support/protos/google_unittest_import.proto b/spec/support/protos/google_unittest_import.proto new file mode 100644 index 00000000..b64f9cdb --- /dev/null +++ b/spec/support/protos/google_unittest_import.proto @@ -0,0 +1,73 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// A proto file which is imported by unittest.proto to test importing. + +syntax = "proto2"; + +// We don't put this in a package within proto2 because we need to make sure +// that the generated code doesn't depend on being in the proto2 namespace. +// In test_util.h we do +// "using namespace unittest_import = protobuf_unittest_import". +package protobuf_unittest_import; + +option optimize_for = SPEED; +option cc_enable_arenas = true; + +// Exercise the java_package option. +option java_package = "com.google.protobuf.test"; + +// Do not set a java_outer_classname here to verify that Proto2 works without +// one. + +// Test public import +import public "protos/google_unittest_import_public.proto"; + +message ImportMessage { + optional int32 d = 1; +} + +enum ImportEnum { + IMPORT_FOO = 7; + IMPORT_BAR = 8; + IMPORT_BAZ = 9; +} + + +// To use an enum in a map, it must has the first value as 0. +enum ImportEnumForMap { + UNKNOWN = 0; + FOO = 1; + BAR = 2; +} diff --git a/spec/support/protos/google_unittest_import_public.pb.rb b/spec/support/protos/google_unittest_import_public.pb.rb new file mode 100644 index 00000000..c6c295d2 --- /dev/null +++ b/spec/support/protos/google_unittest_import_public.pb.rb @@ -0,0 +1,31 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + +module Protobuf_unittest_import + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Message Classes + # + class PublicImportMessage < ::Protobuf::Message; end + + + ## + # File Options + # + set_option :java_package, "com.google.protobuf.test" + + + ## + # Message Fields + # + class PublicImportMessage + optional :int32, :e, 1 + end + +end + diff --git a/spec/support/protos/google_unittest_import_public.proto b/spec/support/protos/google_unittest_import_public.proto new file mode 100644 index 00000000..ffaf7736 --- /dev/null +++ b/spec/support/protos/google_unittest_import_public.proto @@ -0,0 +1,41 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: liujisi@google.com (Pherl Liu) + +syntax = "proto2"; + +package protobuf_unittest_import; + +option java_package = "com.google.protobuf.test"; + +message PublicImportMessage { + optional int32 e = 1; +} diff --git a/spec/support/protos/map-test.bin b/spec/support/protos/map-test.bin new file mode 100644 index 00000000..97ad6500 Binary files /dev/null and b/spec/support/protos/map-test.bin differ diff --git a/spec/support/protos/map-test.pb.rb b/spec/support/protos/map-test.pb.rb new file mode 100644 index 00000000..849bad87 --- /dev/null +++ b/spec/support/protos/map-test.pb.rb @@ -0,0 +1,85 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + +module Foo + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Enum Classes + # + class Frobnitz < ::Protobuf::Enum + define :FROB, 0 + define :NITZ, 1 + end + + + ## + # Message Classes + # + class Baz < ::Protobuf::Message + class DoesNotLookLikeMapEntry < ::Protobuf::Message; end + + end + + class Bar < ::Protobuf::Message + end + + + + ## + # Message Fields + # + class Baz + class DoesNotLookLikeMapEntry + optional :string, :key, 1 + optional :string, :value, 2 + end + + map :string, :string, :looks_like_map, 1 + repeated ::Foo::Baz::DoesNotLookLikeMapEntry, :does_not_look_like_map, 2 + end + + class Bar + map :sint32, ::Foo::Baz, :sint32_to_baz, 1 + map :sint64, ::Foo::Baz, :sint64_to_baz, 2 + map :int32, ::Foo::Baz, :int32_to_baz, 3 + map :int64, ::Foo::Baz, :int64_to_baz, 4 + map :uint32, ::Foo::Baz, :uint32_to_baz, 5 + map :uint64, ::Foo::Baz, :uint64_to_baz, 6 + map :string, ::Foo::Baz, :string_to_baz, 7 + map :sint32, ::Foo::Frobnitz, :sint32_to_frobnitz, 8 + map :sint64, ::Foo::Frobnitz, :sint64_to_frobnitz, 9 + map :int32, ::Foo::Frobnitz, :int32_to_frobnitz, 10 + map :int64, ::Foo::Frobnitz, :int64_to_frobnitz, 11 + map :uint32, ::Foo::Frobnitz, :uint32_to_frobnitz, 12 + map :uint64, ::Foo::Frobnitz, :uint64_to_frobnitz, 13 + map :string, ::Foo::Frobnitz, :string_to_frobnitz, 14 + map :sint32, :string, :sint32_to_string, 15 + map :sint64, :string, :sint64_to_string, 16 + map :int32, :string, :int32_to_string, 17 + map :int64, :string, :int64_to_string, 18 + map :uint32, :string, :uint32_to_string, 19 + map :uint64, :string, :uint64_to_string, 20 + map :string, :string, :string_to_string, 21 + map :sint32, :float, :sint32_to_float, 22 + map :sint64, :float, :sint64_to_float, 23 + map :int32, :float, :int32_to_float, 24 + map :int64, :float, :int64_to_float, 25 + map :uint32, :float, :uint32_to_float, 26 + map :uint64, :float, :uint64_to_float, 27 + map :string, :float, :string_to_float, 28 + map :sint32, :double, :sint32_to_double, 29 + map :sint64, :double, :sint64_to_double, 30 + map :int32, :double, :int32_to_double, 31 + map :int64, :double, :int64_to_double, 32 + map :uint32, :double, :uint32_to_double, 33 + map :uint64, :double, :uint64_to_double, 34 + map :string, :double, :string_to_double, 35 + end + +end + diff --git a/spec/support/protos/map-test.proto b/spec/support/protos/map-test.proto new file mode 100644 index 00000000..447c651d --- /dev/null +++ b/spec/support/protos/map-test.proto @@ -0,0 +1,68 @@ +// Use protoc v3.0.0 to compile this file into map-test.bin: +// protoc --descriptor_set_out=map-test.bin map-test.proto + +syntax = "proto2"; + +package foo; + +enum Frobnitz { + FROB = 0; + NITZ = 1; +} + +message Baz { + message LooksLikeMapEntry { + option map_entry = true; + optional string key = 1; + optional string value = 2; + } + repeated LooksLikeMapEntry looks_like_map = 1; + + message DoesNotLookLikeMapEntry { + optional string key = 1; + optional string value = 2; + } + repeated DoesNotLookLikeMapEntry does_not_look_like_map = 2; +} + +message Bar { + map sint32_to_baz = 1; + map sint64_to_baz = 2; + map int32_to_baz = 3; + map int64_to_baz = 4; + map uint32_to_baz = 5; + map uint64_to_baz = 6; + map string_to_baz = 7; + + map sint32_to_frobnitz = 8; + map sint64_to_frobnitz = 9; + map int32_to_frobnitz = 10; + map int64_to_frobnitz = 11; + map uint32_to_frobnitz = 12; + map uint64_to_frobnitz = 13; + map string_to_frobnitz = 14; + + map sint32_to_string = 15; + map sint64_to_string = 16; + map int32_to_string = 17; + map int64_to_string = 18; + map uint32_to_string = 19; + map uint64_to_string = 20; + map string_to_string = 21; + + map sint32_to_float = 22; + map sint64_to_float = 23; + map int32_to_float = 24; + map int64_to_float = 25; + map uint32_to_float = 26; + map uint64_to_float = 27; + map string_to_float = 28; + + map sint32_to_double = 29; + map sint64_to_double = 30; + map int32_to_double = 31; + map int64_to_double = 32; + map uint32_to_double = 33; + map uint64_to_double = 34; + map string_to_double = 35; +} diff --git a/spec/support/protos/multi_field_extensions.pb.rb b/spec/support/protos/multi_field_extensions.pb.rb new file mode 100644 index 00000000..5e48581d --- /dev/null +++ b/spec/support/protos/multi_field_extensions.pb.rb @@ -0,0 +1,59 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + +module Test + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Message Classes + # + class Header < ::Protobuf::Message + class Type < ::Protobuf::Enum + define :PayloadTypeA, 1 + define :PayloadTypeB, 2 + end + + end + + class PayloadA < ::Protobuf::Message + class Foo < ::Protobuf::Message; end + + end + + class PayloadB < ::Protobuf::Message + class Foo < ::Protobuf::Message; end + + end + + + + ## + # Message Fields + # + class Header + required ::Test::Header::Type, :type, 1 + # Extension Fields + extensions 100...536870912 + optional ::Test::PayloadA, :".test.PayloadA.payload", 100, :extension => true + end + + class PayloadA + class Foo + optional :string, :foo_a, 1 + end + + end + + class PayloadB + class Foo + optional :string, :foo_b, 1 + end + + end + +end + diff --git a/spec/support/protos/multi_field_extensions.proto b/spec/support/protos/multi_field_extensions.proto new file mode 100644 index 00000000..3a9a834d --- /dev/null +++ b/spec/support/protos/multi_field_extensions.proto @@ -0,0 +1,35 @@ +syntax = "proto2"; + +package test; + +message Header { + extensions 100 to max; + + enum Type { + PayloadTypeA = 1; + PayloadTypeB = 2; + } + + required Type type = 1; +} + +message PayloadA { + message Foo { + optional string foo_a = 1; + } + + extend Header { + optional PayloadA payload = 100; + } +} + +message PayloadB { + message Foo { + optional string foo_b = 1; + } + + // UNCOMMENT TO TEST RUNTIME FAILING WITH MULTIPLE FIELDS + // extend Header { + // optional PayloadB payload = 101; + //} +} diff --git a/spec/support/protos/optional_v3_fields.pb.rb b/spec/support/protos/optional_v3_fields.pb.rb new file mode 100644 index 00000000..058f37ff --- /dev/null +++ b/spec/support/protos/optional_v3_fields.pb.rb @@ -0,0 +1,22 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' + + +## +# Message Classes +# +class SomethingWithOptionalFields < ::Protobuf::Message; end + + +## +# Message Fields +# +class SomethingWithOptionalFields + optional :string, :i_am_optional, 1 + optional :string, :i_am_not_optional, 2 +end + diff --git a/spec/support/protos/optional_v3_fields.proto b/spec/support/protos/optional_v3_fields.proto new file mode 100644 index 00000000..b145fba2 --- /dev/null +++ b/spec/support/protos/optional_v3_fields.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; + +message SomethingWithOptionalFields { + optional string i_am_optional = 1; + string i_am_not_optional = 2; +} diff --git a/spec/support/protos/resource.pb.rb b/spec/support/protos/resource.pb.rb new file mode 100644 index 00000000..f81ef52f --- /dev/null +++ b/spec/support/protos/resource.pb.rb @@ -0,0 +1,172 @@ +# encoding: utf-8 + +## +# This file is auto-generated. DO NOT EDIT! +# +require 'protobuf' +require 'protobuf/rpc/service' + + +## +# Imports +# +require 'google/protobuf/descriptor.pb' + +module Test + ::Protobuf::Optionable.inject(self) { ::Google::Protobuf::FileOptions } + + ## + # Enum Classes + # + class StatusType < ::Protobuf::Enum + set_option :allow_alias, true + set_option :".test.enum_option", -789 + + define :PENDING, 0 + define :ENABLED, 1 + define :DISABLED, 2 + define :DELETED, 3 + define :ALIASED, 3 + end + + + ## + # Message Classes + # + class ResourceFindRequest < ::Protobuf::Message; end + class ResourceSleepRequest < ::Protobuf::Message; end + class Resource < ::Protobuf::Message; end + class ResourceWithRequiredField < ::Protobuf::Message; end + class Searchable < ::Protobuf::Message + class SearchType < ::Protobuf::Enum + define :FLAT, 1 + define :NESTED, 2 + end + + end + + class MessageParent < ::Protobuf::Message + class MessageChild < ::Protobuf::Message; end + + end + + class Nested < ::Protobuf::Message + class NestedLevelOne < ::Protobuf::Message; end + + end + + + + ## + # File Options + # + set_option :cc_generic_services, true + set_option :".test.file_option", 9876543210 + + + ## + # Message Fields + # + class ResourceFindRequest + required :string, :name, 1 + optional :bool, :active, 2 + repeated :string, :widgets, 3 + repeated :bytes, :widget_bytes, 4 + end + + class ResourceSleepRequest + optional :int32, :sleep, 1 + end + + class Resource + # Message Options + set_option :map_entry, false + set_option :".test.message_option", -56 + + required :string, :name, 1, :ctype => ::Google::Protobuf::FieldOptions::CType::CORD, :".test.field_option" => 8765432109 + optional :int64, :date_created, 2 + optional ::Test::StatusType, :status, 3 + repeated ::Test::StatusType, :repeated_enum, 4 + # Extension Fields + extensions 100...536870912 + optional :bool, :".test.Searchable.ext_is_searchable", 100, :extension => true + optional :bool, :".test.Searchable.ext_is_hidden", 101, :extension => true + optional ::Test::Searchable::SearchType, :".test.Searchable.ext_search_type", 102, :default => ::Test::Searchable::SearchType::FLAT, :extension => true + optional :bool, :".test.Nested.NestedLevelOne.ext_nested_in_level_one", 105, :extension => true + optional :bool, :".test.Nested.NestedLevelOne.ext_dup_field", 106, :extension => true + end + + class ResourceWithRequiredField + required :string, :foo_is_required, 1 + end + + class MessageParent + class MessageChild + optional :string, :child1, 1 + end + + end + + class Nested + class NestedLevelOne + optional :bool, :level_one, 1, :default => true + # Extension Fields + extensions 100...102 + optional :bool, :".test.ext_nested_level_one_outer", 101, :extension => true + optional :bool, :".test.Nested.ext_nested_level_one", 100, :extension => true + end + + optional :string, :name, 1 + optional ::Test::Resource, :resource, 2 + repeated ::Test::Resource, :multiple_resources, 3 + optional ::Test::StatusType, :status, 4 + # Extension Fields + extensions 100...111 + optional :string, :".test.foo", 100, :extension => true + optional :int64, :".test.bar", 101, :extension => true + end + + + ## + # Extended Message Fields + # + class ::Google::Protobuf::FileOptions < ::Protobuf::Message + optional :uint64, :".test.file_option", 9585869, :extension => true + end + + class ::Google::Protobuf::FieldOptions < ::Protobuf::Message + optional :uint64, :".test.field_option", 858769, :extension => true + end + + class ::Google::Protobuf::EnumOptions < ::Protobuf::Message + optional :int64, :".test.enum_option", 590284, :extension => true + end + + class ::Google::Protobuf::MessageOptions < ::Protobuf::Message + optional :int64, :".test.message_option", 485969, :extension => true + end + + class ::Google::Protobuf::ServiceOptions < ::Protobuf::Message + optional :int64, :".test.service_option", 5869607, :extension => true + end + + class ::Google::Protobuf::MethodOptions < ::Protobuf::Message + optional :int64, :".test.method_option", 7893233, :extension => true + end + + + ## + # Service Classes + # + class ResourceService < ::Protobuf::Rpc::Service + set_option :".test.service_option", -9876543210 + rpc :find, ::Test::ResourceFindRequest, ::Test::Resource do + set_option :".test.method_option", 2 + end + rpc :find_with_rpc_failed, ::Test::ResourceFindRequest, ::Test::Resource + rpc :find_with_sleep, ::Test::ResourceSleepRequest, ::Test::Resource + rpc :find_not_implemented, ::Test::ResourceFindRequest, ::Test::Resource + end + +end + diff --git a/spec/support/protos/resource.proto b/spec/support/protos/resource.proto new file mode 100644 index 00000000..70b338b3 --- /dev/null +++ b/spec/support/protos/resource.proto @@ -0,0 +1,136 @@ +syntax = "proto2"; + +import "google/protobuf/descriptor.proto"; + +package test; + +option cc_generic_services = true; +option (file_option) = 9876543210; + +extend google.protobuf.FileOptions { + optional uint64 file_option = 9585869; +} + +extend google.protobuf.FieldOptions { + optional uint64 field_option = 858769; +} + +extend google.protobuf.EnumOptions { + optional int64 enum_option = 590284; +} + +extend google.protobuf.MessageOptions { + optional int64 message_option = 485969; +} + +extend google.protobuf.ServiceOptions { + optional int64 service_option = 5869607; +} + +extend google.protobuf.MethodOptions { + optional int64 method_option = 7893233; +} + +enum StatusType { + option allow_alias = true; + option (enum_option) = -789; + + PENDING = 0; + ENABLED = 1; + DISABLED = 2; + DELETED = 3; + ALIASED = 3; +} + +message ResourceFindRequest { + required string name = 1; + optional bool active = 2; + repeated string widgets = 3; + repeated bytes widget_bytes = 4; +} + +message ResourceSleepRequest { + optional int32 sleep = 1; +} + +message Resource { + option map_entry = false; + option (message_option) = -56; + + extensions 100 to max; + + required string name = 1 [(field_option) = 8765432109, ctype = CORD]; + optional int64 date_created = 2; + optional StatusType status = 3; + repeated StatusType repeated_enum = 4; +} + +message ResourceWithRequiredField { + required string foo_is_required = 1; +} + +message Searchable { + enum SearchType { + FLAT = 1; + NESTED = 2; + } + + extend test.Resource { + optional bool ext_is_searchable = 100; + optional bool ext_is_hidden = 101; + optional Searchable.SearchType ext_search_type = 102 [default=FLAT]; + } +} + +message MessageParent { + message MessageChild { + optional string child1 = 1; + } +} + +message Nested { + extensions 100 to 110; + + optional string name = 1; + optional Resource resource = 2; + repeated Resource multiple_resources = 3; + optional StatusType status = 4; + + message NestedLevelOne { + extensions 100 to 101; + optional bool level_one = 1 [default=true]; + + extend Resource { + optional bool ext_nested_in_level_one = 105; + optional bool ext_dup_field = 106; + } + } + + extend NestedLevelOne { + optional bool ext_nested_level_one = 100; + } + +// extend Resource { +// optional bool ext_dup_field = 107; +// } +} + +extend Nested { + optional string foo = 100; + optional int64 bar = 101; +} + +extend Nested.NestedLevelOne { + optional bool ext_nested_level_one_outer = 101; +} + +service ResourceService { + option (service_option) = -9876543210; + + rpc Find (ResourceFindRequest) returns (Resource) { + option (method_option) = 2; + } + rpc FindWithRpcFailed (ResourceFindRequest) returns (Resource); + rpc FindWithSleep (ResourceSleepRequest) returns (Resource); + rpc FindNotImplemented (ResourceFindRequest) returns (Resource); +} diff --git a/spec/support/resource_service.rb b/spec/support/resource_service.rb new file mode 100644 index 00000000..8f246b30 --- /dev/null +++ b/spec/support/resource_service.rb @@ -0,0 +1,23 @@ +require PROTOS_PATH.join('resource.pb') + +Test::ResourceService.class_eval do + # request -> Test::ResourceFindRequest + # response -> Test::Resource + def find + response.name = request.name + response.status = request.active ? 1 : 0 + end + + # request -> Test::ResourceSleepRequest + # response -> Test::Resource + def find_with_sleep + sleep(request.sleep || 1) + response.name = 'Request should have timed out' + end + + # request -> Test::ResourceFindRequest + # response -> Test::Resource + def find_with_rpc_failed + rpc_failed('Find failed') + end +end diff --git a/spec/support/server.rb b/spec/support/server.rb new file mode 100644 index 00000000..16fc5678 --- /dev/null +++ b/spec/support/server.rb @@ -0,0 +1,66 @@ +require 'ostruct' + +require 'active_support' +require 'active_support/core_ext/hash/reverse_merge' + +require 'spec_helper' +require 'protobuf/logging' +require 'protobuf/rpc/server' +require 'protobuf/rpc/servers/socket/server' +require 'protobuf/rpc/servers/socket_runner' +require 'protobuf/rpc/servers/zmq/server' +require 'protobuf/rpc/servers/zmq_runner' +require SUPPORT_PATH.join('resource_service') + +# Want to abort if server dies? +Thread.abort_on_exception = true + +class StubServer + include Protobuf::Logging + + private + + attr_accessor :options, :runner, :runner_thread + + public + + def initialize(options = {}) + self.options = OpenStruct.new( + options.reverse_merge( + :host => '127.0.0.1', + :port => 9399, + :worker_port => 9400, + :delay => 0, + :server => Protobuf::Rpc::Socket::Server, + ), + ) + + start + yield self + ensure + stop + end + + def start + runner_class = { + ::Protobuf::Rpc::Zmq::Server => ::Protobuf::Rpc::ZmqRunner, + ::Protobuf::Rpc::Socket::Server => ::Protobuf::Rpc::SocketRunner, + }.fetch(options.server) + + self.runner = runner_class.new(options) + self.runner_thread = Thread.new(runner, &:run) + runner_thread.abort_on_exception = true # Set for testing purposes + Thread.pass until runner.running? + + logger.debug { sign_message("Server started #{options.host}:#{options.port}") } + end + + def stop + runner.stop + runner_thread.join + end + + def log_signature + @_log_signature ||= "[stub-server]" + end +end diff --git a/spec/support/test_app_file.rb b/spec/support/test_app_file.rb new file mode 100644 index 00000000..31da08a1 --- /dev/null +++ b/spec/support/test_app_file.rb @@ -0,0 +1,2 @@ +# For use in testing the rpc_server +ENV['TEST_APP_FILE_LOADED'] = 'true' diff --git a/spec/unit/common/logger_spec.rb b/spec/unit/common/logger_spec.rb deleted file mode 100644 index 6a31392b..00000000 --- a/spec/unit/common/logger_spec.rb +++ /dev/null @@ -1,121 +0,0 @@ -require 'protobuf/common/logger' -require 'stringio' - -describe Protobuf::Logger do - - subject { Protobuf::Logger } - - before(:each) do - Protobuf::Logger.reset_device! - Protobuf::Logger.file = '/dev/null' - Protobuf::Logger.level = ::Logger::INFO - end - - describe '.instance' do - - it 'doesn\'t create a logger if the file was not set' do - subject.file = nil - subject.instance.should be_nil - end - - it 'doesn\'t create a logger if the level was not set' do - subject.level = nil - subject.instance.should be_nil - end - - it 'gets a new instance of the logger when file and level are set' do - subject.file.should_not be_nil - subject.level.should_not be_nil - subject.instance.should_not be_nil - end - - it 'keeps the same object from multiple calls to instance' do - subject.instance === subject.instance - end - - end - - describe '.configure' do - before(:each) { subject.reset_device! } - it 'sets the file and level in one call' do - subject.file.should_not be - subject.level.should_not be - subject.instance.should_not be - subject.configure :file => 'myfile.log', :level => ::Logger::WARN - subject.file.should == 'myfile.log' - subject.level.should == ::Logger::WARN - subject.instance.level.should == ::Logger::WARN - end - - end - - describe '.reset_device!' do - - it 'resets the logger instance, file, and level' do - subject.instance.should be - subject.file.should be - subject.level.should be - subject.reset_device! - subject.instance.should_not be - subject.file.should_not be - subject.level.should_not be - end - - end - - context 'when logging' do - - it 'doesn\'t raise errors when log instance is nil' do - subject.reset_device! - subject.instance.should be_nil - expect { - subject.debug 'No errors here' - subject.info 'No errors here' - subject.warn 'No errors here' - subject.error 'No errors here' - subject.fatal 'No errors here' - subject.add 'No errors here' - subject.log 'No errors here' - }.should_not raise_error - end - - it 'logs correctly when instance is valid' do - subject.instance.should_not be_nil - subject.instance.should_receive(:info).with('Should log great') - subject.info 'Should log great' - end - - end - - describe Protobuf::Logger::LogMethods do - - context 'when included in another class' do - - before(:all) do - class MyTestClass - include Protobuf::Logger::LogMethods - end - end - - subject { MyTestClass.new } - - it 'responds to all logger methods' do - subject.should respond_to :log_debug - subject.should respond_to :log_info - subject.should respond_to :log_warn - subject.should respond_to :log_error - subject.should respond_to :log_fatal - subject.should respond_to :log_add - subject.should respond_to :log_log - end - - it 'passes all embedded log calls to Logger instance' do - Protobuf::Logger.instance.should_receive(:debug).with('log this') - subject.log_debug('log this') - end - - end - - end - -end \ No newline at end of file diff --git a/spec/unit/common/util_spec.rb b/spec/unit/common/util_spec.rb deleted file mode 100644 index 820dbca7..00000000 --- a/spec/unit/common/util_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'spec_helper' - -describe Protobuf::Util do - - describe '.underscore' do - it 'underscores constant name' do - Protobuf::Util.underscore("HelloMoto").should eq "hello_moto" - end - - context 'when constant name has uppercased word' do - it 'keeps the uppercased word together' do - Protobuf::Util.underscore("UPPERCase").should eq "upper_case" - Protobuf::Util.underscore("MDXService").should eq "mdx_service" - end - end - end -end \ No newline at end of file diff --git a/spec/unit/enum_spec.rb b/spec/unit/enum_spec.rb deleted file mode 100644 index 2a5f337f..00000000 --- a/spec/unit/enum_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' -require 'spec/proto/test.pb' - -describe Protobuf::Enum do - context 'when coercing from enum' do - subject { Spec::Proto::StatusType::PENDING } - it { should eq(0) } - end - - context 'when coercing from integer' do - it { 0.should eq(Spec::Proto::StatusType::PENDING) } - end -end diff --git a/spec/unit/message_spec.rb b/spec/unit/message_spec.rb deleted file mode 100644 index 5a76ad92..00000000 --- a/spec/unit/message_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' -require 'spec/proto/test.pb' - -describe Protobuf::Message do - - context 'when converting to json' do - - it "should be jsonable" do - msg = Spec::Proto::ResourceFindRequest.new - msg.should respond_to(:to_json) - msg.name = 'Jeff' - msg.active = false - msg.to_json.should == '{"name":"Jeff","active":false}' - end - - it 'should generate nested messages into nested json objects' do - date_created = Time.now.to_i - - nested = Spec::Proto::Nested.new - nested.name = 'NESTED' - nested.resource = Spec::Proto::Resource.new.tap do |r| - r.name = 'RESOURCE SINGLE' - r.date_created = date_created - r.status = Spec::Proto::StatusType::PENDING - end - 2.times do |i| - nested.multiple_resources << Spec::Proto::Resource.new.tap do |r| - r.name = 'RESOURCE MULTIPLE %d' % i - r.date_created = date_created - r.status = Spec::Proto::StatusType::PENDING - end - end - nested.status = Spec::Proto::StatusType::ENABLED - - nested.to_json.should == %Q{{"name":"NESTED","resource":{"name":"RESOURCE SINGLE","date_created":#{date_created},"status":0,"repeated_enum":[]},"multiple_resources":[{"name":"RESOURCE MULTIPLE 0","date_created":#{date_created},"status":0,"repeated_enum":[]},{"name":"RESOURCE MULTIPLE 1","date_created":#{date_created},"status":0,"repeated_enum":[]}],"status":1}} - end - - - end - - context 'when converting to a hash' do - - context 'when message has repeated enum field' do - - it 'provides an array of integers' do - resource = Spec::Proto::Resource.new :repeated_enum => [ - Spec::Proto::StatusType::PENDING, - Spec::Proto::StatusType::ENABLED, - Spec::Proto::StatusType::ENABLED, - Spec::Proto::StatusType::DISABLED, - Spec::Proto::StatusType::DELETED - ] - - resource.to_hash[:repeated_enum].should == [ - Spec::Proto::StatusType::PENDING.value, - Spec::Proto::StatusType::ENABLED.value, - Spec::Proto::StatusType::ENABLED.value, - Spec::Proto::StatusType::DISABLED.value, - Spec::Proto::StatusType::DELETED.value - ] - end - - end - - end - -end \ No newline at end of file diff --git a/spec/unit/rpc/client_spec.rb b/spec/unit/rpc/client_spec.rb deleted file mode 100644 index 211265b6..00000000 --- a/spec/unit/rpc/client_spec.rb +++ /dev/null @@ -1,197 +0,0 @@ -require 'spec_helper' -require 'spec/proto/test_service_impl' - -describe Protobuf::Rpc::Client do - - context "when using fiber based calls" do - it "waits for response when running synchronously" do - EventMachine.fiber_run do - StubServer.new(:delay => 3) do |server| - client = Spec::Proto::TestService.client(:async => false) - start = now - - client.find(:name => "Test Name", :active => true) do |c| - c.on_success do |succ| - succ.name.should eq("Test Name") - succ.status.should eq(Spec::Proto::StatusType::ENABLED) - end - - c.on_failure do |err| - raise err.inspect - end - end - - (now - start).should be_within(server.options.delay * 0.10).of(server.options.delay) - end - - EM.stop - end - end - - it "doesn't wait for response when running async call inside fiber" do - EventMachine.fiber_run do - StubServer.new(:delay => 3) do |server| - client = Spec::Proto::TestService.client(:async => true) - start = now - client.find(:name => "Test Name", :active => true) - - (now - start).should_not be_within(server.options.delay* 0.10).of(server.options.delay) - end - EM.stop - end - end - - it "throws and error when synchronous code is attempted without 'EventMachine.fiber_run'" do - subject = Proc.new do - EventMachine.run do - StubServer.new(:delay => 1) do |server| - client = Spec::Proto::TestService.client(:async => false) - client.find(:name => "Test Name", :active => true) - end - end - end - - subject.should raise_error(RuntimeError, /EM.fiber_run/) - end - - it "throws a timeout when client timeout is exceeded" do - subject = Proc.new do - EventMachine.fiber_run do - StubServer.new(:delay => 2) do |server| - client = Spec::Proto::TestService.client(:async => false, :timeout => 1) - client.find(:name => "Test Name", :active => true) - end - EM.stop - end - end - - subject.should raise_error(RuntimeError, /timeout/i) - end - - context "without reactor_running?" do - - it "throws a timeout when client timeout is exceeded" do - subject = Proc.new do - StubServer.new(:delay => 2) do |server| - client = Spec::Proto::TestService.client(:async => false, :timeout => 1) - client.find(:name => "Test Name", :active => true) - end - end - - subject.should raise_error(RuntimeError, /timeout/i) - end - - it "calls failure_cb & throws a timeout when client timeout is exceeded" do - failure_message = nil - - subject = Proc.new do - StubServer.new(:delay => 2) do |server| - client = Spec::Proto::TestService.client(:async => false, :timeout => 1) - client.find(:name => "Test Name", :active => true) do |c| - c.on_failure do |f| - failure_message = f.message - end - end - end - end - - subject.call - failure_message.should match(/timeout/) - end - - end - - end - - context 'when creating a client from a service' do - - it 'should be able to get a client through the Service#client helper method' do - Spec::Proto::TestService.client(:port => 9191).should eq(Protobuf::Rpc::Client.new(:service => Spec::Proto::TestService, :port => 9191)) - end - - it "should be able to override a service location's host and port" do - Spec::Proto::TestService.located_at 'somewheregreat.com:12345' - clean_client = Spec::Proto::TestService.client - clean_client.options[:host].should eq('somewheregreat.com') - clean_client.options[:port].should eq(12345) - - updated_client = Spec::Proto::TestService.client(:host => 'amazing.com', :port => 54321) - updated_client.options[:host].should eq('amazing.com') - updated_client.options[:port].should eq(54321) - end - - it 'should be able to define the syncronicity of the client request' do - client = Spec::Proto::TestService.client(:async => false) - client.options[:async].should be_false - client.async?.should be_false - - client = Spec::Proto::TestService.client(:async => true) - client.options[:async].should be_true - client.async?.should be_true - end - - it 'should be able to define which service to create itself for' do - client = Protobuf::Rpc::Client.new :service => Spec::Proto::TestService - client.options[:service].should eq(Spec::Proto::TestService) - end - - it 'should have a hard default for host and port on a service that has not been configured' do - reset_service_location Spec::Proto::TestService - client = Spec::Proto::TestService.client - client.options[:host].should eq(Protobuf::Rpc::Service::DEFAULT_LOCATION[:host]) - client.options[:port].should eq(Protobuf::Rpc::Service::DEFAULT_LOCATION[:port]) - end - - end - - context 'when calling methods on a service client' do - - # NOTE: we are assuming the service methods are accurately - # defined inside spec/proto/test_service.rb, - # namely the :find method - - it 'should respond to defined service methods' do - client = Spec::Proto::TestService.client - client.should_receive(:send_request).and_return(nil) - expect { client.find(nil) }.should_not raise_error - end - - it 'raises a NameError when accessing a var that does not exist' do - pending - end - - it 'should be able to set and get local variables within client response blocks' do - outer_value = 'OUTER' - inner_value = 'INNER' - client = Spec::Proto::TestService.client(:async => true) - - EM.should_receive(:reactor_running?).and_return(true) - EM.stub!(:next_tick) do - client.success_cb.call(inner_value) - end - - client.find(nil) do |c| - c.on_success do |response| - outer_value.should eq('OUTER') - outer_value = response - end - end - outer_value.should eq(inner_value) - end - - end - - context 'when receiving request objects' do - - it 'should be able to create the correct request object if passed a hash' do - client = Spec::Proto::TestService.client - client.should_receive(:send_request) - client.find({:name => 'Test Name', :active => false}) - client.options[:request].should be_a(Spec::Proto::ResourceFindRequest) - client.options[:request].name.should eq('Test Name') - client.options[:request].active.should eq(false) - end - - end - -end diff --git a/spec/unit/rpc/connector_spec.rb b/spec/unit/rpc/connector_spec.rb deleted file mode 100644 index ae966738..00000000 --- a/spec/unit/rpc/connector_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'spec_helper' - -describe Protobuf::Rpc::Connector do - - describe '.connector_for_client' do - - context 'when set to Socket connector' do - it 'returns a socket connector class reference' do - with_constants "Protobuf::ClientType" => "Socket" do - Protobuf::Rpc::Connector.connector_for_client.should eq(Protobuf::Rpc::Connectors::Socket) - end - end - end - - context 'when set to non Socket Connector' do - it 'returns an eventmachine connector class reference' do - with_constants "Protobuf::ClientType" => "EventMachine" do - Protobuf::Rpc::Connector.connector_for_client.should eq Protobuf::Rpc::Connectors::EventMachine - end - end - end - - context 'when connector type not given' do - it 'returns an eventmachine connector class reference' do - Protobuf::Rpc::Connector.connector_for_client.should eq Protobuf::Rpc::Connectors::EventMachine - end - end - - end - -end diff --git a/spec/unit/rpc/connectors/base_spec.rb b/spec/unit/rpc/connectors/base_spec.rb deleted file mode 100644 index 380fd69b..00000000 --- a/spec/unit/rpc/connectors/base_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'spec_helper' - -describe Protobuf::Rpc::Connectors::Base do - - let(:opts) do - { async: false, timeout: 60 } - end - - subject { Protobuf::Rpc::Connectors::Base.new(opts) } - - describe "#send_request" do - it "raising an error when 'send_request' is not overridden" do - expect{ subject.send_request }.to raise_error(RuntimeError, /inherit a Connector/) - end - - it "does not raise error when 'send_request' is overridden" do - new_sub = Class.new(subject.class){ def send_request; end }.new(opts) - expect{ new_sub.send_request }.to_not raise_error - end - end - - describe '.new' do - it 'assigns passed options and initializes success/failure callbacks' do - subject.options.should eq(Protobuf::Rpc::Connectors::DEFAULT_OPTIONS.merge(opts)) - subject.success_cb.should be_nil - subject.failure_cb.should be_nil - end - end - - describe '#success_cb' do - it 'allows setting the success callback and calling it' do - subject.success_cb.should be_nil - cb = proc {|res| raise res } - subject.success_cb = cb - subject.success_cb.should eq(cb) - expect { subject.success_cb.call('an error from cb') }.to raise_error 'an error from cb' - end - end - - describe '#failure_cb' do - it 'allows setting the failure callback and calling it' do - subject.failure_cb.should be_nil - cb = proc {|res| raise res } - subject.failure_cb = cb - subject.failure_cb.should eq(cb) - expect { subject.failure_cb.call('an error from cb') }.to raise_error 'an error from cb' - end - end - - describe '#async?' do - context 'when provided options[:async] is false' do - let(:opts) do - { async: false, timeout: 60 } - end - - subject { Protobuf::Rpc::Connectors::Base.new(opts) } - - it 'returns false' do - subject.async?.should be_false - end - end - - context 'when provided options[:async] is true' do - let(:opts) do - { async: true, timeout: 60 } - end - - subject { Protobuf::Rpc::Connectors::Base.new(opts) } - - it 'returns true' do - subject.async?.should be_true - end - end - - context 'when options doesn\'t denote async' do - let(:opts) do - { timeout: 60 } - end - - subject { Protobuf::Rpc::Connectors::Base.new(opts) } - - it 'returns false' do - subject.async?.should be_false - end - end - end - -end diff --git a/spec/unit/rpc/connectors/common_spec.rb b/spec/unit/rpc/connectors/common_spec.rb deleted file mode 100644 index 72d9c6d6..00000000 --- a/spec/unit/rpc/connectors/common_spec.rb +++ /dev/null @@ -1,132 +0,0 @@ -require 'spec_helper' -require 'protobuf/rpc/service' - -describe Protobuf::Rpc::Connectors::Common do - let(:common_class) do - Class.new(Protobuf::Rpc::Connectors::Base) do - include Protobuf::Rpc::Connectors::Common - attr_accessor :options - attr_accessor :stats - end - end - - subject{ @subject ||= common_class.new({}) } - - context "API" do - specify{ subject.respond_to?(:any_callbacks?).should be_true } - specify{ subject.respond_to?(:data_callback).should be_true } - specify{ subject.respond_to?(:error).should be_true } - specify{ subject.respond_to?(:fail).should be_true } - specify{ subject.respond_to?(:complete).should be_true } - specify{ subject.respond_to?(:parse_response).should be_true } - specify{ subject.respond_to?(:_send_request).should be_true } - specify{ subject.respond_to?(:verify_options).should be_true } - specify{ subject.respond_to?(:verify_callbacks).should be_true } - end - - context "#any_callbacks?" do - - [:@complete_cb, :@success_cb, :@failure_cb].each do |cb| - it "returns true if #{cb} is provided" do - subject.instance_variable_set(cb, "something") - subject.any_callbacks?.should be_true - end - end - - it "returns false when all callbacks are not provided" do - subject.instance_variable_set(:@complete_cb, nil) - subject.instance_variable_set(:@success_cb, nil) - subject.instance_variable_set(:@failure_cb, nil) - - subject.any_callbacks?.should be_false - end - - end - - context "#data_callback" do - it "changes state to use the data callback" do - subject.data_callback("data") - subject.instance_variable_get(:@used_data_callback).should be_true - end - - it "sets the data var when using the data_callback" do - subject.data_callback("data") - subject.instance_variable_get(:@data).should eq("data") - end - end - - context "#verify_callbacks" do - - it "sets @failure_cb to #data_callback when no callbacks are defined" do - subject.verify_callbacks - subject.instance_variable_get(:@failure_cb).should eq(subject.method(:data_callback)) - end - - it "sets @success_cb to #data_callback when no callbacks are defined" do - subject.verify_callbacks - subject.instance_variable_get(:@success_cb).should eq(subject.method(:data_callback)) - end - - it "doesn't set @failure_cb when already defined" do - set_cb = lambda{ true } - subject.instance_variable_set(:@failure_cb, set_cb) - subject.verify_callbacks - subject.instance_variable_get(:@failure_cb).should eq(set_cb) - subject.instance_variable_get(:@failure_cb).should_not eq(subject.method(:data_callback)) - end - - it "doesn't set @success_cb when already defined" do - set_cb = lambda{ true } - subject.instance_variable_set(:@success_cb, set_cb) - subject.verify_callbacks - subject.instance_variable_get(:@success_cb).should eq(set_cb) - subject.instance_variable_get(:@success_cb).should_not eq(subject.method(:data_callback)) - end - - end - - shared_examples "a ConnectorDisposition" do |meth, cb, *args| - - it "calls #complete before exit" do - stats = double("Object") - stats.stub(:end) { true } - stats.stub(:log_stats) { true } - subject.stats = stats - - subject.should_receive(:complete) - subject.method(meth).call(*args) - end - - it "calls the #{cb} callback when provided" do - stats = double("Object") - stats.stub(:end) { true } - stats.stub(:log_stats) { true } - subject.stats = stats - _cb = double("Object") - - subject.instance_variable_set("@#{cb}", _cb) - _cb.should_receive(:call).and_return(true) - subject.method(meth).call(*args) - end - - it "calls the complete callback when provided" do - stats = double("Object") - stats.stub(:end) { true } - stats.stub(:log_stats) { true } - subject.stats = stats - comp_cb = double("Object") - - subject.instance_variable_set(:@complete_cb, comp_cb) - comp_cb.should_receive(:call).and_return(true) - subject.method(meth).call(*args) - end - - end - - it_behaves_like("a ConnectorDisposition", :fail, "failure_cb", "code", "message") - it_behaves_like("a ConnectorDisposition", :fail, "complete_cb", "code", "message") - it_behaves_like("a ConnectorDisposition", :succeed, "complete_cb", "response") - it_behaves_like("a ConnectorDisposition", :succeed, "success_cb", "response") - it_behaves_like("a ConnectorDisposition", :complete, "complete_cb") - -end diff --git a/spec/unit/rpc/connectors/eventmachine_spec.rb b/spec/unit/rpc/connectors/eventmachine_spec.rb deleted file mode 100644 index e69de29b..00000000 diff --git a/spec/unit/rpc/connectors/socket_spec.rb b/spec/unit/rpc/connectors/socket_spec.rb deleted file mode 100644 index 4e7bd335..00000000 --- a/spec/unit/rpc/connectors/socket_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'spec_helper' - -shared_examples "a Protobuf Connector" do - subject{ described_class.new({}) } - - context "API" do - # Check the API - specify{ subject.respond_to?(:send_request, true).should be_true } - specify{ subject.respond_to?(:post_init, true).should be_true } - specify{ subject.respond_to?(:close_connection, true).should be_true } - specify{ subject.respond_to?(:error?, true).should be_true } - end -end - -describe Protobuf::Rpc::Connectors::Socket do - subject{ described_class.new({}) } - - it_behaves_like "a Protobuf Connector" - - specify{ described_class.include?(Protobuf::Rpc::Connectors::Common).should be_true } - - context "#read_response" do - let(:data){ "New data" } - - it "fills the buffer with data from the socket" do - socket = StringIO.new("#{data.bytesize}-#{data}") - subject.instance_variable_set(:@buffer, Protobuf::Rpc::Buffer.new(:read)) - subject.instance_variable_set(:@socket, socket) - subject.should_receive(:parse_response).and_return(true) - - subject.__send__(:read_response) - subject.instance_variable_get(:@buffer).flushed?.should be_true - subject.instance_variable_get(:@buffer).data.should eq(data) - end - end - - context "#check_async" do - it "raises an error when trying to execute asynchronously" do - conn = described_class.new(:async => true) - expect{ conn.__send__(:check_async) }.to raise_error - end - - it "allows execution when synchronous" do - conn = described_class.new(:async => false) - expect{ conn.__send__(:check_async) }.to_not raise_error - end - end - -end diff --git a/spec/unit/rpc/servers/evented_server_spec.rb b/spec/unit/rpc/servers/evented_server_spec.rb deleted file mode 100644 index 90655a20..00000000 --- a/spec/unit/rpc/servers/evented_server_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'spec_helper' -require 'spec/proto/test_service_impl' -require 'protobuf/rpc/servers/evented_runner' - -describe Protobuf::Rpc::EventedServer do - - it "provides a Runner class" do - runner_class = described_class.to_s.gsub(/Server/, "Runner") - expect { Protobuf::Util.constantize(runner_class) }.to_not raise_error - end - - it "Runner provides a stop method" do - runner_class = described_class.to_s.gsub(/Server/, "Runner") - runner_class = Protobuf::Util.constantize(runner_class) - runner_class.respond_to?(:stop).should be_true - end - -end diff --git a/spec/unit/rpc/servers/socket_server_spec.rb b/spec/unit/rpc/servers/socket_server_spec.rb deleted file mode 100644 index 7ba388bf..00000000 --- a/spec/unit/rpc/servers/socket_server_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' -require 'spec/proto/test_service_impl' -require 'protobuf/rpc/servers/socket_runner' - -describe Protobuf::Rpc::SocketServer do - before(:all) do - server = OpenStruct.new(:server => "127.0.0.1", :port => 9399, :backlog => 100, :threshold => 100) - @server_thread = Thread.new(server) { |s| Protobuf::Rpc::SocketRunner.run(s) } - Thread.pass until Protobuf::Rpc::SocketServer.running? - end - - after(:all) do - Protobuf::Rpc::SocketRunner.stop - Thread.kill(@server_thread) - end - - it "Runner provides a stop method" do - runner_class = described_class.to_s.gsub(/Server/, "Runner") - runner_class = Protobuf::Util.constantize(runner_class) - runner_class.respond_to?(:stop).should be_true - end - - it "provides a stop method" do - described_class.respond_to?(:stop).should be_true - end - - it "provides a Runner class" do - runner_class = described_class.to_s.gsub(/Server/, "Runner") - expect { Protobuf::Util.constantize(runner_class) }.to_not raise_error - end - - it "signals the Server is running" do - described_class.running?.should be_true - end - - context "Eventmachine client" do - it "calls the service in the client request" do - with_constants "Protobuf::ClientType" => "Evented" do - client = Spec::Proto::TestService.client(:async => false, :port => 9399, :host => "127.0.0.1") - - client.find(:name => "Test Name", :active => true) do |c| - c.on_success do |succ| - succ.name.should eq("Test Name") - succ.status.should eq(Spec::Proto::StatusType::ENABLED) - end - - c.on_failure do |err| - raise err.inspect - end - end - end - end - - end - -end diff --git a/spec/unit/rpc/service_spec.rb b/spec/unit/rpc/service_spec.rb deleted file mode 100644 index 4b200f74..00000000 --- a/spec/unit/rpc/service_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'spec_helper' -require 'spec/proto/test_service_impl' - -describe Protobuf::Rpc::Service do - - context 'when configuring' do - before :each do - reset_service_location Spec::Proto::TestService - end - - it 'should have a default location configured' do - Spec::Proto::TestService.host.should == Protobuf::Rpc::Service::DEFAULT_LOCATION[:host] - Spec::Proto::TestService.port.should == Protobuf::Rpc::Service::DEFAULT_LOCATION[:port] - end - - it "should be able to pre-configure a service location for clients" do - Spec::Proto::TestService.located_at 'google.com:12345' - client = Spec::Proto::TestService.client - client.options[:host].should == 'google.com' - client.options[:port].should == 12345 - end - - it 'should be able to configure and read the host' do - Spec::Proto::TestService.configure :host => 'somehost.com' - Spec::Proto::TestService.host.should == 'somehost.com' - end - - it 'should be able to configure and read the port' do - Spec::Proto::TestService.configure :port => 12345 - Spec::Proto::TestService.port.should == 12345 - end - - it 'should skip configuring location if the location passed does not match host:port syntax' do - invalid_locations = [nil, 'myhost:', ':9939', 'badhost123'] - invalid_locations.each do |location| - Spec::Proto::TestService.located_at location - Spec::Proto::TestService.host.should == Protobuf::Rpc::Service::DEFAULT_LOCATION[:host] - Spec::Proto::TestService.port.should == Protobuf::Rpc::Service::DEFAULT_LOCATION[:port] - end - end - end - - context 'when server calls the service method' do - - before(:all) do - class ::NewTestService < Protobuf::Rpc::Service - rpc :bad_method, Spec::Proto::ResourceFindRequest, Spec::Proto::Resource - rpc :bad_var, Spec::Proto::ResourceFindRequest, Spec::Proto::Resource - def bad_method - hash = {} - hash[:one].explode - end - def bad_var - invalidvar - end - end - end - - it 'raises an undefined method name error when calling a method on a non-existant object' do - expect { - req = mock('RequestWrapper', :request_proto => Spec::Proto::ResourceFindRequest.new.to_s) - ::NewTestService.new.bad_method(req) - }.to raise_error(NoMethodError) - end - - it 'raises a name error when accessing a non-existant object' do - expect { - req = mock('RequestWrapper', :request_proto => Spec::Proto::ResourceFindRequest.new.to_s) - ::NewTestService.new.bad_var(req) - }.to raise_error(NameError) - end - - end - -end \ No newline at end of file diff --git a/test/check_unbuild.rb b/test/check_unbuild.rb deleted file mode 100755 index 3ca0c32e..00000000 --- a/test/check_unbuild.rb +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env ruby - -$:.push "#{File.dirname(__FILE__)}/../lib" - -require 'test/unit' -require 'protobuf/descriptor/descriptor_builder' -require 'protobuf/descriptor/descriptor_proto' - -class DescriptorTest < Test::Unit::TestCase - def test_unbuild - tutorial_proto = Google::Protobuf::FileDescriptorProto.new - tutorial_proto.parse_from_file 'person.bin' - Protobuf::Descriptor::DescriptorBuilder.build tutorial_proto - - assert_nothing_raised {Tutorial::Person} - assert_nothing_raised {Tutorial::Person.new} - assert_equal(['age', 'email', 'id', 'name', 'phone'], - Tutorial::Person.fields.map{|tag, field| field.name}.sort) - - assert_nothing_raised {Tutorial::Person::PhoneNumber} - assert_nothing_raised {Tutorial::Person::PhoneNumber.new} - assert_equal(['number', 'type'], - Tutorial::Person::PhoneNumber.fields.map{|tag, field| field.name}.sort) - - assert_nothing_raised {Tutorial::Person::PhoneType} - assert_equal(0, Tutorial::Person::PhoneType::MOBILE) - assert_equal(1, Tutorial::Person::PhoneType::HOME) - assert_equal(2, Tutorial::Person::PhoneType::WORK) - end -end diff --git a/test/data/data.bin b/test/data/data.bin deleted file mode 100644 index 42632981..00000000 --- a/test/data/data.bin +++ /dev/null @@ -1,3 +0,0 @@ - -John Doe� jdoe@example.com" -555-4321 \ No newline at end of file diff --git a/test/data/data_source.py b/test/data/data_source.py deleted file mode 100644 index 8e2c17d6..00000000 --- a/test/data/data_source.py +++ /dev/null @@ -1,14 +0,0 @@ -import addressbook_pb2 -import sys - -person = addressbook_pb2.Person() -person.id = 1234 -person.name = "John Doe" -person.email = "jdoe@example.com" -phone = person.phone.add() -phone.number = "555-4321" -phone.type = addressbook_pb2.Person.HOME - -f = open('data.bin', 'wb') -f.write(person.SerializeToString()) -f.close() diff --git a/test/data/types.bin b/test/data/types.bin deleted file mode 100644 index 07e0f18d..00000000 Binary files a/test/data/types.bin and /dev/null differ diff --git a/test/data/types_source.py b/test/data/types_source.py deleted file mode 100644 index 14b3094d..00000000 --- a/test/data/types_source.py +++ /dev/null @@ -1,22 +0,0 @@ -import types_pb2 -import sys - -types = types_pb2.TestTypes() -types.type1 = 0.01 -types.type2 = 0.1 -types.type3 = 1 -types.type4 = 10 -types.type5 = 100 -types.type6 = 1000 -types.type7 = -1 -types.type8 = -10 -types.type9 = 10000 -types.type10 = 100000 -types.type11 = False -types.type12 = 'hello all types' -# TODO test type13 -#types.type13 = - -f = open('types.bin', 'wb') -f.write(types.SerializeToString()) -f.close() diff --git a/test/data/unk.png b/test/data/unk.png deleted file mode 100644 index 37eabea7..00000000 Binary files a/test/data/unk.png and /dev/null differ diff --git a/test/proto/addressbook.pb.rb b/test/proto/addressbook.pb.rb deleted file mode 100644 index 1f9a113a..00000000 --- a/test/proto/addressbook.pb.rb +++ /dev/null @@ -1,66 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# package tutorial; -# -# message Person { -# required string name = 1; -# required int32 id = 2; -# optional string email = 3; -# -# enum PhoneType { -# MOBILE = 0; -# HOME = 1; -# WORK = 2; -# } -# -# message PhoneNumber { -# required string number = 1; -# optional PhoneType type = 2 [default = HOME]; -# } -# -# repeated PhoneNumber phone = 4; -# optional uint32 age = 5 [default = 20]; -# -# extensions 100 to 200; -# } -# -# /* -# extend Person { -# optional int32 age = 100; -# } -# */ -# -# message AddressBook { -# repeated Person person = 1; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Tutorial - class Person < ::Protobuf::Message - defined_in __FILE__ - required :string, :name, 1 - required :int32, :id, 2 - optional :string, :email, 3 - class PhoneType < ::Protobuf::Enum - defined_in __FILE__ - define :MOBILE, 0 - define :HOME, 1 - define :WORK, 2 - end - class PhoneNumber < ::Protobuf::Message - defined_in __FILE__ - required :string, :number, 1 - optional :PhoneType, :type, 2, :default => :HOME - end - repeated :PhoneNumber, :phone, 4 - optional :uint32, :age, 5, :default => 20 - extensions 100..200 - end - class AddressBook < ::Protobuf::Message - defined_in __FILE__ - repeated :Person, :person, 1 - end -end \ No newline at end of file diff --git a/test/proto/addressbook.proto b/test/proto/addressbook.proto deleted file mode 100644 index d8350438..00000000 --- a/test/proto/addressbook.proto +++ /dev/null @@ -1,33 +0,0 @@ -package tutorial; - -message Person { - required string name = 1; - required int32 id = 2; - optional string email = 3; - - enum PhoneType { - MOBILE = 0; - HOME = 1; - WORK = 2; - } - - message PhoneNumber { - required string number = 1; - optional PhoneType type = 2 [default = HOME]; - } - - repeated PhoneNumber phone = 4; - optional uint32 age = 5 [default = 20]; - - extensions 100 to 200; -} - -/* -extend Person { - optional int32 age = 100; -} -*/ - -message AddressBook { - repeated Person person = 1; -} diff --git a/test/proto/addressbook_base.pb.rb b/test/proto/addressbook_base.pb.rb deleted file mode 100644 index 95b2c6ad..00000000 --- a/test/proto/addressbook_base.pb.rb +++ /dev/null @@ -1,58 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# package tutorial_ext; -# -# message Person { -# required string name = 1; -# required int32 id = 2; -# optional string email = 3; -# -# enum PhoneType { -# MOBILE = 0; -# HOME = 1; -# WORK = 2; -# } -# -# message PhoneNumber { -# required string number = 1; -# optional PhoneType type = 2 [default = HOME]; -# } -# -# repeated PhoneNumber phone = 4; -# -# extensions 100 to 200; -# } -# -# message AddressBook { -# repeated Person person = 1; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module TutorialExt - class Person < ::Protobuf::Message - defined_in __FILE__ - required :string, :name, 1 - required :int32, :id, 2 - optional :string, :email, 3 - class PhoneType < ::Protobuf::Enum - defined_in __FILE__ - define :MOBILE, 0 - define :HOME, 1 - define :WORK, 2 - end - class PhoneNumber < ::Protobuf::Message - defined_in __FILE__ - required :string, :number, 1 - optional :PhoneType, :type, 2, :default => :HOME - end - repeated :PhoneNumber, :phone, 4 - extensions 100..200 - end - class AddressBook < ::Protobuf::Message - defined_in __FILE__ - repeated :Person, :person, 1 - end -end \ No newline at end of file diff --git a/test/proto/addressbook_base.proto b/test/proto/addressbook_base.proto deleted file mode 100644 index adb2ece0..00000000 --- a/test/proto/addressbook_base.proto +++ /dev/null @@ -1,26 +0,0 @@ -package tutorial_ext; - -message Person { - required string name = 1; - required int32 id = 2; - optional string email = 3; - - enum PhoneType { - MOBILE = 0; - HOME = 1; - WORK = 2; - } - - message PhoneNumber { - required string number = 1; - optional PhoneType type = 2 [default = HOME]; - } - - repeated PhoneNumber phone = 4; - - extensions 100 to 200; -} - -message AddressBook { - repeated Person person = 1; -} diff --git a/test/proto/addressbook_ext.pb.rb b/test/proto/addressbook_ext.pb.rb deleted file mode 100644 index 79e7b31b..00000000 --- a/test/proto/addressbook_ext.pb.rb +++ /dev/null @@ -1,20 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# import "test/proto/addressbook_base.proto"; -# package tutorial_ext; -# -# extend Person { -# optional int32 age = 100; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -require 'test/proto/addressbook_base.pb' -module TutorialExt - class Person < ::Protobuf::Message - defined_in __FILE__ - optional :int32, :age, 100, :extension => true - end -end \ No newline at end of file diff --git a/test/proto/addressbook_ext.proto b/test/proto/addressbook_ext.proto deleted file mode 100644 index 0fe9397c..00000000 --- a/test/proto/addressbook_ext.proto +++ /dev/null @@ -1,6 +0,0 @@ -import "test/proto/addressbook_base.proto"; -package tutorial_ext; - -extend Person { - optional int32 age = 100; -} diff --git a/test/proto/collision.pb.rb b/test/proto/collision.pb.rb deleted file mode 100644 index 83cd91ac..00000000 --- a/test/proto/collision.pb.rb +++ /dev/null @@ -1,17 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# message CollisionTest { -# optional string a = 1; -# optional string b = 1; -# } -# - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -class CollisionTest < ::Protobuf::Message - defined_in __FILE__ - optional :string, :a, 1 - optional :string, :b, 1 -end diff --git a/test/proto/collision.proto b/test/proto/collision.proto deleted file mode 100644 index b765c8ea..00000000 --- a/test/proto/collision.proto +++ /dev/null @@ -1,5 +0,0 @@ -message CollisionTest { - optional string a = 1; - optional string b = 1; -} - diff --git a/test/proto/ext_collision.pb.rb b/test/proto/ext_collision.pb.rb deleted file mode 100644 index 5a3125fe..00000000 --- a/test/proto/ext_collision.pb.rb +++ /dev/null @@ -1,24 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# message ExtCollisionTest { -# extensions 1 to 10; -# } -# -# message ExtCollisionTest { -# optional string a = 1; -# optional string b = 1; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -class ExtCollisionTest < ::Protobuf::Message - defined_in __FILE__ - extensions 1..10 -end -class ExtCollisionTest < ::Protobuf::Message - defined_in __FILE__ - optional :string, :a, 1 - optional :string, :b, 1 -end diff --git a/test/proto/ext_collision.proto b/test/proto/ext_collision.proto deleted file mode 100644 index 28de7105..00000000 --- a/test/proto/ext_collision.proto +++ /dev/null @@ -1,8 +0,0 @@ -message ExtCollisionTest { - extensions 1 to 10; -} - -message ExtCollisionTest { - optional string a = 1; - optional string b = 1; -} diff --git a/test/proto/ext_range.pb.rb b/test/proto/ext_range.pb.rb deleted file mode 100644 index 1a70161c..00000000 --- a/test/proto/ext_range.pb.rb +++ /dev/null @@ -1,22 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# message ExtRangeTest { -# extensions 1 to 10; -# } -# -# extend ExtRangeTest { -# optional string a = 11; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -class ExtRangeTest < ::Protobuf::Message - defined_in __FILE__ - extensions 1..10 -end -class ExtRangeTest < ::Protobuf::Message - defined_in __FILE__ - optional :string, :a, 11, :extension => true -end diff --git a/test/proto/ext_range.proto b/test/proto/ext_range.proto deleted file mode 100644 index ff1f9b72..00000000 --- a/test/proto/ext_range.proto +++ /dev/null @@ -1,7 +0,0 @@ -message ExtRangeTest { - extensions 1 to 10; -} - -extend ExtRangeTest { - optional string a = 11; -} diff --git a/test/proto/float_default.proto b/test/proto/float_default.proto deleted file mode 100644 index f48d3d63..00000000 --- a/test/proto/float_default.proto +++ /dev/null @@ -1,10 +0,0 @@ -message M { - optional float f = 1 [default = 4.2]; - optional float g = 2 [default = -4.2]; - optional float h = 3 [default = 4352]; - optional float i = 4 [default = 23145.2 ]; - optional float j = 5 [default = -5 ]; - optional float k = 6 [default = +23 ]; - optional float l = 7 [default = +23.42 ]; -} - diff --git a/test/proto/lowercase.pb.rb b/test/proto/lowercase.pb.rb deleted file mode 100644 index 3ebfbf51..00000000 --- a/test/proto/lowercase.pb.rb +++ /dev/null @@ -1,30 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# package test.lowercase; -# -# message foo { -# message bar { -# } -# } -# message baaz { -# required foo.bar x = 1; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Test - module Lowercase - class Foo < ::Protobuf::Message - defined_in __FILE__ - class Bar < ::Protobuf::Message - defined_in __FILE__ - end - end - class Baaz < ::Protobuf::Message - defined_in __FILE__ - required :'foo::bar', :x, 1 - end - end -end \ No newline at end of file diff --git a/test/proto/lowercase.proto b/test/proto/lowercase.proto deleted file mode 100644 index ab52be42..00000000 --- a/test/proto/lowercase.proto +++ /dev/null @@ -1,9 +0,0 @@ -package test.lowercase; - -message foo { - message bar { - } -} -message baaz { - required foo.bar x = 1; -} diff --git a/test/proto/merge.pb.rb b/test/proto/merge.pb.rb deleted file mode 100644 index e31af31c..00000000 --- a/test/proto/merge.pb.rb +++ /dev/null @@ -1,39 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# package test; -# -# message MergeMessage { -# message InnerMessage1 { -# required string name = 1; -# } -# message InnerMessage2 { -# required string name = 1; -# repeated InnerMessage1 repeate_message = 2; -# } -# -# required string name = 1; -# repeated InnerMessage1 repeate_message = 2; -# required InnerMessage2 require_message = 3; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Test - class MergeMessage < ::Protobuf::Message - defined_in __FILE__ - class InnerMessage1 < ::Protobuf::Message - defined_in __FILE__ - required :string, :name, 1 - end - class InnerMessage2 < ::Protobuf::Message - defined_in __FILE__ - required :string, :name, 1 - repeated :InnerMessage1, :repeate_message, 2 - end - required :string, :name, 1 - repeated :InnerMessage1, :repeate_message, 2 - required :InnerMessage2, :require_message, 3 - end -end \ No newline at end of file diff --git a/test/proto/merge.proto b/test/proto/merge.proto deleted file mode 100644 index 45a0bbdd..00000000 --- a/test/proto/merge.proto +++ /dev/null @@ -1,15 +0,0 @@ -package test; - -message MergeMessage { - message InnerMessage1 { - required string name = 1; - } - message InnerMessage2 { - required string name = 1; - repeated InnerMessage1 repeate_message = 2; - } - - required string name = 1; - repeated InnerMessage1 repeate_message = 2; - required InnerMessage2 require_message = 3; -} diff --git a/test/proto/nested.pb.rb b/test/proto/nested.pb.rb deleted file mode 100644 index afbf41bd..00000000 --- a/test/proto/nested.pb.rb +++ /dev/null @@ -1,30 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# package test.nested; -# -# message Foo { -# message Bar { -# } -# } -# message Baaz { -# optional Foo.Bar x = 1; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Test - module Nested - class Foo < ::Protobuf::Message - defined_in __FILE__ - class Bar < ::Protobuf::Message - defined_in __FILE__ - end - end - class Baaz < ::Protobuf::Message - defined_in __FILE__ - optional :'Foo::Bar', :x, 1 - end - end -end \ No newline at end of file diff --git a/test/proto/nested.proto b/test/proto/nested.proto deleted file mode 100644 index 14974480..00000000 --- a/test/proto/nested.proto +++ /dev/null @@ -1,9 +0,0 @@ -package test.nested; - -message Foo { - message Bar { - } -} -message Baaz { - optional Foo.Bar x = 1; -} diff --git a/test/proto/optional_field.pb.rb b/test/proto/optional_field.pb.rb deleted file mode 100644 index 8f57e636..00000000 --- a/test/proto/optional_field.pb.rb +++ /dev/null @@ -1,35 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# package test.optional_field; -# -# message Message { -# enum Enum { -# A = 1; -# B = 2; -# } -# optional uint32 number = 1 [default = 20]; -# optional string text = 2 [default = "default string"]; -# optional Enum enum = 3 [default = B]; -# optional int32 signed = 4 [default = -100]; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Test - module OptionalField - class Message < ::Protobuf::Message - defined_in __FILE__ - class Enum < ::Protobuf::Enum - defined_in __FILE__ - define :A, 1 - define :B, 2 - end - optional :uint32, :number, 1, :default => 20 - optional :string, :text, 2, :default => "default string" - optional :Enum, :enum, 3, :default => :B - optional :int32, :signed, 4, :default => -100 - end - end -end \ No newline at end of file diff --git a/test/proto/optional_field.proto b/test/proto/optional_field.proto deleted file mode 100644 index bb4f45e0..00000000 --- a/test/proto/optional_field.proto +++ /dev/null @@ -1,12 +0,0 @@ -package test.optional_field; - -message Message { - enum Enum { - A = 1; - B = 2; - } - optional uint32 number = 1 [default = 20]; - optional string text = 2 [default = "default string"]; - optional Enum enum = 3 [default = B]; - optional int32 signed = 4 [default = -100]; -} diff --git a/test/proto/packed.pb.rb b/test/proto/packed.pb.rb deleted file mode 100644 index 1c58b2e3..00000000 --- a/test/proto/packed.pb.rb +++ /dev/null @@ -1,22 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# package test.packed_field; -# -# message Message { -# repeated int32 a = 1 [packed = true]; -# repeated fixed32 b = 2 [packed = true]; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Test - module PackedField - class Message < ::Protobuf::Message - defined_in __FILE__ - repeated :int32, :a, 1, :packed => true - repeated :fixed32, :b, 2, :packed => true - end - end -end \ No newline at end of file diff --git a/test/proto/packed.proto b/test/proto/packed.proto deleted file mode 100644 index 499bfa96..00000000 --- a/test/proto/packed.proto +++ /dev/null @@ -1,6 +0,0 @@ -package test.packed_field; - -message Message { - repeated int32 a = 1 [packed = true]; - repeated fixed32 b = 2 [packed = true]; -} diff --git a/test/proto/rpc.proto b/test/proto/rpc.proto deleted file mode 100644 index 5f4f7f48..00000000 --- a/test/proto/rpc.proto +++ /dev/null @@ -1,6 +0,0 @@ -package tutorial; - -service AddressBookService { - rpc Search (Person) returns (AddressBook); - rpc Add (Person) returns (Person); -} diff --git a/test/proto/types.pb.rb b/test/proto/types.pb.rb deleted file mode 100644 index f01d30d0..00000000 --- a/test/proto/types.pb.rb +++ /dev/null @@ -1,84 +0,0 @@ -### Generated by rprotoc. DO NOT EDIT! -### -# package test.types; -# -# message TestTypes { -# required double type1 = 1; -# required float type2 = 2; -# required int32 type3 = 3; -# required int64 type4 = 4; -# required uint32 type5 = 5; -# required uint64 type6 = 6; -# required sint32 type7 = 7; -# required sint64 type8 = 8; -# required fixed32 type9 = 9; -# required fixed64 type10 = 10; -# required bool type11 = 11; -# required string type12 = 12; -# required bytes type13 = 13; -# required sfixed32 type14 = 14; -# required sfixed64 type15 = 15; -# } -# -# message RepeatedTypes { -# repeated double type1 = 1; -# repeated float type2 = 2; -# repeated int32 type3 = 3; -# repeated int64 type4 = 4; -# repeated uint32 type5 = 5; -# repeated uint64 type6 = 6; -# repeated sint32 type7 = 7; -# repeated sint64 type8 = 8; -# repeated fixed32 type9 = 9; -# repeated fixed64 type10 = 10; -# repeated bool type11 = 11; -# repeated string type12 = 12; -# repeated bytes type13 = 13; -# repeated sfixed32 type14 = 14; -# repeated sfixed64 type15 = 15; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Test - module Types - class TestTypes < ::Protobuf::Message - defined_in __FILE__ - required :double, :type1, 1 - required :float, :type2, 2 - required :int32, :type3, 3 - required :int64, :type4, 4 - required :uint32, :type5, 5 - required :uint64, :type6, 6 - required :sint32, :type7, 7 - required :sint64, :type8, 8 - required :fixed32, :type9, 9 - required :fixed64, :type10, 10 - required :bool, :type11, 11 - required :string, :type12, 12 - required :bytes, :type13, 13 - required :sfixed32, :type14, 14 - required :sfixed64, :type15, 15 - end - class RepeatedTypes < ::Protobuf::Message - defined_in __FILE__ - repeated :double, :type1, 1 - repeated :float, :type2, 2 - repeated :int32, :type3, 3 - repeated :int64, :type4, 4 - repeated :uint32, :type5, 5 - repeated :uint64, :type6, 6 - repeated :sint32, :type7, 7 - repeated :sint64, :type8, 8 - repeated :fixed32, :type9, 9 - repeated :fixed64, :type10, 10 - repeated :bool, :type11, 11 - repeated :string, :type12, 12 - repeated :bytes, :type13, 13 - repeated :sfixed32, :type14, 14 - repeated :sfixed64, :type15, 15 - end - end -end \ No newline at end of file diff --git a/test/proto/types.proto b/test/proto/types.proto deleted file mode 100644 index 984bc5e9..00000000 --- a/test/proto/types.proto +++ /dev/null @@ -1,37 +0,0 @@ -package test.types; - -message TestTypes { - required double type1 = 1; - required float type2 = 2; - required int32 type3 = 3; - required int64 type4 = 4; - required uint32 type5 = 5; - required uint64 type6 = 6; - required sint32 type7 = 7; - required sint64 type8 = 8; - required fixed32 type9 = 9; - required fixed64 type10 = 10; - required bool type11 = 11; - required string type12 = 12; - required bytes type13 = 13; - required sfixed32 type14 = 14; - required sfixed64 type15 = 15; -} - -message RepeatedTypes { - repeated double type1 = 1; - repeated float type2 = 2; - repeated int32 type3 = 3; - repeated int64 type4 = 4; - repeated uint32 type5 = 5; - repeated uint64 type6 = 6; - repeated sint32 type7 = 7; - repeated sint64 type8 = 8; - repeated fixed32 type9 = 9; - repeated fixed64 type10 = 10; - repeated bool type11 = 11; - repeated string type12 = 12; - repeated bytes type13 = 13; - repeated sfixed32 type14 = 14; - repeated sfixed64 type15 = 15; -} diff --git a/test/test_addressbook.rb b/test/test_addressbook.rb deleted file mode 100644 index cc04e5f4..00000000 --- a/test/test_addressbook.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'test/unit' -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'test/proto/addressbook.pb' - -class AddressbookTest < Test::Unit::TestCase - def test_enum - phone_number = Tutorial::Person::PhoneNumber.new - assert_equal(Tutorial::Person::PhoneType::HOME, phone_number.type) - phone_number.type = Tutorial::Person::PhoneType::MOBILE - assert_equal(0, phone_number.type) - phone_number.type = Tutorial::Person::PhoneType::HOME - assert_equal(1, phone_number.type) - phone_number.type = Tutorial::Person::PhoneType::WORK - assert_equal(2, phone_number.type) - assert_raise(TypeError) do - phone_number.type = 3 - end - end - - def test_symbol_enum - phone_number = Tutorial::Person::PhoneNumber.new - assert_equal(Tutorial::Person::PhoneType::HOME, phone_number.type) - phone_number.type = :MOBILE - assert_equal(0, phone_number.type) - phone_number.type = :HOME - assert_equal(1, phone_number.type) - phone_number.type = :WORK - assert_equal(2, phone_number.type) - assert_raise(TypeError) do - phone_number.type = :UNKNOWN - end - end - - def test_initial_value - person = Tutorial::Person.new - assert_nil(person.name) - assert_nil(person.id) - assert_equal([], person.phone) - assert_equal('', person.email) - end - - def test_repeatable - address_book = Tutorial::AddressBook.new - assert_equal([], address_book.person) - assert_instance_of(Protobuf::Field::FieldArray, address_book.person) - address_book.person << Tutorial::Person.new - assert_equal(1, address_book.person.size) - assert_raise(TypeError) do - address_book.person << 1 - end - assert_equal(1, address_book.person.size) - address_book.person << Tutorial::Person.new - assert_equal(2, address_book.person.size) - end -end diff --git a/test/test_compiler.rb b/test/test_compiler.rb deleted file mode 100644 index 055c18f6..00000000 --- a/test/test_compiler.rb +++ /dev/null @@ -1,325 +0,0 @@ -require 'test/unit' -require 'protobuf/compiler/compiler' - -class CompilerTest < Test::Unit::TestCase - # Issue 12: Parse error on float default value - def test_compile_float_default - assert_compile_proto(<<-EOS, 'test/proto/float_default.proto') -### Generated by rprotoc. DO NOT EDIT! -### -# message M { -# optional float f = 1 [default = 4.2]; -# optional float g = 2 [default = -4.2]; -# optional float h = 3 [default = 4352]; -# optional float i = 4 [default = 23145.2 ]; -# optional float j = 5 [default = -5 ]; -# optional float k = 6 [default = +23 ]; -# optional float l = 7 [default = +23.42 ]; -# } -# - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -class M < ::Protobuf::Message - defined_in __FILE__ - optional :float, :f, 1, :default => 4.2 - optional :float, :g, 2, :default => -4.2 - optional :float, :h, 3, :default => 4352 - optional :float, :i, 4, :default => 23145.2 - optional :float, :j, 5, :default => -5 - optional :float, :k, 6, :default => 23 - optional :float, :l, 7, :default => 23.42 -end - EOS - end - - def test_create_message - assert_compile_proto(<<-EOS, 'test/proto/addressbook.proto') -### Generated by rprotoc. DO NOT EDIT! -### -# package tutorial; -# -# message Person { -# required string name = 1; -# required int32 id = 2; -# optional string email = 3; -# -# enum PhoneType { -# MOBILE = 0; -# HOME = 1; -# WORK = 2; -# } -# -# message PhoneNumber { -# required string number = 1; -# optional PhoneType type = 2 [default = HOME]; -# } -# -# repeated PhoneNumber phone = 4; -# optional uint32 age = 5 [default = 20]; -# -# extensions 100 to 200; -# } -# -# /* -# extend Person { -# optional int32 age = 100; -# } -# */ -# -# message AddressBook { -# repeated Person person = 1; -# } -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Tutorial - - class Person < ::Protobuf::Message - defined_in __FILE__ - required :string, :name, 1 - required :int32, :id, 2 - optional :string, :email, 3 - - class PhoneType < ::Protobuf::Enum - defined_in __FILE__ - define :MOBILE, 0 - define :HOME, 1 - define :WORK, 2 - end - - class PhoneNumber < ::Protobuf::Message - defined_in __FILE__ - required :string, :number, 1 - optional :PhoneType, :type, 2, :default => :HOME - end - - repeated :PhoneNumber, :phone, 4 - optional :uint32, :age, 5, :default => 20 - - extensions 100..200 - end - - class AddressBook < ::Protobuf::Message - defined_in __FILE__ - repeated :Person, :person, 1 - end -end - EOS - end - - def test_create_nested_message - assert_compile_proto(<<-EOS, 'test/proto/nested.proto') -### Generated by rprotoc. DO NOT EDIT! -### -# package test.nested; -# -# message Foo { -# message Bar { -# } -# } -# message Baaz { -# optional Foo.Bar x = 1; -# } - -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'protobuf/message/extend' - -module Test - module Nested - class Foo < ::Protobuf::Message - defined_in __FILE__ - class Bar < ::Protobuf::Message - defined_in __FILE__ - end - end - class Baaz < ::Protobuf::Message - defined_in __FILE__ - optional :'Foo::Bar', :x, 1 - end - end -end - EOS - end - - def test_nested_message - file_contents = Protobuf::Compiler.new.create_message('test/proto/nested.proto', '.', '.', false) - assert_nothing_raised {Object.class_eval file_contents} - assert_raise(TypeError) {Test::Nested::Baaz.new.x = 1} - assert_nothing_raised {Test::Nested::Baaz.new.x = Test::Nested::Foo::Bar.new} - end - - def test_create_rpc - file_contents = Protobuf::Compiler.new.create_rpc('test/proto/rpc.proto', '.', 'test/proto', false) - - assert_source(<<-EOS, file_contents['test/proto/address_book_service.rb']) -require 'protobuf/rpc/server' -require 'protobuf/rpc/handler' -require 'test/proto/rpc.pb' - -class Tutorial::SearchHandler < Protobuf::Rpc::Handler - request Tutorial::Person - response Tutorial::AddressBook - - def self.process_request(request, response) - # TODO: edit this method - end -end - -class Tutorial::AddHandler < Protobuf::Rpc::Handler - request Tutorial::Person - response Tutorial::Person - - def self.process_request(request, response) - # TODO: edit this method - end -end - -class Tutorial::AddressBookService < Protobuf::Rpc::Server - def setup_handlers - @handlers = { - :search => Tutorial::SearchHandler, - :add => Tutorial::AddHandler, - } - end -end - EOS - - assert_source(<<-EOS, file_contents['test/proto/start_address_book_service']) -#!/usr/bin/env ruby -require 'address_book_service' - -Tutorial::AddressBookService.new(:Port => 9999).start - EOS - - assert_source(<<-EOS, file_contents['test/proto/client_search.rb']) -#!/usr/bin/env ruby -require 'protobuf/rpc/client' -require 'test/proto/rpc.pb' - -# build request -request = Tutorial::Person.new -# TODO: setup a request -raise StandardError, 'setup a request' - -# create blunk response -response = Tutorial::AddressBook.new - -# execute rpc -Protobuf::Rpc::Client.new('localhost', 9999).call :search, request, response - -# show response -puts response - EOS - - assert_source(<<-EOS, file_contents['test/proto/client_add.rb']) -#!/usr/bin/env ruby -require 'protobuf/rpc/client' -require 'test/proto/rpc.pb' - -# build request -request = Tutorial::Person.new -# TODO: setup a request -raise StandardError, 'setup a request' - -# create blunk response -response = Tutorial::Person.new - -# execute rpc -Protobuf::Rpc::Client.new('localhost', 9999).call :add, request, response - -# show response -puts response - EOS - end - - def test_create_descriptor - proto_path = 'test/proto/addressbook.proto' - visitor = Protobuf::Visitor::CreateDescriptorVisitor.new(proto_path) - File.open(proto_path) do |file| - visitor.visit Protobuf::ProtoParser.new.parse(file) - end - file_descriptor = visitor.file_descriptor - assert_equal(proto_path, file_descriptor.name) - assert_equal('tutorial', file_descriptor.package) - - person_descriptor = file_descriptor.message_type[0] - assert_equal('Person', person_descriptor.name) - assert_equal([:name, :id, :email, :phone, :age].size, person_descriptor.field.size) - - name_field_descriptor = person_descriptor.field.find {|d| d.name == 'name'} - assert_equal(1, name_field_descriptor.number) - assert_equal(Google::Protobuf::FieldDescriptorProto::Type::TYPE_STRING, name_field_descriptor.type) - assert_equal(Google::Protobuf::FieldDescriptorProto::Label::LABEL_REQUIRED, name_field_descriptor.label) - assert_equal('string', name_field_descriptor.type_name) - - phone_field_descriptor = person_descriptor.field.find {|d| d.name == 'phone'} - assert_equal(4, phone_field_descriptor.number) - assert_equal(0, phone_field_descriptor.type) #TODO: is this right? - assert_equal(Google::Protobuf::FieldDescriptorProto::Label::LABEL_REPEATED, phone_field_descriptor.label) - assert_equal('PhoneNumber', phone_field_descriptor.type_name) - - age_field_descriptor = person_descriptor.field.find {|d| d.name == 'age'} - assert_equal(5, age_field_descriptor.number) - assert_equal(Google::Protobuf::FieldDescriptorProto::Type::TYPE_UINT32, age_field_descriptor.type) - assert_equal(Google::Protobuf::FieldDescriptorProto::Label::LABEL_OPTIONAL, age_field_descriptor.label) - assert_equal('uint32', age_field_descriptor.type_name) - assert_equal('20', age_field_descriptor.default_value) - - phone_type_descriptor = person_descriptor.enum_type.first - assert_equal('PhoneType', phone_type_descriptor.name) - assert_equal(3, phone_type_descriptor.value.size) - - phone_type_home_descriptor = phone_type_descriptor.value.find {|d| d.name == 'HOME'} - assert_equal('HOME', phone_type_home_descriptor.name) - assert_equal(1, phone_type_home_descriptor.number) - - extensions_descriptor = person_descriptor.extension_range.first - assert_equal(100, extensions_descriptor.start) - assert_equal(200, extensions_descriptor.end) - - phone_number_descriptor = person_descriptor.nested_type.first - assert_equal('PhoneNumber', phone_number_descriptor.name) - assert_equal([:number, :type].size, phone_number_descriptor.field.size) - - #TODO: test extend - #extend_person_descriptor = ?? - #assert_equal extend_person_descriptor - - addressbook_descriptor = file_descriptor.message_type[1] - assert_equal('AddressBook', addressbook_descriptor.name) - end - - def test_collision - assert_raise(Protobuf::TagCollisionError) do require 'test/proto/collision.pb' end - assert_raise(Protobuf::TagCollisionError) do - Protobuf::Compiler.new.create_message('test/proto/collision.proto', '.', '.', false) - end - end - - def test_ext_collision - assert_raise(Protobuf::TagCollisionError) do require 'test/proto/ext_collision.pb' end - assert_raise(Protobuf::TagCollisionError) do - Protobuf::Compiler.new.create_message('test/proto/ext_collision.proto', '.', '.', false) - end - end - - def test_ext_range - assert_raise(RangeError) do require 'test/proto/ext_range.pb' end - assert_raise(RangeError) do - Protobuf::Compiler.new.create_message('test/proto/ext_range.proto', '.', '.', false) - end - end - - def assert_compile_proto(ideal, filename) - assert_equal(ideal.gsub(/^\s*\n/, '').strip, Protobuf::Compiler.new.create_message(filename, '.', '.', false).gsub(/^\s*\n/, '').strip) - end - - def assert_source(ideal, real) - assert_equal(ideal.strip.gsub(/^\s*\n/, '').gsub(/\s+\n/, "\n"), real.strip.gsub(/^\s*\n/, '').gsub(/\s+\n/, "\n")) - end -end diff --git a/test/test_descriptor.rb b/test/test_descriptor.rb deleted file mode 100644 index 519e48b7..00000000 --- a/test/test_descriptor.rb +++ /dev/null @@ -1,122 +0,0 @@ -require 'test/unit' -require 'test/proto/addressbook.pb' -require 'protobuf/descriptor/descriptor_builder' -require 'protobuf/descriptor/descriptor_proto' - -class DescriptorTest < Test::Unit::TestCase - include Google::Protobuf - def test_build - tutorial_proto = FileDescriptorProto.new - tutorial_proto.package = 'Build::Tutorial' - - person_proto = DescriptorProto.new - tutorial_proto.message_type << person_proto - person_proto.name = 'Person' - - person_name_proto = FieldDescriptorProto.new - person_proto.field << person_name_proto - person_name_proto.label = FieldDescriptorProto::Label::LABEL_REQUIRED - person_name_proto.type = FieldDescriptorProto::Type::TYPE_STRING - person_name_proto.name = 'name' - person_name_proto.number = 1 - - person_id_proto = FieldDescriptorProto.new - person_proto.field << person_id_proto - person_id_proto.label = FieldDescriptorProto::Label::LABEL_REQUIRED - person_id_proto.type = FieldDescriptorProto::Type::TYPE_INT32 - person_id_proto.name = 'id' - person_id_proto.number = 2 - - person_email_proto = FieldDescriptorProto.new - person_proto.field << person_email_proto - person_email_proto.label = FieldDescriptorProto::Label::LABEL_OPTIONAL - person_email_proto.type = FieldDescriptorProto::Type::TYPE_STRING - person_email_proto.name = 'email' - person_email_proto.number = 3 - - person_phone_type_proto = EnumDescriptorProto.new - person_proto.enum_type << person_phone_type_proto - person_phone_type_proto.name = 'PhoneType' - - person_phone_type_mobile_proto = EnumValueDescriptorProto.new - person_phone_type_proto.value << person_phone_type_mobile_proto - person_phone_type_mobile_proto.name = 'MOBILE' - person_phone_type_mobile_proto.number = 0 - - person_phone_type_home_proto = EnumValueDescriptorProto.new - person_phone_type_proto.value << person_phone_type_home_proto - person_phone_type_home_proto.name = 'HOME' - person_phone_type_home_proto.number = 1 - - person_phone_type_work_proto = EnumValueDescriptorProto.new - person_phone_type_proto.value << person_phone_type_work_proto - person_phone_type_work_proto.name = 'WORK' - person_phone_type_work_proto.number = 2 - - person_phone_number_proto = DescriptorProto.new - person_proto.nested_type << person_phone_number_proto - person_phone_number_proto.name = 'PhoneNumber' - - person_phone_number_number_proto = FieldDescriptorProto.new - person_phone_number_proto.field << person_phone_number_number_proto - person_phone_number_number_proto.label = FieldDescriptorProto::Label::LABEL_REQUIRED - person_phone_number_number_proto.type = FieldDescriptorProto::Type::TYPE_STRING - person_phone_number_number_proto.name = 'number' - person_phone_number_number_proto.number = 1 - - person_phone_number_type_proto = FieldDescriptorProto.new - person_phone_number_proto.field << person_phone_number_type_proto - person_phone_number_type_proto.label = FieldDescriptorProto::Label::LABEL_OPTIONAL - person_phone_number_type_proto.type = FieldDescriptorProto::Type::TYPE_ENUM - person_phone_number_type_proto.type_name = 'PhoneType' - person_phone_number_type_proto.name = 'type' - person_phone_number_type_proto.number = 2 - person_phone_number_type_proto.default_value = 'HOME' - - person_phone_phone_number_proto = FieldDescriptorProto.new - person_proto.field << person_phone_phone_number_proto - person_phone_phone_number_proto.label = FieldDescriptorProto::Label::LABEL_REPEATED - person_phone_phone_number_proto.type = FieldDescriptorProto::Type::TYPE_MESSAGE - person_phone_phone_number_proto.type_name = 'PhoneNumber' - person_phone_phone_number_proto.name = 'phone' - person_phone_phone_number_proto.number = 4 - - address_book_proto = DescriptorProto.new - tutorial_proto.message_type << address_book_proto - address_book_proto.name = 'AddressBook' - - address_book_person_proto = FieldDescriptorProto.new - address_book_proto.field << address_book_person_proto - address_book_person_proto.label = FieldDescriptorProto::Label::LABEL_REPEATED - address_book_person_proto.type = FieldDescriptorProto::Type::TYPE_MESSAGE - address_book_person_proto.type_name = 'Person' - address_book_person_proto.name = 'person' - address_book_person_proto.number = 1 - - Protobuf::Descriptor::DescriptorBuilder.build(tutorial_proto) - - assert_nothing_raised {Build::Tutorial::Person} - assert_nothing_raised {Build::Tutorial::Person.new} - assert_equal(['email', 'id', 'name', 'phone'], - Build::Tutorial::Person.fields.map{|tag, field| field.name}.sort) - - assert_nothing_raised {Build::Tutorial::Person::PhoneNumber} - assert_nothing_raised {Build::Tutorial::Person::PhoneNumber.new} - assert_equal(['number', 'type'], - Build::Tutorial::Person::PhoneNumber.fields.map{|tag, field| field.name}.sort) - - assert_nothing_raised {Build::Tutorial::Person::PhoneType} - assert_equal(0, Build::Tutorial::Person::PhoneType::MOBILE) - assert_equal(1, Build::Tutorial::Person::PhoneType::HOME) - assert_equal(2, Build::Tutorial::Person::PhoneType::WORK) - - assert_nothing_raised {Build::Tutorial::AddressBook} - end - - def test_unbuild - proto = Protobuf::Descriptor::FileDescriptor.unbuild(Tutorial::Person) - proto.serialize_to_file('person.bin') - puts - puts "run `test/check_unbuild.rb'" - end -end diff --git a/test/test_enum_value.rb b/test/test_enum_value.rb deleted file mode 100644 index 2af2e80f..00000000 --- a/test/test_enum_value.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'test/unit' -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'test/proto/addressbook.pb' -require 'test/proto/addressbook_base.pb' - -class EnumValueTest < Test::Unit::TestCase - def test_enum_value - e = Protobuf::EnumValue.new(Object, :name, 100) - assert_kind_of(Protobuf::EnumValue, e) - assert_equal(:name, e.name) - assert_equal(100, e.value) - assert_equal('name', e.to_s) - assert_equal('name', "#{e}") - assert(e == 100) - assert(100 == e) - assert(101, e + 1) - assert(101, 1 + e) - end - - def test_enum_field_and_enum_value - phone_number = Tutorial::Person::PhoneNumber.new - - assert_kind_of(Protobuf::EnumValue, phone_number.type) - assert_equal(Tutorial::Person::PhoneType::HOME, phone_number.type) - - phone_number.type = 1 - assert_kind_of(Protobuf::EnumValue, phone_number.type) - assert_equal(Tutorial::Person::PhoneType::HOME, phone_number.type) - assert_equal(1, phone_number.type) - - phone_number.type = :HOME - assert_kind_of(Protobuf::EnumValue, phone_number.type) - assert_equal(Tutorial::Person::PhoneType::HOME, phone_number.type) - assert_equal(1, phone_number.type) - - assert_raise(TypeError) do - phone_number.type = TutorialExt::Person::PhoneType::HOME - end - end -end diff --git a/test/test_extension.rb b/test/test_extension.rb deleted file mode 100644 index 6384db7f..00000000 --- a/test/test_extension.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'test/unit' -require 'test/proto/addressbook_ext.pb' - -class ExtensionTest < Test::Unit::TestCase - def test_accessor - assert(TutorialExt::Person.extension_fields.to_a.map{|t, f| f.name}.include?(:age)) - person = TutorialExt::Person.new - assert_nothing_raised {person.age = 100} - assert_equal(100, person.age) - end - - def test_serialize - # serialize to string - person = TutorialExt::Person.new - person.id = 1234 - person.age = 70 - person.name = 'John Doe' - person.email = 'jdoe@example.com' - phone = TutorialExt::Person::PhoneNumber.new - phone.number = '555-4321' - phone.type = TutorialExt::Person::PhoneType::HOME - person.phone << phone - serialized_string = person.serialize_to_string - - # parse the serialized string - person2 = TutorialExt::Person.new - person2.parse_from_string serialized_string - assert_equal(1234, person2.id) - assert_equal(70, person2.age) - assert_equal('John Doe', person2.name) - assert_equal('jdoe@example.com', person2.email) - assert_equal(1, person2.phone.size) - assert_equal('555-4321', person2.phone[0].number) - assert_equal(TutorialExt::Person::PhoneType::HOME, person2.phone[0].type) - end -end diff --git a/test/test_lowercase.rb b/test/test_lowercase.rb deleted file mode 100644 index 7e7f7689..00000000 --- a/test/test_lowercase.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'test/unit' -require 'test/proto/lowercase.pb' - -class LowercaseTest < Test::Unit::TestCase - def test_lowercase - message = nil - assert_nothing_raised { message = Test::Lowercase::Baaz.new } - assert_nothing_raised { message.x = Test::Lowercase::Foo::Bar.new } - assert_equal(Test::Lowercase::Foo::Bar, message.get_field_by_name(:x).type) - end -end diff --git a/test/test_message.rb b/test/test_message.rb deleted file mode 100644 index bb0da7b1..00000000 --- a/test/test_message.rb +++ /dev/null @@ -1,128 +0,0 @@ -require 'protobuf/message/message' -require 'test/proto/addressbook.pb' -require 'test/proto/merge.pb' -require 'test/unit' - -# It should not conflict with Test::InnerMessage1 which is included in merge.proto -class InnerMessage1; end - -class MessageTest < Test::Unit::TestCase - def test_equality - person1 = Tutorial::Person.new :name => 'ando' - person2 = Tutorial::Person.new :name => 'ando' - person3 = Tutorial::Person.new :name => 'Ando' - assert(person1 == person2) - assert(person1 != person3) - assert(person1 != 'ando') - end - - def test_bracketed_access - person = Tutorial::Person.new - name_tag = 1 - person[name_tag] = 'Ichiro' - assert_equal('Ichiro', person.name) - assert_equal('Ichiro', person[name_tag]) - - person[:id] = 100 - assert_equal(100, person.id) - person['id'] = 200 - assert_equal(200, person.id) - assert_equal(200, person[:id]) - assert_equal(200, person['id']) - end - - def test_initialize_with_hash - person = Tutorial::Person.new(:name => 'Jiro', :id => 300, :email => 'jiro@ema.il') - assert_equal('Jiro', person.name) - assert_equal(300, person.id) - assert_equal('jiro@ema.il', person.email) - - # initialize with array of hash - person = Tutorial::Person.new(:phone => [{:number => 'phone1'}, {:number => 'phone2'}]) - assert_equal('phone1', person.phone[0].number) - assert_equal('phone2', person.phone[1].number) - - # initalize with hash in hash - message = Test::MergeMessage.new(:require_message => { :name => 'name1', :repeate_message => [{:name => 'name2'}] }) - assert_equal('name1', message.require_message.name) - assert_equal('name2', message.require_message.repeate_message[0].name) - - message.require_message = { :name => 'name21' } - message.require_message.repeate_message = [ {:name => 'name22'} ] - assert_equal('name21', message.require_message.name) - assert_equal('name22', message.require_message.repeate_message[0].name) - assert_equal(1, message.require_message.repeate_message.size) - end - - def test_defined_filenames - assert(Tutorial::Person.defined_filenames) - assert_equal(1, Tutorial::Person.defined_filenames.size) - assert_match(%r{/.*/proto/addressbook\.pb\.rb}, Tutorial::Person.defined_filenames.first) - end - - def test_proto_filenames - assert(Tutorial::Person.proto_filenames) - assert_equal(1, Tutorial::Person.proto_filenames.size) - assert_equal('test/proto/addressbook.proto', Tutorial::Person.proto_filenames.first) - end - - def test_proto_contents - assert_equal(<<-EOS.strip, Tutorial::Person.proto_contents.values.first.strip) -package tutorial; - -message Person { - required string name = 1; - required int32 id = 2; - optional string email = 3; - - enum PhoneType { - MOBILE = 0; - HOME = 1; - WORK = 2; - } - - message PhoneNumber { - required string number = 1; - optional PhoneType type = 2 [default = HOME]; - } - - repeated PhoneNumber phone = 4; - optional uint32 age = 5 [default = 20]; - - extensions 100 to 200; -} - -/* -extend Person { - optional int32 age = 100; -} -*/ - -message AddressBook { - repeated Person person = 1; -} - EOS - end - - def test_merge_field - inner_message1_2 = Test::MergeMessage::InnerMessage2.new(:name => 'name12') - inner_message1_2.repeate_message << Test::MergeMessage::InnerMessage1.new(:name => 'name121') - message1 = Test::MergeMessage.new(:name => 'name1', :require_message => inner_message1_2) - message1.repeate_message << Test::MergeMessage::InnerMessage1.new(:name => 'name11') - - inner_message2_2 = Test::MergeMessage::InnerMessage2.new(:name => 'name22') - inner_message2_2.repeate_message << Test::MergeMessage::InnerMessage1.new(:name => 'name221') - message2 = Test::MergeMessage.new(:name => 'name2', :require_message => inner_message2_2) - message2.repeate_message << Test::MergeMessage::InnerMessage1.new(:name => 'name21') - - message1.merge_from(message2) - assert_equal('name2', message1.name) - assert_equal(2, message1.repeate_message.size) - assert_equal('name11', message1.repeate_message[0].name) - assert_equal('name21', message1.repeate_message[1].name) - assert_equal('name22', message1.require_message.name) - assert_equal(2, message1.require_message.repeate_message.size) - assert_equal('name121', message1.require_message.repeate_message[0].name) - assert_equal('name221', message1.require_message.repeate_message[1].name) - end -end diff --git a/test/test_optional_field.rb b/test/test_optional_field.rb deleted file mode 100644 index 5b04bb11..00000000 --- a/test/test_optional_field.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'test/unit' -require 'test/proto/optional_field.pb' - -class OptionalFieldTest < Test::Unit::TestCase - def test_accessor - message = Test::OptionalField::Message.new - - # default values - assert(!message.has_field?(:number)) - assert_equal(20, message.number) - - assert(!message.has_field?(:text)) - assert_equal('default string', message.text) - - assert(!message.has_field?(:enum)) - assert_equal(2, message.enum) - - assert(!message.has_field?(:signed)) - assert_equal(-100, message.signed) - - # assign values - assert_nothing_raised { message.number = 100 } - assert(message.has_field?(:number)) - assert_equal(100, message.number) - - assert_nothing_raised { message.text = 'abc' } - assert(message.has_field?(:text)) - assert_equal('abc', message.text) - - assert_nothing_raised { message.enum = Test::OptionalField::Message::Enum::A } - assert(message.has_field?(:enum)) - assert_equal(1, message.enum) - - assert_nothing_raised { message.signed = -20 } - assert(message.has_field?(:signed)) - assert_equal(-20, message.signed) - end - - def test_serialize - message1 = Test::OptionalField::Message.new - message2 = Test::OptionalField::Message.new - - # all fields are empty - serialized_string = message1.to_s - assert(serialized_string.empty?) - message2.parse_from_string(serialized_string) - assert_equal(message1.number, message2.number) - assert_equal(message1.text, message2.text) - assert_equal(message1.enum, message2.enum) - assert_equal(message1.signed, message2.signed) - assert(!message2.has_field?(:number)) - assert(!message2.has_field?(:text)) - assert(!message2.has_field?(:enum)) - assert(!message2.has_field?(:signed)) - - # assign the value whith is equal to default value - message1 = Test::OptionalField::Message.new - message1.number = message1.number - message1.text = message1.text - message1.enum = message1.enum - message1.signed = message1.signed - serialized_string = message1.to_s - assert !serialized_string.empty? - - # set some fields - message1 = Test::OptionalField::Message.new - message1.number = 100 - message1.text = 'new text' - serialized_string = message1.to_s - message2.parse_from_string(serialized_string) - assert_equal(message1.number, message2.number) - assert_equal(message1.text, message2.text) - assert_equal(message1.enum, message2.enum) - assert_equal(message1.signed, message2.signed) - assert( message2.has_field?(:number)) - assert( message2.has_field?(:text)) - assert(!message2.has_field?(:enum)) - assert(!message2.has_field?(:signed)) - end - - def test_merge_optional_fields - src1 = Test::OptionalField::Message.new - src2 = Test::OptionalField::Message.new - dst = Test::OptionalField::Message.new - - src1.number = 100 - src1.text = 'old' - src2.text = 'new' - src2.signed = 20 - - serialized_string = src1.to_s + src2.to_s - dst.parse_from_string(serialized_string) - - assert_equal(100, dst.number) - assert_equal('new', dst.text) - assert_equal(20, dst.signed) - assert dst.has_field?(:number) - assert dst.has_field?(:text) - assert !dst.has_field?(:enum) - assert dst.has_field?(:signed) - end - -end diff --git a/test/test_packed_field.rb b/test/test_packed_field.rb deleted file mode 100644 index 1583cd42..00000000 --- a/test/test_packed_field.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'test/unit' -require 'test/proto/packed.pb' - -class PackedFieldTest < Test::Unit::TestCase - - def test_packed_field - values = [1, 1_000, 10_000, 1_000_000, 1_000_000_000] - - # encode - message = Test::PackedField::Message.new - message.a = values - message.b = values - - expected = '' - serialized_string = nil - assert_nothing_raised { serialized_string = message.serialize_to_string } - - value = values.map {|v| Protobuf::Field::VarintField.encode(v) }.join - expected << Protobuf::Field::VarintField.encode((1 << 3) | 2) # tag=1, wire_type=2 (length-delimited) - expected << Protobuf::Field::VarintField.encode(value.size) - expected << value - - value = values.pack('V*') - expected << Protobuf::Field::VarintField.encode((2 << 3) | 2) # tag=2, wire_type=2 (length-delimited) - expected << Protobuf::Field::VarintField.encode(value.size) - expected << value - - assert_equal(expected, serialized_string) - - # decode - message2 = Test::PackedField::Message.new - assert_nothing_raised { message2.parse_from_string(serialized_string) } - - values.each_with_index do |val, n| - assert_equal(val, message.a[n]) - assert_equal(val, message.b[n]) - end - end - -end diff --git a/test/test_parse.rb b/test/test_parse.rb deleted file mode 100644 index 702ddc26..00000000 --- a/test/test_parse.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'test/unit' -require 'test/proto/addressbook.pb' - -class ParseTest < Test::Unit::TestCase - def test_parse - person = Tutorial::Person.new - person.parse_from_file('test/data/data.bin') - assert_equal(1234, person.id) - assert_equal('John Doe', person.name) - assert_equal('jdoe@example.com', person.email) - assert_equal(1, person.phone.size) - assert_equal('555-4321', person.phone[0].number) - assert_equal(Tutorial::Person::PhoneType::HOME, person.phone[0].type) - end -end diff --git a/test/test_repeated_types.rb b/test/test_repeated_types.rb deleted file mode 100644 index 5e197195..00000000 --- a/test/test_repeated_types.rb +++ /dev/null @@ -1,132 +0,0 @@ -require 'test/unit' -require 'test/proto/types.pb' - -class RepeatedTypesTest < Test::Unit::TestCase - def test_serialize - # empty set - types = Test::Types::RepeatedTypes.new - assert_equal('', types.to_s) - - # has 1 member - types = Test::Types::RepeatedTypes.new - types.type1 << 0.01 - types.type2 << 0.1 - types.type3 << 1 - types.type4 << 10 - types.type5 << 100 - types.type6 << 1000 - types.type7 << -1 - types.type8 << -10 - types.type9 << 10000 - types.type10 << 100000 - types.type11 << false - types.type12 << 'hello all types' - image_bin = File.open('test/data/unk.png', 'r+b'){|f| f.read} - types.type13 << image_bin - types.type14 << -100 - types.type15 << -1000 - - serialized_string = types.serialize_to_string - - types2 = Test::Types::RepeatedTypes.new - types2.parse_from_string serialized_string - assert_in_delta(0.01, types2.type1[0], 0.00001) - assert_in_delta(0.1, types2.type2[0], 0.00001) - assert_equal(1, types2.type3[0]) - assert_equal(10, types2.type4[0]) - assert_equal(100, types2.type5[0]) - assert_equal(1000, types2.type6[0]) - assert_equal(-1, types2.type7[0]) - assert_equal(-10, types2.type8[0]) - assert_equal(10000, types2.type9[0]) - assert_equal(100000, types2.type10[0]) - assert(!types2.type11[0]) - assert_equal('hello all types', types2.type12[0]) - assert_equal(10938, types2.type13[0].size) - assert_equal(image_bin, types2.type13[0]) - assert_equal(-100, types2.type14[0]) - assert_equal(-1000, types2.type15[0]) - - # has 2 members - types.type1 << 1.0/0 # double (Inf) - types.type2 << -1.0/0 # float (-Inf) - types.type3 << -1 # int32 - types.type4 << -10 # int64 - types.type5 << 100 # uint32 - types.type6 << 1000 # uint64 - types.type7 << -1000 # sint32 - types.type8 << -10000 # sint64 - types.type9 << 10000 # fixed32 - types.type10 << 100000 # fixed64 - types.type11 << true - types.type12 << 'hello all types' - image_bin = File.open('test/data/unk.png', 'r+b'){|f| f.read} - types.type13 << image_bin - types.type14 << -2_000_000_000 # sfixed32 - types.type15 << -8_000_000_000_000_000_000 # sfixed64 - - serialized_string = types.serialize_to_string - - types2 = Test::Types::RepeatedTypes.new - types2.parse_from_string serialized_string - assert_in_delta(0.01, types2.type1[0], 0.00001) - assert_in_delta(0.1, types2.type2[0], 0.00001) - assert_equal(1, types2.type3[0]) - assert_equal(10, types2.type4[0]) - assert_equal(100, types2.type5[0]) - assert_equal(1000, types2.type6[0]) - assert_equal(-1, types2.type7[0]) - assert_equal(-10, types2.type8[0]) - assert_equal(10000, types2.type9[0]) - assert_equal(100000, types2.type10[0]) - assert(!types2.type11[0]) - assert_equal('hello all types', types2.type12[0]) - assert_equal(10938, types2.type13[0].size) - assert_equal(image_bin, types2.type13[0]) - assert_equal(-100, types2.type14[0]) - assert_equal(-1000, types2.type15[0]) - - assert_equal(1.0/0.0, types2.type1[1]) - assert_equal(-1.0/0.0, types2.type2[1]) - assert_equal(-1, types2.type3[1]) - assert_equal(-10, types2.type4[1]) - assert_equal(100, types2.type5[1]) - assert_equal(1000, types2.type6[1]) - assert_equal(-1000, types2.type7[1]) - assert_equal(-10000, types2.type8[1]) - assert_equal(10000, types2.type9[1]) - assert_equal(100000, types2.type10[1]) - assert types2.type11[1] - assert_equal('hello all types', types2.type12[1]) - assert_equal(10938, types2.type13[1].size) - assert_equal(image_bin, types2.type13[1]) - assert_equal(-2_000_000_000, types2.type14[1]) - assert_equal(-8_000_000_000_000_000_000, types2.type15[1]) - end - - def test_repeated_types - types = Test::Types::RepeatedTypes.new - # types.type3 is a repeated int32 field. - assert(types.type3.empty?) - types.type3 << 0 - types.type3 << 1 - assert_equal(0, types.type3[0]) - assert_equal(1, types.type3[1]) - assert_equal(2, types.type3.size) - - assert_raise(TypeError) do - types.type3 << 'string' - end - - types.type3[1] = 10 - assert_equal(0, types.type3[0]) - assert_equal(10, types.type3[1]) - assert_equal(2, types.type3.size) - - types.type3.replace([10, 20, 30]) - assert_equal(10, types.type3[0]) - assert_equal(20, types.type3[1]) - assert_equal(30, types.type3[2]) - assert_equal(3, types.type3.size) - end -end diff --git a/test/test_serialize.rb b/test/test_serialize.rb deleted file mode 100644 index 407d3ef7..00000000 --- a/test/test_serialize.rb +++ /dev/null @@ -1,61 +0,0 @@ -# encoding: utf-8 -require 'test/unit' -require 'test/proto/addressbook.pb' - -class SerializeTest < Test::Unit::TestCase - def test_serialize - # serialize to string - person = Tutorial::Person.new - person.id = 1234 - person.name = 'John Doe' - person.email = 'jdoe@example.com' - phone = Tutorial::Person::PhoneNumber.new - phone.number = '555-4321' - phone.type = Tutorial::Person::PhoneType::HOME - person.phone << phone - serialized_string = person.serialize_to_string - - # parse the serialized string - person2 = Tutorial::Person.new - person2.parse_from_string(serialized_string) - assert_equal(1234, person2.id) - assert_equal('John Doe', person2.name) - assert_equal('jdoe@example.com', person2.email) - assert_equal(1, person2.phone.size) - assert_equal('555-4321', person2.phone[0].number) - assert_equal(Tutorial::Person::PhoneType::HOME, person2.phone[0].type) - end - - def test_serialize_utf8string - # serialize to string - person = Tutorial::Person.new - person.id = 1234 - person.name = '山田 太郎' # kanji characters - serialized_string = person.serialize_to_string - - # parse the serialized string - person2 = Tutorial::Person.new - person2.parse_from_string(serialized_string) - assert_equal(1234, person2.id) - assert_equal('山田 太郎', person2.name) - end - - def test_unknown_field - person = Tutorial::Person.new - person.id = 1234 - person.name = 'a b c' - serialized_string = person.serialize_to_string - - # add invalid field - tag = 1000 - wire_type = Protobuf::WireType::VARINT - serialized_string << Protobuf::Field::VarintField.encode((tag << 3) | wire_type) - serialized_string << Protobuf::Field::VarintField.encode(100) - - # decode - person2 = Tutorial::Person.new - person2.parse_from_string(serialized_string) - - assert_equal(person, person2) - end -end diff --git a/test/test_standard_message.rb b/test/test_standard_message.rb deleted file mode 100644 index 9f26d324..00000000 --- a/test/test_standard_message.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'test/unit' -require 'protobuf/message/message' -require 'protobuf/message/enum' -require 'test/proto/addressbook.pb' - -class StandardMessageTest < Test::Unit::TestCase - def test_initialized - person = Tutorial::Person.new - assert(!person.initialized?) - assert_raise(Protobuf::NotInitializedError) { person.to_s } - person.name = 'name' - assert(!person.initialized?) - person.id = 12 - assert( person.initialized?) - - # repeated field - person.phone << Tutorial::Person::PhoneNumber.new - assert(!person.initialized?) - person.phone.last.number = '123-456' - assert( person.initialized?) - end - - def test_clear - person = Tutorial::Person.new - person.name = 'name' - person.id = 1234 - person.email = 'abc@cde.fgh' - person.phone << Tutorial::Person::PhoneNumber.new - person.clear! - - assert_nil(person.name) - assert_nil(person.id) - assert_equal('', person.email) - assert_equal(0, person.phone.size) - - assert(!person.has_field?(:name)) - assert(!person.has_field?(:id)) - assert(!person.has_field?(:email)) - end - - def test_dup - person = Tutorial::Person.new - person.name = 'name' - person.id = 1234 - person.email = 'abc@cde.fgh' - person.phone << Tutorial::Person::PhoneNumber.new - person.phone.last.number = '123-456' - person.phone.last.type = Tutorial::Person::PhoneType::MOBILE - person.phone << Tutorial::Person::PhoneNumber.new - person.phone.last.number = '456-123' - person.phone.last.type = Tutorial::Person::PhoneType::WORK - - person2 = person.dup - assert(person == person2) - assert(!person.eql?(person2)) - assert_equal(person.name, person2.name) - assert_equal(person.id, person2.id) - assert_equal(person.email, person2.email) - assert_equal(person.phone.size, person2.phone.size) - assert(person.phone.first == person2.phone.first) - assert(!person.phone.first.eql?(person2.phone.first)) - assert_equal(person.phone.first.number, person2.phone.first.number) - assert_equal(person.phone.first.type, person2.phone.first.type) - assert(person.phone.last == person2.phone.last) - assert(!person.phone.last.eql?(person2.phone.last)) - assert_equal(person.phone.last.number, person2.phone.last.number) - assert_equal(person.phone.last.type, person2.phone.last.type) - end - - def test_inspect - person = Tutorial::Person.new - person.name = 'name' - person.id = 1234 - person.email = 'abc@cde.fgh' - person.phone << Tutorial::Person::PhoneNumber.new - person.phone.last.number = '123-456' - person.phone.last.type = Tutorial::Person::PhoneType::MOBILE - person.phone << Tutorial::Person::PhoneNumber.new - person.phone.last.number = '456-123' - person.phone.last.type = Tutorial::Person::PhoneType::WORK - - assert_equal <<-EOS, person.inspect -name: "name" -id: 1234 -email: "abc@cde.fgh" -phone { - number: "123-456" - type: MOBILE -} -phone { - number: "456-123" - type: WORK -} - EOS - end -end diff --git a/test/test_types.rb b/test/test_types.rb deleted file mode 100644 index cbd22d18..00000000 --- a/test/test_types.rb +++ /dev/null @@ -1,226 +0,0 @@ -require 'test/unit' -require 'test/proto/types.pb' - -class TypesTest < Test::Unit::TestCase - def test_serialize - types = Test::Types::TestTypes.new - types.type1 = 0.01 - types.type2 = 0.1 - types.type3 = 1 - types.type4 = 10 - types.type5 = 100 - types.type6 = 1000 - types.type7 = -1 - types.type8 = -10 - types.type9 = 10000 - types.type10 = 100000 - types.type11 = false - types.type12 = 'hello all types' - image_bin = File.open('test/data/unk.png', 'r+b'){|f| f.read} - types.type13 = image_bin - types.type14 = -100 - types.type15 = -1000 - - serialized_string = types.serialize_to_string - - types2 = Test::Types::TestTypes.new - types2.parse_from_string(serialized_string) - assert_in_delta(0.01, types2.type1, 0.00001) - assert_in_delta(0.1, types2.type2, 0.00001) - assert_equal(1, types2.type3) - assert_equal(10, types2.type4) - assert_equal(100, types2.type5) - assert_equal(1000, types2.type6) - assert_equal(-1, types2.type7) - assert_equal(-10, types2.type8) - assert_equal(10000, types2.type9) - assert_equal(100000, types2.type10) - assert(!types2.type11) - assert_equal('hello all types', types2.type12) - assert_equal(10938, types2.type13.size) - assert_equal(image_bin, types2.type13) - assert_equal(-100, types2.type14) - assert_equal(-1000, types2.type15) - end - - def test_serialize2 - types = Test::Types::TestTypes.new - types.type1 = 1.0/0 # double (Inf) - types.type2 = -1.0/0 # float (-Inf) - types.type3 = -1 # int32 - types.type4 = -10 # int64 - types.type5 = 100 # uint32 - types.type6 = 1000 # uint64 - types.type7 = -1000 # sint32 - types.type8 = -10000 # sint64 - types.type9 = 10000 # fixed32 - types.type10 = 100000 # fixed64 - types.type11 = true - types.type12 = 'hello all types' - image_bin = File.open('test/data/unk.png', 'r+b'){|f| f.read} - types.type13 = image_bin - types.type14 = -2_000_000_000 # sfixed32 - types.type15 = -8_000_000_000_000_000_000 # sfixed64 - - serialized_string = types.serialize_to_string - - types2 = Test::Types::TestTypes.new - types2.parse_from_string(serialized_string) - assert_equal(1.0/0.0, types2.type1) - assert_equal(-1.0/0.0, types2.type2) - assert_equal(-1, types2.type3) - assert_equal(-10, types2.type4) - assert_equal(100, types2.type5) - assert_equal(1000, types2.type6) - assert_equal(-1000, types2.type7) - assert_equal(-10000, types2.type8) - assert_equal(10000, types2.type9) - assert_equal(100000, types2.type10) - assert(types2.type11) - assert_equal('hello all types', types2.type12) - assert_equal(10938, types2.type13.size) - assert_equal(image_bin, types2.type13) - assert_equal(-2_000_000_000, types2.type14) - assert_equal(-8_000_000_000_000_000_000, types2.type15) - end - - def test_parse - types = Test::Types::TestTypes.new - types.parse_from_file('test/data/types.bin') - assert_in_delta(0.01, types.type1, 0.00001) - assert_in_delta(0.1, types.type2, 0.00001) - assert_equal(1, types.type3) - assert_equal(10, types.type4) - assert_equal(100, types.type5) - assert_equal(1000, types.type6) - assert_equal(-1, types.type7) - assert_equal(-10, types.type8) - assert_equal(10000, types.type9) - assert_equal(100000, types.type10) - assert_equal(false, !!types.type11) - assert_equal('hello all types', types.type12) - assert_equal(File.open('test/data/unk.png', 'r+b'){|f| f.read}, types.type13) - end - - def test_double - # double fixed 64-bit - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type1 = 1 } - assert_nothing_raised { types.type1 = 1.0 } - assert_raise(TypeError) { types.type1 = '' } - assert_nothing_raised { types.type1 = Protobuf::Field::DoubleField.max } - assert_nothing_raised { types.type1 = Protobuf::Field::DoubleField.min } - end - - def test_float - # float fixed 32-bit - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type2 = 1 } - assert_nothing_raised { types.type2 = 1.0 } - assert_raise(TypeError) { types.type2 = '' } - assert_nothing_raised { types.type2 = Protobuf::Field::FloatField.max } - assert_nothing_raised { types.type2 = Protobuf::Field::FloatField.min } - end - - def test_int32 - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type3 = 1 } - assert_nothing_raised { types.type3 = -1 } - assert_raise(TypeError) { types.type3 = 1.0 } - assert_raise(TypeError) { types.type3 = '' } - end - - def test_int64 - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type4 = 1 } - assert_nothing_raised { types.type4 = -1 } - assert_raise(TypeError) { types.type4 = 1.0 } - assert_raise(TypeError) { types.type4 = '' } - end - - def test_uint32 - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type5 = 1 } - assert_raise(RangeError) { types.type5 = -1 } - assert_raise(TypeError) { types.type5 = 1.0 } - assert_raise(TypeError) { types.type5 = '' } - end - - def test_uint64 - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type6 = 1 } - assert_raise(RangeError) { types.type6 = -1 } - assert_raise(TypeError) { types.type6 = 1.0 } - assert_raise(TypeError) { types.type6 = '' } - end - - def test_sint32 - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type7 = 1 } - assert_nothing_raised { types.type7 = -1 } - assert_raise(TypeError) { types.type7 = 1.0 } - assert_raise(TypeError) { types.type7 = '' } - end - - def test_sint64 - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type8 = 1 } - assert_nothing_raised { types.type8 = -1 } - assert_raise(TypeError) { types.type8 = 1.0 } - assert_raise(TypeError) { types.type8 = '' } - end - - def test_fixed32 - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type9 = 1 } - assert_raise(TypeError) { types.type9 = 1.0 } - assert_raise(TypeError) { types.type9 = '' } - assert_nothing_raised { types.type9 = Protobuf::Field::Fixed32Field.max } - assert_raise(RangeError) { types.type9 = Protobuf::Field::Fixed32Field.max + 1 } - assert_nothing_raised { types.type9 = Protobuf::Field::Fixed32Field.min } - assert_raise(RangeError) { types.type9 = Protobuf::Field::Fixed32Field.min - 1 } - end - - def test_fixed64 - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type10 = 1 } - assert_raise(TypeError) { types.type10 = 1.0 } - assert_raise(TypeError) { types.type10 = '' } - assert_nothing_raised { types.type10 = Protobuf::Field::Fixed64Field.max } - assert_raise(RangeError) { types.type10 = Protobuf::Field::Fixed64Field.max + 1 } - assert_nothing_raised { types.type10 = Protobuf::Field::Fixed64Field.min } - assert_raise(RangeError) { types.type10 = Protobuf::Field::Fixed64Field.min - 1 } - end - - def test_bool - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type11 = true } - assert_nothing_raised { types.type11 = false } - assert_nothing_raised { types.type11 = nil } - assert_raise(TypeError) { types.type11 = 0 } - assert_raise(TypeError) { types.type11 = '' } - end - - def test_string - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type12 = '' } - assert_nothing_raised { types.type12 = 'hello' } - assert_nothing_raised { types.type12 = nil } - assert_raise(TypeError) { types.type12 = 0 } - assert_raise(TypeError) { types.type12 = true } - end - - def test_bytes - types = Test::Types::TestTypes.new - assert_nothing_raised { types.type13 = '' } - assert_nothing_raised { types.type13 = 'hello' } - assert_nothing_raised { types.type13 = nil } - assert_raise(TypeError) { types.type13 = 0 } - assert_raise(TypeError) { types.type13 = true } - assert_raise(TypeError) { types.type13 = [] } - end - - def test_varint_getbytes - assert_equal("\xac\x02", Protobuf::Field::VarintField.encode(300)) - end -end diff --git a/varint_prof.rb b/varint_prof.rb new file mode 100644 index 00000000..ddd9abbb --- /dev/null +++ b/varint_prof.rb @@ -0,0 +1,82 @@ +require 'base64' +require 'benchmark/ips' +require 'pry' +require "./spec/support/protos/resource.pb" +#require "jruby/profiler/flame_graph_profile_printer" + +VALUES = { + 0 => "AA==", + 5 => "BQ==", + 51 => "Mw==", + 9_192 => "6Ec=", + 80_389 => "hfQE", + 913_389 => "7d83", + 516_192_829_912_693 => "9eyMkpivdQ==", + 9_999_999_999_999_999_999 => "//+fz8jgyOOKAQ==", +}.freeze + +#DECODEME = ::Derp.encode_cache_hash(2576) +#DECODEME2 = ::Derp.encode_cache_hash(25763111) + +10_000.times do + t = ::Test::Resource.new(:name => "derp", :date_created => 123456789) + t.status = 3 + ss = StringIO.new + ::Protobuf::Encoder.encode(t.to_proto, ss) + ss.rewind + t2 = ::Test::Resource.decode_from(ss) +end + +if ENV["FLAME"] + result = JRuby::Profiler.profile do + 1_000_000.times do + t = ::Test::Resource.new(:name => "derp", :date_created => 123456789) + t.status = 3 + ss = StringIO.new + ::Protobuf::Encoder.encode(t.to_proto, ss) + ss.rewind + t2 = ::Test::Resource.decode_from(ss) + #fail "derp" unless t == t2 + + #t = ::Test::Resource.new(:name => "derp", :date_created => 123456789) + #t.field?(:name) + #t.field?(:date_created) + #t.field?(:derp_derp) + #t.respond_to_has_and_present?(:name) + #t.respond_to_has_and_present?(:date_created) + #t.respond_to_has_and_present?(:derp_derp) + end + end + + printer = JRuby::Profiler::FlameGraphProfilePrinter.new(result) + printer.printProfile(STDOUT) +else + TO_HASH = ::Test::Resource.new(:name => "derp", :date_created => 123456789) + ENCODE = ::Test::Resource.new(:name => "derp", :date_created => 123456789) + DECODE = begin + ss = StringIO.new + ::Protobuf::Encoder.encode(ENCODE.to_proto, ss) + ss.rewind + ss.string + end + + Benchmark.ips do |x| + x.config(:time => 20, :warmup => 10) + x.report("to_hash") do + TO_HASH.to_hash + end + + x.report("to_hash_with_string_keys") do + TO_HASH.to_hash_with_string_keys + end + + x.report("encode") do + ss = StringIO.new + ::Protobuf::Encoder.encode(ENCODE.to_proto, ss) + end + + x.report("decode") do + ::Test::Resource.decode_from(::StringIO.new(DECODE.dup)) + end + end +end