Class: OceanDynamo::Base

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Model, ActiveModel::Validations::Callbacks
Defined in:
lib/ocean-dynamo/base.rb,
lib/ocean-dynamo/schema.rb,
lib/ocean-dynamo/tables.rb,
lib/ocean-dynamo/queries.rb,
lib/ocean-dynamo/callbacks.rb,
lib/ocean-dynamo/attributes.rb,
lib/ocean-dynamo/persistence.rb,
lib/ocean-dynamo/class_variables.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attrs = {}) ⇒ Base



14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/ocean-dynamo/attributes.rb', line 14

def initialize(attrs={})
  run_callbacks :initialize do
    @attributes = Hash.new
    fields.each do |name, md| 
      write_attribute(name, evaluate_default(md[:default], md[:type]))
    end
    @dynamo_item = nil
    @destroyed = false
    @new_record = true
    raise UnknownPrimaryKey unless table_hash_key
  end
  attrs &&  attrs.delete_if { |k, v| !fields.has_key?(k) }
  super(attrs)
end

Instance Attribute Details

#attributesObject (readonly)

The hash of attributes and their values. Keys are strings.



7
8
9
# File 'lib/ocean-dynamo/attributes.rb', line 7

def attributes
  @attributes
end

#destroyedObject (readonly)

:nodoc:



9
10
11
# File 'lib/ocean-dynamo/attributes.rb', line 9

def destroyed
  @destroyed
end

#dynamo_itemObject (readonly)

:nodoc:



11
12
13
# File 'lib/ocean-dynamo/attributes.rb', line 11

def dynamo_item
  @dynamo_item
end

#new_recordObject (readonly)

:nodoc:



10
11
12
# File 'lib/ocean-dynamo/attributes.rb', line 10

def new_record
  @new_record
end

Class Method Details

.attribute(name, type = :string, **pairs) ⇒ Object



62
63
64
65
66
# File 'lib/ocean-dynamo/schema.rb', line 62

def self.attribute(name, type=:string, **pairs)
  raise DangerousAttributeError, "#{name} is defined by OceanDynamo" if self.dangerous_attributes.include?(name.to_s)
  attr_accessor name
  fields[name.to_s] = {type: type, default: pairs[:default]}
end

.compute_table_nameObject



52
53
54
# File 'lib/ocean-dynamo/schema.rb', line 52

def self.compute_table_name
  name.pluralize.underscore
end

.countObject



19
20
21
22
# File 'lib/ocean-dynamo/queries.rb', line 19

def self.count
  _late_connect?
  dynamo_table.item_count || -1    # The || -1 is for fake_dynamo specs.
end

.create(attributes = nil) {|object| ... } ⇒ Object

Yields:

  • (object)


11
12
13
14
15
16
# File 'lib/ocean-dynamo/persistence.rb', line 11

def self.create(attributes = nil, &block)
  object = new(attributes)
  yield(object) if block_given?
  object.save
  object
end

.create!(attributes = nil) {|object| ... } ⇒ Object

Yields:

  • (object)


19
20
21
22
23
24
# File 'lib/ocean-dynamo/persistence.rb', line 19

def self.create!(attributes = nil, &block)
  object = new(attributes)
  yield(object) if block_given?
  object.save!
  object
end

.create_tableObject



53
54
55
56
57
58
59
60
61
62
# File 'lib/ocean-dynamo/tables.rb', line 53

def self.create_table
  self.dynamo_table = dynamo_client.tables.create(table_full_name, 
    table_read_capacity_units, table_write_capacity_units,
    hash_key: { table_hash_key => fields[table_hash_key][:type]},
    range_key: table_range_key && { table_range_key => fields[table_range_key][:type]}
    )
  sleep 1 until dynamo_table.status == :active
  setup_dynamo
  true
end

.delete(hash, range = nil) ⇒ Object



27
28
29
30
31
32
# File 'lib/ocean-dynamo/persistence.rb', line 27

