ricordami 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.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
|