tiny_dyno 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|