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