commutator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ module Commutator
2
+ class ItemModifiers
3
+ def initialize(modifiers, factory: false)
4
+ @modifiers = modifiers
5
+ @factory = factory
6
+ end
7
+
8
+ def factory?
9
+ @factory
10
+ end
11
+
12
+ # This is a mess, but I wasn't sure how else to guarantee to
13
+ # call Procs at the time of collection creation
14
+ def expand_proc_modifiers
15
+ return self unless factory?
16
+
17
+ self.class.new(@modifiers.map(&:call))
18
+ end
19
+
20
+ def modify(item)
21
+ @modifiers.each { |modifier| modifier.call(item) }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,72 @@
1
+ module Commutator
2
+ module Model
3
+ # This module provides methods related to registering attributes
4
+ # and generating attr_accessors, ultimately for the purpose of
5
+ # enabling Commutator::Model to know which attributes to send to the db.
6
+ module Attributes
7
+ extend ActiveSupport::Concern
8
+
9
+ def assign_attributes(attrs = {})
10
+ attrs.slice(*attribute_names).each do |attr_name, value|
11
+ send("#{attr_name}=", value)
12
+ end
13
+ end
14
+
15
+ def attributes
16
+ attribute_names.each_with_object({}) do |attr_name, hash|
17
+ hash[attr_name] = send(attr_name)
18
+ end
19
+ end
20
+
21
+ def attribute_names
22
+ self.class.attribute_names
23
+ end
24
+
25
+ private
26
+
27
+ def convert_type(value, options)
28
+ return if options.fetch(:allow_nil, true) && value.nil?
29
+
30
+ case options[:type]
31
+ when :array then value.to_a
32
+ when :boolean then !!value
33
+ when :float then value.to_f
34
+ when :hash then value.to_h
35
+ when :integer then value.to_i
36
+ when :set then Set.new(value)
37
+ when :string then value.to_s
38
+ else value
39
+ end
40
+ end
41
+
42
+ # :nodoc:
43
+ module ClassMethods
44
+ def attribute(*attr_names)
45
+ options = attr_names.extract_options!
46
+
47
+ attr_names.each do |attr_name|
48
+ attribute_names << attr_name
49
+
50
+ # Skip reader and writer methods entirely
51
+ next if options[:accessor] == false
52
+
53
+ define_writer(attr_name, options) unless options[:writer] == false
54
+ attr_reader attr_name unless options[:reader] == false
55
+ end
56
+ end
57
+
58
+ def attribute_names
59
+ @attribute_names ||= Set.new
60
+ end
61
+
62
+ private
63
+
64
+ def define_writer(attr_name, options)
65
+ define_method "#{attr_name}=" do |value|
66
+ instance_variable_set("@#{attr_name}", convert_type(value, options))
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,41 @@
1
+ module Commutator
2
+ module Model
3
+ module Hooks
4
+ extend ActiveSupport::Concern
5
+
6
+ def run_before_hooks(operation, options)
7
+ self.class.before_hooks[operation].each do |method_name|
8
+ send(method_name, options)
9
+ end
10
+
11
+ options
12
+ end
13
+
14
+ module ClassMethods
15
+ API_ITEM_OPERATIONS.each do |operation|
16
+ define_method "before_#{operation}" do |*args|
17
+ add_before_hook(operation, *args)
18
+ end
19
+ end
20
+
21
+ def add_before_hook(operation, *method_names)
22
+ method_names.each do |method_name|
23
+ before_hooks[operation] << method_name
24
+ end
25
+ end
26
+
27
+ def before_hooks
28
+ @before_hooks ||= Hash.new { |h, k| h[k] = [] }
29
+ end
30
+
31
+ def run_before_hooks(operation, options)
32
+ before_hooks[operation].each do |method_name|
33
+ send(method_name, options)
34
+ end
35
+
36
+ options
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ module Commutator
2
+ module Model
3
+ # Some basic configuration related to a Dynamo table
4
+ module TableConfiguration
5
+ extend ActiveSupport::Concern
6
+
7
+ delegate :primary_key_hash_name,
8
+ :primary_key_range_name,
9
+ to: 'self.class'
10
+
11
+ def primary_key_hash
12
+ send(primary_key_hash_name)
13
+ end
14
+
15
+ def primary_key_range
16
+ send(primary_key_range_name) if primary_key_range_name.present?
17
+ end
18
+
19
+ def table_name
20
+ self.class.table_name
21
+ end
22
+
23
+ # :nodoc:
24
+ module ClassMethods
25
+ attr_reader :primary_key_hash_name,
26
+ :primary_key_range_name
27
+
28
+ def primary_key(options = {})
29
+ @primary_key_hash_name = options[:hash]
30
+ @primary_key_range_name = options[:range]
31
+ end
32
+
33
+ def table_name(table_name = nil)
34
+ return @table_name unless table_name.present?
35
+
36
+ @table_name = table_name
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,239 @@
1
+ require "commutator/model/attributes"
2
+ require "commutator/model/table_configuration"
3
+ require "commutator/model/hooks"
4
+
5
+ module Commutator
6
+ # Basic CRUD wrapper for items in a dynamodb table.
7
+ #
8
+ # This module is focused on collections of homogenous items within a
9
+ # single table.
10
+ #
11
+ # TODO: support multiple tables
12
+ #
13
+ # class Person
14
+ # include Commutator::Model
15
+ #
16
+ # attribute :first_name,
17
+ # :last_name,
18
+ # :email
19
+ #
20
+ # attribute :age, type: :integer
21
+ # attribute :favorite_color, writer: false
22
+ #
23
+ #
24
+ # primary_key hash: :email, range: :age
25
+ #
26
+ # def favorite_color=(color)
27
+ # @color = color.downcase
28
+ # end
29
+ # end
30
+ module Model
31
+ extend ActiveSupport::Concern
32
+
33
+ include ActiveModel::Model
34
+ include Commutator::Model::Attributes
35
+ include Commutator::Model::TableConfiguration
36
+ include Commutator::Model::Hooks
37
+
38
+ # These build up some basic Dynamo request options related to a particular
39
+ # api action so that we don't have to specify them every time. They don't
40
+ # override any values if they are present.
41
+ included do
42
+ before_put_item :configure_default_put_item
43
+ before_update_item :configure_default_update_item
44
+ before_delete_item :configure_default_delete_item
45
+
46
+ before_query :configure_default_query
47
+ before_scan :configure_default_scan
48
+ before_get_item :configure_default_get_item
49
+
50
+ class_attribute :collection_item_modifiers, instance_accessor: false
51
+ end
52
+
53
+ delegate :client, :options_class, to: 'self.class'
54
+
55
+ def initialize(attrs = {})
56
+ assign_attributes(attrs.symbolize_keys)
57
+ end
58
+
59
+ def put_item_options
60
+ self.class.build_options_proxy(:put_item, self)
61
+ end
62
+
63
+ def update_item_options
64
+ self.class.build_options_proxy(:update_item, self)
65
+ end
66
+
67
+ def delete_item_options
68
+ self.class.build_options_proxy(:delete_item, self)
69
+ end
70
+
71
+ def put_item(options = nil)
72
+ dynamo_request(:put_item, options) unless invalid?
73
+ errors.empty?
74
+ end
75
+
76
+ def update_item(options = nil)
77
+ dynamo_request(:update_item, options) unless invalid?
78
+ errors.empty?
79
+ end
80
+
81
+ def delete_item(options = nil)
82
+ dynamo_request(:delete_item, options)
83
+ return false if errors.present?
84
+
85
+ @deleted = true
86
+ freeze
87
+ end
88
+
89
+ def deleted?
90
+ @deleted == true
91
+ end
92
+
93
+ def ==(other)
94
+ self.class == other.class &&
95
+ primary_key_hash == other.primary_key_hash &&
96
+ primary_key_range == other.primary_key_range &&
97
+ attributes == other.attributes
98
+ end
99
+
100
+ private
101
+
102
+ def configure_default_put_item(options)
103
+ options
104
+ .table_name(table_name)
105
+ .item(attributes.stringify_keys)
106
+ end
107
+
108
+ def configure_default_update_item(options)
109
+ options.table_name(table_name)
110
+ end
111
+
112
+ def configure_default_delete_item(options)
113
+ options
114
+ .table_name(table_name)
115
+ .with_key { |key| key[primary_key_hash_name] = primary_key_hash }
116
+
117
+ return unless primary_key_range.present?
118
+
119
+ options.with_key { |key| key[primary_key_range_name] = primary_key_range }
120
+ end
121
+
122
+ def dynamo_request(operation, options)
123
+ options ||= self.class.options_class(operation).new
124
+ run_before_hooks(operation, options)
125
+ client.send(operation, options)
126
+ rescue Aws::DynamoDB::Errors::ValidationException,
127
+ Aws::DynamoDB::Errors::ServiceError => e
128
+ errors.add(:base, e.message)
129
+ end
130
+
131
+ # :nodoc:
132
+ module ClassMethods
133
+ attr_writer :client
134
+
135
+ def inherited(subclass)
136
+ subclass.attribute_names.merge(attribute_names)
137
+ before_hooks.each { |k, v| subclass.before_hooks[k] = v.dup }
138
+
139
+ subclass.table_name(table_name)
140
+ subclass.primary_key(hash: primary_key_hash_name,
141
+ range: primary_key_range_name)
142
+
143
+ scopes = const_defined?("Scopes", false) ? const_get("Scopes") : nil
144
+ subclass.const_set("Scopes", Module.new { include scopes }) if scopes
145
+ end
146
+
147
+ def client
148
+ @client ||= ::Commutator::SimpleClient.new
149
+ end
150
+
151
+ def create(attrs)
152
+ new(attrs).tap { |dp| dp.put_item_options.execute }
153
+ end
154
+
155
+ def modify_collection_items_with(*modifiers, factory: false)
156
+ self.collection_item_modifiers = [ItemModifiers.new(modifiers, factory: factory)].unshift(*collection_item_modifiers)
157
+ end
158
+
159
+ def get_item_options
160
+ build_options_proxy(:get_item)
161
+ end
162
+
163
+ def query_options
164
+ build_options_proxy(:query)
165
+ end
166
+
167
+ def scan_options
168
+ build_options_proxy(:scan)
169
+ end
170
+
171
+ def get_item(options = build_options_proxy(:get_item))
172
+ item = client.get_item(options).item
173
+ new(item) unless item.nil?
174
+ end
175
+
176
+ def query(options = build_options_proxy(:query))
177
+ collection_for(:query, options)
178
+ end
179
+
180
+ def scan(options = build_options_proxy(:scan))
181
+ collection_for(:scan, options)
182
+ end
183
+
184
+ def build_options_proxy(operation, context = self)
185
+ Options::Proxy.new(context, operation)
186
+ end
187
+
188
+ def options_class(operation)
189
+ @scoped_options ||= Hash.new do |h, k|
190
+ scopes = self.const_defined?("Scopes", false) ? self.const_get("Scopes") : nil
191
+ const_name = k.to_s.camelize
192
+ h[k] = enhance_options(const_name, scopes)
193
+ end
194
+ @scoped_options[operation]
195
+ end
196
+
197
+ def method_missing(method, *args)
198
+ super unless respond_to?(method)
199
+ query_options.send(method, *args)
200
+ end
201
+
202
+ def respond_to?(method)
203
+ super || (const_defined?(:Scopes, false) && const_get(:Scopes).method_defined?(method))
204
+ end
205
+
206
+ private
207
+
208
+ def enhance_options(const_name, scopes = nil)
209
+ Class.new(Options.const_get(const_name)) do
210
+ include ::Commutator::Util::Fluent
211
+ include scopes if scopes && %w[Query Scan].include?(const_name)
212
+
213
+ fluent_accessor :_proxy
214
+ delegate :context, to: :_proxy
215
+ end
216
+ end
217
+
218
+ def collection_for(operation, options)
219
+ Collection.new(
220
+ client.send(operation, options),
221
+ self,
222
+ modifiers: Array(collection_item_modifiers)
223
+ )
224
+ end
225
+
226
+ def configure_default_query(options)
227
+ options.table_name(table_name)
228
+ end
229
+
230
+ def configure_default_scan(options)
231
+ options.table_name(table_name)
232
+ end
233
+
234
+ def configure_default_get_item(options)
235
+ options.table_name(table_name)
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,48 @@
1
+ module Commutator
2
+ module Options
3
+ class DeleteItem
4
+ include Util::Fluent
5
+
6
+ fluent_accessor :key,
7
+ :condition_expression,
8
+ :expression_attribute_names,
9
+ :expression_attribute_values,
10
+ :table_name,
11
+ :return_values,
12
+ :return_consumed_capacity,
13
+ :return_item_collection_metrics
14
+
15
+ fluent_wrapper :key,
16
+ :condition_expression,
17
+ :expression_attribute_names,
18
+ :expression_attribute_values
19
+
20
+ def initialize
21
+ @key = {}
22
+
23
+ @expression_attribute_names = Expressions::AttributeNames.new
24
+ @expression_attribute_values = Expressions::AttributeValues.new
25
+
26
+ @condition_expression = Expressions::ConditionExpression.new(
27
+ attribute_names: @expression_attribute_names,
28
+ attribute_values: @expression_attribute_values)
29
+ end
30
+
31
+ def to_h
32
+ hash = {
33
+ key: key,
34
+ table_name: table_name,
35
+ return_values: return_values,
36
+ return_consumed_capacity: return_consumed_capacity,
37
+ return_item_collection_metrics: return_item_collection_metrics,
38
+ expression_attribute_names: expression_attribute_names.to_h,
39
+ expression_attribute_values: expression_attribute_values.to_h,
40
+ condition_expression: condition_expression.to_s(wrap: false)
41
+ }
42
+
43
+ hash.keep_if { |_key, value| value.present? || value == false }
44
+ end
45
+ alias :to_hash :to_h
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ module Commutator
2
+ module Options
3
+ class GetItem
4
+ include Util::Fluent
5
+
6
+ fluent_accessor :key,
7
+ :projection_expression,
8
+ :expression_attribute_names,
9
+ :consistent_read,
10
+ :table_name,
11
+ :return_consumed_capacity
12
+
13
+ fluent_wrapper :key,
14
+ :projection_expression,
15
+ :expression_attribute_names
16
+
17
+ def initialize
18
+ @key = {}
19
+
20
+ @expression_attribute_names = Expressions::AttributeNames.new
21
+
22
+ @projection_expression = Expressions::ProjectionExpression.new(
23
+ attribute_names: @expression_attribute_names)
24
+ end
25
+
26
+ def to_h
27
+ hash = {
28
+ table_name: table_name,
29
+ expression_attribute_names: expression_attribute_names.to_h,
30
+ consistent_read: consistent_read,
31
+ return_consumed_capacity: return_consumed_capacity,
32
+ projection_expression: projection_expression.to_s,
33
+ key: key
34
+ }
35
+
36
+ hash.keep_if { |_key, value| value.present? || value == false }
37
+ end
38
+ alias :to_hash :to_h
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,90 @@
1
+ module Commutator
2
+ module Options
3
+ class Proxy
4
+ attr_reader :proxied_history, :options
5
+
6
+ def initialize(context, callback_name)
7
+ @context = context
8
+ @callback_name = callback_name
9
+ @proxied_history = []
10
+
11
+ @options = instantiate_options
12
+ end
13
+
14
+ class Operation
15
+ attr_reader :method, :args, :block
16
+
17
+ def initialize(method, args, block)
18
+ @method = method
19
+ @args = args
20
+ @block = block
21
+ end
22
+
23
+ def apply(options, chainable_history = nil)
24
+ options.send(method, *args, &block).tap do |result|
25
+ # if result == self then that was a call to #with_context
26
+ chainable_history << self if chainable_history && result == options
27
+ end
28
+ end
29
+ end
30
+
31
+ def method_missing(method_name, *args, &block)
32
+ super unless options.respond_to?(method_name)
33
+
34
+ operation = Operation.new(method_name, args, block)
35
+ result = operation.apply(@options, @proxied_history)
36
+ result != options ? result : self
37
+ end
38
+
39
+ def respond_to?(*args)
40
+ super || options.respond_to?(*args)
41
+ end
42
+
43
+ def context(context)
44
+ @context = context
45
+ @options = rehydrate_options
46
+ self
47
+ end
48
+
49
+ def options
50
+ @options ||= rehydrate_options
51
+ end
52
+
53
+ def execute
54
+ @context.send(callback_name, options)
55
+ end
56
+
57
+ def first
58
+ # TODO: asc / desc only work on Query (not Scan)
59
+ limit(1).asc.execute.items.first
60
+ end
61
+
62
+ def last
63
+ # TODO: asc / desc only work on Query (not Scan)
64
+ limit(1).desc.execute.items.first
65
+ end
66
+
67
+ def count
68
+ response = @context.client.send(callback_name, options.dup.select("COUNT"))
69
+ response.inject(0) { |sum, page| sum + page.count }
70
+ end
71
+
72
+ private
73
+
74
+ def instantiate_options
75
+ @context.run_before_hooks(
76
+ @callback_name,
77
+ @context.options_class(@callback_name).new._proxy(self)
78
+ )
79
+ end
80
+
81
+ def rehydrate_options
82
+ instantiate_options.tap do |options|
83
+ proxied_history.each { |operation| operation.apply(options) }
84
+ end
85
+ end
86
+
87
+ attr_reader :callback_name
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,46 @@
1
+ module Commutator
2
+ module Options
3
+ class PutItem
4
+ include Util::Fluent
5
+
6
+ fluent_accessor :item,
7
+ :condition_expression,
8
+ :expression_attribute_names,
9
+ :expression_attribute_values,
10
+ :return_consumed_capacity,
11
+ :return_item_collection_metrics,
12
+ :return_values,
13
+ :table_name
14
+
15
+ fluent_wrapper :item,
16
+ :condition_expression,
17
+ :expression_attribute_names,
18
+ :expression_attribute_values
19
+
20
+ def initialize
21
+ @expression_attribute_names ||= Expressions::AttributeNames.new
22
+ @expression_attribute_values ||= Expressions::AttributeValues.new
23
+
24
+ @condition_expression ||= Expressions::ConditionExpression.new(
25
+ attribute_names: @expression_attribute_names,
26
+ attribute_values: @expression_attribute_values)
27
+ end
28
+
29
+ def to_h
30
+ hash = {
31
+ condition_expression: condition_expression.to_s(wrap: false),
32
+ expression_attribute_names: expression_attribute_names.to_h,
33
+ expression_attribute_values: expression_attribute_values.to_h,
34
+ item: item.to_h,
35
+ return_consumed_capacity: return_consumed_capacity,
36
+ return_item_collection_metrics: return_item_collection_metrics,
37
+ return_values: return_values,
38
+ table_name: table_name
39
+ }
40
+
41
+ hash.keep_if { |_key, value| value.present? || value == false }
42
+ end
43
+ alias :to_hash :to_h
44
+ end
45
+ end
46
+ end