ricordami 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +13 -0
- data/ISSUES.md +0 -0
- data/MIT-LICENSE +21 -0
- data/README.md +454 -0
- data/TODO.md +21 -0
- data/examples/calls.rb +64 -0
- data/examples/singers.rb +42 -0
- data/lib/ricordami/attribute.rb +52 -0
- data/lib/ricordami/can_be_queried.rb +133 -0
- data/lib/ricordami/can_be_validated.rb +27 -0
- data/lib/ricordami/can_have_relationships.rb +152 -0
- data/lib/ricordami/configuration.rb +39 -0
- data/lib/ricordami/connection.rb +22 -0
- data/lib/ricordami/exceptions.rb +14 -0
- data/lib/ricordami/has_attributes.rb +159 -0
- data/lib/ricordami/has_indices.rb +105 -0
- data/lib/ricordami/is_lockable.rb +52 -0
- data/lib/ricordami/is_persisted.rb +123 -0
- data/lib/ricordami/is_retrievable.rb +35 -0
- data/lib/ricordami/key_namer.rb +63 -0
- data/lib/ricordami/model.rb +29 -0
- data/lib/ricordami/query.rb +68 -0
- data/lib/ricordami/relationship.rb +40 -0
- data/lib/ricordami/unique_index.rb +59 -0
- data/lib/ricordami/unique_validator.rb +21 -0
- data/lib/ricordami/value_index.rb +26 -0
- data/lib/ricordami/version.rb +3 -0
- data/lib/ricordami.rb +26 -0
- data/spec/acceptance/manage_relationships_spec.rb +42 -0
- data/spec/acceptance/model_with_validation_spec.rb +78 -0
- data/spec/acceptance/query_model_spec.rb +93 -0
- data/spec/acceptance_helper.rb +2 -0
- data/spec/ricordami/attribute_spec.rb +113 -0
- data/spec/ricordami/can_be_queried_spec.rb +254 -0
- data/spec/ricordami/can_be_validated_spec.rb +115 -0
- data/spec/ricordami/can_have_relationships_spec.rb +255 -0
- data/spec/ricordami/configuration_spec.rb +45 -0
- data/spec/ricordami/connection_spec.rb +25 -0
- data/spec/ricordami/exceptions_spec.rb +43 -0
- data/spec/ricordami/has_attributes_spec.rb +266 -0
- data/spec/ricordami/has_indices_spec.rb +73 -0
- data/spec/ricordami/is_lockable_spec.rb +45 -0
- data/spec/ricordami/is_persisted_spec.rb +186 -0
- data/spec/ricordami/is_retrievable_spec.rb +55 -0
- data/spec/ricordami/key_namer_spec.rb +56 -0
- data/spec/ricordami/model_spec.rb +65 -0
- data/spec/ricordami/query_spec.rb +156 -0
- data/spec/ricordami/relationship_spec.rb +123 -0
- data/spec/ricordami/unique_index_spec.rb +87 -0
- data/spec/ricordami/unique_validator_spec.rb +41 -0
- data/spec/ricordami/value_index_spec.rb +40 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/constants.rb +43 -0
- data/spec/support/db_manager.rb +18 -0
- data/test/bin/data_loader.rb +107 -0
- data/test/data/domains.txt +462 -0
- data/test/data/first_names.txt +1220 -0
- data/test/data/last_names.txt +1028 -0
- data/test/data/people_100_000.csv.bz2 +0 -0
- data/test/data/people_10_000.csv.bz2 +0 -0
- data/test/data/people_1_000_000.csv.bz2 +0 -0
- metadata +258 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
require "ricordami/has_attributes"
|
2
|
+
|
3
|
+
module Ricordami
|
4
|
+
module IsPersisted
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
attr_reader :save_queue, :delete_queue
|
9
|
+
|
10
|
+
def create(*args)
|
11
|
+
new(*args).tap do |instance|
|
12
|
+
instance.save
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_attributes_for(id)
|
17
|
+
key_name = attributes_key_name_for(id)
|
18
|
+
redis.hgetall(key_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
[[:saving, :save_queue], [:deleting, :delete_queue]].each do |action, queue|
|
22
|
+
var_name = :"@#{queue}"
|
23
|
+
define_method(:"queue_#{action}_operations") do |&block|
|
24
|
+
raise ArgumentError.new("missing block") unless block
|
25
|
+
raise ArgumentError.new("expecting block with 2 arguments") unless block.arity == 2
|
26
|
+
instance_variable_set(var_name, []) unless instance_variable_get(var_name)
|
27
|
+
instance_variable_get(var_name) << block
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def redis
|
32
|
+
@redis ||= Ricordami.driver
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module InstanceMethods
|
37
|
+
def initialize(*args)
|
38
|
+
super(*args)
|
39
|
+
@persisted = false unless instance_variable_defined?(:@persisted)
|
40
|
+
@deleted = false
|
41
|
+
end
|
42
|
+
|
43
|
+
def persisted?
|
44
|
+
@persisted
|
45
|
+
end
|
46
|
+
|
47
|
+
def new_record?
|
48
|
+
!@persisted
|
49
|
+
end
|
50
|
+
|
51
|
+
def deleted?
|
52
|
+
@deleted
|
53
|
+
end
|
54
|
+
|
55
|
+
def save(opts = {})
|
56
|
+
raise ModelHasBeenDeleted.new("can't save a deleted model") if deleted?
|
57
|
+
set_initial_attribute_values if new_record?
|
58
|
+
redis.tap do |driver|
|
59
|
+
session = {}
|
60
|
+
driver.multi
|
61
|
+
driver.hmset(attributes_key_name, *attributes.to_a.flatten)
|
62
|
+
self.class.save_queue.each { |block| block.call(self, session) } if self.class.save_queue
|
63
|
+
driver.exec
|
64
|
+
end
|
65
|
+
@persisted = true
|
66
|
+
attributes_synced_with_db!
|
67
|
+
rescue Exception => ex
|
68
|
+
raise ex if ex.is_a?(ModelHasBeenDeleted)
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
def reload
|
73
|
+
attrs = self.class.send(:load_attributes_for, id)
|
74
|
+
update_mem_attributes(attrs) unless attrs.empty?
|
75
|
+
attributes_synced_with_db!
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
def update_attributes(attrs)
|
80
|
+
raise ModelHasBeenDeleted.new("can't update the attributes of a deleted model") if deleted?
|
81
|
+
update_mem_attributes!(attrs) unless attrs.empty?
|
82
|
+
save
|
83
|
+
end
|
84
|
+
|
85
|
+
def delete
|
86
|
+
raise ModelHasBeenDeleted.new("can't delete a model already deleted") if deleted?
|
87
|
+
db_commands, models = [], []
|
88
|
+
# TODO: use watch (Redis 2.2) and re-run prepare + execute if change
|
89
|
+
session = Struct.new(:models, :commands).new(models, db_commands)
|
90
|
+
prepare_delete(session)
|
91
|
+
execute_delete(db_commands)
|
92
|
+
models.each { |model| model.mark_as_deleted }
|
93
|
+
end
|
94
|
+
|
95
|
+
def prepare_delete(session)
|
96
|
+
session.models << self
|
97
|
+
session.commands << [:del, [attributes_key_name]]
|
98
|
+
self.class.delete_queue.reverse.each { |block| block.call(self, session) } if self.class.delete_queue
|
99
|
+
end
|
100
|
+
|
101
|
+
def execute_delete(db_commands)
|
102
|
+
redis.tap do |driver|
|
103
|
+
driver.multi
|
104
|
+
db_commands.each do |message, args|
|
105
|
+
driver.send(message, *args)
|
106
|
+
end
|
107
|
+
driver.exec
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def mark_as_deleted
|
112
|
+
attributes_synced_with_db!
|
113
|
+
@deleted = true
|
114
|
+
freeze
|
115
|
+
true
|
116
|
+
end
|
117
|
+
|
118
|
+
def redis
|
119
|
+
self.class.redis
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "ricordami/is_persisted"
|
2
|
+
require "ricordami/has_indices"
|
3
|
+
|
4
|
+
module Ricordami
|
5
|
+
module IsRetrievable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
index :unique => :id
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def get(id)
|
14
|
+
attributes = load_attributes_for(id)
|
15
|
+
raise NotFound.new("id = #{id}") if attributes.empty?
|
16
|
+
new(attributes).tap do |instance|
|
17
|
+
instance.instance_eval do
|
18
|
+
@persisted = true
|
19
|
+
attributes_synced_with_db!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
alias :[] :get
|
24
|
+
|
25
|
+
def all(expressions = nil)
|
26
|
+
ids = indices[:id].all
|
27
|
+
ids.map { |id| get(id) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def count
|
31
|
+
indices[:id].count
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "base64"
|
2
|
+
|
3
|
+
module Ricordami
|
4
|
+
module KeyNamer
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def sequence(model, opts = {})
|
8
|
+
"#{model}:seq:#{opts[:type]}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def attributes(model, opts = {})
|
12
|
+
"#{model}:att:#{opts[:id]}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def unique_index(model, opts = {})
|
16
|
+
"#{model}:udx:#{opts[:name]}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def hash_ref(model, opts = {})
|
20
|
+
fields = opts[:fields].join("_") + "_to_id"
|
21
|
+
"#{model}:hsh:#{fields}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def index(model, opts = {})
|
25
|
+
value = encode(opts[:value])
|
26
|
+
"#{model}:idx:#{opts[:field]}:#{value}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def volatile_set(model, opts = {})
|
30
|
+
info = opts[:info].dup
|
31
|
+
op = info.shift
|
32
|
+
if info.empty?
|
33
|
+
info = [op]
|
34
|
+
else
|
35
|
+
info = ["#{op}(#{info.join(",")})"]
|
36
|
+
end
|
37
|
+
unless opts[:key].nil?
|
38
|
+
key = opts[:key].sub("~:#{model}:set:", "")
|
39
|
+
info.unshift(key)
|
40
|
+
end
|
41
|
+
"~:#{model}:set:#{info.join(":")}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def temporary(model)
|
45
|
+
lock_id = model.redis.incr("#{model}:seq:lock")
|
46
|
+
"#{model}:val:_tmp:#{lock_id}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def lock(model, opts = {})
|
50
|
+
"#{model}:val:#{opts[:id]}:_lock"
|
51
|
+
end
|
52
|
+
|
53
|
+
def sort(model, opts = {})
|
54
|
+
"#{model}:att:*->#{opts[:sort_by]}"
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def encode(value)
|
60
|
+
Base64.encode64(value.to_s).gsub("\n", "")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ricordami
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
extend ActiveModel::Naming
|
7
|
+
include ActiveModel::Conversion
|
8
|
+
include ActiveModel::AttributeMethods
|
9
|
+
include IsLockable
|
10
|
+
include HasAttributes
|
11
|
+
include HasIndices
|
12
|
+
include IsPersisted
|
13
|
+
include IsRetrievable
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def model_can(*features)
|
18
|
+
features.each do |feature|
|
19
|
+
require File.expand_path("../can_#{feature}", __FILE__)
|
20
|
+
feature_module = Ricordami.const_get(:"Can#{feature.to_s.camelize}")
|
21
|
+
include feature_module
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
include Configuration
|
28
|
+
include Connection
|
29
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Ricordami
|
2
|
+
class Query
|
3
|
+
attr_reader :expressions, :runner, :builder, :sort_by, :sort_dir
|
4
|
+
|
5
|
+
def initialize(runner, builder = nil)
|
6
|
+
@expressions = []
|
7
|
+
@runner = runner
|
8
|
+
@builder = builder || runner
|
9
|
+
end
|
10
|
+
|
11
|
+
[:and, :not, :any].each do |op|
|
12
|
+
define_method(op) do |*args|
|
13
|
+
options = args.first || {}
|
14
|
+
@expressions << [op, options.dup]
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
alias :where :and
|
19
|
+
|
20
|
+
[:all, :paginate, :first, :last, :rand].each do |cmd|
|
21
|
+
define_method(cmd) do |*args|
|
22
|
+
return runner unless runner.respond_to?(cmd)
|
23
|
+
options = args.first || {}
|
24
|
+
options[:expressions] = expressions
|
25
|
+
options[:sort_by] = @sort_by unless @sort_by.nil?
|
26
|
+
options[:order] = order_for(@sort_dir) unless @sort_dir.nil?
|
27
|
+
runner.send(cmd, options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def sort(attribute, dir = :asc_alpha)
|
32
|
+
unless [:asc_alpha, :asc_num, :desc_alpha, :desc_num].include?(dir)
|
33
|
+
raise ArgumentError.new("sorting direction #{dir.inspect} is invalid")
|
34
|
+
end
|
35
|
+
@sort_by, @sort_dir = attribute, dir
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def build(attributes = {})
|
40
|
+
initial_values = {}
|
41
|
+
expressions.each do |operation, filters|
|
42
|
+
next unless operation == :and
|
43
|
+
initial_values.merge!(filters)
|
44
|
+
end
|
45
|
+
obj = builder.new(initial_values.merge(attributes))
|
46
|
+
end
|
47
|
+
|
48
|
+
def create(attributes = {})
|
49
|
+
build(attributes).tap { |obj| obj.save }
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def order_for(dir)
|
55
|
+
case dir
|
56
|
+
when nil then nil
|
57
|
+
when :asc_alpha then "ALPHA ASC"
|
58
|
+
when :asc_num then "ASC"
|
59
|
+
when :desc_alpha then "ALPHA DESC"
|
60
|
+
else "DESC"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def method_missing(meth, *args, &blk)
|
65
|
+
all.send(meth, *args, &blk)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Ricordami
|
2
|
+
class Relationship
|
3
|
+
SUPPORTED_TYPES = [:references_many, :references_one, :referenced_in]
|
4
|
+
MANDATORY_ARGS = [:other, :self]
|
5
|
+
|
6
|
+
attr_reader :type, :name, :object_kind, :dependent, :self_kind, :alias
|
7
|
+
|
8
|
+
def initialize(type, options = {})
|
9
|
+
options.assert_valid_keys(:other, :as, :self, :alias, :dependent)
|
10
|
+
raise TypeNotSupported.new(type.to_s) unless SUPPORTED_TYPES.include?(type)
|
11
|
+
missing = find_missing_args(options)
|
12
|
+
raise MissingMandatoryArgs.new(missing.map(&:to_s).join(", ")) unless missing.empty?
|
13
|
+
if options[:dependent] && ![:delete, :nullify].include?(options[:dependent])
|
14
|
+
raise OptionValueInvalid.new(options[:dependent].to_s)
|
15
|
+
end
|
16
|
+
@name = options[:as] || options[:other]
|
17
|
+
@type = type
|
18
|
+
@object_kind = options[:other].to_s.singularize.to_sym
|
19
|
+
@self_kind = options[:self].to_s.singularize.to_sym
|
20
|
+
@alias = options[:alias] || options[:self]
|
21
|
+
@dependent = options[:dependent]
|
22
|
+
end
|
23
|
+
|
24
|
+
def object_class
|
25
|
+
@object_kind.to_s.camelize.constantize
|
26
|
+
end
|
27
|
+
|
28
|
+
def referrer_id
|
29
|
+
return @referrer_id unless @referrer_id.nil?
|
30
|
+
referrer = type == :referenced_in ? @name.to_s : @alias.to_s.singularize
|
31
|
+
@referrer_id = referrer.foreign_key
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def find_missing_args(options)
|
37
|
+
MANDATORY_ARGS - options.keys
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "ricordami/key_namer"
|
2
|
+
|
3
|
+
module Ricordami
|
4
|
+
class UniqueIndex
|
5
|
+
SEPARATOR = "_-::-_"
|
6
|
+
|
7
|
+
attr_reader :model, :fields, :name, :need_get_by
|
8
|
+
|
9
|
+
def initialize(model, fields, options = {})
|
10
|
+
@model = model
|
11
|
+
@fields = [fields].flatten.map(&:to_sym)
|
12
|
+
@need_get_by = options[:get_by] && @fields != [:id]
|
13
|
+
@name = @fields.join("_").to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
def uidx_key_name
|
17
|
+
@uidx_key_name ||= KeyNamer.unique_index(@model, :name => @name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def ref_key_name
|
21
|
+
@ref_key_name ||= KeyNamer.hash_ref(@model, :fields => @fields)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add(id, value)
|
25
|
+
value = value.join(SEPARATOR) if value.is_a?(Array)
|
26
|
+
@model.redis.sadd(uidx_key_name, value)
|
27
|
+
@model.redis.hset(ref_key_name, value, id) if @need_get_by
|
28
|
+
end
|
29
|
+
|
30
|
+
def rem(id, value, return_command = false)
|
31
|
+
if return_command
|
32
|
+
commands = []
|
33
|
+
commands << [:hdel, [ref_key_name, id]] if @need_get_by
|
34
|
+
commands << [:srem, [uidx_key_name, value]]
|
35
|
+
return commands
|
36
|
+
end
|
37
|
+
@model.redis.hdel(ref_key_name, id) if @need_get_by
|
38
|
+
value = value.join(SEPARATOR) if value.is_a?(Array)
|
39
|
+
@model.redis.srem(uidx_key_name, value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def id_for_values(*values)
|
43
|
+
values = values.flatten.join(SEPARATOR)
|
44
|
+
@model.redis.hget(ref_key_name, values)
|
45
|
+
end
|
46
|
+
|
47
|
+
def all
|
48
|
+
@model.redis.smembers(uidx_key_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def count
|
52
|
+
@model.redis.scard(uidx_key_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def include?(value)
|
56
|
+
@model.redis.sismember(uidx_key_name, value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "active_model/validator"
|
2
|
+
|
3
|
+
module Ricordami
|
4
|
+
class UniqueValidator < ActiveModel::EachValidator
|
5
|
+
def validate_each(record, attribute, value)
|
6
|
+
return true unless record.new_record? || record.send(:attribute_changed?, attribute)
|
7
|
+
index_name = attribute.to_sym
|
8
|
+
index = record.class.indices[index_name]
|
9
|
+
if index.include?(value)
|
10
|
+
attr_def = record.class.attributes[attribute]
|
11
|
+
unless record.persisted? && attr_def.read_only?
|
12
|
+
record.errors.add(attribute, options[:message] || "is already used")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup(klass)
|
18
|
+
attributes.each { |attribute| klass.index :unique => attribute }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "ricordami/key_namer"
|
2
|
+
|
3
|
+
module Ricordami
|
4
|
+
class ValueIndex
|
5
|
+
attr_reader :model, :field, :name
|
6
|
+
|
7
|
+
def initialize(model, field)
|
8
|
+
@model = model
|
9
|
+
@field = field.to_sym
|
10
|
+
@name = @field
|
11
|
+
end
|
12
|
+
|
13
|
+
def key_name_for_value(value)
|
14
|
+
KeyNamer.index(@model, :field => @field, :value => value)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(id, value)
|
18
|
+
@model.redis.sadd(key_name_for_value(value), id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def rem(id, value, return_command = false)
|
22
|
+
return [[:srem, [key_name_for_value(value), id]]] if return_command
|
23
|
+
@model.redis.srem(key_name_for_value(value), id)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/ricordami.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "digest/sha1"
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "active_support/core_ext/hash/slice"
|
5
|
+
require "active_support/core_ext/hash/keys"
|
6
|
+
require "active_support/core_ext/hash/indifferent_access"
|
7
|
+
require "active_support/core_ext/object/blank"
|
8
|
+
require "active_model"
|
9
|
+
require "active_model/naming"
|
10
|
+
require "active_model/conversion"
|
11
|
+
require "active_model/attribute_methods"
|
12
|
+
require "redis"
|
13
|
+
|
14
|
+
module Ricordami
|
15
|
+
extend self
|
16
|
+
end
|
17
|
+
|
18
|
+
require "ricordami/exceptions"
|
19
|
+
require "ricordami/configuration"
|
20
|
+
require "ricordami/connection"
|
21
|
+
require "ricordami/has_attributes"
|
22
|
+
require "ricordami/has_indices"
|
23
|
+
require "ricordami/is_lockable"
|
24
|
+
require "ricordami/is_persisted"
|
25
|
+
require "ricordami/is_retrievable"
|
26
|
+
require "ricordami/model"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require "acceptance_helper"
|
3
|
+
require "ricordami/can_have_relationships"
|
4
|
+
|
5
|
+
class Performer
|
6
|
+
include Ricordami::Model
|
7
|
+
include Ricordami::CanHaveRelationships
|
8
|
+
|
9
|
+
attribute :first_name
|
10
|
+
attribute :last_name
|
11
|
+
|
12
|
+
references_many :albums, :dependent => :delete
|
13
|
+
end
|
14
|
+
|
15
|
+
class Album
|
16
|
+
include Ricordami::Model
|
17
|
+
include Ricordami::CanHaveRelationships
|
18
|
+
|
19
|
+
attribute :title
|
20
|
+
attribute :year, :indexed => :value
|
21
|
+
|
22
|
+
referenced_in :performer
|
23
|
+
end
|
24
|
+
|
25
|
+
feature "Manage relationships" do
|
26
|
+
scenario "has many / belongs to" do
|
27
|
+
serge = Performer.create(:first_name => "Serge", :last_name => "Gainsbourg")
|
28
|
+
serge.albums.create(:title => "Melody Nelson", :year => "1971").should be_true
|
29
|
+
|
30
|
+
marseillaise = serge.albums.build(:title => "Aux Armes et cætera", :year => "1979")
|
31
|
+
marseillaise.save.should be_true
|
32
|
+
|
33
|
+
performer = Performer.first
|
34
|
+
performer.albums.map(&:title).should =~ ["Melody Nelson", "Aux Armes et cætera"]
|
35
|
+
performer.albums.where(:year => "1979").map(&:title).should =~ ["Aux Armes et cætera"]
|
36
|
+
performer.albums.sort(:year, :desc_num).map(&:title).should == ["Aux Armes et cætera", "Melody Nelson"]
|
37
|
+
|
38
|
+
Album.all.map(&:title).should =~ ["Melody Nelson", "Aux Armes et cætera"]
|
39
|
+
performer.delete
|
40
|
+
Album.all.should be_empty
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "acceptance_helper"
|
2
|
+
require "ricordami/can_be_validated"
|
3
|
+
|
4
|
+
class Singer
|
5
|
+
include Ricordami::Model
|
6
|
+
include Ricordami::CanBeValidated
|
7
|
+
|
8
|
+
attribute :username
|
9
|
+
attribute :email
|
10
|
+
attribute :first_name
|
11
|
+
attribute :last_name
|
12
|
+
attribute :deceased, :default => "false", :indexed => :value
|
13
|
+
|
14
|
+
index :unique => :username, :get_by => true
|
15
|
+
|
16
|
+
validates_presence_of :username, :email, :deceased
|
17
|
+
validates_uniqueness_of :username
|
18
|
+
validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
|
19
|
+
:allow_blank => true, :message => "is not a valid email"
|
20
|
+
validates_inclusion_of :deceased, :in => ["true", "false"]
|
21
|
+
end
|
22
|
+
|
23
|
+
feature "Basic model features with validation" do
|
24
|
+
scenario "validate, update, save and delete a model" do
|
25
|
+
serge = Singer.new
|
26
|
+
serge.should_not be_valid
|
27
|
+
serge.should have(2).errors
|
28
|
+
serge.errors.full_messages.should =~ ["Email can't be blank", "Username can't be blank"]
|
29
|
+
|
30
|
+
serge.email = "what's up?"
|
31
|
+
serge.save.should be_false
|
32
|
+
serge.errors[:email].should == ["is not a valid email"]
|
33
|
+
|
34
|
+
serge.update_attributes(:username => "lucien", :email => "serge@gainsbourg.com",
|
35
|
+
:first_name => "Serge", :last_name => "Gainsbourg")
|
36
|
+
serge.should be_valid
|
37
|
+
serge.should be_persisted
|
38
|
+
serge.should_not be_a_new_record
|
39
|
+
serge.reload.should be_persisted
|
40
|
+
|
41
|
+
lucien = Singer.new(:username => "lucien", :email => "lucien@blahblah.com")
|
42
|
+
lucien.should_not be_valid
|
43
|
+
lucien.errors.full_messages.should == ["Username is already used"]
|
44
|
+
lucien.username = "lucien2"
|
45
|
+
lucien.save.should be_true
|
46
|
+
lucien.should be_persisted
|
47
|
+
|
48
|
+
Singer.count.should == 2
|
49
|
+
deleteme = Singer.get_by_username("lucien2")
|
50
|
+
deleteme.delete
|
51
|
+
deleteme.should be_deleted
|
52
|
+
Singer.count.should == 1
|
53
|
+
Singer.get_by_username("lucien").should be_persisted
|
54
|
+
end
|
55
|
+
|
56
|
+
scenario "dirty state of models" do
|
57
|
+
Singer.create(:username => "bashung", :email => "alain@bashung.com",
|
58
|
+
:first_name => "Alain", :last_name => "Bashung")
|
59
|
+
alain = Singer.get_by_username("bashung")
|
60
|
+
alain.changed?.should be_false
|
61
|
+
|
62
|
+
alain.first_name = "Bob"
|
63
|
+
alain.changed?.should be_true
|
64
|
+
alain.first_name_changed?.should be_true
|
65
|
+
alain.first_name_was.should == "Alain"
|
66
|
+
alain.first_name_change.should == ["Alain", "Bob"]
|
67
|
+
alain.changed.should == ["first_name"]
|
68
|
+
alain.changes.should == {"first_name" => ["Alain", "Bob"]}
|
69
|
+
|
70
|
+
alain.save
|
71
|
+
alain.changed?.should be_false
|
72
|
+
alain.first_name_changed?.should be_false
|
73
|
+
alain.first_name = "Bob"
|
74
|
+
alain.changed?.should be_false
|
75
|
+
alain.first_name_changed?.should be_false
|
76
|
+
alain.previous_changes.should == {"first_name" => ["Alain", "Bob"]}
|
77
|
+
end
|
78
|
+
end
|