tiny_dyno 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,100 @@
1
+ require 'tiny_dyno/document_composition'
2
+
3
+ module TinyDyno
4
+ module Document
5
+
6
+ extend ActiveSupport::Concern
7
+ extend ActiveModel::Naming
8
+ include ActiveModel::Dirty
9
+ include ActiveModel::Model
10
+
11
+ include DocumentComposition
12
+
13
+ included do
14
+ TinyDyno.register_model(self)
15
+ end
16
+
17
+ # Instantiate a new +Document+, setting the Document's attributes if
18
+ # given. If no attributes are provided, they will be initialized with
19
+ # an empty +Hash+.
20
+ #
21
+ # The Hash Key must currently be provided from the applicationIf a HashKey is defined, the document's id will be set to that key,
22
+ #
23
+ # @example Create a new document.
24
+ # Person.new(hash_key: hash_key, title: "Sir")
25
+ #
26
+ # @param [ Hash ] attrs The attributes to set up the document with.
27
+ #
28
+ # @return [ Document ] A new document.
29
+ # @since 1.0.0
30
+ def initialize(attrs = nil)
31
+ @new_record = true
32
+ @attributes ||= {}
33
+ process_attributes(attrs) do
34
+ yield(self) if block_given?
35
+ end
36
+ # run_callbacks(:initialize) unless _initialize_callbacks.empty?
37
+ # raise ::TinyDyno::Errors::MissingHashKey.new(self.name) unless @hash_key.is_a?(Hash)
38
+ end
39
+
40
+ def delete
41
+ request_delete
42
+ end
43
+
44
+ private
45
+
46
+ def request_delete
47
+ request = {
48
+ table_name: self.class.table_name,
49
+ key: hash_key_as_selector
50
+ }
51
+ TinyDyno::Adapter.delete_item(request: request)
52
+ end
53
+
54
+ module ClassMethods
55
+
56
+ def where(options = {})
57
+ valid_option_keys(options)
58
+ get_query = build_where_query(options)
59
+ attributes = TinyDyno::Adapter.get_item(get_item_request: get_query)
60
+ if attributes.nil?
61
+ return false
62
+ else
63
+ self.new(attributes)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # minimimum implementation for now
70
+ # check that each option key relates to a hash_key present on the model
71
+ # do not permit scan queries
72
+ def valid_option_keys(options)
73
+ options.keys.each do |name|
74
+ named = name.to_s
75
+ raise TinyDyno::Errors::HashKeysOnly.new(klass: self.class, name: named) unless hash_key_is_defined?(named)
76
+ end
77
+ end
78
+
79
+ # minimimum implementation for now
80
+ # build simple query to retrieve document
81
+ # via get_item
82
+ # http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#get_item-instance_method
83
+ def build_where_query(options)
84
+ query_keys = {}
85
+ options.each do |k,v|
86
+ # as expected by DynamoDB
87
+ typed_key = k.to_s
88
+ query_keys[typed_key] = dyno_typed_key(key: typed_key, val: v)
89
+ end
90
+ {
91
+ table_name: self.table_name,
92
+ attributes_to_get: attribute_names,
93
+ key: query_keys
94
+ }
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,49 @@
1
+ require 'tiny_dyno/attributes'
2
+ require 'tiny_dyno/changeable'
3
+ require 'tiny_dyno/fields'
4
+ require 'tiny_dyno/stateful'
5
+ require 'tiny_dyno/tables'
6
+ require 'tiny_dyno/hash_keys'
7
+ require 'tiny_dyno/persistable'
8
+
9
+ module TinyDyno
10
+ module DocumentComposition
11
+ extend ActiveSupport::Concern
12
+
13
+ include Attributes
14
+ include Changeable
15
+ include Fields
16
+ include HashKeys
17
+ include Persistable
18
+ include Stateful
19
+ include Tables
20
+
21
+
22
+ MODULES = [
23
+ Attributes,
24
+ Changeable,
25
+ Fields,
26
+ HashKeys,
27
+ Persistable,
28
+ Stateful,
29
+ Tables,
30
+ ]
31
+
32
+ class << self
33
+
34
+ # Get a list of methods that would be a bad idea to define as field names
35
+ # or override when including TinyDyno::Document.
36
+ #
37
+ # @example Bad thing!
38
+ # TinyDyno::Components.prohibited_methods
39
+ #
40
+ # @return [ Array<Symbol> ]
41
+ def prohibited_methods
42
+ @prohibited_methods ||= MODULES.flat_map do |mod|
43
+ mod.instance_methods.map(&:to_sym)
44
+ end
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,4 @@
1
+ require 'tiny_dyno/errors/tiny_dyno_error'
2
+
3
+ require 'tiny_dyno/errors/attribute_errors'
4
+ require 'tiny_dyno/errors/hash_key_errors'
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ module TinyDyno
3
+ module Errors
4
+
5
+ # This error is raised when trying to set a value in TinyDyno that is not
6
+ # already set with dynamic attributes or the field is not defined.
7
+ class UnknownAttribute < TinyDynoError
8
+
9
+ # Create the new error.
10
+ #
11
+ # @example Instantiate the error.
12
+ # UnknownAttribute.new(Person, "gender")
13
+ #
14
+ # @param [ Class ] klass The model class.
15
+ # @param [ String, Symbol ] name The name of the attribute.
16
+ #
17
+ # @since 3.0.0
18
+ def initialize(klass, name)
19
+ super(
20
+ compose_message("unknown_attribute", { klass: klass.name, name: name })
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,78 @@
1
+ # encoding: utf-8
2
+ module TinyDyno
3
+ module Errors
4
+
5
+ # This error is raised when trying to set a value in Mongoid that is not
6
+ # already set with dynamic attributes or the field is not defined.
7
+ class InvalidHashKey < TinyDynoError
8
+
9
+ # Create the new error.
10
+ #
11
+ # @example Instantiate the error.
12
+ # UnknownAttribute.new(Person, "gender")
13
+ #
14
+ # @param [ Class ] klass The model class.
15
+ # @param [ String, Symbol ] name The name of the attribute.
16
+ #
17
+ # @since 3.0.0
18
+ def initialize(klass:, name:)
19
+ super(
20
+ compose_message("invalid hash_key", { klass: klass.name, name: name })
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # encoding: utf-8
28
+ module TinyDyno
29
+ module Errors
30
+
31
+ # This error is raised when trying to set a value in Mongoid that is not
32
+ # already set with dynamic attributes or the field is not defined.
33
+ class MissingHashKey < TinyDynoError
34
+
35
+ # Create the new error.
36
+ #
37
+ # @example Instantiate the error.
38
+ # UnknownAttribute.new(Person, "gender")
39
+ #
40
+ # @param [ Class ] klass The model class.
41
+ # @param [ String, Symbol ] name The name of the attribute.
42
+ #
43
+ # @since 3.0.0
44
+ def initialize(klass:)
45
+ super(
46
+ compose_message("no hash key specified", { klass: klass.name })
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+
54
+ # encoding: utf-8
55
+ module TinyDyno
56
+ module Errors
57
+
58
+ # This error is raised, when a query is performed with fields specified
59
+ # that are not HashKeys, which would result in a table scan
60
+ class HashKeysOnly < TinyDynoError
61
+
62
+ # Create the new error.
63
+ #
64
+ # @example Instantiate the error.
65
+ # HashKeysOnly.new(Person, "gender")
66
+ #
67
+ # @param [ Class ] klass The model class.
68
+ # @param [ String, Symbol ] name The name of the attribute.
69
+ #
70
+ # @since 3.0.0
71
+ def initialize(klass:, name:)
72
+ super(
73
+ compose_message("only search for hash keys", { klass: klass.name, name: name })
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,85 @@
1
+ # encoding: utf-8
2
+ module TinyDyno::Errors
3
+ class TinyDynoError < StandardError
4
+
5
+ BASE_KEY = "tiny_dyno.errors.messages"
6
+
7
+ # Compose the message.
8
+ #
9
+ # @example Create the message
10
+ # error.compose_message
11
+ #
12
+ # @return [ String ] The composed message.
13
+ def compose_message(key, attributes)
14
+ @problem = problem(key, attributes)
15
+ @summary = summary(key, attributes)
16
+ @resolution = resolution(key, attributes)
17
+
18
+ "\nProblem:\n #{@problem}"+
19
+ "\nSummary:\n #{@summary}"+
20
+ "\nResolution:\n #{@resolution}"
21
+ end
22
+
23
+ private
24
+
25
+ # Given the key of the specific error and the options hash, translate the
26
+ # message.
27
+ #
28
+ # @example Translate the message.
29
+ # error.translate("errors", :key => value)
30
+ #
31
+ # @param [ String ] key The key of the error in the locales.
32
+ # @param [ Hash ] options The objects to pass to create the message.
33
+ #
34
+ # @return [ String ] A localized error message string.
35
+ def translate(key, options)
36
+ ::I18n.translate("#{BASE_KEY}.#{key}", options)
37
+ end
38
+
39
+ # Create the problem.
40
+ #
41
+ # @example Create the problem.
42
+ # error.problem("error", {})
43
+ #
44
+ # @param [ String, Symbol ] key The error key.
45
+ # @param [ Hash ] attributes The attributes to interpolate.
46
+ #
47
+ # @return [ String ] The problem.
48
+ #
49
+ # @since 3.0.0
50
+ def problem(key, attributes)
51
+ translate("#{key}.message", attributes)
52
+ end
53
+
54
+ # Create the summary.
55
+ #
56
+ # @example Create the summary.
57
+ # error.summary("error", {})
58
+ #
59
+ # @param [ String, Symbol ] key The error key.
60
+ # @param [ Hash ] attributes The attributes to interpolate.
61
+ #
62
+ # @return [ String ] The summary.
63
+ #
64
+ # @since 3.0.0
65
+ def summary(key, attributes)
66
+ translate("#{key}.summary", attributes)
67
+ end
68
+
69
+ # Create the resolution.
70
+ #
71
+ # @example Create the resolution.
72
+ # error.resolution("error", {})
73
+ #
74
+ # @param [ String, Symbol ] key The error key.
75
+ # @param [ Hash ] attributes The attributes to interpolate.
76
+ #
77
+ # @return [ String ] The resolution.
78
+ #
79
+ # @since 3.0.0
80
+ def resolution(key, attributes)
81
+ translate("#{key}.resolution", attributes)
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1 @@
1
+ require 'tiny_dyno/extensions/module'
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ module TinyDyno
3
+ module Extensions
4
+ module Module
5
+
6
+ # Redefine the method. Will undef the method if it exists or simply
7
+ # just define it.
8
+ #
9
+ # @example Redefine the method.
10
+ # Object.re_define_method("exists?") do
11
+ # self
12
+ # end
13
+ #
14
+ # @param [ String, Symbol ] name The name of the method.
15
+ # @param [ Proc ] block The method body.
16
+ #
17
+ # @return [ Method ] The new method.
18
+ #
19
+ # @since 3.0.0
20
+ def re_define_method(name, &block)
21
+ undef_method(name) if method_defined?(name)
22
+ define_method(name, &block)
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ ::Module.__send__(:include, TinyDyno::Extensions::Module)
@@ -0,0 +1,299 @@
1
+ require 'tiny_dyno/fields/standard'
2
+
3
+ module TinyDyno
4
+ module Fields
5
+ extend ActiveSupport::Concern
6
+
7
+ TYPE_MAPPINGS= {
8
+ # binary_blob: 'B',
9
+ # bool: Boolean,
10
+ # binary_set: Array,
11
+ list: Array,
12
+ map: Hash,
13
+ number: Integer,
14
+ number_set: Array,
15
+ # null: Null,
16
+ string: String,
17
+ string_set: Array,
18
+ time: Time,
19
+ }
20
+
21
+ SUPPORTED_FIELD_TYPES = [Array, Hash, Integer, Array, String, Time].freeze
22
+
23
+ included do
24
+ class_attribute :fields
25
+
26
+ self.fields = {}
27
+
28
+ end
29
+
30
+ class << self
31
+
32
+ # Stores the provided block to be run when the option name specified is
33
+ # defined on a field.
34
+ #
35
+ # No assumptions are made about what sort of work the handler might
36
+ # perform, so it will always be called if the `option_name` key is
37
+ # provided in the field definition -- even if it is false or nil.
38
+ #
39
+ # @example
40
+ # TinyDyno::Fields.option :required do |model, field, value|
41
+ # model.validates_presence_of field if value
42
+ # end
43
+ #
44
+ # @param [ Symbol ] option_name the option name to match against
45
+ # @param [ Proc ] block the handler to execute when the option is
46
+ # provided.
47
+ #
48
+ # @since 2.1.0
49
+
50
+ def option(option_name, &block)
51
+ options[option_name] = block
52
+ end
53
+
54
+ # Return a map of custom option names to their handlers.
55
+ #
56
+ # @example
57
+ # TinyDyno::Fields.options
58
+ # # => { :required => #<Proc:0x00000100976b38> }
59
+ #
60
+ # @return [ Hash ] the option map
61
+ #
62
+ # @since 2.1.0
63
+ def options
64
+ @options ||= {}
65
+ end
66
+ end
67
+
68
+ # Get the name of the provided field as it is stored in the database.
69
+ # Used in determining if the field is aliased or not.
70
+ #
71
+ # @example Get the database field name.
72
+ # model.database_field_name(:authorization)
73
+ #
74
+ # @param [ String, Symbol ] name The name to get.
75
+ #
76
+ # @return [ String ] The name of the field as it's stored in the db.
77
+ #
78
+ # @since 3.0.7
79
+ def database_field_name(name)
80
+ self.class.database_field_name(name)
81
+ end
82
+
83
+ module ClassMethods
84
+
85
+ # Returns an array of names for the attributes available on this object.
86
+ #
87
+ # Provides the field names in an ORM-agnostic way. Rails v3.1+ uses this
88
+ # method to automatically wrap params in JSON requests.
89
+ #
90
+ # @example Get the field names
91
+ # Model.attribute_names
92
+ #
93
+ # @return [ Array<String> ] The field names
94
+ #
95
+ # @since 3.0.0
96
+ def attribute_names
97
+ fields.keys
98
+ end
99
+
100
+ # Get the name of the provided field as it is stored in the database.
101
+ # Used in determining if the field is aliased or not.
102
+ #
103
+ # @example Get the database field name.
104
+ # Model.database_field_name(:authorization)
105
+ #
106
+ # @param [ String, Symbol ] name The name to get.
107
+ #
108
+ # @return [ String ] The name of the field as it's stored in the db.
109
+ #
110
+ # @since 3.0.7
111
+ def database_field_name(name)
112
+ return nil unless name
113
+ normalized = name.to_s
114
+ end
115
+
116
+ # Defines all the fields that are accessible on the Document
117
+ # For each field that is defined, a getter and setter will be
118
+ # added as an instance method to the Document.
119
+ #
120
+ # @example Define a field.
121
+ # field :score, :type => Integer, :default => 0
122
+ #
123
+ # @param [ Symbol ] name The name of the field.
124
+ # @param [ Hash ] options The options to pass to the field.
125
+ #
126
+ # @option options [ Class ] :type The type of the field.
127
+ # @option options [ String ] :label The label for the field.
128
+ # @option options [ Object, Proc ] :default The field's default
129
+ #
130
+ # @return [ Field ] The generated field
131
+ def field(name, options = {})
132
+ named = name.to_s
133
+ added = add_field(named, options)
134
+ added
135
+ end
136
+
137
+ protected
138
+
139
+ # Define a field attribute for the +Document+.
140
+ #
141
+ # @example Set the field.
142
+ # Person.add_field(:name, :default => "Test")
143
+ #
144
+ # @param [ Symbol ] name The name of the field.
145
+ # @param [ Hash ] options The hash of options.
146
+ def add_field(name, options = {})
147
+ field = field_for(name, options)
148
+ fields[name] = field
149
+ create_accessors(name, name, options)
150
+ process_options(field)
151
+ field
152
+ end
153
+
154
+ # Run through all custom options stored in TinyDyno::Fields.options and
155
+ # execute the handler if the option is provided.
156
+ #
157
+ # @example
158
+ # TinyDyno::Fields.option :custom do
159
+ # puts "called"
160
+ # end
161
+ #
162
+ # field = TinyDyno::Fields.new(:test, :custom => true)
163
+ # Person.process_options(field)
164
+ # # => "called"
165
+ #
166
+ # @param [ Field ] field the field to process
167
+ def process_options(field)
168
+ field_options = field.options
169
+
170
+ Fields.options.each_pair do |option_name, handler|
171
+ if field_options.key?(option_name)
172
+ handler.call(self, field, field_options[option_name])
173
+ end
174
+ end
175
+ end
176
+
177
+ def field_for(name, options)
178
+ opts = options.merge(klass: self)
179
+ Fields::Standard.new(name, opts)
180
+ end
181
+
182
+ # Create the field accessors.
183
+ #
184
+ # @example Generate the accessors.
185
+ # Person.create_accessors(:name, "name")
186
+ # person.name #=> returns the field
187
+ # person.name = "" #=> sets the field
188
+ # person.name? #=> Is the field present?
189
+ # person.name_before_type_cast #=> returns the field before type cast
190
+ #
191
+ # @param [ Symbol ] name The name of the field.
192
+ # @param [ Symbol ] meth The name of the accessor.
193
+ # @param [ Hash ] options The options.
194
+ #
195
+ # @since 2.0.0
196
+ def create_accessors(name, meth, options = {})
197
+ field = fields[name]
198
+
199
+ create_field_getter(name, meth, field)
200
+ create_field_setter(name, meth, field)
201
+ create_field_check(name, meth)
202
+
203
+ end
204
+
205
+ # Create the getter method for the provided field.
206
+ #
207
+ # @example Create the getter.
208
+ # Model.create_field_getter("name", "name", field)
209
+ #
210
+ # @param [ String ] name The name of the attribute.
211
+ # @param [ String ] meth The name of the method.
212
+ # @param [ Field ] field The field.
213
+ def create_field_getter(name, meth, field)
214
+ generated_methods.module_eval do
215
+ re_define_method(meth) do
216
+ raw = read_attribute(name)
217
+ value = typed_value_for(name, raw)
218
+ attribute_will_change!(value)
219
+ value
220
+ end
221
+ end
222
+ end
223
+
224
+ # Create the getter_before_type_cast method for the provided field. If
225
+ # the attribute has been assigned, return the attribute before it was
226
+ # type cast. Otherwise, delegate to the getter.
227
+ #
228
+ # @example Create the getter_before_type_cast.
229
+ # Model.create_field_getter_before_type_cast("name", "name")
230
+ #
231
+ # @param [ String ] name The name of the attribute.
232
+ # @param [ String ] meth The name of the method.
233
+ #
234
+ def create_field_getter_before_type_cast(name, meth)
235
+ generated_methods.module_eval do
236
+ re_define_method("#{meth}_before_type_cast") do
237
+ if has_attribute_before_type_cast?(name)
238
+ read_attribute_before_type_cast(name)
239
+ else
240
+ send meth
241
+ end
242
+ end
243
+ end
244
+ end
245
+ # Create the setter method for the provided field.
246
+ #
247
+ # @example Create the setter.
248
+ # Model.create_field_setter("name", "name")
249
+ #
250
+ # @param [ String ] name The name of the attribute.
251
+ # @param [ String ] meth The name of the method.
252
+ # @param [ Field ] field The field.
253
+ def create_field_setter(name, meth, field)
254
+ generated_methods.module_eval do
255
+ re_define_method("#{meth}=") do |value|
256
+ val = write_attribute(name, value)
257
+ val
258
+ end
259
+ end
260
+ end
261
+
262
+ # Create the check method for the provided field.
263
+ #
264
+ # @example Create the check.
265
+ # Model.create_field_check("name", "name")
266
+ #
267
+ # @param [ String ] name The name of the attribute.
268
+ # @param [ String ] meth The name of the method.
269
+ #
270
+ # @since 2.4.0
271
+ def create_field_check(name, meth)
272
+ generated_methods.module_eval do
273
+ re_define_method("#{meth}?") do
274
+ attr = read_attribute(name)
275
+ attr == true || attr.present?
276
+ end
277
+ end
278
+ end
279
+
280
+ # Include the field methods as a module, so they can be overridden.
281
+ #
282
+ # @example Include the fields.
283
+ # Person.generated_methods
284
+ #
285
+ # @return [ Module ] The module of generated methods.
286
+ #
287
+ # @since 2.0.0
288
+ def generated_methods
289
+ @generated_methods ||= begin
290
+ mod = Module.new
291
+ include(mod)
292
+ mod
293
+ end
294
+ end
295
+
296
+ end
297
+
298
+ end
299
+ end