pb-serializer 0.5.1 → 0.5.2

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: 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