pb-serializer 0.5.0 → 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: b46a686595d5e9e919c21ff7e6902cec71be97d716085dc38b8b2ef0192e7cf1
4
- data.tar.gz: c453986c407e5e567bce43f3f45429697c552dedc44356ffee2a8e0b4d5eaea7
3
+ metadata.gz: b06e7d595d2212e0d8c6d36bc652acccb3020094b6370c9494476eb0de483487
4
+ data.tar.gz: 13efc77cdd5a595a0763b16989f7064a5c1f79ebd3bdff7330964eadd7569db7
5
5
  SHA512:
6
- metadata.gz: c58c5ab0714bd7bee52f3f30ede95b676b849cdf0be482a9de0187287d3ecfd0e758b6cc7c17831965d7e8e54fc13763c6290607330077c3a2135c6fe9cfd439
7
- data.tar.gz: 7b3cc2224f7550c72714cc21c1e656182891788b1f0aa67b02dce177ea44cb6dfef5115073aeb08eb130fe40decc15551e73bbb2aa6ec77c0fc997e8fbf3477f
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,17 @@
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
+
7
+ ## 0.5.1
8
+
9
+ - Improving interoperability with `computed_model`
10
+ - Simplify field mask normalizer and add `Pb::Serializer.parse_field_mask` method https://github.com/wantedly/pb-serializer/pull/40
11
+ - Stop defining accessor methods in `attribute` DSL if the method of the same name already existss https://github.com/wantedly/pb-serializer/pull/42
12
+ - Refactoring
13
+ - Extract Dsl and Hook from Serializable module https://github.com/wantedly/pb-serializer/pull/41
14
+
3
15
  ## 0.5.0
4
16
 
