mara 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,100 @@
|
|
1
|
+
module Mara
|
2
|
+
##
|
3
|
+
# Configure Mara
|
4
|
+
#
|
5
|
+
# @param env [String] The default environment that is being configured.
|
6
|
+
#
|
7
|
+
# If this block is called a second time, the previous env version will
|
8
|
+
# be overridden
|
9
|
+
#
|
10
|
+
# @yield [config] The configuration object to set values on.
|
11
|
+
#
|
12
|
+
# @yieldparam config [ Mara::Configure] The configuration object.
|
13
|
+
# @yieldreturn [void]
|
14
|
+
#
|
15
|
+
# @return [void]
|
16
|
+
def self.configure(env)
|
17
|
+
instance = config
|
18
|
+
instance.send(:_set_env, env)
|
19
|
+
yield(instance)
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# @private
|
24
|
+
#
|
25
|
+
# The current config
|
26
|
+
def self.config
|
27
|
+
@config ||= Configure.new
|
28
|
+
@config
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# The configuration for Mara
|
33
|
+
#
|
34
|
+
# @author Maddie Schipper
|
35
|
+
# @since 1.0.0
|
36
|
+
class Configure
|
37
|
+
##
|
38
|
+
# DynamoDB specific config values.
|
39
|
+
#
|
40
|
+
# @!attribute [rw] table_name
|
41
|
+
# The name of the DynamoDB table to use.
|
42
|
+
#
|
43
|
+
# @note If this is not set, pretty much nothing will work.
|
44
|
+
#
|
45
|
+
# @return [String, nil]
|
46
|
+
# @!attribute [rw] endpoint
|
47
|
+
# The DynamoDB endpoint to use. If `nil` this will fallback to the
|
48
|
+
# AWS default endpoint.
|
49
|
+
#
|
50
|
+
# @return [String, nil]
|
51
|
+
DynamoConfig = Struct.new(:table_name, :endpoint)
|
52
|
+
|
53
|
+
##
|
54
|
+
# Aws specific config values.
|
55
|
+
#
|
56
|
+
# @!attribute [rw] region
|
57
|
+
# The region name to use for the Client.
|
58
|
+
#
|
59
|
+
# @note By default this is `us-east-1`
|
60
|
+
#
|
61
|
+
# @return [String]
|
62
|
+
AwsConfig = Struct.new(:region)
|
63
|
+
|
64
|
+
##
|
65
|
+
# The current environment that Mara is configured for.
|
66
|
+
#
|
67
|
+
# @return [String]
|
68
|
+
attr_reader :env
|
69
|
+
|
70
|
+
##
|
71
|
+
# The Aws config
|
72
|
+
#
|
73
|
+
# @return [ Mara::Configure::AwsConfig]
|
74
|
+
attr_reader :aws
|
75
|
+
|
76
|
+
##
|
77
|
+
# The DynamoDB config
|
78
|
+
#
|
79
|
+
# @return [ Mara::Configure::DynamoConfig]
|
80
|
+
attr_reader :dynamodb
|
81
|
+
|
82
|
+
##
|
83
|
+
# @private
|
84
|
+
#
|
85
|
+
# Create a new instance.
|
86
|
+
#
|
87
|
+
# @note This should never be called directly by the client.
|
88
|
+
def initialize
|
89
|
+
@env = 'production'
|
90
|
+
@dynamodb = DynamoConfig.new(nil, nil)
|
91
|
+
@aws = AwsConfig.new('us-east-1')
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def _set_env(env)
|
97
|
+
@env = env.to_s
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Mara
|
2
|
+
##
|
3
|
+
# @private
|
4
|
+
#
|
5
|
+
# Helper methods for formatting data as it comes back from DynamoDB
|
6
|
+
module DynamoHelpers
|
7
|
+
def self.included(klass)
|
8
|
+
klass.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
##
|
13
|
+
# Calculate all the consumed capacity in an array of capacity objects.
|
14
|
+
def calculate_consumed_capacity(consumed_capacity, table_name)
|
15
|
+
consumed = consumed_capacity.is_a?(Array) ? consumed_capacity : [consumed_capacity]
|
16
|
+
|
17
|
+
if table_name
|
18
|
+
consumed.select! { |cap| cap.table_name == table_name }
|
19
|
+
end
|
20
|
+
|
21
|
+
consumed.map! { |cap| _sum_capacity(cap) }
|
22
|
+
consumed.sum
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# @private
|
27
|
+
#
|
28
|
+
# Count the number of capcity unites in a capacity object.
|
29
|
+
def _sum_capacity(cap)
|
30
|
+
cap.capacity_units.to_f
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/mara/error.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module Mara
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
#
|
7
|
+
# Convenience method for {ActiveSupport::Notifications} that also namespaces
|
8
|
+
# the key.
|
9
|
+
#
|
10
|
+
# @param name [string] The name of the action being instrumented.
|
11
|
+
#
|
12
|
+
# @return [Object] The return value of the block
|
13
|
+
def self.instrument(name, *args, &block)
|
14
|
+
ActiveSupport::Notifications.instrument("mara.#{name}", *args, &block)
|
15
|
+
end
|
16
|
+
end
|
data/lib/mara/model.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
require_relative '../error'
|
2
|
+
|
3
|
+
module Mara
|
4
|
+
module Model
|
5
|
+
##
|
6
|
+
# Errors raised when trying to set a key.
|
7
|
+
#
|
8
|
+
# @author Maddie Schipper
|
9
|
+
# @since 1.0.0
|
10
|
+
class AttributeError < Mara::Error; end
|
11
|
+
|
12
|
+
##
|
13
|
+
# The container class for attributes.
|
14
|
+
#
|
15
|
+
# This should not be created directly. Instead use the Model convenience
|
16
|
+
# methods to have this setup automatically.
|
17
|
+
#
|
18
|
+
# @author Maddie Schipper
|
19
|
+
# @since 1.0.0
|
20
|
+
class Attributes
|
21
|
+
##
|
22
|
+
# @private
|
23
|
+
#
|
24
|
+
# Create a new instance of attributes.
|
25
|
+
#
|
26
|
+
# @param default [Hash] The default attribute values to set.
|
27
|
+
def initialize(default)
|
28
|
+
@storage = {}
|
29
|
+
default.each { |k, v| set(k, v) }
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# @private
|
34
|
+
#
|
35
|
+
# Enumerate each key, value pair.
|
36
|
+
def each
|
37
|
+
return @storage.enum_for(:each) unless block_given?
|
38
|
+
|
39
|
+
@storage.each do |*args|
|
40
|
+
yield(*args)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Set a key, value pair on the attributes.
|
46
|
+
#
|
47
|
+
# @param key [#to_s] The key for the attribute you're trying to set.
|
48
|
+
#
|
49
|
+
# This key will be normalized before being stored as an attribute.
|
50
|
+
#
|
51
|
+
# @param value [Any, nil] The value to set for the key.
|
52
|
+
#
|
53
|
+
# @param pre_formatted [true, false] If the key is already normalized.
|
54
|
+
#
|
55
|
+
# @note If the value is +nil+, the key will be deleted.
|
56
|
+
#
|
57
|
+
# @raise [AttributeError] The normalized value of the key can't be blank.
|
58
|
+
#
|
59
|
+
# @return [void]
|
60
|
+
def set(key, value, pre_formatted: false)
|
61
|
+
nkey = normalize_key(key, pre_formatted)
|
62
|
+
|
63
|
+
raise AttributeError, "Can't set an attribute without a key" if nkey.blank?
|
64
|
+
|
65
|
+
if value.nil?
|
66
|
+
@storage.delete(nkey)
|
67
|
+
else
|
68
|
+
@storage[nkey] = value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Get a value an attribute value.
|
74
|
+
#
|
75
|
+
# @param key [#to_s] The key for the attribute you're getting.
|
76
|
+
#
|
77
|
+
# @param pre_formatted [true, false] If the key is already normalized.
|
78
|
+
#
|
79
|
+
# @raise [AttributeError] The normalized value of the key can't be blank.
|
80
|
+
#
|
81
|
+
# @return [Any, nil]
|
82
|
+
def get(key, pre_formatted: false)
|
83
|
+
nkey = normalize_key(key, pre_formatted)
|
84
|
+
|
85
|
+
raise AttributeError, "Can't get an attribute without a key" if nkey.blank?
|
86
|
+
|
87
|
+
@storage[nkey]
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Get an attribute value but it that value is nil, return the default
|
92
|
+
# passed as the second argument.
|
93
|
+
#
|
94
|
+
# @example Get a default value.
|
95
|
+
# attrs.set('foo', nil)
|
96
|
+
# attrs.fetch('foo', 'bar')
|
97
|
+
# # => 'bar'
|
98
|
+
#
|
99
|
+
# @param key [#to_s] The key for the attribute you're getting.
|
100
|
+
#
|
101
|
+
# @param default [Any, nil] The default value to return if the value
|
102
|
+
# for the key is nil.
|
103
|
+
#
|
104
|
+
# @param pre_formatted [true, false] If the key is already normalized.
|
105
|
+
#
|
106
|
+
# @return [Any, nil]
|
107
|
+
def fetch(key, default = nil, pre_formatted: false)
|
108
|
+
value = get(key, pre_formatted: pre_formatted)
|
109
|
+
return default if value.nil?
|
110
|
+
|
111
|
+
value
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Check if a key exists.
|
116
|
+
#
|
117
|
+
# @param key [#to_s] The key you want to check to see if it exists.
|
118
|
+
#
|
119
|
+
# @param pre_formatted [true, false] If the key is already normalized.
|
120
|
+
#
|
121
|
+
# @return [true, false]
|
122
|
+
def key?(key, pre_formatted: false)
|
123
|
+
nkey = normalize_key(key, pre_formatted)
|
124
|
+
@storage.key?(nkey)
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# @private
|
129
|
+
#
|
130
|
+
# Dump the storage backend into a hash.
|
131
|
+
#
|
132
|
+
# @return [Hash<String, Any>]
|
133
|
+
def to_h
|
134
|
+
@storage.dup
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# @private
|
139
|
+
#
|
140
|
+
# Attribute Magic
|
141
|
+
def method_missing(name, *args, &_block)
|
142
|
+
if name.to_s.end_with?('=')
|
143
|
+
set(name.to_s.gsub(/=$/, ''), *args)
|
144
|
+
else
|
145
|
+
get(name, *args)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# @private
|
151
|
+
#
|
152
|
+
# Attribute Magic
|
153
|
+
def respond_to_missing?(_name, _include_private = false)
|
154
|
+
true
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def normalize_key(key, pre_formatted)
|
160
|
+
return key if pre_formatted == true
|
161
|
+
|
162
|
+
key.to_s.camelize
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
|
3
|
+
require_relative '../error'
|
4
|
+
require_relative '../primary_key'
|
5
|
+
|
6
|
+
require_relative 'dsl'
|
7
|
+
require_relative 'query'
|
8
|
+
require_relative 'persistence'
|
9
|
+
require_relative 'attributes'
|
10
|
+
|
11
|
+
module Mara
|
12
|
+
module Model
|
13
|
+
class PrimaryKeyError < Mara::Error; end
|
14
|
+
|
15
|
+
##
|
16
|
+
# The base class for a Mara Model
|
17
|
+
#
|
18
|
+
# @example A basic Person class.
|
19
|
+
# class Person < Mara::Model::Base
|
20
|
+
# # Set the Partition Key & Sort Key names.
|
21
|
+
# primary_key 'PrimaryKey', 'RangeKey'
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# @example Set dynamic attribute values.
|
25
|
+
# person = Person.build
|
26
|
+
# person[:first_name] = 'Maddie'
|
27
|
+
# person.last_name = 'Schipper'
|
28
|
+
#
|
29
|
+
# @author Maddie Schipper
|
30
|
+
# @since 1.0.0
|
31
|
+
class Base
|
32
|
+
# @!parse extend Mara::Model::Dsl::ClassMethods
|
33
|
+
# @!parse extend Mara::Model::Query::ClassMethods
|
34
|
+
# @!parse extend Mara::Model::Persistence::ClassMethods
|
35
|
+
|
36
|
+
include ActiveModel::Validations
|
37
|
+
include Mara::Model::Dsl
|
38
|
+
include Mara::Model::Query
|
39
|
+
include Mara::Model::Persistence
|
40
|
+
|
41
|
+
##
|
42
|
+
# @private
|
43
|
+
#
|
44
|
+
# The attributes container.
|
45
|
+
#
|
46
|
+
# @return [ Mara::Model::Attributes]
|
47
|
+
attr_reader :attributes
|
48
|
+
|
49
|
+
class << self
|
50
|
+
##
|
51
|
+
# Create a new instance of the model.
|
52
|
+
#
|
53
|
+
# @example Building a new model.
|
54
|
+
# person = Person.build(
|
55
|
+
# partition_key: SecureRandom.uuid,
|
56
|
+
# first_name: 'Maddie',
|
57
|
+
# last_name: 'Schipper'
|
58
|
+
# )
|
59
|
+
#
|
60
|
+
# @param attributes [Hash] The default attributes that can be assigned.
|
61
|
+
#
|
62
|
+
# If a +partition_key+ is specified it will be used to set the model's
|
63
|
+
# +partion_key+
|
64
|
+
#
|
65
|
+
# If a +sort_key+ is specified it will be used to set the model's
|
66
|
+
# +sort_key+
|
67
|
+
#
|
68
|
+
# @return [ Mara::Model::Base]
|
69
|
+
def build(attributes = {})
|
70
|
+
partition_key = attributes.delete(:partition_key)
|
71
|
+
sort_key = attributes.delete(:sort_key)
|
72
|
+
|
73
|
+
attrs = Mara::Model::Attributes.new(attributes)
|
74
|
+
|
75
|
+
new(
|
76
|
+
partition_key: partition_key,
|
77
|
+
sort_key: sort_key,
|
78
|
+
attributes: attrs,
|
79
|
+
persisted: false
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# @private
|
85
|
+
def construct(record_hash)
|
86
|
+
partition_key = record_hash.delete(self.partition_key)
|
87
|
+
sort_key = record_hash.delete(self.sort_key)
|
88
|
+
|
89
|
+
attrs = Mara::Model::Attributes.new(record_hash)
|
90
|
+
|
91
|
+
new(
|
92
|
+
partition_key: partition_key,
|
93
|
+
sort_key: sort_key,
|
94
|
+
attributes: attrs,
|
95
|
+
persisted: true
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# @private
|
102
|
+
#
|
103
|
+
# Create a new instance of the model.
|
104
|
+
#
|
105
|
+
# @param partition_key [Any] The partition_key for the model.
|
106
|
+
# @param sort_key [Any] The sort key for the model.
|
107
|
+
# @param attributes [ Mara::Model::Attributes] The already existing
|
108
|
+
# attributes for the model.
|
109
|
+
def initialize(partition_key:, sort_key:, attributes:, persisted:)
|
110
|
+
if self.class.partition_key.blank?
|
111
|
+
raise Mara::Model::PrimaryKeyError,
|
112
|
+
"Can't create instance of #{self.class.name} without a `partition_key` set."
|
113
|
+
end
|
114
|
+
|
115
|
+
unless attributes.is_a?( Mara::Model::Attributes)
|
116
|
+
raise ArgumentError, 'attributes is not Mara::Model::Attributes'
|
117
|
+
end
|
118
|
+
|
119
|
+
@persisted = persisted == true
|
120
|
+
@attributes = attributes
|
121
|
+
|
122
|
+
self.partition_key = partition_key
|
123
|
+
self.sort_key = sort_key if sort_key
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# The partition_key key value for the object.
|
128
|
+
#
|
129
|
+
# @return [Any, nil]
|
130
|
+
attr_accessor :partition_key
|
131
|
+
|
132
|
+
##
|
133
|
+
# Set an attribute key value pair.
|
134
|
+
#
|
135
|
+
# @param key [#to_s] The key for the attribute.
|
136
|
+
# @param value [Any, nil] The value of the attribute.
|
137
|
+
#
|
138
|
+
# @return [void]
|
139
|
+
def []=(key, value)
|
140
|
+
attributes.set(key, value)
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Get an attribute's current value.
|
145
|
+
#
|
146
|
+
# @param key [#to_s] The key for the attribute.
|
147
|
+
#
|
148
|
+
# @return [Any, nil]
|
149
|
+
def [](key)
|
150
|
+
attributes.get(key)
|
151
|
+
end
|
152
|
+
|
153
|
+
def model_primary_key
|
154
|
+
Mara::PrimaryKey.new(model: self)
|
155
|
+
end
|
156
|
+
|
157
|
+
def model_identifier
|
158
|
+
Mara::PrimaryKey.generate(model_primary_key)
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Fetch the current sort key value.
|
163
|
+
#
|
164
|
+
# @return [Any, nil]
|
165
|
+
def sort_key
|
166
|
+
if self.class.sort_key.blank?
|
167
|
+
raise Mara::Model::PrimaryKeyError,
|
168
|
+
"Model #{self.class.name} does not specify a sort_key."
|
169
|
+
end
|
170
|
+
|
171
|
+
@sort_key
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Checks if the model should have a sort key and returns the value if
|
176
|
+
# it does.
|
177
|
+
#
|
178
|
+
# @return [Any, nil]
|
179
|
+
def conditional_sort_key
|
180
|
+
return nil if self.class.sort_key.blank?
|
181
|
+
|
182
|
+
sort_key
|
183
|
+
end
|
184
|
+
|
185
|
+
##
|
186
|
+
# Set a sort key value.
|
187
|
+
#
|
188
|
+
# @param sort_key [String] The sort key value.
|
189
|
+
#
|
190
|
+
# @return [void]
|
191
|
+
def sort_key=(sort_key)
|
192
|
+
if self.class.sort_key.blank?
|
193
|
+
raise Mara::Model::PrimaryKeyError,
|
194
|
+
"Model #{self.class.name} does not specify a sort_key."
|
195
|
+
end
|
196
|
+
|
197
|
+
@sort_key = sort_key
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# @private
|
202
|
+
#
|
203
|
+
# Attribute Magic
|
204
|
+
def method_missing(name, *args, &block)
|
205
|
+
if attributes.respond_to?(name)
|
206
|
+
attributes.send(name, *args, &block)
|
207
|
+
else
|
208
|
+
super
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
##
|
213
|
+
# @private
|
214
|
+
#
|
215
|
+
# Attribute Magic
|
216
|
+
def respond_to_missing?(name, include_private = false)
|
217
|
+
if attributes.respond_to?(name)
|
218
|
+
true
|
219
|
+
else
|
220
|
+
super
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|