def self.delete(hash, range=nil)
  item = dynamo_items[hash, range]
  return false unless item.exists?
  item.delete
  true
end

.delete_tableObject



65
66
67
68
69
# File 'lib/ocean-dynamo/tables.rb', line 65

def self.delete_table
  return false unless dynamo_table.exists? && dynamo_table.status == :active
  dynamo_table.delete
  true
end

.dynamo_schema(table_hash_key = :uuid, table_range_key = nil, table_name: compute_table_name, table_name_prefix: nil, table_name_suffix: nil, read_capacity_units: 10, write_capacity_units: 5, connect: :late, create: false, locking: :lock_version, timestamps: [:created_at, :updated_at], &block) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/ocean-dynamo/schema.rb', line 5

def self.dynamo_schema(table_hash_key=:uuid, 
                       table_range_key=nil,
                       table_name: compute_table_name,
                       table_name_prefix: nil,
                       table_name_suffix: nil,
                       read_capacity_units: 10,
                       write_capacity_units: 5,
                       connect: :late,
                       create: false,
                       locking: :lock_version,
                       timestamps: [:created_at, :updated_at],
                       &block)
  # Set class vars
  self.dynamo_client = nil
  self.dynamo_table = nil
  self.dynamo_items = nil
  self.table_connected = false
  self.table_connect_policy = connect
  self.table_create_policy = create
  self.table_hash_key = table_hash_key
  self.table_range_key = table_range_key
  self.table_name = table_name
  self.table_name_prefix = table_name_prefix
  self.table_name_suffix = table_name_suffix
  self.table_read_capacity_units = read_capacity_units
  self.table_write_capacity_units = write_capacity_units
  self.lock_attribute = locking
  self.timestamp_attributes = timestamps
  # Init
  self.fields = HashWithIndifferentAccess.new
  attribute table_hash_key, :string, default: ''
  timestamp_attributes.each { |name| attribute name, :datetime } if timestamp_attributes
  attribute(lock_attribute, :integer, default: 0) if lock_attribute
  block.call
  # Define attribute accessors
  fields.each do |name, md| 
    self.class_eval "def #{name}; read_attribute('#{name.to_s}'); end"
    self.class_eval "def #{name}=(value); write_attribute('#{name.to_s}', value); end"
    self.class_eval "def #{name}?; read_attribute('#{name.to_s}').present?; end"
  end
  # Connect to AWS
  establish_db_connection if connect == true
  # Finally return the full table name
  table_full_name
end

.establish_db_connectionObject



4
5
6
7
8
9
10
11
12
13
14
# File 'lib/ocean-dynamo/tables.rb', line 4

def self.establish_db_connection
  setup_dynamo  
  if dynamo_table.exists?
    wait_until_table_is_active
    self.table_connected = true
  else
    raise(TableNotFound, table_full_name) unless table_create_policy
    create_table
  end
  set_dynamo_table_keys
end

.find(hash, range = nil, consistent: false) ⇒ Object

Raises:



4
5
6
7
8
9
# File 'lib/ocean-dynamo/queries.rb', line 4

def self.find(hash, range=nil, consistent: false)
  _late_connect?
  item = dynamo_items[hash, range]
  raise RecordNotFound unless item.exists?
  new.send(:dynamo_unpersist, item, consistent)
end

.find_by_key(*args) ⇒ Object



12
13
14
15
16
# File 'lib/ocean-dynamo/queries.rb', line 12

def self.find_by_key(*args)
  find(*args)
rescue RecordNotFound
  nil
end

.set_dynamo_table_keysObject



45
46
47
48
49
50
# File 'lib/ocean-dynamo/tables.rb', line 45

def self.set_dynamo_table_keys
  dynamo_table.hash_key = [table_hash_key, fields[table_hash_key][:type]]
  if table_range_key
    dynamo_table.range_key = [table_range_key, fields[table_range_key][:type]]
  end
end

