remodel 0.1.4 → 0.3.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/README.md +19 -10
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/example/book.rb +1 -0
- data/lib/remodel/entity.rb +57 -45
- data/lib/remodel/has_many.rb +12 -8
- data/lib/remodel.rb +2 -1
- data/test/helper.rb +1 -1
- data/test/test_entity.rb +47 -96
- data/test/test_entity_defaults.rb +55 -0
- data/test/test_entity_delete.rb +61 -0
- data/test/test_many_to_many.rb +7 -7
- data/test/test_many_to_one.rb +28 -15
- data/test/test_mappers.rb +11 -11
- data/test/test_one_to_many.rb +22 -22
- data/test/test_one_to_one.rb +4 -4
- metadata +22 -12
- data/.gitignore +0 -2
- data/lib/remodel/has_one.rb +0 -57
- data/remodel.gemspec +0 -72
    
        data/README.md
    CHANGED
    
    | @@ -5,6 +5,10 @@ use [redis](http://github.com/antirez/redis) instead of mysql to store your appl | |
| 5 5 | 
             
            remodel (= redis model) is an ActiveRecord-like mapping layer which offers familiar syntax 
         | 
| 6 6 | 
             
            like `has_many`, `has_one` etc. to build your domain model in ruby.
         | 
| 7 7 |  | 
| 8 | 
            +
            entities are serialized to json and stored as fields in a redis hash. using different hashes
         | 
| 9 | 
            +
            (called 'contexts' in remodel), you can easily separate data belonging to multiple users, 
         | 
| 10 | 
            +
            for example.
         | 
| 11 | 
            +
             | 
| 8 12 |  | 
| 9 13 | 
             
            ## why redis?
         | 
| 10 14 |  | 
| @@ -23,7 +27,7 @@ persistence to disk. for example, on my macbook (2 ghz): | |
| 23 27 |  | 
| 24 28 | 
             
            ## how to get started
         | 
| 25 29 |  | 
| 26 | 
            -
            1. install [redis](http://github.com/antirez/redis) and  | 
| 30 | 
            +
            1. install [redis](http://github.com/antirez/redis) and the
         | 
| 27 31 | 
             
            [redis-rb](http://github.com/ezmobius/redis-rb) ruby client:
         | 
| 28 32 |  | 
| 29 33 | 
             
            		$ brew install redis
         | 
| @@ -49,6 +53,7 @@ define your domain model [like this](http://github.com/tlossen/remodel/blob/mast | |
| 49 53 | 
             
            	  has_many :chapters, :class => 'Chapter', :reverse => :book
         | 
| 50 54 | 
             
            	  property :title, :class => 'String'
         | 
| 51 55 | 
             
            	  property :year, :class => 'Integer'
         | 
| 56 | 
            +
            	  property :author, :class => 'String', :default => '(anonymous)'
         | 
| 52 57 | 
             
            	end
         | 
| 53 58 |  | 
| 54 59 | 
             
            	class Chapter < Remodel::Entity
         | 
| @@ -58,15 +63,20 @@ define your domain model [like this](http://github.com/tlossen/remodel/blob/mast | |
| 58 63 |  | 
| 59 64 | 
             
            now you can do:
         | 
| 60 65 |  | 
| 61 | 
            -
            	>> require 'example/book'
         | 
| 66 | 
            +
            	>> require './example/book'
         | 
| 62 67 | 
             
            	=> true
         | 
| 63 | 
            -
            	>> book = Book.create :title => 'Moby Dick', :year => 1851
         | 
| 64 | 
            -
            	=> #<Book( | 
| 68 | 
            +
            	>> book = Book.create 'shelf', :title => 'Moby Dick', :year => 1851
         | 
| 69 | 
            +
            	=> #<Book(shelf, 1) title: "Moby Dick", year: 1851, author: "(anonymous)"> 
         | 
| 65 70 | 
             
            	>> chapter = book.chapters.create :title => 'Ishmael'
         | 
| 66 | 
            -
            	=> #<Chapter( | 
| 71 | 
            +
            	=> #<Chapter(shelf, 1) title: "Ishmael"> 
         | 
| 67 72 | 
             
            	>> chapter.book
         | 
| 68 | 
            -
            	=> #<Book( | 
| 73 | 
            +
            	=> #<Book(shelf, 1) title: "Moby Dick", year: 1851, author: "(anonymous)"> 
         | 
| 74 | 
            +
             | 
| 75 | 
            +
            all entities have been created in the redis hash 'shelf' we have used as context:
         | 
| 69 76 |  | 
| 77 | 
            +
            	>> Remodel.redis.hgetall 'shelf'
         | 
| 78 | 
            +
            	=> {"b"=>"1", "b1"=>"{\"title\":\"Moby Dick\",\"year\":1851}", "c"=>"1", 
         | 
| 79 | 
            +
            	   "c1"=>"{\"title\":\"Ishmael\"}", "c1_book"=>"b1", "b1_chapters"=>"[\"c1\"]"}
         | 
| 70 80 |  | 
| 71 81 | 
             
            ## inspired by
         | 
| 72 82 |  | 
| @@ -81,16 +91,15 @@ somewhat similar, but instead of serializing to json, stores each attribute unde | |
| 81 91 | 
             
            ## todo
         | 
| 82 92 |  | 
| 83 93 | 
             
            * better docs
         | 
| 84 | 
            -
            * `find_by`
         | 
| 85 94 | 
             
            * make serializer (json, messagepack, marshal ...) configurable
         | 
| 86 | 
            -
            * benchmarks
         | 
| 87 95 |  | 
| 88 96 |  | 
| 89 97 | 
             
            ## status
         | 
| 90 98 |  | 
| 91 | 
            -
             | 
| 99 | 
            +
            it has some rough edges, but i have successfully been using remodel in production since summer 2010.
         | 
| 100 | 
            +
             | 
| 92 101 |  | 
| 93 102 |  | 
| 94 103 | 
             
            ## license
         | 
| 95 104 |  | 
| 96 | 
            -
            [MIT](http://github.com/tlossen/remodel/raw/master/LICENSE) | 
| 105 | 
            +
            [MIT](http://github.com/tlossen/remodel/raw/master/LICENSE)
         | 
    
        data/Rakefile
    CHANGED
    
    | @@ -5,8 +5,8 @@ begin | |
| 5 5 | 
             
              require 'jeweler'
         | 
| 6 6 | 
             
              Jeweler::Tasks.new do |gem|
         | 
| 7 7 | 
             
                gem.name = "remodel"
         | 
| 8 | 
            -
                gem.summary = " | 
| 9 | 
            -
                gem.description = " | 
| 8 | 
            +
                gem.summary = "remodel variant which uses hashes"
         | 
| 9 | 
            +
                gem.description = "persist your objects to redis hashes."
         | 
| 10 10 | 
             
                gem.email = "tim@lossen.de"
         | 
| 11 11 | 
             
                gem.homepage = "http://github.com/tlossen/remodel"
         | 
| 12 12 | 
             
                gem.authors = ["Tim Lossen"]
         | 
    
        data/VERSION
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            0. | 
| 1 | 
            +
            0.3.0
         | 
    
        data/example/book.rb
    CHANGED
    
    | @@ -4,6 +4,7 @@ class Book < Remodel::Entity | |
| 4 4 | 
             
              has_many :chapters, :class => 'Chapter', :reverse => :book
         | 
| 5 5 | 
             
              property :title, :class => 'String'
         | 
| 6 6 | 
             
              property :year, :class => 'Integer'
         | 
| 7 | 
            +
              property :author, :class => 'String', :default => '(anonymous)'
         | 
| 7 8 | 
             
            end
         | 
| 8 9 |  | 
| 9 10 | 
             
            class Chapter < Remodel::Entity
         | 
    
        data/lib/remodel/entity.rb
    CHANGED
    
    | @@ -2,24 +2,24 @@ module Remodel | |
| 2 2 |  | 
| 3 3 | 
             
              # The superclass of all persistent remodel entities.
         | 
| 4 4 | 
             
              class Entity
         | 
| 5 | 
            -
                attr_accessor :key
         | 
| 5 | 
            +
                attr_accessor :context, :key
         | 
| 6 6 |  | 
| 7 | 
            -
                def initialize(attributes = {}, key = nil)
         | 
| 7 | 
            +
                def initialize(context, attributes = {}, key = nil)
         | 
| 8 | 
            +
                  @context = context
         | 
| 8 9 | 
             
                  @attributes = {}
         | 
| 9 10 | 
             
                  @key = key
         | 
| 10 | 
            -
                  attributes = self.class.default_values.merge(attributes) if key.nil?
         | 
| 11 11 | 
             
                  attributes.each do |name, value|
         | 
| 12 12 | 
             
                    send("#{name}=", value) if respond_to? "#{name}="
         | 
| 13 13 | 
             
                  end
         | 
| 14 14 | 
             
                end
         | 
| 15 15 |  | 
| 16 16 | 
             
                def id
         | 
| 17 | 
            -
                  key && key. | 
| 17 | 
            +
                  key && key.match(/\d+/)[0].to_i
         | 
| 18 18 | 
             
                end
         | 
| 19 19 |  | 
| 20 20 | 
             
                def save
         | 
| 21 | 
            -
                  @key =  | 
| 22 | 
            -
                  Remodel.redis. | 
| 21 | 
            +
                  @key = next_key unless @key
         | 
| 22 | 
            +
                  Remodel.redis.hset(@context, @key, to_json)
         | 
| 23 23 | 
             
                  self
         | 
| 24 24 | 
             
                end
         | 
| 25 25 |  | 
| @@ -30,20 +30,25 @@ module Remodel | |
| 30 30 |  | 
| 31 31 | 
             
                def reload
         | 
| 32 32 | 
             
                  raise EntityNotSaved unless @key
         | 
| 33 | 
            -
                   | 
| 34 | 
            -
                   | 
| 35 | 
            -
             | 
| 33 | 
            +
                  attributes = self.class.parse(self.class.fetch(@context, @key))
         | 
| 34 | 
            +
                  initialize(@context, attributes, @key)
         | 
| 35 | 
            +
                  self.class.associations.each do |name|
         | 
| 36 | 
            +
                    var = "@#{name}".to_sym
         | 
| 37 | 
            +
                    remove_instance_variable(var) if instance_variable_defined? var
         | 
| 36 38 | 
             
                  end
         | 
| 37 39 | 
             
                  self
         | 
| 38 40 | 
             
                end
         | 
| 39 41 |  | 
| 40 42 | 
             
                def delete
         | 
| 41 43 | 
             
                  raise EntityNotSaved unless @key
         | 
| 42 | 
            -
                  Remodel.redis. | 
| 44 | 
            +
                  Remodel.redis.hdel(@context, @key)
         | 
| 45 | 
            +
                  self.class.associations.each do |name|
         | 
| 46 | 
            +
                    Remodel.redis.hdel(@context, "#{@key}_#{name}")
         | 
| 47 | 
            +
                  end
         | 
| 43 48 | 
             
                end
         | 
| 44 49 |  | 
| 45 50 | 
             
                def as_json
         | 
| 46 | 
            -
                  { :id => id }.merge( | 
| 51 | 
            +
                  { :id => id }.merge(attributes)
         | 
| 47 52 | 
             
                end
         | 
| 48 53 |  | 
| 49 54 | 
             
                def to_json
         | 
| @@ -51,29 +56,21 @@ module Remodel | |
| 51 56 | 
             
                end
         | 
| 52 57 |  | 
| 53 58 | 
             
                def inspect
         | 
| 54 | 
            -
                  properties =  | 
| 55 | 
            -
                  "\#<#{self.class.name}(#{id}) #{properties}>"
         | 
| 56 | 
            -
                end
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                def self.create(attributes = {})
         | 
| 59 | 
            -
                  new(attributes).save
         | 
| 59 | 
            +
                  properties = attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')
         | 
| 60 | 
            +
                  "\#<#{self.class.name}(#{context}, #{id}) #{properties}>"
         | 
| 60 61 | 
             
                end
         | 
| 61 62 |  | 
| 62 | 
            -
                def self. | 
| 63 | 
            -
                   | 
| 64 | 
            -
                  restore(key, fetch(key))
         | 
| 63 | 
            +
                def self.create(context, attributes = {})
         | 
| 64 | 
            +
                  new(context, attributes).save
         | 
| 65 65 | 
             
                end
         | 
| 66 66 |  | 
| 67 | 
            -
                def self. | 
| 68 | 
            -
                   | 
| 69 | 
            -
                   | 
| 70 | 
            -
                  keys.zip(values).map do |key, json|
         | 
| 71 | 
            -
                    restore(key, json) if json
         | 
| 72 | 
            -
                  end.compact
         | 
| 67 | 
            +
                def self.find(context, key)
         | 
| 68 | 
            +
                  key = "#{key_prefix}#{key}" if key.kind_of? Integer
         | 
| 69 | 
            +
                  restore(context, key, fetch(context, key))
         | 
| 73 70 | 
             
                end
         | 
| 74 71 |  | 
| 75 | 
            -
                def self.restore(key, json)
         | 
| 76 | 
            -
                  new(parse(json), key)
         | 
| 72 | 
            +
                def self.restore(context, key, json)
         | 
| 73 | 
            +
                  new(context, parse(json), key)
         | 
| 77 74 | 
             
                end
         | 
| 78 75 |  | 
| 79 76 | 
             
              protected # --- DSL for subclasses ---
         | 
| @@ -86,34 +83,37 @@ module Remodel | |
| 86 83 | 
             
                def self.property(name, options = {})
         | 
| 87 84 | 
             
                  name = name.to_sym
         | 
| 88 85 | 
             
                  mapper[name] = Remodel.mapper_for(options[:class])
         | 
| 89 | 
            -
                   | 
| 90 | 
            -
                  define_method(name) { @attributes[name] }
         | 
| 86 | 
            +
                  default_value = options[:default]
         | 
| 87 | 
            +
                  define_method(name) { @attributes[name].nil? ? self.class.copy_of(default_value) : @attributes[name] }
         | 
| 91 88 | 
             
                  define_method("#{name}=") { |value| @attributes[name] = value }
         | 
| 92 89 | 
             
                end
         | 
| 93 90 |  | 
| 94 91 | 
             
                def self.has_many(name, options)
         | 
| 95 | 
            -
                   | 
| 92 | 
            +
                  associations.push(name)
         | 
| 93 | 
            +
                  var = "@#{name}".to_sym
         | 
| 96 94 |  | 
| 97 95 | 
             
                  define_method(name) do
         | 
| 98 96 | 
             
                    if instance_variable_defined? var
         | 
| 99 97 | 
             
                      instance_variable_get(var)
         | 
| 100 98 | 
             
                    else
         | 
| 101 99 | 
             
                      clazz = Class[options[:class]]
         | 
| 102 | 
            -
                      instance_variable_set(var, HasMany.new(self, clazz, "#{key} | 
| 100 | 
            +
                      instance_variable_set(var, HasMany.new(self, clazz, "#{key}_#{name}", options[:reverse]))
         | 
| 103 101 | 
             
                    end
         | 
| 104 102 | 
             
                  end
         | 
| 105 103 | 
             
                end
         | 
| 106 104 |  | 
| 107 105 | 
             
                def self.has_one(name, options)
         | 
| 108 | 
            -
                   | 
| 106 | 
            +
                  associations.push(name)
         | 
| 107 | 
            +
                  var = "@#{name}".to_sym
         | 
| 109 108 |  | 
| 110 109 | 
             
                  define_method(name) do
         | 
| 111 110 | 
             
                    if instance_variable_defined? var
         | 
| 112 111 | 
             
                      instance_variable_get(var)
         | 
| 113 112 | 
             
                    else
         | 
| 114 113 | 
             
                      clazz = Class[options[:class]]
         | 
| 115 | 
            -
                      value_key = Remodel.redis. | 
| 116 | 
            -
                       | 
| 114 | 
            +
                      value_key = Remodel.redis.hget(self.context, "#{key}_#{name}")
         | 
| 115 | 
            +
                      value = value_key && clazz.find(self.context, value_key) rescue nil
         | 
| 116 | 
            +
                      instance_variable_set(var, value)
         | 
| 117 117 | 
             
                    end
         | 
| 118 118 | 
             
                  end
         | 
| 119 119 |  | 
| @@ -125,10 +125,10 @@ module Remodel | |
| 125 125 | 
             
                  define_method("_#{name}=") do |value|
         | 
| 126 126 | 
             
                    if value
         | 
| 127 127 | 
             
                      instance_variable_set(var, value)
         | 
| 128 | 
            -
                      Remodel.redis. | 
| 128 | 
            +
                      Remodel.redis.hset(self.context, "#{key}_#{name}", value.key)
         | 
| 129 129 | 
             
                    else
         | 
| 130 130 | 
             
                      remove_instance_variable(var) if instance_variable_defined? var
         | 
| 131 | 
            -
                      Remodel.redis. | 
| 131 | 
            +
                      Remodel.redis.hdel(self.context, "#{key}_#{name}")
         | 
| 132 132 | 
             
                    end
         | 
| 133 133 | 
             
                  end; private "_#{name}="
         | 
| 134 134 |  | 
| @@ -156,14 +156,22 @@ module Remodel | |
| 156 156 |  | 
| 157 157 | 
             
              private # --- Helper methods ---
         | 
| 158 158 |  | 
| 159 | 
            -
                def  | 
| 160 | 
            -
                   | 
| 159 | 
            +
                def attributes
         | 
| 160 | 
            +
                  result = {}
         | 
| 161 | 
            +
                  self.class.mapper.keys.each do |name|
         | 
| 162 | 
            +
                    result[name] = send(name)
         | 
| 163 | 
            +
                  end
         | 
| 164 | 
            +
                  result
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                def self.fetch(context, key)
         | 
| 168 | 
            +
                  Remodel.redis.hget(context, key) || raise(EntityNotFound, "no #{name} with key #{key} in context #{context}")
         | 
| 161 169 | 
             
                end
         | 
| 162 170 |  | 
| 163 171 | 
             
                # Each entity has its own sequence to generate unique ids.
         | 
| 164 | 
            -
                def  | 
| 165 | 
            -
                  id = Remodel.redis. | 
| 166 | 
            -
                  "#{key_prefix} | 
| 172 | 
            +
                def next_key
         | 
| 173 | 
            +
                  id = Remodel.redis.hincrby(@context, "#{self.class.key_prefix}", 1)
         | 
| 174 | 
            +
                  "#{self.class.key_prefix}#{id}"
         | 
| 167 175 | 
             
                end
         | 
| 168 176 |  | 
| 169 177 | 
             
                # Default key prefix is the first letter of the class name, in lowercase.
         | 
| @@ -187,7 +195,7 @@ module Remodel | |
| 187 195 | 
             
                  result = {}
         | 
| 188 196 | 
             
                  attributes.each do |name, value|
         | 
| 189 197 | 
             
                    name = name.to_sym
         | 
| 190 | 
            -
                    result[name] = mapper[name].unpack(value)
         | 
| 198 | 
            +
                    result[name] = mapper[name].unpack(value) if mapper[name]
         | 
| 191 199 | 
             
                  end
         | 
| 192 200 | 
             
                  result
         | 
| 193 201 | 
             
                end
         | 
| @@ -197,8 +205,12 @@ module Remodel | |
| 197 205 | 
             
                  @mapper ||= {}
         | 
| 198 206 | 
             
                end
         | 
| 199 207 |  | 
| 200 | 
            -
                def self. | 
| 201 | 
            -
                  @ | 
| 208 | 
            +
                def self.associations
         | 
| 209 | 
            +
                  @associations ||= []
         | 
| 210 | 
            +
                end
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                def self.copy_of(value)
         | 
| 213 | 
            +
                  value.is_a?(Array) || value.is_a?(Hash) ? value.dup : value
         | 
| 202 214 | 
             
                end
         | 
| 203 215 |  | 
| 204 216 | 
             
              end
         | 
    
        data/lib/remodel/has_many.rb
    CHANGED
    
    | @@ -3,12 +3,12 @@ module Remodel | |
| 3 3 | 
             
              # Represents the many-end of a many-to-one or many-to-many association.
         | 
| 4 4 | 
             
              class HasMany < Array
         | 
| 5 5 | 
             
                def initialize(this, clazz, key, reverse = nil)
         | 
| 6 | 
            -
                  super _fetch(clazz, key)
         | 
| 6 | 
            +
                  super _fetch(clazz, this.context, key)
         | 
| 7 7 | 
             
                  @this, @clazz, @key, @reverse = this, clazz, key, reverse
         | 
| 8 8 | 
             
                end
         | 
| 9 9 |  | 
| 10 10 | 
             
                def create(attributes = {})
         | 
| 11 | 
            -
                  add(@clazz.create(attributes))
         | 
| 11 | 
            +
                  add(@clazz.create(@this.context, attributes))
         | 
| 12 12 | 
             
                end
         | 
| 13 13 |  | 
| 14 14 | 
             
                def find(id)
         | 
| @@ -29,13 +29,13 @@ module Remodel | |
| 29 29 |  | 
| 30 30 | 
             
                def _add(entity)
         | 
| 31 31 | 
             
                  self << entity
         | 
| 32 | 
            -
                   | 
| 32 | 
            +
                  _store
         | 
| 33 33 | 
             
                  entity
         | 
| 34 34 | 
             
                end
         | 
| 35 35 |  | 
| 36 36 | 
             
                def _remove(entity)
         | 
| 37 37 | 
             
                  delete_if { |x| x.key == entity.key }
         | 
| 38 | 
            -
                   | 
| 38 | 
            +
                  _store
         | 
| 39 39 | 
             
                  entity
         | 
| 40 40 | 
             
                end
         | 
| 41 41 |  | 
| @@ -55,11 +55,15 @@ module Remodel | |
| 55 55 | 
             
                  end
         | 
| 56 56 | 
             
                end
         | 
| 57 57 |  | 
| 58 | 
            -
                def  | 
| 59 | 
            -
                   | 
| 60 | 
            -
             | 
| 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) || '[]').uniq
         | 
| 64 | 
            +
                  values = keys.empty? ? [] : Remodel.redis.hmget(context, *keys)
         | 
| 61 65 | 
             
                  keys.zip(values).map do |key, json|
         | 
| 62 | 
            -
                    clazz.restore(key, json) if json
         | 
| 66 | 
            +
                    clazz.restore(context, key, json) if json
         | 
| 63 67 | 
             
                  end.compact
         | 
| 64 68 | 
             
                end
         | 
| 65 69 | 
             
              end
         | 
    
        data/lib/remodel.rb
    CHANGED
    
    | @@ -34,6 +34,7 @@ module Remodel | |
| 34 34 | 
             
              class EntityNotSaved < Error; end
         | 
| 35 35 | 
             
              class InvalidKeyPrefix < Error; end
         | 
| 36 36 | 
             
              class InvalidType < Error; end
         | 
| 37 | 
            +
              class MissingContext < Error; end
         | 
| 37 38 |  | 
| 38 39 | 
             
              # By default, the redis server is expected to listen at `localhost:6379`.
         | 
| 39 40 | 
             
              # Otherwise you will have to set `Remodel.redis` to a suitably initialized
         | 
| @@ -45,7 +46,7 @@ module Remodel | |
| 45 46 | 
             
              def self.redis=(redis)
         | 
| 46 47 | 
             
                @redis = redis
         | 
| 47 48 | 
             
              end
         | 
| 48 | 
            -
             | 
| 49 | 
            +
              
         | 
| 49 50 | 
             
              # Returns the mapper defined for a given class, or the identity mapper.
         | 
| 50 51 | 
             
              def self.mapper_for(clazz)
         | 
| 51 52 | 
             
                mapper_by_class[Class[clazz]]
         | 
    
        data/test/helper.rb
    CHANGED