modelish 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .yardoc/*
6
+ doc/*
7
+ *~
8
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/.rvmrc ADDED
@@ -0,0 +1,3 @@
1
+ branch_name=`git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'`
2
+ #rvm --create use 1.9.2@modelish-${branch_name}
3
+ rvm --create use 1.8.7@modelish-${branch_name}
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in modelish.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 by Maeve Revels
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,95 @@
1
+ # modelish #
2
+
3
+ When a real modeling framework is too heavy, sometimes you want something just
4
+ a little modelish.
5
+
6
+ If a Hash or OpenStruct almost suits your needs, but you need bits of
7
+ model-like behavior such as simple validation and typed values, modelish can
8
+ help.
9
+
10
+ If you need persistence or anything but the most basic functionality, modelish
11
+ will frustrate you to no end.
12
+
13
+ ## Installation ##
14
+
15
+ For the especially foolhardy, you can:
16
+
17
+ 1. Add this to your Gemfile:
18
+
19
+ gem 'modelish', :git => 'git://github.com/maeve/modelish.git'
20
+
21
+ 2. Execute `bundle install`
22
+
23
+ ## Basics ##
24
+
25
+ The modelish syntax is very similar to some of the classes provided by
26
+ [hashie]. In fact, the initial implementation simply extended
27
+ [Hashie::Trash][trash] to add property types:
28
+
29
+ require 'modelish'
30
+
31
+ class Foo < Modelish::Base
32
+ property :my_date, :type => Date
33
+ property :my_float, :type => Float, :default => 0.0
34
+ property :my_funky_id, :type => Integer, :from => 'MyFUNKYId'
35
+ property :my_simple_property
36
+ end
37
+
38
+ You can then set properties as string value and have them automatically
39
+ converted to the appropriate type, while respecting default values and
40
+ key-mappings:
41
+
42
+ f = Foo.new('MyFUNKYId' => '42',
43
+ :my_date => '2011-03-10',
44
+ :my_simple_property => 'bar')
45
+
46
+ f.my_date
47
+ => #<Date: 2011-03-10 (4911261/2,0,2299161)>
48
+ f.my_float
49
+ => 0.0
50
+ f.my_funky_id
51
+ => 42
52
+ f.my_simple_property
53
+ => "bar"
54
+
55
+ modelish also supports defining simple property validations:
56
+
57
+ class Bar < Modelish::Base
58
+ property :important_field, :required => true
59
+ property :state, :max_length => 2
60
+ property :my_int, :type => Integer, :validate_type => true
61
+ property :another_field, :validator => lambda { |val| "val must respond to []" unless val.respond_to?(:[]) }
62
+ end
63
+
64
+ Validations can be run using methods that return an error map (keyed on property name), raise errors, or return a boolean value to indicate validation outcome.
65
+
66
+ valid_bar = Bar.new(:important_field => 'some value',
67
+ :state => 'OR',
68
+ :my_int => 42,
69
+ :another_field => Hash.new)
70
+ valid_bar.valid?
71
+ => true
72
+
73
+ valid_bar.validate
74
+ => {}
75
+
76
+ valid_bar.validate!
77
+ => nil
78
+
79
+
80
+ invalid_bar = Bar.new(:state => 'a value that is too long',
81
+ :my_int => 'this is not an integer',
82
+ :another_field => Object.new)
83
+ invalid_bar.valid?
84
+ => false
85
+
86
+ invalid_bar.validate
87
+ => {:important_field=>[#<ArgumentError: important_field must not be nil or blank>], :my_int=>[#<ArgumentError: my_int must be of type Integer, but got "this is not an integer">], :another_field=>[#<ArgumentError: val must respond to []>], :state=>[#<ArgumentError: state must be less than 2 characters>]}
88
+
89
+ invalid_bar.validate!
90
+ ArgumentError: important_field must not be nil or blank
91
+ from /Users/maeverevels/projects/modelish/lib/modelish/validations.rb:31:in `validate!'
92
+ ...
93
+
94
+ [hashie]: https://github.com/intridea/hashie
95
+ [trash]: http://rdoc.info/github/intridea/hashie/master/Hashie/Trash
@@ -0,0 +1,17 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
8
+
9
+ namespace :doc do
10
+ require 'yard'
11
+ YARD::Rake::YardocTask.new do |task|
12
+ task.files = ['README.md', 'lib/**/*.rb']
13
+ task.options = [
14
+ '--markup', 'markdown',
15
+ ]
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ $:.unshift(File.expand_path('modelish',File.dirname(__FILE__)))
2
+
3
+ require 'version'
4
+ require 'base'
5
+
6
+ module Modelish
7
+ # Your code goes here...
8
+ end
@@ -0,0 +1,44 @@
1
+ require 'hashie'
2
+ require 'property_types'
3
+ require 'validations'
4
+
5
+ module Modelish
6
+ class Base < Hashie::Trash
7
+ include PropertyTypes
8
+ include Validations
9
+
10
+ # Creates a new attribute.
11
+ #
12
+ # @param [Symbol] name the name of the property
13
+ # @param [Hash] options configuration for the property
14
+ # @option opts [Object] :default the default value for this property
15
+ # when the value has not been explicitly
16
+ # set (defaults to nil)
17
+ # @option opts [#to_s] :from the original key name for this attribute
18
+ # (created as write-only)
19
+ # @option opts [Class,Proc] :type the type of the property value. For
20
+ # a list of accepted types, see
21
+ # {Modelish::PropertyTypes}
22
+ # @options opts [true,false] :required enables validation for the property
23
+ # value's presence; nil or blank values
24
+ # will cause validation methods to fail
25
+ # @options opts [Integer] :max_length the maximum allowable length for a valid
26
+ # property value
27
+ # @options opts [true,false] :validate_type enables validation for the property value's
28
+ # type based on the :type option
29
+ # @options opts [Proc] :validator A block that accepts a value and validates it;
30
+ # should return nil if validation passes, or an error
31
+ # message or error object if validation fails.
32
+ # See {Modelish::Validations}
33
+ def self.property(name, options={})
34
+ super
35
+
36
+ add_property_type(name, options[:type]) if options[:type]
37
+
38
+ add_validator(name) { |val| validate_required(name => val).first } if options[:required]
39
+ add_validator(name) { |val| validate_length(name, val, options[:max_length]) } if options[:max_length]
40
+ add_validator(name, &options[:validator]) if options[:validator]
41
+ add_validator(name) { |val| validate_type(name, val, options[:type]) } if options[:validate_type]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,133 @@
1
+ require 'date'
2
+
3
+ module Modelish
4
+ # Mixes in behavior for automatically converting property types.
5
+ module PropertyTypes
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ # Adds a typed property to the model.
12
+ # This dynamically generates accessor/mutator methods that perform
13
+ # the appropriate type conversions on the property's value.
14
+ #
15
+ # Generated methods:
16
+ # * +<property_name>=(new_value)+ -- sets the property value.
17
+ # * +<property_name>+ -- returns the property value, converted to the configured
18
+ # type. If the value cannot be converted, no error will be
19
+ # raised, and the raw unconverted value will be returned.
20
+ # * +<property_name>!+ -- returns the property value, converted to the configured
21
+ # type. If the value cannot be converted, a TypeError will
22
+ # be raised.
23
+ # * +raw_<property_name> -- the original property value, without any type conversions.
24
+ #
25
+ # @param [Symbol] property_name the name of the property.
26
+ # @param [Class, Proc] property_type the type of the property's value.
27
+ # Valid types include:
28
+ # * +Integer+
29
+ # * +Float+
30
+ # * +Array+
31
+ # * +Date+ -- converts using Date.parse on value.to_s
32
+ # * +Symbol+ -- also converts from camel case to snake case
33
+ # * +String+
34
+ # * any arbitrary +Class+ -- will attempt conversion by passing the raw
35
+ # value into the class's initializer
36
+ # * an instance of +Proc+ -- will convert the value by executing the proc,
37
+ # passing in the raw value as an argument
38
+ def add_property_type(property_name, property_type=String)
39
+ accessor = property_name.to_sym
40
+ property_types[accessor] = property_type
41
+
42
+ raw_accessor = define_raw_accessor(accessor)
43
+ bang_accessor = define_bang_accessor(accessor)
44
+
45
+ typed_accessor = "_typed_#{accessor}".to_sym
46
+ typed_mutator = "#{typed_accessor}=".to_sym
47
+ to_safe = "_to_safe_#{accessor}".to_sym
48
+
49
+ class_eval do
50
+ attr_accessor typed_accessor
51
+ private typed_accessor, typed_mutator
52
+
53
+ define_method(to_safe) do
54
+ self.send(bang_accessor) rescue self.send(raw_accessor)
55
+ end
56
+ private to_safe
57
+
58
+ define_method(accessor) do
59
+ val = self.send(typed_accessor)
60
+
61
+ unless val || self.send(raw_accessor).nil?
62
+ val = self.send(to_safe)
63
+ self.send(typed_mutator, val)
64
+ end
65
+
66
+ val
67
+ end
68
+
69
+ define_method("#{accessor}=") do |val|
70
+ self.send("#{raw_accessor}=", val)
71
+ self.send(typed_mutator, self.send(to_safe))
72
+ end
73
+ end
74
+ end
75
+
76
+ def property_types
77
+ @property_types ||= {}
78
+ end
79
+
80
+ private
81
+ def define_raw_accessor(name)
82
+ accessor = name.to_sym
83
+ raw_accessor = "raw_#{name}".to_sym
84
+
85
+ mutator = "#{name}=".to_sym
86
+ raw_mutator = "raw_#{name}=".to_sym
87
+
88
+ class_eval do
89
+ if method_defined?(accessor) && method_defined?(mutator)
90
+ alias_method(raw_accessor, accessor)
91
+ alias_method(raw_mutator, mutator)
92
+ else
93
+ attr_accessor raw_accessor
94
+ end
95
+ end
96
+
97
+ raw_accessor
98
+ end
99
+
100
+ def define_bang_accessor(property_name)
101
+ bang_accessor = "#{property_name}!".to_sym
102
+ converter = value_converter(property_types[property_name.to_sym])
103
+
104
+ class_eval do
105
+ define_method(bang_accessor) do
106
+ value = self.send("raw_#{property_name}")
107
+ (converter && value) ? converter.call(value) : value
108
+ end
109
+ end
110
+
111
+ bang_accessor
112
+ end
113
+
114
+ def value_converter(property_type)
115
+ if property_type == Date
116
+ lambda { |val| Date.parse(val.to_s) }
117
+ elsif property_type == Symbol
118
+ lambda { |val| val.to_s.strip.gsub(/([A-Z]+)([A-Z][a-z])/, '\\1_\\2').
119
+ gsub(/([a-z\d])([A-Z])/, '\\1_\\2').
120
+ gsub(/\s+|-/,'_').downcase.to_sym }
121
+ elsif property_type == String
122
+ lambda { |val| val.to_s.strip }
123
+ elsif Kernel.respond_to?(property_type.to_s)
124
+ lambda { |val| Kernel.send(property_type.to_s, val) }
125
+ elsif property_type.respond_to?(:call)
126
+ property_type
127
+ else
128
+ lambda { |val| property_type.new(val) }
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,208 @@
1
+ require 'date'
2
+
3
+ module Modelish
4
+ module Validations
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ # Validates all properties based on configured validators.
10
+ #
11
+ # @return [Hash<Symbol,Array>] map of errors where key is the property name
12
+ # and value is the list of errors
13
+ # @see ClassMethods#add_validator
14
+ def validate
15
+ errors = {}
16
+
17
+ call_validators do |name,message|
18
+ errors[name] ||= []
19
+ errors[name] << to_error(message)
20
+ end
21
+
22
+ errors
23
+ end
24
+
25
+ # Validates all properties based on configured validators.
26
+ #
27
+ # @raise ArgumentError when any property fails validation
28
+ def validate!
29
+ call_validators do |name,message|
30
+ error = to_error(message)
31
+ raise error if error
32
+ end
33
+ nil
34
+ end
35
+
36
+ def valid?
37
+ validate.empty?
38
+ end
39
+
40
+ private
41
+ def call_validators(&error_handler)
42
+ self.class.validators.each_pair do |k,v|
43
+ v.each do |prop_validator|
44
+ if msg = prop_validator.call(self.send(k))
45
+ error_handler.call(k, msg)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def to_error(msg)
52
+ msg.is_a?(StandardError) ? msg : ArgumentError.new(msg.to_s)
53
+ end
54
+
55
+ module ClassMethods
56
+ # Sets up a block containing validation logic for a given property.
57
+ # Each property may have 0 or more validators.
58
+ #
59
+ # @param [String,Symbol] property_name the name of the property to validate
60
+ # @param [#call] validator the block containing the validation logic; must return
61
+ # either an error object or a string containing the error
62
+ # message if validation fails.
63
+ #
64
+ # @example adding a validator that only allows non-nil values
65
+ # class MyModel
66
+ # include Modelish::Validations
67
+ # attr_accessor :foo
68
+ # add_validator(:foo) { |val| val.nil? ? 'foo must not be nil': nil }
69
+ # end
70
+ def add_validator(property_name, &validator)
71
+ validators[property_name.to_sym] ||= []
72
+ validators[property_name.to_sym] << validator
73
+
74
+ class_eval do
75
+ attr_accessor property_name.to_sym unless method_defined?(property_name.to_sym)
76
+ end
77
+ end
78
+
79
+ # A map of registered validator blocks, keyed on property_name.
80
+ def validators
81
+ @validators ||= {}
82
+ end
83
+
84
+ # Validates the required values, returning a list of errors when validation
85
+ # fails.
86
+ #
87
+ # @param [Hash] args the map of name => value pairs to validate
88
+ # @return [Array<ArgumentError>] a list of ArgumentErrors for validation failures.
89
+ def validate_required(args)
90
+ errors = []
91
+ args.each do |name, value|
92
+ errors << ArgumentError.new("#{name} must not be nil or blank") if value.nil? || value.to_s.strip.empty?
93
+ end
94
+ errors
95
+ end
96
+
97
+ # Validates the required values, raising an error when validation fails.
98
+ #
99
+ # @param [Hash] args the map of name => value pairs to validate
100
+ # @raise [ArgumentError] when any value in args hash is nil or empty. The name
101
+ # key will be used to construct an informative error message.
102
+ def validate_required!(args)
103
+ errors = validate_required(args)
104
+ raise errors.first unless errors.empty?
105
+ end
106
+
107
+ # Validates the required values, returning a boolean indicating validation success.
108
+ #
109
+ # @param [Hash] args the map of name => value pairs to validate
110
+ # @return [true,false] true when validation passes; false when validation fails
111
+ def validate_required?(args)
112
+ validate_required(args).empty?
113
+ end
114
+
115
+ # Validates the length of a value, raising an error to indicate validation failure.
116
+ #
117
+ # @param (see .validate_length)
118
+ # @raise [ArgumentError] when the value is longer than max_length
119
+ def validate_length!(name, value, max_length)
120
+ error = validate_length(name, value, max_length)
121
+ raise error if error
122
+ end
123
+
124
+ # Validates the length of a value, returning a boolean to indicate validation
125
+ # success.
126
+ #
127
+ # @param (see .validate_length)
128
+ # @return [true,false] true if value does not exceed max_length; false otherwise
129
+ def validate_length?(name, value, max_length)
130
+ validate_length(name, value, max_length).nil?
131
+ end
132
+
133
+ # Validates the length of a value, returning an error when validation fails.
134
+ #
135
+ # @param [Symbol,String] name the name of the property/argument to be validated
136
+ # @param [#length] value the value to be validated
137
+ # @param [#to_i] max_length the maximum allowable length
138
+ # @raise [ArgumentError] when the value is longer than max_length
139
+ def validate_length(name, value, max_length)
140
+ if max_length.to_i > 0 && value.to_s.length > max_length.to_i
141
+ ArgumentError.new("#{name} must be less than #{max_length} characters")
142
+ end
143
+ end
144
+
145
+ # Validates the type of the value, returning a boolean indicating validation
146
+ # outcome.
147
+ #
148
+ # @see #validate_type
149
+ # @param {see #validate_type}
150
+ # @return [true,false] true when the value is the correct type; false otherwise
151
+ def validate_type?(name, value, type)
152
+ validate_type(name, value, type).nil?
153
+ end
154
+
155
+ # Validates the type of the value, raising an error when the value is not
156
+ # of the correct type.
157
+ #
158
+ # @see #validate_type
159
+ # @param {see #validate_type}
160
+ # @raise [ArgumentError] when the value is not the correct type
161
+ def validate_type!(name, value, type)
162
+ error = validate_type(name, value, type)
163
+ raise error if error
164
+ end
165
+
166
+ # Validates the type of the value, returning an error when the value cannot
167
+ # be converted to the appropriate type.
168
+ #
169
+ # @param [Symbol,String] name the name of the property/argument to be validated
170
+ # @param [Object] value the value to be validated
171
+ # @param [Class,Proc] type the type of the class to be validated. Supported types include:
172
+ # * +Integer+
173
+ # * +Float+
174
+ # * +Date+
175
+ # * +Symbol+
176
+ # * any arbitrary +Class+ -- validates based on the results of is_a?
177
+ # @return [ArgumentError] when validation fails
178
+ def validate_type(name, value, type)
179
+ error = nil
180
+
181
+ begin
182
+ if value && type
183
+ # Can't use a case statement because of the way === is implemented on some classes
184
+ if type == Integer
185
+ is_valid = (value.is_a?(Integer) || value.to_s =~ /^\-?\d+$/)
186
+ elsif type == Float
187
+ is_valid = (value.is_a?(Float) || value.to_s =~ /^\-?\d+\.?\d*$/)
188
+ elsif type == Date
189
+ is_valid = (value.is_a?(Date) || Date.parse(value))
190
+ elsif type == Symbol
191
+ is_valid = value.respond_to?(:to_sym)
192
+ else
193
+ is_valid = value.is_a?(type)
194
+ end
195
+
196
+ unless is_valid
197
+ error = ArgumentError.new("#{name} must be of type #{type}, but got #{value.inspect}")
198
+ end
199
+ end
200
+ rescue StandardError => e
201
+ error = ArgumentError.new("An error occurred validating #{name} with value #{value.inspect}: #{e.message}")
202
+ end
203
+
204
+ error
205
+ end
206
+ end
207
+ end
208
+ end