active_nomad 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,7 @@
1
+ == 0.0.2 2010-09-28
2
+
3
+ * Rename to Active Nomad.
4
+
5
+ == 0.0.1 2010-09-28
6
+
7
+ * Hi.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 George Ogata
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,63 @@
1
+ # Active Nomad
2
+
3
+ ActiveRecord objects with a customizable persistence strategy.
4
+
5
+ ## Why
6
+
7
+ Sometimes you want an Active Record object that does not live in the database.
8
+ Perhaps it never needs to be persisted, or you'd like to store it in a cookie,
9
+ or a file, but it would still be handy to have ActiveRecord's ability to cast
10
+ values, run validations, or fire callbacks.
11
+
12
+ Ideally, the persistence strategy would be pluggable. With Active Nomad, it is!
13
+
14
+ ## How
15
+
16
+ Subclass from ActiveNomad::Base and declare your attributes with the
17
+ `attribute` class method. The arguments look just like creating a column in a
18
+ migration:
19
+
20
+ class Thing < ActiveNomad::Base
21
+ attribute :name, :string, :limit => 20
22
+ end
23
+
24
+ To persist the record, Active Nomad calls `persist`, which calls a
25
+ Proc registered by `to_save`. For example, here's how you could
26
+ persist to a cookie:
27
+
28
+ thing = Thing.deserialize(cookies[:thing])
29
+ thing.to_save do |thing|
30
+ cookies[:thing] = thing.serialize
31
+ true
32
+ end
33
+
34
+ Things to note:
35
+
36
+ * Active Nomad defines `serialize` and `deserialize` which will
37
+ serialize to and from a valid query string with predictable
38
+ attribute order (i.e., appropriate for a cookie).
39
+ * The proc should return true if persistence was successful, false
40
+ otherwise. This will be the return value of `save`, etc.
41
+ * You may alternatively override `persist` in a subclass if you
42
+ don't want to register a proc for every instance.
43
+
44
+ ## Notes
45
+
46
+ Only ActiveRecord 2.3 compatible. ActiveRecord 3.0 has a more modular
47
+ architecture which makes this largely unnecessary.
48
+
49
+ ## Contributing
50
+
51
+ * Bug reports: http://github.com/oggy/active_nomad/issues
52
+ * Source: http://github.com/oggy/active_nomad
53
+ * Patches: Fork on Github, send pull request.
54
+ * Ensure patch includes tests.
55
+ * Leave the version alone, or bump it in a separate commit.
56
+
57
+ ## Copyright
58
+
59
+ Copyright (c) 2010 George Ogata. See LICENSE for details.
60
+
61
+ ## Credit
62
+
63
+ Inspired by Jonathan Viney's ActiveRecord::BaseWithoutTable.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require 'ritual'
2
+
3
+ spec_task
@@ -0,0 +1,11 @@
1
+ module ActiveNomad
2
+ VERSION = [0, 0, 2]
3
+
4
+ class << VERSION
5
+ include Comparable
6
+
7
+ def to_s
8
+ join('.')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,127 @@
1
+ require 'active_record'
2
+ require 'cgi'
3
+
4
+ module ActiveNomad
5
+ NoPersistenceStrategy = Class.new(RuntimeError)
6
+
7
+ class Base < ActiveRecord::Base
8
+ #
9
+ # Tell this record how to save itself.
10
+ #
11
+ def to_save(&proc)
12
+ @save_proc = proc
13
+ end
14
+
15
+ #
16
+ # Return the attributes of this object serialized as a valid query
17
+ # string.
18
+ #
19
+ # Attributes are sorted by name.
20
+ #
21
+ def serialize
22
+ self.class.columns.map do |column|
23
+ name = column.name
24
+ value = serialize_value(send(name), column.type) or
25
+ next
26
+ "#{CGI.escape(name)}=#{value}"
27
+ end.compact.sort.join('&')
28
+ end
29
+
30
+ def self.deserialize(string)
31
+ params = string ? CGI.parse(string.strip) : {}
32
+ instance = new
33
+ columns.map do |column|
34
+ next if !params.key?(column.name)
35
+ value = params[column.name].first
36
+ instance.send "#{column.name}=", deserialize_value(value, column.type)
37
+ end
38
+ instance
39
+ end
40
+
41
+ protected
42
+
43
+ #
44
+ # Persist the object.
45
+ #
46
+ # The default is to call the block registered with
47
+ # #to_save. Override if you don't want to use #to_save.
48
+ #
49
+ def persist
50
+ @save_proc or
51
+ raise NoPersistenceStrategy, "no persistence strategy - use #to_save to define one"
52
+ @save_proc.call(self)
53
+ end
54
+
55
+ private
56
+
57
+ class FakeAdapter < ActiveRecord::ConnectionAdapters::AbstractAdapter
58
+ def native_database_types
59
+ @native_database_types ||= Hash.new{|h,k| h[k] = k.to_s}
60
+ end
61
+
62
+ def initialize
63
+ end
64
+ end
65
+
66
+ FAKE_ADAPTER = FakeAdapter.new
67
+
68
+ class << self
69
+ #
70
+ # Declare a column.
71
+ #
72
+ # Works like #add_column in a migration:
73
+ #
74
+ # column :name, :string, :limit => 1, :null => false, :default => 'Joe'
75
+ #
76
+ def attribute(name, type, options={})
77
+ sql_type = FAKE_ADAPTER.type_to_sql(type, options[:limit], options[:precision], options[:scale])
78
+ columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, options[:default], sql_type, options[:null] != false)
79
+ reset_column_information
80
+ end
81
+
82
+ def columns
83
+ @columns ||= []
84
+ end
85
+
86
+ # Reset everything, except the column information
87
+ def reset_column_information
88
+ columns = @columns
89
+ super
90
+ @columns = columns
91
+ end
92
+ end
93
+
94
+ self.abstract_class = true
95
+
96
+ def create_or_update_without_callbacks
97
+ errors.empty?
98
+ persist
99
+ end
100
+
101
+ def serialize_value(value, type)
102
+ return nil if value.nil?
103
+ case type
104
+ when :datetime, :timestamp, :time
105
+ value.to_time.to_i.to_s
106
+ when :date
107
+ (value.to_date - DATE_EPOCH).to_i.to_s
108
+ else
109
+ CGI.escape(value.to_s)
110
+ end
111
+ end
112
+
113
+ def self.deserialize_value(string, type)
114
+ return nil if string.nil?
115
+ case type
116
+ when :datetime, :timestamp, :time
117
+ Time.at(string.to_i)
118
+ when :date
119
+ DATE_EPOCH + string.to_i
120
+ else
121
+ CGI.unescape(string)
122
+ end
123
+ end
124
+
125
+ DATE_EPOCH = Date.parse('1970-01-01')
126
+ end
127
+ end
@@ -0,0 +1,4 @@
1
+ require 'active_nomad'
2
+
3
+ # TODO: This should not be necessary - we're not stubbing out enough.
4
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
@@ -0,0 +1,238 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveNomad::Base do
4
+ describe ".attribute" do
5
+ it "should create a column with the given name and type" do
6
+ klass = Class.new(ActiveNomad::Base) do
7
+ attribute :integer_attribute, :integer
8
+ attribute :string_attribute, :string
9
+ attribute :text_attribute, :text
10
+ attribute :float_attribute, :float
11
+ attribute :decimal_attribute, :decimal
12
+ attribute :datetime_attribute, :datetime
13
+ attribute :timestamp_attribute, :timestamp
14
+ attribute :time_attribute, :time
15
+ attribute :date_attribute, :date
16
+ attribute :binary_attribute, :binary
17
+ attribute :boolean_attribute, :boolean
18
+ end
19
+ klass.columns.should have(11).columns
20
+ klass.columns_hash['integer_attribute'].type.should == :integer
21
+ klass.columns_hash['string_attribute'].type.should == :string
22
+ klass.columns_hash['text_attribute'].type.should == :text
23
+ klass.columns_hash['float_attribute'].type.should == :float
24
+ klass.columns_hash['decimal_attribute'].type.should == :decimal
25
+ klass.columns_hash['datetime_attribute'].type.should == :datetime
26
+ klass.columns_hash['timestamp_attribute'].type.should == :timestamp
27
+ klass.columns_hash['time_attribute'].type.should == :time
28
+ klass.columns_hash['date_attribute'].type.should == :date
29
+ klass.columns_hash['binary_attribute'].type.should == :binary
30
+ klass.columns_hash['boolean_attribute'].type.should == :boolean
31
+ end
32
+
33
+ it "should treat a :limit option like #add_column in a migration" do
34
+ klass = Class.new(ActiveNomad::Base) do
35
+ attribute :name, :string, :limit => 100
36
+ end
37
+ klass.columns_hash['name'].limit.should == 100
38
+ end
39
+
40
+ it "should treat :scale and :precision options like #add_column in a migration" do
41
+ klass = Class.new(ActiveNomad::Base) do
42
+ attribute :value, :decimal, :precision => 5, :scale => 2
43
+ end
44
+ klass.columns_hash['value'].scale.should == 2
45
+ klass.columns_hash['value'].precision.should == 5
46
+ end
47
+
48
+ it "should treat a :null option like #add_column in a migration" do
49
+ klass = Class.new(ActiveNomad::Base) do
50
+ attribute :mandatory, :decimal, :null => false
51
+ attribute :optional, :decimal, :null => true
52
+ end
53
+ klass.columns_hash['mandatory'].null.should be_false
54
+ klass.columns_hash['optional'].null.should be_true
55
+ end
56
+
57
+ it "should treat a :default option like #add_column in a migration" do
58
+ klass = Class.new(ActiveNomad::Base) do
59
+ attribute :name, :string, :default => 'Joe'
60
+ end
61
+ klass.columns_hash['name'].default.should == 'Joe'
62
+ end
63
+ end
64
+
65
+ describe "an integer attribute" do
66
+ it "should cast the value from a string like ActiveRecord" do
67
+ klass = Class.new(ActiveNomad::Base) do
68
+ attribute :value, :integer
69
+ end
70
+ instance = klass.new(:value => '123')
71
+ instance.value.should == 123
72
+ end
73
+ end
74
+
75
+ describe "#save" do
76
+ describe "when no save strategy has been defined" do
77
+ it "should raise a NoPersistenceStrategy error" do
78
+ instance = ActiveNomad::Base.new
79
+ lambda{instance.save}.should raise_error(ActiveNomad::NoPersistenceStrategy)
80
+ end
81
+ end
82
+
83
+ describe "when a save strategy has been defined" do
84
+ before do
85
+ saves = @saves = []
86
+ @instance = ActiveNomad::Base.new
87
+ @instance.to_save do |*args|
88
+ saves << args
89
+ end
90
+ end
91
+
92
+ it "should call the save_proc with the record as an argument" do
93
+ @instance.save
94
+ @saves.should == [[@instance]]
95
+ end
96
+ end
97
+
98
+ describe "when #persist has been overridden" do
99
+ before do
100
+ saves = @saves = []
101
+ @klass = Class.new(ActiveNomad::Base) do
102
+ define_method :persist do |*args|
103
+ saves << args
104
+ end
105
+ end
106
+ end
107
+
108
+ it "should call it and return the result" do
109
+ instance = @klass.new
110
+ instance.save
111
+ @saves.should == [[]]
112
+ end
113
+ end
114
+ end
115
+
116
+ describe "#serialize" do
117
+ it "should serialize the attributes as a query string" do
118
+ klass = Class.new(ActiveNomad::Base) do
119
+ attribute :first_name, :string
120
+ attribute :last_name, :string
121
+ end
122
+ instance = klass.new(:first_name => 'Joe', :last_name => 'Blow')
123
+ instance.serialize.should == 'first_name=Joe&last_name=Blow'
124
+ end
125
+ end
126
+
127
+ describe ".deserialize" do
128
+ it "should create a new record with no attributes set if nil is given" do
129
+ klass = Class.new(ActiveNomad::Base) do
130
+ attribute :name, :string
131
+ end
132
+ instance = klass.deserialize(nil)
133
+ instance.name.should be_nil
134
+ end
135
+
136
+ it "should create a new record with no attributes set if an empty string is given" do
137
+ klass = Class.new(ActiveNomad::Base) do
138
+ attribute :name, :string
139
+ end
140
+ instance = klass.deserialize('')
141
+ instance.name.should be_nil
142
+ end
143
+
144
+ it "should create a new record with no attributes set if a blank string is given" do
145
+ klass = Class.new(ActiveNomad::Base) do
146
+ attribute :name, :string
147
+ end
148
+ instance = klass.deserialize(" \t")
149
+ instance.name.should be_nil
150
+ end
151
+
152
+ it "should leave defaults alone for attributes which are not set" do
153
+ klass = Class.new(ActiveNomad::Base) do
154
+ attribute :name, :string, :default => 'Joe'
155
+ end
156
+ instance = klass.deserialize(" \t")
157
+ instance.name.should == 'Joe'
158
+ end
159
+ end
160
+
161
+ describe "roundtripping through #serialize and .deserialize" do
162
+ it "should not be tripped up by delimiters in the keys" do
163
+ klass = Class.new(ActiveNomad::Base) do
164
+ attribute :'a=x', :string
165
+ attribute :'b&x', :string
166
+ end
167
+ original = klass.new("a=x" => "1", "b&x" => "2")
168
+ roundtripped = klass.deserialize(original.serialize)
169
+ roundtripped.send("a=x").should == "1"
170
+ roundtripped.send("b&x").should == "2"
171
+ end
172
+
173
+ it "should not be tripped up by delimiters in the values" do
174
+ klass = Class.new(ActiveNomad::Base) do
175
+ attribute :a, :string
176
+ attribute :b, :string
177
+ end
178
+ original = klass.new(:a => "1=2", :b => "3&4")
179
+ roundtripped = klass.deserialize(original.serialize)
180
+ roundtripped.a.should == "1=2"
181
+ roundtripped.b.should == "3&4"
182
+ end
183
+
184
+ def self.it_should_roundtrip(type, value)
185
+ value = Time.at(value.to_i) if value.is_a?(Time) # chop off subseconds
186
+ it "should roundtrip #{value.inspect} correctly as a #{type}" do
187
+ klass = Class.new(ActiveNomad::Base) do
188
+ attribute :value, type
189
+ end
190
+ instance = klass.new(:value => value)
191
+ roundtripped = klass.deserialize(instance.serialize)
192
+ roundtripped.value.should == value
193
+ end
194
+ end
195
+
196
+ it_should_roundtrip :integer, nil
197
+ it_should_roundtrip :integer, 0
198
+ it_should_roundtrip :integer, 123
199
+
200
+ it_should_roundtrip :string, nil
201
+ it_should_roundtrip :string, ''
202
+ it_should_roundtrip :string, 'hi'
203
+
204
+ it_should_roundtrip :text, nil
205
+ it_should_roundtrip :text, ''
206
+ it_should_roundtrip :text, 'hi'
207
+
208
+ it_should_roundtrip :float, nil
209
+ it_should_roundtrip :float, 0
210
+ it_should_roundtrip :float, 0.123
211
+
212
+ it_should_roundtrip :decimal, nil
213
+ it_should_roundtrip :decimal, BigDecimal.new('0')
214
+ it_should_roundtrip :decimal, BigDecimal.new('123.45')
215
+
216
+ it_should_roundtrip :datetime, nil
217
+ it_should_roundtrip :datetime, Time.now.in_time_zone
218
+ # TODO: Support DateTime here, which is used when the value is
219
+ # outside the range of a Time.
220
+
221
+ it_should_roundtrip :timestamp, nil
222
+ it_should_roundtrip :timestamp, Time.now.in_time_zone
223
+
224
+ it_should_roundtrip :time, nil
225
+ it_should_roundtrip :time, Time.parse('2000-01-01 01:23:34').in_time_zone
226
+
227
+ it_should_roundtrip :date, nil
228
+ it_should_roundtrip :date, Date.today
229
+
230
+ it_should_roundtrip :binary, nil
231
+ it_should_roundtrip :binary, ''
232
+ it_should_roundtrip :binary, "\0\1"
233
+
234
+ it_should_roundtrip :boolean, nil
235
+ it_should_roundtrip :boolean, true
236
+ it_should_roundtrip :boolean, false
237
+ end
238
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_nomad
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - George Ogata
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-09-28 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 2
32
+ - 3
33
+ - 0
34
+ version: 2.3.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 27
46
+ segments:
47
+ - 1
48
+ - 3
49
+ - 0
50
+ version: 1.3.0
51
+ type: :development
52
+ version_requirements: *id002
53
+ description:
54
+ email:
55
+ - george.ogata@gmail.com
56
+ executables: []
57
+
58
+ extensions: []
59
+
60
+ extra_rdoc_files:
61
+ - LICENSE
62
+ - README.markdown
63
+ files:
64
+ - lib/active_nomad/version.rb
65
+ - lib/active_nomad.rb
66
+ - CHANGELOG
67
+ - LICENSE
68
+ - README.markdown
69
+ - Rakefile
70
+ - spec/spec_helper.rb
71
+ - spec/unit/active_nomad_spec.rb
72
+ has_rdoc: true
73
+ homepage: http://github.com/oggy/active_nomad
74
+ licenses: []
75
+
76
+ post_install_message:
77
+ rdoc_options:
78
+ - --charset=UTF-8
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ hash: 23
96
+ segments:
97
+ - 1
98
+ - 3
99
+ - 6
100
+ version: 1.3.6
101
+ requirements: []
102
+
103
+ rubyforge_project:
104
+ rubygems_version: 1.3.7
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: ActiveRecord objects with a customizable persistence strategy.
108
+ test_files:
109
+ - spec/spec_helper.rb
110
+ - spec/unit/active_nomad_spec.rb