tiny_dyno 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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