pbbuilder 0.3.0 → 0.8.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: 1699e497c5f64a42a5d2233993352dc40669dee7e95b7cff0fae33dfb531d51b
4
- data.tar.gz: 91f2062978f63623af38e58e84b69a54d14d126f0921e2a467fbc5d14ca7a60e
3
+ metadata.gz: e22f13cb28a04d0b767c619b7c63c91e86a246d524abc200e5062d92cadb977e
4
+ data.tar.gz: b323e88b227263bfbd4ebc8b0e50f05208afe24527a2069e1110a6a605e453cb
5
5
  SHA512:
6
- metadata.gz: 925dc28280852d7844ef1404e13e10b016b0f4121ca790011d7b4241ed3a8c843a1045fd8a11e3e489cf50c1cbe3b4176f17f5e29f00f688d189420626a081f6
7
- data.tar.gz: 15b1f3fba2ad2e0404c9dd1df9fdeb0947c0f1c8f4f46e4f6e9a4a0d1ec1368a5891303e0a5cf3088eff60856cd465f8ef0de413999d5e7719b0e9dab09d7991
6
+ metadata.gz: c245be23b0283a134b1dc28b1ab29d96bd9ca0023456f9e984ff3406b164f057147ab9e366bdcf150236fe494417ef182480cc1d89a53880c684403efb60bb95
7
+ data.tar.gz: e4c4bbd500f921b0b2a71a7fc0e23ec99ea005a872f7f0d66a72f04cca712de54ef3e4898023bf22880eedb9afe990d9fac54dfce62ae164fd4c0b267bfc03bb
@@ -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/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  .byebug_history
12
12
  Gemfile.lock
13
13
  *.gem
14
+ /.idea
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,24 +39,43 @@ 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
- # pb.field @array { |element| pb.name = element.name }
48
- raise ::ArgumentError, "wrong number of arguments (expected 1)" unless args.length == 1
48
+ # pb.field @array { |element| pb.name element.name }
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
- # pb.field { pb.name = "hello" }
55
+ ::Kernel.raise ::ArgumentError, "wrong number of arguments (expected 0)" unless args.empty?
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
- @message[name] = args.first
60
+ arg = args.first
61
+ if descriptor.label == :repeated
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)
68
+ # pb.fields ["one", "two"]
69
+ # Using concat so it behaves the same as _append_repeated
70
+ @message[name].concat arg.to_ary
71
+ else
72
+ # pb.fields "one"
73
+ @message[name].push arg
74
+ end
75
+ else
76
+ # pb.field "value"
77
+ @message[name] = arg
78
+ end
60
79
  else
61
80
  # pb.field @value, :id, :name, :url
62
81
  element = args.shift
@@ -89,7 +108,7 @@ class Pbbuilder < BasicObject
89
108
  private
90
109
 
91
110
  def _append_repeated(name, descriptor, collection, &block)
92
- raise ::ArgumentError, "expected Enumerable" unless collection.respond_to?(:map)
111
+ ::Kernel.raise ::ArgumentError, "expected Enumerable" unless collection.respond_to?(:map)
93
112
  elements = collection.map do |element|
94
113
  message = _new_message_from_descriptor(descriptor)
95
114
  _scope(message) { block.call(element) }
@@ -108,7 +127,7 @@ class Pbbuilder < BasicObject
108
127
  end
109
128
 
110
129
  def _new_message_from_descriptor(descriptor)
111
- 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
112
131
 
113
132
  # Here we're using Protobuf reflection to create an instance of the message class
114
133
  message_descriptor = descriptor.subtype
@@ -117,4 +136,5 @@ class Pbbuilder < BasicObject
117
136
  end
118
137
  end
119
138
 
139
+ require "pbbuilder/protobuf_extension"
120
140
  require "pbbuilder/railtie" if defined?(Rails)
@@ -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,7 @@
1
+ require "google/protobuf/message_exts"
2
+
3
+ module Google::Protobuf::MessageExts::ClassMethods
4
+ def build(*args, &block)
5
+ Pbbuilder.new(new(*args), &block).target!
6
+ end
7
+ 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.3.0"
3
+ spec.version = "0.8.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"
@@ -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
@@ -1,5 +1,4 @@
1
1
  require "test_helper"
2
- require "pbbuilder"
3
2
 
4
3
  class PbbuilderTest < ActiveSupport::TestCase
