tiny_dyno 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
+ 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