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.
@@ -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,8 @@
1
+ module Mara
2
+ ##
3
+ # The base error for any Mara specific errors.
4
+ #
5
+ # @author Maddie Schipper
6
+ # @since 1.0.0
7
+ class Error < StandardError; end
8
+ end
@@ -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,13 @@
1
+ require_relative 'model/base'
2
+
3
+ module Mara
4
+ ##
5
+ # A Mara Model.
6
+ #
7
+ # @see Mara::Model::Base
8
+ #
9
+ # @author Maddie Schipper
10
+ # @since 1.0.0
11
+ module Model
12
+ end
13
+ end
@@ -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