.setup_dynamoObject



17
18
19
20
21
22
# File 'lib/ocean-dynamo/tables.rb', line 17

def self.setup_dynamo
  #self.dynamo_client = AWS::DynamoDB::Client.new(:api_version => '2012-08-10') 
  self.dynamo_client ||= AWS::DynamoDB.new
  self.dynamo_table = dynamo_client.tables[table_full_name]
  self.dynamo_items = dynamo_table.items
end

.table_full_nameObject



57
58
59
# File 'lib/ocean-dynamo/schema.rb', line 57

def self.table_full_name
  "#{table_name_prefix}#{table_name}#{table_name_suffix}"
end

.wait_until_table_is_activeObject



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/ocean-dynamo/tables.rb', line 25

def self.wait_until_table_is_active
  loop do
    case dynamo_table.status
    when :active
      set_dynamo_table_keys
      return
    when :updating, :creating
      sleep 1
      next
    when :deleting
      sleep 1 while dynamo_table.exists?
      create_table
      return
    else
      raise UnknownTableStatus.new("Unknown DynamoDB table status '#{dynamo_table.status}'")
    end
  end
end

Instance Method Details

#<=>(other_object) ⇒ Object

Allows sort on objects



31
32
33
34
35
# File 'lib/ocean-dynamo/base.rb', line 31

def <=>(other_object)
  if other_object.is_a?(self.class)
    self.to_key <=> other_object.to_key
  end
end

#==(comparison_object) ⇒ Object Also known as: eql?



8
9
10
11
12
13
# File 'lib/ocean-dynamo/base.rb', line 8

def ==(comparison_object)
  super ||
    comparison_object.instance_of?(self.class) &&
    id.present? &&
    comparison_object.id == id
end

#[](attribute) ⇒ Object



30
31
32
# File 'lib/ocean-dynamo/attributes.rb', line 30

def [](attribute)
  read_attribute attribute
end

#[]=(attribute, value) ⇒ Object



35
36
37
# File 'lib/ocean-dynamo/attributes.rb', line 35

def []=(attribute, value)
  write_attribute attribute, value
end

#assign_attributes(values) ⇒ Object



83
84
85
86
87
88
89
90
91
92
# File 'lib/ocean-dynamo/attributes.rb', line 83

def assign_attributes(values)
  return if values.blank?
  values = values.stringify_keys
  # if values.respond_to?(:permitted?)
  #   unless values.permitted?
  #     raise ActiveModel::ForbiddenAttributesError
  #   end
  # end
  values.each { |k, v| _assign_attribute(k, v) }
end

#create(options = {}) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/ocean-dynamo/persistence.rb', line 104

def create(options={})
  return false if options[:validate] != false && !valid?(:create)
  run_callbacks :commit do
    run_callbacks :save do
      run_callbacks :create do
        k = read_attribute(table_hash_key)
        write_attribute(table_hash_key, SecureRandom.uuid) if k == "" || k == nil
        set_timestamps
        dynamo_persist
        true
      end
    end
  end
end

#create_or_update(options = {}) ⇒ Object



98
99
100
101
# File 'lib/ocean-dynamo/persistence.rb', line 98

def create_or_update(options={})
  result = new_record? ? create(options) : update(options)
  result != false
end

#deleteObject



148
149
150
151
152
153
154
# File 'lib/ocean-dynamo/persistence.rb', line 148

def delete
  if persisted?
    dynamo_delete(lock: lock_attribute)
  end
  freeze
  @destroyed = true
end

#deserialize_attribute(value, metadata, type: ) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/ocean-dynamo/attributes.rb', line 159

