active_nomad 0.0.2

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.
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