pbbuilder 0.2.1 → 0.7.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: c8faf01239529f30f0a006d02e97cbf0b2ffb15c5139bd0e91bdff4cc455404c
4
- data.tar.gz: bf0357a007ac65e10ecbd28baa44f2a71adf3f27fc618c12e96430ead5ce669f
3
+ metadata.gz: 83ca08d74d2463d2d14d5d57127e482a0ea9fde4a0cdcaac3f237c3a8f48ff23
4
+ data.tar.gz: 828fe17a1b1be0feab98d7cc68940350df6093d0995751e0873c46d9a363f487
5
5
  SHA512:
6
- metadata.gz: e4c0cdca64a81e5ecb30fdd5ba7351db5ae5ec3d839bac3e766c9b6c7eb1f55d5081f7a231d44c10a3eb8633f9cb83e0680a673383dee0697c2c0b3f575fe9ba
7
- data.tar.gz: 8ccc14c72a3a732b089f070b091de2d440d38fbb858c4515254b7b1b8155262840181100209ba8f49b78b4a3c56db47c3e037044265e2aed162371fd3c1aef1f
6
+ metadata.gz: 2c4285699991af59101b07e3e04dd987620065362febdc8623d62390b15a17fd2660031f9e3a4cc3eb602609504ab70cde82848012fc061759124a706809fd3a
7
+ data.tar.gz: 214263e610fc46ab2bf978f17dd3dc8633ae757af8859906c4c588827c00e383b542ac749b5f9e089ede8401153a54629b82e83a29d5c212043965c70035fc58
@@ -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,93 @@
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 assume a collection is being rendered with a partial for every element
31
+ # pb.friends @friends, partial: "friend", as: :friend
32
+ if args.one? && _partial_options?(kwargs)
33
+ # Call set! on the super class, passing in a block that renders a partial for every element
34
+ super(field, *args) do |element|
35
+ _set_inline_partial(element, kwargs)
36
+ end
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def _partial_options?(options)
45
+ ::Hash === options && options.key?(:as) && options.key?(:partial)
46
+ end
47
+
48
+ def _is_active_model?(object)
49
+ object.class.respond_to?(:model_name) && object.respond_to?(:to_partial_path)
50
+ end
51
+
52
+ def _render_explicit_partial(name_or_options, locals = {})
53
+ case name_or_options
54
+ when ::Hash
55
+ # partial! partial: 'name', foo: 'bar'
56
+ options = name_or_options
57
+ else
58
+ # partial! 'name', locals: {foo: 'bar'}
59
+ options = if locals.one? && (locals.keys.first == :locals)
60
+ locals.merge(partial: name_or_options)
61
+ else
62
+ {partial: name_or_options, locals: locals}
63
+ end
64
+ # partial! 'name', foo: 'bar'
65
+ as = locals.delete(:as)
66
+ options[:as] = as if as.present?
67
+ options[:collection] = locals[:collection] if locals.key?(:collection)
68
+ end
69
+
70
+ _render_partial_with_options options
71
+ end
72
+
73
+ def _render_active_model_partial(object)
74
+ @context.render object, pb: self
75
+ end
76
+
77
+ def _set_inline_partial(object, options)
78
+ locals = ::Hash[options[:as], object]
79
+ _render_partial_with_options options.merge(locals: locals)
80
+ end
81
+
82
+ def _render_partial_with_options(options)
83
+ options.reverse_merge! locals: options.except(:partial, :as, :collection)
84
+ options.reverse_merge! ::PbbuilderTemplate.template_lookup_options
85
+
86
+ _render_partial options
87
+ end
88
+
89
+ def _render_partial(options)
90
+ options[:locals][:pb] = self
91
+ @context.render options
92
+ end
93
+ 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.2.1"
3
+ spec.version = "0.7.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,97 @@
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 do
13
+ pb.partial! "racers/racer", racer: racer.best_friend
14
+ end if racer.best_friend.present?
15
+ PBBUILDER
16
+
17
+ PARTIALS = {
18
+ "_partial.pb.pbbuilder" => "pb.name name",
19
+ "_person.pb.pbbuilder" => PERSON_PARTIAL,
20
+ "racers/_racer.pb.pbbuilder" => RACER_PARTIAL,
21
+
22
+ # Ensure we find only Pbbuilder partials from within Pbbuilder templates.
23
+ "_person.html.erb" => "Hello world!"
24
+ }
25
+
26
+ test "basic template" do
27
+ result = render('pb.name "hello"')
28
+ assert_equal "hello", result.name
29
+ end
30
+
31
+ test "partial by name with top-level locals" do
32
+ result = render('pb.partial! "partial", name: "hello"')
33
+ assert_equal "hello", result.name
34
+ end
35
+
36
+ test "partial by name with nested locals" do
37
+ result = render('pb.partial! "partial", locals: { name: "hello" }')
38
+ assert_equal "hello", result.name
39
+ end
40
+
41
+ test "partial by options containing nested locals" do
42
+ result = render('pb.partial! partial: "partial", locals: { name: "hello" }')
43
+ assert_equal "hello", result.name
44
+ end
45
+
46
+ test "partial by options containing top-level locals" do
47
+ result = render('pb.partial! partial: "partial", name: "hello"')
48
+ assert_equal "hello", result.name
49
+ end
50
+
51
+ test "partial for Active Model" do
52
+ result = render("pb.partial! @racer", racer: Racer.new(123, "Chris Harris", []))
53
+ assert_equal "Chris Harris", result.name
54
+ end
55
+
56
+ test "collection partial" do
57
+ friends = [Racer.new(1, "Johnny Test", []), Racer.new(2, "Max Verstappen", [])]
58
+ result = render("pb.partial! @racer", racer: Racer.new(123, "Chris Harris", friends))
59
+ assert_equal 2, result.friends.size
60
+ assert_equal "Johnny Test", result.friends[0].name
61
+ assert_equal "Max Verstappen", result.friends[1].name
62
+ end
63
+
64
+ test "nested message partial" do
65
+ other_racer = Racer.new(2, "Max Verstappen", [])
66
+ result = render("pb.partial! @racer", racer: Racer.new(123, "Chris Harris", [], other_racer))
67
+ assert_equal "Max Verstappen", result.best_friend.name
68
+ end
69
+
70
+ private
71
+
72
+ def render(*args)
73
+ render_without_parsing(*args)
74
+ end
75
+
76
+ def render_without_parsing(source, assigns = {})
77
+ view = build_view(fixtures: PARTIALS.merge("source.pb.pbbuilder" => source), assigns: assigns)
78
+ view.render(template: "source", handlers: [:pbbuilder], formats: [:pb])
79
+ end
80
+
81
+ def build_view(options = {})
82
+ resolver = ActionView::FixtureResolver.new(options.fetch(:fixtures))
83
+ lookup_context = ActionView::LookupContext.new([resolver], {}, [""])
84
+ controller = ActionView::TestCase::TestController.new
85
+
86
+ assigns = options.fetch(:assigns, {})
87
+ assigns.reverse_merge! _response_class: API::Person
88
+
89
+ view = ActionView::Base.with_empty_template_cache.new(lookup_context, assigns, controller)
90
+
91
+ def view.view_cache_dependencies
92
+ []
93
+ end
94
+
95
+ view
96
+ end
97
+ 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.2.1
4
+ version: 0.7.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