def deserialize_attribute(value, , type: [:type])
  case type
  when :string
    return "" if value == nil
    value.is_a?(Set) ? value.to_a : value
  when :integer
    return nil if value == nil
    value.is_a?(Set) || value.is_a?(Array) ? value.collect(&:to_i) : value.to_i
  when :float
    return nil if value == nil
    value.is_a?(Set) || value.is_a?(Array) ? value.collect(&:to_f) : value.to_f
  when :boolean
    case value
    when "true"
      true
    when "false"
      false
    else
      nil
    end
  when :datetime
    return nil if value == nil
    Time.zone.at(value.to_i)
  when :serialized
    return nil if value == nil
    JSON.parse(value)
  else
    raise UnsupportedType.new(type.to_s)
  end
end

#destroyObject



134
135
136
137
138
139
140
# File 'lib/ocean-dynamo/persistence.rb', line 134

def destroy
  run_callbacks :commit do
    run_callbacks :destroy do
      delete
    end
  end
end

#destroy!Object



143
144
145
# File 'lib/ocean-dynamo/persistence.rb', line 143

def destroy!
  destroy || raise(RecordNotDestroyed)
end

#destroyed?Boolean


Instance variables and methods



41
42
43
# File 'lib/ocean-dynamo/persistence.rb', line 41

def destroyed?
  @destroyed
end

#freezeObject

Clone and freeze the attributes hash such that associations are still accessible, even on destroyed records, but cloned models will not be frozen.



20
21
22
23
# File 'lib/ocean-dynamo/base.rb', line 20

def freeze
  @attributes = @attributes.clone.freeze
  self
end

#frozen?Boolean

Returns true if the attributes hash has been frozen.



26
27
28
# File 'lib/ocean-dynamo/base.rb', line 26

def frozen?
  @attributes.frozen?
end

#idObject



40
41
42
# File 'lib/ocean-dynamo/attributes.rb', line 40

def id
  read_attribute(table_hash_key)
end

#id=(value) ⇒ Object



45
46
47
# File 'lib/ocean-dynamo/attributes.rb', line 45

def id=(value)
  write_attribute(table_hash_key, value)
end

#new_record?Boolean



46
47
48
# File 'lib/ocean-dynamo/persistence.rb', line 46

def new_record?
  @new_record
end

#persisted?Boolean



51
52
53
# File 'lib/ocean-dynamo/persistence.rb', line 51

def persisted?
  !(new_record? || destroyed?)
end

#read_attribute(attr_name) ⇒ Object



55
56
57
58
59
60
61
# File 'lib/ocean-dynamo/attributes.rb', line 55

def read_attribute(attr_name)
  attr_name = attr_name.to_s
  if attr_name == 'id' && fields[table_hash_key] != attr_name.to_sym
    return read_attribute(table_hash_key)
  end
  @attributes[attr_name]   # Type cast!
end

#read_attribute_for_validation(key) ⇒ Object



50
51
52
# File 'lib/ocean-dynamo/attributes.rb', line 50

def read_attribute_for_validation(key)
  @attributes[key.to_s]
end

#reload(**keywords) ⇒ Object



157
158
159
160
161
162
# File 'lib/ocean-dynamo/persistence.rb', line 157

def reload(**keywords)
  range_key = table_range_key && attributes[table_range_key]
  new_instance = self.class.find(id, range_key, **keywords)
  assign_attributes(new_instance.attributes)
  self
end

#save(options = {}) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
# File 'lib/ocean-dynamo/persistence.rb', line 63

def save(options={})
  if perform_validations(options)
    begin
      create_or_update
    rescue RecordInvalid
      false
    end
  else
    false
  end
end

#save!(options = {}) ⇒ Object



76
77
78
79
80
81
82
83
# File 'lib/ocean-dynamo/persistence.rb', line 76

def save!(options={})
  if perform_validations(options)
    options[:validate] = false
    create_or_update(options) || raise(RecordNotSaved)
  else
    raise RecordInvalid.new(self)
  end
end

#serialize_attribute(attribute, value, metadata = , type: ) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/ocean-dynamo/attributes.rb', line 137

