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