virtus 0.0.1

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.
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