def serialize_attribute(attribute, value, =fields[attribute],
                        type: [:type])
  return nil if value == nil
  case type
  when :string
    ["", []].include?(value) ? nil : value
  when :integer
    value == [] ? nil : value
  when :float
    value == [] ? nil : value
  when :boolean
    value ? "true" : "false"
  when :datetime
    value.to_i
  when :serialized
    value.to_json
  else
    raise UnsupportedType.new(type.to_s)
  end
end

#serialized_attributesObject



127
128
129
130
131
132
133
134
# File 'lib/ocean-dynamo/attributes.rb', line 127

def serialized_attributes
  result = {}
  fields.each do |attribute, |
    serialized = serialize_attribute(attribute, read_attribute(attribute), )
    result[attribute] = serialized unless serialized == nil
  end
  result
end

#to_keyObject



75
76
77
78
79
80
# File 'lib/ocean-dynamo/attributes.rb', line 75

def to_key
  return nil unless persisted?
  key = respond_to?(:id) && id
  return nil unless key
  table_range_key ? [key, read_attribute(table_range_key)] : [key]
end

#touch(name = nil) ⇒ Object

Raises:



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/ocean-dynamo/persistence.rb', line 165

def touch(name=nil)
  raise DynamoError, "can not touch on a new record object" unless persisted?
  _late_connect?
  run_callbacks :touch do
    begin
      dynamo_item.attributes.update(_handle_locking) do |u|
        set_timestamps(name).each do |k|
          u.set(k => serialize_attribute(k, read_attribute(k)))
        end
      end
    rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException
      raise OceanDynamo::StaleObjectError
    end
    self
  end
end

#type_cast_attribute_for_write(name, value, metadata = , type: ) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/ocean-dynamo/attributes.rb', line 95

def type_cast_attribute_for_write(name, value, =fields[name],
                                  type: [:type])
  case type
  when :string
    return nil if value == nil
    return value.collect(&:to_s) if value.is_a?(Array)
    value
  when :integer
    return nil if value == nil
    return value.collect(&:to_i) if value.is_a?(Array)
    value.to_i
  when :float
    return nil if value == nil
    return value.collect(&:to_f) if value.is_a?(Array)
    value.to_f
  when :boolean
    return nil if value == nil
    return true if value == true
    return true if value == "true"
    false
  when :datetime
    return nil if value == nil || !value.kind_of?(Time)
    value
  when :serialized
    return nil if value == nil
    value
  else
    raise UnsupportedType.new(type.to_s)
  end
end

#update(options = {}) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/ocean-dynamo/persistence.rb', line 120

def update(options={})
  return false if options[:validate] != false && !valid?(:update)
  run_callbacks :commit do
    run_callbacks :save do
      run_callbacks :update do
        set_timestamps
        dynamo_persist(lock: lock_attribute)
        true
      end
    end
  end
end

#update_attributes(attrs = {}) ⇒ Object



86
87
88
89
# File 'lib/ocean-dynamo/persistence.rb', line 86

def update_attributes(attrs={})
  assign_attributes(attrs)
  save
end

#update_attributes!(attrs = {}) ⇒ Object



92
93
94
95
# File 'lib/ocean-dynamo/persistence.rb', line 92

def update_attributes!(attrs={})
  assign_attributes(attrs)
  save!
end

#valid?(context = nil) ⇒ Boolean



56
57
58
59
60
# File 'lib/ocean-dynamo/persistence.rb', line 56

def valid?(context = nil)
  context ||= (new_record? ? :create : :update)
  output = super(context)
  errors.empty? && output
end

#write_attribute(attr_name, value) ⇒ Object



64
65
66
67
68
69
70
71
72
# File 'lib/ocean-dynamo/attributes.rb', line 64

def write_attribute(attr_name, value)
  attr_name = attr_name.to_s
  attr_name = table_hash_key.to_s if attr_name == 'id' && fields[table_hash_key]
  if fields.has_key?(attr_name)
    @attributes[attr_name] = type_cast_attribute_for_write(attr_name, value)
  else
    raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
  end
end