pbbuilder 0.5.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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