Skip to content

Commit d0662db

Browse files
authored
Merge pull request #822 from dalibor/binary
Support binary type natively
2 parents 605e0a0 + 6e68419 commit d0662db

File tree

8 files changed

+143
-19
lines changed

8 files changed

+143
-19
lines changed

.rubocop_thread_safety.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
# TODO: Comment out the following to see code needing to be refactored for thread safety!
33
ThreadSafety/ClassAndModuleAttributes:
44
Enabled: false
5-
ThreadSafety/InstanceVariableInClassMethod:
5+
ThreadSafety/ClassInstanceVariable:
66
Enabled: false

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,24 @@ class Document
342342
end
343343
```
344344

345+
#### Note on binary type
346+
347+
By default binary fields are persisted as DynamoDB String value encoded
348+
in the Base64 encoding. DynamoDB supports binary data natively. To use
349+
it instead of String a `store_binary_as_native` field option should be
350+
set:
351+
352+
```ruby
353+
class Document
354+
include Dynamoid::Document
355+
356+
field :image, :binary, store_binary_as_native: true
357+
end
358+
```
359+
360+
There is also a global config option `store_binary_as_native` that is
361+
`false` by default as well.
362+
345363
#### Magic Columns
346364

347365
You get magic columns of `id` (`string`), `created_at` (`datetime`), and
@@ -1138,6 +1156,9 @@ Listed below are all configuration options.
11381156
* `store_boolean_as_native` - if `true` Dynamoid stores boolean fields
11391157
as native DynamoDB boolean values. Otherwise boolean fields are stored
11401158
as string values `'t'` and `'f'`. Default is `true`
1159+
* `store_binary_as_native` - if `true` Dynamoid stores binary fields
1160+
as native DynamoDB binary values. Otherwise binary fields are stored
1161+
as Base64 encoded string values. Default is `false`
11411162
* `backoff` - is a hash: key is a backoff strategy (symbol), value is
11421163
parameters for the strategy. Is used in batch operations. Default id
11431164
`nil`

lib/dynamoid/config.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ module Config
5050
option :store_date_as_string, default: false # store Date fields in ISO 8601 string format
5151
option :store_empty_string_as_nil, default: true # store attribute's empty String value as null
5252
option :store_boolean_as_native, default: true
53+
option :store_binary_as_native, default: false
5354
option :backoff, default: nil # callable object to handle exceeding of table throughput limit
5455
option :backoff_strategies, default: {
5556
constant: BackoffStrategies::ConstantBackoff,

lib/dynamoid/dumping.rb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,24 @@ def process(value)
297297
end
298298
end
299299

300-
# string -> string
300+
# string -> StringIO
301301
class BinaryDumper < Base
302302
def process(value)
303-
Base64.strict_encode64(value)
303+
store_as_binary = if @options[:store_as_native_binary].nil?
304+
Dynamoid.config.store_binary_as_native
305+
else
306+
@options[:store_as_native_binary]
307+
end
308+
309+
if store_as_binary
310+
if value.is_a?(StringIO) || value.is_a?(IO)
311+
value
312+
else
313+
StringIO.new(value)
314+
end
315+
else
316+
Base64.strict_encode64(value)
317+
end
304318
end
305319
end
306320

lib/dynamoid/type_casting.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ def process(value)
288288

289289
class BinaryTypeCaster < Base
290290
def process(value)
291-
if value.is_a? String
291+
if value.is_a?(StringIO) || value.is_a?(IO)
292+
value
293+
elsif value.is_a?(String)
292294
value.dup
293295
else
294296
value.to_s

lib/dynamoid/undumping.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,17 @@ def process(value)
284284

285285
class BinaryUndumper < Base
286286
def process(value)
287-
Base64.strict_decode64(value)
287+
store_as_binary = if @options[:store_as_native_binary].nil?
288+
Dynamoid.config.store_binary_as_native
289+
else
290+
@options[:store_as_native_binary]
291+
end
292+
293+
if store_as_binary
294+
value.string # expect StringIO here
295+
else
296+
Base64.strict_decode64(value)
297+
end
288298
end
289299
end
290300

spec/dynamoid/dumping_spec.rb

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,20 +1606,87 @@ def self.dynamoid_field_type
16061606
end
16071607

16081608
describe 'Binary field' do
1609-
let(:klass) do
1610-
new_class do
1611-
field :image, :binary
1609+
let(:unfrozen_string) { +"\x00\x88\xFF" }
1610+
let(:binary_value) { unfrozen_string.force_encoding('ASCII-8BIT') }
1611+
1612+
context 'default non-native binary' do
1613+
let(:klass) do
1614+
new_class do
1615+
field :image, :binary
1616+
end
1617+
end
1618+
1619+
it 'encodes a string in base64-encoded format' do
1620+
obj = klass.create(image: binary_value)
1621+
1622+
expect(reload(obj).image).to eql(binary_value)
1623+
expect(raw_attributes(obj)[:image]).to eql(Base64.strict_encode64(binary_value))
16121624
end
16131625
end
16141626

1615-
let(:unfrozen_string) { +"\x00\x88\xFF" }
1616-
let(:binary_value) { unfrozen_string.force_encoding('ASCII-8BIT') }
1627+
context 'native binary' do
1628+
let(:klass) do
1629+
new_class do
1630+
field :image, :binary, store_as_native_binary: true
1631+
end
1632+
end
1633+
1634+
it 'converts string to StringIO object' do
1635+
obj = klass.create(image: binary_value)
1636+
1637+
expect(reload(obj).image).to eql(binary_value)
1638+
expect(raw_attributes(obj)[:image].class).to eql(StringIO)
1639+
expect(raw_attributes(obj)[:image].string).to eql(binary_value)
1640+
end
16171641

1618-
it 'encodes a string in base64-encoded format' do
1619-
obj = klass.create(image: binary_value)
1642+
it 'accepts StringIO object' do
1643+
image = StringIO.new(binary_value)
1644+
obj = klass.create(image: image)
16201645

1621-
expect(reload(obj).image).to eql(binary_value)
1622-
expect(raw_attributes(obj)[:image]).to eql(Base64.strict_encode64(binary_value))
1646+
expect(reload(obj).image).to eql(binary_value)
1647+
expect(raw_attributes(obj)[:image].class).to eql(StringIO)
1648+
expect(raw_attributes(obj)[:image].string).to eql(binary_value)
1649+
end
1650+
1651+
it 'accepts IO object' do
1652+
Tempfile.create('image') do |image|
1653+
image.write(binary_value)
1654+
image.rewind
1655+
1656+
obj = klass.create(image: image)
1657+
1658+
expect(reload(obj).image).to eql(binary_value)
1659+
expect(raw_attributes(obj)[:image].class).to eql(StringIO)
1660+
expect(raw_attributes(obj)[:image].string).to eql(binary_value)
1661+
end
1662+
end
1663+
end
1664+
1665+
context 'store_binary_as_native config option' do
1666+
it 'is stored as binary if store_binary_as_native config option is true',
1667+
config: { store_binary_as_native: true } do
1668+
klass = new_class do
1669+
field :image, :binary
1670+
end
1671+
1672+
obj = klass.create(image: binary_value)
1673+
1674+
expect(reload(obj).image).to eql(binary_value)
1675+
expect(raw_attributes(obj)[:image].class).to eql(StringIO)
1676+
expect(raw_attributes(obj)[:image].string).to eql(binary_value)
1677+
end
1678+
1679+
it 'is not stored as binary if store_binary_as_native config option is false',
1680+
config: { store_binary_as_native: false } do
1681+
klass = new_class do
1682+
field :image, :binary
1683+
end
1684+
1685+
obj = klass.create(image: binary_value)
1686+
1687+
expect(reload(obj).image).to eql(binary_value)
1688+
expect(raw_attributes(obj)[:image]).to eql(Base64.strict_encode64(binary_value))
1689+
end
16231690
end
16241691
end
16251692
end

spec/dynamoid/type_casting_spec.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@
274274

275275
obj = klass.new(values: Set.new([1, 1.5, '2'.to_d]))
276276

277-
expect(obj.values).to eql(Set.new([1, 1, 2]))
277+
expect(obj.values).to eql(Set.new([1, 2]))
278278
end
279279

280280
it 'type casts numbers' do
@@ -671,12 +671,21 @@ def settings.to_hash
671671
expect(obj.image).to eql('string representation')
672672
end
673673

674-
it 'dups a string' do
675-
value = 'foo'
674+
it 'does not convert StringIO objects' do
675+
value = StringIO.new('foo')
676676
obj = klass.new(image: value)
677677

678-
expect(obj.image).to eql(value)
679-
expect(obj.image).not_to equal(value)
678+
expect(obj.image).to equal(value)
679+
end
680+
681+
it 'does not convert IO objects' do
682+
Tempfile.create('image') do |value|
683+
value.write('foo')
684+
value.rewind
685+
686+
obj = klass.new(image: value)
687+
expect(obj.image).to equal(value)
688+
end
680689
end
681690
end
682691

0 commit comments

Comments
 (0)