armada 0.1.0

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/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
+