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