mara 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/README.md +3 -0
- data/Rakefile +26 -0
- data/lib/mara.rb +13 -0
- data/lib/mara/attribute_formatter.rb +161 -0
- data/lib/mara/batch.rb +223 -0
- data/lib/mara/client.rb +43 -0
- data/lib/mara/configure.rb +100 -0
- data/lib/mara/dynamo_helpers.rb +34 -0
- data/lib/mara/error.rb +8 -0
- data/lib/mara/instrument.rb +16 -0
- data/lib/mara/model.rb +13 -0
- data/lib/mara/model/attributes.rb +166 -0
- data/lib/mara/model/base.rb +225 -0
- data/lib/mara/model/dsl.rb +208 -0
- data/lib/mara/model/persistence.rb +120 -0
- data/lib/mara/model/query.rb +97 -0
- data/lib/mara/null_value.rb +13 -0
- data/lib/mara/persistence.rb +204 -0
- data/lib/mara/primary_key.rb +117 -0
- data/lib/mara/query.rb +90 -0
- data/lib/mara/table.rb +141 -0
- data/lib/mara/version.rb +5 -0
- metadata +195 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c8a17c4c4d301602b87a18e8c682297d1cd24c9c5085d2b0aa2115d70b819d40
|
4
|
+
data.tar.gz: e31b906856bc625c4630661a6867cdbdd65c0cb1c22627859f217173eab83cb1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5fb35cfd11fec4f646e2b2ea6cc632526dee9eb550ac5e79f4655cb0bdf7d81580f8e48068bc8cf17551ff388c90de6f12fe4d0a00dea45fed2188af5de5b7d9
|
7
|
+
data.tar.gz: ef06b2d073323d716031c97a20e5c71d53b20099916154b48e2f507b0ba1bb2277a7eea3684a94a186e809175cb323371efddf62c54b44ff34229f7d4df42b57
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
task default: :spec
|
8
|
+
|
9
|
+
namespace :docs do
|
10
|
+
desc 'Generate docs'
|
11
|
+
task :generate do
|
12
|
+
YARD::CLI::Yardoc.run
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'Get docs stats'
|
16
|
+
task :stats do
|
17
|
+
YARD::CLI::Stats.run('--list-undoc')
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'View docs'
|
21
|
+
task :view do
|
22
|
+
# Viewing should always generate the freshest
|
23
|
+
Rake::Task['docs:generate'].invoke
|
24
|
+
exec("open #{File.expand_path('./doc/index.html', __dir__)}")
|
25
|
+
end
|
26
|
+
end
|
data/lib/mara.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'mara/batch'
|
2
|
+
require 'mara/client'
|
3
|
+
require 'mara/configure'
|
4
|
+
require 'mara/error'
|
5
|
+
require 'mara/model'
|
6
|
+
require 'mara/table'
|
7
|
+
require 'mara/version'
|
8
|
+
|
9
|
+
##
|
10
|
+
# Mara is a wrapper around DynamoDB that adds a model layer and a bunch of
|
11
|
+
# convenience functions.
|
12
|
+
module Mara
|
13
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'aws-sdk-dynamodb'
|
2
|
+
|
3
|
+
require_relative 'error'
|
4
|
+
require_relative 'null_value'
|
5
|
+
|
6
|
+
module Mara
|
7
|
+
##
|
8
|
+
# Helper class that provides Attribute Formatting for DynamoDB values.
|
9
|
+
#
|
10
|
+
# @author Maddie Schipper
|
11
|
+
# @since 1.0.0
|
12
|
+
class AttributeFormatter
|
13
|
+
##
|
14
|
+
# The error that is raised if an attribute value fails to be converted into
|
15
|
+
# a valid DynamoDB value.
|
16
|
+
#
|
17
|
+
# @author Maddie Schipper
|
18
|
+
# @since 1.0.0
|
19
|
+
class Error < Mara::Error; end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
##
|
23
|
+
# @private
|
24
|
+
#
|
25
|
+
# Format a value into a DynamoDB valid format.
|
26
|
+
#
|
27
|
+
# @param value [Object] The value to be formatted.
|
28
|
+
#
|
29
|
+
# @return [Hash]
|
30
|
+
def format(value)
|
31
|
+
case value
|
32
|
+
when true, false
|
33
|
+
{ bool: value }
|
34
|
+
when nil, Mara::NullValue
|
35
|
+
{ null: true }
|
36
|
+
when String
|
37
|
+
{ s: value }
|
38
|
+
when Symbol
|
39
|
+
{ s: value.to_s }
|
40
|
+
when Numeric
|
41
|
+
{ n: value.to_s }
|
42
|
+
when Time
|
43
|
+
{ n: value.utc.to_i.to_s }
|
44
|
+
when DateTime, Date
|
45
|
+
format(value.to_time)
|
46
|
+
when Hash
|
47
|
+
format_hash(value)
|
48
|
+
when Array
|
49
|
+
format_array(value)
|
50
|
+
when Set
|
51
|
+
format_set(value)
|
52
|
+
else
|
53
|
+
raise Error, "Unexpected value type #{value.class.name} <#{value}>"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# @private
|
59
|
+
#
|
60
|
+
# Convert a Aws::DynamoDB::Types::AttributeValue to a raw value
|
61
|
+
#
|
62
|
+
# @param value [Aws::DynamoDB::Types::AttributeValue] The value to convert
|
63
|
+
#
|
64
|
+
# @return [Object]
|
65
|
+
def flatten(value)
|
66
|
+
unless value.is_a?(Aws::DynamoDB::Types::AttributeValue)
|
67
|
+
raise ArgumentError, 'Not an attribute type'
|
68
|
+
end
|
69
|
+
|
70
|
+
if value.s.present?
|
71
|
+
value.s
|
72
|
+
elsif value.n.present?
|
73
|
+
format_number(value.n)
|
74
|
+
elsif value.ss.present?
|
75
|
+
Set.new(value.ss)
|
76
|
+
elsif value.ns.present?
|
77
|
+
Set.new(value.ns.map { |v| format_number(v) })
|
78
|
+
elsif value.m.present?
|
79
|
+
flatten_hash(value.m)
|
80
|
+
elsif value.l.present?
|
81
|
+
flatten_array(value.l)
|
82
|
+
elsif value.null
|
83
|
+
Mara::NULL
|
84
|
+
elsif !value.bool.nil?
|
85
|
+
value.bool
|
86
|
+
else
|
87
|
+
raise Error, 'Unexpected value type from DynamoDB'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def format_number(number)
|
94
|
+
if number.include?('.')
|
95
|
+
number.to_f
|
96
|
+
else
|
97
|
+
number.to_i
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def flatten_hash(hash)
|
102
|
+
hash.each_with_object({}) do |(key, value), object|
|
103
|
+
object[key] = flatten(value)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def flatten_array(array)
|
108
|
+
array.map { |value| flatten(value) }
|
109
|
+
end
|
110
|
+
|
111
|
+
def format_array(array)
|
112
|
+
value = Array(array)
|
113
|
+
if value.empty?
|
114
|
+
return format(nil)
|
115
|
+
end
|
116
|
+
|
117
|
+
values = value.map do |val|
|
118
|
+
format(val)
|
119
|
+
end
|
120
|
+
|
121
|
+
{ l: values }
|
122
|
+
end
|
123
|
+
|
124
|
+
def format_hash(hash)
|
125
|
+
value = Hash(hash)
|
126
|
+
if value.empty?
|
127
|
+
return format(nil)
|
128
|
+
end
|
129
|
+
|
130
|
+
formatted = value.each_with_object({}) do |(key, sub_value), object|
|
131
|
+
next unless value.present?
|
132
|
+
|
133
|
+
object[key.to_s] = format(sub_value)
|
134
|
+
end
|
135
|
+
|
136
|
+
{ m: formatted }
|
137
|
+
end
|
138
|
+
|
139
|
+
def format_set(set)
|
140
|
+
value = Set.new(set.to_a)
|
141
|
+
if value.empty?
|
142
|
+
return format(nil)
|
143
|
+
end
|
144
|
+
|
145
|
+
kind = value.map(&:class).uniq
|
146
|
+
|
147
|
+
if kind.count != 1 && kind.to_a.reject { |v| v.ancestors.include?(Numeric) }.any?
|
148
|
+
raise Error, "Set type must only contain 1 type #{value.class.name}"
|
149
|
+
end
|
150
|
+
|
151
|
+
if kind.first == String || kind.first == Symbol
|
152
|
+
{ ss: value.to_a.map(&:to_s) }
|
153
|
+
elsif kind.first.ancestors.include?(Numeric)
|
154
|
+
{ ns: value.to_a.map(&:to_s) }
|
155
|
+
else
|
156
|
+
raise Error, "Unexpected Set type #{kind.first.class.name}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
data/lib/mara/batch.rb
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
require_relative 'persistence'
|
2
|
+
require_relative 'instrument'
|
3
|
+
|
4
|
+
module Mara
|
5
|
+
##
|
6
|
+
# Raise this within a { Mara::Batch#in_batch} to quietly rollback the batch.
|
7
|
+
#
|
8
|
+
# @note All current operations will be dropped.
|
9
|
+
#
|
10
|
+
# @author Maddie Schipper
|
11
|
+
# @since 1.0.0
|
12
|
+
class Rollback < StandardError; end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Perform operations in batches.
|
16
|
+
#
|
17
|
+
# @note This is not the same as a transaction. It only saves on the number
|
18
|
+
# of API calls to DynamoDB.
|
19
|
+
#
|
20
|
+
# @example Saving Multiple Records
|
21
|
+
# Mara::Batch.in_batch do
|
22
|
+
# person1.save
|
23
|
+
# person2.save
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @author Maddie Schipper
|
27
|
+
# @since 1.0.0
|
28
|
+
class Batch
|
29
|
+
##
|
30
|
+
# @private
|
31
|
+
#
|
32
|
+
# The name of the thread variable that holds the current batch spec.
|
33
|
+
BATCH_STACK_VAR_NAME = 'mara_batch'.freeze
|
34
|
+
|
35
|
+
class << self
|
36
|
+
##
|
37
|
+
# Perform in a batch.
|
38
|
+
#
|
39
|
+
# All save/destroy calls on a model will be routed into the current batch.
|
40
|
+
#
|
41
|
+
# If there is a error raised all operations will be dropped.
|
42
|
+
#
|
43
|
+
# If the error is a { Mara::Rollback} the batch will silently rollback.
|
44
|
+
# If not, it will be re-thrown after the rollback.
|
45
|
+
#
|
46
|
+
# @yield The batch operation.
|
47
|
+
def in_batch
|
48
|
+
begin_new_batch
|
49
|
+
begin
|
50
|
+
yield
|
51
|
+
rescue Mara::Rollback
|
52
|
+
abort_current_batch
|
53
|
+
# rubocop:disable Lint/RescueException
|
54
|
+
rescue Exception => exception
|
55
|
+
# rubocop:enable Lint/RescueException
|
56
|
+
abort_current_batch
|
57
|
+
raise exception
|
58
|
+
else
|
59
|
+
commit_current_batch
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# @private
|
65
|
+
#
|
66
|
+
# Perform a save model. If there is a current batch it is added to the
|
67
|
+
# operation queue. If there is no current batch, this will be forwarded
|
68
|
+
# directly to the { Mara::Persistence}.
|
69
|
+
#
|
70
|
+
# @param item [Hash] The model to perform the action with.
|
71
|
+
def save_model(item)
|
72
|
+
perform_for_model(:save_model, item)
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# @private
|
77
|
+
#
|
78
|
+
# Perform a save model. If there is a current batch it is added to the
|
79
|
+
# operation queue. If there is no current batch, this will be forwarded
|
80
|
+
# directly to the { Mara::Persistence}.
|
81
|
+
#
|
82
|
+
# @param item [Hash] The model to perform the action with.
|
83
|
+
def save_model!(item)
|
84
|
+
perform_for_model(:save_model!, item)
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# @private
|
89
|
+
#
|
90
|
+
# Perform a delete model. If there is a current batch it is added to the
|
91
|
+
# operation queue. If there is no current batch, this will be forwarded
|
92
|
+
# directly to the { Mara::Persistence}.
|
93
|
+
#
|
94
|
+
# @param item [Hash] The model to perform the action with.
|
95
|
+
def delete_model(item)
|
96
|
+
perform_for_model(:delete_model, item)
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# @private
|
101
|
+
#
|
102
|
+
# Perform a delete model. If there is a current batch it is added to the
|
103
|
+
# operation queue. If there is no current batch, this will be forwarded
|
104
|
+
# directly to the { Mara::Persistence}.
|
105
|
+
#
|
106
|
+
# @param item [Hash] The model to perform the action with.
|
107
|
+
def delete_model!(item)
|
108
|
+
perform_for_model(:delete_model!, item)
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def perform_for_model(action_name, item)
|
114
|
+
if (batch = current_batch)
|
115
|
+
batch.add(action_name, item)
|
116
|
+
else
|
117
|
+
Mara::Persistence.send(action_name, item)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def current_batch
|
122
|
+
batch_stack.first
|
123
|
+
end
|
124
|
+
|
125
|
+
def batch_stack
|
126
|
+
Thread.current[BATCH_STACK_VAR_NAME] ||= []
|
127
|
+
end
|
128
|
+
|
129
|
+
def begin_new_batch
|
130
|
+
batch_stack << Mara::Batch.new
|
131
|
+
end
|
132
|
+
|
133
|
+
def commit_current_batch
|
134
|
+
batch_stack.pop.commit_batch
|
135
|
+
end
|
136
|
+
|
137
|
+
def abort_current_batch
|
138
|
+
batch_stack.pop.abort_batch
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# @private
|
144
|
+
#
|
145
|
+
# The queue of operations to perform on commit.
|
146
|
+
#
|
147
|
+
# @return [Array<Array<Symbol, Hash>>]
|
148
|
+
attr_reader :operations
|
149
|
+
|
150
|
+
##
|
151
|
+
# The current batch id.
|
152
|
+
#
|
153
|
+
# @return [String]
|
154
|
+
attr_reader :batch_id
|
155
|
+
|
156
|
+
##
|
157
|
+
# @private
|
158
|
+
#
|
159
|
+
# Create a new batch.
|
160
|
+
def initialize
|
161
|
+
@batch_id = SecureRandom.uuid
|
162
|
+
@operations = []
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# @private
|
167
|
+
#
|
168
|
+
# Add an item to the operation queue.
|
169
|
+
#
|
170
|
+
# @param action_name [Symbol] The action to perform for the item.
|
171
|
+
# @param item [Hash] The hash of data for the action.
|
172
|
+
#
|
173
|
+
# @return [void]
|
174
|
+
def add(action_name, item)
|
175
|
+
Mara.instrument('batch.add_item', batch_id: batch_id, action: action_name, item: item) do
|
176
|
+
operations << [action_name, item]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# @private
|
182
|
+
#
|
183
|
+
# Perform all the operations in the queue.
|
184
|
+
#
|
185
|
+
# @return [void]
|
186
|
+
def commit_batch
|
187
|
+
Mara.instrument('batch.commit', batch_id: batch_id) do
|
188
|
+
execute_commit
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
##
|
193
|
+
# @private
|
194
|
+
#
|
195
|
+
# Abort the batch and clear the current batch operations.
|
196
|
+
#
|
197
|
+
# @return [void]
|
198
|
+
def abort_batch
|
199
|
+
@operations = []
|
200
|
+
Mara.instrument('batch.abort', batch_id: batch_id)
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def execute_commit
|
206
|
+
ops = operations.map do |action_name, item|
|
207
|
+
case action_name
|
208
|
+
when :save_model, :save_model!
|
209
|
+
Mara::Persistence::CreateRequest.new(item)
|
210
|
+
when :delete_model, :delete_model!
|
211
|
+
Mara::Persistence::DestroyRequest.new(item)
|
212
|
+
else
|
213
|
+
raise "Unexpected operation action name #{action_name}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
Mara::Persistence.perform_requests(
|
217
|
+
Mara::Client.shared,
|
218
|
+
Mara.config.dynamodb.table_name,
|
219
|
+
ops
|
220
|
+
)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
data/lib/mara/client.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'aws-sdk-dynamodb'
|
2
|
+
|
3
|
+
require_relative 'configure'
|
4
|
+
|
5
|
+
module Mara
|
6
|
+
##
|
7
|
+
# @private
|
8
|
+
#
|
9
|
+
# Internal DynamoDB client.
|
10
|
+
#
|
11
|
+
# @author Maddie Schipper
|
12
|
+
# @since 1.0.0
|
13
|
+
class Client
|
14
|
+
class << self
|
15
|
+
##
|
16
|
+
# @private
|
17
|
+
#
|
18
|
+
# Create a new DynamoDB client.
|
19
|
+
#
|
20
|
+
# @return [Aws::DynamoDB::Client]
|
21
|
+
def create_client
|
22
|
+
params = {
|
23
|
+
region: Mara.config.aws.region,
|
24
|
+
simple_attributes: false
|
25
|
+
}
|
26
|
+
if (endpoint = Mara.config.dynamodb.endpoint)
|
27
|
+
params[:endpoint] = endpoint
|
28
|
+
end
|
29
|
+
Aws::DynamoDB::Client.new(params)
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# @private
|
34
|
+
#
|
35
|
+
# The shared client.
|
36
|
+
#
|
37
|
+
# @return [Aws::DynamoDB::Client]
|
38
|
+
def shared
|
39
|
+
@shared ||= create_client
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|