• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Dynamoid / dynamoid / 10441434881

18 Aug 2024 02:32PM UTC coverage: 90.584% (+0.07%) from 90.511%
10441434881

push

github

web-flow
Merge pull request #794 from Dynamoid/add-configuration-option-to-persist-empty-strings

Add configuration option to persist empty strings as is

866 of 979 branches covered (88.46%)

Branch coverage included in aggregate %.

3184 of 3492 relevant lines covered (91.18%)

812.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.02
/lib/dynamoid/dumping.rb
1
# frozen_string_literal: true
2

3
module Dynamoid
1✔
4
  # @private
5
  module Dumping
1✔
6
    def self.dump_attributes(attributes, attributes_options)
1✔
7
      {}.tap do |h|
2,701✔
8
        attributes.each do |attribute, value|
2,701✔
9
          h[attribute] = dump_field(value, attributes_options[attribute])
12,366✔
10
        end
11
      end
12
    end
13

14
    def self.dump_field(value, options)
1✔
15
      return nil if value.nil?
14,010✔
16

17
      dumper = find_dumper(options)
13,918✔
18

19
      if dumper.nil?
13,918!
20
        raise ArgumentError, "Unknown type #{options[:type]}"
×
21
      end
22

23
      dumper.process(value)
13,918✔
24
    end
25

26
    def self.find_dumper(options)
1✔
27
      dumper_class = case options[:type]
13,956!
28
                     when :string     then StringDumper
5,468✔
29
                     when :integer    then IntegerDumper
907✔
30
                     when :number     then NumberDumper
967✔
31
                     when :set        then SetDumper
386✔
32
                     when :array      then ArrayDumper
30✔
33
                     when :map        then MapDumper
11✔
34
                     when :datetime   then DateTimeDumper
6,004✔
35
                     when :date       then DateDumper
88✔
36
                     when :serialized then SerializedDumper
17✔
37
                     when :raw        then RawDumper
19✔
38
                     when :boolean    then BooleanDumper
52✔
39
                     when :binary     then BinaryDumper
1✔
40
                     when Class       then CustomTypeDumper
6✔
41
                     end
42

43
      if dumper_class.present?
13,956!
44
        dumper_class.new(options)
13,956✔
45
      end
46
    end
47

48
    module DeepSanitizeHelper
1✔
49
      extend self
1✔
50

51
      def deep_sanitize(value)
1✔
52
        case value
119✔
53
        when Hash
33✔
54
          sanitize_hash(value).transform_values { |v| deep_sanitize(v) }
80✔
55
        when Array
15✔
56
          sanitize_array(value).map { |v| deep_sanitize(v) }
57✔
57
        else
71✔
58
          value
71✔
59
        end
60
      end
61

62
      private
1✔
63

64
      def sanitize_hash(hash)
1✔
65
        hash.transform_values { |v| invalid_value?(v) ? nil : v }
80✔
66
      end
67

68
      def sanitize_array(array)
1✔
69
        array.map { |v| invalid_value?(v) ? nil : v }
57✔
70
      end
71

72
      def invalid_value?(value)
1✔
73
        (value.is_a?(Set) && value.empty?) ||
89✔
74
          (value.is_a?(String) && value.empty? && Config.store_empty_string_as_nil)
85✔
75
      end
76
    end
77

78
    class Base
1✔
79
      def initialize(options)
1✔
80
        @options = options
13,956✔
81
      end
82

83
      def process(value)
1✔
84
        value
1,892✔
85
      end
86
    end
87

88
    # string -> string
89
    class StringDumper < Base
1✔
90
      def process(string)
1✔
91
        return nil if string.nil?
5,484!
92
        return nil if string.empty? && Config.store_empty_string_as_nil
5,484✔
93

94
        string
5,464✔
95
      end
96
    end
97

98
    # integer -> number
99
    class IntegerDumper < Base
1✔
100
    end
101

102
    # number -> number
103
    class NumberDumper < Base
1✔
104
    end
105

106
    # set -> set
107
    class SetDumper < Base
1✔
108
      ALLOWED_TYPES = %i[string integer number date datetime serialized].freeze
1✔
109

110
      def process(set)
1✔
111
        return nil if set.is_a?(Set) && set.empty?
386✔
112

113
        if @options.key?(:of)
335✔
114
          process_typed_collection(set)
24✔
115
        else
311✔
116
          set
311✔
117
        end
118
      end
119

120
      private
1✔
121

122
      def process_typed_collection(set)
1✔
123
        if allowed_type?
24✔
124
          # StringDumper may replace "" with nil so we cannot distinguish it from an explicit nil
23✔
125
          if element_type == :string && Config.store_empty_string_as_nil
23✔
126
            set.reject! { |s| s && s.empty? }
20✔
127
          end
128

129
          dumper = Dumping.find_dumper(element_options)
23✔
130
          result = set.map { |el| dumper.process(el) }
71✔
131
          result.to_set
23✔
132
        else
1✔
133
          raise ArgumentError, "Set element type #{element_type} isn't supported"
1✔
134
        end
135
      end
136

137
      def allowed_type?
1✔
138
        ALLOWED_TYPES.include?(element_type) || element_type.is_a?(Class)
24✔
139
      end
140

141
      def element_type
1✔
142
        if @options[:of].is_a?(Hash)
78✔
143
          @options[:of].keys.first
20✔
144
        else
58✔
145
          @options[:of]
58✔
146
        end
147
      end
148

149
      def element_options
1✔
150
        if @options[:of].is_a?(Hash)
23✔
151
          @options[:of][element_type].dup.tap do |options|
5✔
152
            options[:type] = element_type
