pbbuilder 0.5.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ef756425a57d8ea7b70338e5d15f4de645a0fe43c6429a7bc83e601c3ae7730
4
- data.tar.gz: 5c7579092048ee2b38f4aca3fa02c0c1490bb2f87a0ae58e9120672ca78b2de7
3
+ metadata.gz: c604d50c66beb44267054888f1a9c4256947513e6f680871a8bd57e405dffa1f
4
+ data.tar.gz: b16fe2eddd9e094157c3d7d26952351a555defd53e308ce4e4486640eb1a7022
5
5
  SHA512:
6
- metadata.gz: 6f963451d5430510700378384d0a996ad6eb15e311dc3669d8246122c2db7e65a0bb9b50b63855b18da1cf5f1484a57f85ad3eda95b3647f09438d04fc78d0e9
7
- data.tar.gz: 52d88436c2f78dbe307f2ce87935a03c293a6ee68d2c471c993ef753166f7450380bf600f759b2d4d5467c5167a50d93dcb63d145085f8bf50acbcdc43531bd1
6
+ metadata.gz: 10bb08396d4fd7d7203b13d80c0cc873f75d91c1fd92fccee095f44d3cbee291d0f95e4e5d3115ac16842799df3b65126e1b2c7bf32dedb68815bfc32b3a7226
7
+ data.tar.gz: 67a859f6c82c0fb5235d295a46c74c7dc2d9197fdf5f9bf1fc864b34d8636a8f7f76296c9f550a0aa7abfbfc019a348b3b4a40333a27e8d6afcf514334132838
@@ -0,0 +1,22 @@
1
+ name: ruby
2
+ on: push
3
+
4
+ jobs:
5
+ test:
6
+ runs-on: ubuntu-latest
7
+ strategy:
8
+ matrix:
9
+ ruby: ["2.7", "3.0.1"]
10
+ name: tests for ruby-${{ matrix.ruby }}
11
+ steps:
12
+ - name: Checkout code
13
+ uses: actions/checkout@v2
14
+
15
+ - name: Setup Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ bundler-cache: true
19
+ ruby-version: ${{ matrix.ruby }}
20
+
21
+ - name: Run tests
22
+ run: bin/test
data/README.md CHANGED
@@ -22,7 +22,8 @@ $ gem install pbbuilder
22
22
  ```
23
23
 
24
24
  ## Contributing
25
- Contribution directions go here.
25
+
26
+ When debugging, make sure you're prepending `::Kernel` to any calls such as `puts` as otherwise the code will think you're trying to add another attribute onto the protobuf.
26
27
 
27
28
  ## License
28
29
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/lib/pbbuilder.rb CHANGED
@@ -28,8 +28,8 @@ class Pbbuilder < BasicObject
28
28
  yield self if ::Kernel.block_given?
29
29
  end
30
30
 
31
- def method_missing(field, *args, &block)
32
- set!(field, *args, &block)
31
+ def method_missing(...)
32
+ set!(...)
33
33
  end
34
34
 
35
35
  def respond_to_missing?(field)
@@ -39,26 +39,32 @@ class Pbbuilder < BasicObject
39
39
  def set!(field, *args, &block)
40
40
  name = field.to_s
41
41
  descriptor = @message.class.descriptor.lookup(name)
42
+ ::Kernel.raise ::ArgumentError, "Unknown field #{name}" if descriptor.nil?
42
43
 
43
44
  if block
44
- raise ::ArgumentError, "can't pass block to non-message field" unless descriptor.type == :message
45
+ ::Kernel.raise ::ArgumentError, "can't pass block to non-message field" unless descriptor.type == :message
45
46
 
46
47
  if descriptor.label == :repeated
47
48
  # pb.field @array { |element| pb.name element.name }
48
- raise ::ArgumentError, "wrong number of arguments (expected 1)" unless args.length == 1
49
+ ::Kernel.raise ::ArgumentError, "wrong number of arguments #{args.length} (expected 1)" unless args.length == 1
49
50
  collection = args.first
50
51
  _append_repeated(name, descriptor, collection, &block)
51
52
  return
52
53
  end
53
54
 
54
- raise ::ArgumentError, "wrong number of arguments (expected 0)" unless args.empty?
55
+ ::Kernel.raise ::ArgumentError, "wrong number of arguments (expected 0)" unless args.empty?
55
56
  # pb.field { pb.name "hello" }
56
57
  message = (@message[name] ||= _new_message_from_descriptor(descriptor))
57
58
  _scope(message, &block)
58
59
  elsif args.length == 1
59
60
  arg = args.first
60
61
  if descriptor.label == :repeated
61
- if arg.respond_to?(:to_ary)
62
+ if arg.respond_to?(:to_hash)
63
+ # pb.fields {"one" => "two"}
64
+ arg.to_hash.each do |k, v|
65
+ @message[name][k] = v
66
+ end
67
+ elsif arg.respond_to?(:to_ary)
62
68
  # pb.fields ["one", "two"]
63
69
  # Using concat so it behaves the same as _append_repeated
64
70
  @message[name].concat arg.to_ary
@@ -102,7 +108,7 @@ class Pbbuilder < BasicObject
102
108
  private
103
109
 
104
110
  def _append_repeated(name, descriptor, collection, &block)
105
- raise ::ArgumentError, "expected Enumerable" unless collection.respond_to?(:map)
111
+ ::Kernel.raise ::ArgumentError, "expected Enumerable" unless collection.respond_to?(:map)
106
112
  elements = collection.map do |element|
107
113
  message = _new_message_from_descriptor(descriptor)
108
114
  _scope(message) { block.call(element) }
@@ -121,7 +127,7 @@ class Pbbuilder < BasicObject
121
127
  end
122
128
 
123
129
  def _new_message_from_descriptor(descriptor)
124
- raise ::ArgumentError, "can't pass block to non-message field" unless descriptor.type == :message
130
+ ::Kernel.raise ::ArgumentError, "can't pass block to non-message field" unless descriptor.type == :message
125
131
 
126
132
  # Here we're using Protobuf reflection to create an instance of the message class
127
133
  message_descriptor = descriptor.subtype
@@ -1,3 +1,5 @@
1
+ require "pbbuilder/template"
2
+
1
3
  # Basically copied and pasted from JbuilderHandler, except it uses Pbbuilder
2
4
 
3
5
  class PbbuilderHandler
@@ -6,7 +8,7 @@ class PbbuilderHandler
6
8
  def self.call(template, source = nil)
7
9
  source ||= template.source
8
10
  # We need to keep `source` on the first line, so line numbers are correct if there's an error
9
- %{pb=Pbbuilder.new(@_response_class.new); #{source}
10
- pb.target!}
11
+ %{__already_defined = defined?(pb); pb ||= PbbuilderTemplate.new(self, @_response_class.new); #{source}
12
+ pb.target! unless (__already_defined && __already_defined != "method")}
11
13
  end
12
14
  end
@@ -0,0 +1,97 @@
1
+ # PbbuilderTemplate is an extension of Pbbuilder to be used as a Rails template
2
+ # It adds support for partials.
3
+ class PbbuilderTemplate < Pbbuilder
4
+ class << self
5
+ attr_accessor :template_lookup_options
6
+ end
7
+
8
+ self.template_lookup_options = {handlers: [:pbbuilder]}
9
+
10
+ def initialize(context, message)
11
+ @context = context
12
+ super(message)
13
+ end
14
+
15
+ # Render a partial. Can be called as:
16
+ # pb.partial! "name/of_partial", argument: 123
17
+ # pb.partial! "name/of_partial", locals: {argument: 123}
18
+ # pb.partial! partial: "name/of_partial", argument: 123
19
+ # pb.partial! partial: "name/of_partial", locals: {argument: 123}
20
+ # pb.partial! @model # @model is an ActiveModel value, it will use the name to look up a partial
21
+ def partial!(*args)
22
+ if args.one? && _is_active_model?(args.first)
23
+ _render_active_model_partial args.first
24
+ else
25
+ _render_explicit_partial(*args)
26
+ end
27
+ end
28
+
29
+ def set!(field, *args, **kwargs, &block)
30
+ # If partial options are being passed, we render a submessage with a partial
31
+ if kwargs.has_key?(:partial)
32
+ if args.one? && kwargs.has_key?(:as)
33
+ # pb.friends @friends, partial: "friend", as: :friend
34
+ # Call set! on the super class, passing in a block that renders a partial for every element
35
+ super(field, *args) do |element|
36
+ _set_inline_partial(element, kwargs)
37
+ end
38
+ else
39
+ # pb.best_friend partial: "person", person: @best_friend
40
+ # Call set! as a submessage, passing in the kwargs as partial options
41
+ super(field, *args) do
42
+ _render_partial_with_options(kwargs)
43
+ end
44
+ end
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def _is_active_model?(object)
53
+ object.class.respond_to?(:model_name) && object.respond_to?(:to_partial_path)
54
+ end
55
+
56
+ def _render_explicit_partial(name_or_options, locals = {})
57
+ case name_or_options
58
+ when ::Hash
59
+ # partial! partial: 'name', foo: 'bar'
60
+ options = name_or_options
61
+ else
62
+ # partial! 'name', locals: {foo: 'bar'}
63
+ options = if locals.one? && (locals.keys.first == :locals)
64
+ locals.merge(partial: name_or_options)
65
+ else
66
+ {partial: name_or_options, locals: locals}
67
+ end
68
+ # partial! 'name', foo: 'bar'
69
+ as = locals.delete(:as)
70
+ options[:as] = as if as.present?
71
+ options[:collection] = locals[:collection] if locals.key?(:collection)
72
+ end
73
+
74
+ _render_partial_with_options options
75
+ end
76
+
77
+ def _render_active_model_partial(object)
78
+ @context.render object, pb: self
79
+ end
80
+
81
+ def _set_inline_partial(object, options)
82
+ locals = ::Hash[options[:as], object]
83
+ _render_partial_with_options options.merge(locals: locals)
84
+ end
85
+
86
+ def _render_partial_with_options(options)
87
+ options.reverse_merge! locals: options.except(:partial, :as, :collection)
88
+ options.reverse_merge! ::PbbuilderTemplate.template_lookup_options
89
+
90
+ _render_partial options
91
+ end
92
+
93
+ def _render_partial(options)
94
+ options[:locals][:pb] = self
95
+ @context.render options
96
+ end
97
+ end
data/pbbuilder.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "pbbuilder"
3
- spec.version = "0.5.0"
3
+ spec.version = "0.9.0"
4
4
  spec.authors = ["Bouke van der Bijl"]
5
5
  spec.email = ["bouke@cheddar.me"]
6
6
  spec.homepage = "https://github.com/cheddar-me/pbbuilder"
@@ -10,5 +10,5 @@ Gem::Specification.new do |spec|
10
10
  spec.files = `git ls-files`.split("\n")
11
11
  spec.test_files = `git ls-files -- test/*`.split("\n")
12
12
 
13
- spec.add_dependency "google-protobuf", "~> 3.15.5"
13
+ spec.add_dependency "google-protobuf", "~> 3.17.3"
14
14
  end
@@ -0,0 +1,107 @@
1
+ require "test_helper"
2
+ require "action_view/testing/resolvers"
3
+
4
+ class PbbuilderTemplateTest < ActiveSupport::TestCase
5
+ PERSON_PARTIAL = <<-PBBUILDER
6
+ pb.extract! person, :name
7
+ PBBUILDER
8
+
9
+ RACER_PARTIAL = <<-PBBUILDER
10
+ pb.extract! racer, :name
11
+ pb.friends racer.friends, partial: "racers/racer", as: :racer
12
+ pb.best_friend partial: "racers/racer", racer: racer.best_friend if racer.best_friend.present?
13
+ PBBUILDER
14
+
15
+ PARTIALS = {
16
+ "_partial.pb.pbbuilder" => "pb.name name",
17
+ "_person.pb.pbbuilder" => PERSON_PARTIAL,
18
+ "racers/_racer.pb.pbbuilder" => RACER_PARTIAL,
19
+
20
+ # Ensure we find only Pbbuilder partials from within Pbbuilder templates.
21
+ "_person.html.erb" => "Hello world!"
22
+ }
23
+
24
+ test "basic template" do
25
+ result = render('pb.name "hello"')
26
+ assert_equal "hello", result.name
27
+ end
28
+
29
+ test "partial by name with top-level locals" do
30
+ result = render('pb.partial! "partial", name: "hello"')
31
+ assert_equal "hello", result.name
32
+ end
33
+
34
+ test "submessage partial" do
35
+ other_racer = Racer.new(2, "Max Verstappen", [])
36
+ racer = Racer.new(123, "Chris Harris", [], other_racer)
37
+ result = render('pb.best_friend partial: "person", person: @racer.best_friend', racer: racer)
38
+ assert_equal "Max Verstappen", result.best_friend.name
39
+ end
40
+
41
+ test "hash" do
42
+ result = render('pb.favourite_foods "pizza" => "yes"')
43
+ assert_equal({"pizza" => "yes"}, result.favourite_foods.to_h)
44
+ end
45
+
46
+ test "partial by name with nested locals" do
47
+ result = render('pb.partial! "partial", locals: { name: "hello" }')
48
+ assert_equal "hello", result.name
49
+ end
50
+
51
+ test "partial by options containing nested locals" do
52
+ result = render('pb.partial! partial: "partial", locals: { name: "hello" }')
53
+ assert_equal "hello", result.name
54
+ end
55
+
56
+ test "partial by options containing top-level locals" do
57
+ result = render('pb.partial! partial: "partial", name: "hello"')
58
+ assert_equal "hello", result.name
59
+ end
60
+
61
+ test "partial for Active Model" do
62
+ result = render("pb.partial! @racer", racer: Racer.new(123, "Chris Harris", []))
63
+ assert_equal "Chris Harris", result.name
64
+ end
65
+
66
+ test "collection partial" do
67
+ friends = [Racer.new(1, "Johnny Test", []), Racer.new(2, "Max Verstappen", [])]
68
+ result = render("pb.partial! @racer", racer: Racer.new(123, "Chris Harris", friends))
69
+ assert_equal 2, result.friends.size
70
+ assert_equal "Johnny Test", result.friends[0].name
71
+ assert_equal "Max Verstappen", result.friends[1].name
72
+ end
73
+
74
+ test "nested message partial" do
75
+ other_racer = Racer.new(2, "Max Verstappen", [])
76
+ result = render("pb.partial! @racer", racer: Racer.new(123, "Chris Harris", [], other_racer))
77
+ assert_equal "Max Verstappen", result.best_friend.name
78
+ end
79
+
80
+ private
81
+
82
+ def render(*args)
83
+ render_without_parsing(*args)
84
+ end
85
+
86
+ def render_without_parsing(source, assigns = {})
87
+ view = build_view(fixtures: PARTIALS.merge("source.pb.pbbuilder" => source), assigns: assigns)
88
+ view.render(template: "source", handlers: [:pbbuilder], formats: [:pb])
89
+ end
90
+
91
+ def build_view(options = {})
92
+ resolver = ActionView::FixtureResolver.new(options.fetch(:fixtures))
93
+ lookup_context = ActionView::LookupContext.new([resolver], {}, [""])
94
+ controller = ActionView::TestCase::TestController.new
95
+
96
+ assigns = options.fetch(:assigns, {})
97
+ assigns.reverse_merge! _response_class: API::Person
98
+
99
+ view = ActionView::Base.with_empty_template_cache.new(lookup_context, assigns, controller)
100
+
101
+ def view.view_cache_dependencies
102
+ []
103
+ end
104
+
105
+ view
106
+ end
107
+ end
@@ -14,11 +14,18 @@ class PbbuilderTest < ActiveSupport::TestCase
14
14
  pb.paths ["ok", "that's"]
15
15
  pb.paths "cool"
16
16
  end
17
+ pb.favourite_foods({
18
+ "Breakfast" => "Eggs",
19
+ "Lunch" => "Shawarma",
20
+ "Dinner" => "Pizza"
21
+ })
17
22
  end.target!
23
+
18
24
  assert_equal "Hello world", person.name
19
25
  assert_equal "Friend #1", person.friends.first.name
20
26
  assert_equal ["ok", "that's", "cool"], person.field_mask.paths
21
27
  assert_equal "Manuelo", person.best_friend.name
28
+ assert_equal "Eggs", person.favourite_foods["Breakfast"]
22
29
  end
23
30
 
24
31
  test "it can extract fields in a nice way" do
data/test/test_helper.rb CHANGED
@@ -4,6 +4,15 @@ ENV["RAILS_ENV"] = "test"
4
4
  require "rails"
5
5
  require "rails/test_help"
6
6
  require "rails/test_unit/reporter"
7
+
8
+ require "active_support"
9
+ require "active_support/core_ext/array/access"
10
+ require "active_support/cache/memory_store"
11
+ require "active_support/json"
12
+ require "active_model"
13
+ require "action_view"
14
+ require "rails/version"
15
+
7
16
  require "pbbuilder"
8
17
 
9
18
  Rails::TestUnitReporter.executable = "bin/test"
@@ -19,6 +28,7 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
19
28
  optional :best_friend, :message, 3, "pbbuildertest.Person"
20
29
  repeated :nicknames, :string, 4
21
30
  optional :field_mask, :message, 5, "google.protobuf.FieldMask"
31
+ map :favourite_foods, :string, :string, 6
22
32
  end
23
33
  end
24
34
  end
@@ -26,3 +36,10 @@ end
26
36
  module API
27
37
  Person = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pbbuildertest.Person").msgclass
28
38
  end
39
+
40
+ class Racer < Struct.new(:id, :name, :friends, :best_friend)
41
+ extend ActiveModel::Naming
42
+ include ActiveModel::Conversion
43
+ end
44
+
45
+ ActionView::Template.register_template_handler :pbbuilder, PbbuilderHandler
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pbbuilder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bouke van der Bijl
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-15 00:00:00.000000000 Z
11
+ date: 2021-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-protobuf
@@ -16,21 +16,22 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 3.15.5
19
+ version: 3.17.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 3.15.5
27
- description:
26
+ version: 3.17.3
27
+ description:
28
28
  email:
29
29
  - bouke@cheddar.me
30
30
  executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
+ - ".github/workflows/test.yml"
34
35
  - ".gitignore"
35
36
  - Gemfile
36
37
  - MIT-LICENSE
@@ -41,8 +42,10 @@ files:
41
42
  - lib/pbbuilder/handler.rb
42
43
  - lib/pbbuilder/protobuf_extension.rb
43
44
  - lib/pbbuilder/railtie.rb
45
+ - lib/pbbuilder/template.rb
44
46
  - lib/tasks/pbbuilder_tasks.rake
45
47
  - pbbuilder.gemspec
48
+ - test/pbbuilder_template_test.rb
46
49
  - test/pbbuilder_test.rb
47
50
  - test/protobuf_extension_test.rb
48
51
  - test/test_helper.rb
@@ -50,7 +53,7 @@ homepage: https://github.com/cheddar-me/pbbuilder
50
53
  licenses:
51
54
  - MIT
52
55
  metadata: {}
53
- post_install_message:
56
+ post_install_message:
54
57
  rdoc_options: []
55
58
  require_paths:
56
59
  - lib
@@ -65,11 +68,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
68
  - !ruby/object:Gem::Version
66
69
  version: '0'
67
70
  requirements: []
68
- rubygems_version: 3.2.3
69
- signing_key:
71
+ rubygems_version: 3.1.4
72
+ signing_key:
70
73
  specification_version: 4
71
74
  summary: Generate Protobuf messages via a Builder-style DSL
72
75
  test_files:
76
+ - test/pbbuilder_template_test.rb
73
77
  - test/pbbuilder_test.rb
74
78
  - test/protobuf_extension_test.rb
75
79
  - test/test_helper.rb