protobuf-mongoid 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1dc8ea63ac8607eb6435e75b8638ff4cf5735c8a0f4d2fdd6df9d72cf2c78c4b
4
+ data.tar.gz: d6769b7aeaf095e6aa1b68444feea2c073e528b588f64dfc42a7cd2b57c14db2
5
+ SHA512:
6
+ metadata.gz: b2edf7540ddebb909650e1741fd09b85d5fe517f1768364ffebd04f58929dcbcc2ceef2888f0ae4306a7ef892df87cbf9159ae98e44a1f6f66f0f0808ac9e3bb
7
+ data.tar.gz: af66b0131bb5e401620b5526ee21831f21c854a33c964c7bcfa64dc9875409a490fe9e22cf7cee0247ee5f0b3affbadec9e8e54f68b797bf3a573baeebe9a15a
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Luilver Garces
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # protobuf-mongoid
2
+
3
+ protobuf-mongoid is a Ruby gem that integrates Protocol Buffers with Mongoid, allowing for efficient serialization and deserialization of Mongoid documents using Protocol Buffers.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'protobuf-mongoid'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```
16
+ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```
22
+ gem install protobuf-mongoid
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ To use protobuf-mongoid, include the necessary modules in your Mongoid models. Here is a basic example:
28
+
29
+ ```ruby
30
+ class User
31
+ include Mongoid::Document
32
+ include Protobuf::Mongoid
33
+
34
+ field :name, type: String
35
+ field :email, type: String
36
+
37
+ # Define your Protocol Buffers message here
38
+ end
39
+ ```
40
+
41
+ ### Tests
42
+
43
+ To test protobuf-mongoid, run:
44
+
45
+ ```bash
46
+ rake
47
+ ```
48
+
49
+ ## Contributing
50
+
51
+ 1. Fork it ( https://github.com/luilver/protobuf-mongoid/fork )
52
+ 2. Create your feature branch (git checkout -b feature/my-new-feature)
53
+ 3. Commit your changes (git commit -am 'Add some feature')
54
+ 4. Push to the branch (git push origin feature/my-new-feature)
55
+ 5. Create a new Pull Request
56
+
57
+ ## License
58
+
59
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE.txt) file for details.
@@ -0,0 +1,33 @@
1
+ module Protobuf
2
+ module Mongoid
3
+ module AttributeMethods
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def alias_field(field_alias, attribute)
8
+ alias_attribute field_alias, attribute
9
+
10
+ attribute_from_proto attribute, fetch_attribute_alias_from_proto(attribute, field_alias)
11
+ field_from_document field_alias, fetch_field_alias_from_document(attribute, field_alias)
12
+ end
13
+
14
+ def fetch_attribute_alias_from_proto(attribute, field_alias)
15
+ lambda do |proto|
16
+ value = proto.__send__(:"#{field_alias}!")
17
+ value ||= proto.__send__(:"#{attribute}!") if proto.respond_to?(attribute)
18
+
19
+ self._protobuf_convert_fields_to_attributes(attribute, value)
20
+ end
21
+ end
22
+
23
+ def fetch_field_alias_from_document(attribute, _field_aliasd)
24
+ lambda do |document|
25
+ value = document.__send__(field_alias)
26
+
27
+ self._protobuf_convert_attributes_to_fields(attribute, value)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,50 @@
1
+ module Protobuf
2
+ module Mongoid
3
+ # = Protobuf Mongoid errors
4
+ #
5
+ # Generic Protobuf Mongoid exception class
6
+ class ProtobufMongoidError < StandardError
7
+ end
8
+
9
+ # Raised by `attribute_from_proto` when the transformer method
10
+ # given is not callable.
11
+ class AttributeTransformerError < ProtobufMongoidError
12
+ def message
13
+ "Attribute transformers must be called with a callable or block!"
14
+ end
15
+ end
16
+
17
+ # Raised by `field_from_document` when the convert method
18
+ # given not callable.
19
+ class FieldTransformerError < ProtobufMongoidError
20
+ def message
21
+ "Field transformers must be called with a callable or block!"
22
+ end
23
+ end
24
+
25
+ # Raised by `to_proto` when no protobuf message is defined.
26
+ class MessageNotDefined < ProtobufMongoidError
27
+ attr_reader :class_name
28
+
29
+ def initialize(klass)
30
+ @class_name = klass.name
31
+ end
32
+
33
+ def message
34
+ "#{class_name} does not define a protobuf message"
35
+ end
36
+ end
37
+
38
+ # Raised by `field_scope` when given scope is not defined.
39
+ class SearchScopeError < ProtobufMongoidError
40
+ end
41
+
42
+ # Raised by `upsert_scope` when a given scope is not defined
43
+ class UpsertScopeError < ProtobufMongoidError
44
+ end
45
+
46
+ # Raised by `for_upsert` when no valid upsert_scopes are found
47
+ class UpsertNotFoundError < ProtobufMongoidError
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protobuf
4
+ module Mongoid
5
+ # A key-value pair within a document
6
+ module Fields
7
+ include ::Heredity
8
+ extend ::ActiveSupport::Concern
9
+
10
+ FIELD_TYPE_MAP_MUTEX = ::Mutex.new
11
+ DATE_OR_TIME_TYPES = ::Set.new(%i[date datetime time timestamp])
12
+
13
+ inheritable_attributes :_protobuf_fields,
14
+ :_protobuf_field_types,
15
+ :_protobuf_date_datetime_time_or_timestamp_field,
16
+ :_protobuf_mapped_fields
17
+
18
+ ##
19
+ # Class Methods
20
+ #
21
+ module ClassMethods
22
+ def _protobuf_fields
23
+ _protobuf_map_fields unless _protobuf_mapped_fields?
24
+
25
+ @_protobuf_fields
26
+ end
27
+
28
+ def _protobuf_field_types
29
+ _protobuf_map_fields unless _protobuf_mapped_fields?
30
+
31
+ @_protobuf_field_types
32
+ end
33
+
34
+ def _protobuf_date_datetime_time_or_timestamp_field
35
+ _protobuf_map_fields unless _protobuf_mapped_fields?
36
+
37
+ @_protobuf_date_datetime_time_or_timestamp_field
38
+ end
39
+
40
+ # :nodoc:
41
+ def _protobuf_date_field?(key)
42
+ _protobuf_field_types[:date].include?(key)
43
+ end
44
+
45
+ # :nodoc:
46
+ def _protobuf_date_datetime_time_or_timestamp_field?(key)
47
+ _protobuf_date_datetime_time_or_timestamp_field.include?(key)
48
+ end
49
+
50
+ # :nodoc:
51
+ def _protobuf_datetime_field?(key)
52
+ _protobuf_field_types[:datetime].include?(key)
53
+ end
54
+
55
+ # Map out the fields for future reference on type conversion
56
+ # :nodoc:
57
+ # TODO: Check if collection exists
58
+ # collection_exists? is not a Mongoid method. We need to check $exists?
59
+ # return unless collection_exists?
60
+ def _protobuf_map_fields(force = false)
61
+ FIELD_TYPE_MAP_MUTEX.synchronize do
62
+ @_protobuf_mapped_fields = false if force
63
+ return if _protobuf_mapped_fields?
64
+
65
+ initialize_protobuf_field_containers
66
+ map_protobuf_fields
67
+
68
+ @_protobuf_mapped_fields = true
69
+ end
70
+ end
71
+
72
+ def _protobuf_mapped_fields?
73
+ @_protobuf_mapped_fields
74
+ end
75
+
76
+ # :nodoc:
77
+ def _protobuf_time_field?(key)
78
+ _protobuf_field_types[:time].include?(key)
79
+ end
80
+
81
+ # :nodoc:
82
+ def _protobuf_timestamp_field?(key)
83
+ _protobuf_field_types[:timestamp].include?(key)
84
+ end
85
+
86
+ private
87
+
88
+ def initialize_protobuf_field_containers
89
+ @_protobuf_fields = {}
90
+ @_protobuf_field_types = ::Hash.new { |h, k| h[k] = ::Set.new }
91
+ @_protobuf_date_datetime_time_or_timestamp_field = ::Set.new
92
+ end
93
+
94
+ def map_protobuf_fields
95
+ fields.each do |field|
96
+ field_name_symbol = field[0].to_sym
97
+ field_type_symbol = field[1].options[:type].to_s.to_sym
98
+
99
+ @_protobuf_fields[field_name_symbol] = field
100
+ @_protobuf_field_types[field_type_symbol] << field_name_symbol
101
+
102
+ if DATE_OR_TIME_TYPES.include?(field_type_symbol)
103
+ @_protobuf_date_datetime_time_or_timestamp_field << field_name_symbol
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'protobuf/mongoid/attribute_methods'
4
+ require 'protobuf/mongoid/errors'
5
+ require 'protobuf/mongoid/fields'
6
+ require 'protobuf/mongoid/nested_attributes'
7
+ require 'protobuf/mongoid/persistence'
8
+ require 'protobuf/mongoid/scope'
9
+ require 'protobuf/mongoid/serialization'
10
+ require 'protobuf/mongoid/transformation'
11
+ require 'protobuf/mongoid/validations'
12
+
13
+ module Protobuf
14
+ module Mongoid
15
+ # Base for Protobuf-Mongoid models
16
+ module Model
17
+ extend ::ActiveSupport::Concern
18
+
19
+ included do
20
+ include Protobuf::Mongoid::AttributeMethods
21
+ include Protobuf::Mongoid::Fields
22
+ include Protobuf::Mongoid::NestedAttributes
23
+ include Protobuf::Mongoid::Persistence
24
+ include Protobuf::Mongoid::Serialization
25
+ include Protobuf::Mongoid::Scope
26
+ include Protobuf::Mongoid::Transformation
27
+ include Protobuf::Mongoid::Validations
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protobuf
4
+ module Mongoid
5
+ # Nested Attributes
6
+ module NestedAttributes
7
+ extend ::ActiveSupport::Concern
8
+
9
+ included do
10
+ include ::Heredity::InheritableClassInstanceVariables
11
+
12
+ class << self
13
+ attr_accessor :_protobuf_nested_attributes
14
+ end
15
+
16
+ @_protobuf_nested_attributes = []
17
+
18
+ inheritable_attributes :_protobuf_nested_attributes
19
+ end
20
+
21
+ ##
22
+ # Class Methods
23
+ #
24
+ module ClassMethods
25
+ # :nodoc:
26
+ def accepts_nested_attributes_for(*attr_names)
27
+ attribute_names = attr_names.dup
28
+ attribute_names.extract_options!
29
+ attribute_names.map!(&:to_s)
30
+
31
+ super
32
+
33
+ self._protobuf_nested_attributes += attribute_names
34
+ end
35
+ end
36
+
37
+ # :nodoc:
38
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
39
+ if attributes_collection.first.is_a?(::Protobuf::Message)
40
+ reflection = self.class._reflect_on_association(association_name)
41
+ attributes_collection = attributes_collection.map do |attributes|
42
+ reflection.klass.attributes_from_proto(attributes)
43
+ end
44
+ end
45
+
46
+ super(association_name, attributes_collection)
47
+ end
48
+
49
+ # :nodoc:
50
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
51
+ if attributes.is_a?(::Protobuf::Message)
52
+ reflection = self.class._reflect_on_association(association_name)
53
+ attributes = reflection.klass.attributes_from_proto(attributes)
54
+ end
55
+
56
+ super(association_name, attributes)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protobuf
4
+ module Mongoid
5
+ # Persistence methods
6
+ module Persistence
7
+ extend ::ActiveSupport::Concern
8
+
9
+ ##
10
+ # Class Methods
11
+ #
12
+ module ClassMethods
13
+ # :nodoc:
14
+ def create(attributes = {}, &block)
15
+ attributes = attributes_from_proto(attributes) if attributes.is_a?(::Protobuf::Message)
16
+
17
+ super(attributes, &block)
18
+ end
19
+
20
+ # :nodoc:
21
+ def create!(attributes = {}, &block)
22
+ attributes = attributes_from_proto(attributes) if attributes.is_a?(::Protobuf::Message)
23
+
24
+ super(attributes, &block)
25
+ end
26
+ end
27
+
28
+ # Override Mongoid's initialize method so it can accept a protobuf
29
+ # message as it's attributes.
30
+ # :noapi:
31
+ def initialize(*args, &block)
32
+ args[0] = attributes_from_proto(args.first) if args.first.is_a?(::Protobuf::Message)
33
+ super(*args, &block)
34
+ end
35
+
36
+ # :nodoc:
37
+ def assign_attributes(attributes)
38
+ attributes = attributes_from_proto(attributes) if attributes.is_a?(::Protobuf::Message)
39
+
40
+ super(attributes)
41
+ end
42
+
43
+ # :nodoc:
44
+ def update(attributes)
45
+ attributes = attributes_from_proto(attributes) if attributes.is_a?(::Protobuf::Message)
46
+
47
+ super(attributes)
48
+ end
49
+
50
+ # :nodoc:
51
+ def update!(attributes)
52
+ attributes = attributes_from_proto(attributes) if attributes.is_a?(::Protobuf::Message)
53
+
54
+ super(attributes)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Protobuf
6
+ module Mongoid
7
+ # Scope methods
8
+ module Scope
9
+ extend ::ActiveSupport::Concern
10
+
11
+ included do
12
+ class << self
13
+ alias_method :by_fields, :search_scope
14
+ alias_method :scope_from_proto, :search_scope
15
+ end
16
+ end
17
+
18
+ ##
19
+ # Class Methods
20
+ #
21
+ module ClassMethods
22
+ # Define fields that should be searchable via `search_scope`. Accepts a
23
+ # protobuf field and an already defined scope. If no scope is specified,
24
+ # the scope will be the field name, prefixed with `by_` (e.g. when the
25
+ # field is :guid, the scope will be :by_guid).
26
+ #
27
+ # Optionally, a parser can be provided that will be called, passing the
28
+ # field value as an argument. This allows custom data parsers to be used
29
+ # so that they don't have to be handled by scopes. Parsers can be procs,
30
+ # lambdas, or symbolized method names and must accept the value of the
31
+ # field as a parameter.
32
+ #
33
+ # Examples:
34
+ #
35
+ # class User
36
+ # include Mongoid::Base
37
+ #
38
+ # scope :by_guid, lambda { |*guids| where(:guid => guids) }
39
+ # scope :custom_guid_scope, lambda { |*guids| where(:guid => guids) }
40
+ #
41
+ # # Equivalent to `field_scope :guid, :by_guid`
42
+ # field_scope :guid
43
+ #
44
+ # # With a custom scope
45
+ # field_scope :guid, :scope => :custom_guid_scope
46
+ #
47
+ # # With a custom parser that converts the value to an integer
48
+ # field_scope :guid, :scope => :custom_guid_scope, :parser => lambda { |value| value.to_i }
49
+ # end
50
+ #
51
+ def field_scope(field, options = {})
52
+ scope_name = if options.include?(:scope)
53
+ options[:scope]
54
+ else
55
+ # When no scope is defined, assume the scope is the field, prefixed with `by_`
56
+ :"by_#{field}"
57
+ end
58
+ searchable_fields[field] = scope_name
59
+
60
+ searchable_field_parsers[field] = options[:parser] if options[:parser]
61
+ end
62
+
63
+ # :noapi:
64
+ def model_scope
65
+ all
66
+ end
67
+
68
+ # :noapi:
69
+ def parse_search_values(proto, field)
70
+ value = proto.__send__(field)
71
+
72
+ if searchable_field_parsers[field]
73
+ parser = searchable_field_parsers[field]
74
+
75
+ if parser.respond_to?(:to_sym)
76
+ value = self.__send__(parser.to_sym, value)
77
+ else
78
+ value = parser.call(value)
79
+ end
80
+ end
81
+
82
+ values = [value].flatten
83
+ values.map!(&:to_i) if proto.class.get_field(field, true).enum?
84
+ values
85
+ end
86
+
87
+ # Builds and returns a Arel relation based on the fields that are present
88
+ # in the given protobuf message using the searchable fields to determine
89
+ # what scopes to use. Provides several aliases for variety.
90
+ #
91
+ # Examples:
92
+ #
93
+ # # Search starting with the default scope and searchable fields
94
+ # User.search_scope(request)
95
+ # User.by_fields(request)
96
+ # User.scope_from_proto(request)
97
+ #
98
+ def search_scope(proto)
99
+ search_relation = model_scope
100
+
101
+ searchable_fields.each do |field, scope_name|
102
+ next unless proto.respond_to_and_has_and_present?(field)
103
+
104
+ search_values = parse_search_values(proto, field)
105
+ search_relation = search_relation.__send__(scope_name, *search_values)
106
+ end
107
+
108
+ search_relation
109
+ end
110
+
111
+ # :noapi:
112
+ def searchable_fields
113
+ @_searchable_fields ||= {}
114
+ end
115
+
116
+ # :noapi:
117
+ def searchable_field_parsers
118
+ @_searchable_field_parsers ||= {}
119
+ end
120
+
121
+ # Defines a scope that is eligible for upsert. The scope will be
122
+ # used to initialize a document with first_or_initialize. An upsert scope
123
+ # declariation must specify one or more fields that are required to
124
+ # be present on the request and also must have a field_scope defined.
125
+ #
126
+ # If multiple upsert scopes are specified, they will be searched in
127
+ # the order they are declared for the first valid scope.
128
+ #
129
+ # Examples:
130
+ #
131
+ # class User
132
+ # include Mongoid::Base
133
+ #
134
+ # scope :by_guid, lambda { |*guids| where(:guid => guids) }
135
+ # scope :by_external_guid, lambda { |*external_guids|
136
+ # where(:external_guid => exteranl_guids)
137
+ # }
138
+ # scope :by_client_guid, lambda { |*client_guids|
139
+ # joins(:client).where(
140
+ # :clients => { :guid => client_guids }
141
+ # )
142
+ # }
143
+ #
144
+ # field_scope :guid
145
+ # field_scope :client_guid
146
+ # field_scope :external_guid
147
+ #
148
+ # upsert_scope :external_guid, :client_guid
149
+ # upsert_scope :guid
150
+ #
151
+ # end
152
+ #
153
+ def upsert_key(*fields)
154
+ fields = fields.flatten
155
+
156
+ fields.each do |field|
157
+ fail UpsertScopeError unless searchable_fields[field].present?
158
+ end
159
+
160
+ upsert_keys << fields
161
+ end
162
+
163
+ def upsert_keys
164
+ @_upsert_keys ||= []
165
+ end
166
+
167
+ def for_upsert(proto)
168
+ valid_upsert = upsert_keys.find do |upsert_key|
169
+ upsert_key.all? do |field|
170
+ proto.respond_to_and_has_and_present?(field)
171
+ end
172
+ end
173
+
174
+ fail UpsertNotFoundError unless valid_upsert.present?
175
+
176
+ upsert_scope = model_scope
177
+ valid_upsert.each do |field|
178
+ value = proto.__send__(field)
179
+ upsert_scope = upsert_scope.__send__(searchable_fields[field], value)
180
+ end
181
+
182
+ upsert_scope.first_or_initialize
183
+ end
184
+
185
+ def upsert(proto)
186
+ document = for_upsert(proto)
187
+ document.assign_attributes(proto)
188
+ document.save
189
+ document
190
+ end
191
+
192
+ def upsert!(proto)
193
+ document = for_upsert(proto)
194
+ document.assign_attributes(proto)
195
+ document.save!
196
+ document
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protobuf
4
+ module Mongoid
5
+ # Serialization methods
6
+ module Serialization
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class << self
11
+ attr_writer :_protobuf_field_symbol_transformers,
12
+ :_protobuf_field_transformers,
13
+ :_protobuf_field_options,
14
+ :protobuf_message
15
+ end
16
+ end
17
+
18
+ ##
19
+ # Class Methods
20
+ #
21
+ module ClassMethods
22
+ def _protobuf_field_objects
23
+ @_protobuf_field_objects ||= {}
24
+ end
25
+
26
+ def _protobuf_field_options
27
+ @_protobuf_field_options ||= {}
28
+ end
29
+
30
+ def _protobuf_field_symbol_transformers
31
+ @_protobuf_field_symbol_transformers ||= {}
32
+ end
33
+
34
+ def _protobuf_field_transformers
35
+ @_protobuf_field_transformers ||= {}
36
+ end
37
+
38
+ def _protobuf_message_deprecated_fields
39
+ @_protobuf_message_deprecated_fields ||=
40
+ protobuf_message.all_fields.map do |field|
41
+ next if field.nil?
42
+ next unless field.deprecated?
43
+
44
+ field.name.to_sym
45
+ end
46
+ end
47
+
48
+ def _protobuf_message_non_deprecated_fields
49
+ @_protobuf_message_non_deprecated_fields ||=
50
+ protobuf_message.all_fields.map do |field|
51
+ next if field.nil?
52
+ next if field.deprecated?
53
+
54
+ field.name.to_sym
55
+ end
56
+ end
57
+
58
+ def field_from_document(field, transformer = nil, &block)
59
+ if transformer.is_a?(Symbol)
60
+ _protobuf_field_symbol_transformers[field] = transformer
61
+ return
62
+ end
63
+
64
+ transformer ||= block
65
+ callable = transformer
66
+
67
+ raise FieldTransformerError unless callable.respond_to?(:call)
68
+
69
+ _protobuf_field_transformers[field.to_sym] = callable
70
+ end
71
+
72
+ def protobuf_fields(*fields)
73
+ options = fields.extract_options!
74
+ options[:only] = fields if fields.present?
75
+
76
+ self._protobuf_field_options = options
77
+ end
78
+
79
+ def protobuf_message(message = nil, options = {})
80
+ unless message.nil?
81
+ @protobuf_message = message.to_s.classify.constantize
82
+ self._protobuf_field_options = options
83
+ end
84
+
85
+ @protobuf_message
86
+ end
87
+
88
+ class CollectionAssociationCaller
89
+ def initialize(method_name)
90
+ @method_name = method_name
91
+ end
92
+
93
+ def call(selph)
94
+ selph.__send__(@method_name).to_a
95
+ rescue NameError
96
+ nil
97
+ end
98
+ end
99
+
100
+ def _protobuf_collection_association_object(field)
101
+ CollectionAssociationCaller.new(field)
102
+ end
103
+
104
+ class DateCaller
105
+ def initialize(field)
106
+ @field = field
107
+ end
108
+
109
+ def call(selph)
110
+ value = selph.__send__(@field)
111
+
112
+ value&.to_time&.utc&.to_i
113
+ rescue NameError
114
+ nil
115
+ end
116
+ end
117
+
118
+ class DateTimeCaller
119
+ def initialize(field)
120
+ @field = field
121
+ end
122
+
123
+ def call(selph)
124
+ value = selph.__send__(@field)
125
+
126
+ value&.to_i
127
+ rescue NameError
128
+ nil
129
+ end
130
+ end
131
+
132
+ class NoConversionCaller
133
+ def initialize(field)
134
+ @field = field
135
+ end
136
+
137
+ def call(selph)
138
+ selph.__send__(@field)
139
+ rescue NameError
140
+ nil
141
+ end
142
+ end
143
+
144
+ def _protobuf_convert_to_fields_object(field)
145
+ is_datetime_time_or_timestamp_field = _protobuf_date_datetime_time_or_timestamp_field?(field)
146
+ is_date_field = _protobuf_date_field?(field)
147
+
148
+ if is_datetime_time_or_timestamp_field
149
+ if is_date_field
150
+ DateCaller.new(field)
151
+ else
152
+ DateTimeCaller.new(field)
153
+ end
154
+ else
155
+ NoConversionCaller.new(field)
156
+ end
157
+ end
158
+
159
+ def _protobuf_field_transformer_object(field)
160
+ _protobuf_field_transformers[field]
161
+ end
162
+
163
+ class NilMethodCaller
164
+ def initialize; end
165
+
166
+ def call(_selph)
167
+ nil
168
+ end
169
+ end
170
+
171
+ def _protobuf_nil_object(_field)
172
+ NilMethodCaller.new
173
+ end
174
+
175
+ class FieldSymbolTransformerCaller
176
+ def initialize(instance_class, method_name)
177
+ @instance_class = instance_class
178
+ @method_name = method_name
179
+ end
180
+
181
+ def call(selph)
182
+ @instance_class.__send__(@method_name, selph)
183
+ end
184
+ end
185
+
186
+ def _protobuf_symbol_transformer_object(field)
187
+ FieldSymbolTransformerCaller.new(self, _protobuf_field_symbol_transformers[field])
188
+ end
189
+ end
190
+
191
+ def _filter_field_attributes(options = {})
192
+ options = _normalize_options(options)
193
+
194
+ fields = _filtered_fields(options)
195
+ fields &= [options[:only]].flatten if options[:only].present?
196
+ fields -= [options[:except]].flatten if options[:except].present?
197
+
198
+ fields
199
+ end
200
+
201
+ def _filtered_fields(options = {})
202
+ include_deprecated = options.fetch(:deprecated, true)
203
+
204
+ fields = []
205
+ fields.concat(self.class._protobuf_message_non_deprecated_fields)
206
+ fields.concat(self.class._protobuf_message_deprecated_fields) if include_deprecated
207
+ fields.concat([options[:include]].flatten) if options[:include].present?
208
+ fields.compact!
209
+ fields.uniq!
210
+
211
+ fields
212
+ end
213
+
214
+ def _is_collection_association?(field)
215
+ reflection = self.class.relations[field.to_s]
216
+ return false unless reflection
217
+
218
+ reflection.class.name.split('::').last.to_sym == :has_many
219
+ end
220
+
221
+ def _normalize_options(options)
222
+ options ||= {}
223
+ options[:only] ||= [] if options.fetch(:except, false)
224
+ options[:except] ||= [] if options.fetch(:only, false)
225
+
226
+ self.class._protobuf_field_options.merge(options)
227
+ end
228
+
229
+ def fields_from_document(options = {})
230
+ hash = {}
231
+ field_attributes = _filter_field_attributes(options)
232
+
233
+ if options[:include].present?
234
+ field_attributes.concat([options[:include]].flatten)
235
+ field_attributes.compact!
236
+ field_attributes.uniq!
237
+ end
238
+
239
+ field_attributes.each do |field|
240
+ field_object = _protobuf_field_objects(field)
241
+ hash[field] = field_object.call(self)
242
+ end
243
+
244
+ hash
245
+ end
246
+
247
+ # TODO: Assignment Branch Condition size for _protobuf_field_objects is too high. [<1, 19, 7> 20.27/17]
248
+ def _protobuf_field_objects(field)
249
+ self.class._protobuf_field_objects[field] ||=
250
+ if _protobuf_field_symbol_transformers.key?(field)
251
+ self.class._protobuf_symbol_transformer_object(field)
252
+ elsif _protobuf_field_transformers.key?(field)
253
+ self.class._protobuf_field_transformer_object(field)
254
+ elsif respond_to?(field)
255
+ if _is_collection_association?(field)
256
+ self.class._protobuf_collection_association_object(field)
257
+ else
258
+ self.class._protobuf_convert_to_fields_object(field)
259
+ end
260
+ else
261
+ self.class._protobuf_nil_object(field)
262
+ end
263
+ end
264
+
265
+ def _protobuf_field_symbol_transformers
266
+ self.class._protobuf_field_symbol_transformers
267
+ end
268
+
269
+ def _protobuf_field_transformers
270
+ self.class._protobuf_field_transformers
271
+ end
272
+
273
+ def _protobuf_message
274
+ self.class.protobuf_message
275
+ end
276
+
277
+ def to_proto(options = {})
278
+ raise MessageNotDefined, self.class if _protobuf_message.nil?
279
+
280
+ fields = fields_from_document(options)
281
+ _protobuf_message.new(fields)
282
+ end
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'protobuf/mongoid/transformer'
4
+
5
+ module Protobuf
6
+ module Mongoid
7
+ # Transformation methods
8
+ module Transformation
9
+ extend ::ActiveSupport::Concern
10
+
11
+ included do
12
+ include ::Heredity::InheritableClassInstanceVariables
13
+
14
+ class << self
15
+ attr_accessor :_protobuf_attribute_transformers
16
+ end
17
+
18
+ @_protobuf_attribute_transformers = {}
19
+
20
+ inheritable_attributes :_protobuf_attribute_transformers
21
+ end
22
+
23
+ ##
24
+ # Class Methods
25
+ #
26
+ module ClassMethods
27
+ # Filters accessible attributes that exist in the given protobuf message's
28
+ # fields or have attribute transformers defined for them.
29
+ #
30
+ # Returns a hash of attribute fields with their respective values.
31
+ #
32
+ # :nodoc:
33
+ # TODO: Assignment Branch Condition size for _filter_attribute_fields is too high. [<13, 26, 9> 30.43/17]
34
+ # TODO: Use `each_with_object` instead of `inject`.
35
+ def _filter_attribute_fields(proto)
36
+ fields = proto.to_hash
37
+ fields.select! do |key, _value|
38
+ field = proto.class.get_field(key, true)
39
+ proto.field?(key) && !field.repeated?
40
+ end
41
+
42
+ filtered_attributes = _filtered_attributes + _protobuf_attribute_transformers.keys
43
+
44
+ attribute_fields = filtered_attributes.inject({}) do |hash, field_name|
45
+ symbolized_field = field_name.to_sym
46
+
47
+ if fields.key?(symbolized_field) || _protobuf_attribute_transformers.key?(symbolized_field)
48
+ hash[symbolized_field] = fields[symbolized_field]
49
+ end
50
+
51
+ hash
52
+ end
53
+
54
+ _protobuf_nested_attributes.each do |attribute_name|
55
+ nested_attribute_name = "#{attribute_name}_attributes".to_sym
56
+ value = if proto.field?(nested_attribute_name)
57
+ proto.__send__(nested_attribute_name)
58
+ elsif proto.field?(attribute_name)
59
+ proto.__send__(attribute_name)
60
+ end
61
+
62
+ next unless value
63
+
64
+ attribute_fields[nested_attribute_name] = value
65
+ end
66
+
67
+ attribute_fields
68
+ end
69
+
70
+ # Overidden by mass assignment security when protected attributes is loaded.
71
+ #
72
+ # :nodoc:
73
+ def _filtered_attributes
74
+ return self.attribute_names
75
+ end
76
+
77
+ # :nodoc:
78
+ def _protobuf_convert_fields_to_attributes(key, value)
79
+ return nil if value.nil?
80
+ return value unless _protobuf_date_datetime_time_or_timestamp_field?(key)
81
+
82
+ value = case
83
+ when _protobuf_datetime_field?(key) then
84
+ convert_int64_to_datetime(value)
85
+ when _protobuf_timestamp_field?(key) then
86
+ convert_int64_to_time(value)
87
+ when _protobuf_time_field?(key) then
88
+ convert_int64_to_time(value)
89
+ when _protobuf_date_field?(key) then
90
+ convert_int64_to_date(value)
91
+ end
92
+
93
+ return value
94
+ end
95
+
96
+ # Define an attribute transformation from protobuf. Accepts a Symbol,
97
+ # callable, or block.
98
+ #
99
+ # When given a callable or block, it is directly used to convert the field.
100
+ #
101
+ # When a symbol is given, it extracts the method with the same name.
102
+ #
103
+ # The callable or method must accept a single parameter, which is the
104
+ # proto message.
105
+ #
106
+ # Examples:
107
+ # attribute_from_proto :public_key, :extract_public_key_from_proto
108
+ # attribute_from_proto :status, lambda { |proto| # Do some stuff... }
109
+ # attribute_from_proto :status do |proto|
110
+ # # Do some blocky stuff...
111
+ # end
112
+ #
113
+ # attribute_from_proto :status, lambda { |proto| nil }, :nullify_on => :status
114
+ # attribute_from_proto :status, :nullify_on => :status do |proto|
115
+ # nil
116
+ # end
117
+ #
118
+ def attribute_from_proto(attribute, *args, &block)
119
+ options = args.extract_options!
120
+ symbol_or_block = args.first || block
121
+
122
+ if symbol_or_block.is_a?(Symbol)
123
+ callable = lambda { |value| self.__send__(symbol_or_block, value) }
124
+ else
125
+ raise AttributeTransformerError unless symbol_or_block.respond_to?(:call)
126
+ callable = symbol_or_block
127
+ end
128
+
129
+ if options[:nullify_on]
130
+ field = protobuf_message.get_field(:nullify)
131
+ unless field&.is_a?(::Protobuf::Field::StringField) && field&.repeated?
132
+ ::Protobuf::Logging.logger.warn "Message: #{protobuf_message} is not compatible with :nullify_on option"
133
+ end
134
+ end
135
+
136
+ transformer = ::Protobuf::Mongoid::Transformer.new(callable, options)
137
+ _protobuf_attribute_transformers[attribute.to_sym] = transformer
138
+ end
139
+
140
+ # Creates a hash of attributes from a given protobuf message.
141
+ #
142
+ # It converts and transforms field values using the field converters and
143
+ # attribute transformers, ignoring repeated and nil fields.
144
+ #
145
+ def attributes_from_proto(proto)
146
+ attribute_fields = _filter_attribute_fields(proto)
147
+
148
+ attributes = attribute_fields.inject({}) do |hash, (key, value)|
149
+ if _protobuf_attribute_transformers.key?(key)
150
+ transformer = _protobuf_attribute_transformers[key]
151
+ attribute = transformer.call(proto)
152
+ hash[key] = attribute unless attribute.nil?
153
+ hash[key] = nil if transformer.nullify?(proto)
154
+ else
155
+ hash[key] = _protobuf_convert_fields_to_attributes(key, value)
156
+ end
157
+
158
+ hash
159
+ end
160
+
161
+ return attributes unless proto.field?(:nullify) && proto.nullify.is_a?(Array)
162
+
163
+ proto.nullify.each do |attribute_name|
164
+ attributes[attribute_name.to_sym] = nil if attribute_names.include?(attribute_name.to_s)
165
+ end
166
+
167
+ attributes
168
+ end
169
+
170
+ # :nodoc:
171
+ def convert_int64_to_time(int64)
172
+ Time.at(int64.to_i)
173
+ end
174
+
175
+ # :nodoc:
176
+ def convert_int64_to_date(int64)
177
+ convert_int64_to_time(int64).utc.to_date
178
+ end
179
+
180
+ # :nodoc:
181
+ def convert_int64_to_datetime(int64)
182
+ convert_int64_to_time(int64).to_datetime
183
+ end
184
+ end
185
+
186
+ # Calls up to the class version of the method.
187
+ #
188
+ def attributes_from_proto(proto)
189
+ self.class.attributes_from_proto(proto)
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protobuf
4
+ module Mongoid
5
+ # Transformer class
6
+ class Transformer
7
+ attr_accessor :callable, :options
8
+
9
+ def initialize(callable, options = {})
10
+ @callable = callable
11
+ @options = options
12
+ end
13
+
14
+ def call(proto)
15
+ return unless proto
16
+
17
+ callable.call(proto)
18
+ end
19
+
20
+ def nullify?(proto)
21
+ return false unless options[:nullify_on]
22
+ return false unless proto.field?(:nullify) && proto.nullify.is_a?(Array)
23
+ return false if proto.nullify.empty?
24
+
25
+ proto.nullify.include?(options[:nullify_on].to_s)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protobuf
4
+ module Mongoid
5
+ # Validation methods
6
+ module Validations
7
+ extend ::ActiveSupport::Concern
8
+
9
+ ##
10
+ # Class Methods
11
+ #
12
+ module ClassMethods
13
+ # Validates whether the value of the specified attribute is available in
14
+ # the given Protobuf Enum. The enumeration should be passed as a class
15
+ # that defines the enumeration:
16
+ #
17
+ # ```
18
+ # class User < Mongoid::Base
19
+ # include ::Protobuf::Mongoid::Model
20
+ #
21
+ # validates_enumeration_of :role_type, :with => RoleType, :allow_nil => true
22
+ # end
23
+ # ```
24
+ #
25
+ # In this example, RoleType is a defined as a protobuf enum.
26
+ #
27
+ # It accepts the same options as `validates_inclusion_of` (the :in option
28
+ # is automatically set and will be overwritten).
29
+ #
30
+ def validates_enumeration_of(*args)
31
+ options = args.extract_options!
32
+ enumerable = options.delete(:with)
33
+
34
+ raise ArgumentError, ':with must be specified' if enumerable.nil?
35
+
36
+ options[:in] = enumerable.all_tags if enumerable < ::Protobuf::Enum
37
+
38
+ args << options
39
+
40
+ validates_inclusion_of(*args)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protobuf
4
+ module Mongoid
5
+ VERSION = '0.0.1'
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'heredity'
5
+
6
+ require 'protobuf/mongoid/model'
7
+ require 'protobuf/mongoid/version'
8
+
9
+ module Protobuf
10
+ # Main module for Protobuf-Mongoid integration
11
+ module Mongoid
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: protobuf-mongoid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Luilver Garces
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-04-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: google-protobuf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.30.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.30.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: heredity
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.1.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.1.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: mongoid
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 9.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 9.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: protobuf
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.10.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.10.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: securerandom
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.4.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.4.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.15.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.15.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: timecop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.9.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.9.0
125
+ description: This gem provides functionality to serialize and deserialize Mongoid
126
+ documents using Protocol Buffers.
127
+ email:
128
+ - luilver@gmail.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - LICENSE.txt
134
+ - README.md
135
+ - lib/protobuf/mongoid.rb
136
+ - lib/protobuf/mongoid/attribute_methods.rb
137
+ - lib/protobuf/mongoid/errors.rb
138
+ - lib/protobuf/mongoid/fields.rb
139
+ - lib/protobuf/mongoid/model.rb
140
+ - lib/protobuf/mongoid/nested_attributes.rb
141
+ - lib/protobuf/mongoid/persistence.rb
142
+ - lib/protobuf/mongoid/scope.rb
143
+ - lib/protobuf/mongoid/serialization.rb
144
+ - lib/protobuf/mongoid/transformation.rb
145
+ - lib/protobuf/mongoid/transformer.rb
146
+ - lib/protobuf/mongoid/validations.rb
147
+ - lib/protobuf/mongoid/version.rb
148
+ homepage: https://github.com/yourusername/protobuf-mongoid
149
+ licenses:
150
+ - MIT
151
+ metadata: {}
152
+ post_install_message:
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: 3.2.2
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ requirements: []
167
+ rubygems_version: 3.5.6
168
+ signing_key:
169
+ specification_version: 4
170
+ summary: A gem to integrate Protocol Buffers with Mongoid.
171
+ test_files: []