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.
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