pb-serializer 0.5.1 → 0.5.2

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: db5867da46505d0facc08d12c1d6a3c93a87d7ca617110e8a9f2265444df72db
4
- data.tar.gz: 594549370135b3237cbdfc2e701554eafa89c87fababd5b64e33476ed2ce11b2
3
+ metadata.gz: b06e7d595d2212e0d8c6d36bc652acccb3020094b6370c9494476eb0de483487
4
+ data.tar.gz: 13efc77cdd5a595a0763b16989f7064a5c1f79ebd3bdff7330964eadd7569db7
5
5
  SHA512:
6
- metadata.gz: d40d26a27feb02d827068be43c3c93f44146cc7a2314047684bd7290cab34beee1e5930fd9d000922ad768fc00fcd7f544e530a85cf5267f7913a8f7e7f3fe4c
7
- data.tar.gz: 7f2e1f146b3f5f466b1d3886b60e70ccc6505a919c7b8ef8c2a8498b2182b441a5695318b27e028f5f4943cdf39ea7f650ddc53370d475ea1385e5e4363dd48b
6
+ metadata.gz: 936b879ffebc21afb812bb8763cfc57132c19db685138ba0b69c74ab156c70909a2f35482464c0c35ed776e5b2ff02546b1f55699bc48b5f44568890771da0b8
7
+ data.tar.gz: 6da97182764357672107dbacf6184e723f55ad203078dc9c660e53741d861ae39e210cf5d7ecb6a9b4155e618273e0963a97bc322bba7e579faa8f066b36a5bb
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --no-private
2
+ --hide-api private
3
+ lib/**/*.rb
4
+ -
5
+ README.ja.md
6
+ docs/examples.md
7
+ CHANGELOG.md
8
+ LICENSE.txt
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 0.5.2
4
+
5
+ - Generate the default mask lazily to prevent infinite recursions https://github.com/wantedly/pb-serializer/pull/52
6
+
3
7
  ## 0.5.1
4
8
 
5
9
  - Improving interoperability with `computed_model`
