simple_model 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -3,3 +3,4 @@ pkg/*
3
3
  .bundle
4
4
  .DS_Store
5
5
  nbproject/
6
+ Gemfile.lock
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Joshua T. Mckinney
1
+ Copyright (c) 2010-2012 Joshua T. Mckinney
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -87,13 +87,50 @@ SimpleModel is available through [Rubygems](http://rubygems.org/gems/simple_mode
87
87
  item.changes # => {"price"=>[#<BigDecimal:7fc61b250da8,'0.1E2',9(27)>, #<BigDecimal:7fc61b1ba600,'0.1024E4',9(27)>]}
88
88
  item.my_array # => []
89
89
  item.valid? # => false
90
+ items.save! # raises SimpleModel::ValidationError exception
90
91
  item.my_array # => [1]
91
92
  item.price = 15
93
+ item.persisted? # => false
92
94
  item.save # => true
95
+ item.persisted? # => true
93
96
  item.changed? # => false
94
97
  item.previous_changes # => {"price"=>[#<BigDecimal:7fc61b1ba600,'0.1024E4',9(27)>, #<BigDecimal:7fc61b1730e8,'0.15E2',9(27)>], "saved"=>[nil, true]}
95
-
96
98
 
99
+ ### Rails Session Modeling
100
+ require 'simple_model'
101
+
102
+ class SessionUser < SimpleModel::Base
103
+ has_attributes :permissions, :default => []
104
+
105
+ # Returns true only if all required permission are set
106
+ def authorized?(*required_permissions)
107
+ (permissions == (required_permissions | permissions))
108
+ end
109
+
110
+ #... lots of other handy methods...#
111
+ end
112
+
113
+ class ApplicationController < ActionController::Base
114
+ #... omitted for space ...#
115
+ # Initialize, if necessary, and return our session user object
116
+ def session_user
117
+ session[:user] ||= {:permissions => [:foo,:baz]}
118
+ @session_user ||= SessionUser.new_with_store(session[:user])
119
+ end
120
+ helper_method :session_user
121
+
122
+ private
123
+
124
+ # redirect if not authorized
125
+ def authorize(*required_permissions)
126
+ redirect_to '/sessions/error' unless session_user.authorized?(*required_permissions)
127
+ end
128
+ end
129
+
130
+ class FoosController < ApplicationController
131
+ before_filter do |c| c.send(:authorize,:foo) # Make sure session user has permission
132
+ end
133
+
97
134
 
98
135
  ## Contributing to simple_model
99
136
 
@@ -105,6 +142,10 @@ SimpleModel is available through [Rubygems](http://rubygems.org/gems/simple_mode
105
142
  * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
106
143
  * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
107
144
 
145
+ ## Notes
146
+
147
+ Release 1.2+ no longer create instance variables, just uses the attributes hash as the data store.
148
+
108
149
  ## Thanks
109
150
 
110
151
  Code based on Rails/ActiveRecord and [Appoxy/SimpleRecord](https://github.com/appoxy/simple_record)
@@ -1,175 +1,184 @@
1
+ require 'simple_model/exceptions'
1
2
  module SimpleModel
2
- # require all that active support we know and love
3
- require 'active_support/core_ext/array/extract_options'
4
- require 'active_support/core_ext/object/blank'
5
-
6
3
  module Attributes
7
4
  include ExtendCore
8
5
  extend ActiveSupport::Concern
9
- include ActiveModel::AttributeMethods
10
- #Set attribute values to those supplied at initialization
11
- def initialize(*attrs)
12
- set_attributes(attrs.extract_options!)
6
+ include ActiveModel::AttributeMethods
7
+
8
+ def initialize(*attrs)
9
+ attrs = attrs.extract_options!
10
+ set(attributes_with_for_init(attrs))
13
11
  end
14
-
15
- # Place to store set attributes and their values
12
+
13
+ # Returns true if attribute has been initialized
14
+ def initialized?(attr)
15
+ attributes.key?(attr.to_sym)
16
+ end
17
+
16
18
  def attributes
17
- @attributes ||= {}
18
- @attributes
19
+ @attributes ||= HashWithIndifferentAccess.new
19
20
  end
21
+
22
+ def attributes=attrs
23
+ @attributes = attrs
24
+ end
25
+
26
+ def get(attr)
27
+ self.send(attr)
28
+ end
29
+ alias :read :get
20
30
 
21
- def set_attributes(attrs)
22
- attrs.each do |attr|
23
- self.send("#{attr[0].to_sym}=",attr[1])
31
+ # Accepts a hash where the keys are methods and the values are values to be set.
32
+ # set(:foo => "bar", :dime => 0.1)
33
+ def set(*attrs)
34
+ attrs.extract_options!.each do |attr,val|
35
+ self.send("#{attr.to_s}=",val)
24
36
  end
25
37
  end
38
+ alias :set_attributes :set
26
39
 
27
- # Hook to run method after attribute is converted but before it is set
28
- def before_attribute_set(method,val)
29
- end
40
+ private
30
41
 
31
- alias :update_attributes :set_attributes
32
-
33
- def self.included(base)
34
- base.extend(ClassMethods)
42
+ def fetch_default_value(arg)
43
+ return self.send(arg) if (arg.is_a?(Symbol) && self.respond_to?(arg))
44
+ arg
35
45
  end
36
46
 
37
- def fetch_default
47
+ # Returns attribute that have defaults in a hash: {:attrbute => "default value"}
48
+ def attributes_with_for_init(attrs)
49
+ d = attrs.with_indifferent_access
50
+ self.class.defined_attributes.each do |k,v|
51
+ d[k] = fetch_default_value(v[:default]) if (d[k].blank? && v[:default] && v[:initialize])
52
+ end
53
+ d
38
54
  end
39
55
 
40
-
41
- module ClassMethods
42
-
43
- # Hook to call class method after attribute method definitions
44
- def after_attribute_definition(attr)
56
+ module ClassMethods
57
+ # Creates a new instance where the attributes store is set to object
58
+ # provided, which allows one to pass a session store hash or any other
59
+ # hash-like object to be used for persistance. Typically used for modeling
60
+ # session stores for authorization or shopping carts
61
+ # EX:
62
+ # class ApplicationController < ActionController::Base
63
+ # def session_user
64
+ # session[:user] ||= {}
65
+ # @session_user ||= SessionUser.new_with_store(session[:user])
66
+ # end
67
+ # helper_method :session_user
68
+ # end
69
+ #
70
+ def new_with_store(session_hash)
71
+ new = self.new()
72
+ new.attributes = session_hash
73
+ new.set(new.send(:attributes_with_for_init,session_hash))
74
+ new
45
75
  end
46
76
 
47
- # Defines a reader method that returns a default value if current value
48
- # is nil, if :default is present in the options hash
49
- def define_reader_with_options(attr,options)
50
- if options.has_key?(:default)
51
- define_method(attr.to_s) do
52
- default = (options[:default].is_a?(Symbol) ? self.send(options[:default]) : options[:default])
53
- val = instance_variable_get("@#{attr.to_s}")
54
- val = default unless instance_variable_defined?("@#{attr.to_s}")
55
- val
56
- end
57
- else
58
- attr_reader attr
59
- end
77
+ def defined_attributes
78
+ @defined_attributes ||= {}
60
79
  end
61
80
 
62
- def define_setter(attr,cast_methods)
63
- define_method("#{attr.to_s}=") do |val|
64
- val = val.cast_to(cast_methods)
65
- before_attribute_set(attr,val)
66
- instance_variable_set("@#{attr}", val)
67
- attributes[attr] = val
68
- val
69
- end
81
+ def defined_attributes=defined_attributes
82
+ @defined_attributes = defined_attributes
70
83
  end
71
84
 
72
- # Builder for attribute methods
73
- def build_attribute_methods(attr,options={},cast_methods=[])
74
- define_reader_with_options(attr,options)
75
- define_setter(attr,cast_methods)
76
- after_attribute_definition attr
85
+ # The default settings for a SimpeModel class
86
+ # Options:
87
+ # * :on_set - accepts a lambda that is run when an attribute is set
88
+ # * :on_get - accepts a lambda that is run when you get/read an attribute
89
+ # * :default - the default value for the attribute, can be a symbol that is sent for a method
90
+ # * :initialize - informations the object whether or not it should initialize the attribute with :default value, defaults to true
91
+ # ** If :intialize is set to false you must set :allow_blank to false or it will never set the default value
92
+ # * :allow_blank - when set to false, if an attributes value is blank attempts to set the default value, defaults to true
93
+ def default_attribute_settings
94
+ @default_attribute_settings ||= {:attributes_method => :attributes,
95
+ :on_set => lambda {|obj,attr| attr},
96
+ :on_get => lambda {|obj,attr| attr},
97
+ :allow_blank => true,
98
+ :initialize => true
99
+ }
77
100
  end
78
-
79
- # Left this use a module eval for reference, saw no noticable improvement
80
- # in speed, so I would rather use code than strings for now
81
- # def define_setter_with_eval(attr,cast_methods)
82
- # module_eval <<-STR, __FILE__, __LINE__
83
- # def #{attr.to_s}=#{attr.to_s}
84
- # val = #{attr.to_s}.cast_to(#{cast_methods})
85
- # before_attribute_set(:#{attr.to_s},val)
86
- # @#{attr.to_s} = val
87
- # attributes[:#{attr.to_s}] = val
88
- # val
89
- # end
90
- # STR
91
- # end
92
101
 
93
- #creates setter and getter datatype special attribute
94
- def has_attributes(*attrs)
95
- options = attrs.extract_options!
96
- attrs.each do |attr|
97
- build_attribute_methods(attr,options)
98
- end
102
+ def default_attribute_settings=default_attribute_settings
103
+ @default_attribute_settings = default_attribute_settings
99
104
  end
100
- alias :has_attribute :has_attributes
101
-
102
- # Creates setter and getter methods for boolean attributes
103
- def has_booleans(*attrs)
104
- options = attrs.extract_options!
105
- attrs.each do |attr|
106
- build_attribute_methods(attr,options,[:to_s,:to_b])
107
- define_method ("#{attr.to_s}?") do
108
- send("#{attr.to_s}".to_sym).to_s.to_b
109
- end
110
- end
105
+
106
+ def add_defined_attribute(attr,options)
107
+ self.defined_attributes[attr] = options
108
+ define_attribute_methods self.defined_attributes.keys
111
109
  end
112
- alias :has_boolean :has_booleans
113
-
114
- # Creates setter and getter methods for integer attributes
115
- def has_ints(*attrs)
116
- options = attrs.extract_options!
117
- attrs.each do |attr|
118
- build_attribute_methods(attr,options,[:to_i])
110
+
111
+ # builds the setter and getter methods
112
+ def create_attribute_methods(attributes,options)
113
+ unless attributes.blank?
114
+ attributes.each do |attr|
115
+ define_reader_with_options(attr,options)
116
+ define_setter_with_options(attr,options)
117
+ end
119
118
  end
120
119
  end
121
- alias :has_int :has_ints
122
-
123
- # Creates setter and getter methods for currency attributes
124
- # attributes are cast to BigDecimal and rounded to nearest cent
125
- # #Warning, rounding occurs on all sets, so if you need to keep higher prescsion
126
- # use has_decimals
127
- def has_currency(*attrs)
128
- options = attrs.extract_options!
129
- attrs.each do |attr|
130
- build_attribute_methods(attr,options,[:to_s,:to_currency])
131
-
120
+
121
+ def define_reader_with_options(attr,options)
122
+ add_defined_attribute(attr,options)
123
+ options = default_attribute_settings.merge(options) if options[:on_get].blank?
124
+ define_method(attr) do
125
+ if (options.key?(:default) && (!self.initialized?(attr) || (!options[:allow_blank] && self.attributes[attr].blank?)))
126
+ self.attributes[attr] = fetch_default_value(options[:default])
127
+ end
128
+ options[:on_get].call(self,self.attributes[attr])
132
129
  end
133
- end
134
-
135
- def has_decimals(*attrs)
136
- options = attrs.extract_options!
137
- attrs.each do |attr|
138
- build_attribute_methods(attr,options,[:to_f,:to_d])
139
-
130
+ define_method("#{attr.to_s}?") do
131
+ val = self.send(attr)
132
+ if val.respond_to?(:to_b)
133
+ val = val.to_b
134
+ else
135
+ val = !val.blank? if val.respond_to?(:blank?)
136
+ end
137
+ val
140
138
  end
141
139
  end
142
- alias :has_decimal :has_decimals
143
-
144
- # Creates setter and getter methods for float attributes
145
- def has_floats(*attrs)
146
- options = attrs.extract_options!
147
- attrs.each do |attr|
148
- build_attribute_methods(attr,options,[:to_f])
149
-
140
+
141
+ def define_setter_with_options(attr,options)
142
+ add_defined_attribute(attr,options)
143
+ options = default_attribute_settings.merge(options) if (options[:on_set].blank? || options[:after_set].blank?)
144
+ define_method("#{attr.to_s}=") do |val|
145
+ val = fetch_default_value(options[:default]) if (!options[:allow_blank] && options.key?(:default) && val.blank?)
146
+ begin
147
+ val = options[:on_set].call(self,val)
148
+ rescue NoMethodError => e
149
+ raise ArgumentError, "#{val} could not be set for #{attr}: #{e.message}"
150
+ end
151
+ will_change = "#{attr}_will_change!".to_sym
152
+ self.send(will_change) if (self.respond_to?(will_change) && val != self.attributes[attr])
153
+ self.attributes[attr] = val
154
+ options[:after_set].call(self,val) if options[:after_set]
150
155
  end
151
156
  end
152
- alias :has_float :has_floats
157
+
158
+ AVAILABLE_ATTRIBUTE_METHODS = {
159
+ :has_attribute => {:alias => :has_attributes},
160
+ :has_boolean => {:cast_to => :to_b, :alias => :has_booleans},
161
+ :has_currency => {:cast_to => :to_d, :alias => :has_currencies},
162
+ :has_date => {:cast_to => :to_date, :alias => :has_dates},
163
+ :has_decimal => {:cast_to => :to_d, :alias => :has_decimals},
164
+ :has_float => {:cast_to => :to_f, :alias => :has_floats},
165
+ :has_int => {:cast_to => :to_i, :alias => :has_ints},
166
+ :has_time => {:cast_to => :to_time, :alias => :has_times}
167
+ }
153
168
 
154
- # Creates setter and getter methods for date attributes
155
- def has_dates(*attrs)
156
- options = attrs.extract_options!
157
- attrs.each do |attr|
158
- build_attribute_methods(attr,options,[:to_s,:to_date])
159
-
169
+ AVAILABLE_ATTRIBUTE_METHODS.each do |method,method_options|
170
+ define_method(method) do |*attributes|
171
+ options = default_attribute_settings.merge(attributes.extract_options!)
172
+ options[:on_set] = lambda {|obj,val| val.send(method_options[:cast_to]) } if method_options[:cast_to]
173
+ create_attribute_methods(attributes,options)
160
174
  end
175
+ module_eval("alias #{method_options[:alias]} #{method}")
161
176
  end
162
- alias :has_date :has_dates
163
-
164
- # Creates setter and getter methods for time attributes
165
- def has_times(*attrs)
166
- options = attrs.extract_options!
167
- attrs.each do |attr|
168
- build_attribute_methods(attr,options,[:to_s,:to_time])
169
-
170
- end
171
- end
172
- alias :has_time :has_times
177
+ end
178
+
179
+ def self.included(base)
180
+ base.extend(Attributes::ClassMethods)
181
+ base.send(:include, ActiveModel::Dirty) if base.is_a?(Class) # Add Dirty to the class
173
182
  end
174
183
  end
175
- end
184
+ end
@@ -18,19 +18,22 @@ module SimpleModel
18
18
 
19
19
  # == SimpleModel::Base
20
20
  #
21
- # Provides an interface for any class to build tabless models.
21
+ # Provides an interface for any class to build tableless models.
22
22
  #
23
23
  # Implements Validations, Callbacks and Dirty from ActiveModel, and datatype specific
24
- # attribute definitions with default options
24
+ # attribute definitions with default options. SimpleModel::Base is intended as
25
+ # an example, while it may be used in production, which it is on many of my apps
26
+ # today, it is recommend you use SimpleModel::Base as an example to implement your
27
+ # own model actions.
25
28
  #
26
29
  # == SimpleModel Actions:
27
30
  #
28
31
  # Model actions provide a tool for making use of Active Model callbacks. Each
29
32
  # action creates an instance method representing the action, which calls the
30
- # method(s) listed as symbolswhen defining the actions. Model actions also accept
33
+ # method(s) listed as symbols when defining the actions. Model actions also accept
31
34
  # a rollback option, which is called if the action fails. If you plan to
32
- # implement SimpleModel's actions make avoid naming you own methods "save", "destory",
33
- # "create", and "update", as this will override the methods defined by action.
35
+ # implement SimpleModel's actions, avoid naming you own methods "save", "destroy",
36
+ # "create", and "update", as these will override the methods defined by action.
34
37
  #
35
38
  # Available Actions:
36
39
  # # save
@@ -53,7 +56,7 @@ module SimpleModel
53
56
  # has_integers :first_int, :second_int, :default => 1
54
57
  # has_times :now, :default => :get_now
55
58
  #
56
- # save :save_record, :rollback => :rollback
59
+ # save :save_record, :rollback => :rollback_save
57
60
  #
58
61
  # def save_record
59
62
  # puts "saved"
@@ -64,37 +67,25 @@ module SimpleModel
64
67
  # Time.now
65
68
  # end
66
69
  #
67
- # def rollback
70
+ # def rollback_save
68
71
  # puts "rolled back"
69
72
  # end
70
73
  # end
71
- #
72
- #
73
- #
74
74
 
75
75
  class Base
76
76
  include SimpleModel::Attributes
77
77
  include SimpleModel::ErrorHelpers
78
-
79
78
  #Use ActiveModel Resources
80
79
  include ActiveModel::Validations
81
80
  include ActiveModel::Conversion
82
81
  extend ActiveModel::Naming
83
82
  extend ActiveModel::Callbacks
84
83
  include ActiveModel::Validations::Callbacks
85
- include ActiveModel::Dirty
86
84
 
87
85
  define_model_callbacks :save, :update, :create, :destroy
88
86
 
89
87
  class << self
90
88
 
91
- # Collect methods as they are defined, then add to define_attribute_methods
92
- def after_attribute_definition(method)
93
- @defined_attribute_methods ||= []
94
- @defined_attribute_methods << method
95
- define_attribute_methods @defined_attribute_methods
96
- end
97
-
98
89
  def save(*methods)
99
90
  define_model_action(methods,:save)
100
91
  end
@@ -107,54 +98,48 @@ module SimpleModel
107
98
  define_model_action(methods,:update)
108
99
  end
109
100
 
110
- #Destroy does not run normal validation by default.
101
+ # Destroy does not run normal validation in Rails, but with this we can if we choose to.
111
102
  def destroy(*methods)
112
103
  define_model_action(methods,:destroy, {:validate => false})
113
104
  end
114
105
  end
115
106
 
116
- has_boolean :saved
107
+ has_boolean :persisted
117
108
  has_boolean :new_record, :default => true
118
- attr_accessor :id
119
-
120
- def persisted?
121
- saved?
122
- end
123
-
124
- def before_attribute_set(method,val)
125
- change_methods_str = "#{method.to_s}_will_change!".to_sym
126
- send(change_methods_str) if val != instance_variable_get("@#{method.to_s}") && self.respond_to?(change_methods_str.to_sym)
127
- end
128
-
109
+ has_attribute :id # may not be an integer
110
+ alias :saved? :persisted?
111
+
129
112
  private
130
113
 
131
114
  # Skeleton for action instance methods
132
115
  def run_model_action(methods,options)
133
116
  completed = true
134
- if !options[:validate] ||
135
- (options[:validation_methods] && valid_using_other?(options[:validation_methods])) ||
136
- self.valid?
137
-
117
+ if (!options[:validate] || (options[:validation_methods] && valid_using_other?(options[:validation_methods])) || self.valid?)
138
118
  methods.each do |method|
139
119
  ran = self.send(method)
140
120
  completed = ran unless ran
141
121
  end
142
-
143
122
  if completed
144
- self.saved = true
123
+ self.persisted = true
145
124
  @previously_changed = changes
146
125
  @changed_attributes.clear
147
126
  else
148
127
  self.send(options[:rollback]) unless options[:rollback].blank?
149
128
  end
150
- else
129
+ else
151
130
  completed = false
152
- end
131
+ end
132
+ if !completed && options[:raise_exception]
133
+ if !self.errors.blank?
134
+ raise ValidationError, self.errors.full_messages.join(" ")
135
+ else
136
+ raise ActionError, "failed action: #{methods.join(', ')}"
137
+ end
138
+ end
153
139
  completed
154
- end
155
-
140
+ end
156
141
 
157
- # Run supplied methods as valdation. Each method should return a boolean
142
+ # Run supplied methods as validation. Each method should return a boolean
158
143
  # If using this option, to see if errors are present use object_name.errors.blank?,
159
144
  # otherwise if you run object_name.valid? you will over write the errors
160
145
  # generated here.
@@ -164,17 +149,21 @@ module SimpleModel
164
149
  valid = false unless self.send(method)
165
150
  end
166
151
  valid
167
- end
168
-
152
+ end
169
153
 
170
- # Defines the model action's instantace methods and applied defaults.
171
- # Defines methods with :validate options as true by default.
154
+ # Defines the model action's instance methods and applied defaults. For every
155
+ # action defined, we also define that actions ! method which raises exceptions
156
+ # when the action fails.
172
157
  def self.define_model_action(methods,action,default_options={:validate => true})
173
158
  default_options.merge!(methods.extract_options!)
174
- define_method(action) do |opts={}|
175
- options = default_options.merge(opts)
176
- self.run_callbacks(action) do
177
- run_model_action(methods,options)
159
+ actions = [action,"#{action}!".to_sym]
160
+ actions.each do |a|
161
+ define_method(a) do |opts={}|
162
+ options = default_options.merge(opts)
163
+ options[:raise_exception] = a.to_s.match(/\!$/)
164
+ self.run_callbacks(action) do
165
+ run_model_action(methods,options)
166
+ end
178
167
  end
179
168
  end
180
169
  end
@@ -0,0 +1,5 @@
1
+ module SimpleModel
2
+ class ActionError < StandardError; end
3
+ class ValidationError < StandardError; end
4
+ class ArgumentError < StandardError; end
5
+ end