simple_mapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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