5
17
  - Bump `computed_model` from 0.2.2 to 0.3.0 https://github.com/wantedly/pb-serializer/pull/38
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
+ ```
@@ -0,0 +1,52 @@
1
+ module Pb
2
+ module Serializable
3
+ # @private
4
+ module ComputedModelSupport
5
+ def self.included(base)
6
+ base.singleton_class.prepend Hook
7
+ end
8
+
9
+ private def primary_object
10
+ primary_object_name = self.class.__pb_serializer_primary_model_name
11
+ if primary_object_name
12
+ send(primary_object_name)
13
+ elsif kind_of?(Serializer::Base)
14
+ send(:object)
15
+ else
16
+ self
17
+ end
18
+ end
19
+
20
+ module Hook
21
+ attr_accessor :__pb_serializer_primary_model_name
22
+
23
+ def define_primary_loader(name)
24
+ self.__pb_serializer_primary_model_name = name
25
+
26
+ super
27
+ end
28
+
29
+ def computed(name)
30
+ __pb_serializer_attrs << name
31
+
32
+ super
33
+ end
34
+
35
+ def define_loader(name, **)
36
+ __pb_serializer_attrs << name
37
+
38
+ super
39
+ end
40
+
41
+ # @param with [Array]
42
+ private def __pb_serializer_filter_only_computed_model_attrs(with)
43
+ with.reject { |c| (__pb_serializer_attrs & (c.kind_of?(Hash) ? c.keys : [c])).empty? }
44
+ end
45
+
46
+ private def __pb_serializer_attrs
47
+ @__pb_serializer_attrs ||= Set.new
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -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
8
+
9
+ # @!parse extend Dsl
10
+ # @!parse extend ClassMethods
11
+
5
12
  def self.included(base)
6
- base.extend ClassMethods
7
- base.singleton_class.prepend Hook
13
+ base.include ComputedModelSupport
14
+ base.extend Dsl
8
15
  end
9
16
 
10
17
  # @param with [
11
18
  # Google::Protobuf::FieldMask,
12
19
  # Array<(Symbol, Hash)>,
13
- # Hash{Symbol=>(Array,Symbol,Hash)},
14
- # Pb::Serializer::NormalizedMask
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)
18
- with = ::Pb::Serializer::NormalizedMask.build(with)
26
+ with ||= ::Pb::Serializer.build_default_mask(self.class.__pb_serializer_message_class.descriptor)
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"
@@ -71,79 +80,7 @@ module Pb
71
80
  o
72
81
  end
73
82
 
74
- private def primary_object
75
- primary_object_name = self.class.__pb_serializer_primary_model_name
76
- if primary_object_name
77
- send(primary_object_name)
78
- elsif kind_of?(Serializer::Base)
79
- send(:object)
80
- else
81
- self
82
- end
83
- end
84
-
85
- module Hook
86
- def define_primary_loader(name)
87
- self.__pb_serializer_primary_model_name = name
88
-
89
- super
90
- end
91
-
92
- def computed(name)
93
- __pb_serializer_attrs << name
94
-
95
- super
96
- end
97
-
98
- def define_loader(name, **)
99
- __pb_serializer_attrs << name
100
-
101
- super
102
- end
103
- end
104
-
105
83
  module ClassMethods
106
- attr_reader :message_class
107
- attr_accessor :__pb_serializer_primary_model_name
108
-
109
- def message(klass)
110
- @message_class = klass
111
- end
112
-
113
- # @param name [Symbol] An attribute name
114
- # @param [Hash] opts options
115
- # @option opts [Boolean] :allow_nil Set true if this attribute allow to be nil
116
- # @option opts [Class] :serializer A serializer class for this attribute
117
- # @option opts [String, Symbol, Proc] :if A method, proc or string to call to determine to serialize this field
118
- def attribute(name, opts = {})
119
- raise ::Pb::Serializer::MissingMessageTypeError, "message specificaiton is missed" unless message_class
120
-
121
- fd = message_class.descriptor.find { |fd| fd.name.to_sym == name }
122
-
123
- raise ::Pb::Serializer::UnknownFieldError, "#{name} is not defined in #{message_class.name}" unless fd
124
-
125
- attr = ::Pb::Serializer::Attribute.new(
126
- name: name,
127
- options: opts,
128
- field_descriptor: fd,
129
- oneof: @current_oneof&.name,
130
- )
131
-
132
- @attr_by_name ||= {}
133
- @attr_by_name[name] = attr
134
-
135
- define_method attr.name do
136
- primary_object.public_send(attr.name)
137
- end
138
- end
139
-
140
- # @param names [Array<Symbol>] Attribute names to be ignored
141
- def ignore(*names)
142
- names.each do |name|
143
- attribute name, ignore: true
144
- end
145
- end
146
-
147
84
  # @param with [Array, Hash, Google::Protobuf::FieldMask, nil]
148
85
  # @return [Array]
149
86
  def bulk_load_and_serialize(with: nil, **args)
@@ -151,9 +88,9 @@ module Pb
151
88
  end
152
89
 
153
90
  def bulk_load(with: nil, **args)
154
- with ||= ::Pb::Serializer.build_default_mask(message_class.descriptor)
155
- with = ::Pb::Serializer::NormalizedMask.build(with)
156
- with = with.reject { |c| (__pb_serializer_attrs & (c.kind_of?(Hash) ? c.keys : [c])).empty? }
91
+ with ||= ::Pb::Serializer.build_default_mask(__pb_serializer_message_class.descriptor)
92
+ with = ::Pb::Serializer.normalize_mask(with)
93
+ with = __pb_serializer_filter_only_computed_model_attrs(with)
157
94
 
158
95
  primary_object_name = __pb_serializer_primary_model_name
159
96
  if primary_object_name
@@ -165,30 +102,26 @@ module Pb
165
102
  bulk_load_and_compute(with, **args)
166
103
  end
167
104
 
168
- def oneof(name, allow_nil: false)
169
- @oneof_by_name ||= {}
170
- @current_oneof = ::Pb::Serializer::Oneof.new(
171
- name: name,
172
- allow_nil: allow_nil,
173
- attributes: [],
174
- )
175
- yield
176
- @oneof_by_name[name] = @current_oneof
177
- @current_oneof = nil
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 ||= {}
178
112
  end
179
113
 
180
- private def __pb_serializer_attrs
181
- @__pb_serializer_attrs ||= Set.new
114
+ # @api private
115
+ # @return [Hash{Symbol=>::Pb::Serializer::Oneof}]
116
+ def __pb_serializer_oneof_by_name
117
+ @__pb_serializer_oneof_by_name ||= {}
182
118
  end
183
119
 
120
+ # @api private
184
121
  # @param fd [Google::Protobuf::FieldDescriptor] a field descriptor
185
122
  # @return [Pb::Serializer::Attribute, nil]
186
- def find_attribute_by_field_descriptor(fd)
187
- (@attr_by_name || {})[fd.name.to_sym]
188
- end
189
-
190
- def oneofs
191
- @oneof_by_name&.values || []
123
+ def __pb_serializer_attr_by_field_descriptor(fd)
124
+ __pb_serializer_attr_by_name[fd.name.to_sym]
192
125
  end
193
126
  end
194
127
  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.0".freeze
3
+ VERSION = "0.5.2".freeze
4
4
  end
5
5
  end
data/lib/pb/serializer.rb CHANGED
@@ -4,10 +4,7 @@ require "computed_model"
4
4
  require "google/protobuf/field_mask_pb"
5
5
 
6
6
  require "pb/serializable"
7
- require "pb/serializer/normalized_mask"
8
7
  require "pb/serializer/base"
9
- require "pb/serializer/attribute"
10
- require "pb/serializer/oneof"
11
8
 
12
9
  module Pb
13
10
  module Serializer
@@ -34,6 +31,7 @@ module Pb
34
31
  end
35
32
 
36
33
  # @param v [:raise, :warn, :ignore]
34
+ # @return [void]
37
35
  def missing_field_behavior=(v)
38
36
  @missing_field_behavior = v
39
37
 
@@ -50,6 +48,7 @@ module Pb
50
48
  # end
51
49
  # @yield [c]
52
50
  # @yieldparam [Configuration] config
51
+ # @return [void]
53
52
  def configure
54
53
  yield configuration
55
54
  end
@@ -64,7 +63,7 @@ module Pb
64
63
  configuration.logger
65
64
  end
66
65
 
67
- # @param [Google::Protobuf::Descriptor]
66
+ # @param descriptor [Google::Protobuf::Descriptor]
68
67
  def build_default_mask(descriptor)
69
68
  set =
70
69
  descriptor.each_with_object(Set[]) do |fd, m|
@@ -82,7 +81,7 @@ module Pb
82
81
  "google.protobuf.BoolValue" ,
83
82
  "google.protobuf.BytesValue" then m << fd.name.to_sym
84
83
  else
85
- m << { fd.name.to_sym => build_default_mask(fd.subtype) }
84
+ m << { fd.name.to_sym => -> { build_default_mask(fd.subtype) } }
86
85
  end
87
86
  else
88
87
  m << fd.name.to_sym
@@ -90,6 +89,48 @@ module Pb
90
89
  end
91
90
  set.to_a
92
91
  end
92
+
93
+ # @param field_mask [Google::Protobuf::FieldMask]
94
+ # @return [Array]
95
+ def parse_field_mask(field_mask)
96
+ unless field_mask.kind_of?(Google::Protobuf::FieldMask)
97
+ raise ArgumentError, "expected Google::Protobuf::FieldMask, but got #{field_mask.class}"
98
+ end
99
+
100
+ field_mask.paths.map do |path|
101
+ path.split(".").reverse.inject(nil) { |h, key| h.nil? ? key.to_sym : { key.to_sym => [h].compact } }
102
+ end
103
+ end
104
+
105
+ # @param input [Google::Protobuf::FieldMask, Symbol, Array<(Symbol,Hash)>, Hash{Symbol=>(Array,Symbol,Hash,Proc)}, Proc]
106
+ # @return [Hash{Symbol=>(Array,Hash,Proc)}]
107
+ def normalize_mask(input)
108
+ if input.kind_of?(Google::Protobuf::FieldMask)
109
+ input = parse_field_mask(input)
110
+ end
111
+
112
+ input = input.call if input.kind_of?(Proc)
113
+ input = [input] if input.kind_of?(Hash)
114
+
115
+ normalized = {}
116
+ Array(input).each do |el|
117
+ case el
118
+ when Symbol
119
+ normalized[el] ||= []
120
+ when Hash
121
+ el.each do |k, v|
122
+ v = v.call if v.kind_of?(Proc)
123
+ v = [v] if v.kind_of?(Hash)
124
+ normalized[k] ||= []
125
+ normalized[k].push(*Array(v))
126
+ end
127
+ else
128
+ raise "not supported field mask type: #{input.class}"
129
+ end
130
+ end
131
+
132
+ normalized
133
+ end
93
134
  end
94
135
  end
95
136
  end
@@ -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.0
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-07 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,30 +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/normalized_mask.rb
214
- - lib/pb/serializer/oneof.rb
215
219
  - lib/pb/serializer/version.rb
216
220
  - pb-serializer.gemspec
217
- homepage: https://github.com/izumin5210/pb-serializer
221
+ homepage: https://github.com/wantedly/pb-serializer
218
222
  licenses:
219
223
  - MIT
220
224
  metadata:
221
- homepage_uri: https://github.com/izumin5210/pb-serializer
222
- source_code_uri: https://github.com/izumin5210/pb-serializer
223
- 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
224
228
  post_install_message:
225
229
  rdoc_options: []
226
230
  require_paths:
@@ -236,7 +240,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
236
240
  - !ruby/object:Gem::Version
237
241
  version: '0'
238
242
  requirements: []
239
- rubygems_version: 3.2.3
243
+ rubygems_version: 3.2.33
240
244
  signing_key:
241
245
  specification_version: 4
242
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 [Pb::Serializer::NormalizedMask]
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,57 +0,0 @@
1
- module Pb::Serializer
2
- class NormalizedMask < ::Hash
3
- class << self
4
- # @param [Google::Protobuf::FieldMask, Symbol, Array<(Symbol,Hash)>, Hash{Symbol=>(Array,Symbol,Hash)}]
5
- # @return [Hash{Symbol=>Hash}]
6
- def build(input)
7
- return input if input.kind_of? self
8
-
9
- normalized = new
10
-
11
- case input
12
- when Google::Protobuf::FieldMask
13
- normalized = normalize_mask_paths(input.paths)
14
- when Array
15
- input.each do |v|
16
- deep_merge!(normalized, build(v))
17
- end
18
- when Hash
19
- input.each do |k, v|
20
- normalized[k] ||= new
21
- deep_merge!(normalized[k], build(v))
22
- end
23
- when Symbol
24
- normalized[input] ||= new
25
- else
26
- raise "not supported field mask type: #{input.class}"
27
- end
28
-
29
- normalized
30
- end
31
-
32
- private
33
-
34
- # @param [Array<String>]
35
- # @return [Hash{Symbol=>Hash}]
36
- def normalize_mask_paths(paths)
37
- paths_by_key = {}
38
-
39
- paths.each do |path|
40
- key, rest = path.split('.', 2)
41
- paths_by_key[key.to_sym] ||= []
42
- paths_by_key[key.to_sym].push(rest) if rest && !rest.empty?
43
- end
44
-
45
- paths_by_key.keys.each_with_object(new) do |key, normalized|
46
- normalized[key] = normalize_mask_paths(paths_by_key[key])
47
- end
48
- end
49
-
50
- def deep_merge!(h1, h2)
51
- h1.merge!(h2) do |_k, v1, v2|
52
- deep_merge!(v1, v2)
53
- end
54
- end
55
- end
56
- end
57
- 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