5
4
  test "it makes it possible to create a person" do
@@ -8,9 +7,25 @@ class PbbuilderTest < ActiveSupport::TestCase
8
7
  pb.friends 1..3 do |number|
9
8
  pb.name "Friend ##{number}"
10
9
  end
10
+ pb.best_friend do
11
+ pb.name "Manuelo"
12
+ end
13
+ pb.field_mask do
14
+ pb.paths ["ok", "that's"]
15
+ pb.paths "cool"
16
+ end
17
+ pb.favourite_foods({
18
+ "Breakfast" => "Eggs",
19
+ "Lunch" => "Shawarma",
20
+ "Dinner" => "Pizza"
21
+ })
11
22
  end.target!
12
- assert_equal person.name, "Hello world"
13
- assert_equal person.friends.first.name, "Friend #1"
23
+
24
+ assert_equal "Hello world", person.name
25
+ assert_equal "Friend #1", person.friends.first.name
26
+ assert_equal ["ok", "that's", "cool"], person.field_mask.paths
27
+ assert_equal "Manuelo", person.best_friend.name
28
+ assert_equal "Eggs", person.favourite_foods["Breakfast"]
14
29
  end
15
30
 
16
31
  test "it can extract fields in a nice way" do
@@ -0,0 +1,18 @@
1
+ require "test_helper"
2
+
3
+ class ProtobufExtensionTest < ActiveSupport::TestCase
4
+ test ".build" do
5
+ person = API::Person.build(name: "Hello world!") do |pb|
6
+ pb.best_friend do
7
+ pb.name "Johnny"
8
+ end
9
+ end
10
+ assert_equal "Hello world!", person.name
11
+ assert_equal "Johnny", person.best_friend.name
12
+ end
13
+
14
+ test ".build without block" do
15
+ person = API::Person.build
16
+ assert_equal "", person.name
17
+ end
18
+ end
data/test/test_helper.rb CHANGED
@@ -5,9 +5,20 @@ require "rails"
5
5
  require "rails/test_help"
6
6
  require "rails/test_unit/reporter"
7
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
+
16
+ require "pbbuilder"
17
+
8
18
  Rails::TestUnitReporter.executable = "bin/test"
9
19
 
10
20
  require "google/protobuf"
21
+ require "google/protobuf/field_mask_pb"
11
22
 
12
23
  Google::Protobuf::DescriptorPool.generated_pool.build do
13
24
  add_file("pbbuilder.proto", syntax: :proto3) do
@@ -15,6 +26,9 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
15
26
  optional :name, :string, 1
16
27
  repeated :friends, :message, 2, "pbbuildertest.Person"
17
28
  optional :best_friend, :message, 3, "pbbuildertest.Person"
29
+ repeated :nicknames, :string, 4
30
+ optional :field_mask, :message, 5, "google.protobuf.FieldMask"
31
+ map :favourite_foods, :string, :string, 6
18
32
  end
19
33
  end
20
34
  end
@@ -22,3 +36,10 @@ end
22
36
  module API
23
37
  Person = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("pbbuildertest.Person").msgclass
24
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.3.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bouke van der Bijl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-31 00:00:00.000000000 Z
11
+ date: 2021-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-protobuf
@@ -31,6 +31,7 @@ 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
@@ -39,10 +40,14 @@ files:
39
40
  - bin/test
40
41
  - lib/pbbuilder.rb
41
42
  - lib/pbbuilder/handler.rb
43
+ - lib/pbbuilder/protobuf_extension.rb
42
44
  - lib/pbbuilder/railtie.rb
45
+ - lib/pbbuilder/template.rb
43
46
  - lib/tasks/pbbuilder_tasks.rake
44
47
  - pbbuilder.gemspec
48
+ - test/pbbuilder_template_test.rb
45
49
  - test/pbbuilder_test.rb
50
+ - test/protobuf_extension_test.rb
46
51
  - test/test_helper.rb
47
52
  homepage: https://github.com/cheddar-me/pbbuilder
48
53
  licenses:
@@ -68,5 +73,7 @@ signing_key:
68
73
  specification_version: 4
69
74
  summary: Generate Protobuf messages via a Builder-style DSL
70
75
  test_files:
76
+ - test/pbbuilder_template_test.rb
71
77
  - test/pbbuilder_test.rb
78
+ - test/protobuf_extension_test.rb
72
79
  - test/test_helper.rb