armada 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2010 Sam Aarons
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
File without changes
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # coding: UTF-8
2
+
3
+ require "rake"
4
+ require "spec/rake/spectask"
5
+
6
+ desc "Run all tests"
7
+ Spec::Rake::SpecTask.new("test") do |t|
8
+ t.spec_opts = ["--color"]
9
+ t.pattern = "test/**/*_spec.rb"
10
+ end
@@ -0,0 +1,76 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ module AttributeMethods
5
+ extend ActiveSupport::Concern
6
+ include ActiveModel::AttributeMethods
7
+
8
+ included do
9
+ attr_reader :attributes
10
+ class_attribute :columns
11
+ self.columns = [:id]
12
+ ["", "="].each { |x| attribute_method_suffix(x) }
13
+ end
14
+
15
+ module ClassMethods
16
+ def add_columns(*cols)
17
+ self.columns = (self.columns + cols.map(&:to_sym)).uniq
18
+ end
19
+ alias :add_column :add_columns
20
+
21
+ def remove_columns(*cols)
22
+ self.columns = (self.columns - cols.map(&:to_sym).delete_if { |x| x == :id })
23
+ end
24
+ alias :remove_column :remove_columns
25
+
26
+ def define_attribute_methods
27
+ super(self.columns)
28
+ end
29
+ end
30
+
31
+ def write_attribute(attribute_name, value)
32
+ attribute_name = attribute_name.to_s
33
+ if !persisted? || attribute_name != "id"
34
+ @attributes[attribute_name] = value
35
+ else
36
+ @attributes["id"]
37
+ end
38
+ end
39
+
40
+ def read_attribute(attribute_name)
41
+ @attributes[attribute_name]
42
+ end
43
+
44
+ def attributes=(attributes)
45
+ attributes.each_pair { |k, v| send("#{k}=",v) }
46
+ @attributes
47
+ end
48
+
49
+ def method_missing(method_id, *args, &block)
50
+ if !self.class.attribute_methods_generated?
51
+ self.class.define_attribute_methods
52
+ method_name = method_id.to_s
53
+ guard_private_attribute_method!(method_name, args)
54
+ send(method_id, *args, &block)
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ def respond_to?(*args)
61
+ self.class.define_attribute_methods
62
+ super
63
+ end
64
+
65
+ private
66
+ def attribute=(attribute_name, value)
67
+ write_attribute(attribute_name, value)
68
+ end
69
+
70
+ def attribute(attribute_name)
71
+ read_attribute(attribute_name)
72
+ end
73
+ alias :read_attribute_for_validation :attribute
74
+
75
+ end
76
+ end
@@ -0,0 +1,77 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ module Callbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ CALLBACKS = [
8
+ :before_validation, :after_validation,
9
+ :before_save, :after_save, :around_save,
10
+ :before_create, :after_create, :around_create,
11
+ :before_update, :after_update, :around_update,
12
+ :before_destroy, :after_destroy, :around_destroy
13
+ ]
14
+
15
+ included do
16
+ %w(create_or_update valid? create update destroy).each do |method|
17
+ alias_method_chain method, :callbacks
18
+ end
19
+ extend ActiveModel::Callbacks
20
+ define_callbacks :validation, :terminator => "result == false", :scope => [:kind, :name]
21
+ define_model_callbacks :save, :create, :update, :destroy
22
+ end
23
+
24
+ module ClassMethods
25
+ def before_validation(*args, &block)
26
+ options = args.last
27
+ if options.is_a?(Hash) && options[:on]
28
+ options[:if] = Array(options[:if])
29
+ options[:if] << "@_on_validate == :#{options[:on]}"
30
+ end
31
+ set_callback(:validation, :before, *args, &block)
32
+ end
33
+
34
+ def after_validation(*args, &block)
35
+ options = args.extract_options!
36
+ options[:prepend] = true
37
+ options[:if] = Array(options[:if])
38
+ options[:if] << "!halted && value != false"
39
+ options[:if] << "@_on_validate == :#{options[:on]}" if options[:on]
40
+ set_callback(:validation, :after, *(args << options), &block)
41
+ end
42
+ end
43
+
44
+ def valid_with_callbacks?
45
+ @_on_validate = new_record? ? :create : :update
46
+ _run_validation_callbacks do
47
+ valid_without_callbacks?
48
+ end
49
+ end
50
+
51
+ def destroy_with_callbacks
52
+ _run_destroy_callbacks do
53
+ destroy_without_callbacks
54
+ end
55
+ end
56
+
57
+ private
58
+ def create_or_update_with_callbacks
59
+ _run_save_callbacks do
60
+ create_or_update_without_callbacks
61
+ end
62
+ end
63
+
64
+ def create_with_callbacks
65
+ _run_create_callbacks do
66
+ create_without_callbacks
67
+ end
68
+ end
69
+
70
+ def update_with_callbacks(*args)
71
+ _run_update_callbacks do
72
+ update_without_callbacks(*args)
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,54 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ mattr_reader :connection
5
+
6
+ class Connection
7
+ def initialize(host, port, password)
8
+ @host = host
9
+ @port = port
10
+ @password = password
11
+ connect
12
+ end
13
+
14
+ def connect
15
+ begin
16
+ @socket = TCPSocket.new(@host, @port)
17
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
18
+ query(["auth", @password]) if @password
19
+ rescue
20
+ raise Armada::ConnectionError, "could not connect"
21
+ end
22
+ end
23
+
24
+ def query(q)
25
+ request = ActiveSupport::JSON.encode(q)
26
+ @socket.write(request << "\r\n")
27
+ status, value = ActiveSupport::JSON.decode(@socket.gets)
28
+ status == 0 ? value : raise(Armada::ServerError.new(request),value)
29
+ end
30
+ end
31
+
32
+ def self.setup!(spec = {})
33
+ return @@connection if @@connection
34
+ config = { address: "127.0.0.1", port: 3400 }.merge!(spec)
35
+ @@connection = Connection.new(config[:address], config[:port], config[:password])
36
+ end
37
+
38
+ def self.compact!
39
+ query_if_connection("compact")
40
+ end
41
+
42
+ def self.list_collections
43
+ query_if_connection("list-collections")
44
+ end
45
+
46
+ def self.explain(query)
47
+ query_if_connection("explain", query)
48
+ end
49
+
50
+ private
51
+ def self.query_if_connection(*query)
52
+ @@connection && @@connection.query(query)
53
+ end
54
+ end
@@ -0,0 +1,32 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ module DatabaseMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ singleton_class.alias_method_chain :inherited, :collection_name
9
+ end
10
+
11
+ module ClassMethods
12
+ def collection_name(name = nil)
13
+ name ? (@collection_name = name) : @collection_name
14
+ end
15
+
16
+ private
17
+ def instantiate(attributes)
18
+ record = self.allocate
19
+ record.instance_variable_set(:@attributes, attributes.with_indifferent_access)
20
+ record.instance_variable_set(:@new_record, false)
21
+ record
22
+ end
23
+
24
+ def inherited_with_collection_name(subclass)
25
+ subclass.collection_name(subclass.model_name.plural)
26
+ inherited_without_collection_name(subclass)
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ module Dirty
5
+ extend ActiveSupport::Concern
6
+ include ActiveModel::Dirty
7
+
8
+ included do
9
+ [:create_or_update, :write_attribute].each do |method|
10
+ alias_method_chain method, :dirty
11
+ end
12
+ end
13
+
14
+ private
15
+ def write_attribute_with_dirty(attribute_name, value)
16
+ send("#{attribute_name}_will_change!") if persisted?
17
+ write_attribute_without_dirty(attribute_name, value)
18
+ end
19
+
20
+ def create_or_update_with_dirty
21
+ if status = create_or_update_without_dirty
22
+ @previously_changed = changes
23
+ changed_attributes.clear
24
+ else
25
+ changed.each { |attribute_name| send("reset_#{attribute_name}!") } if persisted?
26
+ end
27
+ status
28
+ end
29
+
30
+ def serializable_changes
31
+ changed.inject({}) { |h, a| h[a] = @attributes[a]; h }
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ class ServerError < StandardError
5
+ attr_reader :query
6
+ def initialize(query)
7
+ @query = query
8
+ end
9
+ end
10
+
11
+ class ConnectionError < StandardError
12
+ end
13
+
14
+ class RecordNotFound < StandardError
15
+ end
16
+
17
+ class RecordNotSaved < StandardError
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ module FinderMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def find(*args)
10
+ find_from_ids(args)
11
+ end
12
+
13
+ private
14
+ def find_from_ids(ids)
15
+ expects_array = ids.first.kind_of?(Array)
16
+ return ids.first if expects_array && ids.first.empty?
17
+
18
+ ids = ids.flatten.compact.uniq
19
+
20
+ case ids.size
21
+ when 0
22
+ raise(Armada::RecordNotFound)
23
+ when 1
24
+ result = find_one(ids.first)
25
+ expects_array ? [ result ] : result
26
+ else
27
+ find_some(ids)
28
+ end
29
+ end
30
+
31
+ def find_one(id)
32
+ result = self.where(:id => id).limit(1).first
33
+ result.blank? ? raise(Armada::RecordNotFound) : result
34
+ end
35
+
36
+ def find_some(ids)
37
+ results = self.where(:id => ids).all
38
+ results.size == ids.size ? results : raise(Armada::RecordNotFound)
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,92 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ class Model
5
+ include Armada::Validations
6
+ extend ActiveModel::Naming
7
+ include Armada::FinderMethods
8
+ include ActiveModel::Observing
9
+ include Armada::DatabaseMethods
10
+ include Armada::RelationMethods
11
+ include ActiveModel::Conversion
12
+ extend ActiveModel::Translation
13
+ include Armada::AttributeMethods
14
+ include ActiveModel::Validations
15
+ include ActiveModel::Serialization
16
+ include ActiveModel::Serializers::Xml
17
+ include ActiveModel::Serializers::JSON
18
+
19
+ def initialize(attributes = {})
20
+ @attributes = {}.with_indifferent_access
21
+ @new_record = true
22
+ self.attributes = attributes
23
+ end
24
+
25
+ def new_record?
26
+ @new_record || false
27
+ end
28
+
29
+ def destroy
30
+ @destroyed = (persisted? && relation.delete == 1)
31
+ end
32
+
33
+ def destroyed?
34
+ @destroyed || false
35
+ end
36
+
37
+ def persisted?
38
+ !(new_record? || destroyed?)
39
+ end
40
+
41
+ def save
42
+ create_or_update
43
+ end
44
+
45
+ def save!
46
+ save || raise(Armada::RecordNotSaved)
47
+ end
48
+
49
+ def ==(other)
50
+ klass = self.class
51
+ case other
52
+ when klass then klass.collection_name == other.class.collection_name && self.attributes == other.attributes
53
+ else false
54
+ end
55
+ end
56
+
57
+ protected
58
+ def generate_unique_id
59
+ self.id ||= rand(36**26).to_s(36)[0..24]
60
+ end
61
+
62
+ private
63
+ def create_or_update
64
+ (valid? && !destroyed?) && (new_record? ? create : update)
65
+ end
66
+
67
+ def create
68
+ if status = (relation.insert(@attributes) == 1)
69
+ @new_record = false
70
+ end
71
+ status
72
+ end
73
+
74
+ def update
75
+ changed? && relation.update(serializable_changes) == 1
76
+ end
77
+
78
+ def relation
79
+ @relation ||= Armada::Relation.new(self.class).where(:id => self.id)
80
+ end
81
+
82
+ end
83
+ end
84
+
85
+ Armada::Model.class_eval do
86
+ include Armada::Dirty
87
+ include Armada::Callbacks
88
+ include Armada::Timestamp
89
+
90
+ validates :id, :presence => true
91
+ before_validation :generate_unique_id, :on => :create
92
+ end
@@ -0,0 +1,34 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ class Observer < ActiveModel::Observer
5
+ class_attribute :observed_methods
6
+ self.observed_methods = []
7
+
8
+ def initialize
9
+ super
10
+ observed_subclasses.each { |klass| add_observer!(klass) }
11
+ end
12
+
13
+ def self.method_added(method)
14
+ self.observed_methods += [method] if Armada::Callbacks::CALLBACKS.include?(method.to_sym)
15
+ end
16
+
17
+ protected
18
+ def observed_subclasses
19
+ observed_classes.sum([]) { |klass| klass.send(:subclasses) }
20
+ end
21
+
22
+ def add_observer!(klass)
23
+ super
24
+
25
+ self.class.observed_methods.each do |method|
26
+ callback = :"_notify_observers_for_#{method}"
27
+ if (klass.instance_methods & [callback, callback.to_s]).empty?
28
+ klass.class_eval "def #{callback}; notify_observers(:#{method}); end"
29
+ klass.send(method, callback)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,262 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ module RelationMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ RELATION_METHODS = %w(all)
9
+ QUERY_OPTIONS = %w(where offset order limit only distinct)
10
+ QUERY_METHODS = %w(insert select delete count multi_read multi_write checked_write create_index drop_index list_indexes)
11
+
12
+ def relation
13
+ @_class_relation ||= Armada::Relation.new(self)
14
+ end
15
+
16
+ delegate(*(QUERY_METHODS + QUERY_OPTIONS + RELATION_METHODS), :to => :relation)
17
+
18
+ end
19
+
20
+ end
21
+
22
+ class Relation
23
+ FIND_OPTIONS_KEYS = %w(where offset order limit only distinct)
24
+
25
+ # undefine :select to avoid bugs/confusion with Kernel.select
26
+ # ...true story
27
+ undef_method :select
28
+
29
+ def initialize(superclass)
30
+ @superclass = superclass
31
+ @join = "and"
32
+ end
33
+
34
+ def where(conditions)
35
+ where = []
36
+ conditions.each_pair do |attribute, value|
37
+ if value.is_a?(Hash)
38
+ value.each_pair do |operator, val|
39
+ where << [operator, attribute, build_condition_value(val)]
40
+ end
41
+ else
42
+ where << build_condition(attribute, value)
43
+ end
44
+ end
45
+ where = where.size > 1 ? where.unshift("and") : where.first
46
+
47
+ join, old_where = [@join, @where]
48
+
49
+ new_where = if option_defined?(:where)
50
+ if join == old_where.first && old_where.first == where.first
51
+ old_where.concat(where.from(1))
52
+ elsif join == old_where.first && !is_conjunction?(where.first)
53
+ old_where << where
54
+ elsif join == where.first && is_conjunction?(old_where.first)
55
+ old_where.tap { |w| w[-1] = ([join] << w.last).concat(where.from(1)) }
56
+ elsif join == where.first && !is_conjunction?(old_where.first)
57
+ where.insert(1,old_where)
58
+ elsif join == "and" && old_where.first == "or" && !is_conjunction?(where.first) && !is_conjunction?(old_where.last.first)
59
+ old_where.tap { |w| w[-1] = [join] << w.last << where }
60
+ elsif join == "and" && old_where.first == "or" && !is_conjunction?(where.first) && is_conjunction?(old_where.last.first)
61
+ old_where.tap { |w| w[-1] << where }
62
+ else
63
+ [join] << old_where << where
64
+ end
65
+ else
66
+ where
67
+ end
68
+ self.dup.tap do |r|
69
+ r.set_option(:where, new_where)
70
+ r.set_option(:join, "and")
71
+ end
72
+ end
73
+
74
+ def or
75
+ raise(ArgumentError, "Missing 'where' condition") unless option_defined?(:where)
76
+ self.dup.tap { |r| r.set_option(:join, "or") }
77
+ end
78
+
79
+ def offset(value)
80
+ self.dup.tap { |r| r.set_option(:offset, value) }
81
+ end
82
+
83
+ def limit(value)
84
+ self.dup.tap { |r| r.set_option(:limit, value) }
85
+ end
86
+
87
+ def order(*attributes)
88
+ options = attributes.extract_options!
89
+
90
+ attributes.flatten!
91
+ attributes.map! do |attribute|
92
+ attribute.is_a?(Array) ? attribute : [attribute, :asc]
93
+ end
94
+
95
+ order = attributes.concat(options.to_a).tap do |order|
96
+ order.flatten! if order.size == 1
97
+ end
98
+
99
+ old_order = @order
100
+
101
+ new_order = if option_defined?(:order)
102
+ old_order = [old_order] unless old_order.first.is_a?(Array)
103
+ order.first.is_a?(Array) ? old_order.concat(order) : old_order << order
104
+ else
105
+ order
106
+ end
107
+
108
+ self.dup.tap { |r| r.set_option(:order, new_order) }
109
+ end
110
+
111
+ def only(*attributes)
112
+ attributes.flatten!
113
+ only = if option_defined?(:only)
114
+ Array.wrap(@only).concat(attributes)
115
+ else
116
+ attributes.size == 1 ? attributes.first : attributes
117
+ end
118
+
119
+ self.dup.tap { |r| r.set_option(:only, only) }
120
+ end
121
+
122
+ def distinct
123
+ self.dup.tap { |r| r.set_option(:distinct, true) }
124
+ end
125
+
126
+ def to_query(method = nil, *args, &block)
127
+ method ? send("generate_#{method}_query", *args, &block) : find_options
128
+ end
129
+
130
+ def all
131
+ results = self.select
132
+ return results if option_defined?(:only) || @superclass.is_a?(String)
133
+ results.map { |record| @superclass.send(:instantiate, record) }
134
+ end
135
+ delegate :first, :last, :to => :all
136
+
137
+ def set_option(option, value)
138
+ instance_variable_set("@#{option}", value)
139
+ end
140
+
141
+ def get_option(option)
142
+ instance_variable_get("@#{option}")
143
+ end
144
+
145
+ private
146
+
147
+ def method_missing(method, *args, &block)
148
+ method = method.to_s
149
+ if %w(create_index drop_index).include?(method)
150
+ singleton_class.send(:define_method, method) do
151
+ raise(ArgumentError, "Missing 'order' condition") unless option_defined?(:order)
152
+ query(to_query(method, @order)) == 1
153
+ end.call
154
+ elsif %w(delete count update insert select checked_write multi_read multi_write list_indexes).include?(method)
155
+ singleton_class.send(:define_method, method) do |*args, &block|
156
+ query(to_query(method, *args, &block))
157
+ end.call(*args, &block)
158
+ else
159
+ super
160
+ end
161
+ end
162
+
163
+ def generate_list_indexes_query
164
+ ["list-indexes", collection_name]
165
+ end
166
+
167
+ def generate_create_index_query(order)
168
+ ["create-index", collection_name, order]
169
+ end
170
+
171
+ def generate_drop_index_query(order)
172
+ ["drop-index", collection_name, order]
173
+ end
174
+
175
+ def generate_multi_read_query(read_queries = [], &block)
176
+ yield read_queries if block_given?
177
+ ["multi-read", read_queries]
178
+ end
179
+
180
+ def generate_multi_write_query(queries = [], &block)
181
+ yield queries if block_given?
182
+ ["multi-write", queries]
183
+ end
184
+
185
+ def generate_checked_write_query(read_query, expected_result, write_query)
186
+ ["checked-write", read_query, expected_result, write_query]
187
+ end
188
+
189
+ def generate_delete_query
190
+ ["delete", collection_name].tap do |q|
191
+ q << find_options if find_options?
192
+ end
193
+ end
194
+
195
+ def generate_update_query(changes)
196
+ ["update", collection_name, changes, find_options]
197
+ end
198
+
199
+ def generate_insert_query(records)
200
+ ["insert", collection_name].tap do |q|
201
+ q << (records.size == 1 ? records.first : records)
202
+ end
203
+ end
204
+
205
+ def generate_count_query
206
+ ["count", collection_name].tap do |q|
207
+ q << find_options if find_options?
208
+ end
209
+ end
210
+
211
+ def generate_select_query
212
+ ["select", collection_name].tap do |q|
213
+ q << find_options if find_options?
214
+ end
215
+ end
216
+
217
+ def is_conjunction?(value)
218
+ %w(and or).include?(value)
219
+ end
220
+
221
+ def option_defined?(option)
222
+ instance_variable_defined?("@#{option}")
223
+ end
224
+
225
+ def find_options?
226
+ FIND_OPTIONS_KEYS.any? { |x| option_defined?(x) }
227
+ end
228
+
229
+ def find_options
230
+ FIND_OPTIONS_KEYS.dup.inject({}) do |h1, x|
231
+ option_defined?(x) ? h1.tap { |h2| h2[x.to_sym] = instance_variable_get("@#{x}") } : h1
232
+ end
233
+ end
234
+
235
+ def collection_name
236
+ @collection_name ||= @superclass.is_a?(String) ? @superclass : @superclass.collection_name
237
+ end
238
+
239
+ def query(args)
240
+ Armada.connection.query(args)
241
+ end
242
+
243
+ def build_condition(attribute, value)
244
+ operator = case value
245
+ when Array then "in"
246
+ when Range then value.exclude_end? ? ">=<" : ">=<="
247
+ else "="
248
+ end
249
+ [operator, attribute, build_condition_value(value)]
250
+ end
251
+
252
+ def build_condition_value(value)
253
+ case value
254
+ when Range then [value.first, value.last]
255
+ when Time, DateTime then value.to_i
256
+ else value
257
+ end
258
+ end
259
+
260
+
261
+ end
262
+ end
@@ -0,0 +1,58 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ module Timestamp
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ alias_method_chain :create, :timestamps
9
+ alias_method_chain :update, :timestamps
10
+
11
+ class_attribute :record_timestamps
12
+ self.record_timestamps = true
13
+ end
14
+
15
+ def touch(attribute = nil)
16
+ current_time = current_time_from_current_timezone
17
+
18
+ if attribute
19
+ write_attribute(attribute, current_time)
20
+ else
21
+ write_attribute('updated_at', current_time) if respond_to?(:updated_at)
22
+ write_attribute('updated_on', current_time) if respond_to?(:updated_on)
23
+ end
24
+
25
+ save!
26
+ end
27
+
28
+ private
29
+ def create_with_timestamps
30
+ if self.class.record_timestamps?
31
+ current_time = current_time_from_current_timezone
32
+
33
+ write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil?
34
+ write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil?
35
+
36
+ write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil?
37
+ write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil?
38
+ end
39
+
40
+ create_without_timestamps
41
+ end
42
+
43
+ def update_with_timestamps(*args)
44
+ if self.class.record_timestamps? && changed?
45
+ current_time = current_time_from_current_timezone
46
+
47
+ write_attribute('updated_at', current_time) if respond_to?(:updated_at)
48
+ write_attribute('updated_on', current_time) if respond_to?(:updated_on)
49
+ end
50
+
51
+ update_without_timestamps(*args)
52
+ end
53
+
54
+ def current_time_from_current_timezone
55
+ Time.zone.now.to_i
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,41 @@
1
+ # coding: UTF-8
2
+
3
+ module Armada
4
+ module Validations
5
+ extend ActiveSupport::Concern
6
+
7
+ class UniquenessValidator < ActiveModel::EachValidator
8
+
9
+ def validate_each(record, attribute, value)
10
+ relation = record.class.where(attribute => value)
11
+
12
+ Array.wrap(options[:scope]).each do |scope_attribute|
13
+ relation = relation.where(scope_attribute => record.attributes[scope_attribute])
14
+ end
15
+
16
+ relation = relation.where(:id => {"!=" => record.id}) if record.persisted?
17
+
18
+ return if relation.count == 0
19
+ record.errors.add(attribute, :taken, :default => options[:message], :value => value)
20
+ end
21
+
22
+ end
23
+ end
24
+
25
+ module ClassMethods
26
+ # Configuration options:
27
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
28
+ # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint.
29
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
30
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
31
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
32
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
33
+ # method, proc or string should return or evaluate to a true or false value.
34
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
35
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
36
+ # method, proc or string should return or evaluate to a true or false value.
37
+ def validates_uniqueness_of(*attr_names)
38
+ validates_with UniquenessValidator, _merge_attributes(attr_names)
39
+ end
40
+ end
41
+ end
data/lib/armada.rb ADDED
@@ -0,0 +1,27 @@
1
+ # coding: UTF-8
2
+
3
+ require 'yajl'
4
+ require 'socket'
5
+
6
+ require 'active_model'
7
+ require 'active_support/core_ext/array/access'
8
+ require 'active_support/core_ext/array/uniq_by'
9
+ require 'active_support/core_ext/class/attribute'
10
+ require 'active_support/core_ext/module/aliasing'
11
+ require 'active_support/core_ext/module/delegation'
12
+ require 'active_support/core_ext/hash/indifferent_access'
13
+ require 'active_support/core_ext/module/attribute_accessors'
14
+
15
+ require 'armada/dirty'
16
+ require 'armada/errors'
17
+ require 'armada/relation'
18
+ require 'armada/callbacks'
19
+ require 'armada/observer'
20
+ require 'armada/timestamp'
21
+ require 'armada/connection'
22
+ require 'armada/validations'
23
+ require 'armada/finder_methods'
24
+ require 'armada/database_methods'
25
+ require 'armada/attribute_methods'
26
+
27
+ require 'armada/model'
@@ -0,0 +1,20 @@
1
+ # coding: UTF-8
2
+
3
+ require File.join(File.dirname(__FILE__),'spec_helper.rb')
4
+
5
+ describe Bank, :type => :model do
6
+ it "has a collection name" do
7
+ Bank.collection_name.should == "banks"
8
+ end
9
+
10
+ it "will find banks" do
11
+ Bank.find(@bac.id).should == @bac
12
+ Bank.find([@bac.id]).should == [@bac]
13
+ Bank.find(@wfc.id, @c.id).should == [@wfc, @c]
14
+ Bank.find(@wfc.id, @c.id, @bac.id).should == [@wfc, @c, @bac]
15
+ end
16
+
17
+ it "will raise an error when it does not find a bank" do
18
+ lambda { Bank.find("481516") }.should raise_error Armada::RecordNotFound
19
+ end
20
+ end
@@ -0,0 +1,95 @@
1
+ # coding: UTF-8
2
+
3
+ require File.join(File.dirname(__FILE__),'spec_helper.rb')
4
+
5
+ describe "A bank instance" do
6
+ before do
7
+ Bank.delete
8
+ @bank = Bank.new(:name => "Wells Fargo", :rank => 4)
9
+ end
10
+
11
+ after do
12
+ Bank.delete
13
+ end
14
+
15
+ it "has read accessors" do
16
+ @bank.name.should == "Wells Fargo"
17
+ @bank.attributes[:name].should == "Wells Fargo"
18
+ end
19
+
20
+ it "has write accessors" do
21
+ @bank.name = "Citigroup"
22
+ @bank.name.should == "Citigroup"
23
+ @bank.attributes[:name].should == "Citigroup"
24
+ end
25
+
26
+ it "is comparable to other banks" do
27
+ @bank.should == Bank.new(:name => "Wells Fargo", :rank => 4)
28
+ end
29
+
30
+ context "that is unsaved" do
31
+ it "is new" do
32
+ @bank.new_record?.should be_true
33
+ end
34
+
35
+ it "is not persisted" do
36
+ @bank.persisted?.should be_false
37
+ end
38
+
39
+ it "can not be destroyed" do
40
+ @bank.destroy.should be_false
41
+ end
42
+ end
43
+
44
+ context "that is valid" do
45
+ it "will save" do
46
+ @bank.save.should be_true
47
+ end
48
+
49
+ it "will save!" do
50
+ lambda { @bank.save! }.should be_true
51
+ end
52
+ end
53
+
54
+ context "that is invalid" do
55
+ before do
56
+ @bank.rank = 5
57
+ end
58
+
59
+ it "will not save" do
60
+ @bank.save.should be_false
61
+ end
62
+
63
+ it "will not save!" do
64
+ lambda { @bank.save! }.should raise_error Armada::RecordNotSaved
65
+ end
66
+ end
67
+
68
+ context "that is saved" do
69
+ before do
70
+ @bank.save
71
+ end
72
+
73
+ it "is persisted" do
74
+ @bank.persisted?.should be_true
75
+ end
76
+
77
+ it "can be destroyed" do
78
+ @bank.destroy.should be_true
79
+ @bank.destroyed?.should be_true
80
+ end
81
+
82
+ it "can be changed" do
83
+ @bank.name = "Citigroup"
84
+ @bank.rank = 2
85
+ @bank.save.should be_true
86
+ end
87
+
88
+ it "will retain previous value after a failed update" do
89
+ @bank.rank = 5
90
+ @bank.save.should be_false
91
+ @bank.rank.should == 4
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,112 @@
1
+ # coding: UTF-8
2
+
3
+ require File.join(File.dirname(__FILE__),'spec_helper.rb')
4
+
5
+ describe Armada::Relation, :type => :model do
6
+
7
+ it "will find all banks" do
8
+ Bank.all.should =~ [@bac, @c, @wfc]
9
+ end
10
+
11
+ it "will implement select correctly" do
12
+ Bank.select.should =~ [@bac, @c, @wfc].map { |x| x.attributes }
13
+ Bank.where(:rank => 4).select.should == [@wfc.attributes]
14
+ end
15
+
16
+ it "will implement delete correctly" do
17
+ Bank.where(:rank => 4).delete.should == 1
18
+ Bank.delete.should == 2
19
+ end
20
+
21
+ it "will implement count correctly" do
22
+ Bank.count.should == 3
23
+ Bank.where(:rank => 4).count.should == 1
24
+ end
25
+
26
+ it "will implement where correctly" do
27
+ Bank.where(:rank => 4).all.should == [@wfc]
28
+ Bank.where(:rank => 1..2).all.should =~ [@bac, @c]
29
+ end
30
+
31
+ it "will implement order correctly" do
32
+ Bank.order(:rank).all.should == [@bac, @c, @wfc]
33
+ Bank.order(:rank => :asc).all.should == [@bac, @c, @wfc]
34
+ Bank.order(:rank => :desc).all.should == [@wfc, @c, @bac]
35
+ end
36
+
37
+ it "will implement limit correctly" do
38
+ Bank.order(:rank => :desc).limit(1).all.should == [@wfc]
39
+ end
40
+
41
+ it "will implement offset correctly" do
42
+ Bank.order(:rank => :desc).offset(1).all.should == [@c, @bac]
43
+ end
44
+
45
+ it "will implement only correctly" do
46
+ Bank.order(:rank => :desc).only(:rank).all.should == [4,2,1]
47
+ Bank.order(:rank => :desc).only(:rank, :name).all.should == [[4,"Wells Fargo"],[2,"Citigroup"],[1,"Bank of America"]]
48
+ end
49
+
50
+ it "will implement to_query correctly for #where" do
51
+ Bank.where(:rank => 4).to_query.should == {where:["=", :rank, 4]}
52
+
53
+ Bank.where(:rank => 4, :name => "Wells Fargo").to_query.should == {where:["and",["=", :rank, 4],["=", :name, "Wells Fargo"]]}
54
+ Bank.where(:rank => 4).where(:name => "Wells Fargo").to_query.should == {where:["and",["=", :rank, 4],["=", :name, "Wells Fargo"]]}
55
+
56
+ Bank.where(:rank => 4).where(:name => "Wells Fargo").where(:id => 1).to_query.should == {where:["and",["=", :rank, 4],["=", :name, "Wells Fargo"],["=", :id, 1]]}
57
+ Bank.where(:rank => 4, :name => "Wells Fargo").where(:id => 1).to_query.should == {where:["and",["=", :rank, 4],["=", :name, "Wells Fargo"],["=", :id, 1]]}
58
+ Bank.where(:rank => 4).where(:name => "Wells Fargo", :id => 1).to_query.should == {where:["and",["=", :rank, 4],["=", :name, "Wells Fargo"],["=", :id, 1]]}
59
+
60
+ Bank.where(:rank => 4, :name => "Wells Fargo").where(:id => 1, :created_at => {">" => 1}).to_query.should == {where:["and",["=", :rank, 4],["=", :name, "Wells Fargo"],["=", :id, 1],[">", :created_at, 1]]}
61
+ end
62
+
63
+ it "will implement to_query correctly for #where with \"or\" boolean matching" do
64
+ Bank.where(:name => "Wells Fargo").or.where(:rank => 1).to_query.should == {where:["or",["=", :name, "Wells Fargo"],["=", :rank, 1]]}
65
+ Bank.where(:name => "Citigroup").or.where(:rank => 1).or.where(:rank => 4).to_query.should == {where:["or", ["=", :name, "Citigroup"], ["=", :rank, 1], ["=", :rank, 4]]}
66
+
67
+ Bank.where(:rank => 1).or.where(:rank => 4, :name => "Wells Fargo").to_query.should == {where:["or",["=", :rank, 1], ["and", ["=", :rank, 4], ["=", :name, "Wells Fargo"]]]}
68
+ Bank.where(:rank => 1).or.where(:rank => 4).where(:name => "Wells Fargo").to_query.should == {where:["or",["=", :rank, 1], ["and", ["=", :rank, 4], ["=", :name, "Wells Fargo"]]]}
69
+
70
+ Bank.where(:name => "Wells Fargo", :rank => 4).or.where(:rank => 1).to_query.should == {where:["or",["and", ["=", :name, "Wells Fargo"], ["=", :rank, 4]],["=", :rank, 1]]}
71
+
72
+ Bank.where(:name => "Wells Fargo", :rank => 4).or.where(:name => "Bank of America", :rank => 1).to_query.should == {where:["or",["and", ["=", :name, "Wells Fargo"], ["=", :rank, 4]],["and", ["=", :name, "Bank of America"], ["=", :rank, 1]]]}
73
+ Bank.where(:name => "Wells Fargo", :rank => 4).or.where(:name => "Bank of America").where(:rank => 1).to_query.should == {where:["or",["and", ["=", :name, "Wells Fargo"], ["=", :rank, 4]],["and", ["=", :name, "Bank of America"], ["=", :rank, 1]]]}
74
+
75
+ Bank.where(:name => "Wells Fargo", :rank => 4).or.where(:name => "Bank of America").where(:rank => {"=" => 1, "!=" => 4}).to_query.should == {where:["or",["and", ["=", :name, "Wells Fargo"], ["=", :rank, 4]],["and", ["=", :name, "Bank of America"], ["=", :rank, 1], ["!=", :rank, 4]]]}
76
+ Bank.where(:name => "Wells Fargo", :rank => 4).or.where(:name => "Bank of America", :rank => 1).where(:rank => {"!=" => 4}).to_query.should == {where:["or",["and", ["=", :name, "Wells Fargo"], ["=", :rank, 4]],["and", ["=", :name, "Bank of America"], ["=", :rank, 1], ["!=", :rank, 4]]]}
77
+ Bank.where(:name => "Wells Fargo").where(:rank => 4).or.where(:name => "Bank of America").where(:rank => {"=" => 1, "!=" => 4}).to_query.should == {where:["or",["and", ["=", :name, "Wells Fargo"], ["=", :rank, 4]],["and", ["=", :name, "Bank of America"], ["=", :rank, 1], ["!=", :rank, 4]]]}
78
+ end
79
+
80
+ it "will implement to_query correctly for #order" do
81
+ Bank.order(:rank).to_query.should == {order:[:rank, :asc]}
82
+
83
+ Bank.order(:rank, :name => :desc).to_query.should == {order:[[:rank, :asc], [:name, :desc]]}
84
+ Bank.order(:rank).order(:name => :desc).to_query.should == {order:[[:rank, :asc], [:name, :desc]]}
85
+
86
+ Bank.order(:rank, :id, :name => :desc).to_query.should == {order:[[:rank, :asc], [:id, :asc], [:name, :desc]]}
87
+ Bank.order(:rank, :id).order(:name => :desc).to_query.should == {order:[[:rank, :asc], [:id, :asc], [:name, :desc]]}
88
+ Bank.order(:rank).order(:id, :name => :desc).to_query.should == {order:[[:rank, :asc], [:id, :asc], [:name, :desc]]}
89
+
90
+ Bank.order(:rank, :id).order(:name, :created_at).to_query.should == {order:[[:rank, :asc], [:id, :asc], [:name, :asc], [:created_at, :asc]]}
91
+ end
92
+
93
+ it "will implement to_query correctly for #limit" do
94
+ Bank.limit(1).to_query.should == {limit: 1}
95
+ end
96
+
97
+ it "will implement to_query correctly for #offset" do
98
+ Bank.offset(1).to_query.should == {offset: 1}
99
+ end
100
+
101
+ it "will implement to_query correctly for #only" do
102
+ Bank.only(:rank).to_query.should == {only: :rank}
103
+
104
+ Bank.only(:rank, :name).to_query.should == {only: [:rank, :name]}
105
+ Bank.only(:rank).only(:name).to_query.should == {only: [:rank, :name]}
106
+
107
+ Bank.only(:id).only(:rank, :name).to_query.should == {only: [:id, :rank, :name]}
108
+ Bank.only(:id, :rank).only(:name).to_query.should == {only: [:id, :rank, :name]}
109
+
110
+ Bank.only(:id, :rank).only(:name, :created_at).to_query.should == {only: [:id, :rank, :name, :created_at]}
111
+ end
112
+ end
@@ -0,0 +1,15 @@
1
+ # coding: UTF-8
2
+
3
+ require File.join(File.dirname(__FILE__),'spec_helper.rb')
4
+
5
+ describe Armada::Validations, :type => :model do
6
+ it "should validate uniqueness of rank" do
7
+ @wfc.rank = 2
8
+ @wfc.save.should be_false
9
+ end
10
+
11
+ it "should not allow new records to intervene" do
12
+ @jpm = Bank.new(:name => "JPMorgan Chase", :rank => 2, :price => 42.59, :public => true)
13
+ @jpm.save.should be_false
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ # coding: UTF-8
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
4
+
5
+ require "pp"
6
+ require "spec"
7
+ require "armada"
8
+
9
+ Time.zone = "UTC"
10
+
11
+ Armada.setup!
12
+
13
+ class Bank < Armada::Model
14
+ add_columns :name, :created_at, :updated_at, :rank, :price, :public
15
+ validates :rank, :inclusion => {:in => 1..4}, :uniqueness => true
16
+ end
17
+
18
+ Spec::Runner.configure do |config|
19
+ config.before(:each, :type => :model) do
20
+ Bank.delete
21
+
22
+ @bac = Bank.new(:name => "Bank of America", :rank => 1, :price => 16.24, :public => true)
23
+ @c = Bank.new(:name => "Citigroup", :rank => 2, :price => 3.56, :public => true)
24
+ @wfc = Bank.new(:name => "Wells Fargo", :rank => 4, :price => 28.89, :public => true)
25
+
26
+ [@bac, @c, @wfc].each { |x| x.save }
27
+ end
28
+ config.after(:each, :type => :model) do
29
+ Bank.delete
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: armada
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Sam Aarons
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-25 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: yajl-ruby
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 7
30
+ - 4
31
+ version: 0.7.4
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: activemodel
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 3
43
+ - 0
44
+ - 0
45
+ - beta3
46
+ version: 3.0.0.beta3
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 1
58
+ - 3
59
+ - 0
60
+ version: 1.3.0
61
+ type: :development
62
+ version_requirements: *id003
63
+ description: Armada makes it simple and easy to combine ActiveModel and FleetDB together
64
+ email:
65
+ - samaarons@gmail.com
66
+ executables: []
67
+
68
+ extensions: []
69
+
70
+ extra_rdoc_files: []
71
+
72
+ files:
73
+ - lib/armada/attribute_methods.rb
74
+ - lib/armada/callbacks.rb
75
+ - lib/armada/connection.rb
76
+ - lib/armada/database_methods.rb
77
+ - lib/armada/dirty.rb
78
+ - lib/armada/errors.rb
79
+ - lib/armada/finder_methods.rb
80
+ - lib/armada/model.rb
81
+ - lib/armada/observer.rb
82
+ - lib/armada/relation.rb
83
+ - lib/armada/timestamp.rb
84
+ - lib/armada/validations.rb
85
+ - lib/armada.rb
86
+ - test/armada_finder_methods_spec.rb
87
+ - test/armada_model_spec.rb
88
+ - test/armada_relation_spec.rb
89
+ - test/armada_validations_spec.rb
90
+ - test/spec_helper.rb
91
+ - LICENSE
92
+ - Rakefile
93
+ - README.md
94
+ has_rdoc: true
95
+ homepage: http://github.com/saarons/armada
96
+ licenses: []
97
+
98
+ post_install_message:
99
+ rdoc_options: []
100
+
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ segments:
108
+ - 1
109
+ - 9
110
+ - 1
111
+ version: 1.9.1
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ segments:
117
+ - 1
118
+ - 3
119
+ - 6
120
+ version: 1.3.6
121
+ requirements: []
122
+
123
+ rubyforge_project: armada
124
+ rubygems_version: 1.3.6
125
+ signing_key:
126
+ specification_version: 3
127
+ summary: An ActiveModel interface to FleetDB
128
+ test_files: []
129
+