tiny_dyno 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.simplecov +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Guardfile +70 -0
- data/LICENSE +41 -0
- data/README.md +121 -0
- data/Rakefile +6 -0
- data/bin/console +8 -0
- data/bin/fastcheck +25 -0
- data/bin/setup +7 -0
- data/bin/tracer +19 -0
- data/ci/tests.sh +52 -0
- data/lib/config/locales/en.yml +168 -0
- data/lib/tiny_dyno.rb +44 -0
- data/lib/tiny_dyno/adapter.rb +48 -0
- data/lib/tiny_dyno/adapter/items.rb +27 -0
- data/lib/tiny_dyno/adapter/tables.rb +103 -0
- data/lib/tiny_dyno/attributes.rb +157 -0
- data/lib/tiny_dyno/attributes/readonly.rb +56 -0
- data/lib/tiny_dyno/changeable.rb +68 -0
- data/lib/tiny_dyno/document.rb +100 -0
- data/lib/tiny_dyno/document_composition.rb +49 -0
- data/lib/tiny_dyno/errors.rb +4 -0
- data/lib/tiny_dyno/errors/attribute_errors.rb +25 -0
- data/lib/tiny_dyno/errors/hash_key_errors.rb +78 -0
- data/lib/tiny_dyno/errors/tiny_dyno_error.rb +85 -0
- data/lib/tiny_dyno/extensions.rb +1 -0
- data/lib/tiny_dyno/extensions/module.rb +28 -0
- data/lib/tiny_dyno/fields.rb +299 -0
- data/lib/tiny_dyno/fields/standard.rb +64 -0
- data/lib/tiny_dyno/hash_keys.rb +134 -0
- data/lib/tiny_dyno/loggable.rb +69 -0
- data/lib/tiny_dyno/persistable.rb +88 -0
- data/lib/tiny_dyno/range_attributes.rb +15 -0
- data/lib/tiny_dyno/stateful.rb +95 -0
- data/lib/tiny_dyno/tables.rb +98 -0
- data/lib/tiny_dyno/version.rb +3 -0
- data/tiny_dyno.gemspec +41 -0
- metadata +226 -0
data/lib/tiny_dyno.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'aws-sdk'
|
3
|
+
|
4
|
+
if ENV['RACK_ENV'] == 'development'
|
5
|
+
require 'pry'
|
6
|
+
require 'awesome_print'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'tiny_dyno/extensions'
|
10
|
+
|
11
|
+
require 'tiny_dyno/version'
|
12
|
+
require 'tiny_dyno/loggable'
|
13
|
+
require 'tiny_dyno/errors'
|
14
|
+
require 'tiny_dyno/document'
|
15
|
+
require 'tiny_dyno/adapter'
|
16
|
+
|
17
|
+
I18n.load_path << File.join(File.dirname(__FILE__), "config", "locales", "en.yml")
|
18
|
+
|
19
|
+
module TinyDyno
|
20
|
+
extend Loggable
|
21
|
+
extend self
|
22
|
+
|
23
|
+
# Register a model in the application with TinyDyno.
|
24
|
+
#
|
25
|
+
# @example Register a model.
|
26
|
+
# config.register_model(Band)
|
27
|
+
#
|
28
|
+
# @param [ Class ] klass The model to register.
|
29
|
+
def register_model(klass)
|
30
|
+
models.push(klass) unless models.include?(klass)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get all the models in the application - this is everything that includes
|
34
|
+
# TinyDyno::Document.
|
35
|
+
#
|
36
|
+
# @example Get all the models.
|
37
|
+
# config.models
|
38
|
+
#
|
39
|
+
# @return [ Array<Class> ] All the models in the application.
|
40
|
+
def models
|
41
|
+
@models ||= []
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
require 'tiny_dyno/adapter/tables'
|
4
|
+
require 'tiny_dyno/adapter/items'
|
5
|
+
|
6
|
+
module TinyDyno
|
7
|
+
|
8
|
+
# Interactions with the DynamoDB store through aws-sdk-v2
|
9
|
+
module Adapter
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
extend self
|
12
|
+
|
13
|
+
attr_reader :table_names
|
14
|
+
|
15
|
+
@table_names = []
|
16
|
+
|
17
|
+
def connect
|
18
|
+
connection
|
19
|
+
connected?
|
20
|
+
end
|
21
|
+
|
22
|
+
def connected?
|
23
|
+
begin
|
24
|
+
connection.list_tables(limit: 1)
|
25
|
+
rescue Errno::ECONNREFUSED, Aws::DynamoDB::Errors::UnrecognizedClientException => e
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
return true if @connection.class == Aws::DynamoDB::Client
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
|
32
|
+
def disconnect!
|
33
|
+
@connection = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def connection
|
39
|
+
unless @connection
|
40
|
+
TinyDyno.logger.info 'setting up new connection ... ' if TinyDyno.logger
|
41
|
+
@connection = Aws::DynamoDB::Client.new
|
42
|
+
update_table_cache
|
43
|
+
end
|
44
|
+
@connection
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module TinyDyno
|
2
|
+
module Adapter
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def update_item(update_item_request:)
|
6
|
+
connection.update_item(update_item_request).successful?
|
7
|
+
end
|
8
|
+
|
9
|
+
def put_item(put_item_request:)
|
10
|
+
connection.put_item(put_item_request).successful?
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_item(get_item_request:)
|
14
|
+
resp = connection.get_item(get_item_request)
|
15
|
+
if resp.respond_to?(:item)
|
16
|
+
resp.item
|
17
|
+
else
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete_item(request:)
|
23
|
+
connection.delete_item(request).successful?
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module TinyDyno
|
2
|
+
module Adapter
|
3
|
+
extend self
|
4
|
+
|
5
|
+
# Terminology in here is directly derived from the aws sdk language
|
6
|
+
#
|
7
|
+
# http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#create_table-instance_method
|
8
|
+
#
|
9
|
+
# The current implementation is a strict 1:1 relation of 1 Model to 1 table
|
10
|
+
|
11
|
+
# Answer, whether a table is present, first by cache lookup and if miss on datastore
|
12
|
+
#
|
13
|
+
# @example Does the table exists?
|
14
|
+
# TinyDyno::Adapter.table_exists?(table_name: Person.table_name)
|
15
|
+
# @return [ true ] if the table is present
|
16
|
+
|
17
|
+
def table_exists?(table_name:)
|
18
|
+
return true if @table_names.include?(table_name)
|
19
|
+
update_table_cache
|
20
|
+
@table_names.include?(table_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
# http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#create_table-instance_method
|
24
|
+
# expect create_table_request to conform to above schema
|
25
|
+
#
|
26
|
+
# Send the actual table creation to DynamoDB
|
27
|
+
#
|
28
|
+
# @example Create the table for the class
|
29
|
+
# Person.create_table
|
30
|
+
#
|
31
|
+
# @return [ true ] if the operation succeeds
|
32
|
+
|
33
|
+
def create_table(create_table_request)
|
34
|
+
table_settings = {
|
35
|
+
provisioned_throughput: {
|
36
|
+
read_capacity_units: 200,
|
37
|
+
write_capacity_units: 200,
|
38
|
+
},
|
39
|
+
}.merge!(create_table_request)
|
40
|
+
|
41
|
+
# Should or shouldn't we?
|
42
|
+
# begin
|
43
|
+
# resp = connection.describe_table(table_name: table_settings[:table_name])
|
44
|
+
# rescue Aws::DynamoDB::Errors::ResourceNotFoundException
|
45
|
+
# ensure
|
46
|
+
# if resp.respond_to?(:table)
|
47
|
+
# p "Warning, table was already present : #{ table_settings[:table_name]}"
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
connection.create_table(table_settings)
|
51
|
+
if wait_on_table_status(table_status: :table_exists, table_name: table_settings[:table_name])
|
52
|
+
update_table_cache
|
53
|
+
return true
|
54
|
+
else
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete_table(table_name:)
|
60
|
+
begin
|
61
|
+
connection.describe_table(table_name: table_name)
|
62
|
+
rescue Aws::DynamoDB::Errors::ResourceNotFoundException
|
63
|
+
# table not there anyway
|
64
|
+
return true
|
65
|
+
end
|
66
|
+
if wait_on_table_status(table_status: :table_exists, table_name: table_name)
|
67
|
+
connection.delete_table(table_name: table_name)
|
68
|
+
else
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
wait_on_table_status(table_status: :table_not_exists, table_name: table_name)
|
72
|
+
update_table_cache
|
73
|
+
return true unless table_exists?(table_name: table_name)
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
|
77
|
+
# Hold a cache of available table names in an instance variable
|
78
|
+
#
|
79
|
+
def update_table_cache
|
80
|
+
return unless connected?
|
81
|
+
new_table_names = connection.list_tables.table_names
|
82
|
+
@table_names = new_table_names
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Use the aws-sdk provided waiter methods to wait on either
|
88
|
+
# table_exists, table_not_exists
|
89
|
+
def wait_on_table_status(table_status:, table_name:)
|
90
|
+
begin
|
91
|
+
connection.wait_until(table_status, table_name: table_name) do |w|
|
92
|
+
w.interval = 1
|
93
|
+
w.max_attempts = 10
|
94
|
+
end
|
95
|
+
rescue Aws::Waiters::Errors => e
|
96
|
+
p "Waiter failed: #{ e .inspect }"
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
return true
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'active_model/attribute_methods'
|
2
|
+
|
3
|
+
require 'tiny_dyno/attributes/readonly'
|
4
|
+
|
5
|
+
module TinyDyno
|
6
|
+
module Attributes
|
7
|
+
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
include Readonly
|
11
|
+
|
12
|
+
attr_reader :attributes
|
13
|
+
|
14
|
+
# Read a value from the document attributes. If the value does not exist
|
15
|
+
# it will return nil.
|
16
|
+
#
|
17
|
+
# @example Read an attribute.
|
18
|
+
# person.read_attribute(:title)
|
19
|
+
#
|
20
|
+
# @example Read an attribute (alternate syntax.)
|
21
|
+
# person[:title]
|
22
|
+
#
|
23
|
+
# @param [ String, Symbol ] name The name of the attribute to get.
|
24
|
+
#
|
25
|
+
# @return [ Object ] The value of the attribute.
|
26
|
+
#
|
27
|
+
# @since 1.0.0
|
28
|
+
def read_attribute(name)
|
29
|
+
normalized = database_field_name(name.to_s)
|
30
|
+
if attribute_missing?(normalized)
|
31
|
+
raise ActiveModel::MissingAttributeError, "Missing attribute: '#{name}'."
|
32
|
+
end
|
33
|
+
attributes[normalized]
|
34
|
+
end
|
35
|
+
alias :[] :read_attribute
|
36
|
+
|
37
|
+
|
38
|
+
# Write a single attribute to the document attribute hash. This will
|
39
|
+
# also fire the before and after update callbacks, and perform any
|
40
|
+
# necessary typecasting.
|
41
|
+
# called from within ActiveModel
|
42
|
+
#
|
43
|
+
# @example Write the attribute.
|
44
|
+
# person.write_attribute(:title, "Mr.")
|
45
|
+
#
|
46
|
+
# @example Write the attribute (alternate syntax.)
|
47
|
+
# person[:title] = "Mr."
|
48
|
+
#
|
49
|
+
# @param [ String, Symbol ] name The name of the attribute to update.
|
50
|
+
# @param [ Object ] value The value to set for the attribute.
|
51
|
+
#
|
52
|
+
# @since 1.0.0
|
53
|
+
def write_attribute(name, value)
|
54
|
+
access = database_field_name(name.to_s)
|
55
|
+
typed_value = typed_value_for(access, value)
|
56
|
+
if attribute_writable?(access)
|
57
|
+
unless attributes[access] == typed_value|| attribute_changed?(access)
|
58
|
+
attribute_will_change!(access)
|
59
|
+
end
|
60
|
+
attributes[access] = typed_value
|
61
|
+
typed_value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
alias :[]= :write_attribute
|
65
|
+
|
66
|
+
# Process the provided attributes casting them to their proper values if a
|
67
|
+
# field exists for them on the document. This will be limited to only the
|
68
|
+
# attributes provided in the suppied +Hash+ so that no extra nil values get
|
69
|
+
# put into the document's attributes.
|
70
|
+
#
|
71
|
+
# @example Process the attributes.
|
72
|
+
# person.process_attributes(:title => "sir", :age => 40)
|
73
|
+
#
|
74
|
+
# @param [ Hash ] attrs The attributes to set.
|
75
|
+
#
|
76
|
+
# @since 2.0.0.rc.7
|
77
|
+
def process_attributes(attrs = nil)
|
78
|
+
attrs ||= {}
|
79
|
+
if !attrs.empty?
|
80
|
+
attrs.each_pair do |key, value|
|
81
|
+
process_attribute(key, value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
# context?
|
85
|
+
# yield self if block_given?
|
86
|
+
end
|
87
|
+
|
88
|
+
# If the attribute is dynamic, add a field for it with a type of object
|
89
|
+
# and then either way set the value.
|
90
|
+
#
|
91
|
+
# @example Process the attribute.
|
92
|
+
# document.process_attribute(name, value)
|
93
|
+
#
|
94
|
+
# @param [ Symbol ] name The name of the field.
|
95
|
+
# @param [ Object ] value The value of the field.
|
96
|
+
#
|
97
|
+
# @since 2.0.0.rc.7
|
98
|
+
def process_attribute(name, value)
|
99
|
+
responds = respond_to?("#{name}=", true)
|
100
|
+
raise TinyDyno::Errors::UnknownAttribute.new(self.class, name) unless responds
|
101
|
+
send("#{name}=", value)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Return the typecasted value for a field.
|
105
|
+
# Based on the field option, type
|
106
|
+
# which is mandatory
|
107
|
+
#
|
108
|
+
# @example Get the value typecasted.
|
109
|
+
# person.typed_value_for(:title, :sir)
|
110
|
+
#
|
111
|
+
# @param [ String, Symbol ] key The field name.
|
112
|
+
# @param [ Object ] value The uncast value.
|
113
|
+
#
|
114
|
+
# @return [ Object ] The cast value.
|
115
|
+
#
|
116
|
+
# @since 1.0.0
|
117
|
+
def typed_value_for(key, value)
|
118
|
+
# raise MissingAttributeError if fields[key].nil? and hash_keys.find_index { |a| a[:attr] == key }.nil?
|
119
|
+
raise MissingAttributeError if fields[key].nil?
|
120
|
+
typed_class = self.fields[key].options[:type]
|
121
|
+
return (self.class.document_typed(klass: typed_class, value: value))
|
122
|
+
end
|
123
|
+
|
124
|
+
# Determine if the attribute is missing from the document, due to loading
|
125
|
+
# it from the database with missing fields.
|
126
|
+
#
|
127
|
+
# @example Is the attribute missing?
|
128
|
+
# document.attribute_missing?("test")
|
129
|
+
#
|
130
|
+
# @param [ String ] name The name of the attribute.
|
131
|
+
#
|
132
|
+
# @return [ true, false ] If the attribute is missing.
|
133
|
+
#
|
134
|
+
# @since 4.0.0
|
135
|
+
def attribute_missing?(name)
|
136
|
+
return (!self.fields.keys.include?(name))
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
|
142
|
+
module ClassMethods
|
143
|
+
|
144
|
+
# convert to the type used on the Document
|
145
|
+
def document_typed(klass:, value: )
|
146
|
+
if klass == String
|
147
|
+
value.blank? ? nil : value.to_s
|
148
|
+
elsif (klass == Integer or klass == Fixnum )
|
149
|
+
value.to_i
|
150
|
+
else
|
151
|
+
value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module TinyDyno
|
3
|
+
module Attributes
|
4
|
+
|
5
|
+
# This module defines behaviour for readonly attributes.
|
6
|
+
module Readonly
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
class_attribute :readonly_attributes
|
11
|
+
self.readonly_attributes = ::Set.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Are we able to write the attribute with the provided name?
|
15
|
+
#
|
16
|
+
# @example Can we write the attribute?
|
17
|
+
# model.attribute_writable?(:title)
|
18
|
+
#
|
19
|
+
# @param [ String, Symbol ] name The name of the field.
|
20
|
+
#
|
21
|
+
# @return [ true, false ] If the document is new, or if the field is not
|
22
|
+
# readonly.
|
23
|
+
#
|
24
|
+
# @since 3.0.0
|
25
|
+
def attribute_writable?(name)
|
26
|
+
new_record? || !readonly_attributes.include?(database_field_name(name))
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
|
31
|
+
# Defines an attribute as readonly. This will ensure that the value for
|
32
|
+
# the attribute is only set when the document is new or we are
|
33
|
+
# creating. In other cases, the field write will be ignored with the
|
34
|
+
# exception of #remove_attribute and #update_attribute, where an error
|
35
|
+
# will get raised.
|
36
|
+
#
|
37
|
+
# @example Flag fields as readonly.
|
38
|
+
# class Band
|
39
|
+
# include TinyDyno::Document
|
40
|
+
# field :name, type: String
|
41
|
+
# field :genre, type: String
|
42
|
+
# attr_readonly :name, :genre
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# @param [ Array<Symbol> ] names The names of the fields.
|
46
|
+
#
|
47
|
+
# @since 3.0.0
|
48
|
+
def attr_readonly(*names)
|
49
|
+
names.each do |name|
|
50
|
+
readonly_attributes << database_field_name(name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module TinyDyno
|
2
|
+
|
3
|
+
# Defines behaviour for dirty tracking.
|
4
|
+
#
|
5
|
+
# @since 4.0.0
|
6
|
+
module Changeable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# Get the changed attributes for the document.
|
10
|
+
#
|
11
|
+
# @example Get the changed attributes.
|
12
|
+
# model.changed
|
13
|
+
#
|
14
|
+
# @return [ Array<String> ] The changed attributes.
|
15
|
+
#
|
16
|
+
# @since 2.4.0
|
17
|
+
def changed
|
18
|
+
changed_attributes.keys.select { |attr| attribute_change(attr) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get the attribute changes.
|
22
|
+
#
|
23
|
+
# @example Get the attribute changes.
|
24
|
+
# model.changed_attributes
|
25
|
+
#
|
26
|
+
# @return [ Hash<String, Object> ] The attribute changes.
|
27
|
+
#
|
28
|
+
# @since 2.4.0
|
29
|
+
def changed_attributes
|
30
|
+
@changed_attributes ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get all the changes for the document.
|
34
|
+
#
|
35
|
+
# @example Get all the changes.
|
36
|
+
# model.changes
|
37
|
+
#
|
38
|
+
# @return [ Hash<String, Array<Object, Object> ] The changes.
|
39
|
+
#
|
40
|
+
# @since 2.4.0
|
41
|
+
def changes
|
42
|
+
_changes = {}
|
43
|
+
changed.each do |attr|
|
44
|
+
change = attribute_change(attr)
|
45
|
+
_changes[attr] = change if change
|
46
|
+
end
|
47
|
+
_changes
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Get the old and new value for the provided attribute.
|
53
|
+
#
|
54
|
+
# @example Get the attribute change.
|
55
|
+
# model.attribute_change("name")
|
56
|
+
#
|
57
|
+
# @param [ String ] attr The name of the attribute.
|
58
|
+
#
|
59
|
+
# @return [ Array<Object> ] The old and new values.
|
60
|
+
#
|
61
|
+
# @since 2.1.0
|
62
|
+
def attribute_change(attr)
|
63
|
+
attr = database_field_name(attr)
|
64
|
+
[changed_attributes[attr], attributes[attr]] if attribute_changed?(attr)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|