virtus 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.gitignore +4 -0
  2. data/.rvmrc +1 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +7 -0
  5. data/LICENSE +20 -0
  6. data/README.markdown +83 -0
  7. data/Rakefile +27 -0
  8. data/VERSION +1 -0
  9. data/lib/virtus.rb +61 -0
  10. data/lib/virtus/attributes/array.rb +8 -0
  11. data/lib/virtus/attributes/attribute.rb +214 -0
  12. data/lib/virtus/attributes/boolean.rb +39 -0
  13. data/lib/virtus/attributes/date.rb +44 -0
  14. data/lib/virtus/attributes/date_time.rb +43 -0
  15. data/lib/virtus/attributes/decimal.rb +24 -0
  16. data/lib/virtus/attributes/float.rb +20 -0
  17. data/lib/virtus/attributes/hash.rb +8 -0
  18. data/lib/virtus/attributes/integer.rb +20 -0
  19. data/lib/virtus/attributes/numeric.rb +9 -0
  20. data/lib/virtus/attributes/object.rb +8 -0
  21. data/lib/virtus/attributes/string.rb +11 -0
  22. data/lib/virtus/attributes/time.rb +45 -0
  23. data/lib/virtus/attributes/typecast/numeric.rb +32 -0
  24. data/lib/virtus/attributes/typecast/time.rb +27 -0
  25. data/lib/virtus/class_methods.rb +60 -0
  26. data/lib/virtus/instance_methods.rb +80 -0
  27. data/lib/virtus/support/chainable.rb +15 -0
  28. data/spec/integration/virtus/class_methods/attribute_spec.rb +63 -0
  29. data/spec/integration/virtus/class_methods/attributes_spec.rb +24 -0
  30. data/spec/integration/virtus/class_methods/const_missing_spec.rb +44 -0
  31. data/spec/spec_helper.rb +20 -0
  32. data/spec/unit/shared/attribute.rb +157 -0
  33. data/spec/unit/virtus/attributes/array_spec.rb +9 -0
  34. data/spec/unit/virtus/attributes/attribute_spec.rb +13 -0
  35. data/spec/unit/virtus/attributes/boolean_spec.rb +97 -0
  36. data/spec/unit/virtus/attributes/date_spec.rb +52 -0
  37. data/spec/unit/virtus/attributes/date_time_spec.rb +65 -0
  38. data/spec/unit/virtus/attributes/decimal_spec.rb +98 -0
  39. data/spec/unit/virtus/attributes/float_spec.rb +98 -0
  40. data/spec/unit/virtus/attributes/hash_spec.rb +9 -0
  41. data/spec/unit/virtus/attributes/integer_spec.rb +98 -0
  42. data/spec/unit/virtus/attributes/numeric/class_methods/descendants_spec.rb +15 -0
  43. data/spec/unit/virtus/attributes/object/class_methods/descendants_spec.rb +16 -0
  44. data/spec/unit/virtus/attributes/string_spec.rb +20 -0
  45. data/spec/unit/virtus/attributes/time_spec.rb +71 -0
  46. data/spec/unit/virtus/class_methods/new_spec.rb +41 -0
  47. data/spec/unit/virtus/determine_type_spec.rb +20 -0
  48. data/spec/unit/virtus/instance_methods/attribute_get_spec.rb +22 -0
  49. data/spec/unit/virtus/instance_methods/attribute_set_spec.rb +30 -0
  50. data/spec/unit/virtus/instance_methods/attributes_spec.rb +37 -0
  51. data/virtus.gemspec +95 -0
  52. metadata +131 -0
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use 1.9.2@virtus
@@ -0,0 +1,6 @@
1
+ script: "bundle exec rspec spec/"
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - jruby
6
+ - rbx
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "jeweler", "~> 1.5.2"
5
+ gem "rspec", "~> 2.6.0"
6
+ gem "simplecov", "~> 0.4.2", :platforms => [ :mri_19 ]
7
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Piotr Solnica
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,83 @@
1
+ # Virtus [![Build Status](http://travis-ci.org/solnic/virtus.png)](http://travis-ci.org/solnic/virtus)
2
+
3
+ This is a partial extraction of the DataMapper [Property
4
+ API](http://rubydoc.info/github/datamapper/dm-core/master/DataMapper/Property)
5
+ with various modifications. My goal is to provide a common API to define
6
+ attributes on a model along with (auto-)validations so all ORMs/ODMs could use
7
+ it instead of reinventing the wheel all over again. It would be also suitable
8
+ for any other usecase where you need to extend your ruby objects with various
9
+ attributes that require typecasting and/or validations.
10
+
11
+ ## Installation
12
+
13
+ gem i virtus
14
+
15
+ ## Basic Usage
16
+
17
+ require 'virtus'
18
+
19
+ class User
20
+ include Virtus
21
+
22
+ attribute :name, String
23
+ attribute :age, Integer
24
+ attribute :birthday, DateTime
25
+ end
26
+
27
+ # setting attributes in the constructor
28
+ user = User.new(:age => 28)
29
+
30
+ # attribute readers
31
+ user.name # => "Piotr"
32
+
33
+ # hash of attributes
34
+ user.attributes # => { :name => "Piotr" }
35
+
36
+ # automatic typecasting
37
+ user.age = '28'
38
+ user.age # => 28
39
+
40
+ user.birthday = 'November 18th, 1983'
41
+ user.birthday # => #<DateTime: 1983-11-18T00:00:00+00:00 (4891313/2,0/1,2299161)>
42
+
43
+ ## Custom Attributes
44
+
45
+ require 'virtus'
46
+ require 'json'
47
+
48
+ module MyApp
49
+ module Attributes
50
+ class JSON < Virtus::Attributes::Object
51
+ primitive Hash
52
+
53
+ def typecast(value, model = nil)
54
+ ::JSON.parse(value)
55
+ end
56
+ end
57
+ end
58
+
59
+ class User
60
+ include Virtus
61
+
62
+ attribute :info, Attributes::JSON
63
+ end
64
+ end
65
+
66
+ user = MyApp::User.new
67
+
68
+ user.info = '{"email" : "john@domain.com" }'
69
+ user.info # => {"email"=>"john@domain.com"}
70
+
71
+ ## Note on Patches/Pull Requests
72
+
73
+ * Fork the project.
74
+ * Make your feature addition or bug fix.
75
+ * Add tests for it. This is important so I don't break it in a
76
+ future version unintentionally.
77
+ * Commit, do not mess with rakefile, version, or history.
78
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
79
+ * Send me a pull request. Bonus points for topic branches.
80
+
81
+ ## Copyright
82
+
83
+ Copyright (c) 2011 Piotr Solnica. See LICENSE for details.
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'jeweler'
4
+ require 'rspec/core/rake_task'
5
+
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "virtus"
8
+ gem.platform = Gem::Platform::RUBY
9
+ gem.authors = ["Piotr Solnica"]
10
+ gem.email = ["piotr@rubyverse.com"]
11
+ gem.homepage = "https://github.com/solnic/virtus"
12
+ gem.summary = %q{Attributes for your plain ruby objects}
13
+ gem.description = gem.summary
14
+
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ gem.require_paths = ["lib"]
19
+ end
20
+
21
+ Jeweler::GemcutterTasks.new
22
+
23
+ desc "Run specs"
24
+ RSpec::Core::RakeTask.new
25
+
26
+ desc 'Default: run specs.'
27
+ task :default => :spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,61 @@
1
+ require 'pathname'
2
+ require 'set'
3
+ require 'date'
4
+ require 'time'
5
+ require 'bigdecimal'
6
+ require 'bigdecimal/util'
7
+
8
+ module Virtus
9
+ module Undefined; end
10
+
11
+ class << self
12
+ # Extends base class with Attributes and Chainable modules
13
+ #
14
+ # @param [Object] base
15
+ #
16
+ # @api private
17
+ def included(base)
18
+ base.extend(ClassMethods)
19
+ base.send(:include, InstanceMethods)
20
+ base.extend(Support::Chainable)
21
+ end
22
+
23
+ # Returns a Virtus::Attributes::Object sub-class based on a name or class.
24
+ #
25
+ # @param [Class,String] class_or_name
26
+ # name of a class or a class itself
27
+ #
28
+ # @return [Class]
29
+ # one of the Virtus::Attributes::Object sub-class
30
+ #
31
+ # @api semipublic
32
+ def determine_type(class_or_name)
33
+ if class_or_name.is_a?(Class) && class_or_name < Attributes::Object
34
+ class_or_name
35
+ elsif Attributes.const_defined?(name = class_or_name.to_s)
36
+ Attributes.const_get(name)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ dir = Pathname(__FILE__).dirname.expand_path
43
+
44
+ require dir + 'virtus/support/chainable'
45
+ require dir + 'virtus/class_methods'
46
+ require dir + 'virtus/instance_methods'
47
+ require dir + 'virtus/attributes/typecast/numeric'
48
+ require dir + 'virtus/attributes/typecast/time'
49
+ require dir + 'virtus/attributes/attribute'
50
+ require dir + 'virtus/attributes/object'
51
+ require dir + 'virtus/attributes/array'
52
+ require dir + 'virtus/attributes/boolean'
53
+ require dir + 'virtus/attributes/date'
54
+ require dir + 'virtus/attributes/date_time'
55
+ require dir + 'virtus/attributes/numeric'
56
+ require dir + 'virtus/attributes/decimal'
57
+ require dir + 'virtus/attributes/float'
58
+ require dir + 'virtus/attributes/hash'
59
+ require dir + 'virtus/attributes/integer'
60
+ require dir + 'virtus/attributes/string'
61
+ require dir + 'virtus/attributes/time'
@@ -0,0 +1,8 @@
1
+ module Virtus
2
+ module Attributes
3
+ class Array < Object
4
+ primitive ::Array
5
+ complex true
6
+ end # Integer
7
+ end # Attributes
8
+ end # Virtus
@@ -0,0 +1,214 @@
1
+ module Virtus
2
+ module Attributes
3
+ class Attribute
4
+ attr_reader :name, :model, :options, :instance_variable_name,
5
+ :reader_visibility, :writer_visibility
6
+
7
+ OPTIONS = [ :primitive, :complex, :accessor, :reader, :writer ].freeze
8
+
9
+ DEFAULT_ACCESSOR = :public.freeze
10
+
11
+ class << self
12
+ # Returns an array of valid options
13
+ #
14
+ # @return [Array]
15
+ # the array of valid option names
16
+ #
17
+ # @api public
18
+ def accepted_options
19
+ @accepted_options ||= []
20
+ end
21
+
22
+ # Defines which options are valid for a given attribute class.
23
+ #
24
+ # Example:
25
+ #
26
+ # class MyAttribute < Virtus::Attributes::Object
27
+ # accept_options :foo, :bar
28
+ # end
29
+ #
30
+ # @api public
31
+ def accept_options(*args)
32
+ accepted_options.concat(args)
33
+
34
+ # create methods for each new option
35
+ args.each do |attribute_option|
36
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
37
+ def self.#{attribute_option}(value = Undefined) # def self.unique(value = Undefined)
38
+ return @#{attribute_option} if value.equal?(Undefined) # return @unique if value.equal?(Undefined)
39
+ @#{attribute_option} = value # @unique = value
40
+ end # end
41
+ RUBY
42
+ end
43
+
44
+ descendants.each { |descendant| descendant.accepted_options.concat(args) }
45
+ end
46
+
47
+ # Returns all the descendant classes
48
+ #
49
+ # @return [Array]
50
+ # the array of descendants
51
+ #
52
+ # @api public
53
+ def descendants
54
+ @descendants ||= []
55
+ end
56
+
57
+ # Returns default options hash for a give attribute class.
58
+ #
59
+ # @return [Hash]
60
+ # a hash of default option values
61
+ #
62
+ # @api public
63
+ def options
64
+ options = {}
65
+ accepted_options.each do |method|
66
+ value = send(method)
67
+ options[method] = value unless value.nil?
68
+ end
69
+ options
70
+ end
71
+
72
+ # Adds descendant to descendants array and inherits default options
73
+ #
74
+ # @api private
75
+ def inherited(descendant)
76
+ descendants << descendant
77
+ descendant.accepted_options.concat(accepted_options)
78
+ options.each { |key, value| descendant.send(key, value) }
79
+ end
80
+ end
81
+
82
+ accept_options *OPTIONS
83
+
84
+ # Returns if an attribute is a complex one.
85
+ #
86
+ # @return [TrueClass, FalseClass]
87
+ #
88
+ # @api semipublic
89
+ def complex?
90
+ options[:complex]
91
+ end
92
+
93
+ # Initializes an attribute instance
94
+ #
95
+ # @param [Symbol] name
96
+ # the name of an attribute
97
+ #
98
+ # @param [Class] model
99
+ # the object's class
100
+ #
101
+ # @param [Hash] options
102
+ # hash of extra options which overrides defaults set on an attribute class
103
+ #
104
+ # @api private
105
+ def initialize(name, model, options = {})
106
+ @name = name
107
+ @model = model
108
+ @options = self.class.options.merge(options).freeze
109
+
110
+ @instance_variable_name = "@#{@name}".freeze
111
+
112
+ default_accessor = @options.fetch(:accessor, DEFAULT_ACCESSOR)
113
+ @reader_visibility = @options.fetch(:reader, default_accessor)
114
+ @writer_visibility = @options.fetch(:writer, default_accessor)
115
+
116
+ _create_reader
117
+ _create_writer
118
+ end
119
+
120
+ # Returns if the given value's class is an attribute's primitive
121
+ #
122
+ # @return [TrueClass, FalseClass]
123
+ #
124
+ # @api private
125
+ def primitive?(value)
126
+ value.kind_of?(self.class.primitive)
127
+ end
128
+
129
+ # Converts the given value to the primitive type unless it's already
130
+ # the primitive or nil
131
+ #
132
+ # @param [Object] value
133
+ # the value
134
+ #
135
+ # @api private
136
+ def typecast(value, model = nil)
137
+ if value.nil? || primitive?(value)
138
+ value
139
+ else
140
+ typecast_to_primitive(value)
141
+ end
142
+ end
143
+
144
+ # Converts the given value to the primitive type
145
+ #
146
+ # @api private
147
+ def typecast_to_primitive(value, model)
148
+ value
149
+ end
150
+
151
+ # Returns value of an attribute for the given model
152
+ #
153
+ # @api private
154
+ def get(model)
155
+ get!(model)
156
+ end
157
+
158
+ # Returns the instance variable of the attribute
159
+ #
160
+ # @api private
161
+ def get!(model)
162
+ model.instance_variable_get(instance_variable_name)
163
+ end
164
+
165
+ # Sets the value on the model
166
+ #
167
+ # @api private
168
+ def set(model, value)
169
+ return if value.nil?
170
+ set!(model, primitive?(value) ? value : typecast(value, model))
171
+ end
172
+
173
+ # Sets instance variable of the attribute
174
+ #
175
+ # @api private
176
+ def set!(model, value)
177
+ model.instance_variable_set(instance_variable_name, value)
178
+ end
179
+
180
+ # Creates an attribute reader method
181
+ #
182
+ # @api private
183
+ def _create_reader
184
+ model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
185
+ chainable(:attribute) do
186
+ #{reader_visibility}
187
+
188
+ def #{name}
189
+ return #{instance_variable_name} if defined?(#{instance_variable_name})
190
+ attribute = self.class.attributes[#{name.inspect}]
191
+ #{instance_variable_name} = attribute ? attribute.get(self) : nil
192
+ end
193
+ end
194
+ RUBY
195
+
196
+ end
197
+
198
+ # Creates an attribute writer method
199
+ #
200
+ # @api private
201
+ def _create_writer
202
+ model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
203
+ chainable(:attribute) do
204
+ #{writer_visibility}
205
+
206
+ def #{name}=(value)
207
+ self.class.attributes[#{name.inspect}].set(self, value)
208
+ end
209
+ end
210
+ RUBY
211
+ end
212
+ end # Attribute
213
+ end # Attributes
214
+ end # Virtus