simple_mapper 0.0.1

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,196 @@
1
+ module SimpleMapper
2
+ module Attributes
3
+ self.instance_eval do
4
+ def types
5
+ @types ||= {}
6
+ end
7
+
8
+ def type_for(name)
9
+ types[name]
10
+ end
11
+
12
+ def register_type(name, expected_type, converter)
13
+ types[name] = {:name => name, :expected_type => expected_type, :converter => converter}
14
+ end
15
+ end
16
+
17
+ def self.included(klass)
18
+ klass.extend ClassMethods
19
+ end
20
+
21
+ module ClassMethods
22
+ def simple_mapper
23
+ @simple_mapper ||= SimpleMapper::Attributes::Manager.new(self)
24
+ end
25
+
26
+ def maps(attr, *args, &block)
27
+ if block_given?
28
+ hash = args.last
29
+ args << (hash = {}) unless hash.instance_of? Hash
30
+ mapper = simple_mapper.create_anonymous_mapper(&block)
31
+ hash[:type] ||= mapper
32
+ hash[:mapper] = mapper
33
+ end
34
+ attribute = simple_mapper.create_attribute(attr, *args)
35
+ simple_mapper.install_attribute attr, attribute
36
+ end
37
+ end
38
+
39
+ def attribute_object_for(attr)
40
+ self.class.simple_mapper.attributes[attr]
41
+ end
42
+
43
+ def key_for(attr)
44
+ attribute_object_for(attr).key
45
+ end
46
+
47
+ def reset_attribute(attr)
48
+ @simple_mapper_init.delete attr
49
+ attribute_changed!(attr, false)
50
+ remove_instance_variable(:"@#{attr}")
51
+ end
52
+
53
+ def write_attribute(attr, value)
54
+ raise(RuntimeError, "can't modify frozen object") if frozen?
55
+ instance_variable_set(:"@#{attr}", value)
56
+ @simple_mapper_init[attr] = true
57
+ attribute_changed! attr
58
+ value
59
+ end
60
+
61
+ def transform_source_attribute(attr)
62
+ attribute_object_for(attr).transformed_source_value(self)
63
+ end
64
+
65
+ def read_source_attribute(attr)
66
+ attribute_object_for(attr).source_value(self)
67
+ end
68
+
69
+ def read_attribute(attr)
70
+ if @simple_mapper_init[attr]
71
+ instance_variable_get(:"@#{attr}")
72
+ else
73
+ result = instance_variable_set(:"@#{attr}", transform_source_attribute(attr))
74
+ @simple_mapper_init[attr] = true
75
+ result
76
+ end
77
+ end
78
+
79
+ def get_attribute_default(attr)
80
+ attribute_object_for(attr).default_value(self)
81
+ end
82
+
83
+ def simple_mapper_changes
84
+ @simple_mapper_changes ||= SimpleMapper::ChangeHash.new
85
+ end
86
+
87
+ def attribute_changed!(attr, flag=true)
88
+ attribute_object_for(attr).changed!(self, flag)
89
+ end
90
+
91
+ def attribute_changed?(attr)
92
+ attribute_object_for(attr).changed?(self)
93
+ end
94
+
95
+ def all_changed!
96
+ simple_mapper_changes.all_changed!
97
+ end
98
+
99
+ def all_changed?
100
+ simple_mapper_changes.all
101
+ end
102
+
103
+ def changed_attributes
104
+ attribs = self.class.simple_mapper.attributes
105
+ if simple_mapper_changes.all
106
+ attribs.keys
107
+ else
108
+ attribs.inject([]) do |list, keyval|
109
+ list << keyval[0] if keyval[1].changed?(self)
110
+ list
111
+ end
112
+ end
113
+ end
114
+
115
+ def changed?
116
+ changed_attributes.size > 0
117
+ end
118
+
119
+ def freeze
120
+ self.class.simple_mapper.attributes.values.each {|attribute| attribute.freeze_for(self)}
121
+ simple_mapper_changes.freeze
122
+ end
123
+
124
+ def frozen?
125
+ simple_mapper_changes.frozen?
126
+ end
127
+
128
+ attr_reader :simple_mapper_source
129
+
130
+ def initialize(values = {})
131
+ @simple_mapper_source = values
132
+ @simple_mapper_init = {}
133
+ @simple_mapper_changes = SimpleMapper::ChangeHash.new
134
+ end
135
+
136
+ def to_simple(options = {})
137
+ clean_opt = options.clone
138
+ # if all_changed? true, we disregard changed flag entirely, so the entire graph
139
+ # appears to be changed in result set. We
140
+ # also propagate the :all flag, which tells
141
+ # objects that support it to include information
142
+ # about members that were removed.
143
+ if all_changed?
144
+ clean_opt.delete(:changed)
145
+ clean_opt[:all] = true
146
+ end
147
+ changes = (clean_opt[:changed] && true) || false
148
+ self.class.simple_mapper.attributes.values.inject({}) do |container, attrib|
149
+ attrib.to_simple(self, container, clean_opt) if !changes or attrib.changed?(self)
150
+ container
151
+ end
152
+ end
153
+
154
+ class Manager
155
+ attr_accessor :applies_to
156
+
157
+ def initialize(apply_to = nil)
158
+ self.applies_to = apply_to if apply_to
159
+ end
160
+
161
+ def attributes
162
+ @attributes ||= {}
163
+ end
164
+
165
+ def create_attribute(name, options = {})
166
+ attrib_class = options[:attribute_class] || SimpleMapper::Attribute
167
+ attrib_class.new(name, options)
168
+ end
169
+
170
+ def install_attribute(attr, object)
171
+ read_body = Proc.new { read_attribute(attr) }
172
+ write_body = Proc.new {|value| write_attribute(attr, value)}
173
+ applies_to.module_eval do
174
+ define_method(attr, &read_body)
175
+ define_method(:"#{attr}=", &write_body)
176
+ end
177
+ attributes[attr] = object
178
+ end
179
+
180
+ def create_anonymous_mapper(&block)
181
+ mapper = Class.new do
182
+ include SimpleMapper::Attributes
183
+ def self.decode(*arg)
184
+ new(*arg)
185
+ end
186
+ end
187
+ mapper.module_eval &block if block_given?
188
+ mapper
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ require 'simple_mapper/exceptions'
195
+ require 'simple_mapper/attributes/types'
196
+
@@ -0,0 +1,224 @@
1
+ module SimpleMapper::Attributes::Types
2
+ # Provides basic support for floating point numbers, closely tied to
3
+ # the core Ruby :Float:.
4
+ #
5
+ # Registered as type <tt>:float</tt>
6
+ #
7
+ # This is intended to be reasonably flexible and work with inputs
8
+ # that look numeric whether or not they literally appear to be "floats".
9
+ module Float
10
+ PATTERN = /^([0-9]+)(?:(\.)([0-9]*))?$/
11
+
12
+ # Decode a numeric-looking +value+ (whether a string or otherwise) into
13
+ # a +Float+.
14
+ #
15
+ # String inputs for +value+ should be numeric and may or may not have
16
+ # a decimal point, with or without digits following the decimal point.
17
+ # Therefore, the following values would all decode nicely:
18
+ # * +"14"+ to +14.0+
19
+ # * +"0"+ to +0.0+
20
+ # * +"123."+ to +123.0+
21
+ # * +"100.22022"+ to +100.22022+
22
+ # * +"0.0001"+ to +0.0001+
23
+ # * +14+ to +14.0+
24
+ # * +0+ to +0.0+
25
+ #
26
+ # The empty string will result in a value of +nil+
27
+ def self.decode(value)
28
+ return nil if (str = value.to_s).length == 0
29
+ return value if Float === value
30
+ match = str.match(PATTERN)
31
+ raise(SimpleMapper::TypeConversionException, "Cannot decode '#{value}' to Float.") unless match
32
+ value = match[1]
33
+ value += match[2] + match[3] if match[3].to_s.length > 0
34
+ value.to_s.to_f
35
+ end
36
+
37
+ # Encodes a float-like +value+ as a string, conforming to the basic
38
+ # syntax used for +decode+.
39
+ def self.encode(value)
40
+ return nil if value.nil?
41
+ if ! value.respond_to?(:to_f) or value.respond_to?(:match) && ! value.match(PATTERN)
42
+ raise(SimpleMapper::TypeConversionException, "Cannot encode '#{value}' as Float.")
43
+ end
44
+ value.to_f.to_s
45
+ end
46
+
47
+ # Returns the type default value of +nil+.
48
+ def self.default
49
+ nil
50
+ end
51
+ end
52
+ SimpleMapper::Attributes.register_type(:float, ::Float, Float)
53
+
54
+ # Provides simple string type support.
55
+ #
56
+ # Registered as <tt>:string</tt>.
57
+ #
58
+ # This is intended to be quite flexible and rely purely on duck typing.
59
+ # It should work with any input that supports <tt>:to_s</tt>, and is
60
+ # not strictly limited to actual +String+ instances.
61
+ module String
62
+ # Decodes +value+ into a +String+ object via the <tt>:to_s</tt> of
63
+ # +value+. Passes nils through unchanged. For the majority use case,
64
+ # most of the time the result and the input will be equal.
65
+ def self.decode(value)
66
+ return nil if value.nil?
67
+ value.to_s
68
+ end
69
+
70
+ # Encodes +value+ as a string via its <tt>:to_s</tt> method. Passes nils
71
+ # through unchanged.
72
+ def self.encode(value)
73
+ return nil if value.nil?
74
+ value.to_s
75
+ end
76
+
77
+ # Returns the empty string for a "default".
78
+ def self.default
79
+ ''
80
+ end
81
+ end
82
+ SimpleMapper::Attributes.register_type(:string, ::String, String)
83
+
84
+ # Provides basic UUID type support derived from the +simple_uuid+ gem.
85
+ #
86
+ # Passes nils through unchanged, but otherwise expects to work with
87
+ # instances of <tt>SimpleUUID::UUID</tt> or GUID strings. Attributes
88
+ # using this type will store the data simply as a GUID-conforming string
89
+ # as implemented by +SimpleUUID::UUID#to_guid+. For both decoding and
90
+ # encoding, a GUID string or an actual +SimpleUUID::UUID+ instance may
91
+ # be provided, but the result is always the corresponding GUID string.
92
+ #
93
+ # Registered as <tt>:simple_uuid</tt>.
94
+ module SimpleUUID
95
+ require 'simple_uuid'
96
+ EXPECTED_CLASS = ::SimpleUUID::UUID
97
+
98
+ # Encoded a <tt>SimpleUUID::UUID</tt> instance, or a GUID string,
99
+ # as a GUID string. GUID strings for +value+ will be validated by
100
+ # +SimpleUUID::UUID+ prior to passing through as the result.
101
+ #
102
+ # Passes nils through unchanged.
103
+ def self.encode(value)
104
+ normalize(value)
105
+ end
106
+
107
+ # Decode a <tt>SimpleUUID::UUID</tt> instance or GUID string into
108
+ # validated GUID string; strings will be validated by +SimpleUUID::UUID+
109
+ # prior to passing through as the result.
110
+ #
111
+ # Passes nils through unchanged.
112
+ def self.decode(value)
113
+ normalize(value)
114
+ end
115
+
116
+ def self.normalize(value)
117
+ value.nil? ? nil : EXPECTED_CLASS.new(value).to_guid
118
+ end
119
+
120
+ # Returns a new GUID string value.
121
+ def self.default
122
+ EXPECTED_CLASS.new.to_guid
123
+ end
124
+ end
125
+ SimpleMapper::Attributes.register_type(:simple_uuid, nil, SimpleUUID)
126
+
127
+ # Provides timezone-aware second-resolution timestamp support for
128
+ # basic attributes.
129
+ #
130
+ # Attributes of this type will have values that are instances of +DateTime+.
131
+ # These +DateTime+ values will be reduced to strings of format +'%Y-%m-%d %H:%M:%S%z'+
132
+ # when converting to a simple structure.
133
+ #
134
+ # On input, a +DateTime+ instance or a string matching the above format will be
135
+ # accepted and map to the proper +DateTime+.
136
+ #
137
+ # Decoding or encoding nils will simply pass nil through.
138
+ #
139
+ # Registered as type +:timestamp+
140
+ module Timestamp
141
+ require 'date'
142
+
143
+ FORMAT = '%Y-%m-%d %H:%M:%S%z'
144
+
145
+ # Encode a +DateTime+ _value_ as a string of format +'%Y-%m-%d %H:%M:%S%z'+.
146
+ # Given +nil+ for _value, +nil+ will be returned.
147
+ def self.encode(value)
148
+ return nil if value.nil?
149
+ value.strftime FORMAT
150
+ end
151
+
152
+ # Decode a _value_ string of format +'%Y-%m-%d %H:%M:%S%z' into a +DateTime+ instance.
153
+ # If given a +DateTime+ instance for _value_, that instance will be returned.
154
+ # Given +nil+ for _value_, +nil+ will be returned.
155
+ def self.decode(value)
156
+ return nil if value.nil?
157
+ return value if value.instance_of? DateTime
158
+ DateTime.strptime(value, FORMAT)
159
+ end
160
+
161
+ # Return a new +DateTime+ instance representing the current time. Note that this
162
+ # will include fractional seconds; this precision is lost when encoded to string,
163
+ # as the string representation only has a precision to the second.
164
+ def self.default
165
+ DateTime.now
166
+ end
167
+ end
168
+ SimpleMapper::Attributes.register_type(:timestamp, DateTime, Timestamp)
169
+
170
+ module Integer
171
+ def self.convert(value)
172
+ converted = value.to_i
173
+ unless value == converted or value.to_s == converted.to_s or converted.to_f.to_s == value.to_s
174
+ raise SimpleMapper::TypeConversionException, "cannot convert #{value} to Integer"
175
+ end
176
+ converted
177
+ end
178
+
179
+ # raise a TypeConversionException if not a valid Integer
180
+ # otherwise, convert to a string
181
+ def self.encode(value)
182
+ convert(value).to_s
183
+ end
184
+
185
+ def self.decode(value)
186
+ convert(value)
187
+ end
188
+ end
189
+ SimpleMapper::Attributes.register_type(:integer, ::Integer, Integer)
190
+
191
+ module TimestampHighRes
192
+ require 'bigdecimal'
193
+ unless BigDecimal.method_defined?(:to_r)
194
+ require 'bigdecimal/util'
195
+ end
196
+
197
+ SECOND_FRACTION = Rational(1, 24 * 60 * 60)
198
+ PATTERN = /^([^.]+)\.(\d+)([-+]\d{4})$/
199
+ OUT_FORMAT = '%Y-%m-%d %H:%M:%S.%N%z'
200
+ IN_FORMAT = '%Y-%m-%d %H:%M:%S%z'
201
+
202
+ def self.encode(value)
203
+ return nil if value.nil?
204
+ value.strftime(OUT_FORMAT)
205
+ end
206
+
207
+ def self.decode(value)
208
+ if value.kind_of?(DateTime)
209
+ value
210
+ elsif value.nil?
211
+ nil
212
+ else
213
+ if match = PATTERN.match(value.to_s)
214
+ stamp, second_fraction, zone = match.captures
215
+ subseconds = BigDecimal('0.' + second_fraction).to_r
216
+ DateTime.strptime(stamp + zone, IN_FORMAT) + (subseconds * SECOND_FRACTION)
217
+ else
218
+ raise SimpleMapper::TypeConversionException, "cannot transform '#{value}' into hi-res DateTime"
219
+ end
220
+ end
221
+ end
222
+ end
223
+ SimpleMapper::Attributes.register_type(:timestamp_high_res, ::DateTime, TimestampHighRes)
224
+ end
@@ -0,0 +1,14 @@
1
+ module SimpleMapper
2
+ class ChangeHash < Hash
3
+ attr_reader :all
4
+
5
+ def all_changed!
6
+ @all = true
7
+ end
8
+
9
+ def clear
10
+ @all = false
11
+ super
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,169 @@
1
+ require 'delegate'
2
+ module SimpleMapper
3
+ module Collection
4
+ module CommonMethods
5
+ attr_accessor :attribute
6
+ attr_accessor :change_tracking
7
+
8
+ def simple_mapper_changes
9
+ @simple_mapper_changes ||= SimpleMapper::ChangeHash.new
10
+ end
11
+
12
+ def changed_members
13
+ simple_mapper_changes.keys
14
+ end
15
+
16
+ def member_changed!(key, value)
17
+ return nil unless change_tracking
18
+ # If the key is new to the collection, we're fine.
19
+ # If the key is already in the collection, then we have to consider
20
+ # whether or not value is itself a mapper. If it is, we want it to consider
21
+ # all of its attributes changed, since they are all replacing whatever was
22
+ # previously associated with +key+ in the collection.
23
+ if ! value.nil? and value.respond_to?(:all_changed!)
24
+ value.all_changed!
25
+ end
26
+ simple_mapper_changes[key] = true
27
+ end
28
+
29
+ # Predicate that returns +true+ if _key_ is present in the collection.
30
+ # Returns +nil+ by default; this must be implemented appropriately per
31
+ # class that uses this module.
32
+ def is_member?(key)
33
+ nil
34
+ end
35
+
36
+ def []=(key, value)
37
+ member_changed!(key, value)
38
+ super(key, value)
39
+ end
40
+
41
+ def build(*args)
42
+ attribute.mapper.new(*args)
43
+ end
44
+ end
45
+
46
+ class Hash < DelegateClass(::Hash)
47
+ include CommonMethods
48
+
49
+ def is_member?(key)
50
+ key? key
51
+ end
52
+
53
+ def initialize(hash = {})
54
+ super(hash)
55
+ end
56
+
57
+ def delete(key)
58
+ member_changed!(key, nil)
59
+ super(key)
60
+ end
61
+
62
+ def reject!
63
+ changed = false
64
+ each do |key, val|
65
+ if yield(key, val)
66
+ changed = true
67
+ delete(key)
68
+ end
69
+ end
70
+ changed ? self : nil
71
+ end
72
+
73
+ def delete_if
74
+ reject! {|k, v| yield(k, v)}
75
+ self
76
+ end
77
+ end
78
+
79
+ class Array < DelegateClass(::Array)
80
+ include CommonMethods
81
+
82
+ def initialize(array=[])
83
+ super(array)
84
+ end
85
+
86
+ def is_member?(key)
87
+ key = key.to_i
88
+ key >= 0 and key < size
89
+ end
90
+
91
+ def keys
92
+ (0..size - 1).to_a
93
+ end
94
+
95
+ def inject(*args)
96
+ (0..size - 1).inject(*args) {|accum, key| yield(accum, [key, self[key]])}
97
+ end
98
+
99
+ def <<(value)
100
+ member_changed!(size, value)
101
+ super(value)
102
+ end
103
+
104
+ def push(*values)
105
+ values.each {|val| self << val }
106
+ self
107
+ end
108
+
109
+ def slice!(start_or_range, length=1)
110
+ result = nil
111
+ original_size = size
112
+ case start_or_range
113
+ when Range
114
+ result = super(start_or_range)
115
+ if result
116
+ change_min = start_or_range.min
117
+ end
118
+ else
119
+ result = super(start_or_range, length)
120
+ if result
121
+ change_min = start_or_range < 0 ? original_size + start_or_range : start_or_range
122
+ end
123
+ end
124
+ if result
125
+ change_min = 0 if change_min < 0
126
+ (change_min..original_size - 1).each {|index| member_changed!(index, self[index]) }
127
+ end
128
+ result
129
+ end
130
+
131
+ alias_method :_delete, :delete_at
132
+ private :_delete
133
+
134
+ def delete_at(index)
135
+ original_size = size
136
+ result = _delete(index)
137
+ if size != original_size
138
+ (index..original_size - 1).each {|idx| member_changed!(idx, self[idx])}
139
+ end
140
+ result
141
+ end
142
+
143
+ def reject!
144
+ first = nil
145
+ last = size - 1
146
+ index = 0
147
+ while index < size
148
+ if yield(self[index])
149
+ first ||= index
150
+ _delete(index)
151
+ else
152
+ index += 1
153
+ end
154
+ end
155
+ if first
156
+ (first..last).each {|idx| member_changed!(idx, self[idx])}
157
+ self
158
+ else
159
+ nil
160
+ end
161
+ end
162
+
163
+ def delete_if
164
+ reject! {|x| yield(x)}
165
+ self
166
+ end
167
+ end
168
+ end
169
+ end