reindeer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,6 @@
1
+ RSpec.configure do |config|
2
+ # No monkey patching please.
3
+ config.expect_with :rspec do |c|
4
+ c.syntax = :expect
5
+ end
6
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.0.1
2
+
3
+ * Initial release.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ ruby '1.9.3'
4
+
5
+ group :test do
6
+ gem 'rake', '~> 10'
7
+ gem 'rspec', '~> 2'
8
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.3)
5
+ rake (10.0.3)
6
+ rspec (2.11.0)
7
+ rspec-core (~> 2.11.0)
8
+ rspec-expectations (~> 2.11.0)
9
+ rspec-mocks (~> 2.11.0)
10
+ rspec-core (2.11.1)
11
+ rspec-expectations (2.11.3)
12
+ diff-lcs (~> 1.1.3)
13
+ rspec-mocks (2.11.3)
14
+
15
+ PLATFORMS
16
+ ruby
17
+
18
+ DEPENDENCIES
19
+ rake (~> 10)
20
+ rspec (~> 2)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2013 Dan Brook
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the 'Software'), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # Reindeer - Moose sugar in ruby
2
+
3
+ Takes Ruby's existing OO features and extends them with some sugar
4
+ borrowed from [Moose](http://p3rl.org/Moose).
5
+
6
+ # Installation
7
+
8
+ gem install reindeer
9
+
10
+ # Usage
11
+
12
+ require 'reindeer'
13
+ class Point < Reindeer
14
+ has :x, is: :rw, is_a: Integer
15
+ has :y, is: :rw, is_a: Integer
16
+ end
17
+ class Point3D < Point
18
+ has :z, is: :rw, is_a: Integer
19
+ end
20
+
21
+ # Features
22
+
23
+ These features are supported to a greater or less extent:
24
+
25
+ ## Construction
26
+
27
+ The `build` method can be used where one may have previously used
28
+ `initialize`. It is called after all attributes have been setup so,
29
+ laziness permitting, the object should be in a known state.
30
+
31
+ Another facet of this feature is that each `build` method is called in
32
+ the inheritance chain from most-derived to least.
33
+
34
+ ## Attributes
35
+
36
+ Declared with the alternative syntax `has` they provide
37
+ additional functionality while still remaining pure Ruby attributes
38
+ under the hood.
39
+
40
+ Their values can be passed to the `new` constructor in a hash where
41
+ the symbolic keys map to attributes of the same. When a value is
42
+ specified for a `lazy` attribute it obviates the laziness.
43
+
44
+ The following options are supported:
45
+
46
+ ### is (aka accessors)
47
+
48
+ Available in 3 flavours:
49
+
50
+ * `:ro`
51
+ * `:rw`
52
+ * `:bare`
53
+
54
+ The first two provide accessors like `attr_reader` and
55
+ `attr_{reader,writer}` combined. The third explicitly provides no
56
+ accessors which can be useful when delegators are specified.
57
+
58
+ The default behaviour is `:ro`.
59
+
60
+ ### required (aka required attributes)
61
+
62
+ If specified with a `true` value then the attribute must be specified
63
+ at build time. Additionally `required` attributes can't be `lazy`
64
+ attributes.
65
+
66
+ ### default (aka default attribute values)
67
+
68
+ Can take either a value or something callable (e.g a `Proc`). If a
69
+ value is provided it is `clone`d and if a callable is provided it is
70
+ `call`ed without any arguments. The resulting value is used to
71
+ populate the attribute if it wasn't provided to the constructor at
72
+ object construction time or on first access if the attribute is
73
+ `lazy`.
74
+
75
+ ### lazy (aka lazily evaluated)
76
+
77
+ Expects a `Boolean` and if `true` then the attribute's value isn't
78
+ generated until it is accessed (if at all). If specified the attribute
79
+ *must* also either have a `builder` or `default` specified otherwise an
80
+ `Reindeer::Meta::Attribute::AttributeError` is thrown.
81
+
82
+ ### lazy_build
83
+
84
+ If passed `true` makes the attribute `lazy` and expects a private
85
+ `builder` method of the same name as the attribute, but prefixed with
86
+ `build_`, to be defined e.g given `has :foo, lazy_build: true` the
87
+ private instance method `build_foo` should be defined. In addition
88
+ clearer and predicate methods will be installed with the prefixes
89
+ `clear_` and `has_` respectively e.g `clear_foo!` and `has_foo?`.
90
+
91
+ ### handles (aka delegation methods)
92
+
93
+ Given an array of symbols each one adds an instance method that
94
+ delegates to a method of the same name on the attribute value.
95
+
96
+ ### type_of (aka type constraints)
97
+
98
+ Expects a class that composes the `Reindeer::Role::TypeConstraint`
99
+ role. At the point a value is about set against an attribute it is
100
+ checked against the type constraint, if valid then the value is set if
101
+ not then an `Reindeer::TypeConstraint::Invalid` exception is raised.
102
+
103
+ ## Roles
104
+
105
+ These are implemented in terms of `Module` and act to serve a similar
106
+ purpose. What they provide in addition to `Module` are required
107
+ methods and the attributes described above.
108
+
109
+ To compose a role in a Reindeer class two expressions are required,
110
+ `with` and `meta.compose!`, the former behaves like `include` and the
111
+ latter brings in the role attributes and asserts the existence of any
112
+ required methods e.g
113
+
114
+ module Breakable
115
+ include Reindeer::Role
116
+ has :is_broken, default: -> { false }
117
+ requires :fix!
118
+ end
119
+
120
+ class Egg < Reindeer
121
+ with Breakable
122
+
123
+ def fix!
124
+ throw :no_dice if is_broken
125
+ end
126
+
127
+ meta.compose!
128
+ end
129
+
130
+ The `.does?` method can be used to inspect which roles have been
131
+ consumed e.g `Egg.does?(Breakable) == true`.
132
+
133
+ For further elaboration on the subject of roles see the
134
+ [Moose::Manual::Roles](https://metacpan.org/module/Moose::Manual::Roles)
135
+ documentation.
136
+
137
+ ## Class constraints and Type constraints
138
+
139
+ Given that Ruby has a well established class system one need only
140
+ assert an attribute is of a given (existing) class a Reindeer will go
141
+ to the trouble of asserting that when the attribute value is set e.g
142
+
143
+ class AccountSqlTable < Reindeer
144
+ has :id, is_a: Fixnum
145
+ has :owner, is_a: String
146
+ has :amount, is_a: Float
147
+ # ...
148
+ end
149
+
150
+ However if you need a specific type of class (e.g strings of a certain
151
+ length) then a custom type constraint is needed. These can be defined
152
+ simply by composing the `Reindeer::Role::TypeConstraint` and
153
+ implementing a `verify` method e.g
154
+
155
+ class Varchar255 < Reindeer
156
+ with Reindeer::Role::TypeConstraint
157
+ def verify(v)
158
+ v.length <= 255
159
+ end
160
+ meta.compose!
161
+ end
162
+
163
+ class AccountSqlTable # continued from above
164
+ has :summary, type_of: Varchar255
165
+ end
166
+
167
+ *NB* The distinction between class and type constraints seems apt at this
168
+ point but is by no means set in stone. Hopefully the passage of time
169
+ shall enlighten us on the matter.
170
+
171
+ # Contributing
172
+
173
+ Pull requests welcome.
174
+
175
+ # Author
176
+
177
+ Dan Brook `<dan@broquaint.com>`
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'Default: run specs.'
6
+ task :default => :spec
7
+
8
+ desc "Run specs"
9
+ RSpec::Core::RakeTask.new
data/lib/reindeer.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'reindeer/meta'
2
+ require 'reindeer/role'
3
+ require 'reindeer/role/typeconstraint'
4
+
5
+ class Reindeer
6
+ class << self
7
+ # XXX Support a single name for now i.e has %i[a b] won't work yet.
8
+ def has(name, opts={})
9
+ # XXX Should really do this at this once everything has been
10
+ # defined not up front like this. Don't know of any hooks though :(
11
+ meta.add_attribute(name, opts).install_methods_in(self)
12
+ end
13
+
14
+ def with(role)
15
+ meta.add_role(role)
16
+ end
17
+
18
+ def does?(role)
19
+ meta.all_roles.include? role
20
+ end
21
+
22
+ def inherited(subclass)
23
+ provide_meta subclass
24
+ end
25
+
26
+ def provide_meta(subclass)
27
+ meta = Reindeer::Meta.new(subclass)
28
+ meth = Proc.new { meta }
29
+ klass = class << subclass; self; end
30
+ klass.__send__ :define_method, :meta, meth
31
+ subclass.__send__ :define_method, :meta, meth
32
+ end
33
+ end
34
+
35
+ def initialize(args={})
36
+ meta.setup_attributes(self, args)
37
+ meta.build_all(self, args)
38
+ end
39
+
40
+ def build(args); end
41
+
42
+ def does?(role)
43
+ meta.all_roles.include? role
44
+ end
45
+ end
@@ -0,0 +1,99 @@
1
+ require 'reindeer/meta/attribute'
2
+
3
+ class Reindeer
4
+ class Meta
5
+
6
+ attr_reader :klass
7
+ attr_reader :required_methods
8
+
9
+ def initialize(klass)
10
+ @klass = klass # Hrm, circular? Not a problem with constants?
11
+ # TODO Use a hash
12
+ @attributes = []
13
+ @roles = []
14
+ @required_methods = []
15
+ end
16
+
17
+ def build_all(obj, args)
18
+ to_build = obj.class.ancestors.take_while { |klass|
19
+ klass != Reindeer
20
+ }.reverse
21
+
22
+ (to_build - obj.class.included_modules).each { |klass|
23
+ # TODO assume build is private.
24
+ build = klass.instance_method(:build)
25
+ build.bind(obj).call(args)
26
+ }
27
+ end
28
+
29
+ def compose!
30
+ all_roles.each do |role|
31
+ role.assert_requires klass
32
+ # role.compose_methods! klass
33
+ klass.__send__ :include, role # Blech
34
+ role.role_meta.get_attributes.each do |attr|
35
+ attr.install_methods_in klass
36
+ end
37
+
38
+ get_attributes.push(*role.role_meta.get_attributes)
39
+ end
40
+ end
41
+
42
+ def add_role(role)
43
+ @roles << role
44
+ end
45
+
46
+ def all_roles
47
+ @roles
48
+ end
49
+
50
+ # Not sure if this is the best place for it.
51
+ def add_required_method(method)
52
+ @required_methods << method
53
+ end
54
+
55
+ def get_attributes
56
+ @attributes
57
+ end
58
+
59
+ def get_all_attributes
60
+ all_classes = klass.ancestors.take_while{|k| k!=Reindeer}.select{|c|
61
+ c.class == Class
62
+ }.reverse
63
+ all_classes.collect{|c| c.meta.get_attributes}.flatten
64
+ end
65
+
66
+ def add_attribute(name, opts)
67
+ attr = Reindeer::Meta::Attribute.new(name, opts)
68
+ get_attributes << attr
69
+ return attr
70
+ end
71
+
72
+ def setup_attributes(obj, args)
73
+ for attr in get_all_attributes
74
+ name = attr.name
75
+ if attr.required? and not args.has_key? name
76
+ raise Meta::Attribute::AttributeError,
77
+ "Did not specify required argument '#{name}'"
78
+ end
79
+
80
+ obj.instance_eval do
81
+ if args.has_key?(name)
82
+ attr.set_value_for self, args[name]
83
+ elsif attr.has_default? and not attr.is_lazy?
84
+ attr.set_value_for self, attr.get_default_value
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ def has_attribute?(name)
91
+ get_attributes.any? {|a| a.name == name }
92
+ end
93
+
94
+ def get_attribute(sym)
95
+ sym = sym.sub(/^@/, '').to_sym if sym.is_a?(String)
96
+ get_attributes.select{|a| a.name == sym}.first
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,182 @@
1
+ class Reindeer
2
+ class Meta
3
+ class Attribute
4
+ # Exceptions!
5
+ class AttributeError < StandardError; end
6
+
7
+ attr_reader :name
8
+
9
+ attr_reader :is_ro, :is_rw, :is_bare
10
+
11
+ attr_reader :is_a, :type_of
12
+
13
+ attr_reader :default_value
14
+ attr_reader :lazy_builder, :lazy_build
15
+
16
+ attr_reader :handles
17
+
18
+ def initialize(name, opts)
19
+ @name = name
20
+ process_opts opts
21
+ end
22
+
23
+ def to_var
24
+ "@#{name.to_s}"
25
+ end
26
+
27
+ def install_methods_in(klass)
28
+ install_accessors_in klass
29
+ if lazy_build
30
+ install_clearer_in klass
31
+ install_predicate_in klass
32
+ end
33
+ install_delegators klass if has_handles?
34
+ end
35
+
36
+ def install_accessors_in(klass)
37
+ return if is_bare
38
+
39
+ if is_lazy?
40
+ attr_name = to_var
41
+ builder = lazy_builder
42
+ klass.__send__ :define_method, name, Proc.new {
43
+ if instance_variable_defined? attr_name
44
+ instance_variable_get attr_name
45
+ else
46
+ meta.get_attribute(attr_name).set_value_for(
47
+ self,
48
+ builder.is_a?(Symbol) ? __send__(builder) : builder.call()
49
+ )
50
+ end
51
+ }
52
+ else
53
+ name_sym = name
54
+ klass.class_eval { self.__send__ :attr_reader, name_sym }
55
+ if is_rw
56
+ klass.__send__ :define_method, "#{name}=", Proc.new {|v|
57
+ meta.get_attribute(name_sym).set_value_for(self, v)
58
+ }
59
+ end
60
+ end
61
+ end
62
+
63
+ def install_clearer_in(klass)
64
+ attr_name = to_var
65
+ klass.__send__ :define_method, "clear_#{name}!", Proc.new {
66
+ remove_instance_variable attr_name
67
+ }
68
+ end
69
+
70
+ def install_predicate_in(klass)
71
+ attr_name = to_var
72
+ klass.__send__ :define_method, "has_#{name}?", Proc.new {
73
+ instance_variable_defined? attr_name
74
+ }
75
+ end
76
+
77
+ def install_delegators(klass)
78
+ attr_name = to_var
79
+ handles.each do |method|
80
+ klass.__send__ :define_method, method, Proc.new {|*args|
81
+ instance_variable_get(attr_name).__send__(method, *args)
82
+ }
83
+ end
84
+ end
85
+
86
+ def set_value_for(instance, value)
87
+ if is_a and not value.is_a? is_a
88
+ raise Meta::Attribute::AttributeError,
89
+ "The value for '#{name}' of type '#{value.class}' is not a '#{is_a}'"
90
+ end
91
+ type_of.check_constraint(value) if type_of
92
+ instance.instance_variable_set to_var, value
93
+ end
94
+
95
+ def get_default_value
96
+ default_value.call
97
+ end
98
+
99
+ # Predicates
100
+ def required?
101
+ @required
102
+ end
103
+ # There must be a cleaner way to get a boolean.
104
+ def has_default?
105
+ not default_value.nil?
106
+ end
107
+ def is_lazy?
108
+ not @lazy_builder.nil?
109
+ end
110
+ def has_handles?
111
+ not handles.nil?
112
+ end
113
+
114
+ private
115
+
116
+ def process_opts(opts)
117
+ process_is opts[:is]
118
+
119
+ @required = opts[:required]
120
+ @is_a = opts[:is_a] if opts.has_key?(:is_a)
121
+ @type_of = process_type_of opts[:type_of] if opts.has_key?(:type_of)
122
+ @default_value = process_default opts[:default] if opts.has_key?(:default)
123
+
124
+ process_lazy opts[:lazy], opts if opts.has_key?(:lazy)
125
+
126
+ if opts[:lazy_build]
127
+ raise AttributeError, "Can't have lazy_build and default, pick one!" if has_default?
128
+ @lazy_builder = "build_#{name}".to_sym
129
+ @lazy_build = true
130
+ end
131
+
132
+ raise AttributeError, "Can't be lazy and required, pick one!" if required? and is_lazy?
133
+ process_handles opts[:handles] if opts.has_key?(:handles)
134
+ end
135
+
136
+ def process_is(val)
137
+ case val
138
+ when nil then @is_ro = true # Default behaviour if 'is' isn't specified.
139
+ when :ro then @is_ro = true
140
+ when :rw then @is_rw = true
141
+ when :bare then @is_bare = true
142
+ else raise AttributeError, "Unknown value for is '#{val}'"
143
+ end
144
+ end
145
+
146
+ def process_type_of(type_constraint)
147
+ tc = type_constraint.new # XXX Bleh!
148
+ if tc.respond_to? :verify # TODO Implement .does?
149
+ tc
150
+ elsif type_constraint.class == Class # or something
151
+ Reindeer::TypeConstraint::Class.new(isa)
152
+ else
153
+ raise AttributeError, "Unknown type constraint '#{isa}' for #{name}"
154
+ end
155
+ end
156
+
157
+ # TODO check default is callable.
158
+ def process_default(default)
159
+ if default.is_a?(Proc)
160
+ default
161
+ else
162
+ Proc.new { default.clone }
163
+ end
164
+ end
165
+
166
+ def process_lazy(is_lazy, opts)
167
+ if opts[:builder] and opts[:default]
168
+ raise AttributeError, "Can't use lazy & builder for lazy"
169
+ elsif not opts[:builder] and not opts[:default]
170
+ raise AttributeError, "Must specify lazy or builder for lazy"
171
+ end
172
+
173
+ @lazy_builder = opts[:builder] || process_default(opts[:default])
174
+ end
175
+
176
+ def process_handles(handles)
177
+ raise AttributeError, "Only support an Array of methods is supported for handles" unless handles.is_a?(Array)
178
+ @handles = handles
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,32 @@
1
+ class Reindeer
2
+ module Role
3
+ class RoleError < StandardError; end #ahem
4
+
5
+ def Role.included(mod)
6
+ mod.module_eval {
7
+ class << self;
8
+ meta = Reindeer::Meta.new(self)
9
+ define_method :role_meta, Proc.new { meta }
10
+
11
+ # Make this more composable?
12
+ define_method :has, Proc.new { |name, opts={}|
13
+ role_meta.add_attribute(name, opts)
14
+ }
15
+
16
+ define_method :requires, Proc.new { |method|
17
+ role_meta.add_required_method(method)
18
+ }
19
+
20
+ define_method :assert_requires, Proc.new { |klass|
21
+ not_defined = role_meta.required_methods.select do |meth|
22
+ not klass.instance_methods(false).include?(meth)
23
+ end
24
+
25
+ return if not_defined.empty?
26
+ raise RoleError, "The class '#{klass}' composed '#{self}' but didn't define #{not_defined.join ', '}"
27
+ }
28
+ end
29
+ }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ module Reindeer::Role::TypeConstraint
2
+ include Reindeer::Role
3
+ requires :verify
4
+
5
+ def check_constraint(v)
6
+ raise Reindeer::TypeConstraint::Invalid, error_message_for(v) unless verify(v)
7
+ end
8
+
9
+ def error_message_for(v)
10
+ return "The value '%s' not considered valid by %s" % [v, self.class]
11
+ end
12
+ end
13
+
14
+ class Reindeer
15
+ class TypeConstraint
16
+ class Invalid < StandardError; end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Reindeer
2
+ VERSION = '0.0.1'
3
+ end
data/reindeer.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require "reindeer/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'reindeer'
8
+ s.version = Reindeer::VERSION
9
+
10
+ s.required_ruby_version = ">= 1.9"
11
+
12
+ s.summary = "Moose sugar in Ruby"
13
+ s.description = "Adds featureful attributes, roles and type constraints"
14
+
15
+ s.authors = ["Dan Brook"]
16
+ s.email = 'dan@broquaint.com'
17
+ s.homepage = 'http://github.com/broquaint/reindeer'
18
+
19
+ s.files = `git ls-files`.split("\n") - %w(.rvmrc .gitignore)
20
+ s.test_files = `git ls-files spec`.split("\n")
21
+
22
+ s.add_development_dependency 'rake', '~> 0.9.2'
23
+ s.add_development_dependency 'rspec', '~> 2'
24
+ end
@@ -0,0 +1,222 @@
1
+ require 'reindeer'
2
+
3
+ describe 'Reindeer attributes' do
4
+ it 'should setup a method and initialize an attribute' do
5
+ class FirstOne < Reindeer
6
+ has :abc
7
+ end
8
+ obj = FirstOne.new abc: 'Hello!'
9
+ expect(obj.respond_to? :abc).to be_true
10
+ expect(obj.abc).to eq('Hello!')
11
+ end
12
+
13
+ it 'should setup two methods and initialize one attribute' do
14
+ class SecondOne < Reindeer
15
+ has :foo
16
+ has :bar
17
+ end
18
+ obj = SecondOne.new foo: 'World!'
19
+ expect(obj.respond_to? :foo).to be_true
20
+ expect(obj.respond_to? :bar).to be_true
21
+ expect(obj.foo).to eq('World!')
22
+ expect(obj.bar).to be_nil
23
+ end
24
+
25
+ it 'should honour is option' do
26
+ class ThirdOne < Reindeer
27
+ has :baz, is: :ro
28
+ has :quux, is: :rw
29
+ has :xuuq, is: :bare
30
+ end
31
+
32
+ obj = ThirdOne.new baz: 'Sawasdee!'
33
+ expect(obj.respond_to? :baz).to be_true
34
+ expect(obj.respond_to? :baz=).to be_false
35
+ expect(obj.respond_to? :quux).to be_true
36
+ expect(obj.respond_to? :quux=).to be_true
37
+ expect(obj.respond_to? :xuuq).to be_false
38
+ expect(obj.meta.has_attribute?(:xuuq)).to be_true
39
+ expect(obj.baz).to eq('Sawasdee!')
40
+ expect(obj.quux).to be_nil
41
+ obj.quux = 'super'
42
+ expect(obj.quux).to eq('super')
43
+
44
+ expect {
45
+ class FirstFail < Reindeer
46
+ has :epic, is: 'fail'
47
+ end
48
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
49
+ end
50
+
51
+ it 'should have required attributes' do
52
+ class SixthOne < Reindeer
53
+ has :foo, required: true
54
+ end
55
+ expect(SixthOne.new(foo: 'yep').foo).to eq('yep')
56
+ expect {
57
+ SixthOne.new
58
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
59
+ end
60
+
61
+ it 'should have default attribute values' do
62
+ class SeventhOne < Reindeer
63
+ # clone, clone, execute
64
+ has :ichi, default: 'one'
65
+ has :ni, default: %w[two three]
66
+ has :san, default: -> { [:four, :five] }
67
+ end
68
+
69
+ expect(SeventhOne.new.ichi).to eq('one')
70
+
71
+ a = SeventhOne.new.ni
72
+ expect(a).to eq(%w{two three})
73
+ a << 'four five'
74
+ expect(SeventhOne.new.ni).to eq(%w{two three})
75
+
76
+ b = SeventhOne.new.san
77
+ expect(b).to eq([:four, :five])
78
+ b << :six
79
+ expect(SeventhOne.new.san).to eq([:four, :five])
80
+ end
81
+
82
+ it 'should have lazily built attribute values' do
83
+ class EighthOne < Reindeer
84
+ private
85
+ def build_ha
86
+ 'mmm, lazy'
87
+ end
88
+ public
89
+ has :ha, lazy: true, builder: :build_ha
90
+ has :hok, lazy: true, default: -> { 'not eager' }
91
+ end
92
+
93
+ expect(EighthOne.new.ha).to eq('mmm, lazy')
94
+ expect(EighthOne.new.hok).to eq('not eager')
95
+
96
+ expect {
97
+ class SecondFail < Reindeer
98
+ has :blam, lazy: true, builder: :flub, default: 'blub'
99
+ end
100
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
101
+ expect {
102
+ class ThirdFail < Reindeer
103
+ has :blam, lazy: true
104
+ end
105
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
106
+ end
107
+
108
+ it 'should support the lazy_build shorthand' do
109
+ class NinthOne < Reindeer
110
+ has :jet, lazy_build: true
111
+ private
112
+ def build_jet
113
+ :symbolic
114
+ end
115
+ end
116
+
117
+ expect(NinthOne.new.jet).to eq(:symbolic)
118
+
119
+ obj = NinthOne.new
120
+
121
+ expect(obj.has_jet?).to be_false
122
+ expect(obj.jet).to eq(:symbolic)
123
+ expect(obj.has_jet?).to be_true
124
+ obj.clear_jet!
125
+ expect(obj.has_jet?).to be_false
126
+
127
+ expect {
128
+ class FourthFail < Reindeer
129
+ has :nope, lazy_build: true, default: -> { 'boom!' }
130
+ end
131
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
132
+ end
133
+
134
+ it 'should support delegation with handles' do
135
+ class TenthOne < Reindeer
136
+ has :bat, is: :bare, handles: [:sub]
137
+ end
138
+
139
+ obj = TenthOne.new(bat: 'zoo')
140
+ expect(obj.sub /z/, 'f').to eq('foo')
141
+ expect(obj.respond_to?(:bat)).to be_false
142
+
143
+ expect {
144
+ class FifthFail < Reindeer
145
+ has :bleh, handles: Object.new
146
+ end
147
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
148
+ end
149
+
150
+ it 'should have simple type constraints' do
151
+ class EleventhOne < Reindeer
152
+ has :gau, is_a: String
153
+ has :sip, is_a: Fixnum
154
+ end
155
+
156
+ obj = EleventhOne.new(gau: 'foo', sip: 123)
157
+ expect(obj.gau).to eq('foo')
158
+ expect(obj.sip).to eq(123)
159
+
160
+ expect {
161
+ EleventhOne.new(gau: [])
162
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
163
+ expect {
164
+ EleventhOne.new(sip: {})
165
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
166
+ end
167
+
168
+ it 'should should consistently apply type constraints' do
169
+ class TwelvethOne < Reindeer
170
+ has :une, is_a: Array, lazy_build: true
171
+ has :deux, is_a: Hash, is: :rw
172
+ private
173
+ def build_une
174
+ %w{cool beans}
175
+ end
176
+ end
177
+
178
+ obj = TwelvethOne.new
179
+ expect(obj.une).to eq(%w{cool beans})
180
+ obj.deux = { hashie: 'hash' }
181
+ expect(obj.deux).to eq({ hashie: 'hash' })
182
+
183
+ expect {
184
+ TwelvethOne.new.deux = []
185
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
186
+
187
+ expect {
188
+ class SixthFail < Reindeer
189
+ has :saywaht, is_a: Regexp, lazy: true, default: 'this here'
190
+ end
191
+ SixthFail.new.saywaht
192
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
193
+ end
194
+
195
+ it 'should compose attributes up the inheritance chain' do
196
+ class SeventeenthOne < Reindeer
197
+ has :foo
198
+ end
199
+ module FourthRole
200
+ include Reindeer::Role
201
+ has :bar
202
+ end
203
+ class EighteenthOne < SeventeenthOne
204
+ with FourthRole
205
+ has :baz
206
+ meta.compose!
207
+ end
208
+
209
+ obj = EighteenthOne.new(foo: 1, bar: 2, baz: 3)
210
+ expect(obj.foo).to eq(1)
211
+ expect(obj.bar).to eq(2)
212
+ expect(obj.baz).to eq(3)
213
+ end
214
+
215
+ it 'should raise an exception for lazy required attributes' do
216
+ expect {
217
+ class LazyRequiredFail < Reindeer
218
+ has :zoiks, lazy: true, required: true
219
+ end
220
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
221
+ end
222
+ end
@@ -0,0 +1,20 @@
1
+ require 'reindeer'
2
+
3
+ describe 'Reindeer construction' do
4
+ it 'should call all build methods' do
5
+ class FifteenthOne < Reindeer
6
+ def build(args)
7
+ things << :super
8
+ end
9
+ end
10
+ class SixteenthOne < FifteenthOne
11
+ has :things, default: []
12
+ def build(args)
13
+ things << :neat
14
+ end
15
+ end
16
+
17
+ obj = SixteenthOne.new
18
+ expect(obj.things).to eq([:super, :neat])
19
+ end
20
+ end
@@ -0,0 +1,79 @@
1
+ require 'reindeer'
2
+
3
+ describe 'Reindeer roles' do
4
+ it 'should compose a role' do
5
+ # TODO more tests smaller roles (to begin with)
6
+ module FirstRole
7
+ include Reindeer::Role
8
+ has :trois, default: 'cool'
9
+ requires :quatre
10
+ def cinq
11
+ %w{cool beans}
12
+ end
13
+ end
14
+ class ThirteenthOne < Reindeer
15
+ with FirstRole
16
+ def quatre
17
+ 'beans'
18
+ end
19
+ meta.compose!
20
+ end
21
+
22
+ obj = ThirteenthOne.new
23
+ expect(obj.trois).to eq('cool')
24
+ expect(obj.quatre).to eq('beans')
25
+ expect(obj.cinq).to eq(%w{cool beans})
26
+
27
+ expect {
28
+ module BankRole
29
+ include Reindeer::Role
30
+ requires :money
31
+ end
32
+ class SeventhFail < Reindeer
33
+ with BankRole
34
+ meta.compose!
35
+ end
36
+ }.to raise_error(Reindeer::Role::RoleError)
37
+ end
38
+
39
+ it 'should compose multiple roles' do
40
+ module SecondRole
41
+ include Reindeer::Role
42
+ has :foo
43
+ def bar; 'two'; end
44
+ end
45
+ module ThirdRole
46
+ include Reindeer::Role
47
+ has :baz
48
+ def quux; 'four'; end
49
+ end
50
+ class FourteenthOne < Reindeer
51
+ with SecondRole
52
+ with ThirdRole
53
+ meta.compose!
54
+ end
55
+
56
+ obj = FourteenthOne.new(foo: 'one', baz: 'three')
57
+ expect(obj.foo).to eq('one')
58
+ expect(obj.bar).to eq('two')
59
+ expect(obj.baz).to eq('three')
60
+ expect(obj.quux).to eq('four')
61
+ end
62
+
63
+ it 'should know if an object does a role' do
64
+ module DoesRole
65
+ include Reindeer::Role
66
+ end
67
+ class ThatDoesARole < Reindeer
68
+ with DoesRole
69
+ meta.compose!
70
+ end
71
+ class ThatDoesNoRole < Reindeer; end
72
+
73
+ expect(ThatDoesARole.does? DoesRole).to be_true
74
+ expect(ThatDoesARole.new.does? DoesRole).to be_true
75
+
76
+ expect(ThatDoesNoRole.does? DoesRole).to be_false
77
+ expect(ThatDoesNoRole.new.does? DoesRole).to be_false
78
+ end
79
+ end
@@ -0,0 +1,53 @@
1
+ require 'reindeer'
2
+
3
+ describe 'Reindeer types' do
4
+ it 'should constrain attributes' do
5
+ class QuietWord < Reindeer
6
+ with Reindeer::Role::TypeConstraint
7
+
8
+ def verify(val)
9
+ val.downcase == val
10
+ end
11
+
12
+ meta.compose!
13
+ end
14
+
15
+ class SoftlySpoken < Reindeer
16
+ has :start, is_a: String, type_of: QuietWord
17
+ end
18
+
19
+ obj = SoftlySpoken.new(start: 'foo')
20
+ expect(obj.start).to eq('foo')
21
+
22
+ expect {
23
+ obj = SoftlySpoken.new(start: 'LOUD NOISES')
24
+ }.to raise_error(Reindeer::TypeConstraint::Invalid)
25
+ end
26
+
27
+ it 'should have useful a error message' do
28
+ class LoudWord < Reindeer
29
+ with Reindeer::Role::TypeConstraint
30
+
31
+ def verify(val)
32
+ val.upcase == val
33
+ end
34
+
35
+ def error_message_for(val)
36
+ "THE VALUE '#{val}' WASN'T LOUD ENOUGH"
37
+ end
38
+
39
+ meta.compose!
40
+ end
41
+
42
+ class SergeantMajor < Reindeer
43
+ has :order, is_a: String, type_of: LoudWord
44
+ end
45
+
46
+ obj = SergeantMajor.new(order: 'TEN HUT')
47
+ expect(obj.order).to eq('TEN HUT')
48
+
49
+ expect {
50
+ SergeantMajor.new(order: 'quiet please')
51
+ }.to raise_error(Reindeer::TypeConstraint::Invalid, /WASN'T LOUD ENOUGH/)
52
+ end
53
+ end
@@ -0,0 +1,10 @@
1
+ require 'reindeer'
2
+
3
+ describe 'Reindeer' do
4
+ it 'should have a meta per subclass' do
5
+ class FourthOne < Reindeer; end
6
+ class FifthOne < Reindeer; end
7
+ expect(FourthOne.new.meta == FifthOne.new.meta).to be_false
8
+ expect(FourthOne.new.meta).to eql(FourthOne.meta)
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reindeer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dan Brook
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-31 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.9.2
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.9.2
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '2'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '2'
46
+ description: Adds featureful attributes, roles and type constraints
47
+ email: dan@broquaint.com
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - .rspec
53
+ - CHANGELOG.md
54
+ - Gemfile
55
+ - Gemfile.lock
56
+ - LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - lib/reindeer.rb
60
+ - lib/reindeer/meta.rb
61
+ - lib/reindeer/meta/attribute.rb
62
+ - lib/reindeer/role.rb
63
+ - lib/reindeer/role/typeconstraint.rb
64
+ - lib/reindeer/version.rb
65
+ - reindeer.gemspec
66
+ - spec/reindeer/attributes_spec.rb
67
+ - spec/reindeer/construction_spec.rb
68
+ - spec/reindeer/roles_spec.rb
69
+ - spec/reindeer/types_spec.rb
70
+ - spec/reindeer_spec.rb
71
+ homepage: http://github.com/broquaint/reindeer
72
+ licenses: []
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '1.9'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 1.8.24
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: Moose sugar in Ruby
95
+ test_files:
96
+ - spec/reindeer/attributes_spec.rb
97
+ - spec/reindeer/construction_spec.rb
98
+ - spec/reindeer/roles_spec.rb
99
+ - spec/reindeer/types_spec.rb
100
+ - spec/reindeer_spec.rb