data/README.ja.md ADDED
@@ -0,0 +1,66 @@
1
+ <!--
2
+ # @title 日本語版 README
3
+ -->
4
+
5
+ # Pb::Serializer
6
+
7
+ `Pb::Serializer` はRuby オブジェクトの Protocol Buffers シリアライザです。
8
+
9
+ [English version](./README.md)
10
+
11
+ ## Features
12
+
13
+ - [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers) のような宣言的な API
14
+ - [Well-Known Types](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf) への自動変換(例 `google.protobuf.Uint64Value`)
15
+ - [`google.protobuf.FieldMask`](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask) を利用した、GraphQL のような選択的フィールド取得のサポート
16
+ - [ComputedModel](https://github.com/wantedly/computed_model) と組み合わせることで、複雑なロジックと依存関係を持つ API も宣言的に実装できます
17
+
18
+
19
+ ## Usage
20
+
21
+ 以下のような Protocol Buffers のメッセージ定義および ActiveRecord モデルを例にします。
22
+
23
+ ```proto
24
+ syntax = "proto3";
25
+
26
+ package example;
27
+
28
+ option ruby_package = "ExamplesPb";
29
+
30
+ message User {
31
+ uint64 id = 1;
32
+ string name = 2;
33
+ }
34
+ ```
35
+
36
+ ```ruby
37
+ # Schema: [id(integer), name(string)]
38
+ class User < ActiveRecord::Base
39
+ end
40
+ ```
41
+
42
+ `.proto` で定義された `User` メッセージに対応する PbSerializer を実装します。
43
+ 生成されたクラスと定義されているフィールドすべてを PbSerializer に宣言する必要があります。
44
+
45
+ ```ruby
46
+ class UserPbSerializer < Pb::Serializer::Base
47
+ message ExamplesPb::User
48
+
49
+ attribute :id
50
+ attribute :name
51
+ end
52
+ ```
53
+
54
+ 実装した PbSerializer で、Ruby オブジェクトを protobuf message object にシリアライズできます。
55
+
56
+ ```ruby
57
+ user = User.find(123)
58
+ UserPbSerializer.new(user).to_pb
59
+ # => <ExamplesPb::User: id: 123, name: "someuser">
60
+ ```
61
+
62
+ 各 attribute の値は、PbSerializer インスタンス、もしくはコンストラクタに渡されたオブジェクト から決定されます。
63
+
64
+ ## Next read
65
+
66
+ - [Examples](./docs/examples.md)
data/README.md CHANGED
@@ -4,62 +4,66 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/pb-serializer.svg)](https://badge.fury.io/rb/pb-serializer)
5
5
  [![License](https://img.shields.io/github/license/wantedly/pb-serializer)](./LICENSE)
6
6
 
7
- ```rb
8
- class UserSerializer < Pb::Serializer::Base
9
- message YourApp::User
7
+ `Pb::Serializer` is Protocol Buffers serializer for Ruby objects.
10
8
 
11
- attribute :id
12
- attribute :name
13
- attribute :posts
9
+ [日本語版 README](./README.ja.md)
10
+
11
+ ## Features
12
+
13
+ - Declarative APIs such as [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers)
14
+ - Automatic conversion to [Well-Known Types](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf) (e.g. `google.protobuf.Uint64Value`)
15
+ - Support for GraphQL-like selective field fetching using [`google.protobuf.FieldMask`](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask).
16
+ - When combined with [ComputedModel](https://github.com/wantedly/computed_model), APIs with complex logic and dependencies can be implemented declaratively.
17
+
18
+
19
+ ## Usage
20
+
21
+ The following is an example of a message definition and ActiveRecord model for Protocol Buffers.
22
+
23
+ ```proto
24
+ syntax = "proto3";
14
25
 
15
- define_primary_loader :user do |subdeps, ids:, **|
16
- User.where(id: ids).preload(subdeps).map { |u| new(u) }
17
- end
26
+ package example;
18
27
 
19
- define_loader :posts, key: -> { id } do |user_ids, subdeps, **|
20
- PostSerializer.bulk_load(user_id: user_ids, with: subdeps).group_by { |s| s.post.user_id }
21
- end
28
+ option ruby_package = "ExamplesPb";
22
29
 
23
- dependency :posts
24
- computed def post_count
25
- posts.count
26
- end
30
+ message User {
31
+ uint64 id = 1;
32
+ string name = 2;
33
+ }
34
+ ```
35
+
36
+ ```ruby
37
+ # Schema: [id(integer), name(string)]
38
+ class User < ActiveRecord::Base
27
39
  end
40
+ ```
28
41
 
29
- class PostSerializer < Pb::Serializer::Base
30
- message YourApp::Post
42
+ Implements a PbSerializer for the `User` message defined in `.proto`.
43
+ You need to declare the generated class and all defined fields in the PbSerializer.
31
44
 
32
- define_primary_loader :post do |subdeps, user_ids:, **|
33
- Post.where(user_id: user_ids).preload(subdeps).map { |p| new(p) }
34
- end
45
+ ```ruby
46
+ class UserPbSerializer < Pb::Serializer::Base
47
+ message ExamplesPb::User
35
48
 
36
49
  attribute :id
37
- attribute :title
38
- attribute :body
50
+ attribute :name
39
51
  end
52
+ ```
40
53
 
41
- class UserGrpcService < YourApp::UserService::Service
42
- # @param req [YourApp::GetUserRequest]
43
- # @param call [GRPC::ActiveCall::SingleReqView]
44
- # @return [YourApp::User]
45
- def get_users(req, call)
46
- UserSerializer.bulk_load_and_serialize(ids: [req.user_id], with: req.field_mask)[0]
47
- end
48
-
49
- # @param req [YourApp::ListFriendUsersRequest]
50
- # @param call [GRPC::ActiveCall::SingleReqView]
51
- # @return [YourApp::ListFriendUsersResponse]
52
- def list_friend_users(req, call)
53
- current_user = User.find(current_user_id)
54
- YourApp::ListFriendUsersResponse.new(
55
- users: UserSerializer.bulk_load_and_serialize(ids: current_user.friend_ids, with: req.field_mask)
56
- )
57
- end
58
- end
54
+ You can serialize Ruby objects to protobuf message object with the implemented PbSerializer.
55
+
56
+ ```ruby
57
+ user = User.find(123)
58
+ UserPbSerializer.new(user).to_pb
59
+ # => <ExamplesPb::User: id: 123, name: "someuser">
59
60
  ```
60
61
 
61
- More examples are available under [./spec/examples](./spec/examples).
62
+ The value of each attribute is determined from the PbSerializer instance or the object passed to the constructor.
63
+
64
+ ## Next read
62
65
 
66
+ - [Examples](./docs/examples.md)
63
67
 
64
68
  ## Installation
65
69
 
@@ -77,10 +81,6 @@ Or install it yourself as:
77
81
 
78
82
  $ gem install pb-serializer
79
83
 
80
- ## Usage
81
-
82
- TODO: Write usage instructions here
83
-
84
84
  ## Development
85
85
 
86
86
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/docs/examples.md ADDED
@@ -0,0 +1,236 @@
1
+ # Examples
2
+
3
+ ## Sub messages
4
+
5
+ ```proto
6
+ message Post {
7
+ uint64 id = 1;
8
+ string title = 2;
9
+ User author = 3;
10
+ }
11
+
12
+ message User {
13
+ uint64 id = 1;
14
+ string name = 2;
15
+ }
16
+ ```
17
+
18
+ ```ruby
19
+ # Schema: [id(integer), title(string), author_id(integer)]
20
+ class Book < ActiveRecord::Base
21
+ belongs_to :author, class_name: 'User'
22
+ end
23
+
24
+ # Schema: [id(integer), name(string)]
25
+ class User < ActiveRecord::Base
26
+ end
27
+ ```
28
+
29
+ ```ruby
30
+ class BookPbSerializer < Pb::Serializer::Base
31
+ message ExamplesPb::Book
32
+
33
+ attribute :id
34
+ attribute :title
35
+ attribute :author, serializer: UserPbSerializer
36
+ end
37
+
38
+ class UserPbSerializer < Pb::Serializer::Base
39
+ message ExamplesPb::User
40
+
41
+ attribute :id
42
+ attribute :name
43
+ end
44
+ ```
45
+
46
+ ## Enum
47
+
48
+ ```proto
49
+ message Conversation {
50
+ uint64 id = 1;
51
+ Status status = 3;
52
+
53
+ enum Status {
54
+ STATUS_UNSPECIFIED = 0;
55
+ ARCHIVED = 1;
56
+ ACTIVE = 2;
57
+ }
58
+ }
59
+ ```
60
+
61
+ ```ruby
62
+ # https://api.rubyonrails.org/classes/ActiveRecord/Enum.html
63
+
64
+ # Schema: [id(integer), status(integer)]
65
+ class Conversation < ApplicationRecord
66
+ enum status: { active: 0, archived: 1 }, _prefix: true
67
+ end
68
+ ```
69
+
70
+ ```ruby
71
+ # @!attribute [r] object
72
+ # @return [Conversation]
73
+ class ConversationPbSerializer < Pb::Serializer::Base
74
+ message ExamplesPb::Conversation
75
+
76
+ attribute :status
77
+
78
+ def status
79
+ object.status.upcase.to_sym
80
+ end
81
+ end
82
+ ```
83
+
84
+ ## Oneof
85
+
86
+ ```proto
87
+ message Entry {
88
+ oneof entry {
89
+ Message message = 1;
90
+ Comment comment = 2;
91
+ }
92
+ }
93
+
94
+ message Message {
95
+ // ...
96
+ }
97
+
98
+ message Comment {
99
+ // ...
100
+ }
101
+ ```
102
+
103
+ ```ruby
104
+ # see https://api.rubyonrails.org/classes/ActiveRecord/DelegatedType.html
105
+
106
+ class Entry < ApplicationRecord
107
+ delegated_type :entryable, types: %w[Message Comment]
108
+ end
109
+
110
+ class Message < ApplicationRecord
111
+ # ...
112
+ end
113
+
114
+ class Comment < ApplicationRecord
115
+ # ...
116
+ end
117
+ ```
118
+
119
+ ```ruby
120
+ # @!attribute [r] object
121
+ # @return [Entry]
122
+ class EntryPbSerializer < Pb::Serializer::Base
123
+ message ExamplesPb::Entry
124
+
125
+ oneof :entry do
126
+ attribute :message, if: -> { object.message? }, serializer: MessagePbSerializer
127
+ attribute :comment, if: -> { object.comment? }, serializer: CommentPbSerializer
128
+ end
129
+ end
130
+
131
+ # @!attribute [r] object
132
+ # @return [Message]
133
+ class MessagePbSerializer < Pb::Serializer::Base
134
+ message ExamplesPb::Message
135
+
136
+ # ...
137
+ end
138
+
139
+ # @!attribute [r] object
140
+ # @return [Comment]
141
+ class CommentPbSerializer < Pb::Serializer::Base
142
+ message ExamplesPb::Comment
143
+
144
+ # ...
145
+ end
146
+ ```
147
+
148
+ ## Serializable model
149
+
150
+ ```proto
151
+ message User {
152
+ uint64 id = 1;
153
+ string first_name = 2;
154
+ string last_name = 3;
155
+ }
156
+ ```
157
+
158
+ ```ruby
159
+ # Schema: [id(integer), first_name(string), last_name(string)]
160
+ class User < ActiveRecord::Base
161
+ include Pb::Serializable
162
+
163
+ message ExamplesPb::User
164
+
165
+ attribute :id
166
+ attribute :first_name
167
+ attribute :last_name
168
+ end
169
+ ```
170
+
171
+ ```ruby
172
+ User.find(123).to_pb
173
+ # => <ExamplesPb::User: id: 123, first_name: 'Masayuki', last_name: 'Izumi'>
174
+ ```
175
+
176
+ ## With FieldMask and ComputedModel
177
+
178
+ ```proto
179
+ message User {
180
+ uint64 id = 1;
181
+ string first_name = 2;
182
+ string last_name = 3;
183
+ string full_name = 4;
184
+ }
185
+ ```
186
+
187
+ ```ruby
188
+ # Schema: [id(integer), first_name(string), last_name(string)]
189
+ class RawUser < ActiveRecord::Base
190
+ self.table_name = 'users'
191
+ end
192
+
193
+ class User
194
+ include ComputedModel::Model
195
+
196
+ def initialize(raw_user)
197
+ @raw_user = user
198
+ end
199
+
200
+ def self.batch_get(ids, with:)
201
+ bulk_load_and_compute([*Array(with), :id], ids: ids)
202
+ end
203
+
204
+ define_primary_loader :raw_user do |subfields, ids:, **|
205
+ RawUser.where(id: ids).select(subfields).map { new(_1) }
206
+ end
207
+
208
+ delegate_dependency :id, :first_name, :last_name,
209
+ to: :raw_user, include_subfields: true
210
+
211
+ dependency :first_name, :last_name
212
+ computed def full_name
213
+ [first_name, last_name].compact.join(' ')
214
+ end
215
+ end
216
+ ```
217
+
218
+ ```ruby
219
+ class UserPbSerializer < Pb::Serializer::Base
220
+ message ExamplesPb::User
221
+
222
+ attribute :id
223
+ attribute :first_name
224
+ attribute :last_name
225
+ attribute :full_name
226
+ end
227
+ ```
228
+
229
+ ```ruby
230
+ # req.read_mask # => <Google::Protobuf::FieldMask: paths: ['id', 'full_name']>
231
+ mask = Pb::Serializer.parse_field_mask(req.read_mask)
232
+
233
+ user = User.batch_get([123], with: mask)[0]
234
+ UserPbSerializer.new(user).to_pb(with: mask)
235
+ # => <ExamplesPb::User: id: 123, first_name: '', last_name: '', full_name: "Masayuki Izumi">
236
+ ```
@@ -1,5 +1,6 @@
1
1
  module Pb
2
- module Serializer
2
+ module Serializable
3
+ # @private
3
4
  module ComputedModelSupport
4
5
  def self.included(base)
5
6
  base.singleton_class.prepend Hook
@@ -0,0 +1,94 @@
1
+ module Pb
2
+ module Serializable
3
+ module Dsl
4
+ # @api private
5
+ class Attribute < Struct.new(
6
+ :name,
7
+ :options,
8
+ :field_descriptor,
9
+ :oneof,
10
+ keyword_init: true,
11
+ )
12
+
13
+ ALLOWED_OPTIONS = Set[:allow_nil, :if, :serializer, :ignore].freeze
14
+
15
+ def initialize(options:, **)
16
+ super
17
+
18
+ unknown_options = options.keys.to_set - ALLOWED_OPTIONS
19
+ unless unknown_options.empty?
20
+ raise ::Pb::Serializer::InvalidAttributeOptionError, "unknown options are specified in #{name} attribute: #{unknown_options.to_a}"
21
+ end
22
+ end
23
+
24
+ # @return [Boolean]
25
+ def allow_nil?
26
+ options.fetch(:allow_nil, false)
27
+ end
28
+
29
+ # @return [Class]
30
+ def serializer_class
31
+ options[:serializer]
32
+ end
33
+
34
+ # @return [Boolean]
35
+ def repeated?
36
+ field_descriptor.label == :repeated
37
+ end
38
+
39
+ # @return [Boolean]
40
+ def serializable?(s)
41
+ return false if options[:ignore]
42
+
43
+ cond = options[:if]
44
+
45
+ return true unless cond
46
+
47
+ case cond
48
+ when String, Symbol; then s.send(cond)
49
+ when Proc; then s.instance_exec(&cond)
50
+ else raise ::Pb::Serializer::InvalidAttributeOptionError, "`if` option can accept only Symbol, String or Proc. but got #{cond.class}"
51
+ end
52
+ end
53
+
54
+ def oneof?
55
+ !oneof.nil?
56
+ end
57
+
58
+ # @param v [Object]
59
+ # @param with [Hash, Array]
60
+ def convert_to_pb(v, with: nil, should_repeat: repeated?)
61
+ return nil if v.nil?
62
+ return v.map { |i| convert_to_pb(i, should_repeat: false, with: with) } if should_repeat
63
+
64
+ case field_descriptor.type
65
+ when :message
66
+ if v.class < Google::Protobuf::MessageExts && v.class.descriptor.name == field_descriptor.submsg_name
67
+ return v
68
+ end
69
+
70
+ case field_descriptor.submsg_name
71
+ when "google.protobuf.Timestamp" then Pb.to_timestamp(v)
72
+ when "google.protobuf.StringValue" then Pb.to_strval(v)
73
+ when "google.protobuf.Int32Value" then Pb.to_int32val(v)
74
+ when "google.protobuf.Int64Value" then Pb.to_int64val(v)
75
+ when "google.protobuf.UInt32Value" then Pb.to_uint32val(v)
76
+ when "google.protobuf.UInt64Value" then Pb.to_uint64val(v)
77
+ when "google.protobuf.FloatValue" then Pb.to_floatval(v)
78
+ when "google.protobuf.DoubleValue" then Pb.to_doubleval(v)
79
+ when "google.protobuf.BoolValue" then Pb.to_boolval(v)
80
+ when "google.protobuf.BytesValue" then Pb.to_bytesval(v)
81
+ else
82
+ return serializer_class.new(v).to_pb(with: with) if serializer_class
83
+ return v.to_pb(with: with) if v.kind_of?(::Pb::Serializable)
84
+
85
+ raise "serializer was not found for #{field_descriptor.submsg_name}"
86
+ end
87
+ else
88
+ v.nil? ? field_descriptor.default : v
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,18 @@
1
+ module Pb
2
+ module Serializable
3
+ module Dsl
4
+ # @api private
5
+ class Oneof < Struct.new(
6
+ :name,
7
+ :allow_nil,
8
+ :attributes,
9
+ keyword_init: true,
10
+ )
11
+ # @return [Boolean]
12
+ def allow_nil?
13
+ allow_nil
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,75 @@
1
+ require 'pb/serializable/dsl/attribute'
2
+ require 'pb/serializable/dsl/oneof'
3
+
4
+ module Pb
5
+ module Serializable
6
+ module Dsl
7
+ # @param klass [Class] Protobuf message class
8
+ # @return [void]
9
+ def message(klass)
10
+ self.__pb_serializer_message_class = klass
11
+ end
12
+
13
+ # @param name [Symbol] An attribute name
14
+ # @param opts [Hash] options
15
+ # @option opts [Boolean] :allow_nil Set true if this attribute allow to be nil
16
+ # @option opts [Class] :serializer A serializer class for this attribute
17
+ # @option opts [String, Symbol, Proc] :if A method, proc or string to call to determine to serialize this field
18
+ # @return [void]
19
+ # @raise [Pb::Serializer::MissingMessageTypeError] if this class has not been called {#message}
20
+ # @raise [Pb::Serializer::UnknownFieldError] if the field does not defined in .proto
21
+ # @raise [Pb::Serializer::InvalidAttributeOptionError] if unknown options are passed
22
+ def attribute(name, opts = {})
23
+ raise ::Pb::Serializer::MissingMessageTypeError, "message specificaiton is missed" unless __pb_serializer_message_class
24
+
25
+ fd = __pb_serializer_message_class.descriptor.find { |fd| fd.name.to_sym == name }
26
+
27
+ raise ::Pb::Serializer::UnknownFieldError, "#{name} is not defined in #{ __pb_serializer_message_class.name}" unless fd
28
+
29
+ attr = Attribute.new(
30
+ name: name,
31
+ options: opts,
32
+ field_descriptor: fd,
33
+ oneof: @current_oneof&.name,
34
+ )
35
+
36
+ __pb_serializer_attr_by_name[name] = attr
37
+
38
+ unless method_defined?(attr.name)
39
+ define_method attr.name do
40
+ primary_object.public_send(attr.name)
41
+ end
42
+ end
43
+ end
44
+
45
+ # @param names [Array<Symbol>] Attribute names to be ignored
46
+ # @return [void]
47
+ # @example Ignore attributes
48
+ # ignore :deprecated_field, :not_implemented_field
49
+ def ignore(*names)
50
+ names.each do |name|
51
+ attribute name, ignore: true
52
+ end
53
+ end
54
+
55
+ # @param name [Symbol] An oneof attribute name
56
+ # @param allow_nil [Boolean] Set true if this oneof attribute allow to be nil
57
+ # @return [void]
58
+ # @example Define oneof attributes
59
+ # oneof :test_oneof do
60
+ # attribute :name
61
+ # attribute :sub_message
62
+ # end
63
+ def oneof(name, allow_nil: false)
64
+ @current_oneof = Oneof.new(
65
+ name: name,
66
+ allow_nil: allow_nil,
67
+ attributes: [],
68
+ )
69
+ yield
70
+ __pb_serializer_oneof_by_name[name] = @current_oneof
71
+ @current_oneof = nil
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,30 +1,39 @@
1
+ require "pb/serializable/computed_model_support"
2
+ require "pb/serializable/dsl"
3
+
1
4
  module Pb
2
5
  module Serializable
3
6
  extend ActiveSupport::Concern
4
7
  include ComputedModel::Model
5
8
 
9
+ # @!parse extend Dsl
10
+ # @!parse extend ClassMethods
11
+
6
12
  def self.included(base)
7
- base.include Pb::Serializer::ComputedModelSupport
8
- base.extend Pb::Serializer::Dsl
13
+ base.include ComputedModelSupport
14
+ base.extend Dsl
9
15
  end
10
16
 
11
17
  # @param with [
12
18
  # Google::Protobuf::FieldMask,
13
19
  # Array<(Symbol, Hash)>,
14
- # Hash{Symbol=>(Array,Symbol,Hash)},
15
- # ]
20
+ # Hash{Symbol=>(Array,Symbol,Hash,Proc)},
21
+ # ]
22
+ # Specifies the list of fields to be serialized in the Proto message object.
23
+ # `nil` means that all fields defined in .proto will be serialized.
24
+ # @return [Object] a protobuf message object
16
25
  def to_pb(with: nil)
17
- with ||= ::Pb::Serializer.build_default_mask(self.class.message_class.descriptor)
26
+ with ||= ::Pb::Serializer.build_default_mask(self.class.__pb_serializer_message_class.descriptor)
18
27
  with = ::Pb::Serializer.normalize_mask(with)
19
28
 
20
29
  oneof_set = []
21
30
 
22
- o = self.class.message_class.new
23
- self.class.message_class.descriptor.each do |fd|
24
- attr = self.class.find_attribute_by_field_descriptor(fd)
31
+ o = self.class.__pb_serializer_message_class.new
32
+ self.class.__pb_serializer_message_class.descriptor.each do |fd|
33
+ attr = self.class.__pb_serializer_attr_by_field_descriptor(fd)
25
34
 
26
35
  unless attr
27
- msg = "#{self.class.message_class.name}.#{fd.name} is missed in #{self.class.name}"
36
+ msg = "#{self.class.__pb_serializer_message_class.name}.#{fd.name} is missed in #{self.class.name}"
28
37
 
29
38
  case Pb::Serializer.configuration.missing_field_behavior
30
39
  when :raise then raise ::Pb::Serializer::MissingFieldError, msg
@@ -62,7 +71,7 @@ module Pb
62
71
  end
63
72
  end
64
73
 
65
- self.class.oneofs.each do |oneof|
74
+ self.class.__pb_serializer_oneof_by_name.values.each do |oneof|
66
75
  next if oneof_set.include?(oneof.name)
67
76
  next if oneof.allow_nil?
68
77
  raise ::Pb::Serializer::ValidationError, "#{primary_object.class.name}##{oneof.name} is required"
@@ -79,7 +88,7 @@ module Pb
79
88
  end
80
89
 
81
90
  def bulk_load(with: nil, **args)
82
- with ||= ::Pb::Serializer.build_default_mask(message_class.descriptor)
91
+ with ||= ::Pb::Serializer.build_default_mask(__pb_serializer_message_class.descriptor)
83
92
  with = ::Pb::Serializer.normalize_mask(with)
84
93
  with = __pb_serializer_filter_only_computed_model_attrs(with)
85
94
 
@@ -92,6 +101,28 @@ module Pb
92
101
 
93
102
  bulk_load_and_compute(with, **args)
94
103
  end
104
+
105
+ # @api private
106
+ attr_accessor :__pb_serializer_message_class
107
+
108
+ # @api private
109
+ # @return [Hash{Symbol=>::Pb::Serializer::Attribute}]
110
+ def __pb_serializer_attr_by_name
111
+ @__pb_serializer_attr_by_name ||= {}
112
+ end
113
+
114
+ # @api private
115
+ # @return [Hash{Symbol=>::Pb::Serializer::Oneof}]
116
+ def __pb_serializer_oneof_by_name
117
+ @__pb_serializer_oneof_by_name ||= {}
118
+ end
119
+
120
+ # @api private
121
+ # @param fd [Google::Protobuf::FieldDescriptor] a field descriptor
122
+ # @return [Pb::Serializer::Attribute, nil]
123
+ def __pb_serializer_attr_by_field_descriptor(fd)
124
+ __pb_serializer_attr_by_name[fd.name.to_sym]
125
+ end
95
126
  end
96
127
  end
97
128
  end
@@ -1,6 +1,8 @@
1
1
  module Pb
2
2
  module Serializer
3
3
  class Base
4
+ # @!parse include Pb::Serializable
5
+
4
6
  def self.inherited(base)
5
7
  base.include ::Pb::Serializable
6
8
  base.singleton_class.prepend Hook
@@ -12,6 +14,7 @@ module Pb
12
14
  @object = object
13
15
  end
14
16
 
17
+ # @private
15
18
  module Hook
16
19
  def define_primary_loader(name, &block)
17
20
  class_eval <<~RUBY
@@ -1,5 +1,5 @@
1
1
  module Pb
2
2
  module Serializer
3
- VERSION = "0.5.1".freeze
3
+ VERSION = "0.5.2".freeze
4
4
  end
5
5
  end
data/lib/pb/serializer.rb CHANGED
@@ -3,12 +3,8 @@ require "the_pb"
3
3
  require "computed_model"
4
4
  require "google/protobuf/field_mask_pb"
5
5
 
6
- require "pb/serializer/dsl"
7
- require "pb/serializer/computed_model_support"
8
6
  require "pb/serializable"
9
7
  require "pb/serializer/base"
10
- require "pb/serializer/attribute"
11
- require "pb/serializer/oneof"
12
8
 
13
9
  module Pb
14
10
  module Serializer
@@ -35,6 +31,7 @@ module Pb
35
31
  end
36
32
 
37
33
  # @param v [:raise, :warn, :ignore]
34
+ # @return [void]
38
35
  def missing_field_behavior=(v)
39
36
  @missing_field_behavior = v
40
37
 
@@ -51,6 +48,7 @@ module Pb
51
48
  # end
52
49
  # @yield [c]
53
50
  # @yieldparam [Configuration] config
51
+ # @return [void]
54
52
  def configure
55
53
  yield configuration
56
54
  end
@@ -65,7 +63,7 @@ module Pb
65
63
  configuration.logger
66
64
  end
67
65
 
68
- # @param [Google::Protobuf::Descriptor]
66
+ # @param descriptor [Google::Protobuf::Descriptor]
69
67
  def build_default_mask(descriptor)
70
68
  set =
71
69
  descriptor.each_with_object(Set[]) do |fd, m|
@@ -83,7 +81,7 @@ module Pb
83
81
  "google.protobuf.BoolValue" ,
84
82
  "google.protobuf.BytesValue" then m << fd.name.to_sym
85
83
  else
86
- m << { fd.name.to_sym => build_default_mask(fd.subtype) }
84
+ m << { fd.name.to_sym => -> { build_default_mask(fd.subtype) } }
87
85
  end
88
86
  else
89
87
  m << fd.name.to_sym
@@ -92,7 +90,7 @@ module Pb
92
90
  set.to_a
93
91
  end
94
92
 
95
- # @param [Google::Protobuf::FieldMask]
93
+ # @param field_mask [Google::Protobuf::FieldMask]
96
94
  # @return [Array]
97
95
  def parse_field_mask(field_mask)
98
96
  unless field_mask.kind_of?(Google::Protobuf::FieldMask)
@@ -104,22 +102,24 @@ module Pb
104
102
  end
105
103
  end
106
104
 
107
- # @param [Google::Protobuf::FieldMask, Symbol, Array<(Symbol,Hash)>, Hash{Symbol=>(Array,Symbol,Hash)}]
108
- # @return [Hash{Symbol=>(Array,Hash)}]
105
+ # @param input [Google::Protobuf::FieldMask, Symbol, Array<(Symbol,Hash)>, Hash{Symbol=>(Array,Symbol,Hash,Proc)}, Proc]
106
+ # @return [Hash{Symbol=>(Array,Hash,Proc)}]
109
107
  def normalize_mask(input)
110
108
  if input.kind_of?(Google::Protobuf::FieldMask)
111
109
  input = parse_field_mask(input)
112
110
  end
113
111
 
114
- normalized = {}
115
-
112
+ input = input.call if input.kind_of?(Proc)
116
113
  input = [input] if input.kind_of?(Hash)
114
+
115
+ normalized = {}
117
116
  Array(input).each do |el|
118
117
  case el
119
118
  when Symbol
120
119
  normalized[el] ||= []
121
120
  when Hash
122
121
  el.each do |k, v|
122
+ v = v.call if v.kind_of?(Proc)
123
123
  v = [v] if v.kind_of?(Hash)
124
124
  normalized[k] ||= []
125
125
  normalized[k].push(*Array(v))
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = "Serialize objects into Protocol Buffers messages"
12
12
  spec.description = spec.summary
13
- spec.homepage = "https://github.com/izumin5210/pb-serializer"
13
+ spec.homepage = "https://github.com/wantedly/pb-serializer"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.metadata["homepage_uri"] = spec.homepage
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_development_dependency "activerecord", rails_versions
35
35
  spec.add_development_dependency "bundler", "~> 2.0"
36
36
  spec.add_development_dependency "onkcop", "~> 0.53"
37
- spec.add_development_dependency "rake", "~> 10.0"
37
+ spec.add_development_dependency "rake", "~> 13.0"
38
38
  spec.add_development_dependency "rspec", "~> 3.0"
39
39
  spec.add_development_dependency "rubocop", "0.67.2" # for onkcop
40
40
  spec.add_development_dependency "sqlite3", "~> 1.4"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pb-serializer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - izumin5210
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-07-09 00:00:00.000000000 Z
11
+ date: 2023-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-protobuf
@@ -106,14 +106,14 @@ dependencies:
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '10.0'
109
+ version: '13.0'
110
110
  type: :development
111
111
  prerelease: false
112
112
  version_requirements: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '10.0'
116
+ version: '13.0'
117
117
  - !ruby/object:Gem::Dependency
118
118
  name: rspec
119
119
  requirement: !ruby/object:Gem::Requirement
@@ -197,31 +197,34 @@ files:
197
197
  - ".rspec"
198
198
  - ".rubocop.yml"
199
199
  - ".rubocop_todo.yml"
200
+ - ".yardopts"
200
201
  - CHANGELOG.md
201
202
  - CODE_OF_CONDUCT.md
202
203
  - Gemfile
203
204
  - LICENSE.txt
205
+ - README.ja.md
204
206
  - README.md
205
207
  - Rakefile
206
208
  - bin/console
207
209
  - bin/setup
208
210
  - codecov.yml
211
+ - docs/examples.md
209
212
  - lib/pb/serializable.rb
213
+ - lib/pb/serializable/computed_model_support.rb
214
+ - lib/pb/serializable/dsl.rb
215
+ - lib/pb/serializable/dsl/attribute.rb
216
+ - lib/pb/serializable/dsl/oneof.rb
210
217
  - lib/pb/serializer.rb
211
- - lib/pb/serializer/attribute.rb
212
218
  - lib/pb/serializer/base.rb
213
- - lib/pb/serializer/computed_model_support.rb
214
- - lib/pb/serializer/dsl.rb
215
- - lib/pb/serializer/oneof.rb
216
219
  - lib/pb/serializer/version.rb
217
220
  - pb-serializer.gemspec
218
- homepage: https://github.com/izumin5210/pb-serializer
221
+ homepage: https://github.com/wantedly/pb-serializer
219
222
  licenses:
220
223
  - MIT
221
224
  metadata:
222
- homepage_uri: https://github.com/izumin5210/pb-serializer
223
- source_code_uri: https://github.com/izumin5210/pb-serializer
224
- changelog_uri: https://github.com/izumin5210/pb-serializer
225
+ homepage_uri: https://github.com/wantedly/pb-serializer
226
+ source_code_uri: https://github.com/wantedly/pb-serializer
227
+ changelog_uri: https://github.com/wantedly/pb-serializer
225
228
  post_install_message:
226
229
  rdoc_options: []
227
230
  require_paths:
@@ -237,7 +240,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
237
240
  - !ruby/object:Gem::Version
238
241
  version: '0'
239
242
  requirements: []
240
- rubygems_version: 3.2.3
243
+ rubygems_version: 3.2.33
241
244
  signing_key:
242
245
  specification_version: 4
243
246
  summary: Serialize objects into Protocol Buffers messages
@@ -1,91 +0,0 @@
1
- module Pb
2
- module Serializer
3
- class Attribute < Struct.new(
4
- :name,
5
- :options,
6
- :field_descriptor,
7
- :oneof,
8
- keyword_init: true,
9
- )
10
-
11
- ALLOWED_OPTIONS = Set[:allow_nil, :if, :serializer, :ignore].freeze
12
-
13
- def initialize(options:, **)
14
- super
15
-
16
- unknown_options = options.keys.to_set - ALLOWED_OPTIONS
17
- unless unknown_options.empty?
18
- raise InvalidAttributeOptionError, "unknown options are specified in #{name} attribute: #{unknown_options.to_a}"
19
- end
20
- end
21
-
22
- # @return [Boolean]
23
- def allow_nil?
24
- options.fetch(:allow_nil, false)
25
- end
26
-
27
- # @return [Class]
28
- def serializer_class
29
- options[:serializer]
30
- end
31
-
32
- # @return [Boolean]
33
- def repeated?
34
- field_descriptor.label == :repeated
35
- end
36
-
37
- # @return [Boolean]
38
- def serializable?(s)
39
- return false if options[:ignore]
40
-
41
- cond = options[:if]
42
-
43
- return true unless cond
44
-
45
- case cond
46
- when String, Symbol; then s.send(cond)
47
- when Proc; then s.instance_exec(&cond)
48
- else raise InvalidAttributeOptionError, "`if` option can accept only Symbol, String or Proc. but got #{cond.class}"
49
- end
50
- end
51
-
52
- def oneof?
53
- !oneof.nil?
54
- end
55
-
56
- # @param v [Object]
57
- # @param with [Hash, Array]
58
- def convert_to_pb(v, with: nil, should_repeat: repeated?)
59
- return nil if v.nil?
60
- return v.map { |i| convert_to_pb(i, should_repeat: false, with: with) } if should_repeat
61
-
62
- case field_descriptor.type
63
- when :message
64
- if v.class < Google::Protobuf::MessageExts && v.class.descriptor.name == field_descriptor.submsg_name
65
- return v
66
- end
67
-
68
- case field_descriptor.submsg_name
69
- when "google.protobuf.Timestamp" then Pb.to_timestamp(v)
70
- when "google.protobuf.StringValue" then Pb.to_strval(v)
71
- when "google.protobuf.Int32Value" then Pb.to_int32val(v)
72
- when "google.protobuf.Int64Value" then Pb.to_int64val(v)
73
- when "google.protobuf.UInt32Value" then Pb.to_uint32val(v)
74
- when "google.protobuf.UInt64Value" then Pb.to_uint64val(v)
75
- when "google.protobuf.FloatValue" then Pb.to_floatval(v)
76
- when "google.protobuf.DoubleValue" then Pb.to_doubleval(v)
77
- when "google.protobuf.BoolValue" then Pb.to_boolval(v)
78
- when "google.protobuf.BytesValue" then Pb.to_bytesval(v)
79
- else
80
- return serializer_class.new(v).to_pb(with: with) if serializer_class
81
- return v.to_pb(with: with) if v.kind_of?(::Pb::Serializable)
82
-
83
- raise "serializer was not found for #{field_descriptor.submsg_name}"
84
- end
85
- else
86
- v.nil? ? field_descriptor.default : v
87
- end
88
- end
89
- end
90
- end
91
- end
@@ -1,69 +0,0 @@
1
- module Pb
2
- module Serializer
3
- module Dsl
4
- def message(klass)
5
- @message_class = klass
6
- end
7
-
8
- # @param name [Symbol] An attribute name
9
- # @param [Hash] opts options
10
- # @option opts [Boolean] :allow_nil Set true if this attribute allow to be nil
11
- # @option opts [Class] :serializer A serializer class for this attribute
12
- # @option opts [String, Symbol, Proc] :if A method, proc or string to call to determine to serialize this field
13
- def attribute(name, opts = {})
14
- raise ::Pb::Serializer::MissingMessageTypeError, "message specificaiton is missed" unless message_class
15
-
16
- fd = message_class.descriptor.find { |fd| fd.name.to_sym == name }
17
-
18
- raise ::Pb::Serializer::UnknownFieldError, "#{name} is not defined in #{message_class.name}" unless fd
19
-
20
- attr = ::Pb::Serializer::Attribute.new(
21
- name: name,
22
- options: opts,
23
- field_descriptor: fd,
24
- oneof: @current_oneof&.name,
25
- )
26
-
27
- @attr_by_name ||= {}
28
- @attr_by_name[name] = attr
29
-
30
- unless method_defined?(attr.name)
31
- define_method attr.name do
32
- primary_object.public_send(attr.name)
33
- end
34
- end
35
- end
36
-
37
- # @param names [Array<Symbol>] Attribute names to be ignored
38
- def ignore(*names)
39
- names.each do |name|
40
- attribute name, ignore: true
41
- end
42
- end
43
-
44
- def oneof(name, allow_nil: false)
45
- @oneof_by_name ||= {}
46
- @current_oneof = ::Pb::Serializer::Oneof.new(
47
- name: name,
48
- allow_nil: allow_nil,
49
- attributes: [],
50
- )
51
- yield
52
- @oneof_by_name[name] = @current_oneof
53
- @current_oneof = nil
54
- end
55
-
56
- attr_reader :message_class
57
-
58
- # @param fd [Google::Protobuf::FieldDescriptor] a field descriptor
59
- # @return [Pb::Serializer::Attribute, nil]
60
- def find_attribute_by_field_descriptor(fd)
61
- (@attr_by_name || {})[fd.name.to_sym]
62
- end
63
-
64
- def oneofs
65
- @oneof_by_name&.values || []
66
- end
67
- end
68
- end
69
- end
@@ -1,15 +0,0 @@
1
- module Pb
2
- module Serializer
3
- class Oneof < Struct.new(
4
- :name,
5
- :allow_nil,
6
- :attributes,
7
- keyword_init: true,
8
- )
9
- # @return [Boolean]
10
- def allow_nil?
11
- allow_nil
12
- end
13
- end
14
- end
15
- end