Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Epoch Time Attribute Type #83

Merged
merged 2 commits into from Jul 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
@@ -1,6 +1,8 @@
Unreleased Changes
------------------

* Feature - Aws::Record::Marshalers::EpochTimeMarshaler - Adds the `epoch_time_attr`, which behaves much like `time_attr` except the Amazon DynamoDB storage type is numeric, and the serialized value is epoch seconds.

2.1.0 (2018-06-25)
------------------

Expand Down
1 change: 1 addition & 0 deletions lib/aws-record.rb
Expand Up @@ -34,6 +34,7 @@
require_relative 'aws-record/record/marshalers/date_marshaler'
require_relative 'aws-record/record/marshalers/date_time_marshaler'
require_relative 'aws-record/record/marshalers/time_marshaler'
require_relative 'aws-record/record/marshalers/epoch_time_marshaler'
require_relative 'aws-record/record/marshalers/list_marshaler'
require_relative 'aws-record/record/marshalers/map_marshaler'
require_relative 'aws-record/record/marshalers/string_set_marshaler'
Expand Down
22 changes: 22 additions & 0 deletions lib/aws-record/record/attributes.rb
Expand Up @@ -240,6 +240,28 @@ def time_attr(name, opts = {})
attr(name, Marshalers::TimeMarshaler.new(opts), opts)
end

# Define a time-type attribute for your model which persists as
# epoch-seconds.
#
# @param [Symbol] name Name of this attribute. It should be a name
# that is safe to use as a method.
# @param [Hash] opts
# @option opts [Boolean] :hash_key Set to true if this attribute is
# the hash key for the table.
# @option opts [Boolean] :range_key Set to true if this attribute is
# the range key for the table.
# @option opts [Boolean] :persist_nil Optional attribute used to
# indicate whether nil values should be persisted. If true, explicitly
# set nil values will be saved to DynamoDB as a "null" type. If false,
# nil values will be ignored and not persisted. By default, is false.
# @option opts [Object] :default_value Optional attribute used to
# define a "default value" to be used if the attribute's value on an
# item is nil or not set at persistence time.
def epoch_time_attr(name, opts = {})
opts[:dynamodb_type] = "N"
attr(name, Marshalers::EpochTimeMarshaler.new(opts), opts)
end

# Define a list-type attribute for your model.
#
# Lists do not have to be homogeneous, but they do have to be types that
Expand Down
66 changes: 66 additions & 0 deletions lib/aws-record/record/marshalers/epoch_time_marshaler.rb
@@ -0,0 +1,66 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You may not
# use this file except in compliance with the License. A copy of the License is
# located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing permissions
# and limitations under the License.

require 'time'

module Aws
module Record
module Marshalers

class EpochTimeMarshaler
def initialize(opts = {})
@use_local_time = opts[:use_local_time] ? true : false
end

def type_cast(raw_value)
value = _format(raw_value)
if !@use_local_time && value.is_a?(::Time)
value.utc
else
value
end
end

def serialize(raw_value)
time = type_cast(raw_value)
if time.nil?
nil
elsif time.is_a?(::Time)
time.to_i
else
msg = "expected a Time value or nil, got #{time.class}"
raise ArgumentError, msg
end
end

private

def _format(raw_value)
case raw_value
when nil
nil
when ''
nil
when ::Time
raw_value
when Integer # timestamp
::Time.at(raw_value)
else # Date, DateTime, or String
::Time.parse(raw_value.to_s)
end
end
end

end
end
end
5 changes: 4 additions & 1 deletion spec/aws-record/record/item_operations_spec.rb
Expand Up @@ -32,6 +32,7 @@ module Record
map_attr(:map_nil_as_nil, persist_nil: true)
map_attr(:map_no_nil_persist)
boolean_attr(:bool, database_attribute_name: "my_boolean")
epoch_time_attr(:ttl)
end
end

Expand All @@ -54,6 +55,7 @@ module Record
item.id = 1
item.date = '2015-12-14'
item.body = 'Hello!'
item.ttl = Time.parse("2018-07-09 22:02:12 UTC")
item.save!
expect(api_requests).to eq([{
table_name: "TestTable",
Expand All @@ -62,7 +64,8 @@ module Record
"MyDate" => { s: "2015-12-14" },
"body" => { s: "Hello!" },
"list_nil_to_empty" => { l: [] },
"map_nil_to_empty" => { m: {} }
"map_nil_to_empty" => { m: {} },
"ttl" => { n: "1531173732" },
},
condition_expression: "attribute_not_exists(#H)"\
" and attribute_not_exists(#R)",
Expand Down
97 changes: 97 additions & 0 deletions spec/aws-record/record/marshalers/epoch_time_marshaler_spec.rb
@@ -0,0 +1,97 @@
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You may not
# use this file except in compliance with the License. A copy of the License is
# located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing permissions
# and limitations under the License.

require 'spec_helper'
require 'time'

module Aws
module Record
module Marshalers
describe EpochTimeMarshaler do

context 'default settings' do
before(:each) do
@marshaler = EpochTimeMarshaler.new
end

describe 'type casting' do
it 'casts nil and empty string as nil' do
expect(@marshaler.type_cast(nil)).to be_nil
expect(@marshaler.type_cast('')).to be_nil
end

it 'passes through Time objects' do
expected = Time.at(1531173732)
input = Time.at(1531173732)
expect(@marshaler.type_cast(input)).to eq(expected)
end

it 'converts timestamps to Time' do
expected = Time.at(1531173732)
input = 1531173732
expect(@marshaler.type_cast(input)).to eq(expected)
end

it 'converts DateTimes to Time' do
expected = Time.parse("2009-02-13 23:31:30 UTC")
input = DateTime.parse("2009-02-13 23:31:30 UTC")
expect(@marshaler.type_cast(input)).to eq(expected)
end

it 'converts strings to Time' do
expected = Time.parse("2009-02-13 23:31:30 UTC")
input = "2009-02-13 23:31:30 UTC"
expect(@marshaler.type_cast(input)).to eq(expected)
end

it 'converts automatically to utc' do
expected = Time.parse("2016-07-20 23:31:10 UTC")
input = "2016-07-20 16:31:10 -0700"
expect(@marshaler.type_cast(input)).to eq(expected)
end

it 'raises when unable to parse as a Time' do
expect {
@marshaler.type_cast("that time when")
}.to raise_error(ArgumentError)
end
end

describe 'serialization for storage' do
it 'serializes nil as null' do
expect(@marshaler.serialize(nil)).to eq(nil)
end

it 'serializes Time in epoch seconds' do
t = Time.parse('2018-07-09 22:02:12 UTC')
expect(@marshaler.serialize(t)).to eq(1531173732)
end
end
end

context "use local time" do
before(:each) do
@marshaler = TimeMarshaler.new(use_local_time: true)
end

it 'does not automatically convert to utc' do
expected = Time.parse("2016-07-20 16:31:10 -0700")
input = "2016-07-20 16:31:10 -0700"
expect(@marshaler.type_cast(input)).to eq(expected)
end
end

end
end
end
end