5✔
153
          end
154
        else
18✔
155
          { type: element_type }
18✔
156
        end
157
      end
158
    end
159

160
    # array -> array
161
    class ArrayDumper < Base
1✔
162
      ALLOWED_TYPES = %i[string integer number date datetime serialized].freeze
1✔
163

164
      def process(array)
1✔
165
        if @options.key?(:of)
30✔
166
          process_typed_collection(array)
16✔
167
        else
14✔
168
          array
14✔
169
        end
170
      end
171

172
      private
1✔
173

174
      def process_typed_collection(array)
1✔
175
        if allowed_type?
16✔
176
          # StringDumper may replace "" with nil so we cannot distinguish it from an explicit nil
15✔
177
          if element_type == :string && Config.store_empty_string_as_nil
15✔
178
            array.reject! { |s| s && s.empty? }
12✔
179
          end
180

181
          dumper = Dumping.find_dumper(element_options)
15✔
182
          array.map { |el| dumper.process(el) }
39✔
183
        else
1✔
184
          raise ArgumentError, "Array element type #{element_type} isn't supported"
1✔
185
        end
186
      end
187

188
      def allowed_type?
1✔
189
        ALLOWED_TYPES.include?(element_type) || element_type.is_a?(Class)
16✔
190
      end
191

192
      def element_type
1✔
193
        if @options[:of].is_a?(Hash)
54✔
194
          @options[:of].keys.first
20✔
195
        else
34✔
196
          @options[:of]
34✔
197
        end
198
      end
199

200
      def element_options
1✔
201
        if @options[:of].is_a?(Hash)
15✔
202
          @options[:of][element_type].dup.tap do |options|
5✔
203
            options[:type] = element_type
5✔
204
          end
205
        else
10✔
206
          { type: element_type }
10✔
207
        end
208
      end
209
    end
210

211
    # hash -> map
212
    class MapDumper < Base
1✔
213
      def process(value)
1✔
214
        DeepSanitizeHelper.deep_sanitize(value)
11✔
215
      end
216
    end
217

218
    # datetime -> integer/string
219
    class DateTimeDumper < Base
1✔
220
      def process(value)
1✔
221
        value.nil? ? nil : format_datetime(value, @options)
6,004!
222
      end
223

224
      private
1✔
225

226
      def format_datetime(value, options)
1✔
227
        use_string_format = if options[:store_as_string].nil?
6,004✔
228
                              Dynamoid.config.store_datetime_as_string
5,997✔
229
                            else
7✔
230
                              options[:store_as_string]
7✔
231
                            end
232

233
        if use_string_format
6,004✔
234
          value_in_time_zone = Dynamoid::DynamodbTimeZone.in_time_zone(value)
23✔
235
          value_in_time_zone.iso8601
23✔
236
        else
5,981✔
237
          unless value.respond_to?(:to_i) && value.respond_to?(:nsec)
5,981!
238
            value = value.to_time
×
239
          end
240
          BigDecimal(format('%d.%09d', value.to_i, value.nsec))
5,981✔
241
        end
242
      end
243
    end
244

245
    # date -> integer/string
246
    class DateDumper < Base
1✔
247
      def process(value)
1✔
248
        value.nil? ? nil : format_date(value, @options)
88!
249
      end
250

251
      private
1✔
252

253
      def format_date(value, options)
1✔
254
        use_string_format = if options[:store_as_string].nil?
88✔
255
                              Dynamoid.config.store_date_as_string
80✔
256
                            else
8✔
257
                              options[:store_as_string]
8✔
258
                            end
259

260
        if use_string_format
88✔
261
          value.to_date.iso8601
5✔
262
        else
83✔
263
          (value.to_date - Dynamoid::Persistence::UNIX_EPOCH_DATE).to_i
83✔
264
        end
265
      end
266
    end
267

268
    # any standard Ruby object -> self
269
    class RawDumper < Base
1✔
270
      def process(value)
1✔
271
        DeepSanitizeHelper.deep_sanitize(value)
19✔
272
      end
273
    end
274

275
    # object -> string
276
    class SerializedDumper < Base
1✔
277
      def process(value)
1✔
278
        @options[:serializer] ? @options[:serializer].dump(value) : value.to_yaml
17✔
279
      end
280
    end
281

282
    # True/False -> True/False/string
283
    class BooleanDumper < Base
1✔
284
      def process(value)
1✔
285
        unless value.nil?
52!
286
          store_as_boolean = if @options[:store_as_native_boolean].nil?
52✔
287
                               Dynamoid.config.store_boolean_as_native
40✔
288
                             else
12✔
289
                               @options[:store_as_native_boolean]
12✔
290
                             end
291
          if store_as_boolean
52✔
292
            !!value
45✔
293
          else
7✔
294
            value.to_s[0] # => "f" or "t"
7✔
295
          end
296
        end
297
      end
298
    end
299

300
    # string -> string
301
    class BinaryDumper < Base
1✔
302
      def process(value)
1✔
303
        Base64.strict_encode64(value)
1✔
304
      end
305
    end
306

307
    # any object -> string
308
    class CustomTypeDumper < Base
1✔
309
      def process(value)
1✔
310
        field_class = @options[:type]
6✔
311

312
        if value.respond_to?(:dynamoid_dump)
6✔
313
          value.dynamoid_dump
4✔
314
        elsif field_class.respond_to?(:dynamoid_dump)
2✔
315
          field_class.dynamoid_dump(value)
2✔
316
        else
×
317
          raise ArgumentError, "Neither #{field_class} nor #{value} supports serialization for Dynamoid."
×
318
        end
319
      end
320
    end
321
  end
322
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc