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,165 @@
1
+ = SimpleMapper: A Non-Relational Object Mapper
2
+
3
+ The simple data structures expressed through JSON are becoming more relevant every day.
4
+ We use them to exchange information between services via Thrift, from server to HTTP
5
+ client for RESTful services, or from server to HTTP client for Ajax-heavy web applications.
6
+
7
+ While JSON structures map nicely to core Ruby objects (Hash, Array, etc.), those structures
8
+ themselves do not provide business logic specific to an application's problem domain.
9
+ Additionally, it is increasingly the case that the JSON object structure is the canonical
10
+ representation your application receives for a given entity; a Thrift client, for instance,
11
+ will send and receive JSON to the corresponding service.
12
+
13
+ The object-relational mapper (ORM) traditionally addresses the need for business logic and
14
+ domain-specific semantics at the application tier when working with a provider of structured
15
+ data (i.e. a SQL-compliant relational database). SimpleMapper attempts to provide something
16
+ somewhat analogous to the ORM, but with JSON-based or "simple" data structures as the foundation
17
+ for structuring data, rather than a relational model of classes/relations and their constraints,
18
+ references, etc.
19
+
20
+ = What?
21
+
22
+ Say you need a service that serves and accepts JSON structures representing "users"
23
+ (because, really, who doesn't need _that_ service?). You might see data structures
24
+ like this (in JSON):
25
+
26
+ {
27
+ id: '348179ce-4d38-11df-8f4f-cd459e8422de',
28
+ registered_at: '2010-04-01 09:22:17-0400',
29
+ email: 'mister_hot_pantz@example.com',
30
+ title: 'Mr.',
31
+ first_name: 'Hot',
32
+ last_name: 'Pantz',
33
+ address: {
34
+ address: 'One My Way',
35
+ address2: 'Not That Way',
36
+ city: 'New York',
37
+ state: 'NY',
38
+ postal: '10010'
39
+ }
40
+ }
41
+
42
+ That's not a particularly complex data structure, but let's note a few things:
43
+ * The +:id+ appears to be a GUID. Fun.
44
+ * The +:registered_at+ is a timestamp with a particular format. Also fun.
45
+ * There's an +:email+ address. More fun.
46
+
47
+ All of those things and their noted funness would benefit from business logic for validation
48
+ purposes, encapsulation, etc. Our OOPified brains long for these structures to map to
49
+ an object.
50
+
51
+ So, go ahead.
52
+
53
+ class User
54
+ # Get our attribute mapping magic
55
+ include SimpleMapper::Attributes
56
+
57
+ # Define our typed attributes
58
+ maps :id, :type => :simple_uuid, :default => :from_type
59
+ maps :registered_at, :type => :timestamp, :default => :from_type
60
+
61
+ # Simple string attributes don't need a type
62
+ maps :email
63
+ maps :title
64
+ maps :first_name
65
+ maps :last_name
66
+
67
+ # nested attribute for the address.
68
+ maps :address do
69
+ # This block is evaluated in the context of new
70
+ # class that has the SimpleMapper behaviors
71
+ [:address, :address2, :city, :state, :postal].each {|attr| maps attr}
72
+
73
+ # How about a to_s that represents the full address as one string?
74
+ def to_s
75
+ "#{address}; #{address2}; #{city}, #{state} #{postal}"
76
+ end
77
+ end
78
+ end
79
+
80
+ Now you have a class that describes the data structure you're working with.
81
+ What now?
82
+
83
+ You can create new objects and spit out the simple structure.
84
+
85
+ user = User.new(:email => 'mister_hot_pantz@example.com',
86
+ :title => 'Mr.',
87
+ :first_name => 'Hot',
88
+ :last_name => 'Pantz',
89
+ :address => {:address => 'One My Way',
90
+ :address2 => 'Not That Way',
91
+ :city => 'New York',
92
+ :state => 'NY',
93
+ :postal => '10010'})
94
+ # the :simple_uuid type gives GUIDs, with a :default of :from_type
95
+ # meaning it'll autopopulate
96
+ # This prints some GUID like '348179ce-4d38-11df-8f4f-cd459e8422de':
97
+ puts user.id
98
+
99
+ # And the :default of :from_type on a :timestamp type gets the current date/time.
100
+ # This will print 'DateTime'
101
+ puts user.registered_at.class
102
+ # This will print it using DateTime's default :to_s format
103
+ # like '2010-0421T07:41:46-04:00'
104
+ puts user.registered_at
105
+
106
+ # This will print out 'One My Way; Not That Way; New York, NY 10010':
107
+ puts user.address
108
+
109
+ # The :to_simple method dumps the structure out in simple object format,
110
+ # which is readily JSON-ifiable. However, it'll enforce defaults and type
111
+ # formatting and such, so that things like timestamps will be stringified with
112
+ # the correct format.
113
+ user.to_simple
114
+
115
+ # Results in a structure like:
116
+ {
117
+ :id => '348179ce-4d38-11df-8f4f-cd459e8422de',
118
+ :registered_at => '2010-04-21 07:41:46-04:00',
119
+ :email => 'mister_hot_pantz@example.com',
120
+ :title => 'Mr.',
121
+ :first_name => 'Hot',
122
+ :last_name => 'Pantz',
123
+ :address => {
124
+ :address => 'One My Way',
125
+ :address2 => 'Not That Way',
126
+ :city => 'New York',
127
+ :state => 'NY',
128
+ :postal => '10010',
129
+ },
130
+ }
131
+
132
+ So, the +:new+ constructor and the +:to_simple+ method give you input and output
133
+ from/to simple structures, while the +SimpleMapper::Attributes+ module gives you
134
+ semantics for defining higher-level classes on top of those simple structures.
135
+
136
+ What if my service needs to have something analogous to an update, such that I only
137
+ get the simple structure for attributes that were changed?
138
+
139
+ user.last_name = 'Pantalonez'
140
+ user.to_simple(:changed => true)
141
+ # Results in:
142
+ # { :last_name => 'Pantalonez' }
143
+
144
+ The +:changed+ option indicates that we only want attributes that were altered since
145
+ the instance was instantiated. It doesn't care if the values actually differ from the
146
+ original, it only cares if the attribute was assigned since creation.
147
+
148
+ Similarly, you can provide a +:defined+ option to the +:to_simple+ invocation, and
149
+ you'll only get attributes in the resulting structure that have a non-nil value.
150
+ This is useful if you're ultimately dealing with a data source that manages such
151
+ things itself (like allowing +NULL+ on a particular database column) or is sparse
152
+ and would thus prefer to not have any entry for an undefined value at all (like
153
+ Cassandra, MongoDB, etc.)
154
+
155
+ = What's Coming
156
+
157
+ SimpleMapper is young as of this writing. There's a basic type system with
158
+ defaults. Support for nested structures is pretty simple, as shown above.
159
+
160
+ Features expected in the near future include:
161
+ * collections: deal with an attribute that is a collection
162
+ * pattern-based collections: group all key/value pairs into a single collection attribute
163
+ for any keys that match a developer-specific pattern
164
+ * ActiveModel compliance to allow validation, callbacks, etc.
165
+
@@ -0,0 +1,11 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/clean'
4
+
5
+ CLOBBER.include('*.gem')
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.pattern = 'test/**/*_test.rb'
9
+ t.libs = ['test', 'lib']
10
+ end
11
+
@@ -0,0 +1,8 @@
1
+ module SimpleMapper
2
+ require 'simple_mapper/change_hash'
3
+ require 'simple_mapper/collection'
4
+ require 'simple_mapper/attribute'
5
+ require 'simple_mapper/attribute/collection'
6
+ require 'simple_mapper/attribute/pattern'
7
+ require 'simple_mapper/attributes'
8
+ end
@@ -0,0 +1,119 @@
1
+ module SimpleMapper
2
+ class Attribute
3
+ Options = [
4
+ :default,
5
+ :key,
6
+ :type,
7
+ :mapper,
8
+ ]
9
+ attr_accessor :key, :name, :default, :mapper
10
+
11
+ def type=(new_type)
12
+ @type = new_type
13
+ end
14
+
15
+ def type
16
+ @type || mapper
17
+ end
18
+
19
+ def initialize(name, options = {})
20
+ self.key = self.name = name
21
+ process_options(options)
22
+ end
23
+
24
+ def process_options(options = {})
25
+ Options.each do |option|
26
+ self.send(:"#{option.to_s}=", options[option]) if options[option]
27
+ end
28
+ end
29
+
30
+ def source_value(object)
31
+ source = object.simple_mapper_source
32
+ source.has_key?(key) ? source[key] : source[key.to_s]
33
+ end
34
+
35
+ def transformed_source_value(object)
36
+ val = source_value(object)
37
+ val = default_value(object) if val.nil?
38
+ if type = self.type
39
+ if type.respond_to?(:decode)
40
+ type.decode(val)
41
+ else
42
+ type = SimpleMapper::Attributes.type_for(type)
43
+ if expected = type[:expected_type] and val.instance_of? expected
44
+ val
45
+ else
46
+ type[:converter].decode(val)
47
+ end
48
+ end
49
+ else
50
+ val
51
+ end
52
+ end
53
+
54
+ def value(object)
55
+ object.send(name)
56
+ end
57
+
58
+ def converter
59
+ return nil unless type
60
+ converter = type.respond_to?(:encode) || type.respond_to?(:decode) ? type : (t = SimpleMapper::Attributes.type_for(type) and t[:converter])
61
+ raise SimpleMapper::InvalidTypeException unless converter
62
+ converter
63
+ end
64
+
65
+ def encode(value)
66
+ return value unless c = converter
67
+ c.encode(value)
68
+ end
69
+
70
+ def to_simple(object, container, options = {})
71
+ raw_value = self.value(object)
72
+ if mapper
73
+ value = raw_value.nil? ? nil : raw_value.to_simple(options)
74
+ else
75
+ value = encode(raw_value)
76
+ end
77
+ container[options[:string_keys] ? key.to_s : key] = value unless value.nil? and options[:defined]
78
+ end
79
+
80
+ def change_tracking_for(object)
81
+ object.simple_mapper_changes
82
+ end
83
+
84
+ def changed!(object, flag=true)
85
+ if flag
86
+ change_tracking_for(object)[name] = true
87
+ else
88
+ change_tracking_for(object).delete(name)
89
+ false
90
+ end
91
+ end
92
+
93
+ def changed?(object)
94
+ if mapper and val = value(object)
95
+ val.changed?
96
+ else
97
+ (change_tracking_for(object)[name] && true) || false
98
+ end
99
+ end
100
+
101
+ def default_value(object)
102
+ case d = self.default
103
+ when :from_type
104
+ converter.default
105
+ when Symbol
106
+ object.send(d)
107
+ else
108
+ nil
109
+ end
110
+ end
111
+
112
+ def freeze_for(object)
113
+ if mapper and val = value(object)
114
+ val.freeze
115
+ end
116
+ end
117
+ end
118
+
119
+ end
@@ -0,0 +1,130 @@
1
+ # Include in a SimpleMapper::Attribute to give it collection behaviors:
2
+ # * have an attribute be a collection (i.e. a hash or array) of values
3
+ # rather than a single value
4
+ # * map N keys from a source value to the attribute, based on the +:member_key?+ test
5
+ # * map keys/values from the collection object back to a "source" structure
6
+ module SimpleMapper::Attribute::Collection
7
+ # Given an _object_ that has the attribute represented by the receiver,
8
+ # returns the source value for the attribute after applying defaults and types.
9
+ # The type check is applied to either the raw source value (if one exists) or
10
+ # the default value (if no source exists and a default was specified for the attribute).
11
+ #
12
+ # Type checking is not enforced here; it is expected that +:source_value+,
13
+ # +:default_value+, and +:apply_type+ will all return objects of the expected collection
14
+ # type (or consistent interface).
15
+ def transformed_source_value(object)
16
+ val = source_value(object)
17
+ val = default_value(object) if val.nil?
18
+ val = apply_type(val) if type
19
+ val.change_tracking = true if val.respond_to?(:change_tracking)
20
+ val
21
+ end
22
+
23
+ def member_key?(key)
24
+ false
25
+ end
26
+
27
+ def source_value(object)
28
+ object.simple_mapper_source.inject(new_collection) do |hash, keyval|
29
+ hash[from_simple_key(keyval[0])] = keyval[1] if member_key?(keyval[0])
30
+ hash
31
+ end
32
+ end
33
+
34
+ # If the receiver has a valid type specified, returns a new collection based on
35
+ # _value_, with the key/value pairs from _value_ but each value encoded by
36
+ # the type converter.
37
+ def apply_type(value)
38
+ converter = self.converter
39
+ value.inject(new_collection) do |hash, keyval|
40
+ hash[keyval[0]] = converter.decode(keyval[1])
41
+ hash
42
+ end
43
+ end
44
+
45
+ # Returns a new collection object to be used as the basis for building the attribute's
46
+ # value collection; by default, returns instances of SimpleMapper::Collection::Hash.
47
+ # Override this to alter the kinds of collections your attributes work with.
48
+ def new_collection
49
+ h = SimpleMapper::Collection::Hash.new
50
+ h.attribute = self
51
+ h
52
+ end
53
+
54
+ # Given a _key_ from the source structure (the "simple structure"), returns a
55
+ # transformed key as it will be entered in the attribute's collection value.
56
+ # By default, this simply passes through _key_, but it can be overridden to
57
+ # allow for more sophisticated source/object mappings.
58
+ def from_simple_key(key)
59
+ key
60
+ end
61
+
62
+ # The reverse of +:to_simple_key+, given a _key_ from the attribute collection,
63
+ # returns the transformed version of that key as it should appear in the "simple"
64
+ # representation of the object.
65
+ def to_simple_key(key)
66
+ key
67
+ end
68
+
69
+ def freeze_for(object)
70
+ val = value(object)
71
+ if val
72
+ if mapper
73
+ if val.respond_to?(:values)
74
+ val.values.each {|member| member.freeze}
75
+ else
76
+ val.each {|member| member.freeze}
77
+ end
78
+ end
79
+ val.freeze
80
+ end
81
+ end
82
+
83
+ def changed?(object)
84
+ val = value(object)
85
+ return true if val.changed_members.size > 0
86
+ return true if mapper and val.find {|keyval| keyval[1].changed?}
87
+ false
88
+ end
89
+
90
+ # Converts the _object_'s attribute value into its simple representation,
91
+ # putting the keys/values into _container_. This is conceptually consistent
92
+ # with +SimpleMapper::Attributes#to_simple+, but adds a few collection-oriented
93
+ # concerns:
94
+ # * the attribute's value is assumed to be a collection with N key/value pairs
95
+ # to be mapped into _container_
96
+ # * the keys are transformed via +:to_simple_key+ on their way into _container_.
97
+ #
98
+ # This will work with any kind of _container_ that can be assigned to via +[]=+,
99
+ # and any value for the attribute that supports +:inject+ in the same manner as
100
+ # a Hash (yields the accumulated value and a key/value pair to the block).
101
+ def to_simple(object, container, options = {})
102
+ val = value(object)
103
+ mapper = self.mapper
104
+ strings = options[:string_keys] || false
105
+ changed_members = change_tracking_for(val)
106
+ if options[:changed]
107
+ if mapper
108
+ change_proc = Proc.new do |key, val|
109
+ changed_members[key] or (! val.nil? and val.changed?)
110
+ end
111
+ else
112
+ change_proc = Proc.new {|k, v| changed_members[k]}
113
+ end
114
+ else
115
+ change_proc = nil
116
+ end
117
+ changes = options[:changed] || false
118
+ container = val.inject(container) do |hash, keyvalue|
119
+ key = to_simple_key(keyvalue[0])
120
+ hash[strings ? key.to_s : key] = mapper ? keyvalue[1].to_simple(options) : encode(keyvalue[1]) if ! change_proc or change_proc.call(*keyvalue)
121
+ hash
122
+ end
123
+ if (change_proc or options[:all]) and ! options[:defined]
124
+ changed_members.keys.find_all {|x| ! val.is_member?(x)}.each do |key|
125
+ container[to_simple_key(key)] = nil
126
+ end
127
+ end
128
+ container
129
+ end
130
+ end
@@ -0,0 +1,19 @@
1
+ class SimpleMapper::Attribute::Pattern < SimpleMapper::Attribute
2
+ include SimpleMapper::Attribute::Collection
3
+
4
+ attr_reader :pattern
5
+
6
+ def initialize(name, options = {})
7
+ super(name, options)
8
+ self.pattern = options[:pattern]
9
+ end
10
+
11
+ def pattern=(value)
12
+ raise SimpleMapper::InvalidPatternException unless value.respond_to?(:match)
13
+ @pattern = value
14
+ end
15
+
16
+ def member_key?(key)
17
+ (pattern.match(key.to_s) and true) or false
18
+ end
19
+ end