remodel-h-r19 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
data/example/book.rb ADDED
@@ -0,0 +1,13 @@
1
+ require File.dirname(__FILE__) + "/../lib/remodel-h"
2
+
3
+ class Book < Remodel::Entity
4
+ has_many :chapters, :class => 'Chapter', :reverse => :book
5
+ property :title, :class => 'String'
6
+ property :year, :class => 'Integer'
7
+ end
8
+
9
+ class Chapter < Remodel::Entity
10
+ has_one :book, :class => Book, :reverse => :chapters
11
+ property :title, :class => String
12
+ end
13
+
data/lib/remodel-h.rb ADDED
@@ -0,0 +1,69 @@
1
+ require 'rubygems'
2
+ require 'redis'
3
+ require 'date'
4
+
5
+ # If available, use the superfast YAJL lib to parse JSON.
6
+ begin
7
+ require 'yajl/json_gem'
8
+ rescue LoadError
9
+ require 'json'
10
+ end
11
+
12
+ # Define `Boolean` -- the missing superclass of `true` and `false`.
13
+ module Boolean; end
14
+ true.extend(Boolean)
15
+ false.extend(Boolean)
16
+
17
+ # Find the `Class` object for a given class name, which can be
18
+ # a `String` or a `Symbol` (or a `Class`).
19
+ def Class.[](clazz)
20
+ return clazz if clazz.nil? or clazz.is_a?(Class)
21
+ clazz.to_s.split('::').inject(Kernel) { |mod, name| mod.const_get(name) }
22
+ end
23
+
24
+ require File.join(File.dirname(__FILE__), 'remodel', 'mapper')
25
+ require File.join(File.dirname(__FILE__), 'remodel', 'has_many')
26
+ require File.join(File.dirname(__FILE__), 'remodel', 'entity')
27
+
28
+
29
+ module Remodel
30
+
31
+ # Custom errors
32
+ class Error < ::StandardError; end
33
+ class EntityNotFound < Error; end
34
+ class EntityNotSaved < Error; end
35
+ class InvalidKeyPrefix < Error; end
36
+ class InvalidType < Error; end
37
+ class MissingContext < Error; end
38
+
39
+ # By default, the redis server is expected to listen at `localhost:6379`.
40
+ # Otherwise you will have to set `Remodel.redis` to a suitably initialized
41
+ # redis client.
42
+ def self.redis
43
+ @redis ||= Redis.new
44
+ end
45
+
46
+ def self.redis=(redis)
47
+ @redis = redis
48
+ end
49
+
50
+ # Returns the mapper defined for a given class, or the identity mapper.
51
+ def self.mapper_for(clazz)
52
+ mapper_by_class[Class[clazz]]
53
+ end
54
+
55
+ # Define some mappers for common types.
56
+ def self.mapper_by_class
57
+ @mapper_by_class ||= Hash.new(Mapper.new).merge(
58
+ Boolean => Mapper.new(Boolean),
59
+ String => Mapper.new(String),
60
+ Integer => Mapper.new(Integer),
61
+ Float => Mapper.new(Float),
62
+ Array => Mapper.new(Array),
63
+ Hash => Mapper.new(Hash),
64
+ Date => Mapper.new(Date, :to_s, :parse),
65
+ Time => Mapper.new(Time, :to_i, :at)
66
+ )
67
+ end
68
+
69
+ end
@@ -0,0 +1,202 @@
1
+ module Remodel
2
+
3
+ # The superclass of all persistent remodel entities.
4
+ class Entity
5
+ attr_accessor :context, :key
6
+
7
+ def initialize(context, attributes = {}, key = nil)
8
+ @context = context
9
+ @attributes = {}
10
+ @key = key
11
+ attributes = self.class.default_values.merge(attributes) if key.nil?
12
+ attributes.each do |name, value|
13
+ send("#{name}=", value) if respond_to? "#{name}="
14
+ end
15
+ end
16
+
17
+ def id
18
+ key && key.match(/\d+/)[0].to_i
19
+ end
20
+
21
+ def save
22
+ @key = next_key unless @key
23
+ Remodel.redis.hset(@context, @key, to_json)
24
+ self
25
+ end
26
+
27
+ def update(properties)
28
+ properties.each { |name, value| send("#{name}=", value) }
29
+ save
30
+ end
31
+
32
+ def reload
33
+ raise EntityNotSaved unless @key
34
+ initialize(@context, self.class.parse(self.class.fetch(@context, @key)), @key)
35
+ instance_variables.each do |var|
36
+ remove_instance_variable(var) if var =~ /^@association_/
37
+ end
38
+ self
39
+ end
40
+
41
+ def delete
42
+ raise EntityNotSaved unless @key
43
+ Remodel.redis.hdel(@context, @key)
44
+ instance_variables.each do |var|
45
+ Remodel.redis.hdel(@context, var.to_s.sub('@association', @key)) if var =~ /^@association_/
46
+ end
47
+ end
48
+
49
+ def as_json
50
+ { :id => id }.merge(@attributes)
51
+ end
52
+
53
+ def to_json
54
+ JSON.generate(self.class.pack(@attributes))
55
+ end
56
+
57
+ def inspect
58
+ properties = @attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')
59
+ "\#<#{self.class.name}(#{context}, #{id}) #{properties}>"
60
+ end
61
+
62
+ def self.create(context, attributes = {})
63
+ new(context, attributes).save
64
+ end
65
+
66
+ def self.find(context, key)
67
+ key = "#{key_prefix}#{key}" if key.kind_of? Integer
68
+ restore(context, key, fetch(context, key))
69
+ end
70
+
71
+ def self.restore(context, key, json)
72
+ new(context, parse(json), key)
73
+ end
74
+
75
+ protected # --- DSL for subclasses ---
76
+
77
+ def self.set_key_prefix(prefix)
78
+ raise(InvalidKeyPrefix, prefix) unless prefix =~ /^[a-z]+$/
79
+ @key_prefix = prefix
80
+ end
81
+
82
+ def self.property(name, options = {})
83
+ name = name.to_sym
84
+ mapper[name] = Remodel.mapper_for(options[:class])
85
+ default_values[name] = options[:default] if options.has_key?(:default)
86
+ define_method(name) { @attributes[name] }
87
+ define_method("#{name}=") { |value| @attributes[name] = value }
88
+ end
89
+
90
+ def self.has_many(name, options)
91
+ var = "@association_#{name}".to_sym
92
+
93
+ define_method(name) do
94
+ if instance_variable_defined? var
95
+ instance_variable_get(var)
96
+ else
97
+ clazz = Class[options[:class]]
98
+ instance_variable_set(var, HasMany.new(self, clazz, "#{key}_#{name}", options[:reverse]))
99
+ end
100
+ end
101
+ end
102
+
103
+ def self.has_one(name, options)
104
+ var = "@association_#{name}".to_sym
105
+
106
+ define_method(name) do
107
+ if instance_variable_defined? var
108
+ instance_variable_get(var)
109
+ else
110
+ clazz = Class[options[:class]]
111
+ value_key = Remodel.redis.hget(self.context, "#{key}_#{name}")
112
+ instance_variable_set(var, clazz.find(self.context, value_key)) if value_key
113
+ end
114
+ end
115
+
116
+ define_method("#{name}=") do |value|
117
+ send("_reverse_association_of_#{name}=", value) if options[:reverse]
118
+ send("_#{name}=", value)
119
+ end
120
+
121
+ define_method("_#{name}=") do |value|
122
+ if value
123
+ instance_variable_set(var, value)
124
+ Remodel.redis.hset(self.context, "#{key}_#{name}", value.key)
125
+ else
126
+ remove_instance_variable(var) if instance_variable_defined? var
127
+ Remodel.redis.hdel(self.context, "#{key}_#{name}")
128
+ end
129
+ end; private "_#{name}="
130
+
131
+ if options[:reverse]
132
+ define_method("_reverse_association_of_#{name}=") do |value|
133
+ if old_value = send(name)
134
+ association = old_value.send("#{options[:reverse]}")
135
+ if association.is_a? HasMany
136
+ association.send("_remove", self)
137
+ else
138
+ old_value.send("_#{options[:reverse]}=", nil)
139
+ end
140
+ end
141
+ if value
142
+ association = value.send("#{options[:reverse]}")
143
+ if association.is_a? HasMany
144
+ association.send("_add", self)
145
+ else
146
+ value.send("_#{options[:reverse]}=", self)
147
+ end
148
+ end
149
+ end; private "_reverse_association_of_#{name}="
150
+ end
151
+ end
152
+
153
+ private # --- Helper methods ---
154
+
155
+ def self.fetch(context, key)
156
+ Remodel.redis.hget(context, key) || raise(EntityNotFound, "no #{name} with key #{key} in context #{context}")
157
+ end
158
+
159
+ # Each entity has its own sequence to generate unique ids.
160
+ def next_key
161
+ id = Remodel.redis.hincrby(@context, "#{self.class.key_prefix}", 1)
162
+ "#{self.class.key_prefix}#{id}"
163
+ end
164
+
165
+ # Default key prefix is the first letter of the class name, in lowercase.
166
+ def self.key_prefix
167
+ @key_prefix ||= name.split('::').last[0,1].downcase
168
+ end
169
+
170
+ def self.parse(json)
171
+ unpack(JSON.parse(json))
172
+ end
173
+
174
+ def self.pack(attributes)
175
+ result = {}
176
+ attributes.each do |name, value|
177
+ result[name] = mapper[name].pack(value)
178
+ end
179
+ result
180
+ end
181
+
182
+ def self.unpack(attributes)
183
+ result = {}
184
+ attributes.each do |name, value|
185
+ name = name.to_sym
186
+ result[name] = mapper[name].unpack(value) if mapper[name]
187
+ end
188
+ result
189
+ end
190
+
191
+ # Lazy init
192
+ def self.mapper
193
+ @mapper ||= {}
194
+ end
195
+
196
+ def self.default_values
197
+ @default_values ||= {}
198
+ end
199
+
200
+ end
201
+
202
+ end
@@ -0,0 +1,71 @@
1
+ module Remodel
2
+
3
+ # Represents the many-end of a many-to-one or many-to-many association.
4
+ class HasMany < Array
5
+ def initialize(this, clazz, key, reverse = nil)
6
+ super _fetch(clazz, this.context, key)
7
+ @this, @clazz, @key, @reverse = this, clazz, key, reverse
8
+ end
9
+
10
+ def create(attributes = {})
11
+ add(@clazz.create(@this.context, attributes))
12
+ end
13
+
14
+ def find(id)
15
+ detect { |x| x.id == id } || raise(EntityNotFound, "no element with id #{id}")
16
+ end
17
+
18
+ def add(entity)
19
+ _add_to_reverse_association_of(entity) if @reverse
20
+ _add(entity)
21
+ end
22
+
23
+ def remove(entity)
24
+ _remove_from_reverse_association_of(entity) if @reverse
25
+ _remove(entity)
26
+ end
27
+
28
+ private
29
+
30
+ def _add(entity)
31
+ self << entity
32
+ _store
33
+ entity
34
+ end
35
+
36
+ def _remove(entity)
37
+ delete_if { |x| x.key == entity.key }
38
+ _store
39
+ entity
40
+ end
41
+
42
+ def _add_to_reverse_association_of(entity)
43
+ if entity.send(@reverse).is_a? HasMany
44
+ entity.send(@reverse).send(:_add, @this)
45
+ else
46
+ entity.send("_#{@reverse}=", @this)
47
+ end
48
+ end
49
+
50
+ def _remove_from_reverse_association_of(entity)
51
+ if entity.send(@reverse).is_a? HasMany
52
+ entity.send(@reverse).send(:_remove, @this)
53
+ else
54
+ entity.send("_#{@reverse}=", nil)
55
+ end
56
+ end
57
+
58
+ def _store
59
+ Remodel.redis.hset(@this.context, @key, JSON.generate(self.map(&:key)))
60
+ end
61
+
62
+ def _fetch(clazz, context, key)
63
+ keys = JSON.parse(Remodel.redis.hget(context, key) || '[]')
64
+ values = keys.empty? ? [] : Remodel.redis.hmget(context, *keys)
65
+ keys.zip(values).map do |key, json|
66
+ clazz.restore(context, key, json) if json
67
+ end.compact
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,29 @@
1
+ module Remodel
2
+
3
+ # A mapper converts a given value into a native JSON value &mdash;
4
+ # *nil*, *true*, *false*, *Number*, *String*, *Hash*, *Array* &mdash;
5
+ # via `pack`, and back again via `unpack`.
6
+ #
7
+ # Without any arguments, `Mapper.new` returns the identity mapper, which
8
+ # maps every value to itself. If `clazz` is set, the mapper rejects any
9
+ # value which is not of the given type.
10
+ class Mapper
11
+ def initialize(clazz = nil, pack_method = nil, unpack_method = nil)
12
+ @clazz = clazz
13
+ @pack_method = pack_method
14
+ @unpack_method = unpack_method
15
+ end
16
+
17
+ def pack(value)
18
+ return nil if value.nil?
19
+ raise(InvalidType, "#{value.inspect} is not a #{@clazz}") if @clazz && !value.is_a?(@clazz)
20
+ @pack_method ? value.send(@pack_method) : value
21
+ end
22
+
23
+ def unpack(value)
24
+ return nil if value.nil?
25
+ @unpack_method ? @clazz.send(@unpack_method, value) : value
26
+ end
27
+ end
28
+
29
+ end
data/remodel-h.gemspec ADDED
@@ -0,0 +1,73 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{remodel-h}
8
+ s.version = "0.2.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Tim Lossen"]
12
+ s.date = %q{2010-09-24}
13
+ s.default_executable = %q{redis-monitor.rb}
14
+ s.description = %q{persist your objects to redis hashes.}
15
+ s.email = %q{tim@lossen.de}
16
+ s.executables = ["redis-monitor.rb"]
17
+ s.extra_rdoc_files = [
18
+ "LICENSE",
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ ".gitignore",
23
+ "LICENSE",
24
+ "README.md",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "bin/redis-monitor.rb",
28
+ "docs/docco.css",
29
+ "docs/remodel.html",
30
+ "example/book.rb",
31
+ "lib/remodel-h.rb",
32
+ "lib/remodel/entity.rb",
33
+ "lib/remodel/has_many.rb",
34
+ "lib/remodel/mapper.rb",
35
+ "remodel-h.gemspec",
36
+ "test/helper.rb",
37
+ "test/test_entity.rb",
38
+ "test/test_entity_delete.rb",
39
+ "test/test_many_to_many.rb",
40
+ "test/test_many_to_one.rb",
41
+ "test/test_mappers.rb",
42
+ "test/test_monkeypatches.rb",
43
+ "test/test_one_to_many.rb",
44
+ "test/test_one_to_one.rb"
45
+ ]
46
+ s.homepage = %q{http://github.com/tlossen/remodel}
47
+ s.rdoc_options = ["--charset=UTF-8"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.3.5}
50
+ s.summary = %q{remodel variant which uses hashes}
51
+ s.test_files = [
52
+ "test/helper.rb",
53
+ "test/test_entity.rb",
54
+ "test/test_entity_delete.rb",
55
+ "test/test_many_to_many.rb",
56
+ "test/test_many_to_one.rb",
57
+ "test/test_mappers.rb",
58
+ "test/test_monkeypatches.rb",
59
+ "test/test_one_to_many.rb",
60
+ "test/test_one_to_one.rb"
61
+ ]
62
+
63
+ if s.respond_to? :specification_version then
64
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
65
+ s.specification_version = 3
66
+
67
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
68
+ else
69
+ end
70
+ else
71
+ end
72
+ end
73
+