norman 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,122 @@
1
+ require "active_model"
2
+
3
+ module Norman
4
+ # Extend this module if you want {Active Model}[http://github.com/rails/rails/tree/master/activemodel]
5
+ # support. Active Model is an API provided by Rails to make any Ruby object
6
+ # behave like an Active Record model instance. You can read an older writeup
7
+ # about it {here}[http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/].
8
+ module ActiveModel
9
+ def self.extended(base)
10
+ base.instance_eval do
11
+ extend ClassMethods
12
+ include InstanceMethods
13
+ extend ::ActiveModel::Naming
14
+ extend ::ActiveModel::Translation
15
+ include ::ActiveModel::Validations
16
+ include ::ActiveModel::Serializers::JSON
17
+ include ::ActiveModel::Serializers::Xml
18
+ extend ::ActiveModel::Callbacks
19
+ define_model_callbacks :save, :destroy
20
+ end
21
+ end
22
+
23
+ # Custom validations.
24
+ module Validations
25
+ # A uniqueness validator, similar to the one provided by Active Record.
26
+ class Uniqueness< ::ActiveModel::EachValidator
27
+ def validate_each(record, attribute, value)
28
+ return if record.persisted?
29
+ if attribute.to_sym == record.class.id_method
30
+ begin
31
+ if record.class.mapper[value]
32
+ record.errors[attribute] << "must be unique"
33
+ end
34
+ rescue Norman::NotFoundError
35
+ end
36
+ else
37
+ if record.class.all.detect {|x| x.send(attribute) == value}
38
+ record.errors[attribute] << "must be unique"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ module ClassMethods
46
+ # Create and save a model instance, raising an exception if any errors
47
+ # occur.
48
+ def create!(*args)
49
+ new(*args).save!
50
+ end
51
+
52
+ # Validate the uniqueness of a field's value in a model instance.
53
+ def validates_uniqueness_of(*attr_names)
54
+ validates_with Validations::Uniqueness, _merge_attributes(attr_names)
55
+ end
56
+
57
+ def model_name
58
+ @model_name ||= ::ActiveModel::Name.new(self)
59
+ end
60
+ end
61
+
62
+ module InstanceMethods
63
+ def initialize(*args)
64
+ @new_record = true
65
+ super
66
+ end
67
+
68
+ def attributes
69
+ hash = to_hash
70
+ hash.keys.each {|k| hash[k.to_s] = hash.delete(k)}
71
+ hash
72
+ end
73
+
74
+ def keys
75
+ self.class.attribute_names
76
+ end
77
+
78
+ def to_model
79
+ self
80
+ end
81
+
82
+ def new_record?
83
+ @new_record
84
+ end
85
+
86
+ def persisted?
87
+ !new_record?
88
+ end
89
+
90
+ def save
91
+ run_callbacks(:save) do
92
+ @new_record = false
93
+ super
94
+ end
95
+ end
96
+
97
+ def save!
98
+ if !valid?
99
+ raise Norman::NormanError, errors.to_a.join(", ")
100
+ else
101
+ save
102
+ end
103
+ end
104
+
105
+ def to_param
106
+ to_id if persisted?
107
+ end
108
+
109
+ def to_key
110
+ [to_param] if persisted?
111
+ end
112
+
113
+ def destroy
114
+ run_callbacks(:destroy) { delete }
115
+ end
116
+
117
+ def update_attributes
118
+ run_callbacks(:save) { update }
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,53 @@
1
+ module Norman
2
+
3
+ # Adapters are responsible for persisting the database. This base adapter
4
+ # offers no persistence, all IO operations are just stubs. Adapters must also
5
+ # present the full database as a Hash to the mapper, and provide a `key`
6
+ # method that returns an array with all the keys for the specified model
7
+ # class.
8
+ class Adapter
9
+
10
+ attr_reader :name
11
+ attr_reader :db
12
+
13
+ # @option options [String] :name The adapter name. Defaults to {#Norman.default_adapter_name}.
14
+ def initialize(options = {})
15
+ @name = options[:name] || Norman.default_adapter_name
16
+ load_database
17
+ Norman.register_adapter(self)
18
+ end
19
+
20
+ # Get a hash of all the data for the specified model class.
21
+ # @param klass [#to_s] The model class whose data to return.
22
+ def db_for(klass)
23
+ @db[klass.to_s] ||= {}
24
+ end
25
+
26
+ # Loads the database. For this adapter, that means simply creating a new
27
+ # hash.
28
+ def load_database
29
+ @db = {}
30
+ end
31
+
32
+ # These are all just noops for this adapter, which uses an in-memory hash
33
+ # and offers no persistence.
34
+
35
+ # Inheriting adapters can overload this method to export the data to a
36
+ # String.
37
+ def export_data
38
+ true
39
+ end
40
+
41
+ # Inheriting adapters can overload this method to load the data from some
42
+ # kind of storage.
43
+ def import_data
44
+ true
45
+ end
46
+
47
+ # Inheriting adapters can overload this method to persist the data to some
48
+ # kind of storage.
49
+ def save_database
50
+ true
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,55 @@
1
+ require "active_support"
2
+ require "active_support/message_verifier"
3
+ require "zlib"
4
+
5
+ module Norman
6
+ module Adapters
7
+
8
+ # Norman's cookie adapter allows you to store a Norman database inside
9
+ # a zipped and signed string suitable for setting as a cookie. This can be
10
+ # useful for modelling things like basic shopping carts or form wizards.
11
+ # Keep in mind the data is signed, so it can't be tampered with. However,
12
+ # the data is not *encrypted*, so somebody that wanted to could unzip and
13
+ # load the cookie data to see what's inside. So don't send this data
14
+ # client-side if it's at all sensitive.
15
+ class Cookie < Adapter
16
+
17
+ attr :verifier
18
+ attr_accessor :data
19
+
20
+ MAX_DATA_LENGTH = 4096
21
+
22
+ def self.max_data_length
23
+ MAX_DATA_LENGTH
24
+ end
25
+
26
+ def initialize(options)
27
+ @data = options[:data]
28
+ @verifier = ActiveSupport::MessageVerifier.new(options[:secret])
29
+ super
30
+ end
31
+
32
+ def export_data
33
+ cookie = verifier.generate(Zlib::Deflate.deflate(Marshal.dump(db)))
34
+ length = cookie.bytesize
35
+ if length > Cookie.max_data_length
36
+ raise(NormanError, "Data is %s bytes, cannot exceed %s" % [length, Cookie.max_data_length])
37
+ end
38
+ cookie
39
+ end
40
+
41
+ def import_data
42
+ data.blank? ? {} : Marshal.load(Zlib::Inflate.inflate(verifier.verify(data)))
43
+ end
44
+
45
+ def load_database
46
+ @db = import_data
47
+ @db.map(&:freeze)
48
+ end
49
+
50
+ def save_database
51
+ @data = export_data
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,38 @@
1
+ module Norman
2
+ module Adapters
3
+ # Loads and saves hash database from a Marshal.dump file.
4
+ class File < Adapter
5
+
6
+ attr_reader :file_path
7
+ attr :lock
8
+
9
+ def initialize(options)
10
+ @file_path = options[:file]
11
+ @lock = Mutex.new
12
+ super
13
+ end
14
+
15
+ def load_database
16
+ @db = import_data
17
+ @db.blank? ? @db = {} : @db.map(&:freeze)
18
+ rescue Errno::ENOENT
19
+ # @TODO warn via logger when file doesn't exist
20
+ @db = {}
21
+ end
22
+
23
+ def export_data
24
+ Marshal.dump(db)
25
+ end
26
+
27
+ def import_data
28
+ Marshal.load(::File.read(file_path))
29
+ end
30
+
31
+ def save_database
32
+ @lock.synchronize do
33
+ ::File.open(file_path, "w") {|f| f.write(export_data)}
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ require "yaml"
2
+
3
+ module Norman
4
+ module Adapters
5
+ # An Adapter that uses YAML for its storage.
6
+ class YAML < File
7
+
8
+ def import_data
9
+ data = ::YAML.load(::File.read(file_path))
10
+ end
11
+
12
+ def export_data
13
+ db.to_yaml
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ module Norman
2
+ # Wrapper around hash instances that allows values to be accessed as symbols,
3
+ # strings or method invocations. It behaves similary to OpenStruct, with the
4
+ # fundamental difference being that you instantiate *one* HashProxy instance
5
+ # and reassign its Hash during a loop in order to avoid creating garbage.
6
+ class HashProxy
7
+ attr :hash
8
+
9
+ # Allows accessing a hash attribute as a method.
10
+ def method_missing(symbol)
11
+ hash[symbol] or raise NoMethodError
12
+ end
13
+
14
+ # Allows accessing a hash attribute as hash key, either a string or symbol.
15
+ def [](value)
16
+ hash[value || value.to_sym || value.to_s]
17
+ end
18
+
19
+ # Remove the hash.
20
+ def clear
21
+ @hash = nil
22
+ end
23
+
24
+ # Assign the value to hash and return self.
25
+ def using(hash)
26
+ @hash = hash ; self
27
+ end
28
+
29
+ # Set the hash to use while calling the block. When the block ends, the
30
+ # hash is unset.
31
+ def with(hash, &block)
32
+ yield using hash ensure clear
33
+ end
34
+ end
35
+
36
+ # Like HashProxy, but proxies access to two or more Hash instances.
37
+ class HashProxySet
38
+
39
+ attr :proxies
40
+
41
+ def initialize
42
+ @proxies = []
43
+ end
44
+
45
+ def using(*args)
46
+ args.size.times { proxies.push HashProxy.new } if proxies.empty?
47
+ proxies.each_with_index {|proxy, index| proxy.using args[index] }
48
+ end
49
+
50
+ def clear
51
+ proxies.map(&:clear)
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,66 @@
1
+ module Norman
2
+
3
+ # Mappers provide the middle ground between models and adapters. Mappers are
4
+ # responsible for performing finds and moving objects in and out of the
5
+ # hash.
6
+ class Mapper
7
+ extend Forwardable
8
+ attr :hash
9
+ attr_accessor :adapter_name, :klass, :indexes, :options
10
+ def_delegators :hash, :clear, :delete
11
+ def_delegators :key_set, :all, :count, :find, :find_by_key, :first, :keys
12
+
13
+ def initialize(klass, adapter_name = nil, options = {})
14
+ @klass = klass
15
+ @adapter_name = adapter_name || Norman.default_adapter_name
16
+ @indexes = {}
17
+ @lock = Mutex.new
18
+ @options = options
19
+ @hash = adapter.db_for(klass)
20
+ end
21
+
22
+ # Returns a hash or model attributes corresponding to the provided key.
23
+ def [](key)
24
+ hash[key] or raise NotFoundError.new(klass, key)
25
+ end
26
+
27
+ # Sets a hash by key.
28
+ def []=(key, value)
29
+ @lock.synchronize do
30
+ @indexes = {}
31
+ if value.id_changed?
32
+ hash.delete value.to_id(true)
33
+ end
34
+ saved = hash[key] = value.to_hash.freeze
35
+ adapter.save_database if @options[:sync]
36
+ saved
37
+ end
38
+ end
39
+
40
+ # Memoize the output of a find in a threadsafe manner.
41
+ def add_index(name, indexable)
42
+ @lock.synchronize do
43
+ @indexes[name] = indexable
44
+ end
45
+ end
46
+
47
+ # Get the adapter.
48
+ def adapter
49
+ Norman.adapters[adapter_name]
50
+ end
51
+
52
+ # Get an instance by key
53
+ def get(key)
54
+ klass.send :from_hash, self[key]
55
+ end
56
+
57
+ def key_set
58
+ klass.key_class.new(hash.keys.freeze, self)
59
+ end
60
+
61
+ # Sets an instance, invoking its to_id method
62
+ def put(instance)
63
+ self[instance.to_id] = instance
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,164 @@
1
+ module Norman
2
+
3
+ module Model
4
+ def self.extended(base)
5
+ base.instance_eval do
6
+ @lock = Mutex.new
7
+ @attribute_names = []
8
+ @key_class = Class.new(Norman::AbstractKeySet)
9
+ extend ClassMethods
10
+ include InstanceMethods
11
+ include Comparable
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ extend Forwardable
17
+ attr_accessor :attribute_names, :id_method, :mapper
18
+ attr_reader :key_class
19
+ def_delegators(*[:find, Enumerable.public_instance_methods(false)].flatten)
20
+ def_delegators(:mapper, :[], :all, :delete, :first, :get, :count, :find, :find_by_key, :keys)
21
+ alias id_field id_method=
22
+
23
+ def field(*names)
24
+ names.each do |name|
25
+ # First attribute added is the default id
26
+ id_field name if attribute_names.empty?
27
+ attribute_names << name.to_sym
28
+ class_eval(<<-EOM, __FILE__, __LINE__ + 1)
29
+ def #{name}
30
+ @#{name} or (@attributes[:#{name}] if @attributes)
31
+ end
32
+
33
+ def #{name}=(value)
34
+ @#{name} = value
35
+ end
36
+ EOM
37
+ end
38
+ end
39
+
40
+ def use(adapter_name, options = {})
41
+ @mapper = nil
42
+ @adapter_name = adapter_name
43
+ @mapper_options = options
44
+ end
45
+
46
+ # Memoize the output of the method call invoked in the block.
47
+ # @param [#to_s] name If not given, the name of the method calling with_index will be used.
48
+ def with_index(name = nil, &block)
49
+ name ||= caller(1)[0].match(/in `(.*)'\z/)[1]
50
+ mapper.indexes[name.to_s] or begin
51
+ indexable = yield
52
+ mapper.add_index(name, indexable)
53
+ end
54
+ end
55
+
56
+ def create(hash)
57
+ new(hash).save
58
+ end
59
+
60
+ # The point of this method is to provide a fast way to get model instances
61
+ # based on the hash attributes managed by the mapper and adapter.
62
+ #
63
+ # The hash arg gets frozen, which can be a nasty side-effect, but helps
64
+ # avoid hard-to-track-down bugs if the hash is updated somewhere outside
65
+ # the model. This should only be used internally to Norman, which is why
66
+ # it's private.
67
+ def from_hash(hash)
68
+ instance = allocate
69
+ instance.instance_variable_set :@attributes, hash.freeze
70
+ instance
71
+ end
72
+ private :from_hash
73
+
74
+ def filters(&block)
75
+ key_class.class_eval(&block)
76
+ key_class.instance_methods(false).each do |name|
77
+ instance_eval(<<-EOM, __FILE__, __LINE__ + 1)
78
+ def #{name}(*args)
79
+ mapper.key_set.#{name}(*args)
80
+ end
81
+ EOM
82
+ end
83
+ end
84
+
85
+ def mapper
86
+ @mapper or @lock.synchronize do
87
+ name = @adapter_name || Norman.default_adapter_name
88
+ options = @mapper_options || {}
89
+ @mapper ||= Mapper.new(self, name, options)
90
+ end
91
+ end
92
+ end
93
+
94
+ module InstanceMethods
95
+
96
+ # Norman models can be instantiated with a hash of attribures, a block,
97
+ # or both. If both a hash and block are given, then the values set inside
98
+ # the block will take precedence over those set in the hash.
99
+ #
100
+ # @example
101
+ # Person.new :name => "Joe"
102
+ # Person.new {|p| p.name = "Joe"}
103
+ # Person.new(params[:person]) {|p| p.age = 38}
104
+ #
105
+ def initialize(attributes = nil, &block)
106
+ @attributes = {}.freeze
107
+ return unless attributes || block_given?
108
+ if attributes
109
+ self.class.attribute_names.each do |name|
110
+ value = attributes[name] || attributes[name.to_s]
111
+ send("#{name}=", value) if value
112
+ end
113
+ end
114
+ yield self if block_given?
115
+ end
116
+
117
+ # Norman models implement the <=> method and mix in Comparable to provide
118
+ # sorting methods. This default implementation compares the result of
119
+ # #to_id. If the items being compared are not of the same kind.
120
+ def <=>(instance)
121
+ to_id <=> instance.to_id if instance.kind_of? self.class
122
+ end
123
+
124
+ # Get a hash of the instance's model attributes.
125
+ def to_hash
126
+ self.class.attribute_names.inject({}) do |hash, key|
127
+ hash[key] = self.send(key); hash
128
+ end
129
+ end
130
+
131
+ # Returns true is the model's id field has been updated.
132
+ def id_changed?
133
+ to_id != @attributes[self.class.id_method]
134
+ end
135
+
136
+ # Invoke the model's id method to return this instance's unique key. If
137
+ # true is passed, then the id will be read from the attributes hash rather
138
+ # than from an instance variable. This allows you to retrieve the old id,
139
+ # in the event that the id has been changed.
140
+ def to_id(use_old = false)
141
+ use_old ? @attributes[self.class.id_method] : send(self.class.id_method)
142
+ end
143
+
144
+ # Tell the mapper to save the data for this model instance.
145
+ def save
146
+ self.class.mapper.put(self)
147
+ end
148
+
149
+ # Update this instance's attributes and invoke #save.
150
+ def update(attributes)
151
+ self.class.attribute_names.each do |name|
152
+ value = attributes[name] || attributes[name.to_s]
153
+ send("#{name}=", value) if value
154
+ end
155
+ save
156
+ end
157
+
158
+ # Tell the mapper to delete the data for this instance.
159
+ def delete
160
+ self.class.delete(self.to_id)
161
+ end
162
+ end
163
+ end
164
+ end